Load balancer as a service (service owner)¶
The canonical real-life NetOrca deployment, taken from F5 DevCentral's public
bigip-automation level 6
demo: a BIG-IP team offers LOAD_BALANCER as a self-service product. Consumer teams declare
the load balancers they need (from Git or the GUI), NetOrca turns declarations into change
instances, and the BIG-IP team's scheduled Ansible playbooks validate, deploy and report -
no tickets, no email, full audit trail.
This page is that demo's service-owner automation, rebuilt on the v2 modules. The original repositories (service definition, fulfilment automation) used the v1 collection; what changed is summarised at the end. The consumer half of the story is told in Consumer GitOps.
sequenceDiagram
participant C as Consumer (Git repo)
participant N as NetOrca
participant V as validate playbook (scheduled)
participant D as deploy playbook (scheduled)
participant F as BIG-IP
C->>N: declaration merged & submitted
N->>N: change instance created (PENDING)
V->>N: poll PENDING
V->>N: APPROVED (or REJECTED + reason)
D->>N: poll APPROVED
D->>F: render AS3 from declarations, deploy
D->>N: COMPLETED + deployed_item (else stays APPROVED, retried)
N-->>C: request shows COMPLETED, deployed state visible
The service¶
The BIG-IP team publishes LOAD_BALANCER as a JSON Schema (name, partition, type, virtual
server, members - with validation baked in). A consumer's declaration of one instance looks
like this:
application1:
services:
LOAD_BALANCER:
- name: load_balancer1
partition: prod
location: dmz
type: http
virtual_server:
ip: 10.1.10.152
port: 80
members:
- ip: 10.1.20.21
port: 30880
NetOrca compares each submission against the previous state and raises change instances:
CREATE for new entries, MODIFY for edited ones, DELETE for removed ones. Those change
instances are the service owner's work queue, and the whole operating model is two scheduled
playbooks against that queue.
Stage 1 - validate and approve¶
Runs frequently (every few minutes). Pulls PENDING changes and approves them - NetOrca has
already validated each declaration against the service schema, so this stage only applies the
team-specific checks a schema can't, rejecting an offender with the reason in the log so the
consumer sees it immediately instead of waiting for a human to notice.
- name: Validate and approve pending LOAD_BALANCER changes
hosts: localhost
gather_facts: false
tasks:
- name: Poll the validation queue
netautomate.netorca.netorca_change_instance_info:
service_name: [LOAD_BALANCER]
state: [PENDING]
exclude_referenced: true
register: pending
# =================================================================
# HERE IS YOUR CUSTOM VALIDATION
# NetOrca already validated the declaration against the LOAD_BALANCER
# JSON Schema, so partition, VIP shape, members etc. are guaranteed.
# Add only checks the schema can't express (estate conflicts, IPAM,
# quota): reject those with a REJECTED task before this one. With
# nothing extra to check, every PENDING change is approved.
# =================================================================
- name: Approve each pending change
netautomate.netorca.netorca_change_instance:
id: "{{ item.id }}"
state: APPROVED
log: "approved by automated validation"
loop: "{{ pending.change_instances }}"
loop_control:
label: "#{{ item.id }} ({{ item.change_type }})"
There is deliberately no field-level checking here. NetOrca validated the declaration against the
LOAD_BALANCER JSON Schema before it ever became a PENDING change, so re-checking partitions or
VIP ranges in Ansible would only duplicate the platform. The marked block is where your checks
go - the ones a schema can't express: conflicts with the existing estate, IPAM allocation, quotas
(declaration-content search makes conflict checks easy - see
Filtering). Reject those with
a REJECTED task and let the rest approve; the generic, service-agnostic walkthrough is
Writing your own validation, and a runnable version ships as
examples/scenarios/load_balancer/validate_and_approve.yml.
GUI approvals vs API approvals
Transitions performed through the API - what these playbooks do - are always available to the
service owner team's key. The service's allow_manual_approval / allow_manual_completion
flags govern only whether humans may approve/complete from the GUI. A 403 here means the
key's team or context is wrong, not a missing flag.
Stage 2 - deploy and complete¶
Runs on its own schedule. This is the declarative pattern from the original demo: rather than
applying changes one by one, render the entire desired state of the BIG-IP tenant from the
in-service declarations and push it as one AS3
call - then mark every approved change COMPLETED. Deletes need no special handling: a
decommissioned item is no longer IN_SERVICE, so it simply isn't in the next render.
- name: Deploy approved LOAD_BALANCER changes to BIG-IP
hosts: localhost
gather_facts: false
vars:
tenant: level6_netorca_tenant
tasks:
- name: Approved changes waiting for deployment
netautomate.netorca.netorca_change_instance_info:
service_name: [LOAD_BALANCER]
state: [APPROVED]
exclude_referenced: true
register: work
- name: Nothing approved - end the run
ansible.builtin.meta: end_play
when: work.count == 0
- name: The desired state - every in-service declaration
netautomate.netorca.netorca_service_item_info:
service_name: [LOAD_BALANCER]
runtime_state: [IN_SERVICE]
register: estate
- name: Render the whole tenant from the in-service declarations
ansible.builtin.set_fact:
tenant_body: "{{ lookup('template', 'templates/as3_tenant.j2') }}"
vars:
desired_items: "{{ estate.service_items }}"
# -----------------------------------------------------------------
# HERE IS YOUR CUSTOM DEPLOYMENT - swap for your platform
# If this push fails the run fails and nothing below runs, so the
# batch stays APPROVED and the next run retries it.
# -----------------------------------------------------------------
- name: Push the rendered tenant to BIG-IP
ansible.builtin.uri:
url: "{{ lookup('env', 'BIGIP_URL') }}/mgmt/shared/appsvcs/declare"
method: POST
url_username: "{{ lookup('env', 'BIGIP_USER') }}"
url_password: "{{ lookup('env', 'BIGIP_PASSWORD') }}"
force_basic_auth: true
body: "{{ tenant_body }}"
body_format: json
status_code: [200, 202]
timeout: 120
# --- END OF YOUR CUSTOM DEPLOYMENT ---
- name: Mark every approved change COMPLETED
netautomate.netorca.netorca_change_instance:
id: "{{ item.id }}"
state: COMPLETED
log: "deployed by {{ lookup('env', 'CI_JOB_URL') | default('scheduled ansible run', true) }}"
loop: "{{ work.change_instances }}"
loop_control:
label: "#{{ item.id }} ({{ item.change_type }})"
And the AS3 template it renders (templates/as3_tenant.j2, trimmed - full original in the
demo repo):
{
"class": "AS3",
"action": "deploy",
"persist": true,
"declaration": {
"class": "ADC",
"schemaVersion": "3.49.0",
"{{ tenant }}": {
"class": "Tenant",
{% for item in desired_items %}
"{{ item.declaration.name }}": {
"class": "Application",
"serviceMain": {
"class": "Service_HTTP",
"virtualAddresses": ["{{ item.declaration.virtual_server.ip }}"],
"pool": "{{ item.declaration.name }}_pool"
},
"{{ item.declaration.name }}_pool": {
"class": "Pool",
"monitors": ["http"],
"members": [{
"servicePort": 80,
"serverAddresses": {{ item.declaration.members | map(attribute='ip') | list | to_json }}
}]
}
}{{ "," if not loop.last }}
{% endfor %}
}
}
}
Two behaviours worth noticing:
- Success is reported; failure just isn't. A deployed change ends at
COMPLETEDwith adeployed_itemthe consumer can see. A failed push fails the run and completes nothing, so the batch staysAPPROVEDand the next scheduled run retries it - the queue is the state, so the loop is safely re-runnable. We never transition a change toERROR. - Deletes need no special handling. Completing a
DELETEchange decommissions its service item, so it is no longerIN_SERVICEand simply drops out of the next render - AS3 reconciles it away.
If your infrastructure is not declarative (per-object APIs, no full-state render), use the
imperative variant instead: get the APPROVED changes of each type with the change_type
filter and run your action on them - CREATE/MODIFY apply the new declaration, DELETE tears down
the old one - then mark each COMPLETED. That is exactly what
examples/scenarios/load_balancer/deploy_by_change_type.yml does.
Scheduling it¶
Both playbooks are pollers - run them on any scheduler. The original demo used AWX job templates; a GitLab scheduled pipeline works just as well:
# .gitlab-ci.yml of the team's automation repo
validate:
script: ansible-playbook validate_change_instances.yml
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
deploy:
script: ansible-playbook deploy_change_instances.yml
rules:
- if: '$CI_PIPELINE_SOURCE == "schedule"'
with NETORCA_API_URL / NETORCA_API_KEY (and the BIG-IP credentials) as masked CI/CD
variables, and a schedule of every 5-15 minutes. For webhook-triggered instead of polled
fulfilment, the webhook modules arrive later in the v2 series.
Secrets, done right¶
The v1 demo playbook carried its API key inline with a comment saying "in live environment this
should be a secret". Make that structural instead of aspirational: the v2 modules read
NETORCA_API_URL / NETORCA_API_KEY / NETORCA_CONTEXT from the environment, so playbooks
contain no credential material at all - locally the key lives in a git-ignored .env, in CI
it is a masked variable, in AWX a custom credential type
(Authentication has all three patterns). api_key is
no_log, so it never appears in task output either.
What changed from the v1 playbooks¶
If you operated the level-6 demo (or anything like it) on collection 1.x, this is what the same workflow gains on 2.x - see the migration guide for mechanics:
| v1 behaviour | v2 behaviour |
|---|---|
filters: dict, typos silently ignored |
explicit validated parameters; a bad value fails with the server's message |
registered.change_instances.results nesting |
flat work.change_instances list + work.count |
| first API page only - a busy queue was silently truncated | auto-pagination; limit when you want less |
every polled change marked COMPLETED, even when the deploy failed |
only real successes are COMPLETED; a failed deploy leaves the change APPROVED for the next run - never a false success |
| no deployed-item reporting | deployed_item on completion; consumers see what serves their request |
| API key hardcoded in the playbook | environment/CI-variable auth, no_log |
--check performed live writes |
real check mode everywhere |
| illegal transitions surfaced as raw API errors | validated client-side, with the legal targets listed |
Try it¶
- Runnable, service-agnostic versions ship in this repo: validation
(
validate_and_approve.yml) and both deployment styles - declarative (render_and_deploy_as3.yml) and imperative (deploy_by_change_type.yml). - The full original demo (consumer repos, service definition, GUI walkthroughs, GIFs) is at f5devcentral/bigip-automation, level 6.
- The audit side of this scenario - who approved what, when - is
audit_change_history.yml.