Salta ai contenuti

Fase 1 — Build

La Fase 1 distribuisce una zona DNS primaria autorevole su F5 Distributed Cloud con Terraform, supportata da uno stato remoto in Azure Blob Storage. Questa pagina mostra ogni file del piano, spiega le variabili e i segreti richiesti e copre sia l’esecuzione locale che la pipeline GitHub Actions.

  • Una xcsh_dns_zone per il dominio delegato, nel namespace system, con un gruppo demo-records di record A (www, app, api).
  • Lo stato Terraform memorizzato in un container Azure Blob Storage, in modo che lo stesso stato sia condiviso tra il proprio computer e la CI.

Il piano si trova sotto terraform/: un modulo radice leggero che collega gli input a un modulo dns-zone.

Fissa Terraform e il provider. Il vincolo del provider è >= 3.62.0 — la release in cui le risorse DNS solo per il sistema impostano automaticamente il namespace come predefinito.

terraform {
required_version = ">= 1.5"
required_providers {
xcsh = {
source = "f5-sales-demo/xcsh"
# >= 3.62.0: the namespace attribute for system-only DNS resources defaults
# to "system" (spec-driven), so it can be omitted. Locally the provider is
# consumed via dev_overrides, which ignores this constraint.
version = ">= 3.62.0"
}
}
}

Il provider non accetta argomenti nel codice — si autentica dall’ambiente, quindi nessun segreto viene sottoposto a commit.

# The xcsh provider authenticates from the environment — no secrets in code.
# Export one of the following credential sets before running Terraform:
#
# Token auth: XCSH_API_URL + XCSH_API_TOKEN
# P12 auth: XCSH_API_URL + XCSH_P12_FILE + XCSH_P12_PASSWORD
# PEM auth: XCSH_API_URL + XCSH_CERT + XCSH_KEY
#
# See terraform/README.md for local dev setup (dev_overrides + env).
provider "xcsh" {}

Un backend azurerm parziale: nessun valore specifico dell’ambiente è hardcoded. Le coordinate vengono fornite al momento dell’init (da un file locale o dalle variabili GitHub Actions nella CI).

terraform {
# Azure Blob Storage remote state, configured as a PARTIAL backend:
# no environment-specific values are hardcoded here. Supply them at init time.
#
# CI: terraform init -backend-config="resource_group_name=$RG" ...
# (values from GitHub Actions repository variables)
# Local: terraform init -backend-config=backend.hcl (copy backend.hcl.example; gitignored)
#
# Auth is the storage account access key via the ARM_ACCESS_KEY environment
# variable (never committed). Keyless auth (use_oidc / use_azuread_auth) is not
# used: our Contributor-only RBAC cannot assign the "Storage Blob Data
# Contributor" role those methods require.
backend "azurerm" {}
}

domain è obbligatorio (fornito in fase di esecuzione, mai hardcoded); a_records mappa ogni nome di record ai suoi indirizzi IPv4. Non esiste una variabile namespace — il provider fissa gli oggetti DNS al namespace system dal vincolo della specifica API, quindi non viene mai configurato qui.

variable "domain" {
description = "DNS zone FQDN (required; supplied via TF_VAR_domain / a GitHub variable / tfvars)."
type = string
}
variable "labels" {
description = "Labels applied to managed DNS objects."
type = map(string)
default = {
managed_by = "terraform"
use_case = "dns"
}
}
variable "a_records" {
description = "A records: record name (\"\" = apex) to list of IPv4 addresses."
type = map(list(string))
default = {
www = ["203.0.113.10"]
app = ["203.0.113.20"]
api = ["203.0.113.30"]
}
}

Il modulo radice collega gli input a ./modules/dns-zone:

module "dns_zone" {
source = "./modules/dns-zone"
domain = var.domain
labels = var.labels
a_records = var.a_records
}

Il modulo crea la zona (il namespace viene omesso — il provider lo imposta su system come predefinito). Un blocco dynamic "rr_set" trasforma la mappa a_records in un set di record per ogni voce:

resource "xcsh_dns_zone" "this" {
name = var.domain
labels = var.labels
primary {
default_soa_parameters {}
rr_set_group {
metadata {
name = "demo-records"
}
dynamic "rr_set" {
for_each = var.a_records
content {
ttl = var.record_ttl
a_record {
name = rr_set.key
values = rr_set.value
}
}
}
}
}
}

Il file outputs.tf radice riesporta il nome della zona e l’identificatore F5 XC dal modulo.

Nulla di specifico per l’ambiente è incorporato nei file .tf. Tutto viene fornito in fase di esecuzione — dalle variabili e dai segreti di GitHub Actions nella CI, o da file locali e variabili d’ambiente.

ValoreScopoSorgente CISorgente locale
resource_group_name, storage_account_name, container_name, keyCoordinate del backend azurermVariabili repository TFSTATE_RESOURCE_GROUP, TFSTATE_STORAGE_ACCOUNT, TFSTATE_CONTAINER, TFSTATE_KEY (passate tramite -backend-config)backend.hcl (copiare backend.hcl.example; gitignored)
domainInput TerraformVariabile repository DNS_DOMAIN (come TF_VAR_domain)terraform.tfvars (copiare l’esempio) o TF_VAR_domain
ARM_ACCESS_KEYAuth backend azurerm (chiave dell’account di archiviazione)Segreto repositoryexport ARM_ACCESS_KEY=...
XCSH_API_URL, XCSH_API_TOKENAuth provider xcshSegreti repositoryexport XCSH_API_URL=... XCSH_API_TOKEN=...

