Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Terraform CI/CD with GitHub Actions

By Kokil Thapa | Last reviewed: August 2026

Managing infrastructure as code manually is a liability for any production system. Implementing Terraform CI/CD with GitHub Actions automates the validation, planning, and application of your infrastructure changes while enforcing security boundaries that manual CLI workflows cannot match. For teams managing cloud resources for applications like those described in my CI/CD pipeline setup services, this automation eliminates drift and ensures every change is auditable.

How do you configure Terraform CI/CD with GitHub Actions securely?

Security in infrastructure automation is not optional; it is the primary constraint. A common mistake I see in production audits is storing long-lived AWS or Azure credentials as repository secrets. In 2026, the standard for Terraform CI/CD with GitHub Actions is OpenID Connect (OIDC). This allows GitHub Actions to assume an IAM role temporarily without ever storing static access keys in your repository settings.

GitHub ActionsRunner EnvironmentRequest TokenOIDC Providertoken.actions.githubusercontent.comSign JWTCloud ProviderAWS / Azure / GCPAssume Role1. Auth2. VerifyNo Long-Lived Credentials Stored
Secure OIDC authentication flow for Terraform CI/CD with GitHub Actions eliminating static cloud credentials

To implement this, you must configure both your cloud provider and your GitHub workflow. On the AWS side, create an Identity Provider for token.actions.githubusercontent.com and a Role with a trust policy restricting access to your specific repository and environment. In your workflow, use the official aws-actions/configure-aws-credentials action with the role-to-assume parameter instead of access keys.

- name: Configure AWS Credentials via OIDC
  uses: aws-actions/configure-aws-credentials@v4
  with:
    role-to-assume: arn:aws:iam::123456789012:role/TerraformGithubActionsRole
    aws-region: ap-south-1
    # No aws-access-key-id or aws-secret-access-key needed

Beyond authentication, secure your state file. Never store terraform.tfstate in Git. Use S3 with DynamoDB locking, Azure Blob Storage with lease locking, or Terraform Cloud. State files contain sensitive resource attributes and IDs; treating them as public artifacts exposes your entire infrastructure topology. For projects requiring strict compliance, enable server-side encryption on the state backend and restrict bucket access to the OIDC role only.

What is the optimal workflow structure for Terraform automation?

A reliable Terraform CI/CD with GitHub Actions pipeline separates validation from execution. The "Plan" phase should run on every pull request, providing immediate feedback without risk. The "Apply" phase should only trigger on merge to main, gated by environment protection rules. This separation prevents accidental infrastructure modifications during code review.

  1. Format Check: Run terraform fmt -check -recursive first. It fails fast and costs nothing. Consistent formatting reduces diff noise in reviews.
  2. Validation: Run terraform validate to catch syntax errors and invalid module references before attempting any API calls.
  3. Security Scan: Integrate tools like checkov or tfsec here. Catching misconfigured S3 buckets or open security groups in CI is significantly cheaper than remediating them post-deployment.
  4. Plan Generation: Execute terraform plan -out=tfplan. Save the binary plan file as a workflow artifact. This ensures the exact changes reviewed are what gets applied later.
  5. Plan Comment: Use an action to post the plan output directly to the PR. Reviewers should never have to leave GitHub to understand infrastructure impact.
  6. Protected Apply: On merge, download the saved tfplan artifact and run terraform apply tfplan. Never regenerate the plan during apply; drift may have occurred between review and merge.

This sequence enforces immutability. The plan generated during review is cryptographically bound to the apply step. If someone pushes new commits after approval but before merge, the branch protection rules should require re-review, invalidating the stale plan. For teams managing multiple environments, consider reading about DevOps automation best practices to align this workflow with broader operational standards.

How do you manage multi-environment Terraform deployments in GitHub Actions?

Most real-world systems require separate staging and production environments. Managing these within a single repository demands isolation. Two patterns dominate in 2026: directory-based separation and workspace-based separation. Directory-based is generally superior for distinct environments because it allows independent state files, backend configurations, and variable sets.

CriteriaDirectory-Based (/env/prod)Workspace-Based (terraform workspace)
State IsolationComplete (separate backends)Shared backend, prefixed keys
Configuration DivergenceEasy (different .tfvars per env)Difficult (requires conditionals)
Permission GranularityHigh (separate OIDC roles)Low (same role, same state)
ComplexityModerate (more directories)Low initially, high at scale
Recommended ForProduction systems, complianceEphemeral dev environments
Staging Environmentinfra/staging/main.tfstaging.tfvarsAuto-Deploy on MergeS3 State: staging-tfstateProduction Environmentinfra/prod/main.tfprod.tfvarsManual Approval GateS3 State: prod-tfstateShared Modulesmodules/vpc/main.tfmodules/rds/main.tfmodules/laravel-app/main.tf
Directory-based multi-environment architecture for Terraform CI/CD with GitHub Actions showing isolated state and approval gates

Use GitHub Environments to enforce approval gates. Configure a "production" environment in your repository settings with required reviewers. In your workflow, reference this environment in the apply job. GitHub will pause execution until an authorised approver confirms. This is critical for legal-tech platforms or financial systems where unauthorised infrastructure changes carry regulatory risk. I have implemented this pattern for several Nepal-based service portals where audit trails are mandatory.

