# Infrastructure as code (Terraform) The cloud infrastructure for missionMATCH, authored for the primary target AWS GovCloud and parameterized so the secondary target Azure Government is a tfvars and provider plus module swap, not a rewrite. The module split mirrors the application's cloud ports one to one. ## Layout ``` infra/terraform/ versions.tf providers.tf variables.tf main.tf outputs.tf root module environments/aws-govcloud.tfvars primary target values environments/azure-gov.tfvars secondary target (documented stub) modules/ network/ a private subnet VPC, the db subnet group, and the db security group database/ managed PostgreSQL 16 (encrypted, multi AZ, force_ssl, RDS managed master secret) object_storage/ an S3 bucket (versioned, SSE KMS, public access blocked, TLS only) secrets/ the platform KMS key and one empty Secrets Manager container per tenant reference key identity/ a workload OIDC provider and a least privilege app role (the identity hook) ``` ## What Terraform owns, and what it deliberately does NOT Terraform creates the network, the managed Postgres instance and its control plane database, the RDS managed master credential, the object bucket, the KMS key, the per tenant secret containers, and the app IAM role. It does NOT create: - the pgvector extension or the missionmatch_app / missionmatch_migrator / missionmatch_compliance roles or any schema or RLS: those live in goose migrations, run by the migrator, so a single tool owns the schema and the role separation the RLS design depends on is preserved. - any per tenant database: those are created by cmd/provision-tenant at onboarding. - any secret VALUE: the per tenant DSNs are put into the secret containers out of band by the operator (CLAUDE.md rule 3, keys stay on the server). A directory dump or a state file must never be a credential dump, so the database password is the RDS managed master secret and tenant DSNs live only in the secret store. The outputs feed the application's existing environment: the db endpoint forms MM_DATABASE_URL, MM_MIGRATOR_DATABASE_URL, and MM_COMPLIANCE_DATABASE_URL (with the role passwords set out of band), the bucket name the storage port, the secret ARNs the secrets port, and MM_SECRETS_BACKEND is aws_govcloud. ## Validate (no credentials, safe to run in CI) ``` terraform -chdir=infra/terraform fmt -check -recursive terraform -chdir=infra/terraform init -backend=false terraform -chdir=infra/terraform validate ``` fmt and validate require no cloud credentials and run in CI alongside the Go gates. ## Plan and apply (the operator's step, requires real credentials) terraform plan and terraform apply are NOT run automatically and never in CI. They require real AWS GovCloud credentials and the operator's review: ``` # configure the S3 backend (an encrypted, versioned state bucket) at init: terraform -chdir=infra/terraform init \ -backend-config="bucket=" -backend-config="key=missionmatch/prod.tfstate" \ -backend-config="region=us-gov-west-1" terraform -chdir=infra/terraform plan -var-file=environments/aws-govcloud.tfvars terraform -chdir=infra/terraform apply -var-file=environments/aws-govcloud.tfvars # operator only ``` After apply, run goose against the new database (CREATE EXTENSION vector, the roles, the schema, and RLS), then provision-tenant per client. See docs/RUNBOOK_ONBOARDING.md and docs/MIGRATE_TO_PER_TENANT.md. ## Notes - ARNs are built from data.aws_partition.current so GovCloud (partition aws-us-gov) is correct; never hardcode the aws partition. - The state bucket is itself a secret store (state can contain sensitive values), so it must be encrypted, versioned, and access restricted, with native S3 lockfile locking and the FIPS endpoint. - The validation here was run with the OpenTofu compatible toolchain (tofu fmt / validate); the HCL is Terraform compatible and the operator uses terraform.