決めろ最強のECS! 〜起動タイプ × 負荷分散 × デプロイ方法で自分だけのアーキテクチャ〜

# 決めろ最強のECS! 〜起動タイプ × 負荷分散 × デプロイ方法で自分だけのアーキテクチャ〜

※このノートは[JAWS DAYS 2019](https://jawsdays2019.jaws-ug.jp)懇親会LTの発表の詳細です!  

------
# アジェンダ
* 起動タイプ
* 負荷分散
* デプロイ方法
* 実際のシナリオ
* まとめ

------
# 起動タイプ
* ec2
* fargate

------
## ec2
Pros: 
* コスト面でfargateに勝る

Cons: 
* インスタンスの死活を気にする必要が出てくる

------
## fargate
Pros: 
* EC2インスタンスを作成しないため、環境構築の手順を減らせる！
* インスタンスに物理的に入ることができないため、監査が楽(と思われます)！
Cons: 
* 2019年2月時点ではドキュメントがそこまで多くなく、`ecs-cli`との組み合わせなど応用的(?)なことを試すと事例が探しづらい。
* IPアドレスを固定できないため、何らかの負荷分散方法と組み合わせないとインターネットからアクセスできるようにできない

------
# 負荷分散

* ALB
* API Gateway + NLB
* サービスディスカバリ

------
## ALB

Pros: 
* パスベースマッピングが重宝する
* WAFをセットできる

Cons: 
* 普通はALBで十分だと思われるが...API Gatewayにある機能がこちらにも欲しくなる！

------
## API Gateway + NLB

Pros: 
* APIを構築する上ではとても便利！
    * カスタムオーソライザーの提供
        * ex) アクセストークンをみてリクエストヘッダーを書き換える）
    * マネージドなAPIのバージョン管理
* VPC_LINKでNLBをプライベートサブネットに置くことができる。
* API Gatewayをプロキシ統合にすれば、ルーティングは実質アプリのフレームワークで握ることができる → 開発者に嬉しい！！！

Cons: 
* パスベースのマッピングができない。
    * 複数のECSサービスをインターネットと接続する場合、一つのNLBではそれらのサービスをインターネットに接続することができない。
    * 内部にロードバランサーを置く、NLBより手前でポート番号を変換するなど方法は考えられる

------
## サービスディスカバリ
Pros: 
* あくまで名前解決なので、gRPCやWebSocketを使いたい場合にも便利
* 名前で参照するため、サーバー間の参照方法が環境・デプロイ結果に依存しない

Cons: 
* 2019年2月時点ではドキュメントが多くない

------
# デプロイ方法

* CloudFrmation
* ecs-cli
* Terraform(今回は対象外)
* CDK(今回は対象外)

------
## CloudFormation

Pros: 
* 先行事例が多い
* ロードバランサーやサブネットの指定時に`!Ref`で参照できる

Cons: 
* タスク定義を更新した場合、サービスを自前で再起動する必要がある
    * justInCaseでは[silinternational/ecs-deploy](https://github.com/silinternational/ecs-deploy)を利用しています

------
## ecs-cli

Pros: 
* `docker-compose.yml`を利用できる
* タスク定義を更新した場合のサービスの再起動について考慮が不要になる
* ECRとECSのサービスがクロススタック参照で依存しないので、スタックを削除するときにテンプレートをごにょごにょしないてOK

Cons: 
* コマンドの癖が若干強い
    * ex) サービス作成時と更新時でサービスディスカバリを利用するオプションが異なる
    * ...もしかして、CIでヘビーにデプロイすることは想定されていない？？？
* CloudFormationではないので、クロススタック参照を使ってリソースを参照できない

ecs-cliでサブネットやロードバランサーを動的に指定するとちょっと大変でした。次のページを参照。

------
### SAMPLE: ecs-cliでサブネットやロードバランサーを動的に指定する

```
AWS_ACCOUNT_ID=$(aws sts get-caller-identity | jq -r .Account)
export IMAGE="${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_DEFAULT_REGION}.amazonaws.com/${ENV}-app"
export LOGS_GROUP="/aws/ecs/task/${ENV}-App"
export LOGS_REGION="${AWS_DEFAULT_REGION}"
export TASK_ROLE_ARN="$(aws cloudformation describe-stacks --stack-name ${ENV}-infrastracture --query 'Stacks[0].Outputs[?OutputKey==`TaskRoleArn`].OutputValue' --output text)"
export EXECUTION_ROLE_ARN="$(aws cloudformation describe-stacks --stack-name ${ENV}-infrastracture --query 'Stacks[0].Outputs[?OutputKey==`TaskExecutionRoleArn`].OutputValue' --output text)"
export TARGET_GROUP_ARN="$(aws cloudformation describe-stacks --stack-name ${ENV}-infrastracture --query 'Stacks[0].Outputs[?OutputKey==`TargetGroupArn`].OutputValue' --output text)"
export VPC_ID="$(aws cloudformation describe-stacks --stack-name ${ENV}-infrastracture --query 'Stacks[0].Outputs[?OutputKey==`VpcId`].OutputValue' --output text)"
export SUBNET1="$(aws cloudformation describe-stacks --stack-name ${ENV}-infrastracture --query 'Stacks[0].Outputs[?OutputKey==`ECSSubnet1aId`].OutputValue' --output text)"
export SUBNET2="$(aws cloudformation describe-stacks --stack-name ${ENV}-infrastracture --query 'Stacks[0].Outputs[?OutputKey==`ECSSubnet1cId`].OutputValue' --output text)"
export SECURITY_GROUP="$(aws cloudformation describe-stacks --stack-name ${ENV}-infrastracture --query 'Stacks[0].Outputs[?OutputKey==`ECSSecurityGroupId`].OutputValue' --output text)"
export MYSQL_HOST=${MYSQL_HOST}
export MYSQL_USERNAME=${MYSQL_USERNAME}
export MYSQL_PASSWORD=${MYSQL_PASSWORD}
[ $(aws ecs list-services --cluster ${ENV}-ECSCluster --query 'serviceArns' | grep \".*/app\") ] && export SERVICE_DISCOVERY_DEPLOYMENT_OPTION="--update-service-discovery" || export SERVICE_DISCOVERY_DEPLOYMENT_OPTION="--enable-service-discovery"
# ↑↑ ecs-cliでサービスディスカバリを有効にする場合、初回起動では `--enable-service-discovery`オプションを、更新では `--update-service-discovery`オプションを有効にする必要がある...
ecs-cli compose --project-name app -f ecs/docker-compose.yml --ecs-params ecs/ecs-params.yml service up \
    ${SERVICE_DISCOVERY_DEPLOYMENT_OPTION} --private-dns-namespace local --vpc ${VPC_ID} \
    --container-name app --container-port 8080 --cluster ${ENV}-ECSCluster --target-group-arn ${TARGET_GROUP_ARN} --launch-type FARGATE --timeout 10
```

------
# 実際のシナリオ

* toBでAPIを提供する
* 認証はCognito Userpoolを使うが、認可サーバーは自前/サードパーティのものを使う
* ECSサーバーはプライベートサブネットに置きたい
* ECSサーバー同士の相互参照がありえる
* 構築初期なので、スタックの削除もそこそこやる可能性がある

→ API Gateway + NLB + ECS on fargate w/ ecs-cliで構築 

------
# まとめ

要件によって起動タイプ x 負荷分散 x デプロイ方法 を使い分けて、自分だけのECSのアーキテクチャを見つけ出そう！