# AWS ECS Deployment Recipe # Include this in your main Makefile with: include recipes/ecs.mk # Configuration variables ECS_CLUSTER_NAME ?= bifrost-cluster ECS_SERVICE_NAME ?= bifrost-service ECS_TASK_FAMILY ?= bifrost-task IMAGE_TAG ?= latest LAUNCH_TYPE ?= FARGATE SECRET_BACKEND ?= secretsmanager AWS_REGION ?= us-east-1 VPC_ID ?= SUBNET_IDS ?= SECURITY_GROUP_IDS ?= TARGET_GROUP_ARN ?= CONTAINER_PORT ?= 8080 SECRET_NAME ?= bifrost/config SECRET_ARN ?= EXECUTION_ROLE_ARN ?= TASK_ROLE_ARN ?= # Configuration JSON file path (optional - provide path to your config.json file) # Example: CONFIG_JSON_FILE=/path/to/config.json CONFIG_JSON_FILE ?= .PHONY: deploy-ecs list-ecs-network-resources check-ecs-prerequisites create-ecs-secret register-ecs-task-definition create-ecs-service update-ecs-service tail-ecs-logs ecs-status get-ecs-url cleanup-ecs deploy-ecs: ## Deploy Bifrost to ECS (Usage: make deploy-ecs SUBNET_IDS='...' SECURITY_GROUP_IDS='...' [CONFIG_JSON_FILE='...']) @echo "$(BLUE)Starting ECS deployment...$(NC)" @echo "" @$(MAKE) check-ecs-prerequisites @if [ -n "$(CONFIG_JSON_FILE)" ]; then \ $(MAKE) create-ecs-secret; \ else \ echo "$(YELLOW)No CONFIG_JSON_FILE provided, skipping secret creation$(NC)"; \ fi @$(MAKE) register-ecs-task-definition @$(MAKE) create-ecs-service @echo "" @echo "$(GREEN)✓ ECS deployment complete!$(NC)" list-ecs-network-resources: ## List available VPCs, subnets and security groups for ECS deployment @echo "$(BLUE)Listing available network resources in region $(AWS_REGION)...$(NC)" @echo "" @# Check if AWS CLI is configured @aws sts get-caller-identity > /dev/null 2>&1 || \ (echo "$(RED)Error: AWS CLI is not configured.$(NC)" && \ echo "$(YELLOW)Run: aws configure$(NC)" && exit 1) @echo "$(CYAN)Available VPCs:$(NC)" @aws ec2 describe-vpcs \ --region $(AWS_REGION) \ --query 'Vpcs[*].[VpcId,CidrBlock,IsDefault,Tags[?Key==`Name`].Value|[0]]' \ --output table @echo "" @echo "$(CYAN)Available Subnets:$(NC)" @aws ec2 describe-subnets \ --region $(AWS_REGION) \ --query 'Subnets[*].[SubnetId,AvailabilityZone,VpcId,CidrBlock,Tags[?Key==`Name`].Value|[0]]' \ --output table @echo "" @echo "$(CYAN)Available Security Groups:$(NC)" @aws ec2 describe-security-groups \ --region $(AWS_REGION) \ --query 'SecurityGroups[*].[GroupId,GroupName,VpcId,Description]' \ --output table @echo "" @echo "$(YELLOW)Usage (Option 1 - Recommended):$(NC)" @echo " Use VPC ID to auto-fetch all subnets:" @echo " $(GREEN)make deploy-ecs VPC_ID='vpc-xxx' SECURITY_GROUP_IDS='sg-xxx'$(NC)" @echo "" @echo "$(YELLOW)Usage (Option 2):$(NC)" @echo " Specify subnet IDs manually:" @echo " $(GREEN)make deploy-ecs SUBNET_IDS='subnet-xxx,subnet-yyy' SECURITY_GROUP_IDS='sg-xxx'$(NC)" check-ecs-prerequisites: ## Check ECS deployment prerequisites @echo "$(YELLOW)Checking prerequisites...$(NC)" @# Check if AWS CLI is installed @which aws > /dev/null || (echo "$(RED)Error: AWS CLI is not installed.$(NC)" && \ echo "$(YELLOW)Please install AWS CLI first.$(NC)" && \ echo "$(CYAN)Documentation: https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html$(NC)" && \ exit 1) @echo "$(GREEN)✓ AWS CLI is installed$(NC)" @# Check if AWS CLI is configured @aws sts get-caller-identity > /dev/null 2>&1 || \ (echo "$(RED)Error: AWS CLI is not configured.$(NC)" && \ echo "$(YELLOW)Please configure AWS CLI with your credentials.$(NC)" && \ echo "$(CYAN)Run: aws configure$(NC)" && \ echo "$(CYAN)Documentation: https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html$(NC)" && \ exit 1) @echo "$(GREEN)✓ AWS CLI is configured$(NC)" @# Check if cluster exists @aws ecs describe-clusters --clusters $(ECS_CLUSTER_NAME) --region $(AWS_REGION) > /dev/null 2>&1 || \ (echo "$(RED)Error: ECS cluster '$(ECS_CLUSTER_NAME)' not found$(NC)" && exit 1) @echo "$(GREEN)✓ ECS cluster '$(ECS_CLUSTER_NAME)' exists$(NC)" @# Check/fetch execution role ARN if not provided and using Fargate @if [ -z "$(EXECUTION_ROLE_ARN)" ]; then \ echo "$(CYAN)No EXECUTION_ROLE_ARN provided, checking for default role...$(NC)"; \ ACCOUNT_ID=$$(aws sts get-caller-identity --query Account --output text 2>/dev/null); \ DEFAULT_ROLE_ARN="arn:aws:iam::$$ACCOUNT_ID:role/ecsTaskExecutionRole"; \ if aws iam get-role --role-name ecsTaskExecutionRole --region $(AWS_REGION) > /dev/null 2>&1; then \ echo "$$DEFAULT_ROLE_ARN" > /tmp/ecs-execution-role.tmp; \ echo "$(GREEN)✓ Found default execution role: ecsTaskExecutionRole$(NC)"; \ else \ echo ""; \ echo "$(RED)Error: No execution role found$(NC)"; \ echo ""; \ echo "$(YELLOW)ECS tasks require an execution role for CloudWatch logs and pulling images.$(NC)"; \ echo ""; \ echo "$(CYAN)Option 1 - Create the default role:$(NC)"; \ echo " $(GREEN)aws iam create-role --role-name ecsTaskExecutionRole \\"; \ echo " --assume-role-policy-document '{"; \ echo " \"Version\": \"2012-10-17\","; \ echo " \"Statement\": [{"; \ echo " \"Effect\": \"Allow\","; \ echo " \"Principal\": {\"Service\": \"ecs-tasks.amazonaws.com\"},"; \ echo " \"Action\": \"sts:AssumeRole\""; \ echo " }]"; \ echo " }'$(NC)"; \ echo ""; \ echo " $(GREEN)aws iam attach-role-policy --role-name ecsTaskExecutionRole \\"; \ echo " --policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy$(NC)"; \ echo ""; \ echo "$(CYAN)Option 2 - Specify an existing role:$(NC)"; \ echo " $(GREEN)make deploy-ecs EXECUTION_ROLE_ARN='arn:aws:iam::ACCOUNT:role/YOUR_ROLE' ...$(NC)"; \ echo ""; \ exit 1; \ fi; \ else \ echo "$(GREEN)✓ Using provided execution role$(NC)"; \ fi @# Fetch subnets from VPC if VPC_ID is provided but SUBNET_IDS is not @if [ -n "$(VPC_ID)" ] && [ -z "$(SUBNET_IDS)" ]; then \ echo "$(CYAN)Fetching subnets from VPC $(VPC_ID)...$(NC)"; \ FETCHED_SUBNETS=$$(aws ec2 describe-subnets \ --region $(AWS_REGION) \ --filters "Name=vpc-id,Values=$(VPC_ID)" \ --query 'Subnets[*].SubnetId' \ --output text 2>/dev/null | tr '\t' ','); \ if [ -z "$$FETCHED_SUBNETS" ]; then \ echo "$(RED)Error: No subnets found in VPC $(VPC_ID)$(NC)"; \ exit 1; \ fi; \ echo "$$FETCHED_SUBNETS" > /tmp/ecs-subnets.tmp; \ echo "$(GREEN)✓ Found subnets: $$FETCHED_SUBNETS$(NC)"; \ fi @# Check if required network parameters are provided @if [ -z "$(SUBNET_IDS)" ] && [ ! -f /tmp/ecs-subnets.tmp ] && [ -z "$(VPC_ID)" ]; then \ echo ""; \ echo "$(RED)Error: Network configuration is required$(NC)"; \ echo ""; \ echo "$(YELLOW)You must provide either:$(NC)"; \ echo " - VPC_ID (will auto-fetch all subnets in VPC)"; \ echo " - SUBNET_IDS (specific subnet IDs)"; \ echo ""; \ echo "$(CYAN)To list available VPCs and network resources, run:$(NC)"; \ echo " $(GREEN)make list-ecs-network-resources$(NC)"; \ echo ""; \ echo "$(CYAN)Then deploy with VPC ID (recommended):$(NC)"; \ echo " $(GREEN)make deploy-ecs VPC_ID='vpc-xxx' SECURITY_GROUP_IDS='sg-xxx'$(NC)"; \ echo ""; \ echo "$(CYAN)Or deploy with specific subnet IDs:$(NC)"; \ echo " $(GREEN)make deploy-ecs SUBNET_IDS='subnet-xxx,subnet-yyy' SECURITY_GROUP_IDS='sg-xxx'$(NC)"; \ echo ""; \ exit 1; \ fi @if [ -z "$(SECURITY_GROUP_IDS)" ]; then \ echo ""; \ echo "$(RED)Error: SECURITY_GROUP_IDS is required$(NC)"; \ echo ""; \ echo "$(CYAN)To list available security groups, run:$(NC)"; \ echo " $(GREEN)make list-ecs-network-resources$(NC)"; \ echo ""; \ exit 1; \ fi @echo "$(GREEN)✓ Network configuration ready$(NC)" create-ecs-secret: ## Create configuration secret in AWS (Secrets Manager or SSM) @if [ -z "$(CONFIG_JSON_FILE)" ]; then \ echo "$(RED)Error: CONFIG_JSON_FILE is required for secret creation$(NC)"; \ echo "$(YELLOW)Provide CONFIG_JSON_FILE as path to your Bifrost config.json file$(NC)"; \ exit 1; \ fi @if [ ! -f "$(CONFIG_JSON_FILE)" ]; then \ echo "$(RED)Error: Config file not found: $(CONFIG_JSON_FILE)$(NC)"; \ exit 1; \ fi @echo "$(YELLOW)Creating configuration secret...$(NC)" @echo "$(CYAN)Reading config from: $(CONFIG_JSON_FILE)$(NC)" @if [ "$(SECRET_BACKEND)" = "secretsmanager" ]; then \ echo "$(CYAN)Using AWS Secrets Manager...$(NC)"; \ aws secretsmanager describe-secret --secret-id $(SECRET_NAME) --region $(AWS_REGION) > /dev/null 2>&1 && \ (echo "$(YELLOW)Secret already exists, updating...$(NC)" && \ aws secretsmanager update-secret \ --secret-id $(SECRET_NAME) \ --secret-string file://$(CONFIG_JSON_FILE) \ --region $(AWS_REGION) > /dev/null) || \ (echo "$(YELLOW)Creating new secret...$(NC)" && \ aws secretsmanager create-secret \ --name $(SECRET_NAME) \ --secret-string file://$(CONFIG_JSON_FILE) \ --region $(AWS_REGION) > /dev/null); \ echo "$(GREEN)✓ Secret created/updated in Secrets Manager: $(SECRET_NAME)$(NC)"; \ elif [ "$(SECRET_BACKEND)" = "ssm" ]; then \ echo "$(CYAN)Using AWS Systems Manager Parameter Store...$(NC)"; \ aws ssm put-parameter \ --name $(SECRET_NAME) \ --value file://$(CONFIG_JSON_FILE) \ --type SecureString \ --overwrite \ --region $(AWS_REGION) > /dev/null; \ echo "$(GREEN)✓ Parameter created/updated in SSM: $(SECRET_NAME)$(NC)"; \ else \ echo "$(RED)Error: SECRET_BACKEND must be 'secretsmanager' or 'ssm'$(NC)"; \ exit 1; \ fi register-ecs-task-definition: ## Register ECS task definition @echo "$(YELLOW)Registering ECS task definition...$(NC)" @echo "$(CYAN)Launch type: $(LAUNCH_TYPE)$(NC)" @# Create CloudWatch log group if it doesn't exist @echo "$(CYAN)Ensuring CloudWatch log group exists...$(NC)" @aws logs create-log-group \ --log-group-name /ecs/$(ECS_TASK_FAMILY) \ --region $(AWS_REGION) 2>/dev/null || true @echo "$(GREEN)✓ CloudWatch log group ready: /ecs/$(ECS_TASK_FAMILY)$(NC)" @# Get secret ARN if CONFIG_JSON_FILE was provided @if [ -n "$(CONFIG_JSON_FILE)" ]; then \ echo "$(CYAN)Secret backend: $(SECRET_BACKEND)$(NC)"; \ else \ echo "$(YELLOW)No CONFIG_JSON_FILE provided, deploying without secret$(NC)"; \ fi $(eval SECRET_VALUE_ARN := $(shell \ if [ -n "$(CONFIG_JSON_FILE)" ]; then \ if [ "$(SECRET_BACKEND)" = "secretsmanager" ]; then \ if [ -z "$(SECRET_ARN)" ]; then \ aws secretsmanager describe-secret --secret-id $(SECRET_NAME) --region $(AWS_REGION) --query 'ARN' --output text 2>/dev/null; \ else \ echo "$(SECRET_ARN)"; \ fi; \ elif [ "$(SECRET_BACKEND)" = "ssm" ]; then \ if [ -z "$(SECRET_ARN)" ]; then \ aws ssm get-parameter --name $(SECRET_NAME) --region $(AWS_REGION) --query 'Parameter.ARN' --output text 2>/dev/null; \ else \ echo "$(SECRET_ARN)"; \ fi; \ fi; \ fi)) @if [ -n "$(CONFIG_JSON_FILE)" ] && [ -z "$(SECRET_VALUE_ARN)" ]; then \ echo "$(RED)Error: Could not retrieve secret ARN$(NC)"; \ exit 1; \ fi @if [ -n "$(SECRET_VALUE_ARN)" ]; then \ echo "$(GREEN)✓ Secret ARN: $(SECRET_VALUE_ARN)$(NC)"; \ fi @# Create task definition JSON using shell script for proper JSON formatting @TASK_CPU="256"; \ TASK_MEMORY="512"; \ if [ "$(LAUNCH_TYPE)" = "FARGATE" ]; then \ TASK_CPU="512"; \ TASK_MEMORY="1024"; \ fi; \ EXEC_ROLE="$(EXECUTION_ROLE_ARN)"; \ if [ -z "$$EXEC_ROLE" ] && [ -f /tmp/ecs-execution-role.tmp ]; then \ EXEC_ROLE=$$(cat /tmp/ecs-execution-role.tmp); \ fi; \ { \ echo '{'; \ echo ' "family": "$(ECS_TASK_FAMILY)",'; \ echo ' "networkMode": "awsvpc",'; \ echo ' "requiresCompatibilities": ["$(LAUNCH_TYPE)"],'; \ if [ "$(LAUNCH_TYPE)" = "FARGATE" ]; then \ echo ' "cpu": "'$$TASK_CPU'",'; \ echo ' "memory": "'$$TASK_MEMORY'",'; \ fi; \ if [ -n "$$EXEC_ROLE" ]; then \ echo ' "executionRoleArn": "'$$EXEC_ROLE'",'; \ fi; \ if [ -n "$(TASK_ROLE_ARN)" ]; then \ echo ' "taskRoleArn": "$(TASK_ROLE_ARN)",'; \ fi; \ echo ' "containerDefinitions": [{'; \ echo ' "name": "bifrost",'; \ echo ' "image": "maximhq/bifrost:$(IMAGE_TAG)",'; \ echo ' "essential": true,'; \ if [ -n "$(SECRET_VALUE_ARN)" ]; then \ echo ' "entryPoint": ["/bin/sh", "-c"],'; \ echo ' "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"],'; \ fi; \ echo ' "portMappings": [{'; \ echo ' "containerPort": $(CONTAINER_PORT),'; \ echo ' "protocol": "tcp"'; \ echo ' }],'; \ echo ' "environment": [],'; \ if [ -n "$(SECRET_VALUE_ARN)" ]; then \ echo ' "secrets": [{'; \ echo ' "name": "BIFROST_CONFIG",'; \ echo ' "valueFrom": "$(SECRET_VALUE_ARN)"'; \ echo ' }],'; \ fi; \ echo ' "healthCheck": {'; \ echo ' "command": ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:$(CONTAINER_PORT)/health || exit 1"],'; \ echo ' "interval": 30,'; \ echo ' "timeout": 5,'; \ echo ' "retries": 3,'; \ echo ' "startPeriod": 60'; \ echo ' },'; \ echo ' "logConfiguration": {'; \ echo ' "logDriver": "awslogs",'; \ echo ' "options": {'; \ echo ' "awslogs-group": "/ecs/$(ECS_TASK_FAMILY)",'; \ echo ' "awslogs-region": "$(AWS_REGION)",'; \ echo ' "awslogs-stream-prefix": "bifrost"'; \ echo ' }'; \ echo ' }'; \ echo ' }]'; \ echo '}'; \ } > /tmp/ecs-task-def.json @# Register task definition @aws ecs register-task-definition \ --cli-input-json file:///tmp/ecs-task-def.json \ --region $(AWS_REGION) > /dev/null @rm -f /tmp/ecs-task-def.json @echo "$(GREEN)✓ Task definition registered: $(ECS_TASK_FAMILY)$(NC)" create-ecs-service: ## Create or update ECS service @echo "$(YELLOW)Creating/updating ECS service...$(NC)" @# Get subnet IDs from parameter or temp file @SUBNETS="$(SUBNET_IDS)"; \ if [ -z "$$SUBNETS" ] && [ -f /tmp/ecs-subnets.tmp ]; then \ SUBNETS=$$(cat /tmp/ecs-subnets.tmp); \ fi; \ if aws ecs describe-services --cluster $(ECS_CLUSTER_NAME) --services $(ECS_SERVICE_NAME) --region $(AWS_REGION) 2>/dev/null | grep -q "ACTIVE"; then \ echo "$(YELLOW)Service exists, updating...$(NC)"; \ $(MAKE) update-ecs-service; \ else \ echo "$(YELLOW)Creating new service...$(NC)"; \ if [ -z "$(TARGET_GROUP_ARN)" ]; then \ echo "$(CYAN)Creating service without load balancer...$(NC)"; \ aws ecs create-service \ --cluster $(ECS_CLUSTER_NAME) \ --service-name $(ECS_SERVICE_NAME) \ --task-definition $(ECS_TASK_FAMILY) \ --desired-count 1 \ --launch-type $(LAUNCH_TYPE) \ --network-configuration "awsvpcConfiguration={subnets=[$$SUBNETS],securityGroups=[$(SECURITY_GROUP_IDS)],assignPublicIp=ENABLED}" \ --region $(AWS_REGION) > /dev/null; \ else \ echo "$(CYAN)Creating service with load balancer...$(NC)"; \ aws ecs create-service \ --cluster $(ECS_CLUSTER_NAME) \ --service-name $(ECS_SERVICE_NAME) \ --task-definition $(ECS_TASK_FAMILY) \ --desired-count 1 \ --launch-type $(LAUNCH_TYPE) \ --network-configuration "awsvpcConfiguration={subnets=[$$SUBNETS],securityGroups=[$(SECURITY_GROUP_IDS)],assignPublicIp=ENABLED}" \ --load-balancers "targetGroupArn=$(TARGET_GROUP_ARN),containerName=bifrost,containerPort=$(CONTAINER_PORT)" \ --health-check-grace-period-seconds 60 \ --region $(AWS_REGION) > /dev/null; \ fi; \ echo "$(GREEN)✓ Service created: $(ECS_SERVICE_NAME)$(NC)"; \ rm -f /tmp/ecs-subnets.tmp /tmp/ecs-execution-role.tmp; \ fi @echo "" @echo "$(YELLOW)Waiting for service to stabilize...$(NC)" @echo "$(CYAN)This may take a few minutes...$(NC)" @aws ecs wait services-stable \ --cluster $(ECS_CLUSTER_NAME) \ --services $(ECS_SERVICE_NAME) \ --region $(AWS_REGION) && \ echo "$(GREEN)✓ Service is stable and running!$(NC)" || \ (echo "$(RED)✗ Service failed to stabilize$(NC)" && exit 1) @echo "" @echo "$(CYAN)Deployment Status:$(NC)" @aws ecs describe-services \ --cluster $(ECS_CLUSTER_NAME) \ --services $(ECS_SERVICE_NAME) \ --region $(AWS_REGION) \ --query 'services[0].{Status:status,Running:runningCount,Desired:desiredCount,Pending:pendingCount}' \ --output table @echo "" @echo "$(CYAN)Task Details:$(NC)" @TASK_ARN=$$(aws ecs list-tasks \ --cluster $(ECS_CLUSTER_NAME) \ --service-name $(ECS_SERVICE_NAME) \ --region $(AWS_REGION) \ --query 'taskArns[0]' \ --output text 2>/dev/null); \ if [ -n "$$TASK_ARN" ] && [ "$$TASK_ARN" != "None" ]; then \ aws ecs describe-tasks \ --cluster $(ECS_CLUSTER_NAME) \ --tasks $$TASK_ARN \ --region $(AWS_REGION) \ --query 'tasks[0].{TaskARN:taskArn,LastStatus:lastStatus,HealthStatus:healthStatus,StartedAt:startedAt}' \ --output table; \ echo ""; \ if [ -z "$(TARGET_GROUP_ARN)" ]; then \ echo "$(CYAN)Public IP Address:$(NC)"; \ ENI_ID=$$(aws ecs describe-tasks \ --cluster $(ECS_CLUSTER_NAME) \ --tasks $$TASK_ARN \ --region $(AWS_REGION) \ --query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' \ --output text 2>/dev/null); \ if [ -n "$$ENI_ID" ] && [ "$$ENI_ID" != "None" ]; then \ PUBLIC_IP=$$(aws ec2 describe-network-interfaces \ --network-interface-ids $$ENI_ID \ --region $(AWS_REGION) \ --query 'NetworkInterfaces[0].Association.PublicIp' \ --output text 2>/dev/null); \ if [ -n "$$PUBLIC_IP" ] && [ "$$PUBLIC_IP" != "None" ]; then \ echo " $$PUBLIC_IP"; \ echo ""; \ echo "$(GREEN)✓ Service is accessible at: http://$$PUBLIC_IP:$(CONTAINER_PORT)$(NC)"; \ echo "$(CYAN) Health check: http://$$PUBLIC_IP:$(CONTAINER_PORT)/health$(NC)"; \ else \ echo " $(YELLOW)Public IP not yet assigned$(NC)"; \ fi; \ else \ echo " $(YELLOW)Network interface not found$(NC)"; \ fi; \ echo ""; \ fi; \ echo "$(CYAN)Recent logs (last 20 events):$(NC)"; \ LOG_STREAM=$$(aws logs describe-log-streams \ --log-group-name /ecs/$(ECS_TASK_FAMILY) \ --order-by LastEventTime \ --descending \ --max-items 1 \ --region $(AWS_REGION) \ --query 'logStreams[0].logStreamName' \ --output text 2>/dev/null); \ if [ -n "$$LOG_STREAM" ] && [ "$$LOG_STREAM" != "None" ]; then \ aws logs get-log-events \ --log-group-name /ecs/$(ECS_TASK_FAMILY) \ --log-stream-name $$LOG_STREAM \ --limit 20 \ --region $(AWS_REGION) \ --query 'events[*].message' \ --output text 2>/dev/null || echo "$(YELLOW)No logs available yet$(NC)"; \ else \ echo "$(YELLOW)No log stream found yet$(NC)"; \ fi; \ else \ echo "$(YELLOW)No tasks running yet$(NC)"; \ fi @echo "" @echo "$(GREEN)To tail logs continuously, run:$(NC)" @echo " $(CYAN)make tail-ecs-logs$(NC)" update-ecs-service: ## Force new deployment of ECS service @echo "$(YELLOW)Forcing new deployment...$(NC)" @aws ecs update-service \ --cluster $(ECS_CLUSTER_NAME) \ --service $(ECS_SERVICE_NAME) \ --force-new-deployment \ --region $(AWS_REGION) > /dev/null @echo "$(GREEN)✓ Service updated with new deployment$(NC)" tail-ecs-logs: ## Tail CloudWatch logs for the ECS service (Ctrl+C to exit) @echo "$(YELLOW)Tailing logs from /ecs/$(ECS_TASK_FAMILY)...$(NC)" @echo "$(CYAN)Press Ctrl+C to stop$(NC)" @echo "" @# Use aws logs tail command (requires AWS CLI v2) @if aws logs tail --help > /dev/null 2>&1; then \ aws logs tail /ecs/$(ECS_TASK_FAMILY) \ --follow \ --format short \ --region $(AWS_REGION); \ else \ echo "$(YELLOW)AWS CLI v2 'tail' command not available, falling back to polling...$(NC)"; \ echo ""; \ LAST_TIMESTAMP=0; \ while true; do \ LOG_STREAM=$$(aws logs describe-log-streams \ --log-group-name /ecs/$(ECS_TASK_FAMILY) \ --order-by LastEventTime \ --descending \ --max-items 1 \ --region $(AWS_REGION) \ --query 'logStreams[0].logStreamName' \ --output text 2>/dev/null); \ if [ -n "$$LOG_STREAM" ] && [ "$$LOG_STREAM" != "None" ]; then \ if [ $$LAST_TIMESTAMP -eq 0 ]; then \ EVENTS=$$(aws logs get-log-events \ --log-group-name /ecs/$(ECS_TASK_FAMILY) \ --log-stream-name $$LOG_STREAM \ --limit 10 \ --region $(AWS_REGION) 2>/dev/null); \ else \ EVENTS=$$(aws logs get-log-events \ --log-group-name /ecs/$(ECS_TASK_FAMILY) \ --log-stream-name $$LOG_STREAM \ --start-time $$LAST_TIMESTAMP \ --region $(AWS_REGION) 2>/dev/null); \ fi; \ if [ -n "$$EVENTS" ]; then \ echo "$$EVENTS" | jq -r '.events[] | "\(.timestamp | todate) \(.message)"' 2>/dev/null || \ echo "$$EVENTS" | grep -o '"message":"[^"]*"' | sed 's/"message":"//;s/"$$//'; \ NEW_TIMESTAMP=$$(echo "$$EVENTS" | jq -r '.events[-1].timestamp // 0' 2>/dev/null); \ if [ -n "$$NEW_TIMESTAMP" ] && [ "$$NEW_TIMESTAMP" != "0" ] && [ "$$NEW_TIMESTAMP" != "null" ]; then \ LAST_TIMESTAMP=$$(($$NEW_TIMESTAMP + 1)); \ fi; \ fi; \ fi; \ sleep 2; \ done; \ fi ecs-status: ## Show current ECS service status and recent logs @echo "$(CYAN)Service Status:$(NC)" @aws ecs describe-services \ --cluster $(ECS_CLUSTER_NAME) \ --services $(ECS_SERVICE_NAME) \ --region $(AWS_REGION) \ --query 'services[0].{Status:status,Running:runningCount,Desired:desiredCount,Pending:pendingCount,Events:events[0:3]}' \ --output table @echo "" @echo "$(CYAN)Running Tasks:$(NC)" @aws ecs list-tasks \ --cluster $(ECS_CLUSTER_NAME) \ --service-name $(ECS_SERVICE_NAME) \ --region $(AWS_REGION) \ --query 'taskArns[]' \ --output table @echo "" @echo "$(CYAN)Recent Logs:$(NC)" @LOG_STREAM=$$(aws logs describe-log-streams \ --log-group-name /ecs/$(ECS_TASK_FAMILY) \ --order-by LastEventTime \ --descending \ --max-items 1 \ --region $(AWS_REGION) \ --query 'logStreams[0].logStreamName' \ --output text 2>/dev/null); \ if [ -n "$$LOG_STREAM" ] && [ "$$LOG_STREAM" != "None" ]; then \ aws logs get-log-events \ --log-group-name /ecs/$(ECS_TASK_FAMILY) \ --log-stream-name $$LOG_STREAM \ --limit 20 \ --region $(AWS_REGION) \ --query 'events[*].message' \ --output text 2>/dev/null; \ else \ echo "$(YELLOW)No log stream found$(NC)"; \ fi get-ecs-url: ## Get the public URL/IP of the ECS service @echo "$(YELLOW)Fetching service URL...$(NC)" @echo "" @# Check if service uses load balancer @LB_ARN=$$(aws ecs describe-services \ --cluster $(ECS_CLUSTER_NAME) \ --services $(ECS_SERVICE_NAME) \ --region $(AWS_REGION) \ --query 'services[0].loadBalancers[0].targetGroupArn' \ --output text 2>/dev/null); \ if [ -n "$$LB_ARN" ] && [ "$$LB_ARN" != "None" ]; then \ echo "$(CYAN)Service is using Application Load Balancer$(NC)"; \ LB_ARN=$$(aws elbv2 describe-target-groups \ --target-group-arns $$LB_ARN \ --region $(AWS_REGION) \ --query 'TargetGroups[0].LoadBalancerArns[0]' \ --output text 2>/dev/null); \ if [ -n "$$LB_ARN" ] && [ "$$LB_ARN" != "None" ]; then \ LB_DNS=$$(aws elbv2 describe-load-balancers \ --load-balancer-arns $$LB_ARN \ --region $(AWS_REGION) \ --query 'LoadBalancers[0].DNSName' \ --output text 2>/dev/null); \ echo ""; \ echo "$(GREEN)✓ Load Balancer URL: http://$$LB_DNS$(NC)"; \ echo "$(CYAN) Health check: http://$$LB_DNS/health$(NC)"; \ fi; \ else \ echo "$(CYAN)Service is using direct public IP (no load balancer)$(NC)"; \ TASK_ARN=$$(aws ecs list-tasks \ --cluster $(ECS_CLUSTER_NAME) \ --service-name $(ECS_SERVICE_NAME) \ --region $(AWS_REGION) \ --query 'taskArns[0]' \ --output text 2>/dev/null); \ if [ -n "$$TASK_ARN" ] && [ "$$TASK_ARN" != "None" ]; then \ ENI_ID=$$(aws ecs describe-tasks \ --cluster $(ECS_CLUSTER_NAME) \ --tasks $$TASK_ARN \ --region $(AWS_REGION) \ --query 'tasks[0].attachments[0].details[?name==`networkInterfaceId`].value' \ --output text 2>/dev/null); \ if [ -n "$$ENI_ID" ] && [ "$$ENI_ID" != "None" ]; then \ PUBLIC_IP=$$(aws ec2 describe-network-interfaces \ --network-interface-ids $$ENI_ID \ --region $(AWS_REGION) \ --query 'NetworkInterfaces[0].Association.PublicIp' \ --output text 2>/dev/null); \ if [ -n "$$PUBLIC_IP" ] && [ "$$PUBLIC_IP" != "None" ]; then \ echo ""; \ echo "$(GREEN)✓ Service URL: http://$$PUBLIC_IP:$(CONTAINER_PORT)$(NC)"; \ echo "$(CYAN) Health check: http://$$PUBLIC_IP:$(CONTAINER_PORT)/health$(NC)"; \ echo ""; \ echo "$(YELLOW)Note: Public IP may change if task is restarted. Consider using a load balancer for production.$(NC)"; \ else \ echo ""; \ echo "$(RED)✗ Public IP not assigned$(NC)"; \ echo "$(YELLOW)The task may still be starting or the service is not in a VPC with public subnets.$(NC)"; \ fi; \ else \ echo ""; \ echo "$(RED)✗ Network interface not found$(NC)"; \ fi; \ else \ echo ""; \ echo "$(RED)✗ No running tasks found$(NC)"; \ echo "$(YELLOW)Check service status with: make ecs-status$(NC)"; \ fi; \ fi @echo "" cleanup-ecs: ## Remove ECS service and task definitions @echo "$(YELLOW)Cleaning up ECS resources...$(NC)" @# Delete service @if aws ecs describe-services --cluster $(ECS_CLUSTER_NAME) --services $(ECS_SERVICE_NAME) --region $(AWS_REGION) 2>/dev/null | grep -q "ACTIVE"; then \ echo "$(YELLOW)Deleting service...$(NC)"; \ aws ecs update-service \ --cluster $(ECS_CLUSTER_NAME) \ --service $(ECS_SERVICE_NAME) \ --desired-count 0 \ --region $(AWS_REGION) > /dev/null; \ aws ecs delete-service \ --cluster $(ECS_CLUSTER_NAME) \ --service $(ECS_SERVICE_NAME) \ --region $(AWS_REGION) > /dev/null; \ echo "$(GREEN)✓ Service deleted$(NC)"; \ else \ echo "$(YELLOW)Service does not exist$(NC)"; \ fi @# Deregister all task definition revisions @echo "$(YELLOW)Deregistering task definitions...$(NC)" @for arn in $$(aws ecs list-task-definitions --family-prefix $(ECS_TASK_FAMILY) --region $(AWS_REGION) --query 'taskDefinitionArns[]' --output text); do \ aws ecs deregister-task-definition --task-definition $$arn --region $(AWS_REGION) > /dev/null; \ echo "$(GREEN)✓ Deregistered: $$arn$(NC)"; \ done @# Delete CloudWatch log group @echo "$(YELLOW)Deleting CloudWatch log group...$(NC)" @aws logs delete-log-group \ --log-group-name /ecs/$(ECS_TASK_FAMILY) \ --region $(AWS_REGION) 2>/dev/null || echo "$(YELLOW)Log group does not exist$(NC)" @echo "$(GREEN)✓ Log group deleted$(NC)" @# Clean up temp files @rm -f /tmp/ecs-subnets.tmp /tmp/ecs-execution-role.tmp /tmp/ecs-task-def.json @echo "$(GREEN)✓ Cleanup complete$(NC)"