Amazon Web Services ブログ

メタップスにおける ECS デプロイ戦略

AWS では、コンテナワークロードを構築・実行する為のオーケストレーションサービスとして、Amazon Elastic Container Service ( Amazon ECS ) を提供しています。そして、アプリケーションの CI/CD ( 継続的インテグレーション/継続的デリバリー ) を実現する為のサービスとして AWS CodeDeployAWS CodePipeline のようなサービスも提供しています。

しかしながら、CI/CD のフローをどのように設計・構築するかは、アプリケーションの性質によっても異なりますし、組織の規模や開発体制などによっても異なってきます。また、デプロイフローを設計する時にはデプロイを行う人、頻度、場所なども考慮し、どのような手法でデプロイするのか、管理はどのように行うのかなども考慮に入れる必要があります。

本投稿では、株式会社メタップス様が行っている Amazon ECS のデプロイツール genova についてご紹介します。開発者やデザイナーの為の Slack App でのデプロイ機能、開発者向けの CLI でのデプロイ機能、GitHub Actions との連携によるコードの Push をトリガーにした自動デプロイの機能など、デプロイを行う人と環境を便利にするような機能が沢山含まれています。また、genova では、AWS System Manager Parameter Store との連携のほか、 AWS Key Management Service の為のユーティリティの提供も行っています。

genova は、オープンソースとして GitHub 上に公開されていますので、是非こちらの記事と併せてご確認ください。会社でチャットツールとして Slack を利用し、ソースコードのリポジトリとして GitHub をお客様が取り入れられる箇所も多いのでは無いかと思います。

株式会社メタップス 開発部 re:shineグループマネージャー / SRE シニアエンジニアである 山北 尚道 氏によるゲスト投稿

EC2 から ECS への移行

こんにちは。メタップスの SRE としてグループのインフラを横断的に運用している山北です。

メタップスではファイナンスやマーケティング、DX 支援に至るまでさまざまなプロダクトをリリースしていますが、今回はその舞台裏で動いているアプリケーションのデプロイフローについて紹介したいと思います。

Amazon ECS ( 以下 ECS ) は AWS から2014年に発表されたフルマネージドコンテナオーケストレーションツールです。発表当時、メタップスではアプリケーション開発に Docker と Ruby on Rails を利用し、Capistrano を使って Amazon EC2 にデプロイする運用フローを採用していました。

このフローは通常のリリースサイクルであれば問題なかったのですが、機能追加などのリリースによりデグレーションが発生した場合のロールバック手順が煩雑になる難点がありました。

また、ローカル環境では動作するプログラムがサーバー上だと動かない、あるいは動作が不安定になるといった環境依存のバグに悩まされることもありました。

当時私はアプリケーション開発に携わっており、新規プロジェクトの立ち上げのタイミングでシステム構成を一から設計する機会がありました。そこでインフラ基盤に導入したのが ECS です。

ECS は当時の開発チームで課題となっていたデプロイフローや環境依存のバグ、スケーラビリティなどを解決する画期的なサービスでした。

その頃はまだ本番環境をコンテナベースで運用する知見がなかったこともあり、Twelve Factor App のガイドラインを参考にアプリケーションのプロセス管理やログ配送の仕組みを一から設計するところから始めました。

ECS でアプリケーションを運用するための設計手法は、AWS Dev Day 2019において「マイクロサービスを支えるインフラアーキテクチャ」というタイトルで登壇しているので、良ければ資料をご確認ください。

デプロイの課題と内製ツールの開発

Twelve Factor App の柱の一つに「ビルド、リリース、実行」という原則があります。これは安定したデプロイフローを構築するため、アプリケーションのデプロイをビルド、リリース、実行のステージに分離するための方法論です。

ECS を利用する上では Docker イメージをビルドし、Amazon ECR ( 以下 ECR ) にイメージをプッシュ、さらにサービスに紐づくタスクを更新する仕組みが必要でした。

しかし、ECS を利用し始めた頃はデプロイツールが出揃っておらず、半ば職人技のようにコマンドを組み立ててデプロイする形となりました。このフローはプロジェクトの規模が大きくなるに連れ操作ミスが発生しやすくなり、すぐ破綻しました。

そこで開発したのが genova というデプロイツールです。
https://github.com/metaps/genova

genova は次のような特徴を持ちます。

  • GitHub にコミットされたコード、Dockerfile を元にイメージをビルドして ECR にプッシュ、サービスを更新
  • 複数のデプロイ形式をサポート
    • CLI からのデプロイ
    • Slack と連携したインタラクティブなデプロイ
    • GitHub Actions や GitHub Webhookを利用したCI連携、自動デプロイ
  • デプロイ時に Git タグとの紐付け

