石焼麻婆刀削麺中辛

神谷町で働くエンジニアの記録

Amazon ECSのコンテナインスタンスからFargateへ移行した話

この記事は iRidge Advent Calendar 2018 の15日目の記事です。

はじめに

Amazon ECSのコンテナインスタンスでWebアプリケーションを運用していたのですが、ものすごいトイルを感じたのでFargateへ移行を決意しました。

ものすごいトイル

  • コンテナインスタンスAMI更新作業
    • 度重なる深夜対応 ( Fargate移行を見越して自動化しなかった )
  • 2ステップスケーリング
    • リソース不足の場合にコンテナインスタンス数スケールからのTask数スケール を実施する必要がある。

現状の構成

f:id:yacchin0101:20181213105805p:plain
aws_ecs_container_instance

将来の構成

f:id:yacchin0101:20181213105822p:plain
aws_ecs_fargate

前提条件

  • 既存のECS Clusterを利用する
  • 既存のELB, FQDNを利用する
  • 新規にECS Service, TargetGroupを作成する
  • ゼロダウンタイムで移行する

Fargate移行によって得たもの・失ったもの

開発リリースフロー

  1. GitLabへソースコードをPush
  2. GitLabでMR作成 + マージ
  3. GitLab CIによるパイプライン実行
    • Unit Testの実行
    • Radonによる複雑度確認
    • Flake8による規約確認
    • Docker Imageのビルド
    • ecs-cliによるデプロイ

移行作業

  1. .gitlab-ci.ymlを修正
  2. docker-compose.ymlを作成
  3. ecs-params.ymlを作成
  4. GitLab CIのSecret Variablesを修正
  5. ELBのリスナーに新規TargetGroupを関連付ける
  6. GitLab CIで ECRへPush + ecs-cli によるデプロイ
  7. ELBのリスナーのデフォルトアクションを変更

前処理

  1. 新規のECS Serviceに設定するSecurityGroupが作成済み
  2. 1.で作成したSecurityGroupからの通信をRDSのSecurityGroupから許可済み
  3. 新規のECS Serviceに関連づけるTargetGroupを作成済み (TargetTypeが IP なので注意)

具体的な作業

1. .gitlab-ci.ymlを修正

下記はデプロイ部だけ抜き出したもの

.deploy_template: &deploy_definition
  image: python:3.6
  stage: deploy
  before_script:
    - export AWS_ACCESS_KEY_ID=$ECS_CLI_AWS_ACCESS_KEY_ID
    - export AWS_SECRET_ACCESS_KEY=$ECS_CLI_AWS_SECRET_ACCESS_KEY
    - export AWS_DEFAULT_OUTPUT=json
    - curl -o /usr/local/bin/ecs-cli https://s3.amazonaws.com/amazon-ecs-cli/ecs-cli-linux-amd64-latest
    - chmod 755 /usr/local/bin/ecs-cli
    - echo "$(curl -s https://s3.amazonaws.com/amazon-ecs-cli/ecs-cli-linux-amd64-latest.md5) /usr/local/bin/ecs-cli" | md5sum -c -
    - cd deploy
  script:
    - ecs-cli compose --debug -p $AWS_PROJECT_NAME -f app/docker-compose.yml --ecs-params app/ecs-params.yml service up --target-group-arn $AWS_TARGET_GROUP_ARN --cluster $AWS_ECS_CLUSTER_NAME --launch-type FARGATE --container-name web --container-port 80 --force-deployment --deployment-max-percent 200 --deployment-min-healthy-percent 50 --timeout 10
    - ecs-cli compose --debug -p $AWS_PROJECT_NAME -f app/docker-compose.yml service scale $AWS_ECS_CLUSTER_SCALE --cluster $AWS_ECS_CLUSTER_NAME --deployment-max-percent 200 --deployment-min-healthy-percent 50 --timeout 10

deploy_to_stg:
  <<: *deploy_definition
  variables:
    AWS_APP_IMAGE: $AWS_APP_IMAGE_STG
    AWS_ECS_CLUSTER_NAME: $AWS_ECS_CLUSTER_NAME_STG
    AWS_ECS_CLUSTER_SCALE: $AWS_ECS_CLUSTER_SCALE_STG
    AWS_PROJECT_NAME: $AWS_PROJECT_NAME_STG
    AWS_SECURITY_GROUP: $AWS_SECURITY_GROUP_STG
    AWS_SUBNET_A: $AWS_SUBNET_A_STG
    AWS_SUBNET_C: $AWS_SUBNET_C_STG
    AWS_TARGET_GROUP_ARN: $AWS_TARGET_GROUP_ARN_STG
    AWS_LOGS_GROUP: $AWS_LOGS_GROUP_STG
  only:
    - master

