もふもふ技術部

IT技術系mofmofメディア

ECSコンテナの立ち上げをやってみた

はじめに

今回は、Railsの基本スタックのインフラ構築をAWSで行うことに挑戦しました。アプリをAWS上でデプロイするにあたり、ECS(Elastic Container Service)を初めて利用してみました。Railsの基本スタックをAWSで簡単に構築すること、特にDockerとECSを用いたコンテナ化、SidekiqやElastiCacheの導入も含めて実践しました。この記事では、その過程や得られた学びを共有します。

デプロイの目的

  • Railsの基本スタックをAWSで構築
    • AWS上で、Railsアプリの本番環境を簡単にセットアップできるようになりたい。
  • Sidekiqの本番利用を経験する
    • これまでHerokuを使っていたため、AWSでのSidekiqのセットアップに挑戦。
  • Dockerでのデプロイ
    • Dockerで環境構築したアプリを効率的にデプロイできる環境を構築したい。
  • ECSの利用経験を積む
    • これまで使ったことのないECSを学びたかったため、今回の機会に挑戦してみました。
  • ElastiCacheの利用
    • ElastiCacheを利用したことがなかったため、試してみることにしました。

デプロイするアプリの構成

  • フレームワーク: Rails 7
  • Rubyバージョン: 3.2.4
  • データベース: PostgreSQL
  • バックグラウンドジョブ管理: Sidekiq
  • キャッシュ: Redis
  • サーバーレスコンテナ: ECS Fargate

アプリの機能

デプロイしたアプリは、シンプルな管理機能を持つもので、以下の機能を備えています。

  • 管理者ログイン
  • ユーザー一覧の表示
  • メール送信
  • Sidekiqダッシュボード
  • Redisデータのビューア

このアプリを選んだ理由は以下のとおりです。

  • バックグラウンド処理が視覚的に動作していることを確認したかった点
  • Redisのデータを直接確認してみることが面白いと感じた点
  • 基礎の基礎の機能は動かしたかった点

アプリ画像

デプロイ方法

今回は、CloudFormationやTerraformを使用せずに、AWS CLIを活用してインフラを構築するスクリプトを作成しました。スクリプトはChatGPTと相談しながら、AWSリソースの構築をスクリプト化することで、手動操作を減らし、デプロイの手間を軽減しました。

詰まったポイント

  • ECSのタスク概念
    • タスクの管理方法に戸惑い、タスクが起動したままの状態や管理方法について理解するのに時間がかかりました。
  • ロードバランサーとSSL設定
    • 既存のドメインを利用してSSL化する際、ロードバランサーとRoute 53の設定につまづきました。
  • リソース構築の順序
    • AWSリソースの作成順序に何度か詰まりましたが、試行錯誤の末、無事にリソースを構築できました。
  • ChatGPTでのスクリプト作成
    • スクリプトのコード量が増えるにつれて意図しない修正が加わることが多く、エラーなく動作させるのに苦労しました。ただ、400行程度のコードを出力できるのには驚きました。

どの点も理解すれば大したことではありませんが、実際に取り組む中での試行錯誤が良い経験になりました

感想と次の挑戦

AWS CLIを使ってのリソース構築は初めての経験で、手間も多く大変でしたが、その分インフラの知識不足を実感しました。また、久しぶりにRailsを触る機会があり、やはり楽しく感じました。次は、Terraformなどの自動化ツールを使って、効率的にインフラを構築してみたいと思います。


デプロイスクリプトの紹介

以下は実際に使用したデプロイ用のスクリプトです。これを使うことで、VPCやサブネットの作成から、ECSサービスやALBの設定まで、手動操作なしで構築できるようにしました。

別途ALBとドメイン, SESのSMTPの設定が必要です。

deploy.sh

#!/bin/bash

set -e

