terraform-ci

Terraform in CI/CD — plan on PR, apply on merge, OIDC auth, drift detection, importing existing resources, common CLI commands, anti-patterns, and Terraform vs Pulumi vs CDK decision guide.

8 stars

Best use case

terraform-ci is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Terraform in CI/CD — plan on PR, apply on merge, OIDC auth, drift detection, importing existing resources, common CLI commands, anti-patterns, and Terraform vs Pulumi vs CDK decision guide.

Teams using terraform-ci should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/terraform-ci/SKILL.md --create-dirs "https://raw.githubusercontent.com/marvinrichter/clarc/main/skills/terraform-ci/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/terraform-ci/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How terraform-ci Compares

Feature / Agentterraform-ciStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Terraform in CI/CD — plan on PR, apply on merge, OIDC auth, drift detection, importing existing resources, common CLI commands, anti-patterns, and Terraform vs Pulumi vs CDK decision guide.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Terraform CI/CD & Operations

## When to Activate

- Setting up GitHub Actions (or other CI) to run `terraform plan` on pull requests
- Configuring OIDC-based AWS authentication for CI (no stored credentials)
- Importing existing cloud resources under Terraform control
- Deciding between Terraform, Pulumi, and AWS CDK for a new project
- Looking up common Terraform CLI commands
- Reviewing anti-patterns (local state, secrets in tfvars, count=0 to disable)

> For project structure, remote state setup, module design, workspace strategy, ECS/IAM patterns — see skill `terraform-patterns`.

## CI/CD Integration (GitHub Actions)

```yaml
# .github/workflows/terraform.yml
name: Terraform

on:
  pull_request:
    paths: ['infrastructure/**']
  push:
    branches: [main]
    paths: ['infrastructure/**']

jobs:
  plan:
    runs-on: ubuntu-latest
    permissions:
      id-token: write    # OIDC for AWS auth (no stored credentials)
      contents: read
      pull-requests: write

    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/github-terraform
          aws-region: eu-west-1

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: '~1.14'

      - name: Terraform Init
        run: terraform init
        working-directory: infrastructure/environments/prod

      - name: Terraform Validate
        run: terraform validate
        working-directory: infrastructure/environments/prod

      - name: Terraform Plan
        id: plan
        run: |
          terraform plan -var="db_password=${{ secrets.DB_PASSWORD }}" \
            -out=plan.tfplan -no-color 2>&1 | tee plan.txt
        working-directory: infrastructure/environments/prod

      - name: Comment Plan on PR
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v7
        with:
          script: |
            const plan = require('fs').readFileSync('infrastructure/environments/prod/plan.txt', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `\`\`\`hcl\n${plan.slice(0, 65000)}\n\`\`\``
            });

  apply:
    needs: plan
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    environment: production   # requires manual approval in GitHub
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/github-terraform
          aws-region: eu-west-1
      - uses: hashicorp/setup-terraform@v3
      - run: terraform init
        working-directory: infrastructure/environments/prod
      - run: terraform apply -auto-approve -var="db_password=${{ secrets.DB_PASSWORD }}"
        working-directory: infrastructure/environments/prod
```

---

## Importing Existing Resources

```bash
# Bring existing AWS resource under Terraform control
terraform import aws_s3_bucket.uploads my-existing-bucket-name
terraform import aws_security_group.rds sg-0abc123def456

