linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Введение
  • Уроки
  • How it works
  • База знаний
  • Шпаргалка
  • Capstone
  • Собеседование
home/terraform/lessons/tf-production-08-github-actions

lesson ── terraform-production ── ~18 мин ── 5 шагов

GitHub Actions, the full pipeline through act

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 минут, без регистрации.

запустить sandbox →

stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя

Шаги

  1. 01

    Write .github/workflows/terraform.yml

    bash
    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.

  2. 02

    act -l, what GitHub will see

    bash
    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.

  3. 03

    Run the lint job

    bash
    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.

  4. 04

    Run the whole pipeline

    bash
    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.

    The same thing on OpenTofu

    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.

    • → OpenTofu parity
  5. 05

    Add a policy gate to the pipeline

    Let's add a Checkov job. We drop in a baseline:

    bash
    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:

    bash
    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.

    act ≠ GitHub Actions 1:1

    What act does not do, or does differently:

    1. OIDC. Real GHA issues an OIDC token through actions/configure-aws-credentials. act does not; locally only access keys or mocks work.

    2. Permissions. permissions: id-token: write is ignored in act.

    3. Environments. The GitHub UI "environment: prod" with required reviewers is not modeled by act. The job just starts.

    4. Secrets. act takes GHA secrets from .secrets locally or from --secret-file. There is no protection like in real GitHub.

    5. 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.

    • → Plan-as-artifact theory
    • → Linters in the pipeline

Что ты узнал

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.

концепции

  • · act ≠ full GHA, some actions may behave differently
  • · ubuntu-latest does not carry terraform, every job needs hashicorp/setup-terraform
  • · in act, the artifact between jobs is stored in /tmp/artifact
  • · needs: between jobs, sequential ordering

← предыдущий

Declarative import: capturing an existing resource

следующий →

lifecycle: blocking, ignoring, and recreating

Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies