MCPG
Guides
Guides8 min

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, and alekc/kubectl providers (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_manager ON to provision webhook TLS; without cert-manager the install will block. To bring your own TLS instead, pass cert_manager = { enabled = false } plus a webhook Secret config.

Beta note: the operator chart is not yet published to a registry (oci://ghcr.io/mcpg-dev/source-code/charts is reserved but not live). For now point chart_repository at a local path (chart = "${path.module}/../helm/charts/mcpg-operator", no version) or a private OCI mirror.

Quick start

hcl
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

ModuleRenders
modules/crdsthe 8 mcpg.dev CRDs as server-side-applied resources — upgradeable independently of the chart (Helm never upgrades its crds/ dir)
modules/operatorthe operator Helm release (crd.install=false, wait-for-ready, sizing presets, webhook TLS)
modules/gatewayan MCPGGateway + the config builder
modules/plugin-setan MCPGPluginSet (entries + capability grants)
modules/pluginan MCPGPlugin (cluster catalog entry)
modules/trust-bootstrapthe cluster-default MCPGRevocationList + signing-key secretRef pass-through
modules/tenantnamespace + RBAC + NetworkPolicy + plugin-set + gateway, fan-out with for_each
modules/plugin-mirroran in-cluster OCI mirror (MCPGPluginMirror) for air-gap
modules/airgapair-gap profile (mirror + digest-pinned, mirrorRef plugins)
modules/observabilitya 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:

hcl
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):

hcl
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)

bash
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-mcpgOperator chartCRD apiVersionTerraformOpenTofu
0.x0.1.xv1alpha2≥ 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. Ensure module.operator finished (it waits for the Deployment to be Available) before applying gateways; use depends_on = [module.operator].
  • CRDs not upgrading — they're managed by modules/crds, not the chart. Re-apply after 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