Amazon Web Services ブログ

Amazon EFS を Amazon ECS と AWS Fargate で使用するための開発者ガイド – パート 3



Amazon EFS を Amazon ECS と AWS Fargate で使用する方法に関するこのブログ記事シリーズのパート 3 へようこそ。参考までに、このブログ記事シリーズは次のように構成されています。

  • パート 1: このブログ記事は、この統合の必要性とその範囲に関する背景情報を提供し、この機能により道が開かれ、お客様に役立つユースケースとシナリオの概要を示します
  • パート 2: ECS と Fargate に基づきコンテナをデプロイする際の EFS のセキュリティの仕組みの詳細と、リージョンごとの ECS と EFS のデプロイのベストプラクティスに関する高レベルの考慮事項を扱います
  • パート 3: [このブログ記事] コンテナ化されたアプリケーションの再利用可能なコードとコマンドを含む実用的な例を紹介します。アプリケーションは EFS を使用した ECS タスクにデプロイされたものです

この記事では、パート 1 とパート 2 で学んだことを試すためのサンプルコードを作成します。このブログを 2 つのメインブロックに分割します (2 つの個別の例を使用して)。その 2 つは以下のとおりです。

  • ファイルシステムの永続性を必要とするアプリケーションを実行するためのスタンドアロンのステートフルタスク
  • 共有ファイルシステムに並行して複数のタスクにアクセスする

これらの背後にある理論について詳しく知りたい場合は、パート 1 をご覧ください。次に、サンプルコードについて詳しく説明します。

これらの例では、ECS タスクは Fargate で実行されますが、EC2 インスタンスで同じタスクを実行する場合 (異なるタスク起動タイプを使用する場合) はまったく同じワークフローが適用されます。

サンプルを実行するための前提条件と考慮事項

以下の例は、少なくとも 2 つのパブリックサブネットと 2 つのプライベートサブネット (アベイラビリティーゾーンごとに 1 つ) を使用できる VPC があることを前提としています。AWS CLI から CloudFormation、さらには CDK まで、そのような VPC を作成する方法は数多くあります。この演習用に一時的な VPC を作成する標準的な方法がない場合は、こちらが適切なオプションとなるでしょう

サンプルとコマンドでは、最新バージョンの適切な AWS CLI v2 環境があることも前提としています (以前のバージョンでは新しい統合がサポートされていない可能性があります)。クライアントに加えて、ユーザーは (Docker を使用して) コンテナイメージを構築する機能を持ち、jq および curl ユーティリティがインストール済みである必要があります。このブログ記事では AWS Cloud9 環境で eksutils を使用しましたが、前述の前提条件を持つ任意のセットアップを使用できます。

サンプルコードでは高度な自動化を実現することもできましたが、すべてのステップで何が行われているかをご理解いただけるように、適切なレベルの対話を行うように心がけました。これは主に学習用の演習です。本番環境対応の CI/CD パイプラインを構築する際にお勧めする方法ではありません。

各セクションでは、前のセッションで入力されたシステム変数を使用する可能性があるため、同じシェルコンテキストを維持することが重要です。便宜上、以下に概説するスクリプトとコマンドは、ターミナルでこれらの変数の内容をエコーし、ある時点でコンテキストを再作成する必要がある場合は、それらをログファイル (ecs-efs-variables.log) の「上に立てる」ことを行っています。

お使いの端末から、お客様の環境とログファイルの初期化を表す変数を使用して、「配管」のレイアウトを作成するところから開始しましょう。この環境変数の設定に失敗すると、ここで示すサンプルでエラーが発生する可能性があります。

export AWS_ACCESS_KEY_ID=<xxxxxxxxxxxxxx>
export AWS_SECRET_ACCESS_KEY=<xxxxxxxxxxxxxxxxxxxxx>
export AWS_DEFAULT_OUTPUT="json"

export AWS_REGION=<xxxxxxx>
export VPC_ID=<vpc-xxxx>
export PUBLIC_SUBNET1=<subnet-xxxxx>
export PUBLIC_SUBNET2=<subnet-xxxxx>
export PRIVATE_SUBNET1=<subnet-xxxxx>
export PRIVATE_SUBNET2=<subnet-xxxxx>
date | tee ecs-efs-variables.log 

ファイルシステムの永続性を必要とするアプリケーションを実行するためのスタンドアロンのステートフルタスク

この例は、再起動後も設定を維持する必要がある既存のアプリケーションを模倣しています。この例は NGINX に基づいており、かなり基本的ですが、これから使用する機能を必要とするお客様のより複雑なシナリオを具体的に例示することを目的としています。