apply-prod:
  needs: plan
  runs-on: ubuntu-latest
  environment: production  # Triggers approval gate
  permissions:
    id-token: write
    contents: read
  steps:
    - uses: actions/download-artifact@v4
      with:
        name: tfplan-prod
    - name: Terraform Apply
      run: terraform apply -auto-approve tfplan

Variable management should also be environment-scoped. Store sensitive values like database passwords or API keys as GitHub Environment Secrets, not repository secrets. Non-sensitive configuration (instance sizes, CIDR blocks) belongs in .tfvars files committed to the repository. This keeps configuration visible and reviewable while protecting credentials.

How do you handle Terraform state locking and concurrency in CI?

Concurrent Terraform operations against the same state file cause corruption. State locking is non-negotiable. When using S3+DynamoDB, Azure Blob, or GCS backends, Terraform acquires a lock automatically. However, GitHub Actions introduces unique concurrency challenges that backend locking alone does not solve.

Add workflow-level concurrency controls to prevent overlapping runs. Without this, two PRs merged simultaneously could queue up applies that technically respect the state lock but waste time waiting or fail due to stale plans. GitHub's native concurrency key handles this elegantly:

concurrency:
  group: terraform-${{ github.ref }}
  cancel-in-progress: false  # Queue, don't cancel active applies

Set cancel-in-progress: false for apply jobs. Cancelling a running apply mid-execution can leave infrastructure in a partially provisioned state. For plan jobs, true is acceptable since plans are read-only. This distinction matters significantly when managing high-traffic e-commerce infrastructure where partial deployments cause outages.

Monitor lock contention. If your team frequently encounters "Error acquiring state lock" messages, investigate whether workflows are properly cleaning up after failures. Stale locks from crashed runners require manual intervention (terraform force-unlock LOCK_ID). Implement a scheduled workflow to detect and alert on locks older than 30 minutes. For teams scaling their infrastructure automation, understanding these operational nuances is as important as the code itself; see my notes on full-stack development practices that include infrastructure ownership.

How do you optimise Terraform CI/CD performance and cost?

Terraform workflows can become slow and expensive as infrastructure grows. Optimisation focuses on three areas: runner efficiency, plan scope, and module caching.

  • Cache Provider Plugins: Terraform downloads providers on every run by default. Use actions/cache to persist the .terraform/providers directory between runs. This saves 30-60 seconds per job and reduces GitHub Actions billing.
  • Targeted Plans: For monorepos with multiple root modules, only plan changed directories. Use dorny/paths-filter to detect which paths were modified. Running a full infrastructure plan for a documentation change wastes resources.
  • Parallelism Tuning: Default parallelism is 10. For large infrastructures hitting API rate limits, reduce it with -parallelism=5. For small, fast APIs, increase it cautiously. Monitor cloud provider throttling metrics.
  • Artifact Retention: Plan files can be large. Set artifact retention to 1-3 days. You only need them until the next apply succeeds. Storing them indefinitely inflates storage costs.
Detect Changespaths-filterSkip if no TFMatrix StrategyValidate & PlanRestore Cacheterraform initfmt + validateplan -out=tfplanReview GateUpload ArtifactPR Comment BotEnv ApprovalApply & NotifyDownload Planterraform applyUpdate CacheSlack / Teams
Performance-optimised Terraform CI/CD with GitHub Actions pipeline featuring caching, conditional execution, and notification stages

Consider reusable workflows for multi-repository organisations. Define your Terraform pipeline once in a central repository and call it from application repos. This ensures consistent security policies, tool versions, and optimisation strategies across all projects. Maintenance becomes centralised rather than duplicated across dozens of .github/workflows files.

Implementing Reliable Terraform CI/CD with GitHub Actions

Building production-grade Terraform CI/CD with GitHub Actions requires disciplined attention to security, state management, and workflow design. Start with OIDC authentication and remote state backends as non-negotiable foundations. Separate plan and apply phases with environment protection rules. Optimise for developer feedback speed through caching and targeted execution. These patterns have proven reliable across diverse production environments, from legal-tech compliance systems to high-traffic e-commerce platforms.

If your team needs assistance implementing secure infrastructure automation or integrating Terraform with existing Laravel or WordPress deployment pipelines, contact me to discuss your specific requirements. I help organisations in Nepal and globally establish robust IaC practices that scale safely.

Frequently Asked Questions

It is an automated workflow where GitHub Actions runners execute terraform plan and apply commands upon code commits, ensuring infrastructure changes are versioned, reviewed via pull requests, and deployed consistently without manual CLI intervention.

GitHub Actions provides 2,000 free minutes monthly for private repos; typical Terraform runs consume negligible time, so most small-to-medium projects stay within the free tier, costing NPR 0 (~USD 0) unless exceeding limits or using larger runners.

Choose GitHub Actions when your repository already lives on GitHub to avoid cross-platform sync complexity; choose GitLab CI if you need native container registry integration or self-hosted runners on private Nepal-based infrastructure for compliance.

