最近の砂場活動その2: ECS/Fargate/AWS Batch編

ゴールデンウィークということもあり、前回に引き続き砂場活動をやりました。前回はCloudFormationを使い、ElastiCache/RDSを立てて、EC2の上でWebアプリを動かすところまでをやりました。

今回はECSやFargate、AWS Batchなどコンテナの恩恵をより受けられるようにしてみようと思います。大分モダンになってきた。

コンテナ化

ECSで動かすことを見越して、Webアプリをコンテナ化しました。といってもDockerfileを書く程度なので、大したことはない。中間ファイルに吐いていたような状態はRDS/ElastiCache/S3などの外部のDBやストレージに持たせます。作ったコンテナはあとでECSで使えるようにECRのリポジトリにpushしておきます。

ECSで動かす on EC2

コンテナ化したので、ECSの上で動かします。ECSはあまり馴染がないので、概念を少し整理します。ECSはコントロールプレーンというものの一つで、コンテナを管理するものです。コンテナの管理とは

  • どこでコンテナを動かすか
  • コンテナの生死の判定
  • デプロイ時のコンテナの配置戦略

などのことを指します。最近Preview版になったEKS(Amazon Elastic Container Service for Kubernetes)もコントロールプレーンの一つです。また、実際のコンテナが稼動する場所のことをデータプレーンと呼びます。データプレーンの具体例としては、EC2やFargateがあります。コントロールプレーンとデータプレーンの組み合わせがあるので

  • ECS on EC2
  • ECS on Fargate
  • EKS on EC2
  • EKS on Fargate
    • これはまだっぽい?

が可能な選択肢のようです(xxx on yyyの呼び方は適切か分からない...)。一気にFargateまで行くとよく分からないことになりそうだったので、データプレーンに馴染みのあるECS on EC2を選択してひとまず進めます。

ECSを構成する概念

大きく分けると3つあります。

  • クラスタ
  • ジョブ定義
  • サービス

ジョブ定義ではどのイメージを使うか、cpu/memoryの制限、コンテナのEntryPointの設定などをします。サービスは指定されたジョブ定義に対して、どのようなネットワーク構成(セキュリティグループやサブネット)で動かすか、走らせるジョブの個数、(必要であれば)ロードバランサーの設定などをするところです。今回はALBをロードバランサーとして選びました。

ECS用のEC2の準備

ECSのクラスタにEC2インスタンスをjoinさせるには準備が必要です。ECSに最適化されたAMIがあるので、それを使うことにしました。リージョン毎に用意されていることに注意。

また、立ち上げたインスタンスがどのECSクラスタに所属するかをユーザーデータに記述する必要があります。CloudFormationだと以下のような感じ。

          UserData:
            Fn::Base64:
              !Sub |
                #!/bin/bash
                echo ECS_CLUSTER=my-cluster >> /etc/ecs/ecs.config

ECSのクラスタを作ると、このインスタンスが使える状態として見えるようになります。

タスク定義/サービス定義

タスク定義にはdocker-compose.ymlを書くようなイメージです。サンプルを真似すれば大体書ける。クラスタの準備ができあがっていれば、個々のタスクを走らせることができます。ホストのEC2にsshしてdocker statsで様子を見ていました。

サービス定義は単発で終わるようなタスクではなく、ずっといくつかのコンテナを走らせ続けたいようなWebアプリなどに使う定義です(理解がちょっと怪しい)。あるコンテナが不調で死んでしまった場合、全体でいくつのコンテナが立ち上がっているのが望ましい状態なのか、deploy中は最大何個までコンテナが立ち上がっているのを許すのか、ロードバランサーのターゲットグループとして指定するものはどれかといったことを書いていきます。CloudFormationで具体的に書くとこんな感じです。

      ServerService:
        Type: AWS::ECS::Service
        Properties:
          Cluster: !Ref ECSCluster
          DeploymentConfiguration:
            MaximumPercent: 200
            MinimumHealthyPercent: 100
          DesiredCount: 1
          TaskDefinition: !Ref ServerTaskDefinition
          LoadBalancers:
            - TargetGroupArn: 
                Fn::ImportValue:
                  !Sub "${ALBStackName}:DefaultTargetGroup"
              ContainerPort: 7777
              ContainerName: go-active-learning-container
          NetworkConfiguration:
            AwsvpcConfiguration:
              AssignPublicIp: ENABLED
              SecurityGroups:
                - Fn::ImportValue:
                    !Sub "${VPCStackName}:WebSecurity7777Group"
                - Fn::ImportValue:
                    !Sub "${VPCStackName}:EC2SecurityGroup"
              Subnets:
                - Fn::ImportValue:
                    !Sub "${VPCStackName}:PublicSubnetAZa"

