docs/self-hosting/deployment-options/aws-native.mdx
Learn how to deploy Infisical on Amazon Web Services using Elastic Container Service (ECS) with Fargate. This guide covers setting up Infisical in a production-ready AWS environment using Amazon RDS (PostgreSQL) for the database, Amazon ElastiCache (Redis) for caching, and an Application Load Balancer (ALB) for routing traffic.
The following are minimum requirements for running Infisical on AWS ECS:
| Component | Minimum | Recommended (Production) |
|---|---|---|
| ECS Task vCPU | 0.25 vCPU | 1 vCPU |
| ECS Task Memory | 512 MB | 2 GB |
| RDS Instance | db.t3.micro | db.t3.small or larger |
| ElastiCache Node | cache.t3.micro | cache.t3.small or larger |
For production deployments with many users or secrets, increase these values accordingly.
VPC and Subnets:
NAT Gateway:
Security Groups: Create the following security groups:
| Security Group | Inbound Rules | Purpose |
|---|---|---|
| ALB SG | 80, 443 from 0.0.0.0/0 | Allow HTTP/HTTPS from internet |
| ECS Tasks SG | 8080 from ALB SG | Allow traffic from ALB only |
| RDS SG | 5432 from ECS Tasks SG | Allow PostgreSQL from ECS |
| Redis SG | 6379 from ECS Tasks SG | Allow Redis from ECS |
Verify: After creating the VPC, confirm you have:
# Verify VPC and subnets
aws ec2 describe-vpcs --filters "Name=tag:Name,Values=*infisical*"
aws ec2 describe-subnets --filters "Name=vpc-id,Values=<vpc-id>"
Amazon RDS (PostgreSQL):
aws rds create-db-instance \
--db-instance-identifier infisical-db \
--db-instance-class db.t3.small \
--engine postgres \
--engine-version 14 \
--master-username infisical \
--master-user-password <your-secure-password> \
--allocated-storage 20 \
--db-name infisical \
--vpc-security-group-ids <rds-sg-id> \
--db-subnet-group-name <db-subnet-group> \
--multi-az \
--backup-retention-period 7 \
--no-publicly-accessible
Amazon ElastiCache (Redis):
aws elasticache create-replication-group \
--replication-group-id infisical-redis \
--replication-group-description "Redis for Infisical" \
--engine redis \
--cache-node-type cache.t3.small \
--num-cache-clusters 2 \
--automatic-failover-enabled \
--multi-az-enabled \
--at-rest-encryption-enabled \
--transit-encryption-enabled \
--security-group-ids <redis-sg-id> \
--cache-subnet-group-name <cache-subnet-group>
Verify: Wait for both services to become available:
# Check RDS status
aws rds describe-db-instances --db-instance-identifier infisical-db --query 'DBInstances[0].DBInstanceStatus'
# Check ElastiCache status
aws elasticache describe-replication-groups --replication-group-id infisical-redis --query 'ReplicationGroups[0].Status'
Note the connection endpoints:
postgresql://infisical:<password>@<rds-endpoint>:5432/infisicalredis://<elasticache-endpoint>:6379
</Step>
Generate secrets:
# Generate ENCRYPTION_KEY (16-byte hex string)
ENCRYPTION_KEY=$(openssl rand -hex 16)
echo "ENCRYPTION_KEY: $ENCRYPTION_KEY"
# Generate AUTH_SECRET (32-byte base64 string)
AUTH_SECRET=$(openssl rand -base64 32)
echo "AUTH_SECRET: $AUTH_SECRET"
Verify: Confirm secrets are stored:
# For Parameter Store
aws ssm get-parameters --names "/infisical/ENCRYPTION_KEY" "/infisical/AUTH_SECRET" --with-decryption
# For Secrets Manager
aws secretsmanager list-secrets --filters Key=name,Values=infisical
Task Execution Role (for ECS agent operations):
Create a file named ecs-task-execution-trust-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
Create the role and attach policies:
# Create the execution role
aws iam create-role \
--role-name InfisicalECSExecutionRole \
--assume-role-policy-document file://ecs-task-execution-trust-policy.json
# Attach the managed policy
aws iam attach-role-policy \
--role-name InfisicalECSExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Task Role (for application access to AWS services):
Create a file named infisical-task-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssm:GetParameter",
"ssm:GetParameters",
"ssm:GetParametersByPath"
],
"Resource": "arn:aws:ssm:*:*:parameter/infisical/*"
},
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:*:*:secret:infisical/*"
},
{
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"kms:ViaService": "ssm.*.amazonaws.com"
}
}
}
]
}
Create the task role:
# Create the task role
aws iam create-role \
--role-name InfisicalTaskRole \
--assume-role-policy-document file://ecs-task-execution-trust-policy.json
# Create and attach the custom policy
aws iam put-role-policy \
--role-name InfisicalTaskRole \
--policy-name InfisicalSecretsAccess \
--policy-document file://infisical-task-policy.json
Enable ECS Exec (for container debugging):
Add the following to the task role policy to enable aws ecs execute-command:
{
"Effect": "Allow",
"Action": [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
"Resource": "*"
}
Verify: Confirm roles are created:
aws iam get-role --role-name InfisicalECSExecutionRole
aws iam get-role --role-name InfisicalTaskRole
Create Task Definition:
Create a file named infisical-task-definition.json:
{
"family": "infisical",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"executionRoleArn": "arn:aws:iam::<account-id>:role/InfisicalECSExecutionRole",
"taskRoleArn": "arn:aws:iam::<account-id>:role/InfisicalTaskRole",
"containerDefinitions": [
{
"name": "infisical",
"image": "infisical/infisical:v0.151.0",
"essential": true,
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp"
}
],
"environment": [
{ "name": "HOST", "value": "0.0.0.0" },
{ "name": "SITE_URL", "value": "https://infisical.example.com" }
],
"secrets": [
{
"name": "ENCRYPTION_KEY",
"valueFrom": "arn:aws:ssm:<region>:<account-id>:parameter/infisical/ENCRYPTION_KEY"
},
{
"name": "AUTH_SECRET",
"valueFrom": "arn:aws:ssm:<region>:<account-id>:parameter/infisical/AUTH_SECRET"
},
{
"name": "DB_CONNECTION_URI",
"valueFrom": "arn:aws:ssm:<region>:<account-id>:parameter/infisical/DB_CONNECTION_URI"
},
{
"name": "REDIS_URL",
"valueFrom": "arn:aws:ssm:<region>:<account-id>:parameter/infisical/REDIS_URL"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/infisical",
"awslogs-region": "<region>",
"awslogs-stream-prefix": "infisical",
"awslogs-create-group": "true"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "wget -q --spider http://localhost:8080/api/status || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
}
}
]
}
Register the task definition:
aws ecs register-task-definition --cli-input-json file://infisical-task-definition.json
Verify: Confirm task definition is registered:
aws ecs describe-task-definition --task-definition infisical
Create Target Group:
aws elbv2 create-target-group \
--name infisical-tg \
--protocol HTTP \
--port 8080 \
--vpc-id <vpc-id> \
--target-type ip \
--health-check-path /api/status \
--health-check-interval-seconds 30 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 3
Create HTTP Listener:
aws elbv2 create-listener \
--load-balancer-arn <alb-arn> \
--protocol HTTP \
--port 80 \
--default-actions Type=forward,TargetGroupArn=<target-group-arn>
Verify: Check ALB is active:
aws elbv2 describe-load-balancers --names infisical-alb --query 'LoadBalancers[0].State.Code'
Note the ALB DNS name for accessing Infisical:
aws elbv2 describe-load-balancers --names infisical-alb --query 'LoadBalancers[0].DNSName' --output text
aws ecs create-service \
--cluster infisical-cluster \
--service-name infisical-service \
--task-definition infisical \
--desired-count 2 \
--launch-type FARGATE \
--platform-version LATEST \
--network-configuration "awsvpcConfiguration={subnets=[<private-subnet-1>,<private-subnet-2>],securityGroups=[<ecs-tasks-sg-id>],assignPublicIp=DISABLED}" \
--load-balancers "targetGroupArn=<target-group-arn>,containerName=infisical,containerPort=8080" \
--enable-execute-command \
--deployment-configuration "minimumHealthyPercent=50,maximumPercent=200"
Verify deployment:
# Check service status
aws ecs describe-services --cluster infisical-cluster --services infisical-service --query 'services[0].status'
# Watch task status
aws ecs list-tasks --cluster infisical-cluster --service-name infisical-service
aws ecs describe-tasks --cluster infisical-cluster --tasks <task-arn>
# Check target health
aws elbv2 describe-target-health --target-group-arn <target-group-arn>
Once tasks are running and healthy, access Infisical via the ALB DNS name:
curl http://<alb-dns-name>/api/status
After completing the above steps, your Infisical instance should be running on AWS. Visit http://<alb-dns-name> to access the Infisical web interface and create your admin account.
**1. Request an SSL Certificate:**
```bash
aws acm request-certificate \
--domain-name infisical.example.com \
--validation-method DNS \
--region <region>
```
**2. Validate the certificate** by adding the CNAME record to your DNS (Route 53 or external DNS).
**3. Create HTTPS Listener:**
```bash
aws elbv2 create-listener \
--load-balancer-arn <alb-arn> \
--protocol HTTPS \
--port 443 \
--ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
--certificates CertificateArn=<acm-certificate-arn> \
--default-actions Type=forward,TargetGroupArn=<target-group-arn>
```
**4. Redirect HTTP to HTTPS:**
```bash
aws elbv2 modify-listener \
--listener-arn <http-listener-arn> \
--default-actions Type=redirect,RedirectConfig="{Protocol=HTTPS,Port=443,StatusCode=HTTP_301}"
```
**5. Create Route 53 Record:**
```bash
aws route53 change-resource-record-sets \
--hosted-zone-id <hosted-zone-id> \
--change-batch '{
"Changes": [{
"Action": "CREATE",
"ResourceRecordSet": {
"Name": "infisical.example.com",
"Type": "A",
"AliasTarget": {
"HostedZoneId": "<alb-hosted-zone-id>",
"DNSName": "<alb-dns-name>",
"EvaluateTargetHealth": true
}
}
}]
}'
```
**6. Update SITE_URL** in your ECS task definition to use `https://infisical.example.com`.
**1. Verify your domain in SES:**
```bash
aws ses verify-domain-identity --domain example.com
```
**2. Create SMTP credentials:**
- Go to AWS SES Console > SMTP Settings > Create SMTP Credentials
- Note the SMTP username and password
**3. Add SMTP environment variables** to your ECS task definition:
```json
{
"environment": [
{ "name": "SMTP_HOST", "value": "email-smtp.<region>.amazonaws.com" },
{ "name": "SMTP_PORT", "value": "587" },
{ "name": "SMTP_SECURE", "value": "false" },
{ "name": "SMTP_FROM_ADDRESS", "value": "[email protected]" },
{ "name": "SMTP_FROM_NAME", "value": "Infisical" }
],
"secrets": [
{ "name": "SMTP_USERNAME", "valueFrom": "arn:aws:ssm:<region>:<account-id>:parameter/infisical/SMTP_USERNAME" },
{ "name": "SMTP_PASSWORD", "valueFrom": "arn:aws:ssm:<region>:<account-id>:parameter/infisical/SMTP_PASSWORD" }
]
}
```
**4. Request production access** if you're in the SES sandbox (limited to verified emails only).
| SMTP Provider | Host | Port |
|---------------|------|------|
| AWS SES | email-smtp.{region}.amazonaws.com | 587 |
| SendGrid | smtp.sendgrid.net | 587 |
| Mailgun | smtp.mailgun.org | 587 |
**Create VPC Endpoints:**
```bash
# ECR API endpoint
aws ec2 create-vpc-endpoint \
--vpc-id <vpc-id> \
--service-name com.amazonaws.<region>.ecr.api \
--vpc-endpoint-type Interface \
--subnet-ids <private-subnet-1> <private-subnet-2> \
--security-group-ids <vpc-endpoint-sg>
# ECR Docker endpoint
aws ec2 create-vpc-endpoint \
--vpc-id <vpc-id> \
--service-name com.amazonaws.<region>.ecr.dkr \
--vpc-endpoint-type Interface \
--subnet-ids <private-subnet-1> <private-subnet-2> \
--security-group-ids <vpc-endpoint-sg>
# S3 Gateway endpoint (for ECR image layers)
aws ec2 create-vpc-endpoint \
--vpc-id <vpc-id> \
--service-name com.amazonaws.<region>.s3 \
--vpc-endpoint-type Gateway \
--route-table-ids <private-route-table-id>
# CloudWatch Logs endpoint
aws ec2 create-vpc-endpoint \
--vpc-id <vpc-id> \
--service-name com.amazonaws.<region>.logs \
--vpc-endpoint-type Interface \
--subnet-ids <private-subnet-1> <private-subnet-2> \
--security-group-ids <vpc-endpoint-sg>
```
**Push Infisical image to ECR:**
```bash
# Create ECR repository
aws ecr create-repository --repository-name infisical
# Login to ECR
aws ecr get-login-password --region <region> | docker login --username AWS --password-stdin <account-id>.dkr.ecr.<region>.amazonaws.com
# Pull and push image
docker pull infisical/infisical:v0.151.0
docker tag infisical/infisical:v0.151.0 <account-id>.dkr.ecr.<region>.amazonaws.com/infisical:v0.151.0
docker push <account-id>.dkr.ecr.<region>.amazonaws.com/infisical:v0.151.0
```
Update your task definition to use the ECR image URL.
**Check migration status:**
```bash
# Exec into a running container
aws ecs execute-command \
--cluster infisical-cluster \
--task <task-id> \
--container infisical \
--interactive \
--command "/bin/sh"
# Inside the container, check migration status
npm run migration:status
```
**Run migrations manually:**
```bash
# Inside the container
npm run migration:latest
```
**Rollback migrations:**
```bash
# Inside the container
npm run migration:rollback
```
<Warning>
Always back up your database before running migrations manually. Take an RDS snapshot before any upgrade.
</Warning>
**Prerequisites:**
- ECS service must have `--enable-execute-command` flag
- Task role must have SSM permissions (included in IAM setup above)
- AWS CLI Session Manager plugin installed locally
**Install Session Manager plugin:**
```bash
# macOS
brew install session-manager-plugin
# Linux
curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o "session-manager-plugin.deb"
sudo dpkg -i session-manager-plugin.deb
```
**Exec into a container:**
```bash
# Get task ID
TASK_ID=$(aws ecs list-tasks --cluster infisical-cluster --service-name infisical-service --query 'taskArns[0]' --output text | cut -d'/' -f3)
# Start interactive session
aws ecs execute-command \
--cluster infisical-cluster \
--task $TASK_ID \
--container infisical \
--interactive \
--command "/bin/sh"
```
**Common debugging commands:**
```bash
# Check environment variables
env | grep -E "DB_|REDIS_|SITE_"
# Test database connectivity
nc -zv <rds-endpoint> 5432
# Test Redis connectivity
nc -zv <elasticache-endpoint> 6379
# Check application logs
cat /app/logs/*.log
```
**Export to S3 (for cross-region DR):**
```bash
aws rds start-export-task \
--export-task-identifier infisical-export-$(date +%Y%m%d) \
--source-arn arn:aws:rds:<region>:<account-id>:snapshot:infisical-snapshot \
--s3-bucket-name infisical-backups \
--iam-role-arn arn:aws:iam::<account-id>:role/RDSExportRole \
--kms-key-id <kms-key-id>
```
**Encryption Key Backup:**
Store your `ENCRYPTION_KEY` in multiple secure locations:
- AWS Secrets Manager (already done)
- Offline secure storage (e.g., hardware security module, safe deposit box)
- Secondary AWS region
<Warning>
Without the `ENCRYPTION_KEY`, encrypted secrets cannot be recovered even with a database restore.
</Warning>
**2. Update the task definition** with the new image tag:
```bash
# Edit infisical-task-definition.json with new version
# Then register the new revision
aws ecs register-task-definition --cli-input-json file://infisical-task-definition.json
```
**3. Update the service:**
```bash
aws ecs update-service \
--cluster infisical-cluster \
--service infisical-service \
--task-definition infisical:<new-revision>
```
**4. Monitor the deployment:**
```bash
aws ecs describe-services --cluster infisical-cluster --services infisical-service --query 'services[0].deployments'
```
**5. Verify health:**
```bash
curl https://infisical.example.com/api/status
```
**Rollback if needed:**
```bash
aws ecs update-service \
--cluster infisical-cluster \
--service infisical-service \
--task-definition infisical:<previous-revision>
```
**CloudWatch Alarms:**
```bash
# High CPU alarm
aws cloudwatch put-metric-alarm \
--alarm-name infisical-high-cpu \
--metric-name CPUUtilization \
--namespace AWS/ECS \
--statistic Average \
--period 300 \
--threshold 80 \
--comparison-operator GreaterThanThreshold \
--dimensions Name=ClusterName,Value=infisical-cluster Name=ServiceName,Value=infisical-service \
--evaluation-periods 2 \
--alarm-actions <sns-topic-arn>
# Unhealthy target alarm
aws cloudwatch put-metric-alarm \
--alarm-name infisical-unhealthy-targets \
--metric-name UnHealthyHostCount \
--namespace AWS/ApplicationELB \
--statistic Average \
--period 60 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--dimensions Name=TargetGroup,Value=<target-group-arn-suffix> Name=LoadBalancer,Value=<alb-arn-suffix> \
--evaluation-periods 2 \
--alarm-actions <sns-topic-arn>
```
**Enable Container Insights:**
```bash
aws ecs update-cluster-settings \
--cluster infisical-cluster \
--settings name=containerInsights,value=enabled
```
**Register scalable target:**
```bash
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/infisical-cluster/infisical-service \
--min-capacity 2 \
--max-capacity 10
```
**Create scaling policy (target tracking):**
```bash
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/infisical-cluster/infisical-service \
--policy-name infisical-cpu-scaling \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
},
"ScaleOutCooldown": 60,
"ScaleInCooldown": 120
}'
```
```hcl
# main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
}
# Variables
variable "aws_region" {
default = "us-east-1"
}
variable "environment" {
default = "production"
}
# VPC
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "infisical-vpc"
cidr = "10.0.0.0/16"
azs = ["${var.aws_region}a", "${var.aws_region}b"]
private_subnets = ["10.0.1.0/24", "10.0.2.0/24"]
public_subnets = ["10.0.101.0/24", "10.0.102.0/24"]
enable_nat_gateway = true
single_nat_gateway = var.environment != "production"
}
# Security Groups
resource "aws_security_group" "alb" {
name_prefix = "infisical-alb-"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_security_group" "ecs_tasks" {
name_prefix = "infisical-ecs-"
vpc_id = module.vpc.vpc_id
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.alb.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
# RDS PostgreSQL
module "rds" {
source = "terraform-aws-modules/rds/aws"
version = "~> 6.0"
identifier = "infisical-db"
engine = "postgres"
engine_version = "14"
family = "postgres14"
major_engine_version = "14"
instance_class = "db.t3.small"
allocated_storage = 20
db_name = "infisical"
username = "infisical"
port = 5432
multi_az = var.environment == "production"
db_subnet_group_name = module.vpc.database_subnet_group_name
vpc_security_group_ids = [aws_security_group.rds.id]
backup_retention_period = 7
deletion_protection = var.environment == "production"
}
# ElastiCache Redis
resource "aws_elasticache_replication_group" "redis" {
replication_group_id = "infisical-redis"
description = "Redis for Infisical"
node_type = "cache.t3.small"
num_cache_clusters = var.environment == "production" ? 2 : 1
parameter_group_name = "default.redis7"
port = 6379
automatic_failover_enabled = var.environment == "production"
multi_az_enabled = var.environment == "production"
subnet_group_name = aws_elasticache_subnet_group.redis.name
security_group_ids = [aws_security_group.redis.id]
at_rest_encryption_enabled = true
transit_encryption_enabled = true
}
# ECS Cluster
resource "aws_ecs_cluster" "infisical" {
name = "infisical-cluster"
setting {
name = "containerInsights"
value = "enabled"
}
}
# ECS Service (simplified - full implementation requires task definition, ALB, etc.)
# Expand this example with ECS task definitions, services, ALB, and target groups
```
<Note>
This is a simplified example to get you started. For a complete deployment, you'll need to add ECS task definitions, services, ALB configuration, and target groups. Consider using community Terraform modules for ECS or adapting this example to your infrastructure standards.
</Note>
```yaml
# infisical-cloudformation.yaml
AWSTemplateFormatVersion: '2010-09-09'
Description: 'Infisical on AWS ECS Fargate'
Parameters:
Environment:
Type: String
Default: production
AllowedValues: [development, staging, production]
InfisicalVersion:
Type: String
Default: v0.151.0
DomainName:
Type: String
Description: Domain name for Infisical (e.g., infisical.example.com)
Conditions:
IsProduction: !Equals [!Ref Environment, production]
Resources:
# VPC
VPC:
Type: AWS::EC2::VPC
Properties:
CidrBlock: 10.0.0.0/16
EnableDnsHostnames: true
EnableDnsSupport: true
Tags:
- Key: Name
Value: !Sub infisical-vpc-${Environment}
# Public Subnets
PublicSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [0, !GetAZs '']
CidrBlock: 10.0.1.0/24
MapPublicIpOnLaunch: true
PublicSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [1, !GetAZs '']
CidrBlock: 10.0.2.0/24
MapPublicIpOnLaunch: true
# Private Subnets
PrivateSubnet1:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [0, !GetAZs '']
CidrBlock: 10.0.10.0/24
PrivateSubnet2:
Type: AWS::EC2::Subnet
Properties:
VpcId: !Ref VPC
AvailabilityZone: !Select [1, !GetAZs '']
CidrBlock: 10.0.11.0/24
# Internet Gateway
InternetGateway:
Type: AWS::EC2::InternetGateway
AttachGateway:
Type: AWS::EC2::VPCGatewayAttachment
Properties:
VpcId: !Ref VPC
InternetGatewayId: !Ref InternetGateway
# NAT Gateway
NATGatewayEIP:
Type: AWS::EC2::EIP
DependsOn: AttachGateway
NATGateway:
Type: AWS::EC2::NatGateway
Properties:
AllocationId: !GetAtt NATGatewayEIP.AllocationId
SubnetId: !Ref PublicSubnet1
# ECS Cluster
ECSCluster:
Type: AWS::ECS::Cluster
Properties:
ClusterName: !Sub infisical-cluster-${Environment}
ClusterSettings:
- Name: containerInsights
Value: enabled
# ECS Task Definition
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: infisical
NetworkMode: awsvpc
RequiresCompatibilities: [FARGATE]
Cpu: '1024'
Memory: '2048'
ExecutionRoleArn: !GetAtt ECSExecutionRole.Arn
TaskRoleArn: !GetAtt ECSTaskRole.Arn
ContainerDefinitions:
- Name: infisical
Image: !Sub infisical/infisical:${InfisicalVersion}
Essential: true
PortMappings:
- ContainerPort: 8080
Protocol: tcp
Environment:
- Name: HOST
Value: '0.0.0.0'
- Name: SITE_URL
Value: !Sub https://${DomainName}
Secrets:
- Name: ENCRYPTION_KEY
ValueFrom: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/infisical/ENCRYPTION_KEY
- Name: AUTH_SECRET
ValueFrom: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/infisical/AUTH_SECRET
- Name: DB_CONNECTION_URI
ValueFrom: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/infisical/DB_CONNECTION_URI
- Name: REDIS_URL
ValueFrom: !Sub arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/infisical/REDIS_URL
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: infisical
HealthCheck:
Command: ['CMD-SHELL', 'wget -q --spider http://localhost:8080/api/status || exit 1']
Interval: 30
Timeout: 5
Retries: 3
StartPeriod: 60
# ECS Service
ECSService:
Type: AWS::ECS::Service
DependsOn: ALBListener
Properties:
ServiceName: infisical-service
Cluster: !Ref ECSCluster
TaskDefinition: !Ref TaskDefinition
DesiredCount: !If [IsProduction, 2, 1]
LaunchType: FARGATE
EnableExecuteCommand: true
NetworkConfiguration:
AwsvpcConfiguration:
AssignPublicIp: DISABLED
Subnets:
- !Ref PrivateSubnet1
- !Ref PrivateSubnet2
SecurityGroups:
- !Ref ECSSecurityGroup
LoadBalancers:
- ContainerName: infisical
ContainerPort: 8080
TargetGroupArn: !Ref TargetGroup
# Application Load Balancer
ALB:
Type: AWS::ElasticLoadBalancingV2::LoadBalancer
Properties:
Name: !Sub infisical-alb-${Environment}
Scheme: internet-facing
Type: application
Subnets:
- !Ref PublicSubnet1
- !Ref PublicSubnet2
SecurityGroups:
- !Ref ALBSecurityGroup
# CloudWatch Log Group
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub /ecs/infisical-${Environment}
RetentionInDays: 30
Outputs:
ALBDNSName:
Description: ALB DNS Name
Value: !GetAtt ALB.DNSName
ECSClusterName:
Description: ECS Cluster Name
Value: !Ref ECSCluster
```
**Deploy the stack:**
```bash
aws cloudformation create-stack \
--stack-name infisical-stack \
--template-body file://infisical-cloudformation.yaml \
--parameters ParameterKey=DomainName,ParameterValue=infisical.example.com \
--capabilities CAPABILITY_IAM
```
**View CloudWatch logs:**
```bash
aws logs tail /ecs/infisical --follow
```
**Common causes:**
- **Secrets not found:** Verify SSM parameters exist and task role has permissions
- **Image pull failed:** Check ECR permissions or Docker Hub rate limits
- **Insufficient resources:** Increase task CPU/memory or check Fargate capacity
- **Network issues:** Verify NAT Gateway is working and security groups allow egress
# Test database connection
nc -zv <rds-endpoint> 5432
```
**Check security groups:**
- RDS security group must allow inbound 5432 from ECS tasks security group
- ECS tasks security group must allow outbound to RDS
**Verify connection string:**
```bash
aws ssm get-parameter --name /infisical/DB_CONNECTION_URI --with-decryption
```
**Verify health check endpoint:**
```bash
# From inside the container
curl http://localhost:8080/api/status
```
**Common causes:**
- Health check path is wrong (should be `/api/status`)
- Security group doesn't allow ALB to reach ECS tasks on port 8080
- Application is crashing on startup (check logs)
- Health check timeout is too short (increase to 10 seconds)
**Verify DNS resolution:**
```bash
nslookup infisical.example.com
```
**Check ALB security group:**
- Must allow inbound 80/443 from 0.0.0.0/0
**Check SITE_URL:**
- Must match the URL you're accessing (including protocol)
**Verify SMTP credentials:**
- Ensure SMTP username/password are correct
- Check if you're still in SES sandbox (can only send to verified emails)
**Test SMTP connectivity:**
```bash
# From inside container
nc -zv email-smtp.<region>.amazonaws.com 587
```
**Check application logs for email errors:**
```bash
aws logs filter-log-events --log-group-name /ecs/infisical --filter-pattern "smtp OR email OR mail"
```
**Check RDS metrics:**
```bash
aws cloudwatch get-metric-statistics \
--namespace AWS/RDS \
--metric-name CPUUtilization \
--dimensions Name=DBInstanceIdentifier,Value=infisical-db \
--start-time $(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 300 \
--statistics Average
```
**Solutions:**
- Scale ECS tasks horizontally (increase desired count)
- Scale RDS vertically (larger instance class)
- Enable RDS Performance Insights for query analysis
- Consider connection pooling (PgBouncer)
# Check task role has SSM permissions
aws iam get-role-policy --role-name InfisicalTaskRole --policy-name InfisicalSecretsAccess
```
**Check managed agent status:**
```bash
aws ecs describe-tasks --cluster infisical-cluster --tasks <task-arn> --query 'tasks[0].containers[0].managedAgents'
```
**Common fixes:**
- Ensure Session Manager plugin is installed locally
- Verify VPC has route to SSM endpoints (via NAT or VPC endpoint)
- Redeploy service after enabling execute-command