Post

Y-IaC: CI/CD and Release Management

Y-IaC's pipelines: plan on PR, approved apply on main, plan-after-apply, weekly drift, and releasing the platform itself through refs consumers pin.

Y-IaC: CI/CD and Release Management

Y-IaC has two pipelines, and they’re easy to confuse. One is the pipeline every consumer runs: plan on a pull request, approved apply on main, drift on a schedule. The other is how Y-IaC itself is released, which, because consumers pull it by ref, is really a question of which ref each consumer points at. This post covers both, with an animated walk through the first.

Build automation

Nothing in Y-IaC is compiled. “Build” means preparing a runner and generating the stack:

  • set-build-variables reads the consumer’s config.yaml workflow block: shared repo, Y-IaC ref (iac_shared_module_ref, falling back to the TERRAGRUNT_SHARED_MODULE_VERSION input, then main) and runner.
  • The composite action .github/actions/setup-environment installs Terragrunt, TFLint, kubelogin and Wiz CLI in parallel, with a shared cache keyed on their versions.
  • The CLI generates terragrunt.stack.hcl, the units and the merged variables at run time.

For Y-IaC itself, code-quality-ci.yaml and python-quality.yaml are the build: lint, format and tests on every PR. docs-pages.yaml builds and publishes the module reference site.

Automated test execution

TriggerWhat runs
PR to Y-IaCPython quality (ruff, pytest), code quality, module validation for touched modules
Manual, before a releaseterragrunt-compatibilty.yaml: init and plan across consumer stacks against the branch
Consumer PRPlan for every changed stack, with gates
Consumer push to mainPlan, approved apply, plan-after-apply
Weekly schedule (Sunday 06:00 UTC)Plan of every stack, drift analysis

The plan-after-apply job is an underrated test. If a plan straight after a successful apply still shows changes, the module or provider doesn’t converge, and that’s a bug to fix before anyone else hits it.

Static analysis and security scanning

On the platform code: ruff for Python (with the security rules that make it flag subprocess calls, which the package answers with argv-form calls and explicit noqa notes), TFLint and terraform validate per module, terragrunt hcl format --check on unit templates, yamllint for YAML.

On every consumer plan: OPA policies, Trivy or Wiz on the redacted plan JSON (selected by iac_scanner: wiz, trivy, both or none), Trivy’s secret scan on the working directory, and Infracost when enabled. A TFLint hook also runs before every init and plan through root.hcl, and skips with a warning if TFLint isn’t installed.

Artifact and container management

Y-IaC produces no containers and no packages. The artifacts are:

ArtifactWhereKept
Plan files and plan JSON per unitWorkflow artifacts7 days (retention-days: 7)
Summary reportJob summary, PRWith the run
Wiz and Trivy reportsWorkflow artifacts / local report retention in configPer tool config
The platform itselfGit refs (branch or tag)Forever

Seven days is right for raw plan files, which can contain sensitive diffs. It’s too short if those plans are the audit evidence; the retention decision should export summaries elsewhere rather than keep plans longer.

Deployment automation

