Install MCPG with Terraform
Install the MCPG Kubernetes operator and manage a gateway fleet with the terraform-mcpg module suite — CRDs, trust, plugins, gateways, and multi-tenant fan-out as declarative infrastructure (works on Terraform and OpenTofu).
terraform-mcpg is an opinionated module suite that installs the MCPG
Kubernetes operator and manages your gateway fleet — CRDs, trust, plugins,
gateways, and multi-tenant fan-out — as declarative infrastructure. It runs on
Terraform ≥ 1.7 and OpenTofu ≥ 1.7 (see the
OpenTofu guide for OpenTofu-only features like state
encryption).
Prefer typed resources with plan-time validation? See the native Terraform provider. The module suite ships first and covers the 80% case with zero Go.
Prerequisites
- A Kubernetes cluster and a kubeconfig.
- The
hashicorp/helm,hashicorp/kubernetes, andalekc/kubectlproviders (declared for you by the example/root module). - Network access to pull the operator Helm chart and the gateway image.
- cert-manager installed in the cluster. The admission webhook fails closed,
so the operator module defaults
cert_managerON to provision webhook TLS; without cert-manager the install will block. To bring your own TLS instead, passcert_manager = { enabled = false }plus awebhookSecret config.
Beta note: the operator chart is not yet published to a registry (
oci://ghcr.io/mcpg-dev/source-code/chartsis reserved but not live). For now pointchart_repositoryat a local path (chart = "${path.module}/../helm/charts/mcpg-operator", noversion) or a private OCI mirror.
Quick start
terraform {
required_providers {
kubernetes = { source = "hashicorp/kubernetes", version = "~> 2.30" }
helm = { source = "hashicorp/helm", version = "~> 2.13" }
kubectl = { source = "alekc/kubectl", version = "~> 2.0" }
}
}
provider "kubernetes" { config_path = "~/.kube/config" }
provider "helm" { kubernetes { config_path = "~/.kube/config" } }
provider "kubectl" { config_path = "~/.kube/config", load_config_file = true }
# Layer 2 — CRDs (managed independently of the chart).
module "crds" {
source = "./modules/crds"
crds_dir = "${path.module}/codegen/schemas/v1alpha2/crds"
}
# Layer 1 — operator install (ordered after the CRDs).
module "operator" {
source = "./modules/operator"
chart_version = "0.1.0"
depends_on = [module.crds]
}
# Layer 3 — a gateway (ordered after the operator).
module "gateway" {
source = "./modules/gateway"
name = "orders"
namespace = "mcpg-system"
image = { repository = "ghcr.io/mcpg-dev/gateway", tag = "v1.0.0-rc.17" }
replicas = 2
governance = { audit = { sinks = [{ kind = "dev.mcpg.builtin.audit.local-file" }] } }
depends_on = [module.operator]
}
terraform apply installs the operator, applies the CRDs, and creates a gateway
that reaches Ready. A runnable version is in examples/single-gateway.
Modules
| Module | Renders |
|---|---|
modules/crds | the 8 mcpg.dev CRDs as server-side-applied resources — upgradeable independently of the chart (Helm never upgrades its crds/ dir) |
modules/operator | the operator Helm release (crd.install=false, wait-for-ready, sizing presets, webhook TLS) |
modules/gateway | an MCPGGateway + the config builder |
modules/plugin-set | an MCPGPluginSet (entries + capability grants) |
modules/plugin | an MCPGPlugin (cluster catalog entry) |
modules/trust-bootstrap | the cluster-default MCPGRevocationList + signing-key secretRef pass-through |
modules/tenant | namespace + RBAC + NetworkPolicy + plugin-set + gateway, fan-out with for_each |
modules/plugin-mirror | an in-cluster OCI mirror (MCPGPluginMirror) for air-gap |
modules/airgap | air-gap profile (mirror + digest-pinned, mirrorRef plugins) |
modules/observability | a config helper for the gateway observability block |
The CRDs the suite installs are documented in the operator CRD reference.
The config builder
modules/gateway assembles spec.config (the gateway AppConfig) from typed
convenience sections plus an extra_config escape hatch merged last — so no
config field is unreachable:
module "gateway" {
source = "./modules/gateway"
name = "orders"
namespace = "mcpg-system"
image = { repository = "…", tag = "…" }
governance = { audit = { sinks = [{ kind = "dev.mcpg.builtin.audit.local-file" }] } } # typed section
plugins = [{ id = "db.read", source = { oci = "plugins/sql:1.4.2" }, enforce = true }]
extra_config = { /* anything not yet typed — wins on conflict */ }
}
Outputs include config_fingerprint (a client-side SHA-256 change detector). The
rendered spec.config is the gateway's own AppConfig schema — see the
configuration reference and validate it with
mcpg-config-check before committing.
Multi-tenancy
modules/tenant is for_each-friendly — one map edit adds or removes a whole
tenant (namespace + isolation + gateway + plugin-set):
module "tenant" {
source = "./modules/tenant"
for_each = {
team-a = { environment = "prod", replicas = 2 }
team-b = { environment = "staging" }
}
name = each.key
gateway = { image = { repository = "…", tag = "…" }, replicas = each.value.replicas }
plugin_set = { entries = [] }
}
See examples/multi-tenant and the
multi-tenant deployments guide.
Air-gap
modules/airgap stands up an in-cluster OCI mirror and your plugins reference it
by digest — no public-registry pulls. See examples/airgap.
Hybrid Terraform + GitOps
Recommended topology: Terraform owns layers 0–2 (cluster, operator,
cluster-scoped trust, tenant namespaces); Argo CD / Flux own the layer-3 CRs.
examples/hybrid-gitops shows the handoff outputs (namespaces, signing-key
ref, revocation list) a GitOps controller references rather than re-declares.
Commands (Nx)
nx run terraform-mcpg:lint # tofu fmt -check + tflint
nx run terraform-mcpg:validate:tofu # tofu init + validate (:terraform for TF)
nx run terraform-mcpg:test:tofu # tofu test (offline, mock providers)
nx run terraform-mcpg:build:production # bundle the module suite -> dist/
Raw terraform fmt, terraform validate, terraform test (or the tofu
equivalents) all work directly.
Compatibility
| terraform-mcpg | Operator chart | CRD apiVersion | Terraform | OpenTofu |
|---|---|---|---|---|
0.x | 0.1.x | v1alpha2 | ≥ 1.7 | ≥ 1.7 |
A CRD apiVersion bump is a minor with a regenerated CRD snapshot; a breaking
CRD change is a major.
Troubleshooting
- A CR fails admission right after install — the operator's validating
webhook is
failurePolicy: Fail. Ensuremodule.operatorfinished (it waits for the Deployment to be Available) before applying gateways; usedepends_on = [module.operator]. - CRDs not upgrading — they're managed by
modules/crds, not the chart. Re-applyafter refreshing the CRD snapshot (nx run terraform-mcpg:codegen). - Secrets in state — the modules reference Secrets by name only; never read signing-key bytes into Terraform. On OpenTofu, also enable state encryption.
See also
- Terraform provider — typed resources, plan-time validation
- OpenTofu — state encryption, OCI distribution, air-gap
- Pulumi — the same capabilities in a general-purpose language
- Operator CRD reference — the 8
mcpg.devCRDs - Kubernetes install with Helm — the manual Helm path