Amazon Web Services ブログ

面白法人カヤックにおけるビルディングブロックとしてのAmazon ECSの活用とサービス間連携の工夫

開発者がアプリケーションを開発・パッケージング・デプロイするための強力な手法として、コンテナ技術はその代表的な1つに挙げられます。そしてそのようなコンテナ技術における様々なユースケースをサポートすべく、AWS では Amazon Elastic Container Service (Amazon ECS) に代表される多様なサービスを提供しています。

Amazon ECS はコンテナの運用管理を容易にするマネージドサービスです。他の AWS サービスとの組み合わせにより多様なワークロードをサポートするシステムを素早く構築可能です。一例として、 AWS Secrets Manager を利用した秘匿情報の連携が挙げられます。これにより、IDやパスワードをセキュアに管理しつつ、必要な時にアプリケーションから利用することができます。コンテナサービスそのものが秘匿情報の管理機能を持つのでなく、サービスの組み合わせによってワークロードの多様性をカバーする方法は一見複雑ですが、コンテナを AWS Lambda に置き換えるようなアーキテクチャ変更を柔軟に行うことを可能にする優れた仕組みとも言えます。

一方、このように疎結合なシステムを作ろうとした場合、サービスとサービスの間にうまく繋がらない「隙間」が生まれることがあります。例えば、 Amazon ECS 上の Fargate タスクから標準出力を Amazon CloudWatch Logs に保存できますが、全てのログを Amazon CloudWatch Logs に保存したくない場合もあります。この隙間を埋める方法として、 Amazon CloudWatch Logs に保存されたログを AWS Lambda でフィルタリングして保存し直したり、 Fluentd や Fluent Bit をサイドカーとして動作させる方法がありました(なお、現在は AWS FireLens をご利用いただけます)。
このように、「隙間」を埋める機能を AWS Lambda などで実装することは可能ですが、長期に渡る運用の過程で自社固有の課題解決のため様々な事情を抱えた実装が増えていきます。結果、隙間を埋めることだけが目的だったはずの実装が自社固有のビジネスロジックを含んでしまい、AWS サービス側のアップデート時に新機能に移行できなくなってしまうようなことも起こり得ます。

本投稿では、面白法人カヤックが構築したシステムを通し、上記のような各 AWS サービスをビルディングブロックとして組み合わせて利用していく上での課題と、その解決策の実例を紹介します。

面白法人カヤック の技術部/インフラエンジニアである 藤原 俊一郎 氏によるゲスト投稿
Amazon ECS とマネージドサービスを活用したサーバ構築と運用

この投稿では、カヤックがリリースした Web サービスやソーシャルゲームのサーバを、Amazon ECS と AWS の豊富なマネージドサービスを活用して構築するにあたって得られた知見をお伝えできればと思います。

なお、この投稿は CEDEC2018 の講演を元に再構成したものです。 AWS DevDay Tokyo 2019 にて 関連する講演があります ので、併せてご覧ください。

概要

  • Amazon ECS におけるサーバ運用
  • デプロイ手法と秘匿情報の管理
  • ログの集約
  • Go 言語による運用ツール/ミドルウェア開発

ECS におけるサーバ運用

最初に、これまで Amazon EC2 上に構築していたアプリケーション/ミドルウェアを Amazon ECS 上で動作させるにあたっての方針をまとめます。

  • 状態を持たないアプリケーションのみを動かす
  • 長時間の状態を持つミドルウェアは動かさない (RDBMS などのストレージ)
  • 状態はすべてマネージドサービスへ保存する (Amazon Relational Database Service (Amazon RDS), Amazon S3, Amazon ElastiCache, …)

Amazon ECS ではデプロイのたびに新しいタスクが起動し、古いタスクは終了します。タスクが終了すると、タスクに含まれているコンテナで行われたローカルファイルへの書き込みは失われます。

タスクから Amazon EC2 ホストのファイルシステムをマウントすることは可能ですが、クラスタ内のどの Amazon EC2 でタスクを起動するかは Amazon ECS が決定するため、特定のタスクからの書き込みを特定のインスタンスに固定することができません。

つまり、永続状態をファイルに書き込むことは基本的に行えないものとして構成する必要があります。そのため、永続する状態はすべてマネージドサービス (Amazon RDS, Amazon S3, Amazon ElastiCache 等) へ保存するようにアプリケーションを設計しました。

また、アプリケーション/ミドルウェアから発生するログも、ローカルファイルに書き込まず、マネージドサービスへ転送する必要があります。

Amazon ECS にしてよかったこと

利点としてはまず、ミドルウェアの構成管理が簡素化されました。