# After import: run plan to check drift
terraform plan
# Fix .tf files until plan shows "No changes"
```

---

## Common Commands

```bash
terraform init          # download providers + configure backend
terraform validate      # check syntax and references
terraform fmt -recursive # format all .tf files
terraform plan          # preview changes (never modifies infra)
terraform apply         # apply the plan (prompts for confirmation)
terraform apply -auto-approve  # skip confirmation (CI only)
terraform destroy       # destroy everything (use with extreme care)
terraform state list    # list all managed resources
terraform state show aws_db_instance.main  # inspect state of one resource
terraform output        # print outputs
terraform graph | dot -Tpng > graph.png  # visualize dependency graph
```

---

## Anti-Patterns

| Anti-Pattern | Problem | Fix |
|---|---|---|
| Local state (no backend) | Lost on disk failure, no team sharing | S3 + DynamoDB locking from day 1 |
| Secrets in `.tfvars` committed to git | Credential exposure | Pass via CI env vars or Secrets Manager |
| `count = 0` to "disable" resources | Leaves dangling state | Remove the resource block + `terraform state rm` |
| Manual changes in console | State drift — next `plan` tries to revert | All changes through Terraform |
| No `deletion_protection` on prod DB | Accidental `destroy` deletes production data | Always set on prod databases |
| One giant `main.tf` | Impossible to navigate | Split by concern: `vpc.tf`, `rds.tf`, `ecs.tf` |
| `depends_on` overuse | Hides missing implicit references | Fix the reference chain instead |
| `ignore_changes` for everything | Terraform loses awareness of drift | Only ignore what autoscaling legitimately changes |

---

## Terraform vs. Pulumi vs. AWS CDK — When to Use What

Terraform is the right choice in many situations, but modern IaC alternatives (Pulumi, AWS CDK) offer advantages worth evaluating for new projects.

### Decision Matrix

| Criterion | Terraform HCL | Pulumi | AWS CDK |
|-----------|:-------------:|:------:|:-------:|
| Multi-cloud support | ✅ Best (3000+ providers) | ✅ Good | ❌ AWS only |
| Real programming language | ❌ HCL DSL | ✅ TS/Python/Go | ✅ TS/Python/Java |
| Unit testing infrastructure | ❌ | ✅ | ✅ |
| Type safety + IDE autocomplete | ❌ | ✅ | ✅ |
| Native loops & conditionals | ⚠️ `count`/`for_each` | ✅ | ✅ |
| Existing Terraform state migration | ✅ N/A | ⚠️ Via cdktf | ❌ |
| Provider ecosystem maturity | ✅ Largest | ✅ Uses TF providers | ⚠️ AWS-specific |
| Reusable abstractions | Modules | ComponentResource | L3 Constructs |
| Governance / policy | Sentinel / OPA | CrossGuard | CDK Aspects + OPA |
| Team HCL familiarity | ✅ | ❌ | ❌ |

### When to Stay with Terraform

- Existing large Terraform codebase — migration cost outweighs benefits
- Multi-cloud with providers not available in Pulumi (niche SaaS providers)
- Team is HCL-fluent and finds DSL simpler than TypeScript for infra
- Terraform Cloud governance (Sentinel policies, team VCS integration) is a requirement
- Working with providers only available via Terraform Registry

### When to Choose Pulumi

- New project and team writes TypeScript/Python/Go daily — same language for infra and app
- Need real unit tests for infrastructure code (`pulumi.runtime.setMocks`)
- Multi-cloud: deploy to AWS + GCP + Azure + Kubernetes in one program
- Need complex dynamic resource generation (lists of services, conditional resource graphs)
- CrossGuard policy enforcement in CI is a requirement

### When to Choose AWS CDK

- AWS-only infrastructure (no multi-cloud requirement)
- Team values strong defaults (L2 Constructs include encryption, proper IAM by default)
- Publishing reusable constructs to Construct Hub for other teams to consume
- Need CDK Pipelines for self-mutating CI/CD
- tighter AWS service integration and early access to new AWS services

### Hybrid Strategy: Terraform + Pulumi/CDK

For large organizations, a hybrid works well:
- **Terraform** manages foundational infrastructure: VPCs, accounts, DNS, IAM org-wide roles
- **Pulumi/CDK** manages application-specific resources: ECS services, databases, queues
- Pulumi/CDK reads Terraform outputs via remote state (`terraform_remote_state` or `pulumi.StackReference`)

```typescript
// Pulumi: read VPC from existing Terraform state
const tfState = new pulumi.StackReference("tf-networking/prod");
const vpcId = tfState.getOutput("vpc_id");

// Use VPC created by Terraform in a Pulumi resource
const service = new aws.ecs.Service("api", {
  networkConfiguration: {
    subnets: tfState.getOutput("private_subnet_ids").apply(ids => ids as string[]),
  },
});
```

### Migration Path: Terraform → Pulumi

If migrating an existing Terraform codebase:

1. Use `pulumi convert --from terraform` to auto-convert HCL to TypeScript/Python
2. Import existing state with `pulumi import` (matches existing resources without recreating)
3. Migrate module by module — keep Terraform for unmigrated modules, use Pulumi for new ones
4. Run `pulumi refresh` after import to align state with actual cloud resources

```bash
# Convert existing Terraform module to Pulumi TypeScript
pulumi convert --from terraform --language typescript ./infra/modules/vpc
```

---

## Infracost Cost Gate — Runnable CI Step

Add this step to your `plan` job (after `Terraform Plan`, before `Comment Plan on PR`) to block PRs that increase monthly cloud costs by more than 15%:

```yaml
      - name: Install Infracost
        run: |
          curl -fsSL https://raw.githubusercontent.com/infracost/infracost/master/scripts/install.sh | sh
        env:
          INFRACOST_API_KEY: ${{ secrets.INFRACOST_API_KEY }}

      - name: Run Infracost diff
        id: infracost
        run: |
          infracost diff \
            --path infrastructure/environments/prod \
            --compare-to main \
            --format json \
            --out-file /tmp/infracost.json
          # Extract percentage change (positive = cost increase)
          PCT=$(jq '.diffTotalMonthlyCost | tonumber' /tmp/infracost.json 2>/dev/null || echo "0")
          echo "cost_delta_pct=$PCT" >> "$GITHUB_OUTPUT"
        working-directory: .

      - name: Enforce cost threshold
        run: |
          DELTA="${{ steps.infracost.outputs.cost_delta_pct }}"
          echo "Monthly cost delta: $DELTA USD"
          # Block if increase > $500/month; tag PR for cost-review if > $50
          if (( $(echo "$DELTA > 500" | bc -l) )); then
            echo "::error::Monthly cost increase \$$DELTA exceeds \$500 threshold — add 'cost-approved' label to override"
            exit 1
          elif (( $(echo "$DELTA > 50" | bc -l) )); then
            gh pr edit "${{ github.event.pull_request.number }}" --add-label "cost-review"
            echo "::warning::Monthly cost increase \$$DELTA — tagged for cost-review"
          fi