Never hardcode secrets in workflow files. Store AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, or TF_VAR_* values as GitHub Encrypted Secrets under repository settings. Access them via ${{ secrets.SECRET_NAME }} syntax. For production, prefer OIDC federation with AWS IAM or Azure AD to eliminate long-lived static credentials entirely, reducing breach risk if a secret leaks.

Split into two distinct jobs: validate-and-plan triggered on pull requests, and apply triggered only on merge to main. The plan job outputs a binary plan file saved as an artifact. The apply job downloads that exact artifact rather than re-running terraform plan. This guarantees what was reviewed in the PR comment is exactly what gets applied, preventing drift between review and execution.

Use remote backends like S3+DynamoDB, GCS, or Terraform Cloud which provide native locking. Local state files cannot safely handle concurrent GitHub Actions runners. If two PRs trigger simultaneously against local state, corruption occurs. Configure backend blocks in your versions.tf before first init. For Nepal projects on budget, S3+DynamoDB costs roughly NPR 150/month (~USD 1.10) and prevents catastrophic state conflicts during team collaboration.

Version mismatches cause this. Pin exact terraform and provider versions in required_version and required_providers blocks. Use hashicorp/setup-terraform action with specific version input rather than latest. Also ensure identical environment variables, workspace selection, and backend configuration between local and CI. I have debugged many deployments where developers ran terraform 1.9 locally but CI pulled 1.10, causing subtle provider behavior differences that broke applies.

Use terraform show -no-color -json plan.out to generate machine-readable output, then pipe through a formatting tool or custom script. Post via actions/github-script or third-party actions like tfsec-pr-commenter. Include the full plan summary, resource count, and warning indicators. This lets reviewers approve infrastructure changes without checking out code locally. On legal-tech portals I have built, this review step caught accidental security group deletions before they reached production.

Yes, and you must. Pass variable values as environment variables mapped from GitHub Secrets using TF_VAR_ prefix convention. Alternatively, generate tfvars dynamically during workflow from secrets. Never commit .tfvars containing passwords, API keys, or PII. For multi-environment setups, maintain separate secret sets per environment and select via workflow dispatch inputs or branch naming conventions. This pattern keeps repositories clean while supporting dev, staging, and production configurations securely.

Use directory-based separation like envs/dev/, envs/staging/, envs/prod/ with shared modules/. Create matrix strategies in workflows to run plans across all environments on PR, but restrict applies to targeted paths using dorny/paths-filter action. Each environment maintains independent state and variables. Avoid workspaces for environment isolation as they share backend config and increase blast radius. Directory separation provides clearer ownership, simpler RBAC, and safer partial applies when managing Nepal client infrastructure across regions.

Insufficient IAM permissions for the CI service account. Terraform needs create, update, delete, and tag permissions for every resource type in your configuration. Run terraform plan first to identify required actions, then scope IAM policies minimally. Also verify the GitHub Actions runner has network access to cloud APIs; VPC-restricted resources require self-hosted runners or VPN tunneling. Check CloudTrail or audit logs for exact denied actions. I have seen this repeatedly when teams grant admin locally but restrict CI accounts properly.

Add hashicorp/setup-terraform with terraform_wrapper: false, then run terraform fmt -check -recursive and tflint in a dedicated validation job before plan. Fail the workflow immediately on formatting violations to enforce consistency. Integrate checkov or tfsec for security scanning. Cache plugin directories using actions/cache to speed up subsequent runs. These checks cost seconds but prevent merged code that violates team standards or introduces security misconfigurations. Automated gates remove subjective code review debates about style.

Terraform has no native rollback. Revert the git commit that introduced the change, push to main, and let the apply workflow re-run with previous desired state. Maintain immutable infrastructure patterns where resources are replaced not mutated. Keep plan artifacts for at least 30 days as audit trail. For databases or stateful services, rely on application-level backups taken before apply. Document rollback procedures in README. In my experience, teams without tested rollback playbooks panic during outages; rehearse reversions quarterly.

Yes. Set TFC_TOKEN as encrypted secret and configure cloud blocks in terraform settings. GitHub Actions then delegates execution to Terraform Cloud workers while retaining PR commenting and approval gates. This hybrid approach gives you centralized state management, policy-as-code via Sentinel, and cost estimation without managing CI runner dependencies. Useful when scaling beyond solo developer projects. Pricing starts around NPR 8,000/month (~USD 60) for team tier. Evaluate whether standalone GitHub Actions suffices before adding this operational dependency.

Enable plugin caching via TF_PLUGIN_CACHE_DIR and persist with actions/cache. Use targeted applies with -target flag for large stacks during development. Split monolithic configurations into smaller, independently deployable stacks. Run validation and lint in parallel jobs. Use ARM64 runners which are cheaper and faster for Terraform. Avoid unnecessary terraform init on every run by caching .terraform directory. Monitor usage in GitHub billing dashboard. Small optimizations compound; shaving 30 seconds per run saves meaningful minutes across hundreds of monthly executions.

Share this article

Quick Contact Options
Choose how you want to connect me: