- State Management — Bootstraps the S3 bucket used to store Terraform state for the environment.
- EKS Infrastructure — Provisions the VPC, subnets, NAT gateways, IAM roles, and the EKS cluster and managed node groups.
- Storage & Load Balancing — Deploys the EBS CSI driver for persistent volumes and the AWS Load Balancer Controller for ALB ingress.
- Karpenter Autoscaling — Sets up dynamic node provisioning with Spot instance support and interruption handling via SQS and EventBridge.
- KEDA Autoscaling — Deploys KEDA for pod-level autoscaling based on CPU and memory thresholds.
- Data Services — Provisions Supabase (self-hosted or Cloud), ElastiCache Redis, and Amazon MQ RabbitMQ.
- Odin Services — Deploys the Odin AI Platform application stack (Web, FastAPI, Celery, Automator) via Helm.
- SigNoz Observability — Deploys distributed tracing, metrics, and log aggregation via SigNoz and the k8s-infra agent.
- Final Deployment — Runs a full
terragrunt applyto reconcile any remaining resources.
<YOUR_*> placeholders in the environment template are filled in. Several values — including the VPC ID, EKS cluster endpoint, and Redis and RabbitMQ endpoints — are only available after specific phases complete, so the guide flags exactly when to capture and apply them.
Prerequisites
- AWS CLI configured with appropriate permissions
- Terraform (>= 1.0)
- Terragrunt (latest version)
kubectlfor Kubernetes managementhelmfor Helm chart management
Installation Guide
Installing Terragrunt
macOS (Homebrew)Installing kubectl
macOS (Homebrew)Installing Helm
macOS (Homebrew)Verifying Installation
AWS CLI Configuration
Creating a New Environment
Step 1: Copy the Environment Template
Theenv-template-folder contains pre-structured files with <YOUR_*> placeholders ready to be filled in. Copy it entirely to create your new environment folder.
Step 2: Verify All Placeholders Are Present
<YOUR_*> convention. The steps below walk through filling them in file by file.
Step 3: Provision SSL Certificates (AWS ACM)
Before setting environment variables you need the certificate ARNs. Use the AWS Console to request SSL certificates in AWS Certificate Manager (ACM) for all domains your environment will serve. Option A: Single wildcard certificate (recommended) A single wildcard certificate covers all subdomains with one ARN. For example, if your base domain isapp.example.com, a single *.app.example.com certificate covers:
Option B: Per-service certificates
Request one certificate per domain if you cannot use a wildcard. Repeat the steps below for each domain:
<YOUR_WEB_DOMAIN>, <YOUR_API_DOMAIN>, <YOUR_AUTOMATOR_DOMAIN>, <YOUR_SUPABASE_DOMAIN> (only if ENABLE_SUPABASE=true), <YOUR_SIGNOZ_DOMAIN> (only if ENABLE_SIGNOZ=true).
Requesting a certificate in the AWS Console
- Open the AWS Certificate Manager console
- Switch to the correct region (top-right) — must match
<YOUR_AWS_REGION> - Click Request a certificate → Request a public certificate → Next
- Under Fully qualified domain name, enter the wildcard (e.g.,
*.app.example.com) or a specific domain - Set Validation method to DNS validation
- Click Request — the certificate is created in
Pending validationstate
Include the trailing dot (
.) at the end of the CNAME values if your DNS provider requires it.- Log in to Cloudflare → select your domain → go to DNS → Records → Add record
- Set Type to
CNAME - Paste the ACM CNAME name into Name and the ACM CNAME value into Target
- Set Proxy status to DNS only (grey cloud icon) — the certificate will not validate through the Cloudflare proxy
- Click Save
- Open the Route 53 console → Hosted zones → select your zone → Create record
- Set Record type to
CNAME - Paste the ACM CNAME name into Record name (subdomain portion only) and the value into Value
- Set TTL to
300and click Create records
arn:aws:acm:<region>:<account-id>:certificate/<uuid>. Keep the ARN(s) handy for the next step.
Step 4: Set Environment Variables
Set these shell environment variables before running any Terragrunt commands. They are read directly byterragrunt.hcl via get_env().
Spot Instances & Stateful Workloads
Spot instances are configured per NodePool invalues/karpenter.yaml, not via environment variables. Each NodePool declares its own capacity strategy:
The
application NodePool uses m/c instance families (generation 5+) with On-Demand only. Supabase service pods are pinned here via nodeSelector: workload-type: "application" to guarantee they are never interrupted by a Spot reclamation event.
The database-dedicated NodePool never uses Spot. It uses consolidationPolicy: WhenEmpty so Karpenter will not evict a node that still has a running pod, making it safe for stateful workloads such as PostgreSQL and CloudNativePG replicas.
Guidelines for stateful applications on Spot:
- Do not schedule databases, persistent queues, or any pod with a
PersistentVolumeClaimon Spot NodePools. - Use a
nodeSelectortargetingnode-type: database-dedicatedwith the matchingdatabase-workload: "true"toleration for database pods. - Use
nodeSelector: workload-type: "application"for user-facing stateless services that must remain available without interruption. - For background workloads (Web, API, Celery, Automator), the
generalSpot NodePool is appropriate — Karpenter’s SQS interruption handler drains Spot nodes gracefully before AWS reclaims them, and KEDA’s minimum replica count (≥ 2) ensures availability during node replacement. - To disable Spot globally, remove
"spot"from the values list in every NodePool insidevalues/karpenter.yaml.
karpenter block in terragrunt.hcl:
Step 5: Update Environment-Specific File Values
Do a find-and-replace across all files in your new env folder for the following placeholders:5.1 terragrunt.hcl — Core cluster configuration
5.2 state/terragrunt.hcl — S3 state bucket
5.3 values/infrastructure.yaml — AWS Load Balancer Controller
5.4 values/karpenter-values.yaml — Karpenter controller
5.5 values/karpenter-nodeclasses.yaml — Karpenter node classes
5.6 values/aws-ebs-csi-driver.yaml — EBS CSI Driver
5.7 values/karpenter.yaml — Karpenter NodePools
Node class names (
general, compute-intensive, memory-intensive, gpu, database) must match entries in karpenter-nodeclasses.yaml.
5.8 values/keda.yaml — KEDA Autoscaler
No environment-specific placeholders required. Resource limits and replica counts are pre-configured with sensible defaults. Review and adjust if needed.
5.9 values/supabase.yaml — Supabase application (only if ENABLE_SUPABASE=true)
5.10 values/ha-supabase-db.yaml — Supabase HA Database (only if ENABLE_HA_SUPABASE_DB=true)
Storage class (
ebs-csi-gp2), instance counts, and resource limits are pre-configured. Adjust postgres.storage.size and postgres.walStorage.size for your expected data volume.
5.11 values/cloudnative-pg.yaml — CloudNativePG Operator (only if ENABLE_CNPG=true)
No environment-specific placeholders required. This deploys the CNPG operator controller only. Default settings (3 replicas, resource limits) are suitable for most environments.
5.12 values/odin-services.yaml — Odin application services
Supabase (
dataServiceConfig) — self-hosted (ENABLE_SUPABASE=true):
Supabase (
dataServiceConfig) — Supabase Cloud (ENABLE_SUPABASE=false):
Redis:
ENABLE_SUPABASE=true):
Web frontend Supabase keys — Supabase Cloud (
ENABLE_SUPABASE=false):
5.13 values/signoz.yaml — SigNoz Observability (only if ENABLE_SIGNOZ=true)
5.14 values/signoz-k8s-infra.yaml — SigNoz K8s Metrics (only if ENABLE_SIGNOZ=true)
The OTel collector endpoint (
signoz-otel-collector.monitoring.svc.cluster.local:4317) is pre-configured assuming both SigNoz and k8s-infra are deployed in the monitoring namespace. No change needed unless you use a custom release name.
Deployment Ordering Reminder
Some values are only available after certain infrastructure has been deployed. Follow this order:- Before any deployment — Set:
<YOUR_ENV_NAME>,<YOUR_AWS_REGION>,<YOUR_AWS_ACCOUNT_ID>,<YOUR_ENVIRONMENT>,<YOUR_PROJECT>,<YOUR_VPC_CIDR>, all domain names, all certificate ARNs, all Supabase values,<YOUR_TOOLKIT_ENCRYPTION_KEY>, RabbitMQ username/password - After EKS cluster created — Set:
<YOUR_VPC_ID>(infrastructure.yaml),<YOUR_EKS_CLUSTER_ENDPOINT>(karpenter-values.yaml) - After
terraform applyfor AWS services — Set:<YOUR_REDIS_HOST>,<YOUR_RABBITMQ_HOST>(odin-services.yaml)
Step 6: Verify No Placeholders Remain
Phase 1: State Management Setup
Purpose: S3 bucket creation for Terraform state. Each environment’s state management module creates an S3 bucket with the patternodin-terraform-state-{environment-name}, configures encryption, versioning, and public access blocking, and uses local state for the state module itself (bootstrap pattern).
Phase 2: EKS Infrastructure Deployment
Purpose: Core networking (VPC, subnets, NAT gateway), IAM roles and policies, EKS cluster and managed node groups.2.1 Dry Run — EKS Infrastructure
Core InfrastructureUsing a Custom / Private Docker Registry
By default, Odin AI Platform images are pulled from Docker Hub using a secret namedregcred. If the customer hosts images in a different registry, follow these steps before deploying odin-services.
Step 1 — Create the imagePullSecret in the target namespace
values/odin-services.yaml
2.2 Deploy EKS Infrastructure
Step 1: Core InfrastructurePhase 3: Storage and Load Balancing
Purpose: EBS CSI driver for persistent volumes, AWS Load Balancer Controller running on managed node group.3.1 Dry Run — Storage and Load Balancing
EBS CSI Driver3.2 Deploy Storage and Load Balancing
Step 1: EBS CSI DriverPhase 4: Karpenter Autoscaling
Purpose: IAM roles for Karpenter, Spot interruption handling, Karpenter controller and node pools.4.1 Dry Run — Karpenter
Karpenter IAM Resourcesterragrunt.hcl)
An expected error may appear during plan:
API did not recognize GroupVersionKind from manifest (CRD may not be installed). This is safe to ignore — Kubernetes validates resources against the live API at plan time, before CRDs are installed.4.2 Deploy Karpenter
Step 1: Karpenter IAM Resourcesterragrunt.hcl)
Phase 5: KEDA Autoscaling
Purpose: KEDA for application-level autoscaling.5.1 Dry Run — KEDA
5.2 Deploy KEDA
Phase 6: Data Services
Purpose: Supabase (database), ElastiCache (Redis), RabbitMQ (message queue).Deploy CloudNativePG operator first, then the HA Supabase DB cluster, then the Supabase application. The DB cluster must be ready before Supabase starts.
6.1 Dry Run — Data Services
Step 1: CloudNativePG operator (if enabled)6.2 Deploy Data Services
Step 1: CloudNativePG operator (if enabled)EXTERNAL-IP if LoadBalancer) as the SUPABASE_POSTGRES_HOST value in values/odin-services.yaml and as secret.db.postgresHost in values/supabase.yaml.
Step 3: Supabase application (if enabled)
All Supabase service pods run exclusively on the Karpenter application NodePool (On-Demand only) to prevent Spot interruptions.
Phase 7: Odin Services
Purpose: Application deployment via Helm.Before deploying, temporarily scale down
fastapiBackend to a single replica for the initial database migration run — set replicaCount: 1, workers: 1, and keda.minReplicas: 1. Once the migration completes successfully, revert these values to their production defaults before re-deploying.7.1 Dry Run — Odin Services
7.2 Deploy Odin Services
Phase 8: SigNoz Observability
Purpose: Logs and metrics monitoring.8.1 Dry Run — SigNoz Charts
8.2 Deploy SigNoz Charts
Phase 9: Final Deployment
9.1 Complete Deployment
9.2 Verify Deployment
Troubleshooting
State lock issuesCleanup
Monitoring and Logging
Quick Reference — All Deployment Commands
Replace
your-env-name with your actual environment name throughout. Always run dry runs (terragrunt plan) first to validate your configuration before applying changes.