サービスを起動して、ALBのDNS名にアクセスしてページが見れていればokです。サービス部分のデバッグは最初少し自分には難しく、「ALBのターゲットグループにipが上がってこないから、サービスのネットワーク設定がいけないのか??」といった具合に試行錯誤していました。大分勘でやっていました。

オートスケール

アプリケーションサーバー(コンテナ)の台数を増減させて遊びたかったので、オートスケールもできるようにしようと思いました。少し調べてみると、ECSのオートスケールは

  • ECSクラスタのオートスケール
    • データプレーンであるEC2の台数自体の増減
    • 台数は変化させずにEC2のスペックをもっと強力なものにすることもできる
  • サービスのオートスケール
    • 立ち上げるコンテナの数の増減

があることが分かりました。

「2つの変数を自分でいい感じにチューニングするの絶対ダルいな…自分が欲しいのは可変にできるコンピューティングリソースなんだけどな…」と思ったところでFargateの存在を思い出しました。FargateならばEC2の管理は自分でやる必要がないので、自分で管理する変数が一つ減りますね!

ECSで動かす on Fargate

Fargateまでやるつもりはなかったんですが、EC2捨てたいという気持ちが高まったので、Fargate対応していきます。Fargateは東京リージョンにまだきていなかったので、VPCなどこれまでの設定をバージニアで作りなおしました。手で作りなおそうとすると結構面倒ですが、CloudFormationで構成を書いていたので、同じ構成がすぐに別リージョンで立ち上がってお手軽でした。

Fargateにするといっても、元々ECSで動く状態だったので、それほどやることは多くありません。ひとまず以下の設定を追加しました。

  • LaunchType: FARGATE
  • NetworkMode: awsvpc

Fargateになると完全にEC2の存在が見えなくなるので、ホストにsshしてdocker logsで様子を見るといったことができなくなります。Data Volumeも指定できないので、ログはCloudwatch Logsに吐くようにします。CloudFormationだとこんな感じ。

              LogConfiguration:
                LogDriver: awslogs
                Options:
                  awslogs-group: !Ref LogGroup
                  awslogs-region: !Sub "${AWS::Region}"
                  awslogs-stream-prefix: ecs

AWSのコンソール画面からログを見るのはつらいので、awslogsを使ってtail -fっぽく使うのがいいみたいですね。

慣れない場合はいきなりFargate上で動かすのではなく、デバッグしやすいEC2上で動くことを確認してからFargateにするのがいいかもしれません。自分がいきなりFargateにしていたら動作確認がちょっとしんどくなっていた可能性が高い。

Fargateの準備が整ったので、負荷をかけて遊びます。abコマンドのようなベンチマークツールでリクエストを飛ばしまくります。そうすると、ALBのターゲットグループでhealthyだったステータスがunhealthyになったり、drainingになったりします(つまり、過負荷でコンテナが死んだ)。しばらくすると新しいコンテナが立ち上がって、ALBのターゲットグループにhealthyの状態で登録されていきます。あまりにかわいいので、何回も殺してしまった…。下のスクリーンショットが死んだり新しいのが生えてきている様子です。

f:id:syou6162:20180506200529p:plain

手元でdocker runする感覚でコンテナがすぐ立ち上がるかと思ったけどそういうわけではなく、多少待たされます。docker pullの分時間がかかったりするのかな。

必要なコンテナの数やデプロイ時の設定はサービスに書いていきます。DesiredCountを適当に増やすと立ち上がるコンテナも増えます。余裕ができたらBlue-Green Deploymentみたいなこともやってみたいと思います。

      ServerService:
        Type: AWS::ECS::Service
        Properties:
          Cluster: !Ref ECSCluster
          DeploymentConfiguration:
            MaximumPercent: 200
            MinimumHealthyPercent: 100
          DesiredCount: 1
          LaunchType: FARGATE

