Git, CI/CD & GitOpsXCII · Protected EnvironmentsAsCode
Environment policy as code — terraform-github-actions and terraform-gitlab-provider
What you'll learn
- Manage protected environments as Terraform resources through the github and gitlab providers
- Recognise the protected environment as configuration that must itself be version-controlled
- Detect drift between the declared policy and the live platform configuration
- Apply the production discipline of treating environment policy like infrastructure
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A protected environment is platform configuration. The configuration can be edited in the platform UI; it can also be edited through the GitHub and GitLab Terraform providers, which expose the environment as a resource declarable in HCL. Treating the environment as Terraform configuration means the environment’s rules - reviewer pool, wait timer, branch restriction - live in a version-controlled, reviewable, drift-detectable place. This lesson covers the provider resources, the repository-of-record pattern, and the production discipline of environment policy as code.
The protected environment as a Terraform resource
The Terraform GitHub provider exposes the environment
through the github_repository_environment resource. The
canonical declaration:
resource "github_repository_environment" "production" {
repository = "platform-iac"
environment = "production"
reviewers {
users = [1234567, 2345678]
teams = [4567890]
}
deployment_branch_policy {
protected_branches = true
custom_branch_policies = false
}
}
The reviewers block declares the reviewer pool: a list
of user IDs and team IDs. The deployment_branch_policy
block restricts the environment to protected branches
(main, release/*). The resource is declarative: the
desired state is in the file, the live state is in the
platform, and terraform apply reconciles the two.
The Terraform GitLab provider exposes the same concept
through gitlab_protected_environment:
resource "gitlab_protected_environment" "production" {
project = 12345
environment = "production"
deploy_access_levels = ["maintainer"]
}
The GitLab resource uses role-based access levels rather than named users. The principle is identical: declare the desired rules in HCL, apply through CI, drift-detect on a schedule.
The repository-of-record pattern
The protected environment’s HCL lives in a repository-of-record - a separate Terraform repository (or a dedicated module within an existing one) that owns the platform configuration. The repository has its own review process, its own CI, and its own apply pipeline.
flowchart LR
A["Engineer opens PR"] --> B["Repository-of-record CI"]
B --> C["terraform plan"]
C --> D{"Plan reviewed?"}
D -- "no" --> E["PR blocked"]
D -- "yes" --> F["Merge to main"]
F --> G["Apply pipeline runs"]
G --> H["Platform configuration updated"]
H --> I["Drift detection on schedule"]
I -. "drift" .-> J["Alert"]
The pattern is identical to any other infrastructure-as-code repository. The protected environment is configuration; the configuration is declared in HCL; the HCL is reviewed through a PR; the apply runs in CI. The protected environment’s rules are now governed by the same discipline as the infrastructure they gate.
Drift detection
Drift is the gap between declared state and live state. A
team that has a repository-of-record and applies on every
merge has little drift, but drift accumulates over time -
the platform UI may have been used, an emergency fix may
have bypassed the pipeline, or a third-party integration
may have edited the configuration. The remediation is a
scheduled terraform plan that runs against the live
state and compares it to the declared state.
terraform plan -detailed-exitcode -out=diff.tfplan
The -detailed-exitcode flag returns:
0if no changes1if an error occurred2if the plan has changes
A CI job that runs on a schedule and fails (exit code 2) when drift is detected is a CI job that catches the configuration that has drifted without the team’s knowledge. The alert is the trigger for the audit.
flowchart TD
A["Scheduled terraform plan"] --> B{"Drift detected?"}
B -- "no" --> C["No-op"]
B -- "yes" --> D["Alert to team channel"]
D --> E["Investigate source of drift"]
E --> F["Either commit or revert"]
The drift detection does not fix the drift; it surfaces it. The fix is a new PR that either commits the drifted configuration or reverts it. The discipline is to never silence the alert without one of those two outcomes.
What policy as code does not solve
Policy as code does not solve the human gate. The reviewer pool, the wait timer, the branch restriction - all are still human or platform-side rules. What policy as code solves is the configuration of those rules: who is on the reviewer pool, what the timer is set to, which branches are permitted. The rule behaviour is governed by the platform; the rule values are governed by the repository-of-record.
Production discipline
- Manage protected environments in Terraform, not the UI. The UI is for viewing; the repo is for editing.
- Disable UI editing of protected environments for everyone except the apply pipeline’s identity.
- Run drift detection on a schedule. Drift is inevitable; silent drift is the failure mode.
- Treat the policy repo like infrastructure. Pull requests, reviews, signed commits, branch protection.
Cross-course references
- This course, Part XXXIX-01 (Pipeline as code) covers the pattern the repository-of-record instantiates.
- Terraform for Production Sysadmins - Parts IX-XII cover Terraform state, which the environment policy shares.
- This course, Part XXXV-06 (Prevention by design) covers the prevention-discipline framing.
Quiz
Knowledge check · 4 questions
Q1. A team manages the production environment through the platform UI but version-controls everything else in Terraform. An emergency change is made in the UI; six months later the team runs a `terraform plan` and discovers drift. What should the team do?
Q2. Policy as code replaces the human gate: once the protected environment is in Terraform, no further human review is needed because the configuration is declarative.
Q3. Name the Terraform resources used to declare a protected environment on GitHub and GitLab.
Q4. Diagnose why a production environment's wait timer was silently reset to zero despite the team believing the configuration was managed in Terraform.
A team manages the production environment in a Terraform repository-of-record. The HCL declares a five-minute wait timer. The apply pipeline runs on every merge to main. Six months in, the wait timer is observed to be zero. The team runs `terraform plan` and the result is *no changes*. The Terraform state shows the timer is five minutes; the platform shows zero. A platform administrator - who has UI access for incident response - had reset the timer to zero during an incident and forgotten to restore it.
Passing score: 75%. Answers are checked in this browser.