2. docker-compose.ymlを作成

version: "3"
services:
  web:
    image: $AWS_WEB_IMAGE
    ports:
      - "80:80"
    logging:
      driver: awslogs
      options:
        awslogs-group: $AWSLOGS_GROUP
        awslogs-region: $AWS_DEFAULT_REGION
        awslogs-stream-prefix: "ecs"
  app:
    image: $AWS_APP_IMAGE
    ports:
      - "8000:8000"
    environment:
      hoge: $HOGE
    command: gunicorn -b 0.0.0.0:8000 --access-logfile=- --error-logfile=- -w=8 -t=600 -k=gevent --threads=8 --worker-connections=1024 app.wsgi
    working_dir: /app
    logging:
      driver: awslogs
      options:
        awslogs-group: $AWSLOGS_GROUP
        awslogs-region: $AWS_DEFAULT_REGION
        awslogs-stream-prefix: "ecs"
  x-ray:
    image: $AWS_XRAY_IMAGE
    ports:
      - "2000:2000"
    logging:
      driver: awslogs
      options:
        awslogs-group: $AWSLOGS_GROUP
        awslogs-region: $AWS_DEFAULT_REGION
        awslogs-stream-prefix: "ecs"

3. ecs-params.ymlを作成

version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_role_arn: $AWS_TASK_ROLE_ARN
  task_execution_role: ecsTaskExecutionRole
  task_size:
    cpu_limit: 1024
    mem_limit: 2.0GB
  services:
    web:
      essential: true
      cpu_shares: 256
      mem_reservation: 0.5GB
    app:
      essential: true
      cpu_shares: 512
      mem_reservation: 1.0GB
    x-ray:
      essential: true
      cpu_shares: 256
      mem_reservation: 0.5GB

run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - $AWS_SUBNET_A
        - $AWS_SUBNET_C
      security_groups:
        - $AWS_SECURITY_GROUP
      assign_public_ip: ENABLED

4. GitLab CIのSecret Variablesを修正

事前準備で作成したSecurityGroup, TargetGroup等の情報を格納する

f:id:yacchin0101:20181211163855p:plain
secret_variables

5. ELBのリスナーに新規TargetGroupを関連付ける

新規ルールで、ホスト: example.com, 転送先: 新規作成したTargetGroup とし、保存する。*4

f:id:yacchin0101:20181211171607p:plain
aws_elb_listener_before

6. GitLab CIで ECRへPush + ecs-cli によるデプロイ

パイプラインを実行し、イメージビルド + ECRへPush + ecs-cliによるデプロイを実施

7. ELBのリスナーのデフォルトアクションを変更

5.で作成したルールを削除し、デフォルトアクションの転送先を新規に作成したTargetGroupへ変更し、保存する。

これで、バチッと切り替わる。

f:id:yacchin0101:20181211171650p:plain
aws_elb_listener_after

疎通確認して、旧ECS ServiceのTask数・コンテナインスタンス数を0へ変更する。

おわりに

  • vCPUに対するメモリの制限をなくしてほしい。 *5
  • 🎉祝トイル撲滅 🎉

*1:1コンテナインスタンスに配置されるTaskの組み合わせによってはリソースをフル活用できていなかったり、TaskScheduleで定期実行するTask用のコンテナインスタンスのリソースを余らせておかないといけなかったりと、過剰にコンテナインスタンス料金を支払っていた。リソース(vCPU, メモリ)を使った分だけの課金であるFargateを使った方が結果的に安くなった。

*2:Fargateの場合、Runningまで3~5分ほどかかる。つまりecs-cliによるデプロイも時間がかかる。TaskScheduleでTaskを定期的に実行している場合は、起動が遅くなるで実装によっては意図しない振る舞いになる可能性がある。

*3:普段からやっているわけではない。運用としてほしい情報はすべて外出している。最終奥義として使ったこともある。

*4:TargetGroupが関連していないとデプロイに失敗するため。

*5:vCPU:1, メモリ:1GBとかできると嬉しい。