This site uses cookies for authentication, security, and preferences. Privacy Policy

Snap CD vs GitLab CI / GitHub Actions for Terraform

Most teams start running Terraform from their existing CI system. GitHub Actions, GitLab CI, Jenkins, CircleCI — they all support it. You write a pipeline that runs terraform plan on a pull request and terraform apply on merge. It works.

Until it doesn't.

This guide looks at what happens as Terraform usage grows inside a general-purpose CI tool, where the friction starts, and when a purpose-built deployment system like Snap CD becomes worth the switch.

General-purpose CI vs purpose-built infrastructure deployment

CI tools are designed for building and testing software. They execute steps in order, pass artifacts between jobs, and report success or failure. They're very good at that.

Terraform deployments have a different shape:

  • Stateful operations — every apply mutates remote state. A failed run can leave state locked or partially applied. CI doesn't know what Terraform state is.
  • Cross-state dependencies — one Terraform root's outputs feed into another's inputs. CI has no concept of this relationship.
  • Approval requirements — you want humans to review a plan before it applies. CI can approximate this with manual jobs, but it's bolted on, not built in.
  • Drift detection — infrastructure can change outside Terraform. CI only knows about your code; it doesn't watch for divergence between state and reality.

CI tools can run terraform apply, but they don't understand what that command means for your infrastructure.

The glue code problem

Here's what a real-world Terraform CI pipeline looks like once a team has more than two or three states:

Passing outputs between states

# GitHub Actions — networking outputs to compute
jobs:
  networking:
    steps:
      - run: terraform apply -auto-approve
      - run: terraform output -json > networking-outputs.json
      - uses: actions/upload-artifact@v4
        with:
          name: networking-outputs
          path: networking-outputs.json

  compute:
    needs: networking
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: networking-outputs
      - run: |
          VPC_ID=$(jq -r '.vpc_id.value' networking-outputs.json)
          SUBNET_IDS=$(jq -r '.private_subnet_ids.value' networking-outputs.json)
          terraform apply -auto-approve \
            -var="vpc_id=$VPC_ID" \
            -var="private_subnet_ids=$SUBNET_IDS"

This is fragile. The jq parsing breaks silently if output names change. The needs: graph has to mirror your Terraform dependency graph manually. If you add a new output, you update Terraform and the pipeline and the downstream jobs.

Manual approval gates

# GitLab CI — manual approval before apply
plan:
  stage: plan
  script:
    - terraform plan -out=tfplan
  artifacts:
    paths:
      - tfplan

approve:
  stage: approve
  script:
    - echo "Approved"
  when: manual
  needs: [plan]

apply:
  stage: apply
  script:
    - terraform apply tfplan
  needs: [approve]

This gives you a button to click, but no structured review. The plan output is buried in CI logs. There's no quorum ("require two approvers"). There's no audit trail beyond "someone clicked the button".

Scheduled drift detection

# GitHub Actions — check for drift nightly
on:
  schedule:
    - cron: '0 2 * * *'

jobs:
  drift:
    strategy:
      matrix:
        state: [networking, compute, database, dns]
    steps:
      - run: |
          cd ${{ matrix.state }}
          terraform plan -detailed-exitcode
          if [ $? -eq 2 ]; then
            echo "DRIFT DETECTED in ${{ matrix.state }}"
            # ... send Slack notification, open issue, etc.
          fi

You've now written a custom drift detection system. It runs on a schedule (not in real time), and the notification/remediation logic is entirely your responsibility.

The pattern

Every capability that a deployment tool provides natively, CI teams end up building from shell scripts, artifacts, manual jobs, and scheduled pipelines. Each one works in isolation. Together, they form a brittle system where the Terraform dependency graph lives in CI YAML rather than in code.

Credential management

CI tools store secrets as environment variables or masked variables. They're available to every job in the pipeline unless you carefully scope them with environment protections.

The problem: your networking state needs AWS credentials for Route53. Your compute state needs AWS credentials for EKS. Your database state needs AWS credentials for RDS plus a different set for the database root password. In CI, all of these secrets exist in the same project, and scoping them to specific jobs requires per-job environment configuration that's easy to misconfigure. And even when credentials are scoped, plan output posted to PRs can leak sensitive values — a problem any CI-based Terraform workflow shares.

