App Mesh と ECS のサンプルを試したら CloudFormation とシェルスクリプトや jq の知見が詰まっていて X-Ray も付いてきた話
Medium 記事にもなっている AWS App Mesh のサンプルを試してみたらいい内容だったので、備忘も兼ねて一年半ぶりにブログを書いてみます。
- AWS App Mesh Walkthrough – A Cloud Guru
- aws-app-mesh-examples/examples/apps/colorapp at master · aws/aws-app-mesh-examples
TL; DR
以下、サンプルの内容に沿って紹介します。
Prerequisites
サンプルを試す前提条件は以下の通りです。
- AWS CLI 1.16.124 以上をインストールしている
- AWS CLI を default または名前付きのプロファイルで適切に設定している
- EC2 インスタンスにログインする SSH キーペアを作っている
- github.com/aws/aws-app-mesh-examples リポジトリをクローンしている
- jq をインストールしている
Create the VPC and other core Infrastructure
まず以下の環境変数を指定してシェルスクリプトを実行し CloudFormation で VPC とサブネットを作るところから始めます。
このシェルスクリプトは基本的に aws cloudformation deploy
を実行しているだけで難しいことはしていません。
#!/bin/bash set -ex DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" aws --profile "${AWS_PROFILE}" --region "${AWS_DEFAULT_REGION}" \ cloudformation deploy \ --stack-name "${ENVIRONMENT_NAME}-vpc" \ --capabilities CAPABILITY_IAM \ --template-file "${DIR}/vpc.yaml" \ --parameter-overrides \ EnvironmentName="${ENVIRONMENT_NAME}"
ここで使っている CloudFormation テンプレートには、知っている人には当たり前でも知らない人やどう書いたらいいか迷っている人の参考になる知見が詰まっています。
例えば !Select
と !GetAZs
でアベイラビリティゾーンを指定したり、!Join
ではなく !Sub
と変数で文字列を組み上げたり
PublicSubnet1: Type: AWS::EC2::Subnet Properties: VpcId: !Ref VPC AvailabilityZone: !Select [ 0, !GetAZs '' ] CidrBlock: !Ref PublicSubnet1CIDR MapPublicIpOnLaunch: true Tags: - Key: Name Value: !Sub ${EnvironmentName} Public Subnet (AZ1)
!DependsOn
で依存するリソースの作成順序を制御したり
NatGateway1EIP: Type: AWS::EC2::EIP DependsOn: InternetGatewayAttachment Properties: Domain: vpc
!GetAtt
で作成したリソースの属性を参照する方法も、通常はトライアンドエラーで覚えるものなので、このようなサンプルでショートカットして学べるのはとてもいいと思います。
NatGateway1: Type: AWS::EC2::NatGateway Properties: AllocationId: !GetAtt NatGateway1EIP.AllocationId SubnetId: !Ref PublicSubnet1
クロススタック参照する Outputs の名前の付け方も地味に悩むので参考になります。
Outputs: VPC: Description: A reference to the created VPC Value: !Ref VPC Export: Name: !Sub "${EnvironmentName}:VPC"
Create an App Mesh
先ほどと同様に環境変数に App Mesh の名前を指定してシェルスクリプトを実行します。
MESH_NAME
: App Mesh の名前
ここでは AWS::AppMesh::Mesh リソースを作っているだけです。
- https://github.com/aws/aws-app-mesh-examples/blob/c7446dcf11c02b38e830320cec68d387c019fb47/examples/infrastructure/appmesh-mesh.sh
- https://github.com/aws/aws-app-mesh-examples/blob/c7446dcf11c02b38e830320cec68d387c019fb47/examples/infrastructure/appmesh-mesh.yaml
Create compute resources
続いて ECS クラスターを作ります。
サービスディスカバリで使う Route 53 プライベートホストゾーンもここで作るので環境変数に指定します。
コンテナインスタンスは AutoScaling グループでプライベートサブネットに作成されますが、パブリックサブネットに踏み台インスタンスも作成されるので SSH キーペアも指定します。
SERVICES_DOMAIN
: プライベートホストゾーン名KEY_PAIR_NAME
: SSH キーペア名
シェルスクリプトはこれまでと同じで CloudFormation スタックを作るだけです。
ここで使う CloudFormation テンプレートも参考になります。
コンテナインスタンスと踏み台インスタンスに使う AMI ID はパラメータストアから取得することができます。
ECSAmi: Description: ECS AMI ID Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id> Default: "/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id" EC2Ami: Description: EC2 AMI ID Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id> Default: "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
Fn::ImportValue
と !Sub
でクロススタック参照したり
ECSInstancesSecurityGroup: Type: AWS::EC2::SecurityGroup Properties: GroupDescription: "Security group for the instances" VpcId: 'Fn::ImportValue': !Sub "${EnvironmentName}:VPC" ...
AutoScalingGroup の UpdatePolicy 属性 もちゃんと指定しています。
ECSAutoScalingGroup: Type: AWS::AutoScaling::AutoScalingGroup ... UpdatePolicy: AutoScalingRollingUpdate: MinInstancesInService: 1 MaxBatchSize: 1 PauseTime: PT15M SuspendProcesses: - HealthCheck - ReplaceUnhealthy - AZRebalance - AlarmNotification - ScheduledActions WaitOnResourceSignals: true
UserData で SSM エージェントをインストールしているので、コンテナインスタンスにセッションマネージャで接続することもできます。
ECSLaunchConfiguration: Type: AWS::AutoScaling::LaunchConfiguration ... UserData: "Fn::Base64": !Sub | #!/bin/bash yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm yum install -y aws-cfn-bootstrap hibagent /opt/aws/bin/cfn-init -v --region ${AWS::Region} --stack ${AWS::StackName} --resource ECSLaunchConfiguration /opt/aws/bin/cfn-signal -e $? --region ${AWS::Region} --stack ${AWS::StackName} --resource ECSAutoScalingGroup /usr/bin/enable-ec2-spot-hibernation
UserData で指定するには複雑過ぎる設定は AWS::CloudFormation::Init リソースを使って指定します。
Metadata: AWS::CloudFormation::Init: config: packages: yum: awslogs: [] commands: 01_add_instance_to_cluster: command: !Sub echo ECS_CLUSTER=${ECSCluster} >> /etc/ecs/ecs.config files: "/etc/cfn/cfn-hup.conf": mode: 000400 owner: root group: root ...
Configure App Mesh resources
いよいよ本命の App Mesh を作ります。
シェルスクリプトはこれまでと同様で、
CloudFormation テンプレートで VirtualNode, VirtualRouter, Route, VirtualService を作ります。
バーチャルバーチャル言ってて何のこっちゃ分からんという感情が先行しますが、雰囲気が掴めてくるとたしかに仮想的なノードと、それに対するルートを提供するサービスだなあという気もしてきます。
自分はブログ記事 "Learning AWS App Mesh" の "DJ App revisited" の辺りを読んで腑に落ちた感じがしたので、もし未読の方は一読されるといいかもしれません。
Deploy services to ECS
ここまででネットワークインフラを作って ECS クラスタを作って App Mesh を作ったので、最後に実際に ECS サービスをデプロイします。
Deploy images to ECR for your account
まず AWS CLI で ECR を作ってコンテナイメージを push します。
これまでのシェルスクリプトとは毛色が違いますが、基本的に docker build して作った ECR に push しているだけです。
#!/usr/bin/env bash # vim:syn=sh:ts=4:sw=4:et:ai set -ex if [ -z $COLOR_GATEWAY_IMAGE ]; then echo "COLOR_GATEWAY_IMAGE environment variable is not set" exit 1 fi # build docker build -t $COLOR_GATEWAY_IMAGE . # push if [ -z $AWS_PROFILE ]; then $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION) else $(aws ecr get-login --no-include-email --region $AWS_DEFAULT_REGION --profile $AWS_PROFILE) fi docker push $COLOR_GATEWAY_IMAGE
Dockerfile では真面目にマルチステージビルドしています。
FROM golang:1.10 AS builder # Download and install the latest release of dep ADD https://github.com/golang/dep/releases/download/v0.4.1/dep-linux-amd64 /usr/bin/dep RUN chmod +x /usr/bin/dep # Copy the code from the host and compile it WORKDIR $GOPATH/src/github.com/username/repo COPY Gopkg.toml Gopkg.lock ./ RUN dep ensure --vendor-only COPY . ./ RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix nocgo -o /app . FROM scratch COPY --from=builder /app ./ ENTRYPOINT ["./app"]
バックエンドサービスを build して push するシェルスクリプト、Dockerfile も同じ内容です。
- https://github.com/aws/aws-app-mesh-examples/blob/c7446dcf11c02b38e830320cec68d387c019fb47/examples/apps/colorapp/src/colorteller/deploy.sh
- https://github.com/aws/aws-app-mesh-examples/blob/c7446dcf11c02b38e830320cec68d387c019fb47/examples/apps/colorapp/src/colorteller/Dockerfile
Deploy gateway and colorteller services
環境変数に最新の Envoy コンテナイメージと先ほど ECR に push したコンテナイメージを指定したら、シェルスクリプトを実行して ECS サービスをデプロイします。
ENVOY_IMAGE
: 最新の Envoy コンテナイメージ。現時点では v1.9.1.0-prod が最新COLOR_GATEWAY_IMAGE
: ゲートウェイサービスのコンテナイメージCOLOR_TELLER_IMAGE
: バックエンドサービスのコンテナイメージ
シェルスクリプトは今まで変わり映えしないように見えますが、create-task-defs.sh
を呼び出している点が異なります。
#!/bin/bash ... # Creating Task Definitions source ${DIR}/create-task-defs.sh aws --profile "${AWS_PROFILE}" --region "${AWS_DEFAULT_REGION}" \ cloudformation deploy \ --stack-name "${ENVIRONMENT_NAME}-ecs-colorapp" \ ...
このシェルスクリプトでは AWS CLI と jq をうまく使ってタスク定義を登録していました。
describe-stacks
の結果を jq でパースして必要な値を取得したら
stack_output=$(aws --profile "${AWS_PROFILE}" --region "${AWS_DEFAULT_REGION}" \ cloudformation describe-stacks --stack-name "${ENVIRONMENT_NAME}-ecs-cluster" \ | jq '.Stacks[].Outputs[]') task_role_arn=($(echo $stack_output \ | jq -r 'select(.OutputKey == "TaskIamRoleArn") | .OutputValue')) execution_role_arn=($(echo $stack_output \ | jq -r 'select(.OutputKey == "TaskExecutionIamRoleArn") | .OutputValue')) ...
やはり jq の -f
オプションで読み込んだテンプレートファイルに --arg
や --argjson
オプションで変数の値を渡してタスク定義の入力となる JSON 文字列を組み上げ、AWS CLI でタスク定義を登録しています。
generate_color_teller_task_def() { color=$1 task_def_json=$(jq -n \ --arg NAME "$ENVIRONMENT_NAME-ColorTeller-${color}" \ --arg STAGE "$APPMESH_STAGE" \ --arg COLOR "${color}" \ --arg APP_IMAGE $COLOR_TELLER_IMAGE \ --arg AWS_REGION $AWS_DEFAULT_REGION \ --arg ECS_SERVICE_LOG_GROUP $ecs_service_log_group \ --arg AWS_LOG_STREAM_PREFIX_APP "colorteller-${color}-app" \ --arg TASK_ROLE_ARN $task_role_arn \ --arg EXECUTION_ROLE_ARN $execution_role_arn \ --argjson ENVOY_CONTAINER_JSON "${envoy_container_json}" \ --argjson XRAY_CONTAINER_JSON "${xray_container_json}" \ -f "${DIR}/colorteller-base-task-def.json") task_def=$(aws --profile "${AWS_PROFILE}" --region "${AWS_DEFAULT_REGION}" \ ecs register-task-definition \ --cli-input-json "$task_def_json") }
CloudFormation テンプレートでは登録したタスク定義を使用するサービスおよびサービスディスカバリレコード、そしてゲートウェイサービスにリクエストをフォワードする ALB を作ります。
その他にテスト用と思われるサービスやサービスディスカバリレコードも作っていますが本質ではないので割愛。
作成した ALB のエンドポイントに curl でリクエストを送ると App Mesh によってルーティングされたいずれかの ECS サービスから応答が得られます。
$ colorapp=$(aws cloudformation describe-stacks --stack-name=$ENVIRONMENT_NAME-ecs-colorapp --query="Stacks[0 ].Outputs[?OutputKey=='ColorAppEndpoint'].OutputValue" --output=text); echo $colorapp http://DEMO-Publi-M7WJ5RU13M0T-553915040.us-west-2.elb.amazonaws.com $ curl $colorapp/color {"color":"red", "stats": {"red":1}}
Shape traffic
Apply traffic rules
作成した CloudFormation スタックに含まれる AWS::AppMesh::Route
リソースの WeightedTargets
を変更して CloudFormation スタックを更新することで、App Mesh のルーティング情報を変更できます。
また、App Mesh コンソールで対象の Virtual routes を Edit
して Virtual nodes の Weight を変更することでもルーティング情報を変更できます。
Monitor with AWS X-Ray
Envoy コンテナで X-Ray 統合が有効に設定されていて、かつ各サービス内で稼働する Go アプリケーションにも X-Ray SDK が設定されているので、X-Ray サービスマップからVirtualNode を経由して各アプリケーションにアクセスしている様子を確認できます。
各トレースをドリルダウンすることで詳細な内訳を確認できます。
まとめ
- AWS App Mesh Walkthrough – A Cloud Guru を読むと ECS, App Mesh, X-Ray の概要を把握できる
- aws/aws-app-mesh-examples を読むと CloudFormation のベストプラクティスや AWS CLI, jq およびシェルスクリプトによる活用事例を理解できる
- マネジメントコンソールから App Mesh のルーティング情報を変更して X-Ray でトレーシングできる
ちょっと App Mesh やってみようかなと軽い気持ちで手を出したサンプルから予想を大きく上回る収穫が得られて満足です。気になった方は是非お試しください。