デプロイは次のステップで実行されます。

  1. 開発者がコードをリポジトリにプッシュ。
  2. genova からデプロイ対象のリポジトリや ECS クラスター、サービスを指定してデプロイを実行。
  3. genova はリポジトリからコードを取得し、リポジトリに含まれるデプロイ設定ファイルに定義されたビルド情報を元にイメージをビルド。
  4. 作成したイメージを ECR にプッシュ。
  5. デプロイ設定ファイルを元に、サービスの設定に変更があればサービスを更新。
  6. リポジトリに含まれるタスク定義ファイルを元に ECS のタスク定義を更新。
  7. デプロイ対象サービスのタスク定義を更新。

開発者が準備することは、デプロイ設定ファイルとタスク定義ファイルの作成になります。デプロイ設定ファイルにはビルドするイメージファイルやデプロイ先の ECS クラスター、サービスなどを定義します。

clusters:
  # クラスター名
  - name: production
    services:
      # サービス名
      api:
        # apiサービスで実行するアプリケーションのイメージを定義
        containers:
          - name: rails
            build:
              context: ..
              dockerfile: Dockerfile

        # タスク定義ファイルの指定
        path: ./deploy/production.yml

        # タスク数
        desired_count: 1

        # デプロイメント中に、サービスに対して実行する必要があるタスク数の下限
        minimum_healthy_percent: 100

        # デプロイメント中に、サービスに対して実行する必要があるタスク数の上限
        maximum_percent: 200

続いてタスク定義ファイルを作成します。利用可能なパラメータは タスク定義パラメータ で公開されている内容と同じです。

family: backend
network_mode: bridge
container_definitions:
  - name: rails
    image: ***.dkr.ecr.ap-northeast-1.amazonaws.com/backend-rails:{{tag}}
    memory: 1024
    port_mappings:
      - container_port: 3000
        host_port: 0
        protocol: tcp
    essential: true
    readonly_root_filesystem: false

image パラメータには {{tag}} という特別な変数が割り当てられていますが、これは genova が ECS にタスクを登録する際、自動的にデプロイ日時に置き換えられます。

更に同じ名前のタグが Git に作成されるため、実行中のタスクに紐づくコミットの確認や、タスクのロールバックが容易となります。

デプロイ設定においては path パラメータを用いてタスク定義ファイルを指定しますが、ワークロードによってはタスクに割り当てる CPU やメモリのサイズを調整したい場合があります。

このようなケースでは task_overrides パラメータが利用できます。

clusters:
  - name: production
    run_tasks:
      db_migrate:
        containers:
          - name: rails
            build:
              context: ..
              dockerfile: Dockerfile
        path: ./deploy/production.yml
        command:
          - rake
          - db:migrate

        # production.ymlで定義されたタスクを上書きする
        task_overrides:
          container_definitions:
            - name: rails
              memory: 2048
              memoryReservation: 512

上記例の場合、db_migrate のスタンドアロンタスクに割り当てられるメモリのソフト制限は512MiB、ハード制限は2048Mibに上書きされます。

また、データベースパスワードなどの秘匿値は AWS System Manager Parameter Store に格納した上でタスク定義に登録できるほか、genova が提供する encrypt ユーティリティ ( AWS Key Management Service のラッパー ) で暗号化した文字列を登録することもできます。

# パラメータストアに保管したキーを指定
secrets:
  - name: MYSQL_PASSWORD
    value_from: /mysql_password

# KMSで暗号化した文字列。タスク定義のほか、デプロイ設定ファイルのビルドオプションにも指定可能
environment:
  - name: MYSQL_PASSWORD
    value: ${xxx}

デプロイ設定ファイル、タスク定義ファイルをプッシュすれば準備は完了です。開発者は genova がセットアップされたインスタンスに接続し、ワンライナーでデプロイを走らせることができます。

# fooリポジトリのコードをproductionクラスターのbackendサービスにデプロイ
$ docker-compose run --rm rails thor genova:deploy service -r foo -c production -s backend

genova は Web コンソールを提供するため、どのようなデプロイを実行したか一覧で確認することができます。これはリリース後にバグが発生した際、どのデプロイが影響したか調査する上でも役立ちます。

詳細ページからはビルドログのほか、デプロイにかかった時間やデプロイ実施者といった情報を確認することができます。

サービスやスケジュールタスクごとに、稼働中のタスクに紐づくコミット ID やブランチを確認することもできます。

Slack との統合、デプロイの自動化

リリース当初、genova が提供する機能はサービスの更新のみでしたが、その後も機能アップデートを繰り返し、スタンドアロンタスクやスケジューリングされたタスク、Fargate デプロイにも対応しています。

ワンライナーでデプロイが実行されることで開発者にとっての恩恵はありましたが、その反面、コーダーやデザイナーにとって CLI の操作は敷居が高く、デザインの修正が入るたびに開発者にデプロイを依頼するフローが必要でした。