これまでは Chef による構成管理を行っていました。Chef のコミュニティ cookbook は使用せず、すべて自分らで書き起こしてチーム内でレビューしていたため、内容の把握については問題がなく、世間で喧伝されるよりは維持管理の負担は多くなかったと考えています。それでも一度実行した環境に再度適用した場合の冪等性確保を確実にするのが、特に専門ではないアプリケーションエンジニアにとっては負担でした。

ECS 化により構成管理は Dockerfile となったため、一度作られた環境を壊さないように上書きする配慮が不要になり、毎回クリーンな状態から構築ができればよい状態になったため、構成管理が簡素化されました。

また、サーバの追加削除が容易になりました。

これまではサーバの種類 (アプリケーション、WebSocket、バッチ、ログ集約など) ごとに EC2 のインスタンスを作成し、種類ごとに AMI を作成していました。これが一種類の ECS コンテナインスタンスを管理するだけでよくなりました。

たとえば OS に再起動必須なパッチを適用する場合、これまでは

  • Amazon Elastic Load Balancing (Amazon ELB) から切り離して順次適用し、再起動後 Amazon ELB へ戻す
  • 新しい Amazon Machine Images (AMI) をサーバの種類ごとに作成し、そこから起動したインスタンスと古いインスタンスを種類ごとに全台入れ換える

のどちらかの手段を取る必要がありましたが、これは繁雑な作業でした。

Amazon ECS では一種類の AMI を作成して起動、タスクを古いインスタンスから新しいインスタンスへ移動し、古いインスタンスをすべて停止するだけで作業が完了します。

Amazon ECS にして大変だったこと

もちろん、大変だったこともあります。

まず Amazon ECS の概念について、特にアプリケーションエンジニアに理解してもらうことが必要です。アプリケーションエンジニアは Docker 自体には馴染みがあるのですが、多数のコンテナを協調して動かすための Amazon ECS における概念 (タスク、サービスなど) については多少の学習が必要です。

また、一口にローカルファイルに依存しない仕組みを作るといっても、徹底するのは意外と大変です。

たとえばこれまではインスタンス上にファイルで保持されていたログファイルを tail -f で追尾するのと同等のことを行うにはどうすればいいか、というとそこまで自明な解はすぐに出てきません。これは Kinesis Streams と自作の kinesis-tailf を使って解決することにしました(後述します)。

直接プロセスの状態をみるような調査も困難になりました。

動作しているプロセスに strace でアタッチして発行しているシステムコールを観察するにしても、Amazon EC2上であれば直接 ssh すればいいのですが、Amazon ECS では

  1. 狙ったタスクが動作している Amazon EC2 のホストを特定する
  2. その Amazon EC2 に ssh して docker exec sh & strace

という手間が掛かりますし、アプリケーション実行用のコンテナには strace のようなコマンドが入っていないこともよくあります。また、Amazon EC2 上ではなく Fargate で動作させる場合、そもそもホストに ssh で接続してコンテナ上のプロセスにアタッチすることは不可能です。

Amazon ECS / コンテナ化の(よい)副作用

状態を持たないことを徹底し、ホストはいつ消えても問題ないようにすると、スポットインスタンスが利用しやすくなります。スポットインスタンスはオンデマンドインスタンスよりも非常に低価格(通常25%〜30%程度)で提供されているため、大きなコスト削減効果が見込めます。

ただし、スポットインスタンスはその低価格と引き換えに、いつ停止されるか分からないという性質があります。一種類のインスタンスタイプ、Availability Zone (AZ) ですべてのインスタンスを起動するのは危険です。

複数のインスタンスタイプ、AZ に分散してインスタンスを配置し、停止したインスタンスを適宜起動可能なインスタンスで補充するスポットフリートという仕組みが提供されているため、それを利用すると可用性を上げつつ一定のリソースを確保できます。

spotfleet2

スポットインスタンスを Amazon ECS で使うための一手間

2019年09月以降は AWSのアップデート により不要となっていますが、スポットインスタンスを Amazon ECS コンテナインスタンスとして利用する場合、インスタンス停止時に工夫が必要でした。このような工夫は他の機能やサービスでも有用なため、当時行なった対処を紹介いたします。

スポットインスタンスは停止される120秒前に通知される API が提供されているのですが、Amazon ECS のインスタンスがこの通知によって自動的にクラスタから外れるような処理は行われなかったため、インスタンス停止時にタスクが突然死してしまいます。

以下のような処理を各インスタンスで動かすことで、停止予告がきたインスタンス上で動作しているタスクを、正常にクラスタから外して他のインスタンスに移動できます。