REGION="ap-northeast-1"
APP_NAME="sample-app"
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
VPC_CIDR="10.0.0.0/16"
SUBNET1_CIDR="10.0.1.0/24"
SUBNET2_CIDR="10.0.2.0/24"
DB_USERNAME="mydbuser"
DB_PASSWORD="mydbpassword"
DB_NAME="sampledb"
RDS_INSTANCE_CLASS="db.t3.micro"
CACHE_NODE_TYPE="cache.t3.micro"
ALB_NAME="${APP_NAME}-alb"
TARGET_GROUP_NAME="${APP_NAME}-targets"
SERVICE_NAME="${APP_NAME}-service"
CLUSTER_NAME="${APP_NAME}-cluster"
DB_INSTANCE_IDENTIFIER="${APP_NAME}-db"
CACHE_CLUSTER_ID="${APP_NAME}-cache"
DB_SUBNET_GROUP_NAME="${APP_NAME}-db-subnet-group"
CACHE_SUBNET_GROUP_NAME="${APP_NAME}-cache-subnet-group"
SECURITY_GROUP_NAME="${APP_NAME}-sg"
TASK_DEFINITION_NAME="${APP_NAME}-task"
TASK_EXECUTION_ROLE="arn:aws:iam::${ACCOUNT_ID}:role/ecsTaskExecutionRole"
SECRETS_NAME="${APP_NAME}-env"
DOMAIN_NAME="sample-app.example.com"

SKIP_BUILD_IMAGE=false

for arg in "$@"
do
    case $arg in
        --skip-build-image)
        SKIP_BUILD_IMAGE=true
        shift
        ;;
    esac
done

echo "Starting deployment..."

# Function to log and collect output
output_log="deployment_output.log"
function log {
    echo "$1" | tee -a $output_log
}

# Create or get ECR repository
if ! aws ecr describe-repositories --repository-names ${APP_NAME} --region ${REGION} > /dev/null 2>&1; then
  aws ecr create-repository --repository-name ${APP_NAME} --region ${REGION} > /dev/null 2>&1
  log "ECR repository ${APP_NAME} created."
else
  log "ECR repository ${APP_NAME} already exists."
fi

# Authenticate Docker to the ECR registry
aws ecr get-login-password --region ${REGION} | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com > /dev/null 2>&1

if [ "$SKIP_BUILD_IMAGE" = false ]; then
  # Build and push Docker image
  docker build --platform linux/amd64 . -t ${APP_NAME}
  docker tag ${APP_NAME}:latest ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${APP_NAME}:latest > /dev/null 2>&1
  docker push ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${APP_NAME}:latest > /dev/null 2>&1
  log "Docker image for ${APP_NAME} built and pushed to ECR."
else
  log "Skipping Docker image build and push."
fi

# Create or get VPC
VPC_ID=$(aws ec2 describe-vpcs --filters "Name=cidr-block,Values=${VPC_CIDR}" --query 'Vpcs[0].VpcId' --output text --region ${REGION} 2>&1)
if [ "$VPC_ID" = "None" ]; then
  VPC_ID=$(aws ec2 create-vpc --cidr-block ${VPC_CIDR} --query 'Vpc.VpcId' --output text --region ${REGION} 2>&1)
  log "VPC created with ID: ${VPC_ID}."
else
  log "VPC already exists with ID: ${VPC_ID}."
fi

# Create or get subnets
SUBNET_ID1=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=${VPC_ID}" "Name=cidr-block,Values=${SUBNET1_CIDR}" --query 'Subnets[0].SubnetId' --output text --region ${REGION} 2>&1)
if [ "$SUBNET_ID1" = "None" ]; then
  SUBNET_ID1=$(aws ec2 create-subnet --vpc-id ${VPC_ID} --cidr-block ${SUBNET1_CIDR} --availability-zone ${REGION}a --query 'Subnet.SubnetId' --output text --region ${REGION} 2>&1)
  log "Subnet created with ID: ${SUBNET_ID1}."
else
  log "Subnet already exists with ID: ${SUBNET_ID1}."
fi

aws ec2 modify-subnet-attribute --subnet-id ${SUBNET_ID1} --map-public-ip-on-launch

SUBNET_ID2=$(aws ec2 describe-subnets --filters "Name=vpc-id,Values=${VPC_ID}" "Name=cidr-block,Values=${SUBNET2_CIDR}" --query 'Subnets[0].SubnetId' --output text --region ${REGION} 2>&1)
if [ "$SUBNET_ID2" = "None" ]; then
  SUBNET_ID2=$(aws ec2 create-subnet --vpc-id ${VPC_ID} --cidr-block ${SUBNET2_CIDR} --availability-zone ${REGION}c --query 'Subnet.SubnetId' --output text --region ${REGION} 2>&1)
  log "Subnet created with ID: ${SUBNET_ID2}."