このカスタムアプリケーションは、設計上、スタンドアロンでのみ実行できます。これは事実上シングルトンです。重要な設定情報を /server/config.json というファイルに保存します。このアプリケーションは、この情報をファイルシステムに保存するように制限されています。コードに変更を加えることはできません。アプリケーションアーキテクチャの特性の範囲内で作業する必要があります。

設定ファイルの情報は、アプリケーションがインストールされて初めて起動したときに生成されますが、タスクの再起動時にも保持する必要があります。まず、アプリケーションを起動すると RANDOM_ID が生成され、非常に重要な /server/config.json ファイルに保存されます。次に、一意のコード ID がウェブサーバーのホームページにインポートされます。アプリケーションを再起動する必要がある場合は、ファイルが存在するかどうかを確認します。存在しない場合は、アプリケーションが初めて起動されたと想定してファイルが作成されます。存在する場合、それを再作成するプロセスはスキップされます。

以下は、このアプリケーションの起動スクリプト (startup.sh) にこのロジックをどのように実装しているかを示しています。

#!/bin/bash
apt-get update
apt-get install -y curl jq
CONFIG_FILE="/server/config.json"

# this grabs the private IP of the container
CONTAINER_METADATA=$(curl ${ECS_CONTAINER_METADATA_URI_V4}/task)
PRIVATE_IP=$(echo $CONTAINER_METADATA | jq --raw-output .Containers[0].Networks[0].IPv4Addresses[0])
AZ=$(echo $CONTAINER_METADATA | jq --raw-output .AvailabilityZone)
echo $CONTAINER_METADATA
echo $PRIVATE_IP
echo $AZ

# this generates a unique ID
RANDOM_ID=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)

# if this is the first time the server starts the config file is populated
mkdir -p /server
if [ ! -f "$CONFIG_FILE" ]; then echo $RANDOM_ID > /server/config.json; fi

# the index.html file is generated with the private ip and the unique id
echo -n "Unique configuration ID       : " > /usr/share/nginx/html/index.html
echo $(cat $CONFIG_FILE) >> /usr/share/nginx/html/index.html
echo -n "Origin - Private IP            : " >> /usr/share/nginx/html/index.html
echo $PRIVATE_IP >> /usr/share/nginx/html/index.html
echo -n "Origin - Availability Zone     : " >> /usr/share/nginx/html/index.html
echo $AZ >> /usr/share/nginx/html/index.html
# this starts the nginx service
nginx -g "daemon off;"

このアプリケーションは、特定のタイムゾーンの標準的な勤務時間中にのみ使用します。ここでは、朝 7 時にアプリを開始し、夜 7 時にシャットダウンするワークフローを作成します。これにより、アプリケーションの費用を半分に削減できます。

EFS 統合の前は、ECS タスクでこのアプリケーションを起動すると、再起動時に /server/config.json ファイルの RANDOM_ID が失われました。スクリプトは新しい ID でファイルを再生成するため、アプリケーションが不具合を起こします。

このアプリケーションをコンテナにパッケージ化することにしました。これを行うには、startup.sh ファイルを作成したのと同じディレクトリ内に次の Dockerfile を作成します。

FROM nginx:1.11.5
MAINTAINER massimo@it20.info
ADD startup.sh .
RUN chmod +x startup.sh
CMD ["./startup.sh"]

これで以下の作業を行う準備ができました。

  • standalone-app と呼ばれる ECR リポジトリを作成する
  • イメージを構築する
  • ECR にログインする
  • コンテナイメージを ECR リポジトリにプッシュする
ECR_STANDALONE_APP_REPO=$(aws ecr create-repository --repository-name standalone-app --region $AWS_REGION)
ECR_STANDALONE_APP_REPO_URI=$(echo $ECR_STANDALONE_APP_REPO | jq --raw-output .repository.repositoryUri)
echo The ECR_STANDALONE_APP_REPO_URI is: $ECR_STANDALONE_APP_REPO_URI | tee -a ecs-efs-variables.log

docker build -t $ECR_STANDALONE_APP_REPO_URI:1.0 .

echo $(aws ecr get-login-password --region $AWS_REGION) | docker login --password-stdin --username AWS $ECR_STANDALONE_APP_REPO_URI

docker push $ECR_STANDALONE_APP_REPO_URI:1.0 

この時点で、EFS サポートなしで基本的な ECS タスクを作成して、一時的なデプロイの制限を示す準備ができています。その前に、タスクが実行ロールを引き受けることを許可する IAM ポリシードキュメントを作成する必要があります。ecs-tasks-trust-policy.json というポリシードキュメントを作成し、次のコンテンツを追加します。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": [
          "ecs-tasks.amazonaws.com"
        ]
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