#!/bin/bash
while sleep 5; do
    CONTENT=$(curl -sf http://169.254.169.254/latest/meta-data/spot/instance-action)
    if [ -z "$CONTENT" ]; then
        continue
    fi
    CLUSTER=$(curl -s http://localhost:51678/v1/metadata | jq -r .Cluster)
    CONTAINER_INSTANCE=$(curl -s http://localhost:51678/v1/metadata | jq -r .ContainerInstanceArn)
    aws ecs update-container-instances-state \
        --cluster "$CLUSTER" \
        --container-instances "$CONTAINER_INSTANCE" \
        --status DRAINING \
        && exit 0
done

このスクリプトは

  • 5秒ごとに停止予告があるか確認
  • 停止予告が来たら aws ecs update-container-instances-state コマンドにより、自分自身のインスタンスをクラスタから除外する

という動作を行います。これはホスト上で直接プロセスとして起動してもいいですし、Amazon ECS のサービスとして配置し、DAEMON スケジューリング戦略によって各インスタンスに1タスクずつ起動してもいいでしょう。

詳しくは AWS Computeブログの投稿を参照してください

デプロイ手法と秘匿情報の管理

Amazon ECS でのデプロイは、サービスに対して新しいタスクを起動し、順次古いタスクと入れ換えていくことによって行われます。ロールバックについては現時点では Amazon ECS が仕組みとして提供していないため、利用者各自が行う必要があります。

具体的には「ひとつ前に実行していたタスク定義」を使用するようにデプロイしなおす、という作業です。

例えば app:10 (リビジョン10) というタスク定義を実行していて、app:11 をデプロイしたが問題があったのでロールバックしたい、という場合に行うことは以下のようになります。

  1. app:10 をサービスに適用してデプロイを行う
  2. app:11 を削除する

app:11 を削除しないでおくと、その後 app:12 でデプロイを行ってそこで更に問題が出たのでロールバックする、という場合に、先ほど問題があった app:11 を使用してしまう可能性があるため注意しましょう。

また、このようにロールバックを行うためには、タスクのコンテナ起動後に動的にコードを取得するような作りにしてはいけません。コードはコンテナイメージに焼き込んで、実行時点で書き換わらないようしておく必要があります。

なお、2018年11月には AWS CodeDeploy による Blue/Green デプロイメントがサポート されました。AWS CodeDeploy を利用することにより、新旧バージョンのアプリケーションを同時に起動し、新しいバージョンが正常に稼働することを確認してから全てを入れ換えることが可能になっています。

秘匿情報の管理

コードはイメージに焼き込むべきですが、秘匿情報(API key や秘密鍵など)を焼き込むのは好ましくありません。イメージは docker pull されたらその環境に残りますし、すべてを破棄するのは困難です。秘匿情報については、プロセスのメモリ上に保持されるように与える必要があります。具体的には、実行時に環境変数によって渡すのがよいでしょう。

タスク定義内に環境変数の設定は可能ですが、タスク定義自体は平文で保持されますし、古い情報の破棄についてイメージと同じく問題があります。

タスク起動時に、安全な場所から取得して環境変数としてプロセスに渡すために、AWS Systems Manager (AWS SSM) パラメータストアを利用しました。

AWS SSM パラメータストアからの値取得

AWS SSM パラメータストアから値を取り出して環境変数として設定するツールは多数ありますが、例として aws-ssm-env を利用する場合は以下のようになります。

$ aws-ssm-env --paths=/prod/
API_KEY=xxxxxx
DB_PASS=productionpass

aws-ssm-env は path を指定して、特定階層下の設定値を shell の変数定義の形式で出力します。コンテナの起動時に以下のような shell script を介することで、AWS SSM パラメータストアの設定値を環境変数に設定した状態でプロセスを起動できます。

#!/bin/sh
export AWS_REGION=ap-northeast-1
export $(aws-ssm-env --paths=/prod/)
exec /path/to/myapp

AWS SSM パラメータストアの SecureString という型の値は AWS Key Management Service (AWS KMS) により暗号化されるため、AWS KMS によって復号する権限がないアカウントでは値を得ることができません。Amazon ECS のタスクロールに AWS KMS の権限を与えることで、デプロイ作業を行う人には値を見せずに Amazon ECS タスク上のプロセスでは値を得られる、という状態も達成できます。

2018年11月には Secrets Support が提供されました。Amazon ECS コンテナエージェント1.22.0以上を利用し、以下のようにタスク定義に containerDefinitions[].secrets の定義をすることで、AWS SSM パラメータストアに保管されている値をタスクの環境変数に設定して起動できます。

"secrets": [
  {
    "name": "MY_SECRETS",
    "valueFrom": "my_secret_value"
  }
],

ssmwrap

aws-ssm-env とほぼ同等のツールですが、ssmwrap を弊社同僚が公開しています。

$ ssmwrap -paths=/prod/ -- /path/to/myapp

aws-ssm-env とは異なり、指定したコマンドを直接 exec するラッパーコマンドとして動作します。
これには以下のような利点があります。

  • Docker の entrypoint に直接指定可能
  • shell script を経由しないため、値に改行文字が含まれている場合の配慮が不要
  • -retries オプションにより、AWS SSM 呼び出しが失敗した場合にリトライできる
    • SSM パラメータストアは API rate limit があまり高くないため、一度に大量にタスクを起動してそこから大量の API 呼び出しを行うと、失敗することがあります

ログの集約とストリーミング処理

Amazon ECS タスクから発生するログは以下のようなものがあります。

  1. nginx などの Web サーバが出力するアクセスログ
  2. アプリケーション/ミドルウェアが標準出力、標準エラー出力に出力するログ
  3. アプリケーション的な意味があるログ (行動ログ)

アクセスログについては、Web サーバの設定により標準出力に書き出すことができます。つまり、実質的には2種類、コンテナから標準出力と標準エラー出力に出力されるものと、アプリケーションが独自に出力するものに大別されます。

各コンテナが stdout, stderr に出力したログは、Docker logging driver によって扱われます。
Amazon ECS においては、タスク定義でコンテナごとに logging driver と出力先を設定できます

  • awslogs: CloudWatch Logs へ送信
  • fluentd: Fluentd へ送信
  • json-file: ホストのファイルへ保存
  • syslog: syslog protocol で送信

これらのドライバのうち、awslogs と fluentd を検討しました。アプリケーションから発生する構造化された行動ログを Fluentd で取り扱う必要があったため、今回はすべて fluentd driver で統一することにしました。ただし Fluentd 自体のログについては、Fluentd へ配信すると Fluentd 自体にトラブルが発生した場合にログを確認できないため、stdout, stderr に出力したものを awslogs driver で Amazon CloudWatch Logs へ送信しています。

ecs-fluentd

この図のように、stdout, stderr のログについては Docker logging driver fluentd から、NLB (Network Load Balancer) 配下の Fluentd のタスクへ送信します。

Amazon ECS での Fluentd の扱い

Fluentd 経由でログを Amazon S3 へ保存する場合、Amazon S3 への出力プラグインを利用することが多いでしょう。

この場合、Amazon S3 へオブジェクトを生成するのは1〜5分程度の単位で行うのが一般的で、Amazon S3 へ書き出すまでは Fluentd が保持しているメモリやファイル上のバッファにログを保存しています。書き出し間隔を分単位にするのは、Amazon S3 への書き出し間隔が短すぎると S3 上のオブジェクトが細切れになりすぎて、その後扱うのが困難になるためです。

しかし、コンテナで動作させる場合、障害時に消失することを考慮すると、数分程度でもコンテナ上のメモリやファイルに保持するのは不安があります。そのため、信頼のできるバッファとして、Amazon Kinesis Data Streams (Amazon KDS) を採用しました。

Fluentd からは数秒程度のバッファリングを行って、Amazon KDS へ fluent-plugin-kienesis を使用してログを転送していきます。

kinesis-streams

Amazon KDS は AWS が提供しているマネージドサービスで保持期間(デフォルト24時間)は確実にデータを保全して、期間中は何度でも順序を保って再読み取りが可能ため、信頼の置けるバッファとして最適です。

Amazon KDS から Amazon S3 へのデータ書き出しは、これも AWS のマネージドサービスである Amazon Kinesis Data Firehose を利用しました。指定した間隔で Amazon S3, Amazon Redshift, Amazon Elasticsearch Service へ出力できますし、出力先に障害があった場合もリトライを行ってくれます。

ecs-log-diagram

最終的には、このような流れでコンテナから発生したログを Amazon S3 と Amazon Redshift へ保存する構成になりました。

tail -f kinesis-streams

先述したとおり、これまでファイルに追記されていたログをファイルに書かなくなったため、tail -f でログを順次読み取るようなオペレーションが困難になりました。Fluentd から Amazon KDS へ送信することで、この課題も解決できました。

kinesis-tailf というツールを、Go + aws-sdk-go で自作しています。

$ kinesis-tailf -stream docker-logs -region ap-northeast-1

このコマンドは、指定した stream を追尾し、標準出力へ出力します。

  • Amazon Kinesis Producer Library (Amazon KPL) によって圧縮されたログの展開に対応しています
  • 指定した shard のみ追尾ができます
  • -start -end オプションで、指定した時間帯の抽出も可能です

Go 言語による運用ツール/ミドルウェア開発

今回紹介した自作のツール、ssmwrap, kinesis-tailf の他にも、カヤックでは ecspresso という Amazon ECS のデプロイツールも開発していますし、他にも多数 OSS を公開しています。

このような OSS を作成したのは、AWS のマネージドサービス間にある隙間を埋めることで、自分たちに合った運用をスムーズにしようという考えからでした。

このようなツール・ミドルウェアを私は「隙間家具」と呼んでいます。

マネージドサービスは機能が十二分に揃った状態でリリースされるわけではなく、リリースされてから徐々に(利用者の要望も取り入れられて)機能が拡張されていきます。リリースから間もない頃は、他のマネージドサービスとの連携も十分に提供されていない状態がままあります。その隙間を、小さく、適度に汎用的なツールで埋めることで、よりよい運用が可能になるのです。

Rin to Firehose

隙間家具の具体例を、私がかつて作った Rin というツールを例に説明します。

rin

Rin は、Amazon S3 のイベント通知から SQS に送信されたメッセージを処理し、Amazon Redshift に COPY 文を発行して Amazon S3 → Amazon Redshift へのデータ取り込みを行うソフトウェアです。

Amazon Redshift へのまとまった量のデータの取り込みは S3 から行う必要があります。しかし2012年に Redshift がリリースされてから2015年に Amazon Kinesis Data Firehose が発表されるまで、マネージドサービスで Amazon Redshift に Amazon S3 から取りこむことはできませんでした。

社内のプロジェクトごとに取り込み処理を作るのは面倒なので、汎用的な取り込み手段として開発したのが Rin です。典型的なユースケースとしては、Fluentd で Amazon S3 へログを送信し、それを Rin によって Amazon Redshift へ取りこむ、というものでした。

without-rin

Amazon Kinesis Data Firehose は2015年10月に発表されました。Firehose によって、Fluentd から送信したログが Amazon S3 / Amazon Redshift へマネージドで取りこむことができるようになったものの、我々が主に利用している東京リージョン(ap-northeast-1) で Amazon Kinesis Data Firehose が利用可能になったのは 2017年です。そのため、Rin は開発後2年以上必要とされました。

ここで重要なのは、Fluentd から Amazon S3 / Amazon Redshift にデータが取りこまれるという、入口と出口の部分はそのままに、Rin から Amazon Kinesis Data Firehose に置き換えられたことです。

本来マネージドサービスがあるべき姿を想定し、隙間を適度な汎用性で埋めると、将来マネージドサービスに機能が拡張され、その隙間が埋まったときには無理のない状態で移行が可能になるのです。

ツール開発言語としての Go

我々は主に Go 言語をツール・ミドルウェア開発に利用しています。

この分野での Go 言語の利点としては、

  • 成果物がシングルバイナリになる
  • 書きやすさよりも読みやすさ、エラー処理がわかりやすいことを重視した言語設計

が上げられます。

実行バイナリが生成できると、コンテナ環境には言語ランタイムをインストールしなくてよくなるため、イメージサイズもビルド時間も節約できます。

ツール類は比較的長期間メンテナンスされるため、メンテナンス時に作者以外でも理解しやすい言語のほうが望ましいでしょう。

AWS のマネージドサービスを Go 言語から利用するためには、aws-sdk-go を利用しています。新しい機能がリリースされた時点で即 SDK のコードから利用可能になっているなど、機能の追従が早くで確実なので、大変便利です。

OSS として作る

我々が作成したツール類は、多くを OSS として公開しています。社内での必要性を元に作成されたものでも、OSS として公開することには意味があるためです。

OSS として公開するためには、ドキュメントを最低限書かないといけません。書かなくても利用者が困るだけなのですが、名前を出して公開するからには書かないと恥ずかしいという意識が出るためです。

また、過度の社内事情の混入を防ぐためにも有効です。公開する場合には社内の特定プロジェクトの事情を考慮した機能は入れられないため、ツールとしてどこまでが責任分界点なのか、ということを意識して設計したり、必要であれば拡張のための機構を作ることになります。

さらに、Go で実装されてシングルバイナリになっていると、各自でビルドするのではなく OSS 版のリリースバイナリ (や Dockerhub の公開イメージ) を使ってください、という運用が容易です。プロジェクトごとに改変された fork 版の増殖を抑える効果もあります。

最後に

Amazon ECS でのサービス運用に関わる設計方針、運用手法、ツール開発についての雑多な話題となりましたが、皆様のご参考になれば幸いです。