Terraform reference
The Terraform root pins f5-sales-demo/xcsh 15.0.0 and immutable commits of the origin
server and traffic
generator. It uses the signed-in Azure
subscription, uses the selected deployer and environment, and stores local state under
$HOME/.local/state/statistics. The two default East US 2 VMs are Standard_D16s_v3 and
Standard_F16s_v2.
Prepare Terraform
Section titled “Prepare Terraform”Use Terraform 1.8 or later and the Azure CLI in your prepared Bash session. For a running deployment, use its existing local state; initializing a different state path can recreate resources.
-
Sign in to Azure and select the approved demo subscription:
Terminal window az loginaz account set --subscription '<XCSH_AZURE_SUBSCRIPTION_ID>' -
Bind Terraform to that subscription:
Terminal window export TF_VAR_subscription_id="<XCSH_AZURE_SUBSCRIPTION_ID>"export TF_CLI_CONFIG_FILE=/dev/null
Select the deployment inputs
Section titled “Select the deployment inputs”Use the form values with the native Terraform environment interface. Keep credentials in XCSH_API_TOKEN; tfvars must contain no credentials. For an existing deployment, load its private identity overrides before planning.
export TF_VAR_namespace="<XCSH_NAMESPACE>"export TF_VAR_domainname="<XCSH_DOMAINNAME>"export TF_VAR_lb_name="<XCSH_LB_NAME>"export TF_VAR_origin_pool_name="<XCSH_ORIGIN_POOL_NAME>"export TF_VAR_healthcheck_name="<XCSH_HC_NAME>"export TF_VAR_waf_name="<XCSH_WAF_NAME>"export TF_VAR_deployer="<XCSH_DEPLOYER>"export TF_VAR_environment="<XCSH_ENVIRONMENT>"export TF_VAR_owner="<XCSH_OWNER>"export TF_VAR_purpose="<XCSH_PURPOSE>"export TF_VAR_timer_name="<XCSH_TIMER_NAME>"export TF_VAR_location="<XCSH_AZURE_REGION>"export TF_VAR_ssh_public_key_path="<XCSH_SSH_PUBLIC_KEY_PATH>"A sanitized example tfvars file provides the same inputs. Copy the personalized block below to a private tfvars file; TF_VAR_subscription_id stays in the environment.
namespace = "<XCSH_NAMESPACE>"domainname = "<XCSH_DOMAINNAME>"lb_name = "<XCSH_LB_NAME>"origin_pool_name = "<XCSH_ORIGIN_POOL_NAME>"healthcheck_name = "<XCSH_HC_NAME>"waf_name = "<XCSH_WAF_NAME>"deployer = "<XCSH_DEPLOYER>"environment = "<XCSH_ENVIRONMENT>"owner = "<XCSH_OWNER>"purpose = "<XCSH_PURPOSE>"timer_name = "<XCSH_TIMER_NAME>"location = "<XCSH_AZURE_REGION>"ssh_public_key_path = "<XCSH_SSH_PUBLIC_KEY_PATH>"Review the plan
Section titled “Review the plan”For a first deployment, confirm that the hostname and named namespace objects are unowned. For this running demo, use the owner’s existing private state.
-
Create the private state directory.
Terminal window XCSH_STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/statistics"install -d -m 0700 "$XCSH_STATE_DIR" -
Initialize the local backend.
Terminal window terraform -chdir=terraform init -input=false \-backend-config="path=$XCSH_STATE_DIR/terraform.tfstate" -
Validate the configuration.
Terminal window terraform -chdir=terraform validate -
Save a plan outside Git.
Terminal window terraform -chdir=terraform plan -input=false \-out="$XCSH_STATE_DIR/statistics.tfplan" -
Inspect the planned resource actions.
Terminal window terraform -chdir=terraform show -json "$XCSH_STATE_DIR/statistics.tfplan" |jq -r '.resource_changes[] | [.address, (.change.actions | join(","))] | @tsv'
A first deployment plan should contain only creates for the dedicated VMs, networking, namespace, health check, origin pool, blocking WAF, HTTPS load balancer, and generator timer extension. Investigate any update, replacement, or deletion before applying. The origin module permits inbound ports 22, 80, 443, and 8888; the generator permits 22.
Check idempotence
Section titled “Check idempotence”For a running deployment, use the same pinned configuration, subscription, credentials, and existing private state. A fresh read-only plan checks whether Terraform proposes any change:
XCSH_PLAN_EXIT=0terraform -chdir=terraform plan -input=false -detailed-exitcode \ -out="$XCSH_STATE_DIR/statistics-idempotence.tfplan" || XCSH_PLAN_EXIT=$?case "$XCSH_PLAN_EXIT" in 0) printf 'No changes; the deployment is idempotent.\n' ;; 2) printf 'Changes proposed; investigate before any apply.\n' ;; 1) printf 'Terraform plan failed; resolve the error.\n' ;;esactest "$XCSH_PLAN_EXIT" -eq 0Exit 0 confirms no changes. Exit 2 requires investigation of the saved plan; exit 1 is an error. Retain plans and receipts outside Git. Running resource queries needs no infrastructure apply.
Apply the reviewed plan
Section titled “Apply the reviewed plan”Apply the saved plan:
terraform -chdir=terraform apply -input=false "$XCSH_STATE_DIR/statistics.tfplan"Follow Demo readiness after the origin and generator finish cloud-init. Keep the timer running so later API examples have fresh traffic.
Observed deployment output
Section titled “Observed deployment output”The first reviewed apply completed on 5 October 2026. The local receipt reported:
Apply complete! Resources: 22 added, 0 changed, 0 destroyed.
Outputs:
demo_hostname = "<XCSH_DOMAINNAME>"generator_vm_name = "<XCSH_GENERATOR_VM_NAME>"origin_vm_name = "<XCSH_ORIGIN_VM_NAME>"Teardown
Section titled “Teardown”Keep the current demo running. Execute these commands only when its owner decides to retire the infrastructure. Use the same credentials, subscription, backend, and state as deployment.
test -f "$XCSH_STATE_DIR/terraform.tfstate"terraform -chdir=terraform plan -destroy -input=false \ -out="$XCSH_STATE_DIR/statistics-destroy.tfplan"terraform -chdir=terraform show -json "$XCSH_STATE_DIR/statistics-destroy.tfplan" | jq -r '.resource_changes[] | [.address, (.change.actions | join(","))] | @tsv'Review every resource address and deletion before applying that exact saved plan.
terraform -chdir=terraform apply -input=false "$XCSH_STATE_DIR/statistics-destroy.tfplan"Verify that the owned namespace objects and demo DNS answer are absent. Retain the protected local state and ignored .env until the owner separately decides to remove them. State, saved plans, and cloud command output may contain credentials or infrastructure identities; keep them outside Git with owner-only permissions.