これで、次の内容で standalone-app.json というタスク定義を作成できます。画像を編集する際は、必ず変数 $ECR_STANDALONE_APP_REPO_URI のコンテンツ、アカウント ID、リージョンを使用して行うようにしてください。

{"family": "standalone-app",
    "networkMode": "awsvpc",
    "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/task-exec-role",
    "containerDefinitions": [
        {"name": "standalone-app",
            "image": "<xxxxxxxxxxxxxxxxxxxxx>:1.0",
            "portMappings": [
                {
                    "containerPort": 80
                }
            ],
            "logConfiguration": {
                「logDriver」: "awslogs"、
                    "options": {
                       "awslogs-group": "/aws/ecs/standalone-app",
                       "awslogs-region": "REGION",
                       "awslogs-stream-prefix": "standalone-app"
                       }
            }
        }
    ],
    "requiresCompatibilities": [
        "FARGATE",
        "EC2"
    ],
    "cpu": "256",
    "memory": "512"
}

コマンドの次のバッチでは、以下のことを行います。

  • ECS クラスターを作成してタスクを保持する
  • タスク実行ロール (task-exec-role) を作成する
  • AWS マネージドの AmazonECSTaskExecutionRolePolicy ポリシーをロールに割り当てる
  • 上記のタスク定義を登録する
  • ロググループを作成する
  • セキュリティグループ (standalone-app-SG) を作成して設定し、ポート 80 へのアクセスを許可する
aws ecs create-cluster --cluster-name app-cluster --region $AWS_REGION

aws iam create-role --role-name task-exec-role --assume-role-policy-document file://ecs-tasks-trust-policy.json --region $AWS_REGION

aws iam attach-role-policy --role-name task-exec-role  --policy-arn "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" 

aws ecs register-task-definition --cli-input-json file://standalone-app.json --region $AWS_REGION

aws logs create-log-group --log-group-name /aws/ecs/standalone-app --region $AWS_REGION

STANDALONE_APP_SG=$(aws ec2 create-security-group --group-name standalone-app-SG --description "Standalone app SG" --vpc-id $VPC_ID --region $AWS_REGION) 
STANDALONE_APP_SG_ID=$(echo $STANDALONE_APP_SG | jq --raw-output .GroupId)
aws ec2 authorize-security-group-ingress --group-id $STANDALONE_APP_SG_ID --protocol tcp --port 80 --cidr 0.0.0.0/0 --region $AWS_REGION 
export STANDALONE_APP_SG_ID # the STANDALONE_APP_SG_ID variable needs to be exported to be able to use it from scripts
echo The STANDALONE_APP_SG_ID is: $STANDALONE_APP_SG_ID  | tee -a ecs-efs-variables.log

これは、一時的なタスクを使用して構築したセットアップで、アプリケーションがリサイクルされることを表しています。

次に、このタスクを開始および停止したときに何が起こるかを示します。そのために、ループで循環するスクリプトを作成します。スクリプトは、アプリケーションを 2 分ごとに 5 回起動および停止します。スクリプトは、タスクの実行中にアプリケーションをクエリします。

以下は standalone-loop-check.sh のスクリプトです。

#!/bin/bash 
COUNTER=0
echo ------------
while [  $COUNTER -lt 5 ]; do
    TASK=$(aws ecs run-task --cluster app-cluster --task-definition standalone-app --count 1 --launch-type FARGATE --platform-version 1.4.0 --network-configuration "awsvpcConfiguration={subnets=[$PUBLIC_SUBNET1, $PUBLIC_SUBNET2],securityGroups=[$STANDALONE_APP_SG_ID],assignPublicIp=ENABLED}" --region $AWS_REGION)
    TASK_ARN=$(echo $TASK | jq --raw-output .tasks[0].taskArn) 
    sleep 20
    TASK=$(aws ecs describe-tasks --cluster app-cluster --tasks $TASK_ARN --region $AWS_REGION)
    TASK_ENI=$(echo $TASK | jq --raw-output '.tasks[0].attachments[0].details[] | select(.name=="networkInterfaceId") | .value')
    ENI=$(aws ec2 describe-network-interfaces --network-interface-ids $TASK_ENI --region $AWS_REGION)
    PUBLIC_IP=$(echo $ENI | jq --raw-output .NetworkInterfaces[0].Association.PublicIp)
    sleep 100
    curl $PUBLIC_IP
    echo ------------
    aws ecs stop-task --cluster app-cluster --task $TASK_ARN --region $AWS_REGION > /dev/null
    let COUNTER=COUNTER+1 
done

スクリプトに実行フラグを追加し (chmod + x standalone-loop-check.sh)、それを起動します。出力は次のようになります。