そこで次に取り組んだのがチャットツールとの統合です。メタップスではチャットツールに Slack を導入していたので、Slack API を利用した Bot 機能を追加しました。

Slack App で genova のアプリを作成し、Slack チャンネルにアプリを追加すれば準備完了です。デプロイする際は Bot に対し deploy コマンドを送ることで、インタラクティブなデプロイを始めることができます。

Bot からレスポンスが返されたらデプロイ対象のリポジトリとブランチ (あるいはタグ) を指定します。

※以下は担当している re:shine サービスのデプロイフローの例となります。

続いてデプロイ対象のクラスターを指定します。サービスの更新のほか、スケジュールタスクの更新やスタンドアロンタスクの実行も対応しています。

デプロイ準備が整うと、現在稼働中のタスクとデプロイ対象コードの差分を確認するための GitHub URL が発行されます。開発者はレビュワーに最終チェックを依頼した上で安全にデプロイを開始することができます。また、デプロイチャンネルに参加している特定のメンバーのみデプロイを許可したい場合はパーミッションを設けることもできます。

デプロイが始まると genova は全てのコンテナが入れ替わるまで状態を監視し、結果を Slack に通知します。

Bot にはインタラクティブモードのほか、CLI ライクにワンライナーでデプロイするコマンドモード、前回のデプロイを再実行する redeploy といったコマンドが備わっています。

また、デプロイが自動化されたとはいえ、開発段階で都度 Slack からデプロイを走らせるのは手間となります。そこで次に実装したのが GitHub Webhook との統合です。

GitHub にはプッシュなどのイベントをトリガーに任意の URL をリクエストする機能があるため、この機能を利用しました。

Webhook を利用したデプロイフローは次のような流れです。

  1. 開発者がコードをプッシュする。
  2. プッシュをトリガーとして、GitHub から Webhook 経由で genova のデプロイ API を実行。
  3. genova は GitHub から送信されたプッシュイベントとデプロイ設定ファイルに定義された条件を元にデプロイを実行。
  4. デプロイが終わり次第、結果を Slack に通知。

デプロイ設定ファイルでは自動デプロイをトリガーするブランチや、デプロイの実行をステップ形式で指定することができます。

auto_deploy:
  # 適用するブランチ
  - branch: 'feature/*'

    # デプロイの実行手順
    steps:
      # サービスの更新
      - cluster: staging
        type: service
        resources:
          - backend

      # スタンドアロンタスクの実行
      - cluster: staging
        type: run_task
        resources:
          - db_migrate

上記の定義であれば、プッシュされたブランチが feature から始まるブランチ名に一致した場合、staging クラスターの backend サービスを更新したのち、スタンドアロンタスクとしてデータベースのマイグレーションを実行します。

このような仕組みを導入したことで開発環境のデプロイは自動化され、開発者の手を煩わせることがなく安定したデプロイサイクルを提供することが可能となりました。

メタップスでは現在も多数のプロダクト開発が進められていますが、一日辺り平均100回以上のデプロイが安定したサイクルで行われています。

GitHub Actions との統合

自動デプロイの次に求められたのが CI との連携でした。メタップスでは CI ツールとして GitHub Actions を導入したこともあり、ワークフローを利用したデプロイをサポートする形を取りました。

GitHub Actions を利用したデプロイのワークフローは次のような設定です。

name: deploy
on: push
jobs:
  deploy:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v1
      - uses: fjogeleit/http-request-action@master
        with:
          # コードがプッシュされたタイミングでデプロイを要求
          url: https://***/api/v2/github/actions/push
          method: POST
          customHeaders: >-
            {
              "X-GITHUB-SECRET-KEY": "${{secrets.GENOVA_GITHUB_SECRET_KEY}}"
            }
          data: >-
            {
              "account": "${{github.event.repository.owner.name}}",
              "repository": "${{github.event.repository.name}}",
              "ref": "${{github.event.ref}}",
              "commit_url": "${{github.event.head_commit.url}}",
              "author": "${{github.event.head_commit.author.username}}"
            }

以上で設定は完了です。開発者がコードがプッシュしたタイミングでジョブが走り、前述のデプロイ定義ファイルで指定したターゲットにデプロイが実行されます。

この機能の実装により、開発者はユニットテストの結果を元にデプロイを制御したり、サードパーティのインテグレーションとデプロイを組み合わせることが可能となりました。

おわりに

今回はメタップスにおける ECS のデプロイフローや genova の使い方を紹介させていただきました。

もともと genova は社内ツールとして利用することを前提に開発していましたが、2018年以降は OSS として一般公開する運びとなりました。

現在、メタップスで稼働するほぼ全てのシステムは ECS をベースに稼働しています。今後も ECS の機能拡張に伴い開発は続けていくので、導入を検討いただけると幸いです。

genova の詳しい使い方は GitHub の Wiki にまとめてありますので、要望やバグのフィードバックもお待ちしています。