else
  log "Subnet already exists with ID: ${SUBNET_ID2}."
fi

aws ec2 modify-subnet-attribute --subnet-id ${SUBNET_ID2} --map-public-ip-on-launch

# Create and attach internet gateway
IGW_ID=$(aws ec2 describe-internet-gateways --filters "Name=attachment.vpc-id,Values=${VPC_ID}" --query 'InternetGateways[0].InternetGatewayId' --output text --region ${REGION} 2>&1)
if [ "$IGW_ID" = "None" ]; then
  IGW_ID=$(aws ec2 create-internet-gateway --query 'InternetGateway.InternetGatewayId' --output text --region ${REGION} 2>&1)
  aws ec2 attach-internet-gateway --vpc-id ${VPC_ID} --internet-gateway-id ${IGW_ID} --region ${REGION} > /dev/null 2>&1
  log "Internet gateway created and attached with ID: ${IGW_ID}."
else
  log "Internet gateway already exists with ID: ${IGW_ID}."
fi

# Create or get route table
ROUTE_TABLE_ID=$(aws ec2 describe-route-tables --filters "Name=vpc-id,Values=${VPC_ID}" --query 'RouteTables[0].RouteTableId' --output text --region ${REGION} 2>&1)
if [ "$ROUTE_TABLE_ID" = "None" ]; then
  ROUTE_TABLE_ID=$(aws ec2 create-route-table --vpc-id ${VPC_ID} --query 'RouteTable.RouteTableId' --output text --region ${REGION} 2>&1)
  log "Route table created with ID: ${ROUTE_TABLE_ID}."
else
  log "Route table already exists with ID: ${ROUTE_TABLE_ID}."
fi

# Create route
if ! aws ec2 describe-route-tables --route-table-ids ${ROUTE_TABLE_ID} --query 'RouteTables[0].Routes[?DestinationCidrBlock == `0.0.0.0/0`]' --output text --region ${REGION} 2>&1 | grep -q "0.0.0.0/0"; then
  aws ec2 create-route --route-table-id ${ROUTE_TABLE_ID} --destination-cidr-block 0.0.0.0/0 --gateway-id ${IGW_ID} --region ${REGION} > /dev/null 2>&1
  log "Route created in route table ${ROUTE_TABLE_ID}."
else
  log "Route already exists in route table ${ROUTE_TABLE_ID}."
fi

# Associate route table with subnets
aws ec2 associate-route-table --subnet-id ${SUBNET_ID1} --route-table-id ${ROUTE_TABLE_ID} --region ${REGION} > /dev/null 2>&1
aws ec2 associate-route-table --subnet-id ${SUBNET_ID2} --route-table-id ${ROUTE_TABLE_ID} --region ${REGION} > /dev/null 2>&1
log "Route table associated with subnets."

# Create or get security group
SECURITY_GROUP_ID=$(aws ec2 describe-security-groups --filters "Name=group-name,Values=${SECURITY_GROUP_NAME}" --query 'SecurityGroups[0].GroupId' --output text --region ${REGION} 2>&1)
if [ "$SECURITY_GROUP_ID" = "None" ]; then
  SECURITY_GROUP_ID=$(aws ec2 create-security-group --group-name ${SECURITY_GROUP_NAME} --description "Security group for ${APP_NAME}" --vpc-id ${VPC_ID} --query 'GroupId' --output text --region ${REGION} 2>&1)
  log "Security group created with ID: ${SECURITY_GROUP_ID}."
else
  log "Security group already exists with ID: ${SECURITY_GROUP_ID}."
fi

