If you ever deployed a larger fleet of F5 BIG-IPs on appliances, you probably had an automation job get the tenant ready to even start initial provisioning.
Since F5OS 2.0, cloud-init is supported for BIG-IP TMOS tenants v21.1 and above. This changes the game. There is no need to start with default credentials and implement specific automation pipelines/jobs to prepare for further provisioning; it is now built in.
Cloud-init does not have to configure the whole BIG-IP tenant, but it is incredibly helpful for the first steps. Whether you only make the tenant ready for Declarative Onboarding (DO), include a DO declaration, build BIG-IP clusters automatically, add AS3 declarations, or write your own custom scripts: it’s a choice you now have.
What changed at first boot
F5OS 2.0+ can pass cloud-init user-data to a BIG-IP 21.1+ tenant when it is deployed. Cloud-init is a startup agent that reads user-data and applies early configuration. You probably know it well; it is a crucial part of most cloud deployments for Linux systems. For these tenants, F5 documents the NoCloud data source and supported modules, including bootcmd, runcmd, write_files, hostname, and users. BIG-IP also supports chpasswd for initial passwords and the F5-specific tmos_declared module for DO and, optionally, AS3. For cloud-init concepts and more module examples, see the BIG-IP VE cloud-init guide.
The useful difference is where provisioning starts. F5OS supplies the user-data at deployment; BIG-IP processes it on first boot. You can hand over to DO instead of waiting for a technician or an external job to perform the first login. Import a compatible BIG-IP tenant image and plan its resources and management connectivity before following along. The API example below uses an ALL-F5OS image with example resource sizes. Confirm CPU and memory requirements for your actual platform and modules.
Cloud-init objects, tenants and immutability
The deployment has two steps: create a named cloud-init object containing the user-data, then set the tenant’s cloud-init property to that object’s name. The F5OS RESTCONF field user-data/encrypted-data holds the cloud-config as a JSON string, not as nested JSON translated from YAML! F5OS stores the submitted user-data securely, but that does not make plaintext passwords safe to put in source control or share in requests.
This is not limited to the API. The CLI has cloud-inits cloud-init <name> config user-data to enter user-data and tenants tenant <name> config cloud-init <name> to attach it. The webUI exposes the setting under Tenant Management > Tenant Deployments > Additional Tenant Settings (cloud-init). The same object can bootstrap many tenants, which is helpful when the goal is minimal first provisioning for another automation to take over. Create one cloud-init object per tenant when tenants need individual settings. I lean towards the latter, also because I’d recommend unique credentials for each tenant.
Treat each object like an immutable artifact. F5OS does not permit editing or deleting it while a deployed tenant references it. In practice, treat it as nuke and pave: give a changed cloud-init a new name and use it for the next tenants.
In the examples, the GenCloudInitName helper derives a lowercase, alphanumeric name of at most 32 characters from the tenant name, using a short SHA-256 suffix to distinguish otherwise similar names. Cloud-init objects have pretty strict naming requirements ((a-z0-9)[1-32]).
Note that cloud-init is intended for first-boot provisioning (mostly); it is not meant for system or configuration management.
A cloud-config with built-in declarative onboarding
Here is the complete cloud-config used by the runnable httpYac examples. The IP addresses, hostname, artifact location, and initial passwords are lab values, so choose your own. Host the DO RPM somewhere the tenant can reach, such as where you already keep ISOs and other artifacts.
#cloud-config
chpasswd:
# Set initial credentials
# The plaintext admin credentials are NOT recommended but included for demonstrative purposes
list: |
admin:Admin_StartPW_356
root:$6$ozX2YB1ynyheTwQG$WqWrhqniKK6EAhJfapeUt/onM.UUT6cBdvKGr/tg2LrJMN/SM8jFljYiJdfPDRKq26cU9JERNaDU/ftXyq2GF.
# expire: true would force an immediate password change after logon, not great for automation
expire: false
bootcmd:
# https://clouddocs.f5.com/cloud/public/v1/shared/cloudinit.html#cloud-config-using-write-files-and-runcmd-modules-example
# Run only once: the tmsh add commands are unnecessary to repeat on each boot.
# Wait for MCPD before setting the host entry needed to fetch the DO RPM.
- cloud-init-per once bootstrap_mcpd_wait bash -ec 'source /usr/lib/bigstart/bigip-ready-functions; wait_bigip_ready_config; tmsh modify sys global-settings remote-host add { internal-artifact-server.s3.example.com { hostname internal-artifact-server.s3.example.com addr 192.168.245.9 } }'
tmos_declared:
enabled: true
icontrollx_trusted_sources: false
icontrollx_package_urls:
# retrieve from https://my.f5.com/manage/s/downloads?productFamily=DO&productLine=Declarative+Onboarding
# then store on an internal artifact server (e.g. S3 buckets or jump hosts)
- http://internal-artifact-server.s3.example.com/F5artifacts/f5-declarative-onboarding-1.49.0-14.noarch.rpm
do_declaration:
schemaVersion: 1.49.0
class: Device
async: true
label: "Simon's DO declaration to demo BIG-IP TMOS 21.1+ CloudInit on F5OS 2.0+"
Common:
class: Tenant
myFavoriteSystemSettings:
class: System
hostname: simons-bigip.example.com
guiSecurityBanner: true
guiSecurityBannerText: |
**************************** W A R N I N G *****************************
* Access to this system is restricted to authorized users only. *
**************************************************************************
myFavoriteSystemDbKeys:
class: DbVariables
ui.advisory.enabled: true
ui.advisory.color: orange
ui.advisory.text: This is an advisory text with an orange background.
# Do not run the setup wizard
setup.run: false
There are three jobs here (cloud-init modules, actually).
First, chpasswd changes the default admin and root passwords to the provided value, without requiring changing the password after first logon (expire: false). The root value is a SHA-512 crypt hash ($6$...), while the admin value is plaintext for demonstration purposes! Of course, generating hashes is the safe and recommended choice for production.
Even when using generated hashes, you might want to consider rotating the passwords set by cloud-init soon after in your automation pipeline, following your best practices for secrets handling.
Second, bootcmd runs early, before tmos_declared tries to download the DO package. This is why we use it even though the BIG-IP troubleshooting guidance suggests runcmd for commands with timing issues. We deliberately wait for MCPD (the control plane) to be ready, delaying other cloud-init modules, to make sure it can accept configuration. We do this by running BIG-IP’s readiness function wait_bigip_ready_config. bash is needed for source, and -e stops on failure. The cloud-init-per once wrapper ensures the operation is run exactly once on the first boot for initial provisioning. The host entry, added after MCPD is ready, is for this lab’s artifact server.
Third and finally, tmos_declared downloads and installs the DO RPM and applies a small device declaration: hostname, login banner, advisory, and a setup setting. You can put more device configuration in DO and add AS3 if you want. See TMOS Declared module example in the documentation for further details.
No plaintext passwords, use hashes instead
The root password, which is Initial!Root!123, was generated with the below handy shell function. Run it on a trusted workstation. OpenSSL prompts for the password and prints a salted SHA-512 crypt hash. Other tools, such as mkpasswd, do the same, of course.
gen_passwd() {
local salt
salt="$(openssl rand -base64 24 | tr -dc 'A-Za-z0-9./' | head -c 16)"
openssl passwd -6 -salt "${salt}"
}
gen_passwd
# Password:
# $6$<random-salt>$<hash>
The hash is neither the password nor reversible, but it still belongs in protected configuration. Give each tenant a distinct initial secret, preferably supplied through a controlled deployment process. Do not publish a hash, especially not alongside its plaintext value, as this lab example does.
Create and deploy through the F5OS RESTCONF API
The full markdown source file contains runnable httpYac http blocks with the helper function definitions and complete payloads.
What’s important to understand for the below examples is that the requests are pointed to the F5OS RESTCONF API endpoint, in our case /api on port :443, using the variable @host and with lab credentials. The magic placeholder ...F5OS_HTTP_headers contain the relevant headers including the basic auth header to talk to the F5OS RESTCONF API. This is automatically expanded by httpYac, a http file client for VS Code, which is a refreshing alternative to Postman & friends.
Another variable, the @tenant_name, is used to name the BIG-IP tenant on the F5OS level. This name is not automatically propagated into the tenant. As mentioned before, the cloud-init configuration object naming is pretty strict, this is why the GenCloudInitName() helper function deterministically generates a name based on the input variable @tenant_name.
The helper function toJsonString() performs JSON.stringify so the multiline YAML (the cloud-init cloud-config, stored in the variable cloudInitCfg) becomes one large JSON string in the encrypted-data field.
POST /data/f5-cloud-init:cloud-inits
...F5OS_HTTP_headers
{
"f5-cloud-init:cloud-init": [
{
"name": "{{GenCloudInitName(tenant_name)}}",
"config": {
"user-data": {
"encrypted-data": {{toJsonString(cloudInitCfg)}}
}
}
}
]
}
After creating the cloud-init artifact with the above POST request, check the actual object name by issuing a GET /data/f5-cloud-init:cloud-inits?fields=cloud-init/name.
Next, deploy a tenant referencing the cloud-init object. The below request also sets the image, management IP and gateway, CPU, memory, and storage:
POST /data/f5-tenants:tenants
...F5OS_HTTP_headers
{
"f5-tenants:tenant": [
{
"name": "{{tenant_name}}",
"config": {
"name": "{{tenant_name}}",
"type": "BIG-IP",
"nodes": [1],
"image": "BIGIP-21.1.0.2-0.0.22.ALL-F5OS.tar.bundle",
"cloud-init": "{{GenCloudInitName(tenant_name)}}",
"mgmt-ip": "192.168.245.102",
"prefix-length": "24",
"gateway": "192.168.245.254",
"vcpu-cores-per-node": "8",
"memory": 29184,
"storage": { "size": 90 },
"running-state": "deployed"
}
}
]
}
Quick walkthrough of the above settings:
- For rSeries,
nodesis[1]; this is different for VELOS, where multiple blades are available. - The other resource sizes are “lab-chosen”, i.e. they work in my lab. Choose appropriate settings for your environment.
running-state: deployedstarts the tenant immediately.
Wait for the tenant to boot and for DO to finish before treating the deployment as ready. A successful POST to the F5OS RESTCONF API does not prove that onboarding inside BIG-IP succeeded, it just says you were successful in submitting your desired tenant config to the F5OS platform layer. The whole onboarding only starts after the tenant OS boots up, cloud-init is part of the boot phase.
As an exercise, read back the reference from F5OS:
GET /data/f5-tenants:tenants/tenant={{tenant_name}}
?content=config&with-defaults=explicit
...F5OS_HTTP_headers
Confirm that the response contains the expected config.cloud-init name and running-state. If this was a disposable test, reverse the dependency order during cleanup: DELETE /data/f5-tenants:tenants/tenant={{tenant_name}} first; only then DELETE /data/f5-cloud-init:cloud-inits/cloud-init={{GenCloudInitName(tenant_name)}}. You cannot delete a cloud-init object still referenced by a deployed tenant.
Onboarding status on the Tenant API
Once the tenant’s management iControl REST API is reachable, check whether the DO endpoint responds using the temporary admin credentials. You will likely need to wait 5–7 minutes before you see a successful response with the DO details.
GET https://192.168.245.102/mgmt/shared/declarative-onboarding
Authorization: Basic admin:Admin_StartPW_356
That Authorization line is again httpYac client syntax, this time explicitly inline not as part of multiple headers. Check DO’s returned state rather than assuming a reachable endpoint means its asynchronous declaration has completed successfully. If the bootstrapping or declaration fails, use F5’s BIG-IP 21.1 cloud-init troubleshooting guide and the BIG-IP cloud-init troubleshooting reference for the in-tenant diagnostic steps. For DO check F5 Declarative Onboarding > Troubleshooting.
Very briefly for “on Tenant (BIG-IP)” troubleshooting:
# check cloud-init
cloud-init status
# logs
/var/log/cloud-init.log
/var/log/cloud-init-output.log
# check DO
curl -sku admin \
https://localhost/mgmt/shared/declarative-onboarding
# logs
/var/log/restnoded/restnoded.log
Next steps
The full cloud-config and API requests are in the GitHub repository. If you decide to try httpYac, the http blocks are immediately runnable in your VS Code.
If you followed along this far, let me know in the comments if you would like to see an example in Ansible.