lesson ── terraform-production ── ~18 мин ── 5 шагов
We put it all into one pipeline. .github/workflows/terraform.yml with three
jobs: lint (fmt+validate+tflint+checkov), plan (with artifact upload), apply
(with download + apply). Locally we run it through act, which emulates
GitHub Actions without going out to github.com. We do not touch real AWS,
the workflow reads the provider env variables that point at LocalStack.
интерактивный sandbox
Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
cd /home/student/tf-gha
cat > main.tf <<'EOF'
resource "aws_s3_bucket" "gha_demo" {bucket = "linuxlab-gha-demo"
tags = {ManagedBy = "terraform"
Environment = "dev"
}
}
EOF
cat > .github/workflows/terraform.yml <<'EOF'
name: Terraform CI
on:
push:
branches: [main]
pull_request:
env:
TF_IN_AUTOMATION: "true"
TF_INPUT: "false"
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: test
AWS_DEFAULT_REGION: us-east-1
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.8
- name: terraform fmt -check
run: terraform fmt -check -recursive
- name: terraform validate
run: |
terraform init -backend=false -no-color
terraform validate -no-color
plan:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.8
- name: terraform plan
run: |
terraform init -no-color
terraform plan -no-color -out=plan.tfplan
- name: show plan
run: terraform show -no-color plan.tfplan > plan.txt
- uses: actions/upload-artifact@v4
with:
name: tf-plan
path: |
plan.tfplan
plan.txt
apply:
needs: plan
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.8
- uses: actions/download-artifact@v4
with:
name: tf-plan
- name: terraform apply
run: |
terraform init -no-color
terraform apply -no-color plan.tfplan
EOF
The structure, three jobs, sequential through needs:. apply runs only
on main (through if).
✓ The workflow is ready. Let's run act.
act -l -W .github/workflows/terraform.yml
It should show three jobs: lint, plan, apply, with their dependencies.
act parses the workflow, it understands needs: and the conditional
if:. For our sandbox refs/heads/main works by default, but it is simpler
to run a specific job.
✓ act parses the workflow correctly.
act -j lint \
-W .github/workflows/terraform.yml \
--container-architecture linux/amd64 \
--bind 2>&1 | tail -20
What the flags do:
-j lint, a single job only.--container-architecture linux/amd64, needed on arm64 hosts.--bind, mounts the current directory instead of cloning (faster for
the local dev loop).On the first run act downloads the Ubuntu runner image, which can take a minute. After that it is fast.
It should show Success - lint (or the detailed fmt and validate steps).
✓ The lint job ran inside the emulated GHA.
act push \
-W .github/workflows/terraform.yml \
--container-architecture linux/amd64 \
--bind 2>&1 | tail -30
act push, the push event. By default act runs everything triggered by
this event. The jobs run sequentially because of needs:.
act stores artifacts in /tmp/artifact inside the runner container or in
.artifacts/ locally (depends on the version). If the pipeline succeeds,
all three jobs are green.
✓ The full pipeline ran. lint → plan → apply.
OpenTofu keeps the CLI and state compatible with Terraform for the
commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. On a first switch, though, back up
the state and do a run on a feature branch, the differences cluster in
the newer features (variables in backend, state encryption, OCI
registry-backed modules). See tf-opentofu-parity for the full matrix.
Let's add a Checkov job. We drop in a baseline:
cat > policies/check.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
checkov -d . --quiet --no-guide --soft-fail-on CKV_AWS_18
echo "Checkov gate passed."
EOF
chmod +x policies/check.sh
mkdir -p policies
Update the workflow, add the policy job after lint and before plan:
python3 - <<'EOF'
with open('.github/workflows/terraform.yml') as f:wf = f.read()
injection = """
policy:
needs: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: checkov
run: checkov -d . --quiet --no-guide --soft-fail-on CKV_AWS_18
"""
wf = wf.replace(" plan:\n needs: lint",injection + "\n plan:\n needs: [lint, policy]")
with open('.github/workflows/terraform.yml', 'w') as f:f.write(wf)
EOF
grep -A2 "needs:" .github/workflows/terraform.yml
plan now requires both lint and policy. If the bucket in HCL is weak,
Checkov catches it and plan does not start.
✓ The policy gate is in the pipeline. The pipeline is now production-grade.
What act does not do, or does differently:
OIDC. Real GHA issues an OIDC token through
actions/configure-aws-credentials. act does not; locally only
access keys or mocks work.
Permissions. permissions: id-token: write is ignored in act.
Environments. The GitHub UI "environment: prod" with required reviewers is not modeled by act. The job just starts.
Secrets. act takes GHA secrets from .secrets locally or from
--secret-file. There is no protection like in real GitHub.
Cache. actions/cache works in act, but through a local path.
A difference in content is sometimes visible on the first run.
When act is worth it: checking that "the workflow runs at all". When it is not: production debugging of security and OIDC specifics, which you need to check in real GitHub on a feature branch.
A workflow, YAML under .github/workflows/. Jobs: lint → plan (uploads
artifact) → apply (downloads artifact). act runs the workflow locally
in a Docker container, using an Ubuntu image close to the GHA runner.
команды
act --container-architecture linux/amd64 -W .github/workflows/terraform.ymlrun the workflow.act -llist the jobs in a workflow.act -j lintrun a single job.act -ndry-run, see what would run, without execution.концепции