Skip to main content
This guide walks through the full deployment of the Odin AI Platform EKS infrastructure on AWS using Terragrunt. It covers tool installation, environment setup, and a phased deployment sequence designed to ensure proper dependency ordering across all infrastructure components. Deployments are organized into nine phases:
  1. State Management — Bootstraps the S3 bucket used to store Terraform state for the environment.
  2. EKS Infrastructure — Provisions the VPC, subnets, NAT gateways, IAM roles, and the EKS cluster and managed node groups.
  3. Storage & Load Balancing — Deploys the EBS CSI driver for persistent volumes and the AWS Load Balancer Controller for ALB ingress.
  4. Karpenter Autoscaling — Sets up dynamic node provisioning with Spot instance support and interruption handling via SQS and EventBridge.
  5. KEDA Autoscaling — Deploys KEDA for pod-level autoscaling based on CPU and memory thresholds.
  6. Data Services — Provisions Supabase (self-hosted or Cloud), ElastiCache Redis, and Amazon MQ RabbitMQ.
  7. Odin Services — Deploys the Odin AI Platform application stack (Web, FastAPI, Celery, Automator) via Helm.
  8. SigNoz Observability — Deploys distributed tracing, metrics, and log aggregation via SigNoz and the k8s-infra agent.
  9. Final Deployment — Runs a full terragrunt apply to reconcile any remaining resources.
Before starting, complete the prerequisites checklist with the customer and ensure all <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)
  • kubectl for Kubernetes management
  • helm for Helm chart management

Installation Guide

Installing Terragrunt

macOS (Homebrew)
Linux (apt)
Windows (Chocolatey)

Installing kubectl

macOS (Homebrew)
Linux
Windows (Chocolatey)

Installing Helm

macOS (Homebrew)
Linux
Windows (Chocolatey)

Verifying Installation

AWS CLI Configuration


Creating a New Environment

Step 1: Copy the Environment Template

The env-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

All placeholders follow the <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 is app.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
  1. Open the AWS Certificate Manager console
  2. Switch to the correct region (top-right) — must match <YOUR_AWS_REGION>
  3. Click Request a certificateRequest a public certificateNext
  4. Under Fully qualified domain name, enter the wildcard (e.g., *.app.example.com) or a specific domain
  5. Set Validation method to DNS validation
  6. Click Request — the certificate is created in Pending validation state
Adding the DNS CNAME validation record ACM generates a CNAME record that you must add to your DNS provider to prove domain ownership. Get the values from the ACM Console by opening the certificate and expanding the domain under Domains.
Include the trailing dot (.) at the end of the CNAME values if your DNS provider requires it.
Cloudflare
  1. Log in to Cloudflare → select your domain → go to DNSRecordsAdd record
  2. Set Type to CNAME
  3. Paste the ACM CNAME name into Name and the ACM CNAME value into Target
  4. Set Proxy status to DNS only (grey cloud icon) — the certificate will not validate through the Cloudflare proxy
  5. Click Save
Route 53
  1. Open the Route 53 console → Hosted zones → select your zone → Create record
  2. Set Record type to CNAME
  3. Paste the ACM CNAME name into Record name (subdomain portion only) and the value into Value
  4. Set TTL to 300 and click Create records
In ACM you can also click Create records in Route 53 to have ACM add the record automatically if the hosted zone is in the same account.
Once DNS propagates (typically 1–5 minutes), the certificate status changes to Issued. Copy the ARN from the top of the certificate — it looks like 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 by terragrunt.hcl via get_env().

Spot Instances & Stateful Workloads

Spot instances are configured per NodePool in values/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 PersistentVolumeClaim on Spot NodePools.
  • Use a nodeSelector targeting node-type: database-dedicated with the matching database-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 general Spot 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 inside values/karpenter.yaml.
How Karpenter handles Spot interruption warnings: AWS gives a 2-minute interruption notice before terminating a Spot instance. Karpenter uses EventBridge and SQS to act on this automatically:
This is configured in the 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

Obtain the VPC ID after the EKS cluster is created before deploying the AWS Load Balancer Controller.

5.4 values/karpenter-values.yaml — Karpenter controller

Obtain the EKS cluster endpoint after the EKS cluster is created and before deploying Karpenter.

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)

All keys below must be generated consistently and shared with ha-supabase-db.yaml. Generate them once and use the same values in both files.

5.10 values/ha-supabase-db.yaml — Supabase HA Database (only if ENABLE_HA_SUPABASE_DB=true)