# Authorize security group ingress
function authorize_security_group_ingress {
  local group_id=$1
  local protocol=$2
  local port=$3
  local source_group_id=$4

  if ! aws ec2 describe-security-groups --group-ids $group_id --query "SecurityGroups[0].IpPermissions[?IpProtocol == '$protocol' && FromPort == \`$port\` && ToPort == \`$port\` && UserIdGroupPairs[0].GroupId == '$source_group_id']" --region $REGION 2>&1 | grep -q "$source_group_id"; then
    aws ec2 authorize-security-group-ingress --group-id $group_id --protocol $protocol --port $port --source-group $source_group_id --region $REGION > /dev/null 2>&1
    log "Ingress rule added to security group $group_id: $protocol $port from $source_group_id."
  else
    log "Ingress rule already exists in security group $group_id: $protocol $port from $source_group_id."
  fi
}

function authorize_security_group_ingress_cidr {
  local group_id=$1
  local protocol=$2
  local port=$3
  local cidr=$4

  if ! aws ec2 describe-security-groups --group-ids $group_id --query "SecurityGroups[0].IpPermissions[?IpProtocol == '$protocol' && FromPort == \`$port\` && ToPort == \`$port\` && IpRanges[0].CidrIp == '$cidr']" --region $REGION 2>&1 | grep -q "$cidr"; then
    aws ec2 authorize-security-group-ingress --group-id $group_id --protocol $protocol --port $port --cidr $cidr --region $REGION > /dev/null 2>&1
    log "Ingress rule added to security group $group_id: $protocol $port from $cidr."
  else
    log "Ingress rule already exists in security group $group_id: $protocol $port from $cidr."
  fi
}

authorize_security_group_ingress_cidr ${SECURITY_GROUP_ID} "tcp" 80 "0.0.0.0/0"
authorize_security_group_ingress ${SECURITY_GROUP_ID} "tcp" 5432 ${SECURITY_GROUP_ID}
authorize_security_group_ingress ${SECURITY_GROUP_ID} "tcp" 6379 ${SECURITY_GROUP_ID}
authorize_security_group_ingress_cidr ${SECURITY_GROUP_ID} "tcp" 3000 "0.0.0.0/0"

log "Security group ingress rules authorized."

# Create or get RDS subnet group and instance
if ! aws rds describe-db-subnet-groups --db-subnet-group-name ${DB_SUBNET_GROUP_NAME} --region ${REGION} > /dev/null 2>&1; then
  aws rds create-db-subnet-group --db-subnet-group-name ${DB_SUBNET_GROUP_NAME} --db-subnet-group-description "Subnet group for ${APP_NAME}" --subnet-ids ${SUBNET_ID1} ${SUBNET_ID2} --region ${REGION} > /dev/null 2>&1
  log "RDS subnet group created."
else
  log "RDS subnet group already exists."
fi

if ! aws rds describe-db-instances --db-instance-identifier ${DB_INSTANCE_IDENTIFIER} --region ${REGION} > /dev/null 2>&1; then
  aws rds create-db-instance --db-instance-identifier ${DB_INSTANCE_IDENTIFIER} --db-instance-class ${RDS_INSTANCE_CLASS} --engine postgres --master-username ${DB_USERNAME} --master-user-password ${DB_PASSWORD} --allocated-storage 20 --vpc-security-group-ids ${SECURITY_GROUP_ID} --db-subnet-group-name ${DB_SUBNET_GROUP_NAME} --region ${REGION} > /dev/null 2>&1
  aws rds wait db-instance-available --db-instance-identifier ${DB_INSTANCE_IDENTIFIER} --region ${REGION} > /dev/null 2>&1
  log "RDS instance created and available."
else
  log "RDS instance already exists."
fi

# Create or get ElastiCache subnet group and cluster
if ! aws elasticache describe-cache-subnet-groups --cache-subnet-group-name ${CACHE_SUBNET_GROUP_NAME} --region ${REGION} > /dev/null 2>&1; then
  aws elasticache create-cache-subnet-group --cache-subnet-group-name ${CACHE_SUBNET_GROUP_NAME} --cache-subnet-group-description "Subnet group for ${APP_NAME}" --subnet-ids ${SUBNET_ID1} ${SUBNET_ID2} --region ${REGION} > /dev/null 2>&1
  log "ElastiCache subnet group created."
else
  log "ElastiCache subnet group already exists."
fi