sh-4.2# ./standalone-loop-check.sh 
------------
Unique configuration ID       : FZaeWiQfCFRxy4Kb3VrQGCOtGXH1AZGL
Origin - Private IP            : 10.0.42.251
Origin - Availability Zone     : us-west-2a
------------
Unique configuration ID       : 0zPEpXGxGvwNxHlcpb2s2tV85VjDYsyK
Origin - Private IP            : 10.0.2.50
Origin - Availability Zone     : us-west-2a
------------
Unique configuration ID       : CwjjgmNB8TQSc9UYkj2V2Z3cbQ25STZh
Origin - Private IP            : 10.0.76.91
Origin - Availability Zone     : us-west-2b
------------
Unique configuration ID       : KB3FtvDoAAChOxwe993MzyPKX37t3Vgy
Origin - Private IP            : 10.0.26.76
Origin - Availability Zone     : us-west-2a
------------
Unique configuration ID       : ngdUNWomeqSm1uOUAaYG84JULVm5dBAV
Origin - Private IP            : 10.0.42.181
Origin - Availability Zone     : us-west-2a
------------
sh-4.2# 

ご覧のように、RANDOM_ID は再起動のたびに変更され、これによりアプリケーションが不具合を起こします。再起動後も /server/config.json ファイルを保持する方法を見つける必要があります。EFS を入力します。

この後 EFS ファイルシステムを設定し、ECS タスクからアクセスできるようにします。これを実行する AWS CLI コマンドに飛び込む前に、efs-policy.json というポリシードキュメントを作成する必要があります。CLI で使用するこのポリシーには、安全でないトラフィックを拒否する 1 つのルールが含まれています。このポリシーは、ファイルシステムをマウントする機能を明示的に誰にも許可しません。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Deny",
            "Principal": {
                "AWS": "*"
            },
            "Action": "*",
            "Condition": {
                "Bool": {
                    "aws:SecureTransport": "false"
                }
            }
        }
    ]
}

これで、EFS サービスを設定する準備ができました。コマンドの次のバッチでは、以下のことを行います。

  • EFS ファイルシステムを作成する
  • すべてのクライアントに転送中の暗号化を強制するデフォルトポリシーを設定する
  • standalone-app-SG からポート 2049 (NFS プロトコル) へのインバウンドアクセスを許可するセキュリティグループ (efs-SG) を作成して設定する
  • 2 つのプライベートサブネットに 2 つのマウントターゲットを作成する
  • ディレクトリ /server にマップする standalone-app-EFS-AP という EFS アクセスポイントを作成する

これで、上記のセットアップを作成する AWS CLI コマンドを起動する準備ができました。

EFS=$(aws efs create-file-system --region $AWS_REGION)
FILESYSTEM_ID=$(echo $EFS | jq --raw-output .FileSystemId)
echo The FILESYSTEM_ID is: $FILESYSTEM_ID | tee -a ecs-efs-variables.log 
sleep 10

aws efs put-file-system-policy --file-system-id $FILESYSTEM_ID --policy file://efs-policy.json --region $AWS_REGION

EFS_SG=$(aws ec2 create-security-group --group-name efs-SG --description "EFS SG" --vpc-id $VPC_ID --region $AWS_REGION) 
EFS_SG_ID=$(echo $EFS_SG | jq --raw-output .GroupId)
aws ec2 authorize-security-group-ingress --group-id $EFS_SG_ID --protocol tcp --port 2049 --source-group $STANDALONE_APP_SG_ID --region $AWS_REGION
echo The EFS_SG_ID is: $EFS_SG_ID | tee -a ecs-efs-variables.log

EFS_MOUNT_TARGET_1=$(aws efs create-mount-target --file-system-id $FILESYSTEM_ID --subnet-id $PRIVATE_SUBNET1 --security-groups $EFS_SG_ID --region $AWS_REGION)
EFS_MOUNT_TARGET_2=$(aws efs create-mount-target --file-system-id $FILESYSTEM_ID --subnet-id $PRIVATE_SUBNET2 --security-groups $EFS_SG_ID --region $AWS_REGION)
EFS_MOUNT_TARGET_1_ID=$(echo $EFS_MOUNT_TARGET_1 | jq --raw-output .MountTargetId)
EFS_MOUNT_TARGET_2_ID=$(echo $EFS_MOUNT_TARGET_2 | jq --raw-output .MountTargetId)
echo The EFS_MOUNT_TARGET_1_ID is: $EFS_MOUNT_TARGET_1_ID | tee -a ecs-efs-variables.log
echo The EFS_MOUNT_TARGET_2_ID is: $EFS_MOUNT_TARGET_2_ID | tee -a ecs-efs-variables.log