Secrets here must match supabase.yaml. Use the same generated values for postgresPassword, jwtSecret, anonKey, and serviceRoleKey.
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

Redis and RabbitMQ endpoints are only available after Terraform creates those AWS resources. Certificate ARNs must be provisioned in ACM before deployment.
General settings: Supabase (dataServiceConfig) — self-hosted (ENABLE_SUPABASE=true): Supabase (dataServiceConfig) — Supabase Cloud (ENABLE_SUPABASE=false): Redis:
RabbitMQ:
SSL / Certificate ARNs:
Web frontend Supabase keys — self-hosted (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:
  1. 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
  2. After EKS cluster created — Set: <YOUR_VPC_ID> (infrastructure.yaml), <YOUR_EKS_CLUSTER_ENDPOINT> (karpenter-values.yaml)
  3. After terraform apply for AWS services — Set: <YOUR_REDIS_HOST>, <YOUR_RABBITMQ_HOST> (odin-services.yaml)

Step 6: Verify No Placeholders Remain

Expected output should be empty, or contain only references to resources about to be created (VPC, Redis, MQ, EKS). If any placeholders remain, refer to the Step 5 sub-sections above. Files checklist:

Phase 1: State Management Setup

Purpose: S3 bucket creation for Terraform state. Each environment’s state management module creates an S3 bucket with the pattern odin-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 Infrastructure
IAM Roles and Policies
EKS Cluster and Node Groups

Using a Custom / Private Docker Registry

By default, Odin AI Platform images are pulled from Docker Hub using a secret named regcred. 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
Step 2 — Set the secret name in values/odin-services.yaml
Step 3 — Update image references
Step 4 — Verify pull access before full deployment

2.2 Deploy EKS Infrastructure

Step 1: Core Infrastructure
After this step, update vpcId in values/infrastructure.yaml before deploying the AWS Load Balancer Controller.
Step 2: EKS Cluster and IAM Roles and Policies
Step 3: Node Groups and Addons
After this step, update CLUSTER_ENDPOINT in values/karpenter-values.yaml before deploying Karpenter.
Check EKS Cluster Connectivity

Phase 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 Driver
AWS Load Balancer Controller

3.2 Deploy Storage and Load Balancing

Step 1: EBS CSI Driver
Verification
Step 2: AWS Load Balancer Controller
Verification

Phase 4: Karpenter Autoscaling

Purpose: IAM roles for Karpenter, Spot interruption handling, Karpenter controller and node pools.

4.1 Dry Run — Karpenter

Karpenter IAM Resources
EC2 Spot Service-Linked Role (if spot instances are enabled)
Karpenter Spot Interruption (if enabled in terragrunt.hcl)
Karpenter Helm Charts
Karpenter NodePools and EC2NodeClasses
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 Resources
Verification
Step 2: EC2 Spot Service-Linked Role (if spot instances are enabled)
The EC2 Spot service-linked role is account-wide (only one per AWS account) and must exist before Karpenter can launch Spot instances.
Option A: Let Terraform create it (recommended for new deployments)
Option B: Import if the role already exists
Step 3: Karpenter Spot Interruption (if enabled in terragrunt.hcl)
Verification
Step 4: Karpenter Helm Chart
Verification
Step 5: Karpenter Kubernetes Manifests
Verification

Phase 5: KEDA Autoscaling

Purpose: KEDA for application-level autoscaling.

5.1 Dry Run — KEDA

5.2 Deploy KEDA

Verification

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)
Step 2: HA Supabase DB (if enabled)
Step 3: Supabase application (if enabled)
Step 4: AWS Services — ElastiCache and RabbitMQ (if enabled)

6.2 Deploy Data Services

Step 1: CloudNativePG operator (if enabled)
Step 2: HA Supabase DB (if enabled)
Verify PgBouncer pooler and credentials after deployment:
Use the pooler ClusterIP (or 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.
Step 4: AWS Services — ElastiCache and RabbitMQ (if enabled)
Verification
Before deploying Odin Services, update values/odin-services.yaml with the Redis endpoint, RabbitMQ endpoint, and all certificate ARNs obtained in this phase.

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

Verification

Phase 8: SigNoz Observability

Purpose: Logs and metrics monitoring.

8.1 Dry Run — SigNoz Charts

8.2 Deploy SigNoz Charts

Verification

Phase 9: Final Deployment

9.1 Complete Deployment

This final apply handles any remaining resources not explicitly targeted in previous phases.

9.2 Verify Deployment


Troubleshooting

State lock issues
Karpenter not working
Load Balancer issues
Helm chart issues

Cleanup


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.