Terraform Pipeline Generator for GitLab CI & GitHub Actions
Analyze dependencies, estimate costs, enforce policies, and generate optimal pipelines for Terraform/OpenTofu monorepos
Documentation • Installation • Quick Start • Examples
Managing Terraform in a monorepo is painful:
- Dependencies between modules must be respected — EKS can't deploy before VPC
- Manual pipelines are tedious to write and a nightmare to maintain
- Full deployments waste CI minutes when only one module changed
- No visibility into what a plan will cost or whether it violates policies
TerraCi solves all of this. Point it at your repo, and it generates correct, dependency-aware CI pipelines — with cost estimates and policy checks baked in.
|
Pipeline Generation
|
Intelligence
|
|
Flexibility
|
Developer Experience
|
# Homebrew
brew install edelwud/tap/terraci
# Go
go install github.com/edelwud/terraci/cmd/terraci@latest
# Docker
docker run --rm -v $(pwd):/workspace ghcr.io/edelwud/terraci:latest generate
# Binary — download from GitHub Releases
# https://github.com/edelwud/terraci/releases# Interactive setup wizard
terraci init
# Non-interactive with GitHub Actions
terraci init --ci --provider github
# Validate structure & dependencies
terraci validate
# Generate pipeline
terraci generate -o .gitlab-ci.yml # GitLab
terraci generate -o .github/workflows/terraform.yml # GitHub Actions
# Only changed modules (CI)
terraci generate --changed-only --base-ref main -o .gitlab-ci.yml Your Repo TerraCi CI Pipeline
┌─────────────┐ ┌──────────────────────────┐ ┌──────────────────────────┐
│ platform/ │ │ 1. Discover modules │ │ │
│ prod/ │───>│ 2. Parse remote_state │───>│ plan-vpc → apply-vpc │
│ eu-west/ │ │ 3. Build dependency DAG │ │ ↓ ↓ │
│ vpc/ │ │ 4. Topological sort │ │ plan-eks plan-rds │
│ eks/ │ │ 5. Detect CI provider │ │ ↓ ↓ │
│ rds/ │ │ 6. Generate YAML │ │ apply-eks apply-rds │
└─────────────┘ └──────────────────────────┘ │ │
│ Output: .gitlab-ci.yml │
│ or workflow.yml │
└──────────────────────────┘
Scans directories matching a configurable pattern (default: {service}/{environment}/{region}/{module}):
platform/prod/eu-central-1/vpc/ → Module: platform/prod/eu-central-1/vpc
platform/prod/eu-central-1/eks/ → Module: platform/prod/eu-central-1/eks
platform/prod/eu-central-1/ec2/rabbitmq/ → Submodule (depth 5)
The pattern is fully configurable — {team}/{project}/{component} works too.
Dependencies are extracted from terraform_remote_state data sources. The key in the remote state config must mirror your directory structure (matching structure.pattern) — this is how TerraCi maps state file paths back to modules:
# eks/main.tf — key follows the same {service}/{env}/{region}/{module} pattern
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
bucket = "my-state"
key = "platform/prod/eu-central-1/vpc/terraform.tfstate"
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^
# matches directory structure → TerraCi resolves: eks depends on vpc
region = "eu-central-1"
}
}TerraCi evaluates Terraform functions statically (split, element, length, abspath, lookup, join, format, etc.) and resolves locals that depend on path.module. A common pattern that works out of the box:
locals {
path_arr = split("/", abspath(path.module))
service = local.path_arr[length(local.path_arr) - 4]
environment = local.path_arr[length(local.path_arr) - 3]
region = local.path_arr[length(local.path_arr) - 2]
}
data "terraform_remote_state" "vpc" {
backend = "s3"
config = {
key = "${local.service}/${local.environment}/${local.region}/vpc/terraform.tfstate"
}
}Limitations:
- Static analysis only — TerraCi does not connect to remote backends or execute
terraform init. Dependencies that rely on runtime values (e.g.,data.terraform_remote_state.X.outputs.Yused as a key in another remote state) cannot be resolved. Derive your state keys from the filesystem path (abspath(path.module)) or explicit locals, not from other modules' outputs.- Backend-aware matching — When the
keypath alone is ambiguous (e.g., two modules with the same key in different buckets), TerraCi parses each module'sterraform { backend "s3" { ... } }block and uses thebucketto disambiguate. This requires backend configuration to be defined in the module's.tffiles (not solely via-backend-configCLI flags).
Pipeline jobs are built as a dependency DAG. Independent modules can run in parallel, while dependent modules wait for the jobs that produce their inputs:
plan-vpc -> apply-vpc
apply-vpc -> plan-eks -> apply-eks
apply-vpc -> plan-rds -> apply-rds
CI providers render the same DAG with their native mechanics: GitHub uses
needs, while GitLab derives stages from topological job groups.
# .terraci.yaml
structure:
pattern: "{service}/{environment}/{region}/{module}"
exclude:
- "*/test/*"
- "*/sandbox/*"
execution:
binary: "terraform" # or "tofu"
extensions:
# GitLab CI (omit for GitHub Actions)
gitlab:
image: { name: "hashicorp/terraform:1.6" }
# GitHub Actions (omit for GitLab CI)
# github:
# runs_on: "ubuntu-latest"
# MR/PR summary comments (enabled by default)
summary:
on_changes_only: false
include_details: true
labels:
- terraform
- "{environment}"
- "{module}"
- "resource:{resource_type}"
# AWS cost estimation
# cost:
# providers:
# aws: { enabled: true }
# OPA policy checks
# policy:
# enabled: true
# sources:
# - type: path
# path: policies
# decisions:
# deny: block # block, warn, ignore
# warn: warn # block, warn, ignore
# Dependency update checks
# tfupdate:
# enabled: true
# target: all # all, modules, providers
# policy:
# bump: minor # patch, minor, majorTip: Add
# yaml-language-server: $schema=https://raw.githubusercontent.com/edelwud/terraci/main/terraci.schema.jsonat the top of your.terraci.yamlfor IDE autocomplete. Or runterraci schemato generate the schema locally.
| Command | Description |
|---|---|
terraci init |
Interactive TUI wizard to create .terraci.yaml |
terraci validate |
Validate project structure and dependencies |
terraci generate |
Generate CI pipeline (GitLab CI or GitHub Actions) |
terraci graph |
Visualize dependency graph (DOT, PlantUML, levels) |
terraci cost |
Estimate AWS costs from Terraform plan files |
terraci summary |
Post plan/cost/policy summary to MR/PR (CI) |
terraci policy pull |
Materialize policies from configured sources |
terraci policy check |
Evaluate plans against OPA policies |
terraci schema |
Generate JSON schema for config validation |
terraci tfupdate |
Resolve Terraform dependency versions and sync lock files |
terraci version |
Show version and embedded OPA version |
CI integration patterns
GitLab CI supports generative pipelines — TerraCi runs as a job inside a parent pipeline and generates a child pipeline that GitLab picks up automatically:
# .gitlab-ci.yml (parent pipeline)
generate:
stage: generate
image: ghcr.io/edelwud/terraci:latest
script:
- terraci generate --changed-only --base-ref $CI_MERGE_REQUEST_DIFF_BASE_SHA -o generated.yml
artifacts:
paths: [generated.yml]
deploy:
stage: deploy
trigger:
include:
- artifact: generated.yml
job: generateGitHub Actions does not support dynamic workflow generation at runtime. Use a pre-commit hook to regenerate the workflow file and commit it alongside your changes:
# .husky/pre-commit or .git/hooks/pre-commit
terraci generate --changed-only --base-ref main -o .github/workflows/terraform.yml
git add .github/workflows/terraform.ymlCommon usage examples
# Changed modules only (for MR/PR pipelines)
terraci generate --changed-only --base-ref main -o .gitlab-ci.yml
# Filter by any segment name
terraci generate --filter environment=prod --filter service=platform
# Plan-only mode (no apply jobs)
terraci generate --plan-only
# Exclude patterns
terraci generate --exclude "*/sandbox/*" --exclude "*/test/*"
# Dependency graph as DOT
terraci graph --format dot -o deps.dot
# Show execution levels
terraci graph --format levels
# Show what depends on a module
terraci graph --module platform/prod/eu-central-1/vpc --dependents
# Policy check a specific module
terraci policy check --module platform/prod/eu-central-1/vpc --format json
# Dry run
terraci generate --dry-run
# Estimate AWS costs from plan.json files
terraci cost
# Cost for a single module
terraci cost --module platform/prod/eu-central-1/rds
# Cost as JSON
terraci cost --output jsonJSON output now uses explicit resource status values:
exact— fully priced at plan timeusage_estimated— partly estimated from configured capacityusage_unknown— still unknown at plan time and needs runtime telemetryunsupported/failed— not priced, with optionalfailure_kindandstatus_detail
Build a custom TerraCi binary with additional (or fewer) plugins using xterraci:
# Install xterraci
go install github.com/edelwud/terraci/cmd/xterraci@latest
# Add an external plugin
xterraci build --with github.com/myorg/terraci-plugin-slack
# Use a local plugin during development
xterraci build --with github.com/myorg/plugin=../my-plugin
# Remove a built-in plugin
xterraci build --without cost
# List available built-in plugins
xterraci list-pluginsSee examples/external-plugin/ for a minimal plugin example.
Full documentation is available at edelwud.github.io/terraci.
| Topic | Link |
|---|---|
| Getting Started | guide/getting-started |
| Project Structure | guide/project-structure |
| Dependencies | guide/dependencies |
| Pipeline Generation | guide/pipeline-generation |
| Configuration Reference | config/ |
| Summary Comments | config/summary |
| Cost Estimation | config/cost |
| Policy Checks | config/policy |
| CLI Reference | cli/ |
Contributions are welcome! Please open an issue or pull request.