if ! aws elasticache describe-cache-clusters --cache-cluster-id ${CACHE_CLUSTER_ID} --region ${REGION} > /dev/null 2>&1; then
  aws elasticache create-cache-cluster --cache-cluster-id ${CACHE_CLUSTER_ID} --engine redis --cache-node-type ${CACHE_NODE_TYPE} --num-cache-nodes 1 --security-group-ids ${SECURITY_GROUP_ID} --cache-subnet-group-name ${CACHE_SUBNET_GROUP_NAME} --region ${REGION} > /dev/null 2>&1
  aws elasticache wait cache-cluster-available --cache-cluster-id ${CACHE_CLUSTER_ID} --region ${REGION} > /dev/null 2>&1
  log "ElastiCache cluster created and available."
else
  log "ElastiCache cluster already exists."
fi

# Get RDS and ElastiCache endpoints
RDS_ENDPOINT=$(aws rds describe-db-instances --db-instance-identifier ${DB_INSTANCE_IDENTIFIER} --query 'DBInstances[0].Endpoint.Address' --output text --region ${REGION} 2>&1)
CACHE_ENDPOINT=$(aws elasticache describe-cache-clusters --cache-cluster-id ${CACHE_CLUSTER_ID} --show-cache-node-info --query 'CacheClusters[0].CacheNodes[0].Endpoint.Address' --output text --region ${REGION} 2>&1)

# Write DB information to .env.deploy
echo "Writing DB information to .env.deploy"
cat > .env.deploy <<EOL
POSTGRES_HOST=${RDS_ENDPOINT}
POSTGRES_USER=${DB_USERNAME}
POSTGRES_PASSWORD=${DB_PASSWORD}
POSTGRES_DB=${DB_NAME}
REDIS_URL=redis://${CACHE_ENDPOINT}:6379/0
DATABASE_URL=postgresql://${DB_USERNAME}:${DB_PASSWORD}@${RDS_ENDPOINT}/${DB_NAME}
EOL

# Combine .env.production and .env.deploy
echo "Combining .env.production and .env.deploy"
cat .env.production .env.deploy > .env.full

# Create or get ECS cluster
if ! aws ecs describe-clusters --clusters ${CLUSTER_NAME} --region ${REGION} --query 'clusters[0].status' --output text 2>&1 | grep -q "ACTIVE"; then
  aws ecs create-cluster --cluster-name ${CLUSTER_NAME} --region ${REGION} > /dev/null 2>&1
  log "ECS cluster created with name: ${CLUSTER_NAME}."
else
  log "ECS cluster already exists with name: ${CLUSTER_NAME}."
fi

# Create or update Secrets Manager secret
ENV_VARIABLES=$(cat .env.full | jq -Rs 'split("\n") | map(select(length > 0)) | map(split("=")) | map({name: .[0], value: .[1]})')
if ! aws secretsmanager describe-secret --secret-id ${SECRETS_NAME} --region ${REGION} > /dev/null 2>&1; then
  aws secretsmanager create-secret --name ${SECRETS_NAME} --secret-string "$(echo $ENV_VARIABLES | jq -c '.')" --region ${REGION} > /dev/null 2>&1
  log "Secrets Manager secret created."
else
  aws secretsmanager update-secret --secret-id ${SECRETS_NAME} --secret-string "$(echo $ENV_VARIABLES | jq -c '.')" --region ${REGION} > /dev/null 2>&1
  log "Secrets Manager secret updated."
fi

# Create log group if it does not exist
if ! aws logs describe-log-groups --log-group-name-prefix /ecs/${TASK_DEFINITION_NAME} --region ${REGION} | grep -q ${TASK_DEFINITION_NAME}; then
  aws logs create-log-group --log-group-name /ecs/${TASK_DEFINITION_NAME} --region ${REGION} > /dev/null 2>&1
  log "Log group /ecs/${TASK_DEFINITION_NAME} created."
else
  log "Log group /ecs/${TASK_DEFINITION_NAME} already exists."
fi

# Create log group about sidekiq if it does not exist
if ! aws logs describe-log-groups --log-group-name-prefix /ecs/${TASK_DEFINITION_NAME}-sidekiq --region ${REGION} | grep -q ${TASK_DEFINITION_NAME}-sidekiq; then
  aws logs create-log-group --log-group-name /ecs/${TASK_DEFINITION_NAME}-sidekiq --region ${REGION} > /dev/null 2>&1
  log "Log group /ecs/${TASK_DEFINITION_NAME}-sidekiq created."
