Amazon Elastic Container Service (ECS)においてアベイラビリティゾーンのリバランシングを自由に設定できるようになりました[Amazon ECS + AWS CloudFormation]

# はじめに
[Japan AWS Jr. Champions Advent Calendar 2024](https://qiita.com/advent-calendar/2024/japan-aws-jr-champions) 20日目の記事です。
こんにちは。SCSKのふくちーぬです。皆さんre:Inventは楽しみましたでしょうか？インフラエンジニア的には、Pre re:Invent(re:Invent予選落ち)のアップデートがアツかったため、解説していきます。
今回は、Amazon Elastic Container Service (ECS)においてアベイラビリティゾーンのリバランシングを自由に設定できるようになりましたので紹介します。

## ECSにおいてアベイラビリティゾーンのリバランシングを自由に設定できるようになりました
2024/11/20のアップデートにより、コンテナ化されたワークロードをアベイラビリティーゾーン (AZ) 間で自動的に再分散する新機能リバランㇲの機能が利用できるようになりました。

https://aws.amazon.com/jp/about-aws/whats-new/2024/11/amazon-ecs-az-rebalancing-speeds-mean-time-recovery-event/?nc1=h_ls

### リバランシングが無効な場合
ECSでは、タスクを複数のアベイラビリティゾーンに分散させることで、アプリケーションの耐障害性を高めることがベストプラクティスです。しかし、何らかの理由でECSサービスのタスク分散が不均衡な状態になった際アベイラビリティーゾーン障害が発生すると、意図せず可用性が失われる可能性を含んでいました。つまりECSのタスク分散は、ベストエフォートであるという仕様でした。

### リバランシングが有効な場合
今回のアップデートにより、ECS側ではタスクが不均衡な状態になった場合でも、自動的に認識し調整することで、タスク配置を実施し均一な状態にします。例えアベイラビリティゾーン障害が発生した場合でも、アプリケーションの高可用性を維持できるようになりました。

# 検証
今回のアップデートを早速検証してみます。リバランシングを有効に設定して、不均衡なタスクが均一な状態に自動調整されることを確認することがゴールとなります。

### 事前準備
VPC、サブネット、ルートテーブル、インターネットゲートウェイ、ネットワークACL、ECRが作成済みであることを確認してください。

### ECRへコンテナイメージをpush
nginx-helloリポジトリにイメージをpushします。
![ECSマルチAZ1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925293/06161692-6197-a90d-f7a8-6792c54f4274.png)


下記のdockerfileを利用して、コンテナイメージを作成します。
```dockerfile:Dockerfile
# ベースイメージとしてnginxの公式イメージを使用
FROM nginx:latest

# "Hello, world!" を返すHTMLファイルを作成
RUN echo "Hello, world!" > /usr/share/nginx/html/index.html

#80番ポートで公開
```
Cloud9やCloudShell等のdockerインストール済みのサーバを用意して、上記のdockerfileをビルドします。

サーバのIAMロールには、下記の権限を付与しておいてください。

・arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryPowerUser

```
docker build . -t <AWSアカウントId>.dkr.ecr.ap-northeast-1.amazonaws.com/nginx-hello:latest
```
ECRへログインした後に、ECRのリポジトリにコンテナイメージをpushします。
```
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin <AWSアカウントId>.dkr.ecr.us-east-1.amazonaws.com
docker push <AWSアカウントId>.dkr.ecr.us-east-1.amazonaws.com/nginx-hello:latest
```
ECRのリポジトリ内にコンテナイメージが1つ格納されます。

## CloudFormationテンプレート
AvailabilityZoneRebalancingプロパティを設定します。ENABLED/DISABLEDを選択することが可能です。

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ecs-service.html#cfn-ecs-service-availabilityzonerebalancing

まずは、無効化するために”DISABLED”を設定します。

以下のテンプレートを使用して、デプロイします。
```python
AWSTemplateFormatVersion: 2010-09-09
# ---------------------------------
# パラメータ
# ---------------------------------
Parameters: 
  Env:
    Type: String 
  VpcId:
    Type: String    
  PublicSubnetA:
    Type: String     
  PublicSubnetB:
    Type: String      
  MyIp:
    Type: String    

Resources:
  # ================================
  # ECS (Cluster)
  # ================================
  ECSCluster:
    Type: "AWS::ECS::Cluster"
    Properties:
      ClusterName: !Sub "ECS-${Env}-helloworld-cluster"
      ServiceConnectDefaults:
        Namespace: !Sub "ECS-${Env}-helloworld-cluster"
      CapacityProviders:
        - FARGATE
  # ================================
  # ECS (Task Difinition)
  # ================================          
  ECSTaskDefinition:
    Type: "AWS::ECS::TaskDefinition"
    UpdateReplacePolicy: Retain
    Properties:
      Cpu: 256
      ExecutionRoleArn: !Sub "arn:aws:iam::${AWS::AccountId}:role/ecsTaskExecutionRole"
      Family: !Sub "ECS-${Env}-helloworld-taskdef"
      Memory: 512
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - FARGATE
      ContainerDefinitions:
        - Name: helloworld
          Image: !Sub "${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/nginx-hello:latest"
          versionConsistency: disabled
          LogConfiguration:
            LogDriver: awslogs
            Options:
              awslogs-group: !Sub "/ecs/ECS-${Env}-helloworld-service" 
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: latest
          PortMappings:
            - AppProtocol: http
              HostPort: 80
              Protocol: tcp
              ContainerPort: 80
              Name: helloworld-8080-tcp
          ReadonlyRootFilesystem: false
      RuntimePlatform: 
        CpuArchitecture: X86_64
        OperatingSystemFamily: LINUX
# ------------------------------------------------------------#
#  Security Group
# ------------------------------------------------------------#        
  ECSServiceSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties: 
      GroupDescription: ecs security group 
      VpcId: !Ref VpcId
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: !Ref MyIp
      SecurityGroupEgress:
        - IpProtocol: -1
          FromPort: -1
          ToPort: -1
          CidrIp: "0.0.0.0/0"     
# ------------------------------------------------------------#
#  ECS Service
# ------------------------------------------------------------#
  ECSService:
    Type: AWS::ECS::Service
    Properties:
      AvailabilityZoneRebalancing: DISABLED
      Cluster: !Ref ECSCluster
      DesiredCount: 20
      DeploymentConfiguration: 
        DeploymentCircuitBreaker: 
          Enable: TRUE
          Rollback: TRUE
        MaximumPercent: 200
        MinimumHealthyPercent: 100
      DeploymentController:
        Type: ECS
      LaunchType: FARGATE
      NetworkConfiguration:
        AwsvpcConfiguration:
            AssignPublicIp: ENABLED
            SecurityGroups:
              - !Ref ECSServiceSecurityGroup
            Subnets:
              - !Ref PublicSubnetA    
              - !Ref PublicSubnetB
      PlatformVersion: 1.4.0
      ServiceName: !Sub "ECS-${Env}-helloworld-service"
      TaskDefinition: !Ref ECSTaskDefinition  
# ------------------------------------------------------------#
#  ECS LogGroup
# ------------------------------------------------------------#
  ECSLogGroup:
    Type: "AWS::Logs::LogGroup"
    Properties:
      LogGroupName: !Sub "/ecs/ECS-${Env}-helloworld-service" 
```
### 下記コマンドで、タスクの配置場所を確認しておきます。
```
sh ecs.sh
```
```Bash:ecs.sh
#!/bin/bash

# クラスター名を設定
CLUSTER_NAME="ECS-dev-helloworld-cluster"
# タスクARNを取得し100個ごとに分割して処理
aws ecs list-tasks --cluster $CLUSTER_NAME --query "taskArns" --output text | tr '\t' '\n' > all_tasks.txt
split -l 100 all_tasks.txt tasks_chunk_

declare -A AZ_COUNT

for chunk in tasks_chunk_*; do
  TASK_IDS=$(cat $chunk | tr '\n' ' ')
  
  # Describe tasks and get availability zones
  AZ_LIST=$(aws ecs describe-tasks --cluster $CLUSTER_NAME --tasks $TASK_IDS \
            --query "tasks[*].availabilityZone" --output text)

  # Increment AZ counts in the associative array
  for az in $AZ_LIST; do
    if [[ -n "${AZ_COUNT[$az]}" ]]; then
      AZ_COUNT[$az]=$((AZ_COUNT[$az] + 1))
    else
      AZ_COUNT[$az]=1
    fi
  done
done

# AZごとにタスク数を出力
echo "Availability Zone Task Counts:"
for az in "${!AZ_COUNT[@]}"; do
  echo "AZ: $az, TaskCount: ${AZ_COUNT[$az]}"
done

# Cleanup temporary files
rm -f all_tasks.txt tasks_chunk_*
```

AZ-A、AZ-Bに10つのタスクが分散されている初期状態を作り出します。
![ECSマルチAZ2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925293/4d0292a1-3509-e8bf-a613-214ebab44ca6.png)

## 不均一な状態を作りだすには？
今回は、強制的に不均一な状態を作りだすために、以下のようにCloudFormationテンプレートを更新して、スタックを再デプロイします。

サブネットをAZ-Aのみに指定します。
```python
+            Subnets:
               - !Ref PublicSubnetA    
-            Subnets:
               - !Ref PublicSubnetA    
               - !Ref PublicSubnetB
```
デプロイ完了後に、以下のようにCloudFormationテンプレートを戻して再度デプロイします。
```python
+            Subnets:
               - !Ref PublicSubnetA   
               - !Ref PublicSubnetB              
-            Subnets:
               - !Ref PublicSubnetA    
```

再度[コマンド](###タスクの配置場所を確認しておきます。)を打って、タスクの配置場所を確認しておきます。
![ECSマルチAZ3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925293/af3cb9c0-52a5-8317-41c3-2dc8e808eeda.png)
AZ-Aのみにタスクが配置されている状態を作り出すことができました。リバランシングが無効なため、従来通りタスクの再配置がされない仕様となります。
このままだと、AZ-Aのアベイラビリティゾーン障害が発生した際に、コンテンツの提供が停止してしまいます。

## リバランシングの有効
リバランシングを有効にするためにECSサービスの"AvailabilityZoneRebalancing"の値を変更します。スタックを再デプロイします。
```python
+    Properties:
       AvailabilityZoneRebalancing: ENABLED
-    Properties:
       AvailabilityZoneRebalancing: DISABLED 
```
少々待つと、リバランシング機能が正常に動いたことが分かります。
他のリージョンや時間帯により検知時間が変化する可能性はありますが、
参考までにバージニア北部リージョンで日本時間1時の場合だと以下のような実測値でした。
①不均衡な状態を検知（ここまで1分かからず）
②リバランシングを開始（ここまで1分30秒）
③均一な状態を維持（3分経過）

![ECSマルチAZ4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925293/9fc9db5a-4e85-4566-21a0-b94ba36f451c.png)
思ったよりかなり迅速にリバランシングの検知ができることが判明しました。


念のため、再度[コマンド](###タスクの配置場所を確認しておきます。)を打って、タスクの配置場所を確認しておきます
AZ-A、AZ-Bにタスクが均一に自動で配置されました。もし、AZ-Aのアベイラビリティゾーン障害が発生した際でも、AZ-Bでコンテンツを提供し続けることが可能となります。
![ECSマルチAZ5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3925293/a0ec8eb3-6e07-2b07-085b-078fea3f0d3f.png)


# まとめ
いかがだったでしょうか。

今まで皆さんがECS利用時に気にされていた痒いところに手が届いたアップデートだったのではないでしょうか。

従来、ECSのリバランシングをLambda等で作りこんでいた箇所が、ECS側で対応された点が嬉しいですね。1点注意事項としては、今回は迅速な検知をしてくれましたが、リバランシングの検知自体はベストエフォートな点です。今後は基本的にECSのリバランシングは有効にして利用していただけますと、高可用性アプリケーションを提供できます。

本記事が皆様のお役にたてば幸いです。

ではサウナラ～🔥