Simplified ECS deployment guide using Oxy’s built-in authentication system with container orchestration
This deployment includes:
Before starting, ensure you have:
# Create VPC
VPC_ID=$(aws ec2 create-vpc \
--cidr-block 10.0.0.0/16 \
--tag-specifications 'ResourceType=vpc,Tags=[{Key=Name,Value=oxy-ecs-simple-vpc}]' \
--query 'Vpc.VpcId' --output text)
# Enable DNS hostnames
aws ec2 modify-vpc-attribute --vpc-id $VPC_ID --enable-dns-hostnames
# Create Internet Gateway
IGW_ID=$(aws ec2 create-internet-gateway \
--tag-specifications 'ResourceType=internet-gateway,Tags=[{Key=Name,Value=oxy-ecs-simple-igw}]' \
--query 'InternetGateway.InternetGatewayId' --output text)
# Attach Internet Gateway to VPC
aws ec2 attach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID
# Create public subnets in multiple AZs
SUBNET_1_ID=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.1.0/24 \
--availability-zone us-west-2a \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=oxy-ecs-simple-subnet-1}]' \
--query 'Subnet.SubnetId' --output text)
SUBNET_2_ID=$(aws ec2 create-subnet \
--vpc-id $VPC_ID \
--cidr-block 10.0.2.0/24 \
--availability-zone us-west-2b \
--tag-specifications 'ResourceType=subnet,Tags=[{Key=Name,Value=oxy-ecs-simple-subnet-2}]' \
--query 'Subnet.SubnetId' --output text)
# Enable auto-assign public IP
aws ec2 modify-subnet-attribute --subnet-id $SUBNET_1_ID --map-public-ip-on-launch
aws ec2 modify-subnet-attribute --subnet-id $SUBNET_2_ID --map-public-ip-on-launch
# Create route table
ROUTE_TABLE_ID=$(aws ec2 create-route-table \
--vpc-id $VPC_ID \
--tag-specifications 'ResourceType=route-table,Tags=[{Key=Name,Value=oxy-ecs-simple-rt}]' \
--query 'RouteTable.RouteTableId' --output text)
# Add route to Internet Gateway
aws ec2 create-route --route-table-id $ROUTE_TABLE_ID --destination-cidr-block 0.0.0.0/0 --gateway-id $IGW_ID
# Associate route table with subnets
aws ec2 associate-route-table --subnet-id $SUBNET_1_ID --route-table-id $ROUTE_TABLE_ID
aws ec2 associate-route-table --subnet-id $SUBNET_2_ID --route-table-id $ROUTE_TABLE_ID
# Create security group for ECS tasks
ECS_SG_ID=$(aws ec2 create-security-group \
--group-name oxy-ecs-simple-tasks-sg \
--description "Security group for Oxy ECS tasks with built-in auth" \
--vpc-id $VPC_ID \
--tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=oxy-ecs-simple-tasks-sg}]' \
--query 'GroupId' --output text)
# Allow HTTP access (port 3000 for Oxy)
aws ec2 authorize-security-group-ingress \
--group-id $ECS_SG_ID \
--protocol tcp \
--port 3000 \
--cidr 0.0.0.0/0
# Allow MCP SSE access (port 8000)
aws ec2 authorize-security-group-ingress \
--group-id $ECS_SG_ID \
--protocol tcp \
--port 8000 \
--cidr 0.0.0.0/0
# Allow NFS access for EFS (port 2049)
aws ec2 authorize-security-group-ingress \
--group-id $ECS_SG_ID \
--protocol tcp \
--port 2049 \
--source-group $ECS_SG_ID
# Create security group for ALB
ALB_SG_ID=$(aws ec2 create-security-group \
--group-name oxy-ecs-simple-alb-sg \
--description "Security group for Oxy ECS ALB" \
--vpc-id $VPC_ID \
--tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=oxy-ecs-simple-alb-sg}]' \
--query 'GroupId' --output text)
# Allow HTTP and HTTPS for ALB
aws ec2 authorize-security-group-ingress \
--group-id $ALB_SG_ID \
--protocol tcp \
--port 80 \
--cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress \
--group-id $ALB_SG_ID \
--protocol tcp \
--port 443 \
--cidr 0.0.0.0/0
# Create security group for EFS
EFS_SG_ID=$(aws ec2 create-security-group \
--group-name oxy-ecs-simple-efs-sg \
--description "Security group for Oxy ECS EFS" \
--vpc-id $VPC_ID \
--tag-specifications 'ResourceType=security-group,Tags=[{Key=Name,Value=oxy-ecs-simple-efs-sg}]' \
--query 'GroupId' --output text)
# Allow NFS access from ECS tasks
aws ec2 authorize-security-group-ingress \
--group-id $EFS_SG_ID \
--protocol tcp \
--port 2049 \
--source-group $ECS_SG_ID
# Create EFS file system
EFS_ID=$(aws efs create-file-system \
--creation-token oxy-ecs-simple-$(date +%s) \
--performance-mode generalPurpose \
--throughput-mode provisioned \
--provisioned-throughput-in-mibps 20 \
--tags Key=Name,Value=oxy-ecs-simple-workspace \
--query 'FileSystemId' \
--output text)
echo "Created EFS: $EFS_ID"
# Create mount targets in each subnet
MT_1_ID=$(aws efs create-mount-target \
--file-system-id $EFS_ID \
--subnet-id $SUBNET_1_ID \
--security-groups $EFS_SG_ID \
--query 'MountTargetId' \
--output text)
MT_2_ID=$(aws efs create-mount-target \
--file-system-id $EFS_ID \
--subnet-id $SUBNET_2_ID \
--security-groups $EFS_SG_ID \
--query 'MountTargetId' \
--output text)
# Create EFS access point
ACCESS_POINT_ID=$(aws efs create-access-point \
--file-system-id $EFS_ID \
--posix-user Uid=0,Gid=0 \
--root-directory Path=/oxy-workspace,CreationInfo='{OwnerUid=0,OwnerGid=0,Permissions=755}' \
--tags Key=Name,Value=oxy-simple-access-point \
--query 'AccessPointId' \
--output text)
echo "Created EFS Access Point: $ACCESS_POINT_ID"
# Create ECR repository for Oxy
aws ecr create-repository \
--repository-name oxy-simple-app \
--region us-west-2
# Get account ID for ECR URI
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
ECR_URI="${ACCOUNT_ID}.dkr.ecr.us-west-2.amazonaws.com/oxy-simple-app"
# Get login token
aws ecr get-login-password --region us-west-2 | \
docker login --username AWS --password-stdin $ECR_URI
Create the application configuration and Dockerfile:
# Create config.yml
cat > config.yml << 'EOF'
databases:
- name: "shared_duckdb"
type: "duckdb"
dataset: "/mnt/efs/oxy_data/shared.db"
models:
- name: "gpt-4"
vendor: "openai"
model_ref: "gpt-4"
key_var: "OPENAI_API_KEY"
authentication:
basic:
smtp_user: "noreply@yourdomain.com"
smtp_password_var: "SMTP_PASSWORD"
smtp_server: "smtp.gmail.com"
smtp_port: 587
defaults:
database: "shared_duckdb"
EOF
# Create Dockerfile
cat > Dockerfile << 'EOF'
FROM ubuntu:22.04
# Install dependencies
RUN apt-get update && apt-get install -y \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Install Oxy
RUN curl --proto '=https' --tlsv1.2 -LsSf https://internal.oxy.tech | /bin/bash
# Create app directory
WORKDIR /app
# Copy configuration file
COPY config.yml /app/config.yml
# Create data directory
RUN mkdir -p /mnt/efs
# Expose ports
EXPOSE 3000 8000
# Create startup script
RUN echo '#!/bin/bash\n\
echo "Starting Oxy with built-in authentication..."\n\
/usr/local/bin/oxy serve --auth-mode built-in --config /app/config.yml --port 3000 & \n\
/usr/local/bin/oxy mcp-sse --port 8000 & \n\
wait' > /app/start.sh && chmod +x /app/start.sh
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["/app/start.sh"]
EOF
# Build the image
docker build -t oxy-simple-app .
# Tag for ECR
docker tag oxy-simple-app:latest $ECR_URI:latest
# Push to ECR
docker push $ECR_URI:latest
echo "Container image pushed to: $ECR_URI:latest"
# Create trust policy for ECS tasks
cat > ecs-task-execution-trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
# Create execution role
aws iam create-role \
--role-name OxyECSSimpleExecutionRole \
--assume-role-policy-document file://ecs-task-execution-trust-policy.json
# Attach AWS managed policy
aws iam attach-role-policy \
--role-name OxyECSSimpleExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
# Create task role
aws iam create-role \
--role-name OxyECSSimpleTaskRole \
--assume-role-policy-document file://ecs-task-execution-trust-policy.json
# Create policy for EFS access
cat > ecs-task-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"elasticfilesystem:ClientMount",
"elasticfilesystem:ClientWrite",
"elasticfilesystem:ClientRootAccess"
],
"Resource": "*"
}
]
}
EOF
# Get account ID
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
# Create and attach policy
aws iam create-policy \
--policy-name OxyECSSimpleTaskPolicy \
--policy-document file://ecs-task-policy.json
aws iam attach-role-policy \
--role-name OxyECSSimpleTaskRole \
--policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/OxyECSSimpleTaskPolicy
# Create ECS cluster
CLUSTER_ARN=$(aws ecs create-cluster \
--cluster-name oxy-simple \
--capacity-providers FARGATE \
--default-capacity-provider-strategy capacityProvider=FARGATE,weight=1 \
--tags key=Name,value=oxy-simple-cluster \
--query 'cluster.clusterArn' \
--output text)
echo "Created ECS Cluster: $CLUSTER_ARN"
# Create CloudWatch log group
aws logs create-log-group \
--log-group-name "/aws/ecs/oxy-simple" \
--retention-in-days 30
# Create task definition JSON
cat > oxy-task-definition.json << EOF
{
"family": "oxy-simple-app",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::${ACCOUNT_ID}:role/OxyECSSimpleExecutionRole",
"taskRoleArn": "arn:aws:iam::${ACCOUNT_ID}:role/OxyECSSimpleTaskRole",
"containerDefinitions": [
{
"name": "oxy-app",
"image": "${ECR_URI}:latest",
"portMappings": [
{
"containerPort": 3000,
"protocol": "tcp"
},
{
"containerPort": 8000,
"protocol": "tcp"
}
],
"environment": [
{
"name": "OXY_STATE_DIR",
"value": "/mnt/efs/oxy_data"
},
{
"name": "AWS_REGION",
"value": "us-west-2"
},
{
"name": "OPENAI_API_KEY",
"value": "your_openai_api_key_here"
},
{
"name": "SMTP_PASSWORD",
"value": "your_smtp_password_here"
}
],
"mountPoints": [
{
"sourceVolume": "efs-storage",
"containerPath": "/mnt/efs",
"readOnly": false
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/aws/ecs/oxy-simple",
"awslogs-region": "us-west-2",
"awslogs-stream-prefix": "ecs"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
}
}
],
"volumes": [
{
"name": "efs-storage",
"efsVolumeConfiguration": {
"fileSystemId": "${EFS_ID}",
"accessPointId": "${ACCESS_POINT_ID}",
"transitEncryption": "ENABLED"
}
}
]
}
EOF
# Register task definition
TASK_DEF_ARN=$(aws ecs register-task-definition \
--cli-input-json file://oxy-task-definition.json \
--query 'taskDefinition.taskDefinitionArn' \
--output text)
echo "Created Task Definition: $TASK_DEF_ARN"
# Create ALB
ALB_ARN=$(aws elbv2 create-load-balancer \
--name oxy-simple-alb \
--subnets $SUBNET_1_ID $SUBNET_2_ID \
--security-groups $ALB_SG_ID \
--scheme internet-facing \
--type application \
--ip-address-type ipv4 \
--tags Key=Name,Value=oxy-simple-alb \
--query 'LoadBalancers[0].LoadBalancerArn' \
--output text)
echo "Created ALB: $ALB_ARN"
# Create target group
TG_ARN=$(aws elbv2 create-target-group \
--name oxy-simple-tg \
--protocol HTTP \
--port 3000 \
--vpc-id $VPC_ID \
--target-type ip \
--health-check-protocol HTTP \
--health-check-path /health \
--health-check-interval-seconds 30 \
--health-check-timeout-seconds 5 \
--healthy-threshold-count 2 \
--unhealthy-threshold-count 3 \
--query 'TargetGroups[0].TargetGroupArn' \
--output text)
echo "Created Target Group: $TG_ARN"
# Create ALB listener
aws elbv2 create-listener \
--load-balancer-arn $ALB_ARN \
--protocol HTTP \
--port 80 \
--default-actions Type=forward,TargetGroupArn=$TG_ARN
# Get ALB DNS name
ALB_DNS=$(aws elbv2 describe-load-balancers \
--load-balancer-arns $ALB_ARN \
--query 'LoadBalancers[0].DNSName' \
--output text)
echo "ALB DNS: $ALB_DNS"
# Create ECS service
SERVICE_ARN=$(aws ecs create-service \
--cluster oxy-simple \
--service-name oxy-simple-service \
--task-definition oxy-simple-app \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[$SUBNET_1_ID,$SUBNET_2_ID],securityGroups=[$ECS_SG_ID],assignPublicIp=ENABLED}" \
--load-balancers targetGroupArn=$TG_ARN,containerName=oxy-app,containerPort=3000 \
--health-check-grace-period-seconds 60 \
--tags key=Name,value=oxy-simple-service \
--query 'service.serviceArn' \
--output text)
echo "Created ECS Service: $SERVICE_ARN"
# Wait for service to stabilize
echo "Waiting for service to stabilize..."
aws ecs wait services-stable --cluster oxy-simple --services oxy-simple-service
echo "Service is stable"
# Register scalable target
aws application-autoscaling register-scalable-target \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/oxy-simple/oxy-simple-service \
--min-capacity 1 \
--max-capacity 10
# Create scaling policy for CPU utilization
aws application-autoscaling put-scaling-policy \
--service-namespace ecs \
--scalable-dimension ecs:service:DesiredCount \
--resource-id service/oxy-simple/oxy-simple-service \
--policy-name oxy-simple-cpu-scaling \
--policy-type TargetTrackingScaling \
--target-tracking-scaling-policy-configuration '{
"TargetValue": 70.0,
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ECSServiceAverageCPUUtilization"
},
"ScaleOutCooldown": 300,
"ScaleInCooldown": 300
}'
echo "Access your Oxy application at: http://$ALB_DNS"
echo "The application may take a few minutes to fully start up."
http://$ALB_DNS
To update API keys or other environment variables:
# Update task definition with new environment variables
# Edit oxy-task-definition.json with new values
aws ecs register-task-definition --cli-input-json file://oxy-task-definition.json
# Update service to use new task definition
aws ecs update-service \
--cluster oxy-simple \
--service oxy-simple-service \
--task-definition oxy-simple-app
# Get cluster info
aws ecs describe-clusters --clusters oxy-simple
# Check service status
aws ecs describe-services \
--cluster oxy-simple \
--services oxy-simple-service
# List running tasks
aws ecs list-tasks \
--cluster oxy-simple \
--service-name oxy-simple-service
# View service logs
aws logs describe-log-streams \
--log-group-name "/aws/ecs/oxy-simple"
# Get latest log events
aws logs get-log-events \
--log-group-name "/aws/ecs/oxy-simple" \
--log-stream-name "$(aws logs describe-log-streams --log-group-name "/aws/ecs/oxy-simple" --order-by LastEventTime --descending --max-items 1 --query 'logStreams[0].logStreamName' --output text)"
# Check EFS mount targets
aws efs describe-mount-targets --file-system-id $EFS_ID
# Monitor EFS performance
aws efs describe-file-systems --file-system-id $EFS_ID
# Check task definition
aws ecs describe-task-definition --task-definition oxy-simple-app
# Check stopped tasks
aws ecs list-tasks \
--cluster oxy-simple \
--desired-status STOPPED
# Get task failure reasons
STOPPED_TASK=$(aws ecs list-tasks --cluster oxy-simple --desired-status STOPPED --query 'taskArns[0]' --output text)
aws ecs describe-tasks --cluster oxy-simple --tasks $STOPPED_TASK
# Check target group health
aws elbv2 describe-target-health --target-group-arn $TG_ARN
# Check container logs for startup issues
aws logs get-log-events \
--log-group-name "/aws/ecs/oxy-simple" \
--log-stream-name "ecs/oxy-app/$(date +%Y/%m/%d)"
# Verify EFS mount targets are available
aws efs describe-mount-targets --file-system-id $EFS_ID
# Check EFS security group allows NFS traffic
aws ec2 describe-security-groups --group-ids $EFS_SG_ID
# Create CloudWatch alarms
aws cloudwatch put-metric-alarm \
--alarm-name "OxySimple-HighCPU" \
--alarm-description "Alert when CPU exceeds 80%" \
--metric-name CPUUtilization \
--namespace AWS/ECS \
--statistic Average \
--period 300 \
--threshold 80 \
--comparison-operator GreaterThanThreshold \
--dimensions Name=ServiceName,Value=oxy-simple-service Name=ClusterName,Value=oxy-simple \
--evaluation-periods 2
To destroy the infrastructure:
# Stop ECS service
aws ecs update-service \
--cluster oxy-simple \
--service oxy-simple-service \
--desired-count 0
# Wait for tasks to stop
aws ecs wait services-stable \
--cluster oxy-simple \
--services oxy-simple-service
# Delete ECS service
aws ecs delete-service \
--cluster oxy-simple \
--service oxy-simple-service
# Delete ECS cluster
aws ecs delete-cluster --cluster oxy-simple
# Delete ALB and target group
aws elbv2 delete-load-balancer --load-balancer-arn $ALB_ARN
aws elbv2 wait load-balancers-deleted --load-balancer-arns $ALB_ARN
aws elbv2 delete-target-group --target-group-arn $TG_ARN
# Delete EFS resources
aws efs delete-access-point --access-point-id $ACCESS_POINT_ID
aws efs delete-mount-target --mount-target-id $MT_1_ID
aws efs delete-mount-target --mount-target-id $MT_2_ID
aws efs wait mount-target-deleted --mount-target-id $MT_1_ID
aws efs wait mount-target-deleted --mount-target-id $MT_2_ID
aws efs delete-file-system --file-system-id $EFS_ID
# Delete IAM roles and policies
aws iam detach-role-policy --role-name OxyECSSimpleExecutionRole --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
aws iam detach-role-policy --role-name OxyECSSimpleTaskRole --policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/OxyECSSimpleTaskPolicy
aws iam delete-role --role-name OxyECSSimpleExecutionRole
aws iam delete-role --role-name OxyECSSimpleTaskRole
aws iam delete-policy --policy-arn arn:aws:iam::${ACCOUNT_ID}:policy/OxyECSSimpleTaskPolicy
# Delete ECR repository
aws ecr delete-repository --repository-name oxy-simple-app --force
# Delete security groups
aws ec2 delete-security-group --group-id $ECS_SG_ID
aws ec2 delete-security-group --group-id $ALB_SG_ID
aws ec2 delete-security-group --group-id $EFS_SG_ID
# Delete VPC resources
aws ec2 delete-route --route-table-id $ROUTE_TABLE_ID --destination-cidr-block 0.0.0.0/0
aws ec2 disassociate-route-table --association-id $(aws ec2 describe-route-tables --route-table-ids $ROUTE_TABLE_ID --query 'RouteTables[0].Associations[?Main==`false`].RouteTableAssociationId' --output text)
aws ec2 delete-route-table --route-table-id $ROUTE_TABLE_ID
aws ec2 delete-subnet --subnet-id $SUBNET_1_ID
aws ec2 delete-subnet --subnet-id $SUBNET_2_ID
aws ec2 detach-internet-gateway --internet-gateway-id $IGW_ID --vpc-id $VPC_ID
aws ec2 delete-internet-gateway --internet-gateway-id $IGW_ID
aws ec2 delete-vpc --vpc-id $VPC_ID
# Delete CloudWatch log group
aws logs delete-log-group --log-group-name "/aws/ecs/oxy-simple"
# Clean up local files
rm config.yml Dockerfile oxy-task-definition.json ecs-task-*.json