Example: bootstrap a full GCP infrastructure¶
This walkthrough shows terragrunt-generator in a realistic end-to-end flow:
scaffold a repository with a Cookiecutter
template, then populate it module by module with terragrunt-generator.
Two tools, two distinct jobs:
| Tool | Responsibility |
|---|---|
cookiecutter-terragrunt-project |
Scaffolds the repo skeleton: the root terragrunt.hcl (providers, GCS remote state, Terraform version), the config.<env>.yaml seed, and shell helpers. |
terragrunt-generator |
Generates one child terragrunt.hcl per Terraform module and merges that module's documented inputs into config.<env>.yaml. |
The result is a DRY, multi-environment Terragrunt repository where every module's source and inputs are declared once, and all values live in a single per-environment YAML file.
Prerequisites¶
- uv — runs both
cookiecutterandterragrunt-generatoron demand viauvx(no manual install; uv also provisions a suitable Python) - Terraform and Terragrunt
gcloudauthenticated against your project
Step 1 — scaffold the repository¶
The template is non-interactive friendly. Feed it a default context and let it
generate the project (the slug gcp_infrastructure is derived from
project_name):
tmpfile=$(mktemp)
cat <<'EOF' > "$tmpfile"
default_context:
full_name: "Chris"
email: "goabonga@pm.me"
github_username: "goabonga"
project_name: "GCP Infrastructure"
remote_state_bucket_name: ""
default_environment: "dev"
EOF
uvx cookiecutter -f --no-input --config-file "$tmpfile" \
https://github.com/goabonga/cookiecutter-terragrunt-project.git
rm "$tmpfile"
What the scaffold gives you¶
gcp_infrastructure/
├── .bashrc # terragrunt helper functions (switch_env, plan, apply, …)
├── config.dev.yaml # per-environment values (starts with remote_state.bucket)
└── google/
└── terragrunt.hcl # the ROOT config every module inherits
The root google/terragrunt.hcl is the parent that each generated module
includes via find_in_parent_folders(). It wires up three things:
- Environment + values —
local.environmentfromget_env("ENV", "dev"), andlocal.configmerged fromconfig.<env>.yaml. - Remote state — a
gcsbackend keyed by<environment>/<path_relative_to_include>, so every module gets an isolated state path automatically. - Generated provider + versions files —
google/google-betaproviders and arequired_versionpin, regenerated on each run.
locals {
environment = get_env("ENV", "dev")
config = merge(
yamldecode(file(find_in_parent_folders(format("config.%s.yaml", local.environment)))),
)
}
remote_state {
backend = "gcs"
config = {
bucket = "${local.config.remote_state.bucket}"
prefix = "${format("%s/%s", local.environment, path_relative_to_include())}"
}
generate = { path = "generated_backend.tf", if_exists = "overwrite_terragrunt" }
}
Set your state bucket in config.dev.yaml:
Step 2 — populate modules with terragrunt-generator¶
Move into the generated project, then generate one module per Terraform
source. With uv, uvx terragrunt-generator
fetches and runs the tool in a cached, ephemeral environment — no virtualenv
to create or activate:
Each invocation follows the same pattern:
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-network.git \
-v v10.0.0 \
-p modules/vpc \
-l network.vpc \
--yaml-output ./ \
--yaml-for-env dev \
--output google/network/vpc
What that one command does:
-u/-v/-p— the module's git source, ref, and subpath. The childterragrunt.hclpinsterraform.sourceto exactly this.-l network.vpc— the lookup key. Inputs resolve fromlocal.all.network.vpc.*, and the module is gated onnetwork.vpc.enabledin the YAML.--output google/network/vpc— where the childterragrunt.hclis written. Itsinclude { path = find_in_parent_folders() }finds the rootgoogle/terragrunt.hcl.--yaml-output ./+--yaml-for-env dev— append (and merge, never overwrite) the module's documented inputs under thenetwork.vpckey ofconfig.dev.yaml.
Run it once per module. The reference bootstrap builds a full GCP foundation from Google's official modules:
| Area | Lookup key(s) | Output path under google/ |
|---|---|---|
| Cloud DNS | network.dns.internal, network.dns.external |
network/dns/{internal,external} |
| VPC / subnets | network.vpc, network.subnets |
network/{vpc,subnets} |
| Firewall / routes / peering | network.firewall, network.routes, network.peering |
network/{firewall,routes,peering} |
| NAT | network.nat |
network/nat |
| Addresses | network.address.internal, network.address.external |
network/address/{internal,external} |
| KMS | kms |
kms |
| Artifact Registry | registries.docker |
registries/docker |
| Cloud Storage | buckets |
buckets |
| Log export / bucket | log.logexport, log.logbucket |
log/{logexport,logbucket} |
| GKE cluster / workload identity | gke.cluster, gke.workload-identity |
gke/{cluster,workload-identity} |
Resulting structure¶
gcp_infrastructure/
├── .bashrc
├── config.dev.yaml # every module's inputs, merged under one tree
└── google/
├── terragrunt.hcl # root: providers, GCS backend, versions
├── network/
│ ├── dns/{internal,external}/terragrunt.hcl
│ ├── vpc/terragrunt.hcl
│ ├── subnets/terragrunt.hcl
│ ├── firewall/terragrunt.hcl
│ ├── routes/terragrunt.hcl
│ ├── peering/terragrunt.hcl
│ ├── nat/terragrunt.hcl
│ └── address/{internal,external}/terragrunt.hcl
├── kms/terragrunt.hcl
├── registries/docker/terragrunt.hcl
├── buckets/terragrunt.hcl
├── log/{logexport,logbucket}/terragrunt.hcl
└── gke/{cluster,workload-identity}/terragrunt.hcl
config.dev.yaml mirrors that tree — one block per lookup key, each commented
with the module's variable documentation so you know what to fill in:
remote_state:
bucket: my-tf-state-bucket
network:
vpc:
enabled: true
# project_id - The ID of the project where this VPC will be created
project_id: ""
# network_name - The name of the network being created
network_name: ""
# dns: { internal: { … }, external: { … } }
gke:
cluster:
enabled: true
# …
Day-2 workflow¶
The scaffold's .bashrc wraps Terragrunt with environment-aware helpers.
Source it, pick an environment, then plan/apply a subtree:
source .bashrc # adds switch_env / init / plan / apply / destroy / …
switch_env dev # sets ENV=dev and `gcloud config set project`
plan ./google/network # terragrunt run-all plan on the whole network subtree
apply ./google/network/vpc
Because every module shares the root config and the single config.<env>.yaml,
adding a new environment is just a new config.staging.yaml and
switch_env staging — no module edits required.
The full bootstrap script¶
The complete reference script — cookiecutter scaffold plus every
terragrunt-generator invocation — is reproduced below. Save it as
bootstrap_gcp_infra.sh and run it from an empty directory; it creates the
project, provisions a virtualenv, and writes the Terragrunt files. It does not
apply anything to your cloud account on its own.
#!/usr/bin/env bash
echo "build infra"
tmpfile=$(mktemp)
cat <<EOF > "$tmpfile"
default_context:
full_name: "Chris"
email: "goabonga@pm.me"
github_username: "goabonga"
project_name: "GCP Infrastructure"
remote_state_bucket_name: ""
default_environment: "dev"
EOF
uvx cookiecutter -f --no-input --config-file "$tmpfile" https://github.com/goabonga/cookiecutter-terragrunt-project.git
rm "$tmpfile"
cd gcp_infrastructure
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-cloud-dns.git \
-v v5.3.0 \
-l network.dns.internal \
--yaml-output ./ \
--yaml-for-env dev \
--output google/network/dns/internal
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-cloud-dns.git \
-v v5.3.0 \
-l network.dns.external \
--yaml-output ./ \
--yaml-for-env dev \
--output google/network/dns/external
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-network.git \
-v v10.0.0 \
-l network.vpc \
-p modules/vpc \
--yaml-output ./ \
--yaml-for-env dev \
--output google/network/vpc
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-network.git \
-v v10.0.0 \
-l network.subnets \
-p modules/subnets-beta \
--yaml-output ./ \
--yaml-for-env dev \
--output google/network/subnets
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-network.git \
-v v10.0.0 \
-l network.firewall \
-p modules/firewall-rules \
--yaml-output ./ \
--yaml-for-env dev \
--output google/network/firewall
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-network.git \
-v v10.0.0 \
-l network.routes \
-p modules/routes-beta \
--yaml-output ./ \
--yaml-for-env dev \
--output google/network/routes
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-network.git \
-v v10.0.0 \
-l network.peering \
-p modules/network-peering \
--yaml-output ./ \
--yaml-for-env dev \
--output google/network/peering
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-cloud-nat.git \
-v v5.3.0 \
-l network.nat \
--yaml-output ./ \
--yaml-for-env dev \
--output google/network/nat
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-address.git \
-v v4.1.0 \
-l network.address.internal \
--yaml-output ./ \
--yaml-for-env dev \
--output google/network/address/internal
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-address.git \
-v v4.1.0 \
-l network.address.external \
--yaml-output ./ \
--yaml-for-env dev \
--output google/network/address/external
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-kms.git \
-v v4.0.0 \
-l kms \
--yaml-output ./ \
--yaml-for-env dev \
--output google/kms
uvx terragrunt-generator \
-u https://github.com/GoogleCloudPlatform/terraform-google-artifact-registry.git \
-v v0.3.0 \
-l registries.docker \
--yaml-output ./ \
--yaml-for-env dev \
--output google/registries/docker
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-cloud-storage.git \
-v v10.0.1 \
-l buckets \
--yaml-output ./ \
--yaml-for-env dev \
--output google/buckets
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-log-export.git \
-v v10.0.0 \
-l log.logexport \
--yaml-output ./ \
--yaml-for-env dev \
--output google/log/logexport
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-log-export.git \
-v v10.0.0 \
-l log.logbucket \
-p modules/logbucket \
--yaml-output ./ \
--yaml-for-env dev \
--output google/log/logbucket
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-kubernetes-engine.git \
-v v36.3.0 \
-l gke.cluster \
-p modules/beta-private-cluster-update-variant \
--yaml-output ./ \
--yaml-for-env dev \
--output google/gke/cluster
uvx terragrunt-generator \
-u https://github.com/terraform-google-modules/terraform-google-kubernetes-engine.git \
-v v36.3.0 \
-l gke.workload-identity \
-p modules/workload-identity \
--yaml-output ./ \
--yaml-for-env dev \
--output google/gke/workload-identity