Terraform Deployment - ericfitz/tmi GitHub Wiki
This guide covers deploying TMI infrastructure on AWS using Terraform. AWS is the only cloud target with Terraform support. The other supported deployment targets are k3s (deployments/k8s/dev/k3s) and local docker-desktop development; see Planning-Your-Deployment.
TMI provides two Terraform environments under terraform/environments/:
| Environment | Purpose |
|---|---|
aws-public |
The live, internet-accessible deployment: VPC, EKS, RDS PostgreSQL, Secrets Manager, ACM certificate, ECR repositories, and bootstrap Kubernetes objects |
aws-persistent |
Long-lived account-level resources that must outlive any single aws-public deployment: the NAT egress Elastic IP, S3 log bucket, CloudTrail, security alerting, and IAM Access Analyzer |
Terraform manages infrastructure and bootstrap objects only (namespace, tmi-server-config ConfigMap, secrets, ServiceAccount, AWS Load Balancer Controller). The workloads themselves (server, Redis, workers, ingress) are applied by scripts/deploy-aws.sh using the deployments/k8s/dev/aws kustomize overlay. For a full deployment, use make deploy-aws (see below) rather than running Terraform by hand.
In-cluster Redis requires TLS and a password, and NATS requires mTLS (TMI_NATS_URL=tls://nats.tmi-platform.svc:4222 in the ConfigMap). deploy-aws.sh applies the vendored cert-manager and Stakater Reloader, then the internal CA (deployments/k8s/platform/pki.yml) that issues the Redis and NATS certificates, before the workloads.
Route 53 -> ALB (HTTPS 443, ACM certificate, WebSocket-capable)
↓
EKS cluster (2 managed t3.medium nodes, private subnets)
├── TMI Server pod (1 replica)
├── Redis pod (separate Deployment; TLS + password)
└── NATS JetStream + workers (mTLS)
↓
RDS PostgreSQL (db.t3.micro, private subnet)
↓
AWS Secrets Manager
| Resource | Details |
|---|---|
| Kubernetes | EKS, managed node group of 2 × t3.medium in private subnets; public API endpoint restricted to a CIDR set by deploy-aws.sh
|
| Database | RDS PostgreSQL db.t3.micro, deletion protection enabled, final snapshot on destroy, storage autoscaling up to 100 GB |
| Load Balancer | ALB (internet-facing) created by the AWS Load Balancer Controller from the ingress in the kustomize overlay |
| Certificates | ACM certificate for domain_name, DNS-validated in Route 53 |
| Secrets | AWS Secrets Manager (JWT secret, DB password, Redis password, settings encryption key) |
| Logging | CloudWatch (30-day retention); ALB access logs and VPC flow logs go to the S3 log bucket owned by aws-persistent
|
| Registry | ECR (one repository per component: server, Redis, extractor, chunk-embed, controller) |
| Egress | NAT gateway using the fixed Elastic IP owned by aws-persistent
|
Key constraints:
- Single TMI server instance always -- TMI is stateful and not designed for HA
-
Multi-arch container images -- amd64 + arm64 via Docker buildx (
--arch both;make build-app-awsbuilds amd64 only) - TMI logs to stdout -- collected by CloudWatch
- Terraform 1.5+
-
AWS CLI with a configured profile (
deploy-aws.shdefaults to thetmiprofile; override with--profile) - kubectl (for verifying deployment)
- Docker (for building container images)
- An existing Route 53 hosted zone for the API hostname
-
aws-persistent applied first:
aws-publicreads the NAT egress EIP from it
make build-app-aws builds linux/amd64 images and pushes them to ECR. For local-only builds use make build-app. The container build script accepts --target local or --target aws.
make deploy-aws ARGS="--domain tmi.example.com --zone-id Z0123456789ABCDEFGHIJ --api-cidr 203.0.113.5/32"The EKS public API endpoint allowlist comes from --api-cidr <ip>/32, or, when that is not given, from the untracked .local/aws-deploy.json ({"api_cidr_host": "<hostname that resolves to your public IP>"}), which the script resolves to a /32. The deploy fails if neither is present.
deploy-aws.sh runs terraform apply for aws-public, builds and pushes images to ECR, configures kubeconfig, applies the kustomize overlay, upserts the server DNS record, and verifies. Useful options: --api-cidr, --region, --name-prefix, --profile, --skip-build, --dry-run (plan only), --auto-approve, --destroy. Run scripts/deploy-aws.sh --help for the full list. Preview with make deploy-aws-dry-run ARGS="--domain ... --zone-id ...".
The tf-* Make targets wrap scripts/manage-terraform.py. TF_ENV selects the environment and defaults to aws-public:
make tf-init
make tf-plan
make tf-apply
make tf-output
make tf-destroy # DESTRUCTIVE
make tf-plan TF_ENV=aws-persistent # operate on the persistent stackOr run Terraform by hand:
cd terraform/environments/aws-public
cp terraform.tfvars.example terraform.tfvars # edit values
terraform init -backend-config=backend.hcl
terraform plan -out=tfplan
terraform apply tfplanKey aws-public variables (see variables.tf and terraform.tfvars.example):
| Variable | Default | Description |
|---|---|---|
aws_region |
us-east-1 |
AWS region |
name_prefix |
tmi |
Prefix for resource names |
domain_name |
(required) | API hostname for the ACM certificate |
hosted_zone_id |
(required) | Route 53 zone containing domain_name
|
extra_env_vars |
{} |
Extra server environment variables merged into the ConfigMap |
eks_public_access_cidrs |
null |
CIDRs allowed to reach the EKS public API endpoint (set by deploy-aws.sh) |
tags |
{} |
Additional resource tags |
kubectl get pods -n tmi-platform
curl https://<domain_name>/Terraform sets the server's defaults through the tmi-server-config ConfigMap. Notable values for aws-public:
| Variable | Value |
|---|---|
TMI_BUILD_MODE |
production |
TMI_AUTH_AUTO_PROMOTE_FIRST_USER |
false |
TMI_AUTH_EVERYONE_IS_A_REVIEWER |
true |
TMI_LOG_ALSO_LOG_TO_CONSOLE |
true |
TMI_LOG_REDACT_AUTH_TOKENS |
true |
TMI_LOG_SUPPRESS_UNAUTH_LOGS |
true |
TMI_SERVER_INTERFACE |
0.0.0.0 |
TMI_SERVER_PORT |
8080 |
TMI_SERVER_TLS_ENABLED |
true (the ALB re-encrypts to the pod) |
TMI_CORS_ALLOWED_ORIGINS |
Pinned browser origin (set in terraform/modules/kubernetes/aws/k8s_resources.tf) |
The ConfigMap is defined in terraform/modules/kubernetes/aws/k8s_resources.tf; that file is the source of truth for the complete list.
Add custom environment variables (IdP configuration, timeouts, etc.) via the extra_env_vars variable in your .tfvars:
extra_env_vars = {
"OAUTH_PROVIDERS_GOOGLE_CLIENT_ID" = "your-client-id"
"OAUTH_PROVIDERS_GOOGLE_CLIENT_SECRET" = "your-client-secret"
"TMI_WEBSOCKET_INACTIVITY_TIMEOUT_SECONDS" = "600"
}The JWT secret, database password, and Redis password are generated via Terraform's random_password resource and the settings encryption key via random_id; all are stored in AWS Secrets Manager. They are stable across subsequent terraform apply runs.
TMI uses WebSockets for real-time collaborative diagram editing. The ALB idle timeout is set to 3600 seconds (idle_timeout.timeout_seconds=3600 in deployments/k8s/dev/aws/ingress.yml) to support long-lived connections. TMI's default WebSocket inactivity timeout is shorter, so the LB timeout is not the limiting factor.
-
Verify deployment:
kubectl get pods -n tmi-platform-- confirm TMI and Redis pods are running -
Test connectivity:
curl https://<domain_name>/-- should return TMI version info -
Register OAuth provider: Configure at least one OAuth identity provider:
- Set the callback URL at the IdP to
https://<domain_name>/oauth2/callback - Add the IdP's client ID/secret via
extra_env_varsand re-apply
- Set the callback URL at the IdP to
The AWS deployment pins the allowed browser origin with TMI_CORS_ALLOWED_ORIGINS (default set in terraform/modules/kubernetes/aws/k8s_resources.tf). If the client is served from a different origin, update that value.
terraform/
├── environments/
│ ├── aws-public/ # Live EKS/RDS deployment
│ └── aws-persistent/ # NAT EIP, log bucket, CloudTrail, alerts, Access Analyzer
└── modules/
├── certificates/aws/ # ACM certificates
├── database/aws/ # RDS PostgreSQL
├── dns/aws/ # Route 53 records (not used by aws-public; deploy-aws.sh upserts the DNS record)
├── kubernetes/aws/ # EKS cluster + bootstrap objects
├── logging/aws/ # CloudWatch log collection
├── network/aws/ # VPC, subnets, NAT, security groups
└── secrets/aws/ # Secrets Manager
Both environments use an S3 backend with a lockfile. The bucket and region are supplied at init time:
terraform init -backend-config=backend.hclaws-persistent reuses ../aws-public/backend.hcl with its own state key (tmi/aws-persistent/terraform.tfstate).
make deploy-aws ARGS="--destroy --domain tmi.example.com --zone-id Z0123456789ABCDEFGHIJ"
# or
make tf-destroyThe RDS database has deletion protection enabled and takes a final snapshot, so terraform destroy requires disabling deletion protection first.
aws-persistent is never destroyed by a deploy or undeploy. In particular, the NAT egress Elastic IP is protected: it has prevent_destroy and an IAM deny on ec2:ReleaseAddress, because an external system depends on that address. Do not destroy the aws-persistent stack casually.
kubectl describe pod -n tmi-platform <pod-name>
kubectl logs -n tmi-platform <pod-name>A pod stuck Pending with "Too many pods" means the node has hit the VPC CNI pod-per-node ceiling (17 pods on t3.medium); the deployment uses two nodes to leave room for rolling updates.
Check the ALB idle timeout. It must be >= the TMI WebSocket inactivity timeout (TMI_WEBSOCKET_INACTIVITY_TIMEOUT_SECONDS).
Ensure TMI_OAUTH_CALLBACK_URL matches the URL registered with your identity provider. If you use a custom domain, set it via extra_env_vars.
The EKS public API endpoint is restricted to a CIDR set by deploy-aws.sh (--api-cidr, or api_cidr_host from .local/aws-deploy.json resolved to a /32). If your IP changed, re-run the deploy script with --api-cidr <your-ip>/32, or let it re-resolve api_cidr_host.
make build-app-aws builds linux/amd64 only. For amd64 + arm64 images, call the build script directly with --arch both --push (Docker buildx):
make build-app-aws # linux/amd64, build and push to ECR
uv run scripts/build-app-containers.py --target aws --arch both --push # multi-arch
make build-app # local platform only (testing)The Chainguard base images and Go cross-compilation (CGO_ENABLED=0) make this work seamlessly. Oracle-tagged builds (requiring CGO) remain x86_64 only.
- Planning-Your-Deployment -- Pre-deployment planning and decision matrix
- Oracle-Container-Images -- Oracle ADB server image variant
- Cloud-Logging -- Cloud logging extension point and AWS CloudWatch logging
- Database-Setup -- Database installation and configuration
- Post-Deployment -- Verification, smoke tests, and next steps
- Configuration-Reference -- Full configuration variable reference