はじめに
Japan AWS Jr. Champions Advent Calendar 2024 20日目の記事です。
こんにちは。SCSKのふくちーぬです。皆さんre:Inventは楽しみましたでしょうか?インフラエンジニア的には、Pre re:Invent(re:Invent予選落ち)のアップデートがアツかったため、解説していきます。
今回は、Amazon Elastic Container Service (ECS)においてアベイラビリティゾーンのリバランシングを自由に設定できるようになりましたので紹介します。
ECSにおいてアベイラビリティゾーンのリバランシングを自由に設定できるようになりました
2024/11/20のアップデートにより、コンテナ化されたワークロードをアベイラビリティーゾーン (AZ) 間で自動的に再分散する新機能リバランㇲの機能が利用できるようになりました。
リバランシングが無効な場合
ECSでは、タスクを複数のアベイラビリティゾーンに分散させることで、アプリケーションの耐障害性を高めることがベストプラクティスです。しかし、何らかの理由でECSサービスのタスク分散が不均衡な状態になった際アベイラビリティーゾーン障害が発生すると、意図せず可用性が失われる可能性を含んでいました。つまりECSのタスク分散は、ベストエフォートであるという仕様でした。
リバランシングが有効な場合
今回のアップデートにより、ECS側ではタスクが不均衡な状態になった場合でも、自動的に認識し調整することで、タスク配置を実施し均一な状態にします。例えアベイラビリティゾーン障害が発生した場合でも、アプリケーションの高可用性を維持できるようになりました。
検証
今回のアップデートを早速検証してみます。リバランシングを有効に設定して、不均衡なタスクが均一な状態に自動調整されることを確認することがゴールとなります。
事前準備
VPC、サブネット、ルートテーブル、インターネットゲートウェイ、ネットワークACL、ECRが作成済みであることを確認してください。
ECRへコンテナイメージをpush
nginx-helloリポジトリにイメージをpushします。
下記の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を選択することが可能です。
まずは、無効化するために”DISABLED”を設定します。
以下のテンプレートを使用して、デプロイします。
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
#!/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つのタスクが分散されている初期状態を作り出します。
不均一な状態を作りだすには?
今回は、強制的に不均一な状態を作りだすために、以下のようにCloudFormationテンプレートを更新して、スタックを再デプロイします。
サブネットをAZ-Aのみに指定します。
+ Subnets:
- !Ref PublicSubnetA
- Subnets:
- !Ref PublicSubnetA
- !Ref PublicSubnetB
デプロイ完了後に、以下のようにCloudFormationテンプレートを戻して再度デプロイします。
+ Subnets:
- !Ref PublicSubnetA
- !Ref PublicSubnetB
- Subnets:
- !Ref PublicSubnetA
再度コマンドを打って、タスクの配置場所を確認しておきます。
AZ-Aのみにタスクが配置されている状態を作り出すことができました。リバランシングが無効なため、従来通りタスクの再配置がされない仕様となります。
このままだと、AZ-Aのアベイラビリティゾーン障害が発生した際に、コンテンツの提供が停止してしまいます。
リバランシングの有効
リバランシングを有効にするためにECSサービスの"AvailabilityZoneRebalancing"の値を変更します。スタックを再デプロイします。
+ Properties:
AvailabilityZoneRebalancing: ENABLED
- Properties:
AvailabilityZoneRebalancing: DISABLED
少々待つと、リバランシング機能が正常に動いたことが分かります。
他のリージョンや時間帯により検知時間が変化する可能性はありますが、
参考までにバージニア北部リージョンで日本時間1時の場合だと以下のような実測値でした。
①不均衡な状態を検知(ここまで1分かからず)
②リバランシングを開始(ここまで1分30秒)
③均一な状態を維持(3分経過)
思ったよりかなり迅速にリバランシングの検知ができることが判明しました。
念のため、再度コマンドを打って、タスクの配置場所を確認しておきます
AZ-A、AZ-Bにタスクが均一に自動で配置されました。もし、AZ-Aのアベイラビリティゾーン障害が発生した際でも、AZ-Bでコンテンツを提供し続けることが可能となります。
まとめ
いかがだったでしょうか。
今まで皆さんがECS利用時に気にされていた痒いところに手が届いたアップデートだったのではないでしょうか。
従来、ECSのリバランシングをLambda等で作りこんでいた箇所が、ECS側で対応された点が嬉しいですね。1点注意事項としては、今回は迅速な検知をしてくれましたが、リバランシングの検知自体はベストエフォートな点です。今後は基本的にECSのリバランシングは有効にして利用していただけますと、高可用性アプリケーションを提供できます。
本記事が皆様のお役にたてば幸いです。
ではサウナラ~🔥