EFS_ACCESSPOINT=$(aws efs create-access-point --file-system-id $FILESYSTEM_ID --posix-user "Uid=1000,Gid=1000" --root-directory "Path=/server,CreationInfo={OwnerUid=1000,OwnerGid=1000,Permissions=755}" --region $AWS_REGION)
EFS_ACCESSPOINT_ID=$(echo $EFS_ACCESSPOINT | jq --raw-output .AccessPointId)
EFS_ACCESSPOINT_ARN=$(echo $EFS_ACCESSPOINT | jq --raw-output .AccessPointArn)
echo The EFS_ACCESSPOINT_ID is: $EFS_ACCESSPOINT_ID | tee -a ecs-efs-variables.log
echo The EFS_ACCESSPOINT_ARN is: $EFS_ACCESSPOINT_ARN | tee -a ecs-efs-variables.log

ファイルシステムに /server ディレクトリをマウントするために EFS アクセスポイントを作成することを選択した理由の詳細については、パート 2 を参照してください。パート 2 では、アクセスポイントを使用する利点について説明しています。

EFS ファイルシステムが適切に設定されたので、アプリケーションにそれを認識させる必要があります。そのために、次のことを行います。

  • EFS アクセスポイントをマップする権限を付与する IAM ロール (standalone-app-role) を作成します
  • タスク定義 (standalone-app.json) を次のように調整します。
    • EFS アクセスポイントをマップする権限を付与するタスクロールを追加する
    • 上記で作成した EFS アクセスポイントに接続するためのディレクティブを追加する

standalone-app-task-role-policy.json というポリシーを作成し、以下を追加して、EFS ファイルシステム ARN と EFS アクセスポイント ARN が正しく設定されていることを確認します。この情報は、上記の変数を印刷したときに画面に表示されます。または、ecs-efs-variables.log ファイルを参照することもできます。このポリシーは、作成した特定のアクセスポイントへのアクセスを許可します。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "elasticfilesystem:ClientMount",
                "elasticfilesystem:ClientWrite"
            ],
            "Resource": "arn:aws:elasticfilesystem:REGION:ACCOUNT_ID:file-system/fs-xxxxxx",
            "Condition": {
                "StringEquals": {
                    "elasticfilesystem:AccessPointArn": "arn:aws:elasticfilesystem:REGION:ACCOUNT_ID:access-point/fsap-xxxxxxxxxxxxx"
                }
            }
        }
    ]
}

standalone-app.json タスク定義を開き、taskRoleArnmountPoints セクション、および volumes セクションを追加します。このスケルトンからファイルを再作成する (再カスタマイズする) か、すでにカスタマイズした元の standalone-app.json タスク定義に上記のディレクティブを追加することができます。

{"family": "standalone-app",
    "networkMode": "awsvpc",
    "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/task-exec-role",
    "taskRoleArn": "arn:aws:iam::ACCOUNT_ID:role/standalone-app-role",
    "containerDefinitions": [
        {"name": "standalone-app",
            "image": "<xxxxxxxxxxxxxxxxxxxxx>:1.0",
            "logConfiguration": {
                "logDriver": "awslogs",
                    "options": {
                       "awslogs-group": "/aws/ecs/standalone-app",
                       "awslogs-region": "REGION",
                       "awslogs-stream-prefix": "standalone-app"
                       }
                },
            "mountPoints": [
                {"containerPath": "/server",
                    "sourceVolume": "efs-server-AP"
                }
            ]
        }
    ],
    "requiresCompatibilities": [
        "FARGATE",
        "EC2"
    ],
    "volumes": [
        {"name": "efs-server-AP",
            "efsVolumeConfiguration": {"fileSystemId": "fs-xxxxxx",
                "transitEncryption": "ENABLED",
                "authorizationConfig": {
                    "accessPointId": "fsap-xxxxxxxxxxxxxxxx",
                    "iam": "ENABLED"
             }
            }
        }
    ],
    "cpu": "256",
    "memory": "512"
}

これで、ECS と EFS の統合を実装するコマンドのバッチを起動する準備ができました。

aws iam create-role --role-name standalone-app-role --assume-role-policy-document file://ecs-tasks-trust-policy.json --region $AWS_REGION
aws iam put-role-policy --role-name standalone-app-role --policy-name efs-ap-rw --policy-document file://standalone-app-task-role-policy.json --region $AWS_REGION

aws ecs register-task-definition --cli-input-json file://standalone-app.json --region $AWS_REGION

これにより、タスクのライフサイクルを「データ」から切り離しました。この場合、データは単なる設定ファイルですが、何でもかまいません。以下は、設定したものを視覚的に表したものです。

先に使用したのとまったく同じスクリプトを起動するとどうなるかを見てみましょう。念のため繰り返しますが、このスクリプトは 5 回連続してタスクを繰り返し、2 分ごとに停止します。

