Deploy Bifrost on AWS ECS using either Makefile automation or direct AWS CLI commands. This guide covers both Fargate and EC2 launch types, with options for managing configuration secrets.Documentation Index
Fetch the complete documentation index at: https://docs.getbifrost.ai/llms.txt
Use this file to discover all available pages before exploring further.
This guide assumes you already have:
- An ECS cluster
- VPC with subnets
- Security groups configured (must allow inbound traffic on port 8080 or your container port)
- (Optional) Application Load Balancer with target group
- For direct access (no load balancer): Allow inbound traffic on port 8080 (or
CONTAINER_PORT) from your IP or0.0.0.0/0 - For load balancer: Allow inbound traffic from the load balancer’s security group
If you use PostgreSQL for
config_store or logs_store, ensure the target database is UTF8 encoded. See PostgreSQL UTF8 Requirement.Deployment Methods
Choose your preferred deployment method:- Using Makefile
- Using AWS CLI
- Using CloudFormation
Quick Start with Makefile
The easiest way to deploy Bifrost to ECS is using the provided Makefile.First-time deployment? If you don’t know your VPC ID or network configuration, run:This will list all available VPCs, subnets, and security groups in your AWS region.
make list-ecs-network-resources
# First, create your config.json file with your Bifrost configuration
cat > /tmp/bifrost-config.json <<EOF
{
"config_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "your-db-host",
"port": "5432",
"user": "your-db-user",
"password": "your-db-password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
},
"logs_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "your-db-host",
"port": "5432",
"user": "your-db-user",
"password": "your-db-password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
}
}
EOF
# Deploy with VPC ID (recommended - auto-fetches all subnets)
make deploy-ecs \
VPC_ID='vpc-xxx' \
SECURITY_GROUP_IDS='sg-xxx' \
CONFIG_JSON_FILE='/tmp/bifrost-config.json'
# Deploy with specific subnet IDs
make deploy-ecs \
SUBNET_IDS='subnet-xxx,subnet-yyy' \
SECURITY_GROUP_IDS='sg-xxx' \
CONFIG_JSON_FILE='/tmp/bifrost-config.json'
# Deploy with EC2 launch type and SSM Parameter Store
make deploy-ecs \
LAUNCH_TYPE=EC2 \
SECRET_BACKEND=ssm \
VPC_ID='vpc-xxx' \
SECURITY_GROUP_IDS='sg-xxx' \
CONFIG_JSON_FILE='/tmp/bifrost-config.json'
# Deploy with Application Load Balancer
make deploy-ecs \
VPC_ID='vpc-xxx' \
SECURITY_GROUP_IDS='sg-xxx' \
TARGET_GROUP_ARN='arn:aws:elasticloadbalancing:...' \
CONFIG_JSON_FILE='/tmp/bifrost-config.json'
# Deploy without configuration secret
make deploy-ecs \
VPC_ID='vpc-xxx' \
SECURITY_GROUP_IDS='sg-xxx'
Available Makefile Parameters
| Parameter | Default | Description |
|---|---|---|
ECS_CLUSTER_NAME | bifrost-cluster | Name of the ECS cluster |
ECS_SERVICE_NAME | bifrost-service | Name of the ECS service |
ECS_TASK_FAMILY | bifrost-task | Task definition family name |
IMAGE_TAG | latest | Bifrost Docker image tag |
LAUNCH_TYPE | FARGATE | Launch type: FARGATE or EC2 |
SECRET_BACKEND | secretsmanager | Secret storage: secretsmanager or ssm |
AWS_REGION | us-east-1 | AWS region |
VPC_ID | (optional*) | VPC ID (auto-fetches all subnets in VPC) |
SUBNET_IDS | (optional*) | Comma-separated subnet IDs (if VPC_ID not provided) |
SECURITY_GROUP_IDS | (required) | Comma-separated security group IDs |
TARGET_GROUP_ARN | (optional) | ALB target group ARN |
CONTAINER_PORT | 8080 | Container port |
SECRET_NAME | bifrost/config | Secret/parameter name |
CONFIG_JSON_FILE | (optional) | Path to config.json file |
SECRET_ARN | (optional) | Existing secret ARN (skip auto-lookup) |
EXECUTION_ROLE_ARN | (optional) | ECS task execution role ARN |
TASK_ROLE_ARN | (optional) | ECS task role ARN |
Network Configuration (*):
You must provide either
You must provide either
VPC_ID OR SUBNET_IDS:- VPC_ID (recommended): Automatically fetches all subnets in the VPC. Simpler and works across all availability zones.
- SUBNET_IDS: Specify exact subnet IDs if you want fine-grained control over subnet placement.
Makefile Targets
list-ecs-network-resources: List available VPCs, subnets and security groups in your AWS region (helpful for first deployment)deploy-ecs: Complete deployment (creates secret if CONFIG_JSON_FILE provided, registers task definition, creates service, waits for stabilization, and shows deployment status)create-ecs-secret: Create/update configuration secret (requires CONFIG_JSON_FILE parameter)register-ecs-task-definition: Register new task definition (with or without secret)create-ecs-service: Create or update ECS serviceupdate-ecs-service: Force new deploymenttail-ecs-logs: Continuously tail CloudWatch logs in real-time (Ctrl+C to exit)ecs-status: Show current service status, running tasks, and recent logsget-ecs-url: Get the public URL/IP to access the service (works with or without load balancer)cleanup-ecs: Remove service and deregister task definitions
CONFIG_JSON_FILE Parameter: This is optional. If provided, the Makefile will create a secret in AWS Secrets Manager or SSM Parameter Store and mount it in the ECS task. If omitted, the task will be deployed without a secret, and you can use other configuration methods (environment variables, mounted volumes, etc.).How Configuration Secrets Work: When
CONFIG_JSON_FILE is provided, the deployment:- Stores your
config.jsonin AWS Secrets Manager or SSM Parameter Store - Injects the secret as an environment variable
BIFROST_CONFIGinto the container - Uses a custom entrypoint that:
- Silently writes the secret content to
/app/data/config.json - Exits with error only if
BIFROST_CONFIGis not set - Then starts Bifrost normally
- Silently writes the secret content to
- Bifrost reads the configuration from the file at startup
Deployment with AWS CLI
Deploy Bifrost to ECS using direct AWS CLI commands. This section provides step-by-step instructions for both Fargate and EC2 launch types.- Fargate
- EC2
1. Configuration Secret
Choose between AWS Secrets Manager or SSM Parameter Store to store your Bifrost configuration.- Secrets Manager
- SSM Parameter Store
Create a secret containing the Bifrost configuration with Postgres backend:
# Create the configuration JSON
cat > /tmp/bifrost-config.json <<EOF
{
"config_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "your-postgres-host",
"port": "5432",
"user": "your-postgres-user",
"password": "your-postgres-password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
},
"logs_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "your-postgres-host",
"port": "5432",
"user": "your-postgres-user",
"password": "your-postgres-password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
}
}
EOF
# Create the secret
aws secretsmanager create-secret \
--name bifrost/config \
--secret-string file:///tmp/bifrost-config.json \
--region us-east-1
# Get the secret ARN (save this for later)
aws secretsmanager describe-secret \
--secret-id bifrost/config \
--region us-east-1 \
--query 'ARN' \
--output text
Create a parameter containing the Bifrost configuration:
# Create the configuration JSON
cat > /tmp/bifrost-config.json <<EOF
{
"config_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "your-postgres-host",
"port": "5432",
"user": "your-postgres-user",
"password": "your-postgres-password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
},
"logs_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "your-postgres-host",
"port": "5432",
"user": "your-postgres-user",
"password": "your-postgres-password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
}
}
EOF
# Create the parameter
aws ssm put-parameter \
--name /bifrost/config \
--value file:///tmp/bifrost-config.json \
--type SecureString \
--region us-east-1
# Get the parameter ARN (save this for later)
aws ssm get-parameter \
--name /bifrost/config \
--region us-east-1 \
--query 'Parameter.ARN' \
--output text
Important: The task definitions below include a custom
entryPoint and command that:- Reads the
BIFROST_CONFIGenvironment variable (injected from the secret) - Silently writes it to
/app/data/config.json(where Bifrost expects the configuration file) - Exits with error if
BIFROST_CONFIGis not set - Then starts the Bifrost application
2. Task Definition
Create a task definition for Fargate with the configuration secret injected:- With Secrets Manager
- With SSM Parameter Store
# Create task definition JSON
cat > /tmp/bifrost-task-definition.json <<EOF
{
"family": "bifrost-task",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "bifrost",
"image": "maximhq/bifrost:latest",
"essential": true,
"entryPoint": ["/bin/sh", "-c"],
"command": ["if [ -n \"$BIFROST_CONFIG\" ]; then echo \"$BIFROST_CONFIG\" > /app/data/config.json; else echo \"ERROR: BIFROST_CONFIG not set\" >&2 && exit 1; fi && exec /app/docker-entrypoint.sh /app/main"],
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp"
}
],
"secrets": [
{
"name": "BIFROST_CONFIG",
"valueFrom": "arn:aws:secretsmanager:us-east-1:YOUR_ACCOUNT_ID:secret:bifrost/config"
}
],
"healthCheck": {
"command": ["CMD-SHELL", "wget --no-verbose --tries=1 -O /dev/null http://127.0.0.1:8080/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/bifrost-task",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "bifrost",
"awslogs-create-group": "true"
}
}
}
]
}
EOF
# Register the task definition
aws ecs register-task-definition \
--cli-input-json file:///tmp/bifrost-task-definition.json \
--region us-east-1
The
executionRoleArn must have permissions to:- Pull images from Docker Hub
- Read secrets from Secrets Manager
- Create CloudWatch log groups and streams
# Create task definition JSON
cat > /tmp/bifrost-task-definition.json <<EOF
{
"family": "bifrost-task",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "512",
"memory": "1024",
"executionRoleArn": "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "bifrost",
"image": "maximhq/bifrost:latest",
"essential": true,
"entryPoint": ["/bin/sh", "-c"],
"command": ["if [ -n \"$BIFROST_CONFIG\" ]; then echo \"$BIFROST_CONFIG\" > /app/data/config.json; else echo \"ERROR: BIFROST_CONFIG not set\" >&2 && exit 1; fi && exec /app/docker-entrypoint.sh /app/main"],
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp"
}
],
"secrets": [
{
"name": "BIFROST_CONFIG",
"valueFrom": "arn:aws:ssm:us-east-1:YOUR_ACCOUNT_ID:parameter/bifrost/config"
}
],
"healthCheck": {
"command": ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/bifrost-task",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "bifrost",
"awslogs-create-group": "true"
}
}
}
]
}
EOF
# Register the task definition
aws ecs register-task-definition \
--cli-input-json file:///tmp/bifrost-task-definition.json \
--region us-east-1
The
executionRoleArn must have permissions to:- Pull images from Docker Hub
- Read parameters from SSM Parameter Store
- Create CloudWatch log groups and streams
3. Create ECS Service
- Without Load Balancer
- With Application Load Balancer
aws ecs create-service \
--cluster bifrost-cluster \
--service-name bifrost-service \
--task-definition bifrost-task \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-xxx,subnet-yyy],securityGroups=[sg-xxx],assignPublicIp=ENABLED}" \
--region us-east-1
aws ecs create-service \
--cluster bifrost-cluster \
--service-name bifrost-service \
--task-definition bifrost-task \
--desired-count 1 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={subnets=[subnet-xxx,subnet-yyy],securityGroups=[sg-xxx],assignPublicIp=ENABLED}" \
--load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:us-east-1:YOUR_ACCOUNT_ID:targetgroup/bifrost-tg/xxx,containerName=bifrost,containerPort=8080" \
--health-check-grace-period-seconds 60 \
--region us-east-1
When using an ALB:
- The security group must allow traffic from the ALB
- The target group health check should point to
/health - Set an appropriate health check grace period (60+ seconds)
4. Update Service
To deploy a new version or force a redeployment:aws ecs update-service \
--cluster bifrost-cluster \
--service bifrost-service \
--force-new-deployment \
--region us-east-1
1. Configuration Secret
Choose between AWS Secrets Manager or SSM Parameter Store to store your Bifrost configuration.- Secrets Manager
- SSM Parameter Store
Create a secret containing the Bifrost configuration with Postgres backend:
# Create the configuration JSON
cat > /tmp/bifrost-config.json <<EOF
{
"config_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "your-postgres-host",
"port": "5432",
"user": "your-postgres-user",
"password": "your-postgres-password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
},
"logs_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "your-postgres-host",
"port": "5432",
"user": "your-postgres-user",
"password": "your-postgres-password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
}
}
EOF
# Create the secret
aws secretsmanager create-secret \
--name bifrost/config \
--secret-string file:///tmp/bifrost-config.json \
--region us-east-1
# Get the secret ARN (save this for later)
aws secretsmanager describe-secret \
--secret-id bifrost/config \
--region us-east-1 \
--query 'ARN' \
--output text
Create a parameter containing the Bifrost configuration:
# Create the configuration JSON
cat > /tmp/bifrost-config.json <<EOF
{
"config_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "your-postgres-host",
"port": "5432",
"user": "your-postgres-user",
"password": "your-postgres-password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
},
"logs_store": {
"enabled": true,
"type": "postgres",
"config": {
"host": "your-postgres-host",
"port": "5432",
"user": "your-postgres-user",
"password": "your-postgres-password",
"db_name": "bifrost",
"ssl_mode": "disable"
}
}
}
EOF
# Create the parameter
aws ssm put-parameter \
--name /bifrost/config \
--value file:///tmp/bifrost-config.json \
--type SecureString \
--region us-east-1
# Get the parameter ARN (save this for later)
aws ssm get-parameter \
--name /bifrost/config \
--region us-east-1 \
--query 'Parameter.ARN' \
--output text
Important: The task definitions below include a custom
entryPoint and command that:- Reads the
BIFROST_CONFIGenvironment variable (injected from the secret) - Silently writes it to
/app/data/config.json(where Bifrost expects the configuration file) - Exits with error if
BIFROST_CONFIGis not set - Then starts the Bifrost application
2. Task Definition
Create a task definition for EC2 launch type with the configuration secret injected:- With Secrets Manager
- With SSM Parameter Store
# Create task definition JSON
cat > /tmp/bifrost-task-definition.json <<EOF
{
"family": "bifrost-task",
"networkMode": "awsvpc",
"requiresCompatibilities": ["EC2"],
"executionRoleArn": "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "bifrost",
"image": "maximhq/bifrost:latest",
"cpu": 256,
"memory": 512,
"essential": true,
"entryPoint": ["/bin/sh", "-c"],
"command": ["if [ -n \"$BIFROST_CONFIG\" ]; then echo \"$BIFROST_CONFIG\" > /app/data/config.json; else echo \"ERROR: BIFROST_CONFIG not set\" >&2 && exit 1; fi && exec /app/docker-entrypoint.sh /app/main"],
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp"
}
],
"secrets": [
{
"name": "BIFROST_CONFIG",
"valueFrom": "arn:aws:secretsmanager:us-east-1:YOUR_ACCOUNT_ID:secret:bifrost/config"
}
],
"healthCheck": {
"command": ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/bifrost-task",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "bifrost",
"awslogs-create-group": "true"
}
}
}
]
}
EOF
# Register the task definition
aws ecs register-task-definition \
--cli-input-json file:///tmp/bifrost-task-definition.json \
--region us-east-1
For EC2 launch type:
- CPU and memory are specified at the container level
- Ensure your EC2 instances have sufficient resources
- The ECS agent must be running on the instances
# Create task definition JSON
cat > /tmp/bifrost-task-definition.json <<EOF
{
"family": "bifrost-task",
"networkMode": "awsvpc",
"requiresCompatibilities": ["EC2"],
"executionRoleArn": "arn:aws:iam::YOUR_ACCOUNT_ID:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "bifrost",
"image": "maximhq/bifrost:latest",
"cpu": 256,
"memory": 512,
"essential": true,
"entryPoint": ["/bin/sh", "-c"],
"command": ["if [ -n \"$BIFROST_CONFIG\" ]; then echo \"$BIFROST_CONFIG\" > /app/data/config.json; else echo \"ERROR: BIFROST_CONFIG not set\" >&2 && exit 1; fi && exec /app/docker-entrypoint.sh /app/main"],
"portMappings": [
{
"containerPort": 8080,
"protocol": "tcp"
}
],
"secrets": [
{
"name": "BIFROST_CONFIG",
"valueFrom": "arn:aws:ssm:us-east-1:YOUR_ACCOUNT_ID:parameter/bifrost/config"
}
],
"healthCheck": {
"command": ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
},
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/bifrost-task",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "bifrost",
"awslogs-create-group": "true"
}
}
}
]
}
EOF
# Register the task definition
aws ecs register-task-definition \
--cli-input-json file:///tmp/bifrost-task-definition.json \
--region us-east-1
3. Create ECS Service
- Without Load Balancer
- With Application Load Balancer
aws ecs create-service \
--cluster bifrost-cluster \
--service-name bifrost-service \
--task-definition bifrost-task \
--desired-count 1 \
--launch-type EC2 \
--network-configuration "awsvpcConfiguration={subnets=[subnet-xxx,subnet-yyy],securityGroups=[sg-xxx]}" \
--region us-east-1
aws ecs create-service \
--cluster bifrost-cluster \
--service-name bifrost-service \
--task-definition bifrost-task \
--desired-count 1 \
--launch-type EC2 \
--network-configuration "awsvpcConfiguration={subnets=[subnet-xxx,subnet-yyy],securityGroups=[sg-xxx]}" \
--load-balancers "targetGroupArn=arn:aws:elasticloadbalancing:us-east-1:YOUR_ACCOUNT_ID:targetgroup/bifrost-tg/xxx,containerName=bifrost,containerPort=8080" \
--health-check-grace-period-seconds 60 \
--region us-east-1
4. Update Service
To deploy a new version or force a redeployment:aws ecs update-service \
--cluster bifrost-cluster \
--service bifrost-service \
--force-new-deployment \
--region us-east-1
CloudFormation Deployment
Deploy Bifrost to ECS using AWS CloudFormation for infrastructure as code management.The CloudFormation template is available in the repository at
cloudformation/ecs-deployment.yaml.
You can use it directly or customize it for your needs.Configuration Secret Handling: When you provide ConfigSecretArn, the template automatically:- Injects the secret as an environment variable
BIFROST_CONFIGinto the container - Uses a custom entrypoint that:
- Silently writes the secret content to
/app/data/config.json - Exits with error if secret is not set
- Silently writes the secret content to
- This ensures Bifrost can read the configuration from the expected file location
CloudFormation Template
The template (cloudformation/ecs-deployment.yaml):AWSTemplateFormatVersion: '2010-09-09'
Description: 'Deploy Bifrost service on ECS'
Parameters:
ClusterName:
Type: String
Default: bifrost-cluster
Description: Name of the ECS cluster
ServiceName:
Type: String
Default: bifrost-service
Description: Name of the ECS service
TaskFamily:
Type: String
Default: bifrost-task
Description: Task definition family name
ImageTag:
Type: String
Default: latest
Description: Bifrost Docker image tag
LaunchType:
Type: String
Default: FARGATE
AllowedValues:
- FARGATE
- EC2
Description: ECS launch type
ContainerPort:
Type: Number
Default: 8080
Description: Container port
DesiredCount:
Type: Number
Default: 1
Description: Desired number of tasks
VpcId:
Type: AWS::EC2::VPC::Id
Description: VPC ID where the service will run
SubnetIds:
Type: List<AWS::EC2::Subnet::Id>
Description: Subnet IDs for the service (use public subnets for direct access)
SecurityGroupIds:
Type: List<AWS::EC2::SecurityGroup::Id>
Description: Security group IDs (must allow inbound on ContainerPort)
ConfigSecretArn:
Type: String
Default: ''
Description: (Optional) ARN of Secrets Manager secret or SSM parameter containing config.json
ExecutionRoleArn:
Type: String
Default: ''
Description: (Optional) ECS task execution role ARN (will create default if not provided)
TaskRoleArn:
Type: String
Default: ''
Description: (Optional) ECS task role ARN
TargetGroupArn:
Type: String
Default: ''
Description: (Optional) ALB target group ARN for load balancing
AssignPublicIp:
Type: String
Default: ENABLED
AllowedValues:
- ENABLED
- DISABLED
Description: Assign public IP to tasks (ENABLED for direct access without load balancer)
Conditions:
IsFargate: !Equals [!Ref LaunchType, FARGATE]
HasSecret: !Not [!Equals [!Ref ConfigSecretArn, '']]
HasExecutionRole: !Not [!Equals [!Ref ExecutionRoleArn, '']]
HasTaskRole: !Not [!Equals [!Ref TaskRoleArn, '']]
HasTargetGroup: !Not [!Equals [!Ref TargetGroupArn, '']]
CreateExecutionRole: !And
- !Not [!Condition HasExecutionRole]
- !Condition IsFargate
Resources:
# CloudWatch Log Group
LogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/ecs/${TaskFamily}'
RetentionInDays: 7
# ECS Task Execution Role (created only if not provided and using Fargate)
TaskExecutionRole:
Type: AWS::IAM::Role
Condition: CreateExecutionRole
Properties:
RoleName: !Sub '${ServiceName}-execution-role'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
Policies:
- PolicyName: SecretAccess
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- secretsmanager:GetSecretValue
- ssm:GetParameter
- ssm:GetParameters
Resource:
- !Sub 'arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:bifrost/*'
- !Sub 'arn:aws:ssm:${AWS::Region}:${AWS::AccountId}:parameter/bifrost/*'
- Effect: Allow
Action:
- kms:Decrypt
Resource: '*'
# ECS Task Definition
TaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Ref TaskFamily
NetworkMode: awsvpc
RequiresCompatibilities:
- !Ref LaunchType
Cpu: !If [IsFargate, '512', '256']
Memory: !If [IsFargate, '1024', '512']
ExecutionRoleArn: !If
- HasExecutionRole
- !Ref ExecutionRoleArn
- !If
- CreateExecutionRole
- !GetAtt TaskExecutionRole.Arn
- !Ref AWS::NoValue
TaskRoleArn: !If [HasTaskRole, !Ref TaskRoleArn, !Ref AWS::NoValue]
ContainerDefinitions:
- Name: bifrost
Image: !Sub 'maximhq/bifrost:${ImageTag}'
Essential: true
EntryPoint: !If
- HasSecret
- - /bin/sh
- -c
- !Ref AWS::NoValue
Command: !If
- HasSecret
- - 'if [ -n "$BIFROST_CONFIG" ]; then echo "$BIFROST_CONFIG" > /app/data/config.json; else echo "ERROR: BIFROST_CONFIG not set" >&2 && exit 1; fi && exec /app/docker-entrypoint.sh /app/main'
- !Ref AWS::NoValue
PortMappings:
- ContainerPort: !Ref ContainerPort
Protocol: tcp
Environment: []
Secrets: !If
- HasSecret
- - Name: BIFROST_CONFIG
ValueFrom: !Ref ConfigSecretArn
- !Ref AWS::NoValue
HealthCheck:
Command:
- CMD-SHELL
- !Sub 'wget --no-verbose --tries=1 --spider http://localhost:${ContainerPort}/health || exit 1'
Interval: 30
Timeout: 5
Retries: 3
StartPeriod: 60
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Ref LogGroup
awslogs-region: !Ref AWS::Region
awslogs-stream-prefix: bifrost
# ECS Service
Service:
Type: AWS::ECS::Service
Properties:
ServiceName: !Ref ServiceName
Cluster: !Ref ClusterName
TaskDefinition: !Ref TaskDefinition
DesiredCount: !Ref DesiredCount
LaunchType: !Ref LaunchType
NetworkConfiguration:
AwsvpcConfiguration:
Subnets: !Ref SubnetIds
SecurityGroups: !Ref SecurityGroupIds
AssignPublicIp: !Ref AssignPublicIp
LoadBalancers: !If
- HasTargetGroup
- - ContainerName: bifrost
ContainerPort: !Ref ContainerPort
TargetGroupArn: !Ref TargetGroupArn
- !Ref AWS::NoValue
HealthCheckGracePeriodSeconds: !If [HasTargetGroup, 60, !Ref AWS::NoValue]
Outputs:
ServiceName:
Description: ECS Service Name
Value: !Ref Service
Export:
Name: !Sub '${AWS::StackName}-ServiceName'
TaskDefinitionArn:
Description: Task Definition ARN
Value: !Ref TaskDefinition
Export:
Name: !Sub '${AWS::StackName}-TaskDefinitionArn'
LogGroupName:
Description: CloudWatch Log Group
Value: !Ref LogGroup
Export:
Name: !Sub '${AWS::StackName}-LogGroupName'
ExecutionRoleArn:
Condition: CreateExecutionRole
Description: Created Task Execution Role ARN
Value: !GetAtt TaskExecutionRole.Arn
Export:
Name: !Sub '${AWS::StackName}-ExecutionRoleArn'
Deploy with CloudFormation
- Without Load Balancer
- With Load Balancer
- EC2 Launch Type
Deploy without configuration secret:Deploy with Secrets Manager:First, create the secret:Then deploy with the secret:
aws cloudformation create-stack \
--stack-name bifrost-ecs-stack \
--template-body file://cloudformation/ecs-deployment.yaml \
--parameters \
ParameterKey=VpcId,ParameterValue=vpc-xxx \
ParameterKey=SubnetIds,ParameterValue="subnet-xxx\,subnet-yyy" \
ParameterKey=SecurityGroupIds,ParameterValue="sg-xxx" \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1
# Wait for stack creation
aws cloudformation wait stack-create-complete \
--stack-name bifrost-ecs-stack \
--region us-east-1
# Get service details
aws cloudformation describe-stacks \
--stack-name bifrost-ecs-stack \
--region us-east-1 \
--query 'Stacks[0].Outputs'
aws secretsmanager create-secret \
--name bifrost/config \
--secret-string file://config.json \
--region us-east-1
# Get the secret ARN
SECRET_ARN=$(aws secretsmanager describe-secret \
--secret-id bifrost/config \
--region us-east-1 \
--query 'ARN' \
--output text)
aws cloudformation create-stack \
--stack-name bifrost-ecs-stack \
--template-body file://cloudformation/ecs-deployment.yaml \
--parameters \
ParameterKey=VpcId,ParameterValue=vpc-xxx \
ParameterKey=SubnetIds,ParameterValue="subnet-xxx\,subnet-yyy" \
ParameterKey=SecurityGroupIds,ParameterValue="sg-xxx" \
ParameterKey=ConfigSecretArn,ParameterValue=$SECRET_ARN \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1
aws cloudformation create-stack \
--stack-name bifrost-ecs-stack \
--template-body file://cloudformation/ecs-deployment.yaml \
--parameters \
ParameterKey=VpcId,ParameterValue=vpc-xxx \
ParameterKey=SubnetIds,ParameterValue="subnet-xxx\,subnet-yyy" \
ParameterKey=SecurityGroupIds,ParameterValue="sg-xxx" \
ParameterKey=TargetGroupArn,ParameterValue=arn:aws:elasticloadbalancing:... \
ParameterKey=AssignPublicIp,ParameterValue=DISABLED \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1
When using a load balancer, you can set
AssignPublicIp=DISABLED if your tasks don’t need direct internet access (they’ll use NAT Gateway via the load balancer).aws cloudformation create-stack \
--stack-name bifrost-ecs-stack \
--template-body file://cloudformation/ecs-deployment.yaml \
--parameters \
ParameterKey=VpcId,ParameterValue=vpc-xxx \
ParameterKey=SubnetIds,ParameterValue="subnet-xxx\,subnet-yyy" \
ParameterKey=SecurityGroupIds,ParameterValue="sg-xxx" \
ParameterKey=LaunchType,ParameterValue=EC2 \
ParameterKey=ExecutionRoleArn,ParameterValue=arn:aws:iam::ACCOUNT:role/ecsTaskExecutionRole \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1
For EC2 launch type, you must provide an existing
ExecutionRoleArn as the template only auto-creates roles for Fargate.Update Stack
To update your deployment (e.g., change image tag or configuration):# Update the stack
aws cloudformation update-stack \
--stack-name bifrost-ecs-stack \
--template-body file://cloudformation/ecs-deployment.yaml \
--parameters \
ParameterKey=VpcId,UsePreviousValue=true \
ParameterKey=SubnetIds,UsePreviousValue=true \
ParameterKey=SecurityGroupIds,UsePreviousValue=true \
ParameterKey=ImageTag,ParameterValue=v1.2.0 \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1
# Wait for update to complete
aws cloudformation wait stack-update-complete \
--stack-name bifrost-ecs-stack \
--region us-east-1
Get Service URL
After deployment, get your service URL:# Get the task public IP (without load balancer)
TASK_ARN=$(aws ecs list-tasks \
--cluster bifrost-cluster \
--service-name bifrost-service \
--region us-east-1 \
--query 'taskArns[0]' \
--output text)
ENI_ID=$(aws ecs describe-tasks \
--cluster bifrost-cluster \
--tasks $TASK_ARN \
--region us-east-1 \
--query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' \
--output text)
PUBLIC_IP=$(aws ec2 describe-network-interfaces \
--network-interface-ids $ENI_ID \
--region us-east-1 \
--query 'NetworkInterfaces[0].Association.PublicIp' \
--output text)
echo "Service URL: http://$PUBLIC_IP:8080"
echo "Health check: http://$PUBLIC_IP:8080/health"
# Test the service
curl http://$PUBLIC_IP:8080/health
Monitor Logs
# Tail logs
aws logs tail /ecs/bifrost-task --follow --region us-east-1
# View recent logs
LOG_STREAM=$(aws logs describe-log-streams \
--log-group-name /ecs/bifrost-task \
--order-by LastEventTime \
--descending \
--max-items 1 \
--region us-east-1 \
--query 'logStreams[0].logStreamName' \
--output text)
aws logs get-log-events \
--log-group-name /ecs/bifrost-task \
--log-stream-name $LOG_STREAM \
--region us-east-1
Delete Stack
To remove all resources:aws cloudformation delete-stack \
--stack-name bifrost-ecs-stack \
--region us-east-1
# Wait for deletion
aws cloudformation wait stack-delete-complete \
--stack-name bifrost-ecs-stack \
--region us-east-1
CloudFormation Parameters Reference
| Parameter | Default | Required | Description |
|---|---|---|---|
ClusterName | bifrost-cluster | No | ECS cluster name (must exist) |
ServiceName | bifrost-service | No | ECS service name |
TaskFamily | bifrost-task | No | Task definition family |
ImageTag | latest | No | Docker image tag |
LaunchType | FARGATE | No | FARGATE or EC2 |
ContainerPort | 8080 | No | Container port |
DesiredCount | 1 | No | Number of tasks |
VpcId | - | Yes | VPC ID |
SubnetIds | - | Yes | Comma-separated subnet IDs |
SecurityGroupIds | - | Yes | Comma-separated security group IDs |
ConfigSecretArn | (empty) | No | Secret/parameter ARN |
ExecutionRoleArn | (empty) | No | Task execution role ARN |
TaskRoleArn | (empty) | No | Task role ARN |
TargetGroupArn | (empty) | No | ALB target group ARN |
AssignPublicIp | ENABLED | No | Assign public IP to tasks |
IAM Permissions
Task Execution Role
The task execution role (ecsTaskExecutionRole) needs the following permissions:
The Makefile automatically creates the CloudWatch log group
/ecs/bifrost-task, so the execution role only needs CreateLogStream and PutLogEvents permissions, not CreateLogGroup.- For Secrets Manager
- For SSM Parameter Store
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:log-group:/ecs/bifrost-task:*"
},
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:us-east-1:YOUR_ACCOUNT_ID:secret:bifrost/config*"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage"
],
"Resource": "*"
},
{
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:*:*:log-group:/ecs/bifrost-task:*"
},
{
"Effect": "Allow",
"Action": [
"ssm:GetParameters",
"ssm:GetParameter"
],
"Resource": "arn:aws:ssm:us-east-1:YOUR_ACCOUNT_ID:parameter/bifrost/config"
},
{
"Effect": "Allow",
"Action": [
"kms:Decrypt"
],
"Resource": "arn:aws:kms:us-east-1:YOUR_ACCOUNT_ID:key/YOUR_KMS_KEY_ID"
}
]
}
Accessing Your Service
Without Load Balancer
When deployed without a load balancer, the ECS task gets a public IP address. You can find it using AWS CLI:# Get the public IP address of your running task
aws ec2 describe-network-interfaces \
--network-interface-ids $(aws ecs describe-tasks \
--cluster bifrost-cluster \
--tasks $(aws ecs list-tasks \
--cluster bifrost-cluster \
--service-name bifrost-service \
--region us-east-1 \
--query 'taskArns[0]' \
--output text) \
--region us-east-1 \
--query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' \
--output text) \
--region us-east-1 \
--query 'NetworkInterfaces[0].Association.PublicIp' \
--output text
Important Notes:
- The public IP changes every time the task is restarted
- You must allow inbound traffic on port 8080 (or your
CONTAINER_PORT) in your security group - For production, consider using an Application Load Balancer for a stable endpoint
# Test health endpoint (replace YOUR_PUBLIC_IP with the IP from above)
curl http://YOUR_PUBLIC_IP:8080/health
# Expected response
{"status":"ok"}
With Load Balancer
If you deployed withTARGET_GROUP_ARN, your service is accessible via the load balancer’s DNS name:
# Get the load balancer DNS name (replace YOUR_TARGET_GROUP_ARN with your actual ARN)
aws elbv2 describe-load-balancers \
--load-balancer-arns $(aws elbv2 describe-target-groups \
--target-group-arns YOUR_TARGET_GROUP_ARN \
--region us-east-1 \
--query 'TargetGroups[0].LoadBalancerArns[0]' \
--output text) \
--region us-east-1 \
--query 'LoadBalancers[0].DNSName' \
--output text
# Test via load balancer (replace YOUR_ALB_DNS with the DNS from above)
curl http://YOUR_ALB_DNS/health
- ✅ Stable DNS endpoint
- ✅ SSL/TLS termination (if configured)
- ✅ Health checks with automatic failover
- ✅ Multiple task load balancing
Monitoring and Logs
Tail Logs (Makefile)
The easiest way to monitor your deployment logs:# Tail logs in real-time (press Ctrl+C to exit)
make tail-ecs-logs
# Check service status and recent logs
make ecs-status
The
deploy-ecs command automatically waits for the deployment to stabilize and shows you:- Deployment status (running/desired count)
- Task details (ARN, status, health)
- Recent logs (last 20 events)
make tail-ecs-logs to continuously monitor your application.View Logs (AWS CLI)
# Tail logs using AWS CLI v2 (recommended)
aws logs tail /ecs/bifrost-task --follow --region us-east-1
# Get log stream names
aws logs describe-log-streams \
--log-group-name /ecs/bifrost-task \
--order-by LastEventTime \
--descending \
--max-items 5 \
--region us-east-1
# View logs from a specific stream
aws logs get-log-events \
--log-group-name /ecs/bifrost-task \
--log-stream-name bifrost/bifrost/TASK_ID \
--region us-east-1
Check Service Status
# Describe service
aws ecs describe-services \
--cluster bifrost-cluster \
--services bifrost-service \
--region us-east-1
# List tasks
aws ecs list-tasks \
--cluster bifrost-cluster \
--service-name bifrost-service \
--region us-east-1
# Describe task
aws ecs describe-tasks \
--cluster bifrost-cluster \
--tasks TASK_ARN \
--region us-east-1
Cleanup
To remove all ECS resources:# Using Makefile
make cleanup-ecs
# Or manually
# Delete service
aws ecs update-service \
--cluster bifrost-cluster \
--service bifrost-service \
--desired-count 0 \
--region us-east-1
aws ecs delete-service \
--cluster bifrost-cluster \
--service bifrost-service \
--region us-east-1
# Deregister task definitions
aws ecs list-task-definitions \
--family-prefix bifrost-task \
--region us-east-1 \
--query 'taskDefinitionArns[]' \
--output text | \
xargs -n 1 aws ecs deregister-task-definition --task-definition --region us-east-1
# Delete secret (optional)
aws secretsmanager delete-secret \
--secret-id bifrost/config \
--force-delete-without-recovery \
--region us-east-1
# Or delete SSM parameter (optional)
aws ssm delete-parameter \
--name /bifrost/config \
--region us-east-1

