Medium 記事にもなっている AWS App Mesh のサンプルを試してみたらいい内容だったので、備忘も兼ねて一年半ぶりにブログを書いてみます。
TL; DR
- App Mesh のサンプルに多い EKS ではなく ECS
- App Mesh だけでなく CloudFormation やシェルスクリプト、jq の使い方も学べる
- X-Ray も学べる
以下、サンプルの内容に沿って紹介します。
Prerequisites
サンプルを試す前提条件は以下の通りです。
Create the VPC and other core Infrastructure
まず以下の環境変数を指定してシェルスクリプトを実行し CloudFormation で VPC とサブネットを作るところから始めます。
AWS_PROFILE: AWS CLI のプロファイル
AWS_DEFAULT_REGION: リージョン
ENVIRONMENT_NAME: デモに使う環境の名前
このシェルスクリプトは基本的に 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 の名前を指定してシェルスクリプトを実行します。
ここでは AWS::AppMesh::Mesh リソースを作っているだけです。
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 も同じ内容です。
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 を変更することでもルーティング情報を変更できます。

Envoy コンテナで X-Ray 統合が有効に設定されていて、かつ各サービス内で稼働する Go アプリケーションにも X-Ray SDK が設定されているので、X-Ray サービスマップからVirtualNode を経由して各アプリケーションにアクセスしている様子を確認できます。

各トレースをドリルダウンすることで詳細な内訳を確認できます。

まとめ
ちょっと App Mesh やってみようかなと軽い気持ちで手を出したサンプルから予想を大きく上回る収穫が得られて満足です。気になった方は是非お試しください。