sh-4.2# ./standalone-loop-check.sh 
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.110.38
Origin - Availability Zone     : us-west-2b
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.83.106
Origin - Availability Zone     : us-west-2b
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.10.159
Origin - Availability Zone     : us-west-2a
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.126.224
Origin - Availability Zone     : us-west-2b
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.89.26
Origin - Availability Zone     : us-west-2b
------------
sh-4.2# 

前回の実行と違うところは何かありましたか? 設定 (この例ではファイル /server/config.json) が EFS 共有に移動したため、RANDOM_ID はアプリケーションの再起動後も保持されます。スタンドアロンタスクが異なるアベイラビリティーゾーンでどのように開始されるかにも注意を払う必要がありますが、タスクはどこからでも同じファイルシステムに到達できます。任務を完了しました!

共有ファイルシステムに並行して複数のタスクにアクセスする

このセクションでは、これまでに見てきたことを基にして、並行して動作するタスクが共通の共有ファイルシステムにどのようにアクセスするかを示します。このパターンをお客様が実現する無限の可能性へのプロキシとして、アプリケーションを使用し続けます (スケールアウト WordPress ワークロードのデプロイであろうと、並列機械学習ジョブであろうと)。

私たちの (架空の) アプリケーションは現在、より広く分散したコミュニティにサービスを提供しています。現在 24 時間年中無休でユーザーにサービスを提供しているため、勤務時間外にオフにすることはもうできません。それだけでなく、アーキテクチャに変更が加えられ、アプリケーションがスケールアウトできるようになりました。これは、サポートする必要がある負荷を考えると、歓迎すべき改善です。ただし、常に /server/config.json ファイルを永続化するための前提条件があり、複数の ECS タスクが同じファイルに並行してアクセスできるようにするにはどうしたらよいかという問題を解決する必要があります。前のセクションで定義したタスクを、EFS /server フォルダに対する読み取り/書き込み権限を持つこのアプリケーションの「マスター」として選択します。このセクションでは、/server ディレクトリを指す同じ EFS アクセスポイントを利用し、4 つの ECS タスクのセットへの読み取り専用アクセスを提供して、ロードバランサーの背後にある負荷を処理します。

上記のアプローチは、POSIX アクセス許可をバイパスし、AWS ポリシーに EFS ファイルシステムへのさまざまなレベルのアクセスを委任する方法を示しています。詳細については、このブログシリーズの パート 2 をご覧ください。

scale-out-app-task-role-policy.json という新しいポリシードキュメントを作成します。このポリシーは、アクセスポイントへの読み取り専用アクセスを許可します。EFS ファイルシステムの ARN と EFS アクセスポイントの ARN が正しく設定されていることを確認してください。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "elasticfilesystem:ClientMount"
            ],
            "Resource": "arn:aws:elasticfilesystem:REGION:ACCOUNT_ID:file-system/fs-xxxxxx",
            "Condition": {
                "StringEquals": {
                    "elasticfilesystem:AccessPointArn": "arn:aws:elasticfilesystem:REGION:ACCOUNT_ID:access-point/fsap-xxxxxxxxxxxxx"
                }
            }
        }
    ]
}

これで、新しいタスクロールを作成し、作成したポリシードキュメントをアタッチできます。

aws iam create-role --role-name scale-out-app-role --assume-role-policy-document file://ecs-tasks-trust-policy.json --region $AWS_REGION
aws iam put-role-policy --role-name scale-out-app-role --policy-name efs-ap-r --policy-document file://scale-out-app-task-role-policy.json --region $AWS_REGION

次に、scale-out-app.json という新しいタスク定義を作成します。このファイルは、前のセクションで使用した standalone-app.json タスク定義に似ていますが、顕著な違いがいくつかあります。

  • family
  • containerDefinitions/name
  • awslogs-group および awslogs-stream-prefix
  • taskRoleArn (このセクションで作成したもの)
  • accessPointId
{"family": "scale-out-app",
    "networkMode": "awsvpc",
    "executionRoleArn": "arn:aws:iam::ACCOUNT_ID:role/task-exec-role",
    "taskRoleArn": "arn:aws:iam::ACCOUNT_ID:role/scale-out-app-role",
    "containerDefinitions": [
        {"name": "scale-out-app",
            "image": "<xxxxxxxxxxxxxxxxxxxxx>:1.0",
            "portMappings": [
                {
                    "containerPort": 80
                }
            ],
            "logConfiguration": {
                "logDriver": "awslogs",
                    "options": {
                       "awslogs-group": "/aws/ecs/scale-out-app",
                       "awslogs-region": "REGION",
                       "awslogs-stream-prefix": "scale-out-app"
                       }
                },
            "mountPoints": [
                {"containerPath": "/server",
                    "sourceVolume": "efs-server-AP"
                }
            ]
        }
    ],
    "requiresCompatibilities": [
        "FARGATE",
        "EC2"
    ],
    "volumes": [
        {"name": "efs-server-AP",
            "efsVolumeConfiguration": {"fileSystemId": "fs-xxxxxx",
                "transitEncryption": "ENABLED",
                "authorizationConfig": {
                    "accessPointId": "fsap-xxxxxxxxxxxxxxxx",
                    "iam": "ENABLED"
             }
            }
        }
    ],
    "cpu": "256",
    "memory": "512"
}