```

**Prerequisites:** `INFRACOST_API_KEY` secret set in GitHub repo settings (free at infracost.io).

---

## Related Skills

- `iac-modern-patterns` — Pulumi TypeScript, AWS CDK L1/L2/L3, Bicep, cdktf — full reference for non-HCL IaC
- `kubernetes-patterns` — Kubernetes resource management (Terraform can provision clusters, Helm provider manages workloads)
- `devsecops-patterns` — Checkov, OPA/Conftest for IaC compliance scanning in CI
- `ci-cd-patterns` — GitHub Actions integration for Terraform plan/apply pipelines

Related Skills

terraform-patterns

8
from marvinrichter/clarc

Infrastructure as Code with Terraform — project structure, remote state, modules, workspace strategy, AWS/GCP patterns, CI/CD integration, and security hardening. The standard for managing production infrastructure.

zero-trust-patterns

8
from marvinrichter/clarc

Zero-Trust security patterns — mTLS between microservices (Istio/SPIFFE), SPIRE workload identity, OPA/Envoy authorization, NetworkPolicy default-deny-all, short-lived credentials, service mesh security, and Kubernetes RBAC hardening.

wireframing

8
from marvinrichter/clarc

Wireframing and prototyping workflow: fidelity levels (lo-fi sketch → mid-fi wireframe → hi-fi prototype), tool selection (Figma, Excalidraw, Balsamiq), user flow diagrams, wireframe annotation standards, information architecture (IA) mapping, and the handoff from wireframe to visual design. For developers who need to communicate UI structure before writing code.

webrtc-patterns

8
from marvinrichter/clarc

WebRTC patterns — peer connection setup, ICE/STUN/TURN configuration, signaling server design, SFU vs mesh topology, screen sharing, media track management, and reconnect/ICE restart handling.

webhook-patterns

8
from marvinrichter/clarc

Webhook patterns for receiving, verifying (HMAC), and idempotently processing third-party events. Covers Stripe, GitHub, and generic webhook patterns, delivery guarantees, retry handling, and testing.

web-performance

8
from marvinrichter/clarc

Web performance optimization: Core Web Vitals (LCP, CLS, INP), Lighthouse CI with budget configuration, bundle analysis (webpack-bundle-analyzer, vite-bundle-visualizer), hydration performance, network waterfall reading, image optimization (WebP/AVIF, srcset), and font performance.

wasm-performance

8
from marvinrichter/clarc

WebAssembly performance: wasm-opt binary optimization, size reduction (panic=abort, LTO, strip), profiling WASM in Chrome DevTools, memory management (linear memory, avoiding GC pressure), SIMD, and multi-threading with SharedArrayBuffer.

wasm-patterns

8
from marvinrichter/clarc

WebAssembly patterns: wasm-pack, wasm-bindgen (JS↔Wasm interop), WASI, Component Model, wasm-opt, Rust-to-WASM compilation, JS integration (web workers, streaming instantiation), and production deployment (CDN, Content-Type headers).

visual-testing

8
from marvinrichter/clarc

Visual Regression Testing: tool comparison (Chromatic/Percy/Playwright screenshots/BackstopJS), pixel-diff vs AI-based comparison, baseline management, flakiness strategies (masks, tolerances, waitForLoadState), CI integration with GitHub Actions, and Storybook integration.

visual-identity

8
from marvinrichter/clarc

Brand identity development: color palette construction (primary/secondary/semantic/neutral), logo concept brief writing, typeface pairings, brand voice definition, mood board direction, and Brand Guidelines document structure. Use when establishing or evolving a visual brand — not for implementing existing tokens.

ux-micro-patterns

8
from marvinrichter/clarc

UX micro-patterns for every product state: Empty States, Loading States (skeleton screens, spinners, optimistic UI), Error States, Success States, Confirmation Dialogs, Onboarding Flows, and Progressive Disclosure. These patterns apply to every feature — done wrong, they're the biggest source of user confusion.

typography-design

8
from marvinrichter/clarc

Typography as a creative discipline: typeface selection criteria, type pairing (serif + sans, display + body), modular scale systems, line-height and tracking ratios, hierarchy construction, and web/mobile rendering considerations. The decisions behind design tokens, not the tokens themselves.