else
  log "Log group /ecs/${TASK_DEFINITION_NAME}-sidekiq already exists."
fi

# Register ECS task definition
ENV_VARS=$(cat .env.full | jq -R -s -c 'split("\n") | map(select(length > 0)) | map(split("=")) | map({"name": .[0], "value": .[1]})')

TASK_DEFINITION=$(cat <<EOF
{
  "family": "${TASK_DEFINITION_NAME}",
  "networkMode": "awsvpc",
  "containerDefinitions": [
    {
      "name": "${APP_NAME}-container",
      "image": "${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${APP_NAME}:latest",
      "essential": true,
      "memory": 256,
      "cpu": 128,
      "portMappings": [
        {
          "containerPort": 3000,
          "hostPort": 3000
        }
      ],
      "environment": $ENV_VARS,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/${TASK_DEFINITION_NAME}",
          "awslogs-region": "${REGION}",
          "awslogs-stream-prefix": "${APP_NAME}"
        }
      }
    },
    {
      "name": "${APP_NAME}-sidekiq",
      "image": "${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/${APP_NAME}:latest",
      "essential": true,
      "memory": 256,
      "cpu": 128,
      "command": ["sidekiq"],
      "environment": $ENV_VARS,
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/ecs/${TASK_DEFINITION_NAME}-sidekiq",
          "awslogs-region": "${REGION}",
          "awslogs-stream-prefix": "${APP_NAME}-sidekiq"
        }
      }
    }
  ],
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "${TASK_EXECUTION_ROLE}",
  "taskRoleArn": "${TASK_EXECUTION_ROLE}"
}
EOF
)

echo "${TASK_DEFINITION}" > task-definition.json
aws ecs register-task-definition --cli-input-json file://task-definition.json --region ${REGION} > /dev/null 2>&1

log "ECS task definition registered."

# Create or get ALB
ALB_ARN=$(aws elbv2 describe-load-balancers --names ${ALB_NAME} --query 'LoadBalancers[0].LoadBalancerArn' --output text --region ${REGION} 2>/dev/null || echo "None")
if [ "$ALB_ARN" = "None" ]; then
  ALB_ARN=$(aws elbv2 create-load-balancer \
    --name ${ALB_NAME} \
    --subnets ${SUBNET_ID1} ${SUBNET_ID2} \
    --security-groups ${SECURITY_GROUP_ID} \
    --region ${REGION} \
    --query 'LoadBalancers[0].LoadBalancerArn' \
    --output text)
  log "ALB created with ARN: ${ALB_ARN}."
else
  log "ALB already exists with ARN: ${ALB_ARN}."
fi

# Create or get Target Group
TARGET_GROUP_ARN=$(aws elbv2 describe-target-groups --names ${TARGET_GROUP_NAME} --query 'TargetGroups[0].TargetGroupArn' --output text --region ${REGION} 2>/dev/null || echo "None")
if [ "$TARGET_GROUP_ARN" = "None" ]; then
  TARGET_GROUP_ARN=$(aws elbv2 create-target-group \
    --name ${TARGET_GROUP_NAME} \
    --protocol HTTP \
    --port 3000 \
    --vpc-id ${VPC_ID} \
    --target-type ip \
    --query 'TargetGroups[0].TargetGroupArn' \
    --output text \
    --region ${REGION})
  log "Target group created with ARN: ${TARGET_GROUP_ARN}."
else
  log "Target group already exists with ARN: ${TARGET_GROUP_ARN}."
fi

# SSL化

# Request a new SSL certificate using ACM
CERTIFICATE_ARN=$(aws acm request-certificate \
  --domain-name ${DOMAIN_NAME} \
  --validation-method DNS \
  --query 'CertificateArn' \
  --output text \
  --region ${REGION})

log "SSL certificate requested with ARN: ${CERTIFICATE_ARN}."

# Wait until the certificate is issued
aws acm wait certificate-validated --certificate-arn ${CERTIFICATE_ARN} --region ${REGION}
log "SSL certificate issued."