The consumer pipeline, generated from template/terragrunt-cicd.yaml by bootstrap. Step through a pull request to an approved apply, or tick the box to see a rejected approval:

  1. A pull request: The workflow runs on a pull request or push to main that touches environments/** or the workflow file itself.
  2. Detect changes: Changed files are mapped to a matrix of cloud, environment and stack. If no stack changed, nothing else runs.
  3. Build variables: The shared repo, the Y-IaC ref (iac_shared_module_ref) and the runner come from the workflow block in the consumer's config.yaml.
  4. Plan every changed stack: Each matrix entry calls terragrunt-ci from Y-IaC, which checks out the shared module and runs the CLI with plan, including the OPA, scanner and cost gates.
  5. Summary report: Plan results, gate outcomes and the add/change/destroy counts are rendered into one job summary for the reviewer.
  6. Review and merge: A reviewer reads the plan, not just the YAML diff, and merges to main.
  7. Waiting for approval: On push to main the plan runs again, then each apply job waits on its GitHub environment's protection rules. Jobs run one at a time (max-parallel 1).
  8. Apply (approve): An approver releases the job and terragrunt-cd runs the same CLI with apply for that stack.
  9. Plan after apply (approve): A fresh plan right after the apply should show no changes. Anything else means the module or provider did not converge.
  10. Apply summary and Jira (approve): An apply summary is generated and the GitHub to Jira webhook links the change to its ticket.
  11. Converged (approve): State matches the YAML on main. The weekly drift scan checks it stays that way.
  12. Not applied (reject): Nobody approves, or the approver rejects, so nothing touches the cloud. The plan artifacts stay on the run as a record.

The jobs behind it, all calling reusable workflows in Y-IaC:

JobReusable workflowRuns when
detect-changesdetect-changes.yamlPush or PR
manual-dispatchmanual-dispatch.yamlManual run (not a drift scan)
scheduled-drift-scaniac-scheduled-drift-scan.yamlSchedule or manual drift scan
terragrunt-planterragrunt-ci.yamlAny of the above found changes
generate-summary-reportterragrunt-summary-report.yamlAfter plan
terragrunt-applyterragrunt-cd.yamlPush to main (or manual apply on main), after plan and summary succeed
terragrunt-plan-after-applyterragrunt-ci.yamlAfter a successful apply
drift-detection-analyze, -notify, -jira-ticketiac-drift-detection-analyze.yaml, iac-teams-notification.yaml, iac-jira-drift-ticket.yamlScheduled runs with drift or failed stacks
invoke-jira-webhook-*github-jira-webhook.yamlPR opened; apply on main

Database deployment strategy

Databases are mostly consumer business, but two parts touch the platform. First, data-bearing resources (storage accounts, managed disks, FHIR services, Key Vaults) need the same care as schema changes: a plan that replaces one must be treated as a migration, not a routine apply. The OPA and plan summary gates should make any delete on these types impossible to miss.

Second, Y-IaC has an Atlas provider family (providers/atlas/, with atlas_schema and atlas_migration modules). That brings declarative database schema changes into the same YAML, plan and approval flow as infrastructure. I’d document when to use it and when a team’s own migration tooling is the better choice.

None of the consumers I could inspect declares Atlas resources, so I’d treat the Atlas modules as experimental until a consumer adopts them and documents the pattern.

Approval gates

Four gates stand between a YAML change and the cloud:

  1. PR review on the consumer repo (branch protection, CODEOWNERS).
  2. Plan gates: OPA, scanner and cost, each at advisory, soft-mandatory or mandatory.
  3. GitHub environment protection on the apply job: required reviewers per environment, configured in the consumer’s GitHub settings (for cloudutsukpilot, declared in infra-github).
  4. Serial applies (max-parallel: 1), so a reviewer approves one stack at a time and an early failure stops the rest from piling up.

For Y-IaC itself, the gate is the PR plus the compatibility run for shared-path changes.

Versioning and release notes

The release mechanism is the ref each consumer pins in config.yaml:

1
2
3
4
5
workflow:
  - iac_github_org: cloudutsukpilot
    iac_shared_repo: y-iac
    iac_runner: ubuntu-latest
    iac_shared_module_ref: v1.4.0     # a tag, not main

Today there’s one tag (v1.0.0) and consumers point at main or a feature branch. That means every merge is a release to every consumer on main. My recommendation:

  • Semantic versioning on tags. Breaking a module input, a YAML shape, a generated file or a gate default is a major version.
  • CHANGELOG sections per release, moving entries out of “Unreleased” and dropping the “Pending Verification” prefix only when verified.
  • Release notes that list affected modules and the consumer action needed, if any.
  • Production consumers pin tags; non-production can track main to catch problems early.

Rollback and roll-forward procedures

Two kinds of rollback, with very different costs:

Platform rollback is cheap: point the consumer’s iac_shared_module_ref back to the last good tag and re-run. The bootstrap hash changes, so the consumer re-syncs the old version on the next run.

Infrastructure rollback is not a Y-IaC button. Terraform has no “undo”; you roll forward by reverting the YAML change and applying again. For a change that destroyed or replaced a data-bearing resource, that’s a restore, covered in Data and Integration.

The rule that keeps rollbacks rare: never approve an apply whose plan you didn’t read, and never approve a replacement of a stateful resource without a restore path.

Release calendar

I’d release Y-IaC on a fixed cadence, every two weeks after the iteration, with exceptions only for security fixes:

WhenWhat
Iteration endTag a release candidate, run compatibility, trial on volunteer consumers
+1 weekTag the release, publish notes, announce in the platform channel
MonthlyProvider version review (providers.yaml) as its own release
Any timeSecurity fixes as patch releases

Avoid releasing on Fridays or before holidays; consumers on main would pick the change up on their next run with nobody around.

Checklist

  • Consumers’ iac_shared_module_ref values are inventoried
  • Semantic versioning and a tagging procedure are written down
  • Production consumers pin a tag
  • Plan-after-apply failures open an issue automatically
  • Compatibility runs are required for shared-path PRs
  • Artifact retention matches the audit decision (summaries exported if needed)
  • Atlas provider usage is documented or marked experimental
  • Release cadence is announced to consumers

This post is licensed under CC BY 4.0 by the author.