Snap CD's Runner model solves this architecturally. Each Runner is a separate process with its own credentials. A Runner for production Azure only has production Azure credentials. A Runner for development AWS only has development AWS credentials. The credentials never pass through the Snap CD Server — they live on the Runner, and the Runner only executes Modules it's been assigned to.

Dependency orchestration

In CI, you build the dependency graph manually:

jobs:
  networking:
    # ...
  compute:
    needs: [networking]
  database:
    needs: [networking]
  dns:
    needs: [compute]
  monitoring:
    needs: [compute, database]

This graph has to match your actual Terraform dependency graph. If you add a new dependency (compute now needs an output from database), you update the needs: list, add artifact passing, and modify the downstream job's variables.

In Snap CD, dependencies are declared as snapcd_module_input_from_output resources:

resource "snapcd_module_input_from_output" "vpc_id" {
  module_id        = snapcd_module.compute.id
  input_kind       = "Param"
  name             = "vpc_id"
  output_module_id = snapcd_module.networking.id
  output_name      = "vpc_id"
}

Snap CD builds the dependency graph from these declarations. When networking's outputs change, compute automatically re-plans. No manual DAG maintenance.

Drift detection

CI tools don't know about drift. They run when you push code, not when your infrastructure changes. If someone manually modifies a security group in the AWS console, CI won't notice until the next terraform plan — which might be days or weeks later.

Snap CD runs drift detection on a configurable schedule per Module. When drift is detected, it can automatically create a plan to bring infrastructure back in line, and route that plan through the same approval workflow as any other change.

Plan review and approvals

CI shows plan output as raw text in build logs. For a small plan, this is fine. For a plan that touches fifty resources, you're scrolling through hundreds of lines of terminal output in a log viewer that wasn't designed for this.

Snap CD provides structured plan review:

  • Plans are displayed with resource-level detail
  • Approval gates require a configurable number of approvals before apply proceeds
  • Approvers can be scoped by role — only users with the Approver role on the relevant Stack can approve
  • All approvals are audited

Comparison

Capability GitLab CI / GitHub Actions Snap CD
Run terraform plan and apply Yes Yes
Cross-state dependency graph Manual (needs: + artifacts) Declarative (snapcd_module_input_from_output)
Automatic cascading on output changes No (manual re-trigger) Yes
Credential scoping per state Per-environment secrets (manual) Per-Runner (architectural)
Drift detection Custom scheduled pipelines Built-in, configurable per Module
Plan approval gates Manual jobs (no quorum) N-of-M approval with role scoping
Audit trail CI logs Structured audit log
Source watching (GitOps) Webhook on push Continuous polling with tag/branch/commit support
Multi-cloud in one pipeline Possible but messy Natural (one Runner per cloud)
Setup effort Low (you already have CI) Moderate (deploy Server + Runners)

When CI is enough

CI works well for Terraform when:

  • You have one or two states. The dependency graph is trivial. Passing outputs between two jobs isn't painful.
  • One team owns all infrastructure. There's no credential scoping concern. Everyone has access to everything.
  • Deployments are infrequent. You apply once a week, manually. The overhead of glue code is low because you rarely touch it.
  • You don't need drift detection. Your infrastructure is stable, or you have other monitoring in place.

The tipping point usually comes when you have five or more states with cross-dependencies, multiple teams needing scoped access, or compliance requirements that demand structured approval workflows. At that point, the CI glue code has become a project in itself — one that no one signed up to maintain.

Making the switch

Snap CD doesn't require you to abandon CI entirely. A common migration path:

  1. Keep CI for application builds and tests. CI is still the right tool for docker build, npm test, and go vet.
  2. Move Terraform deployments to Snap CD. Start with the states that have the most cross-dependencies.
  3. Delete the glue code. The shell scripts, the artifact passing, the manual approval jobs — they're replaced by Snap CD's Module system and approval gates.

The Terraform code itself doesn't change. Snap CD runs the same terraform plan and terraform apply commands that your CI pipeline ran. The difference is in how the surrounding orchestration — dependencies, credentials, approvals, drift detection — is managed.

See also

Snap CD

Intelligent GitOps for Infrastructure as Code. Automate, orchestrate, and scale your infrastructure deployments with confidence.


© 2026 Snap CD. All rights reserved.

An unhandled error has occurred. Reload 🗙