これで、次の新しいタスク定義を登録できます。

aws ecs register-task-definition --cli-input-json file://scale-out-app.json --region $AWS_REGION

これで、コマンドの最後のバッチを起動して、アプリケーションのスケールアウトバージョンのデプロイを作成する準備ができました。コマンドは次のことを行います。

  • スケールアウトアプリケーションのセキュリティグループを作成して設定する (scale-out-app-SG)
  • scale-out-app-SGefs-SG に追加して、スケールアウトアプリが EFS マウントターゲットと通信できるようにする
  • ALB を作成および設定して、4 つのタスク間でトラフィックのバランスをとる
  • ログを収集するための専用のロググループ (/aws/ecs/scale-out-app) を作成する
  • 4 つの Fargate タスクを開始する ECS サービスを作成する
SCALE_OUT_APP_SG=$(aws ec2 create-security-group --group-name scale-out-app-SG --description "Scale-out app SG" --vpc-id $VPC_ID --region $AWS_REGION) 
SCALE_OUT_APP_SG_ID=$(echo $SCALE_OUT_APP_SG | jq --raw-output .GroupId)
aws ec2 authorize-security-group-ingress --group-id $SCALE_OUT_APP_SG_ID --protocol tcp --port 80 --cidr 0.0.0.0/0 --region $AWS_REGION
echo The SCALE_OUT_APP_SG_ID is: $SCALE_OUT_APP_SG_ID | tee -a ecs-efs-variables.log

aws ec2 authorize-security-group-ingress --group-id $EFS_SG_ID --protocol tcp --port 2049 --source-group $SCALE_OUT_APP_SG_ID --region $AWS_REGION

LOAD_BALANCER=$(aws elbv2 create-load-balancer --name scale-out-app-LB --subnets $PUBLIC_SUBNET1 $PUBLIC_SUBNET2 --security-groups $SCALE_OUT_APP_SG_ID --region $AWS_REGION)
LOAD_BALANCER_ARN=$(echo $LOAD_BALANCER | jq --raw-output .LoadBalancers[0].LoadBalancerArn)
LOAD_BALANCER_DNSNAME=$(echo $LOAD_BALANCER | jq --raw-output .LoadBalancers[0].DNSName)
export LOAD_BALANCER_DNSNAME # the LOAD_BALANCER_DNSNAME variable needs to be exported to be able to use it from scripts
echo The LOAD_BALANCER_ARN is: $LOAD_BALANCER_ARN | tee -a ecs-efs-variables.log
TARGET_GROUP=$(aws elbv2 create-target-group --name scale-out-app-TG --protocol HTTP --port 80 --target-type ip --vpc-id $VPC_ID --region $AWS_REGION)
TARGET_GROUP_ARN=$(echo $TARGET_GROUP | jq --raw-output .TargetGroups[0].TargetGroupArn)
echo The TARGET_GROUP_ARN is: $TARGET_GROUP_ARN | tee -a ecs-efs-variables.log
LB_LISTENER=$(aws elbv2 create-listener --load-balancer-arn $LOAD_BALANCER_ARN --protocol HTTP --port 80 --default-actions Type=forward,TargetGroupArn=$TARGET_GROUP_ARN --region $AWS_REGION)
LB_LISTENER_ARN=$(echo $LB_LISTENER | jq --raw-output .Listeners[0].ListenerArn)
echo The LB_LISTENER_ARN is: $LB_LISTENER_ARN | tee -a ecs-efs-variables.log

aws logs create-log-group --log-group-name /aws/ecs/scale-out-app --region $AWS_REGION

aws ecs create-service --service-name scale-out-app --cluster app-cluster --load-balancers "targetGroupArn=$TARGET_GROUP_ARN,containerName=scale-out-app,containerPort=80" --task-definition scale-out-app --desired-count 4 --launch-type FARGATE --platform-version 1.4.0 --network-configuration "awsvpcConfiguration={subnets=[$PRIVATE_SUBNET1, $PRIVATE_SUBNET2],securityGroups=[$SCALE_OUT_APP_SG_ID],assignPublicIp=DISABLED}" --region $AWS_REGION