# Create HTTPS listener on ALB
HTTPS_LISTENER_ARN=$(aws elbv2 describe-listeners --load-balancer-arn ${ALB_ARN} --query "Listeners[?Port==\`443\`].ListenerArn" --output text --region ${REGION})

if [ -z "$HTTPS_LISTENER_ARN" ]; then
  # Create HTTPS listener if it doesn't exist
  HTTPS_LISTENER_ARN=$(aws elbv2 create-listener \
    --load-balancer-arn ${ALB_ARN} \
    --protocol HTTPS \
    --port 443 \
    --certificates CertificateArn=${CERTIFICATE_ARN} \
    --default-actions Type=forward,TargetGroupArn=${TARGET_GROUP_ARN} \
    --query 'Listeners[0].ListenerArn' \
    --output text \
    --region ${REGION})
  log "HTTPS listener created with ARN: ${HTTPS_LISTENER_ARN}."
else
  log "HTTPS listener already exists with ARN: ${HTTPS_LISTENER_ARN}."
fi

authorize_security_group_ingress_cidr ${SECURITY_GROUP_ID} "tcp" 443 "0.0.0.0/0"
log "Security group updated to allow HTTPS traffic."

# Create or get HTTP listener on ALB (for redirect to HTTPS)
HTTP_LISTENER_ARN=$(aws elbv2 describe-listeners --load-balancer-arn ${ALB_ARN} --query "Listeners[?Port==\`80\`].ListenerArn" --output text --region ${REGION})

if [ -z "$HTTP_LISTENER_ARN" ]; then
  HTTP_LISTENER_ARN=$(aws elbv2 create-listener \
    --load-balancer-arn ${ALB_ARN} \
    --protocol HTTP \
    --port 80 \
    --default-actions Type=redirect,RedirectConfig="{Protocol=HTTPS,Port=443,StatusCode=HTTP_301}" \
    --query 'Listeners[0].ListenerArn' \
    --output text \
    --region ${REGION})
  log "HTTP listener created with ARN: ${HTTP_LISTENER_ARN} and redirects to HTTPS."
else
  log "HTTP listener already exists with ARN: ${HTTP_LISTENER_ARN}."
fi

# Create or update ECS service
SERVICE_STATUS=$(aws ecs describe-services --cluster ${CLUSTER_NAME} --services ${SERVICE_NAME} --region ${REGION} --query 'services[0].status' --output text 2>&1 || true)
if [[ "$SERVICE_STATUS" == "None" || "$SERVICE_STATUS" == "MISSING" || -z "$SERVICE_STATUS" ]]; then
    if ! aws ecs create-service --cluster ${CLUSTER_NAME} --service-name ${SERVICE_NAME} --task-definition ${TASK_DEFINITION_NAME} --desired-count 1 --launch-type FARGATE --network-configuration "awsvpcConfiguration={subnets=[${SUBNET_ID1},${SUBNET_ID2}],securityGroups=[${SECURITY_GROUP_ID}],assignPublicIp=ENABLED}" --load-balancers "targetGroupArn=${TARGET_GROUP_ARN},containerName=${APP_NAME}-container,containerPort=3000" --region ${REGION} > /dev/null 2>&1; then
      log "Failed to create ECS service."
      exit 1
    else
      log "ECS service created."
    fi
else
    if ! aws ecs update-service --cluster ${CLUSTER_NAME} --service ${SERVICE_NAME} --task-definition ${TASK_DEFINITION_NAME} --desired-count 1 --region ${REGION} > /dev/null 2>&1; then
      log "Failed to update ECS service."
      exit 1
    else
      log "ECS service updated."
    fi
fi

ALB_DNS_NAME=$(aws elbv2 describe-load-balancers \
  --names ${ALB_NAME} \
  --query 'LoadBalancers[0].DNSName' \
  --output text \
  --region ${REGION})

log "Deployment of ${APP_NAME} completed successfully!"
log "You can access the application at: https://${ALB_DNS_NAME}"

.env.production.sample

AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
RAILS_MASTER_KEY=
RAILS_ENV=production
SECRET_KEY_BASE=
SMTP_IAM_USER_NAME=
SMTP_USERNAME=
SMTP_PASSWORD=