I due file di esempio inclusi nel repository:

resource_group_name = "f5-sales-demo-tfstate"
storage_account_name = "f5salesdemotfstate"
container_name = "tfstate"
key = "dns.tfstate"

Il backend di archiviazione non può memorizzare il proprio bootstrap, quindi è necessario crearlo una volta, fuori banda, con una sessione Azure CLI autenticata. Il repository include scripts/bootstrap-azure-state.sh:

Terminal window
az group create --name f5-sales-demo-tfstate --location eastus2 \
--tags managed_by=terraform use_case=dns purpose=tfstate
az storage account create --name f5salesdemotfstate --resource-group f5-sales-demo-tfstate \
--location eastus2 --sku Standard_LRS --kind StorageV2 \
--min-tls-version TLS1_2 --allow-blob-public-access false
# State safety: keep prior versions and allow recovery of deleted state blobs.
az storage account blob-service-properties update \
--account-name f5salesdemotfstate --resource-group f5-sales-demo-tfstate \
--enable-versioning true \
--enable-delete-retention true --delete-retention-days 7 \
--enable-container-delete-retention true --container-delete-retention-days 7
KEY="$(az storage account keys list \
--account-name f5salesdemotfstate --resource-group f5-sales-demo-tfstate \
--query '[0].value' -o tsv)"
az storage container create --name tfstate \
--account-name f5salesdemotfstate --auth-mode key --account-key "$KEY"

Quindi esportare la chiave per le esecuzioni locali e impostarla (insieme alle credenziali del provider) come segreti GitHub:

Terminal window
export ARM_ACCESS_KEY="$KEY"
gh secret set ARM_ACCESS_KEY -R f5-sales-demo/dns
gh secret set XCSH_API_URL -R f5-sales-demo/dns
gh secret set XCSH_API_TOKEN -R f5-sales-demo/dns
  1. Configurare gli input. Copiare gli esempi e inserire i propri valori:

    Terminal window
    cp terraform/backend.hcl.example terraform/backend.hcl
    cp terraform/terraform.tfvars.example terraform/terraform.tfvars
  2. Esportare le credenziali:

    Terminal window
    export XCSH_API_URL="https://<tenant>.console.ves.volterra.io"
    export XCSH_API_TOKEN="<api-token>"
    export ARM_ACCESS_KEY="<storage-account-key>"
  3. Inizializzare il backend con la configurazione parziale:

    Terminal window
    cd terraform
    terraform init -backend-config=backend.hcl
  4. Verificare la formattazione e validare:

    Terminal window
    terraform fmt -check -recursive
    terraform validate
  5. Pianificare e applicare:

    Terminal window
    terraform plan
    terraform apply

Un’applicazione riuscita crea la zona e scrive lo stato nel container Azure. Continuare con Fase 2 — Validate per confermare che si risolve correttamente.

Il workflow .github/workflows/terraform.yml esegue un plan su ogni pull request e un apply al merge su main. Legge le coordinate del backend e gli input dalle variabili repository e le credenziali dai segreti, iniettando ciascuna solo nello step che ne ha bisogno, e serializza le esecuzioni su un gruppo di concorrenza in modo che due apply non si sovrappongano mai sul blob di stato condiviso.

name: Terraform
on:
pull_request:
branches: [main]
paths: ['terraform/**', '.github/workflows/terraform.yml']
push:
branches: [main]
paths: ['terraform/**']
permissions:
contents: read
concurrency:
group: terraform-state
cancel-in-progress: false
env:
TF_IN_AUTOMATION: 'true'
jobs:
terraform:
name: ${{ github.event_name == 'push' && 'apply' || 'plan' }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: terraform
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Setup Terraform
uses: hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e # v4.0.1
- name: Init
env:
ARM_ACCESS_KEY: ${{ secrets.ARM_ACCESS_KEY }}
RESOURCE_GROUP: ${{ vars.TFSTATE_RESOURCE_GROUP }}
STORAGE_ACCOUNT: ${{ vars.TFSTATE_STORAGE_ACCOUNT }}
CONTAINER: ${{ vars.TFSTATE_CONTAINER }}
STATE_KEY: ${{ vars.TFSTATE_KEY }}
run: |
terraform init -input=false \
-backend-config="resource_group_name=${RESOURCE_GROUP}" \
-backend-config="storage_account_name=${STORAGE_ACCOUNT}" \
-backend-config="container_name=${CONTAINER}" \
-backend-config="key=${STATE_KEY}"
- name: Format check
run: terraform fmt -check -recursive
- name: Validate
run: terraform validate -no-color
- name: Plan
if: github.event_name == 'pull_request'
env:
ARM_ACCESS_KEY: ${{ secrets.ARM_ACCESS_KEY }}
XCSH_API_URL: ${{ secrets.XCSH_API_URL }}
XCSH_API_TOKEN: ${{ secrets.XCSH_API_TOKEN }}
TF_VAR_domain: ${{ vars.DNS_DOMAIN }}
run: terraform plan -input=false -no-color
- name: Apply
if: github.event_name == 'push'
env:
ARM_ACCESS_KEY: ${{ secrets.ARM_ACCESS_KEY }}
XCSH_API_URL: ${{ secrets.XCSH_API_URL }}
XCSH_API_TOKEN: ${{ secrets.XCSH_API_TOKEN }}
TF_VAR_domain: ${{ vars.DNS_DOMAIN }}
run: terraform apply -input=false -auto-approve -no-color

Le azioni sono fissate a SHA di commit (con commenti di versione), e i segreti sono limitati agli step init, plan e apply anziché all’intero job.