次の図は、作成したものを示しています。

アプリケーションが実際にどのように動作するかを見てみましょう。これを行うには、curl がロードバランサーのパブリック DNS 名にヒットするループを実行して、ホームページをクエリします。以下は scale-out-loop-check.sh スクリプトです。

#!/bin/bash
COUNTER=0
echo ------------
while [  $COUNTER -lt 10 ]; do
    curl $LOAD_BALANCER_DNSNAME
    echo ------------
    let COUNTER=COUNTER+1
done

スクリプトに実行フラグ (chmod + x scale-out-loop-check.sh) を追加して起動します。出力は次のようになります。

sh-4.2# ./scale-out-loop-check.sh
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.228.203
Origin - Availability Zone     : us-west-2b
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.224.117
Origin - Availability Zone     : us-west-2b
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.228.203
Origin - Availability Zone     : us-west-2b
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.224.117
Origin - Availability Zone     : us-west-2b
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.168.140
Origin - Availability Zone     : us-west-2a
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.145.194
Origin - Availability Zone     : us-west-2a
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.168.140
Origin - Availability Zone     : us-west-2a
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.145.194
Origin - Availability Zone     : us-west-2a
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.228.203
Origin - Availability Zone     : us-west-2b
------------
Unique configuration ID       : S5yvUI12p1SqwyMqunJweD57gofJG6ho
Origin - Private IP            : 10.0.224.117
Origin - Availability Zone     : us-west-2b
------------
sh-4.2# 

ご覧のように、4 つのタスクすべてが ALB によって分散されており、全員が、現在共有されている /server/config.json ファイルからの同じ RANDOM_ID で応答しています。繰り返しになりますが、これらのタスクは設計上、設定したアベイラビリティーゾーン全体にデプロイされますが、同じデータにアクセスできます。任務を完了しました!

環境を壊す

作成した環境を壊す段階になりました。以下は、このブログ記事で実装したリソースを削除するコマンドのリストです。

aws ecs update-service --service scale-out-app --desired-count 0 --cluster app-cluster --region $AWS_REGION 
sleep 10
aws ecs delete-service --service scale-out-app --cluster app-cluster --region $AWS_REGION

aws efs delete-mount-target --mount-target-id $EFS_MOUNT_TARGET_1_ID --region $AWS_REGION
aws efs delete-mount-target --mount-target-id $EFS_MOUNT_TARGET_2_ID --region $AWS_REGION
aws efs delete-access-point --access-point-id $EFS_ACCESSPOINT_ID --region $AWS_REGION
sleep 10
aws efs delete-file-system --file-system-id $FILESYSTEM_ID --region $AWS_REGION

aws elbv2 delete-listener --listener-arn $LB_LISTENER_ARN 
aws elbv2 delete-target-group --target-group-arn $TARGET_GROUP_ARN  
aws elbv2 delete-load-balancer --load-balancer-arn $LOAD_BALANCER_ARN

aws ec2 delete-security-group --group-id $EFS_SG_ID --region $AWS_REGION
aws ec2 delete-security-group --group-id $STANDALONE_APP_SG_ID --region $AWS_REGION
sleep 20
aws ec2 delete-security-group --group-id $SCALE_OUT_APP_SG_ID --region $AWS_REGION

aws logs delete-log-group --log-group-name /aws/ecs/standalone-app --region $AWS_REGION
aws logs delete-log-group --log-group-name /aws/ecs/scale-out-app --region $AWS_REGION

aws ecr delete-repository --repository-name standalone-app --force --region $AWS_REGION

aws ecs delete-cluster --cluster app-cluster --region $AWS_REGION

aws iam detach-role-policy --role-name task-exec-role --policy-arn "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
aws iam delete-role --role-name task-exec-role --region $AWS_REGION
aws iam delete-role-policy --role-name standalone-app-role --policy-name efs-ap-rw
aws iam delete-role --role-name standalone-app-role --region $AWS_REGION
aws iam delete-role-policy --role-name scale-out-app-role --policy-name efs-ap-r
aws iam delete-role --role-name scale-out-app-role --region $AWS_REGION

この演習のために VPC を作成した場合は、忘れずに削除してください。

まとめ

これで、このブログシリーズは終了です。パート 1 では、ECS と EFS の統合の基本とコンテキストについて説明しました。パート 2 では、EFS へのアクセスを保護する方法に焦点を当てて、アーキテクチャの考慮事項に関する技術的な詳細を詳しく説明しました。この最後のパートでは、すべてを結び付け、先の記事で見たものを実装する方法の例を示しました。これで、統合の適用可能性を理解し、統合を使ってお客様のニーズに合ったものを構築するための知識を身につけるための土台が整いました。