> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runlayer.com/llms.txt
> Use this file to discover all available pages before exploring further.

# ECS + Terraform

> Deploy production-ready infrastructure on AWS ECS using Terraform

<Note>
  **Looking for Kubernetes?** This guide covers ECS (Elastic Container Service) deployment. For Kubernetes/EKS deployment, see the [EKS + Terraform guide](/deployment/eks-terraform).
</Note>

## Overview

Deploy Runlayer on AWS ECS (Elastic Container Service) with Fargate using Terraform. This provides a serverless container deployment without managing Kubernetes clusters. Choose between **minimal configuration** (5 required parameters) or **enterprise configuration** (full customization).

**Infrastructure Repository:** [runlayer/runlayer-infra](https://github.com/runlayer/runlayer-infra)

<Note>
  This deployment creates AWS resources that incur costs. Typical costs range
  from \$100-400/month depending on configuration.
</Note>

## Quick Start

**Step 1: Get the infrastructure code**

```bash theme={null}

cd infra  # Infrastructure code is included as a subtree
```

<Tabs>
  <Tab title="Minimal (Recommended)">
    **Get production-ready infrastructure with these parameters:**

    ```bash theme={null}
    cp minimal.tfvars.example production.tfvars

    # Edit these required values:
    # region = "us-east-1"
    # domain_name = "ai.yourcompany.com"  # Must own this domain
    # account = 123456789012
    # database_username = "postgres"

    terraform init
    terraform apply -var-file="production.tfvars"
    ```

    **That's it!** You get production-ready defaults for everything else.

    <Warning>
      You must own the domain name and be able to validate it via DNS for SSL certificate creation.
    </Warning>
  </Tab>

  <Tab title="Enterprise">
    **Full control over all infrastructure settings:**

    ```bash theme={null}
    cp enterprise.tfvars.example production.tfvars

    # Customize all settings as needed
    nano production.tfvars

    terraform init
    terraform apply -var-file="production.tfvars"
    ```

    **160+ configuration options** for enterprise customization.
  </Tab>
</Tabs>

## Configuration Options

### Minimal Configuration

**Required:**

```hcl theme={null}
environment       = "production"              # Environment name
region            = "us-east-1"              # AWS region
domain_name       = "ai.yourcompany.com"     # Your domain (required for SSL)
account           = 123456789012             # AWS account ID
```

**Smart production-ready defaults include:**

* **Database**: Aurora PostgreSQL 16.6, 2-16 ACUs, private subnets, 7-day backups
* **Security**: Public ALB with internet access, private database/cache
* **SSL**: Automatic wildcard certificate lookup (e.g., `*.staging.runlayer.com`); set `enable_acm_dns_validation = true` to create a new certificate instead
* **Scaling**: 2 backend + 2 frontend containers, auto-scale to 10 max
* **Resources**: Backend 512 CPU/1024 MB, Frontend 512 CPU/1024 MB
* **Resources**: Backend 2048 CPU/4096 MB, Frontend 512 CPU/1024 MB
* **Network**: 3-AZ VPC with /16 CIDR, public/private subnets
* **Monitoring**: CloudWatch logs for all services

### Enterprise Configuration

**All minimal options plus 160+ customizable parameters:**

<Accordion title="Database Configuration">
  ```hcl theme={null}
  database_name     = "anysource_prod"
  database_username = "postgres"        # Database master username
  database_config = {
    engine_version      = "16.6"        # PostgreSQL version
    min_capacity       = 4              # Min Aurora capacity (ACUs)
    max_capacity       = 32             # Max Aurora capacity (ACUs)
    publicly_accessible = false         # Keep private (recommended)
    backup_retention   = 30             # Backup retention days
    subnet_type        = "private"      # Use private subnets
  }
  ```
</Accordion>

<Accordion title="Bring Your Own Database / Cache">
  Skip Aurora and/or ElastiCache when you operate PostgreSQL and Redis/Valkey yourself. Postgres and Redis can be configured independently.

  ```hcl theme={null}
  external_database = {
    host            = "postgres.internal.example.com"
    reader_host     = "postgres-reader.internal.example.com" # optional
    port            = 5432
    database        = "runlayer"
    username        = "runlayer"
    ssl_mode        = "require"
  }
  external_database_password = var.external_db_password

  external_redis = {
    host        = "valkey.internal.example.com"
    port        = 6379
    tls_enabled = true
  }
  external_redis_password = var.external_redis_password
  ```

  ECS tasks receive `POSTGRES_*` env vars and Secrets Manager entries for `PLATFORM_DB_PASSWORD` and `REDIS_URL`. Ensure private subnets can reach your endpoints. See also [Helm external database configuration](/deployment/helm#database-configuration).
</Accordion>

<Accordion title="Security Configuration">
  ```hcl theme={null}
  alb_access_type = "public"            # "public" or "private"
  alb_allowed_cidrs = [                 # Security group IPv4 ranges
    "0.0.0.0/0"                        # Internet (change for security)
    # "203.0.113.0/24",                # Your office IPs
    # "198.51.100.0/24"                # Your VPN IPs
  ]
  alb_allowed_ipv6_cidrs = ["::/0"]    # Security group IPv6 ranges (default: all)

  # WAF IP Allowlisting (recommended for production)
  waf_enable_ip_allowlisting = true     # Enable WAF-based IP filtering
  waf_allowlist_ipv4_cidrs = [          # Allowed IPv4 CIDR blocks
    "203.0.113.0/24",                   # Office network
    "198.51.100.42/32",                 # Specific IP address
  ]
  waf_allowlist_ipv6_cidrs = [          # Allowed IPv6 CIDR blocks
    "2001:db8::/48",                    # Office IPv6 range
  ]

  # SSL Certificate options (choose one):

  # Option 1: Look up existing wildcard certificate (DEFAULT)
  # Module derives wildcard from domain: ai.prod.yourcompany.com → *.prod.yourcompany.com
  hosted_zone_name = "prod.yourcompany.com"  # Used for DNS records and certificate lookup

  # Option 2: Create new wildcard certificate with DNS validation
  enable_acm_dns_validation = true           # Creates new cert instead of lookup
  hosted_zone_name = "prod.yourcompany.com"  # Required for Route53 validation

  # Option 3: Use specific certificate ARN (bypasses module)
  ssl_certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/xxx"

  # DNS record control (separate from certificate management)
  create_dns_records = true  # Creates Route53 ALIAS record pointing domain to ALB (default: false)

  ```
</Accordion>

<Accordion title="Dual ALB Setup (Split-Horizon DNS)">
  ```hcl theme={null}
  # Enable dual ALB for split-horizon DNS
  # Public ALB for internet traffic + Internal ALB for private network traffic
  enable_dual_alb = true

  # Optional: Use an existing private hosted zone
  private_hosted_zone_id = "Z1234567890ABC"

  # Optional: Associate with a specific VPC (defaults to service VPC)
  private_hosted_zone_vpc_id = "vpc-1234567890abcdef0"

  # Optional: Associate with additional VPCs (for VPC peering scenarios)
  private_hosted_zone_additional_vpc_ids = [
    "vpc-0987654321fedcba0",  # Peered VPC 1
    "vpc-1111222233334444",   # Peered VPC 2
  ]

  # Optional: Allow additional CIDRs to reach the internal ALB
  # Useful for VPN, Transit Gateway, or other private networks
  # that are not in the VPC or peered VPCs
  alb_internal_allowed_cidrs = [
    "10.200.0.0/16",  # Corporate VPN
  ]
  alb_internal_allowed_ipv6_cidrs = [
    "fd00::/64",      # Internal IPv6 range
  ]
  ```

  **Use Case:**

  * Public access required (e.g., ChatGPT, external integrations)
  * Private network access for internal services (stays within VPC/peering)
  * Single domain name (`runlayer.example.com`) resolves differently based on network context

  **How it works:**

  * Public DNS → Public ALB (internet traffic)
  * Private DNS → Internal ALB (VPC/peered traffic)
  * ECS services register with both ALBs
  * WAF applies only to public ALB
</Accordion>

<Accordion title="VPC Peering for Cross-VPC Connectivity">
  <Warning>
    **Security Note:** VPC peering connections require `peer_owner_id` to be specified for all connections. This validation ensures you only accept connections from known and trusted AWS accounts. Connections are automatically accepted after validation.
  </Warning>

  Enable VPC peering to allow traffic between the Runlayer VPC and other VPCs (for example, a customer's existing VPC or internal services VPC).

  ```hcl theme={null}
  # Accept and configure VPC peering connections
  vpc_peering_connections = {
    "customer-vpc" = {
      peering_connection_id = "pcx-0abc123def456"  # VPC peering connection ID from the initiating side
      peer_vpc_cidr         = "172.16.0.0/16"      # CIDR block of the peer VPC
      peer_owner_id         = "123456789012"        # AWS account ID of the peer (REQUIRED)
      peer_region           = "us-east-1"           # AWS region of the peer (optional)
    }
  }

  # Optional: For dual ALB setup, associate private hosted zone with peered VPC
  enable_dual_alb = true
  private_hosted_zone_additional_vpc_ids = ["vpc-customer123"]
  ```

  **Security Features:**

  * ✅ **Required peer validation** - All connections must specify `peer_owner_id`
  * ✅ **Automatic acceptance after validation** - Validation is the security control
  * ✅ **Automatic routing** - Routes configured only for validated connections
  * ✅ **Security group integration** - Backend allows traffic from validated peer CIDRs

  **Prerequisites:**

  1. The peer VPC must initiate the peering connection first
  2. You need the peering connection ID (`pcx-xxxxx`)
  3. You need the peer VPC's CIDR block
  4. **You MUST provide the peer AWS account ID** for security validation

  **Use Cases:**

  * Connecting to a customer's existing VPC for internal API access
  * Multi-VPC architectures with centralized services
  * Hybrid cloud setups with on-premises connectivity

  <Note>
    VPC peering only works when the module creates the VPC (not with `existing_vpc_id`).
  </Note>
</Accordion>

<Accordion title="Service Scaling">
  ```hcl theme={null}
  services_configurations = {
    "backend" = {
      desired_count     = 3             # Number of instances
      min_capacity      = 2             # Min for auto-scaling
      max_capacity      = 10            # Max for auto-scaling
      cpu              = 2048           # CPU units (1024 = 1 vCPU)
      memory           = 4096           # Memory in MB

      # Auto-scaling thresholds
      cpu_auto_scalling_target_value    = 70    # Scale at 70% CPU
      memory_auto_scalling_target_value = 80    # Scale at 80% memory
    }

    "frontend" = {
      desired_count = 2
      cpu          = 512
      memory       = 1024
    }
  }
  ```
</Accordion>

<Accordion title="Network Configuration">
  ```hcl theme={null}
  cidr = "10.0.0.0/16" # VPC CIDR block
  region_az = ["us-east-1a", "us-east-1b", "us-east-1c"]
  public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  private_subnets = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"]
  database_subnets = ["10.0.7.0/24", "10.0.8.0/24", "10.0.9.0/24"]
  ```
</Accordion>

<Accordion title="Monitoring & Alerting">
  ```hcl theme={null}
  # Enable CloudWatch monitoring and alerting
  enable_monitoring = true              # Create CloudWatch alarms
  slack_channel_id = "C1234567890"      # Your #alerts channel ID (enables Slack alerts)
  slack_team_id = "T1234567890"         # Your Slack workspace ID

  # ECS Container Insights: omit or set null to use the environment default
  # (production → "enabled", staging/development → "disabled"). Override with
  # "enhanced", "enabled", or "disabled" when needed.
  container_insights = "enabled"
  ```

  **Automatic CloudWatch alarms created for:**

  * ECS Services: CPU utilization (80%), Memory utilization (85%)
  * RDS Database: CPU utilization (80%), Connection count (50), Freeable Memory, Disk Queue Depth, Read/Write IOPS, Free Storage Space
  * Redis Cache: CPU utilization (80%), Memory utilization (85%)
  * Load Balancer: Response time (5s), Unhealthy targets, 5XX error count
  * VPC: Flow logs for network traffic analysis and security monitoring

  **Enhanced monitoring capabilities include:**

  * Configurable alarm thresholds for all RDS metrics
  * 365-day log retention for ECS services and prestart containers
  * VPC Flow Logs with customizable traffic type monitoring
  * ALB 5XX error tracking with configurable thresholds

  **CloudWatch monitoring** - comprehensive monitoring with configurable alarms for all infrastructure components.

  **Advanced RDS monitoring configuration:**

  ```hcl theme={null}
  rds_alarm_config = {
    FreeableMemory = {
      period    = 300
      threshold = 268435456 # 256MB
      unit      = "Bytes"
    }
    DiskQueueDepth = {
      period    = 300
      threshold = 5
      unit      = "Count"
    }
    WriteIOPS = {
      period    = 300
      threshold = 1000
      unit      = "Count"
    }
    ReadIOPS = {
      period    = 300
      threshold = 1000
      unit      = "Count"
    }
    Storage = {
      period    = 300
      threshold = 107374182400 # 100GB
      unit      = "Bytes"
    }
  }

  # ALB 5XX error monitoring
  alb_5xx_alarm_period    = 300  # 5 minute period
  alb_5xx_alarm_threshold = 1    # Alert on any 5XX error
  ```
</Accordion>

<Accordion title="Optional Services">
  ```hcl theme={null}
  # Environment Variables

  env_vars = {
  ENVIRONMENT = "production"
  COMPANY = "YourCompany"
  }

  ```
</Accordion>

<Accordion title="Agent Sandbox (Optional)">
  ```hcl theme={null}
  # Optional: Lambda memory for Agent Sandbox Runner (default: 4096 MB)
  # Some AWS accounts cap at 3008 MB; valid range: 128–10240
  runlayer_agent_sandbox_memory_size = 4096

  # Optional: restrict Agent Sandbox Lambda egress at the security group.
  # Defaults to all IPv4/IPv6 egress. Set [] to deny all egress.
  agent_sandbox_security_group_egress_rules = [
    {
      description      = "Allow HTTPS to approved network"
      from_port        = 443
      to_port          = 443
      protocol         = "tcp"
      cidr_blocks      = ["10.0.0.0/16"] # pragma: allowlist secret
      ipv6_cidr_blocks = []
      prefix_list_ids  = []
      security_groups  = []
    }
  ]
  ```

  By default, Agent Sandbox routes LLM calls through the backend Anthropic proxy. To use OpenAI or an OpenAI-compatible gateway, add a provider in **Settings → AI Providers** (no Terraform change needed). The Agent model dropdown then shows models from all configured providers.
</Accordion>

## Prerequisites

<Steps>
  <Step title="Install Tools">
    ```bash theme={null}
    # AWS CLI
    brew install awscli
    aws configure

    # Terraform

    brew install terraform
    terraform version

    ```
  </Step>

  <Step title="AWS Requirements">
    * AWS account with sufficient permissions
    * **Domain name you control** (for DNS records and SSL)
    * Adequate service quotas (VPC, RDS, ECS)

    <Note>
      By default, the module looks up an existing wildcard ACM certificate (e.g., `*.staging.runlayer.com`). If no wildcard certificate exists, set `enable_acm_dns_validation = true` to create one automatically with DNS validation.
    </Note>
  </Step>

  <Step title="SSL Certificate (Enterprise Only)">
    ```bash theme={null}
    # Optional: Request certificate in ACM
    aws acm request-certificate \
      --domain-name "*.yourcompany.com" \
      --validation-method DNS \
      --region us-east-1

    # Get Route53 hosted zone ID
    aws route53 list-hosted-zones-by-name \
      --dns-name yourcompany.com
    ```
  </Step>
</Steps>

## Deployment

### 1. Get Infrastructure Code

```bash theme={null}
# Clone the infrastructure repository
git clone https://github.com/runlayer/runlayer-infra.git
cd runlayer-infra

# OR use the subtree in your main project
cd infra
```

### 2. Configure

```bash theme={null}
# Minimal configuration (recommended)
cp minimal.tfvars.example production.tfvars

# OR Enterprise configuration
cp enterprise.tfvars.example production.tfvars
```

### 3. Deploy

```bash theme={null}
terraform init
terraform plan -var-file="production.tfvars"
terraform apply -var-file="production.tfvars"
```

<Note>
  Deployment takes 15-20 minutes. SSL certificate validation may add 5-10
  minutes.
</Note>

### 4. Automated Database Setup

**Database initialization is completely automated:**

```mermaid theme={null}
sequenceDiagram
    participant TF as Terraform
    participant ECS as ECS Service
    participant PC as Prestart Container
    participant DB as Aurora PostgreSQL
    participant BC as Backend Container

    TF->>ECS: Deploy task definition
    ECS->>PC: Start prestart container
    PC->>DB: Wait for database availability
    PC->>DB: Run Alembic migrations
    PC->>DB: Create initial data & superuser
    PC->>ECS: Signal completion
    ECS->>BC: Start backend container
    BC->>DB: Application ready
```

**What happens automatically:**

* **Database Connection**: Prestart container waits for Aurora PostgreSQL to be ready
* **Schema Migration**: Runs `alembic upgrade head` to apply latest database schema
* **Logging**: All setup activity logged to CloudWatch under `prestart-logs-[environment]`
* **Error Handling**: Backend won't start if database setup fails

**Secrets are automatically generated:**

* Database password and secret keys are created securely

### 5. Update Application Secrets (Optional)

```bash theme={null}
# Secrets are automatically generated during deployment!
# Database password and secret keys are created securely.

# If you need to update any secrets after deployment:
aws secretsmanager update-secret \
  --secret-id "anysource-production-app-secrets-PROD2024" \
  --secret-string '{
    "CUSTOM_API_KEY": "your-custom-value"
  }'
```

### 6. Verify Deployment

```bash theme={null}
# Get application URL
terraform output alb_dns_name

# Test health endpoint
curl https://your-domain.com/api/v1/utils/health-check/
```

## Architecture

```mermaid theme={null}
graph TB
    Internet[Internet] --> ALB[Application Load Balancer]
    ALB --> ECS[ECS Fargate Cluster]

    subgraph "ECS Services"
        Backend[Backend Service<br/>2048 CPU / 4096 MB]
        Frontend[Frontend Service<br/>512 CPU / 1024 MB]
    end

    ECS --> Backend
    ECS --> Frontend

    Backend --> RDS[(Aurora PostgreSQL<br/>2-16 ACUs)]
    Backend --> Redis[(ElastiCache Redis)]

    subgraph "Monitoring & Alerting"
        CloudWatch[CloudWatch Alarms]
    end

    ECS -.-> CloudWatch
    RDS -.-> CloudWatch
    Redis -.-> CloudWatch
    ALB -.-> CloudWatch

    subgraph "VPC - 10.0.0.0/16"
        subgraph "Public Subnets"
            ALB
        end
        subgraph "Private Subnets"
            ECS
        end
        subgraph "Database Subnets"
            RDS
            Redis
        end
    end
```

## Cost Estimation

| Component                | Minimal       | Enterprise    |
| ------------------------ | ------------- | ------------- |
| **ECS Services**         | \$50-80       | \$150-300     |
| **Aurora Database**      | \$30-60       | \$100-400     |
| **ElastiCache**          | \$15-30       | \$50-150      |
| **Load Balancer**        | \$20-25       | \$20-25       |
| **Monitoring**           | \$5-10        | \$10-25       |
| **Other (NAT, Storage)** | \$10-20       | \$30-50       |
| **Monthly Total**        | **\$130-225** | **\$360-950** |

<Note>
  Costs vary by region and usage. Use [AWS Pricing
  Calculator](https://calculator.aws/) for precise estimates.
</Note>

## Common Use Cases

### Private Enterprise Deployment

```hcl theme={null}
alb_access_type = "private"
alb_allowed_cidrs = ["10.0.0.0/8"]  # Corporate network only

# Additional WAF protection for corporate network
waf_enable_ip_allowlisting = true
waf_allowlist_ipv4_cidrs = [
  "10.100.0.0/16",    # Corporate HQ
  "10.200.0.0/16",    # Regional offices
]
waf_allowlist_ipv6_cidrs = [
  "2001:db8:1::/48",  # Corporate HQ IPv6
]

database_config = {
  publicly_accessible = false
  backup_retention = 30
}
```

### High Availability Production

```hcl theme={null}
database_config = {
  min_capacity = 8
  max_capacity = 64
}
services_configurations = {
  "backend" = {
    desired_count = 4
    max_capacity = 20
  }
}
```

### Development Environment

```hcl theme={null}
environment = "development"
database_config = {
  min_capacity = 2
  max_capacity = 4
  backup_retention = 1
}
services_configurations = {
  "backend" = { desired_count = 1 }
  "frontend" = { desired_count = 1 }
}
# Disable monitoring for development
enable_monitoring = false
```

### Production with Monitoring

```hcl theme={null}
environment = "production"
enable_monitoring = true
slack_channel_id = "C1234567890"
slack_team_id = "T1234567890"

database_config = {
  min_capacity = 4
  max_capacity = 32
  backup_retention = 30
}
services_configurations = {
  "backend" = {
    desired_count = 3
    max_capacity = 15
  }
}
```

## Troubleshooting

<Accordion title="SSL Certificate Issues">
  **Wildcard lookup fails** (default behavior):

  ```bash theme={null}
  # Check if wildcard certificate exists
  aws acm list-certificates --query "CertificateSummaryList[?contains(DomainName, '*')]"

  # The module derives: your-domain.example.com → *.example.com
  # Ensure a certificate exists for the wildcard domain
  ```

  **DNS validation fails** (when `enable_acm_dns_validation = true`):

  ```bash theme={null}
  # Check certificate status
  aws acm describe-certificate --certificate-arn your-cert-arn

  # Verify DNS records exist
  dig _acme-challenge.your-domain.com CNAME
  ```
</Accordion>

<Accordion title="Application Not Accessible">
  **Solution**: Check security groups and target health:

  ```bash theme={null}
  # Check target group health
  aws elbv2 describe-target-health --target-group-arn your-tg-arn

  # Check ECS service status
  aws ecs describe-services --cluster anysource-production
  ```
</Accordion>

<Accordion title="High Costs">
  **Solution**: Optimize resource sizing:

  * Use `environment = "development"` for testing
  * Reduce database `min_capacity` and `max_capacity`
  * Lower service `desired_count` and CPU/memory
  * Set shorter `backup_retention` periods
</Accordion>

<Accordion title="Monitoring Alerts Not Working">
  **Solution**: Check CloudWatch alarm configuration:

  ```bash theme={null}
  # Check CloudWatch alarms exist
  aws cloudwatch describe-alarms --state-value ALARM

  # Check alarm actions and thresholds
  aws cloudwatch describe-alarms --alarm-names "your-alarm-name"
  ```

  **Common issues:**

  * CloudWatch alarm thresholds too high
  * Alarm actions disabled
  * Missing CloudWatch permissions
</Accordion>

## ECS vs EKS: Which to Choose?

| Factor         | ECS (This Guide)                | EKS                                  |
| -------------- | ------------------------------- | ------------------------------------ |
| **Complexity** | Lower - Simpler to manage       | Higher - Kubernetes expertise needed |
| **Cost**       | \$130-225/month                 | \$200-800/month                      |
| **Scaling**    | Auto-scaling with Fargate       | More granular control                |
| **Ecosystem**  | AWS-specific                    | Kubernetes ecosystem                 |
| **Best For**   | Simpler deployments, AWS-native | Complex workloads, multi-cloud       |

## Next Steps

<CardGroup cols={2}>
  <Card title="EKS + Terraform" icon="kubernetes" href="/deployment/eks-terraform">
    Deploy on Kubernetes using EKS with Terraform for more advanced scenarios
  </Card>

  <Card title="Helm + Kubernetes" icon="helm" href="/deployment/helm">
    Deploy application using Helm charts (after provisioning EKS infrastructure)
  </Card>
</CardGroup>

```
```