Batch処理

私が動かしているのは機械学習を利用したWebアプリケーションなので、推薦の結果をWeb UIに表示させるだけではなく、学習のプロセスなどが必要です。学習はメモリもCPUもある程度必要なバッチ処理なので、CloudWatch Eventから定期的にAWS Batchを起動させるようにします。AWS Batchは仕事で使っているのでさすがにスムーズに行きました。AWS Batchは以下の3つから構成されています。

  • コンピューティング環境
    • VPCやサブネット、必要なVCpu数などを管理。リソースが足りなければ追加でEC2インスタンスを立ち上げ、不要になったら落としてくれる
  • ジョブキュー
    • キューのくせにどのコンピューティング環境にどういう優先順位でジョブを投入するかなどが指定できる
    • キューに状態が管理されているので、EC2が立ち上がりまくってAWS破産するといった心配がない
  • ジョブ定義
    • ECSのタスク定義と大体似たようなものです。最近、タイムアウトのサポートもされて安心感が増した

察しのいい方は分かるかもしれませんが、AWS Batchの裏側ではECSのクラスタやサービス、ジョブ定義が勝手に作られます。

学習ジョブや推薦結果の計算のジョブを定期的に投下していきたいのですが、そういったcron的なことまではAWS Batchは面倒を見てくれません。Cloudwatch Event経由でジョブをkickさせます。CloudFormationを使っていると、Cloudwatch Eventから直接AWS Batchのジョブをkickすることはできないので、AWS BatchのジョブをkickするLambdaをkickさせるようにしました。コンソールからは直接指定できるようです。

AWS Batchのモニタリングについてはジョブキューの各statusに存在しているジョブの個数を見るプラグインや、ECSのモニタリングをするプラグインがあるので是非ご利用ください(宣伝)。

困ったこと

  • Fargate、早く東京リージョンにきて!
  • そもそもcloudwatch logsにログが出ていないような場合にFargateでのよいデバッグ方法が分からない
  • 個人の遊びとしてはまあまあお金がかかる。勉強代と思って割切る
    • RDS/ElastiCache/ALB/Fargate、今回は登場していないけどクロールしてきたデータを貯めるAmazon Elasticsearch Serviceもいる…
  • モニタリングの仕方がこれまでと結構変わってくるのでベストプラックティスを確立していく必要がある
    • 例: mackerelのcheck-logでログ監視していたものが同じようにはできなくなる
  • ちょっとしたWebアプリを作っているつもりだったけど、CloudFormationのymlが合わせると1000行近く & スタックが9つもできていてどうしてこんなことに…

f:id:syou6162:20180506195347p:plain

まとめ

今年の自分の目標の一つに自分の砂場を作るというのをかかげていましたが、ひとまずそれなりの砂場はできました。いきなり完成系を目指すのではなく、incrementalに改善していけたことはよかったところとして挙げられるかなと思います。具体的には以下の通りに進めていきました。

  • 学習事例やキャッシュをファイルで保持するcli機械学習アプリケーションを書く
    • 去年までにやっていた
  • cliアプリのまま、学習事例はpostgresに保存、キャッシュはredisに保持するように
  • cliアプリをwebアプリとしても使えるように
  • ローカルで立ち上げていたwebアプリを1台のEC2上で動くように
    • 最初は手動でやっていたprovisioningをansibleでやるように
  • 同一のEC2上で動いていたpostgres/redisをRDS/ElastiCacheを利用するように
  • EC2で動かしていたwebアプリをECS on EC2で動かすように
  • ロードバランサーとしてALBを使うように
  • EC2の管理が不要となるようにECS on Fargateで動かすように
  • 重たい処理はAWS Batchで動かすように

亀のような進捗でしたが、自分の理解力だと一気に飛ばしてやるのはしんどかったと思います。一個一個確実に進めていったのがよかった。2018年の上期で自分が遊ぶ砂場ができたので、下期ではこの砂場を使ってインフラ/ミドルウェアの理解をさらに進められるように遊んでいこうと思います。