先日こんな記事を見かけて、

Web, Worker, Websocketと要素が揃っているMastodonのスタックをXXで動かしてみる、というのは感触をつかむのによいなと思いました。

で、じゃあAWS Fargateではどうかなと、このようにしてみました。

Amazon ECS .png

事前準備

Dockerコンテナ以外で、マネージドサービスで揃えられるものは優先的にそちらを使用します。

  • AWSで用意
    • VPS (既存でも新規でも)
    • RDS (Postgresで起動)
    • ACMで証明書 (ALBに適用する)
    • ALB
      • TargetGroup-web (HTTP/3000)
      • TargetGroup-stream (HTTP/4000)
    • S3バケット (PAPERCLIP Wikiのポリシー適用が楽)
  • Redis => Redis Labs / 30MBまでならFreeプランでOK
    • ElastiCacheが使いたければそちらでも
  • MailGunアカウント
    • 専用の認証情報をつくるとよいです

ALBは以下のルーティングを最後に割り当てます。

  • / (default)=> TargetGroup-web
  • /api/v1/streaming* TargetGroup-stream

こんな感じです。

EC2 Management Console 2017-12-03 14-04-04.png

Fargateを考慮したTask definitions

事前準備の内容が揃っていれば、Dockerのコンテナとして動かすのは次の3要素。

  • Web(Rails/puma)
  • Sidekiq(Ruby)
  • Streaming(Node.js)

これらをTask definitionsに起こすとして、全部のコンテナ定義を一つにまとめるとスケール変更が大げさになるので、それぞれを別のECSサービスとして登録できるようにバラします。
compose感はなくなりますが、Fargateならその方が良いように思います。

各タスクはmastodonリポジトリのdocker-compose.ymlをベースに、kubedonを参考にしつつアレンジ。

.env.production

大筋として本家のdocker-composeを使いまわすので、もともと用意されている.env.productionから環境変数という仕組みをほぼ踏襲します。
今回このようにしました。

.env.production
RAILS_ENV=production
NODE_ENV=production
REDIS_URL=redis://:<RedisLabsのパスワード>@<RedisLabsのエンドポイント>

DB_HOST=<RDSのエンドポイント>
DB_USER=<RDSのユーザ>
DB_NAME=<RDSのDB名>
DB_PASS=<RDSのパスワード>
DB_PORT=5432

LOCAL_DOMAIN=<ALBに向けるドメイン>
LOCAL_HTTPS=true

PAPERCLIP_SECRET=<rake secretで作成>
SECRET_KEY_BASE=<rake secretで作成>
OTP_SECRET=<rake secretで作成>

VAPID_PRIVATE_KEY=<rake mastodon:webpush:generate_vapid_key で作成>
VAPID_PUBLIC_KEY=<rake mastodon:webpush:generate_vapid_key で作成>

DEFAULT_LOCALE=ja

SMTP_SERVER=smtp.mailgun.org
SMTP_PORT=465
SMTP_LOGIN=< MAILGUNの認証ユーザ >
SMTP_PASSWORD=< MAILGUNのパスワード >
SMTP_FROM_ADDRESS=noreply@< MAILGUNのドメイン >
SMTP_TLS=true

S3_ENABLED=true
S3_BUCKET=< S3のバケット >
AWS_ACCESS_KEY_ID=< S3用のIAMユーザのキー >
AWS_SECRET_ACCESS_KEY=< S3用のIAMユーザのシークレットキー >
S3_REGION=< S3のリージョン >
S3_PROTOCOL=https
S3_HOSTNAME=< S3のリージョン別エンドポイント >

STREAMING_CLUSTER_NUM=1

web

web用にdocker-compose.web.yml, ecs-param.web.ymlを用意します。ecs-cliの取り回しを考えると、ディレクトリ分けて標準のファイル名を使う方が楽だったかも。
また、汎用性を考えたらloggingは分けて、--fileで追加指定してもよいはずです。

docker-compose.web.yml
version: '2'
services:
  web:
    image: gargron/mastodon:v2.0.0
    env_file: .env.production
    command: ["/bin/ash", "-c", "rails db:migrate && rails assets:precompile && rails s -p 3000 -b 0.0.0.0"]
    ports:
      - "3000:3000"
    mem_limit: 2GB
    logging:
      driver: awslogs
      options:
        awslogs-group: mstdn-fargate-demo
        awslogs-region: us-east-1
        awslogs-stream-prefix: web

assign_public_ipはイメージの取得にDockerhubを使う場合ENABLEDでないとPullできません。(※他にルートを用意できればおそらく可)
SGはALBとさえ通信できればOKなので、特にきにせずENABLEDで良いでしょう。

ecs-param.web.yml
version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_execution_role: ecsTaskExecutionRole
  task_size:
    cpu_limit: 512
    mem_limit: 2GB
  services:
    web:
      essential: true
run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - <YOUR_SUBNETa>
        - <YOUR_SUBNETb>
      security_groups:
        - <YOUR_SG>
      assign_public_ip: ENABLED

webは他に比べてスペック高、ほぼasset:precompileのため(メモリが足りないと落ちる)です。

作成したファイルたちを指定してecs-cli composeを使ってサービス登録するにはこんな感じ。
CLUSTER_NAME=既存ECSクラスタの名前、TARGET_GROUP_WEB=参加させるALBのターゲットグループに置き換えで。

$ ecs-cli compose \
  --file docker-compose.web.yml \
  --ecs-params ecs-param.web.yml \
  --project-name mstdn-fargate-web \
  --cluster ${CLUSTER_NAME} \
  service up --launch-type FARGATE \
  --container-name web \
  --container-port 3000 \
  --target-group-arn ${TARGET_GROUP_WEB}
  # --timeout 0

--target-group-arnは後から変更できないので、変更が必要な場合はサービスを一旦消すか別名で作成です。

なお実行の際、CPUをケチる(<1024)と--timeout 0を付与しておかないとデフォルト待ちの5分ではassets生成が終わりません。
このあたり考慮すると、実際に運用する場合は起動時間のことも考えて、よそでprecompileしてpublicを別に用意するのが得策でしょう。

streaming

streaming用にdocker-compose.streaming.yml, ecs-param.streaming.ymlを用意します。ecs-cliの取り回し(以下略

docker-compose.streaming.yml
version: '2'
services:
  streaming:
    image: gargron/mastodon:v2.0.0
    env_file: .env.production
    command: ["npm", "run", "start"]
    ports:
      - "4000:4000"
    logging:
      driver: awslogs
      options:
        awslogs-group: mstdn-fargate-demo
        awslogs-region: us-east-1
        awslogs-stream-prefix: streaming

streamingのパラメータ、サイズは最小。FargateはCPUとメモリの組み合わせに制限があるようです。

ecs-param.streaming.yml
version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_execution_role: ecsTaskExecutionRole
  task_size:
    cpu_limit: 256
    mem_limit: 512
  services:
    streaming:
      essential: true
run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - <YOUR_SUBNETa>
        - <YOUR_SUBNETb>
      security_groups:
        - <YOUR_SG>
      assign_public_ip: ENABLED

作成したファイルたちを指定してecs-cli composeを使ってサービス登録するにはこんな感じ。webとほぼおなじですね。
CLUSTER_NAME=既存ECSクラスタの名前、TARGET_GROUP_STREAMING=参加させるALBのターゲットグループに置き換えで。

$ ecs-cli compose \
  --file docker-compose.streaming.yml \
  --ecs-params ecs-param.streaming.yml \
  --project-name mstdn-fargate-streaming \
  --cluster ${CLUSTER_NAME} \
  service up --launch-type FARGATE \
  --container-name streaming \
  --container-port 4000 \
  --target-group-arn ${TARGET_GROUP_STREAMING}

sidekiq

sidekiq用にdocker-compose.sidekiq.yml, ecs-param.sidekiq.ymlを用意します。ecs-cliの取り回し(以下略

docker-compose.sidekiq.yml
version: '2'
services:
  sidekiq:
    image: gargron/mastodon:v2.0.0
    env_file: .env.production
    command: ["sidekiq", "-q", "default", "-q", "mailers", "-q", "pull", "-q", "push"]
    logging:
      driver: awslogs
      options:
        awslogs-group: mstdn-fargate-demo
        awslogs-region: us-east-1
        awslogs-stream-prefix: sidekiq

こちらも最小でOK。

ecs-param.sidekiq.yml
version: 1
task_definition:
  ecs_network_mode: awsvpc
  task_execution_role: ecsTaskExecutionRole
  task_size:
    cpu_limit: 256
    mem_limit: 512
  services:
    sidekiq:
      essential: true
run_params:
  network_configuration:
    awsvpc_configuration:
      subnets:
        - <YOUR_SUBNETa>
        - <YOUR_SUBNETb>
      security_groups:
        - <YOUR_SG>
      assign_public_ip: ENABLED

CLUSTER_NAME=既存ECSクラスタの名前に置き換えで。

$ ecs-cli compose \
  --file docker-compose.sidekiq.yml \
  --ecs-params ecs-param.sidekiq.yml \
  --project-name mstdn-fargate-sidekiq \
  --cluster ${CLUSTER_NAME} \
  service up --launch-type FARGATE

ECS状況

$ ecs-cli ps --cluster ${CLUSTER_NAME}
Name                                            State                                                                               Ports                          TaskDefinition
775a399c-19e1-4e2a-bd3d-98f8b4d7304e/streaming  RUNNING                                                                             34.204.191.189:4000->4000/tcp  mstdn-fargate-streaming:1
921f2d1a-0e4f-49a6-a5b5-434e2b91cf11/sidekiq    RUNNING                                                                                                            mstdn-fargate-sidekiq:2
edb1ba2e-533d-4afd-aaee-39db59ee616a/web        RUNNING                                                                             34.204.174.122:3000->3000/tcp  mstdn-fargate-web:15

$ aws ecs list-services --cluster ${CLUSTER_NAME} 
{
    "serviceArns": [
        "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:service/mstdn-fargate-web", 
        "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:service/mstdn-fargate-sidekiq", 
        "arn:aws:ecs:us-east-1:xxxxxxxxxxxx:service/mstdn-fargate-streaming"
    ]
}

それぞれ別のECSサービスとして動かせたので、個別にコンテナ数を変更できて良い感じのはず。

Mastodon 2017-12-03 14-02-01.png

Mastdon admin登録

さて、Mastodonの管理者になるにはmastodon:make_adminタスクを実行すればよいのですが、Fargate上のDockerコンテナで何か実行(docker exec相当)できるのだろうか。

さっとアタッチはできないぽいので、これもtask definitionからやりますか。

yamlをかいてー

docker-compose.web_make_admin.yml
version: '2'
services:
  web:
    image: gargron/mastodon:v2.0.0
    env_file: .env.production
    command: ["/bin/ash", "-c", "rails mastodon:make_admin USERNAME=sawanoboly"]
    mem_limit: 512MB
    logging:
      driver: awslogs
      options:
        awslogs-group: mstdn-fargate-demo
        awslogs-region: us-east-1
        awslogs-stream-prefix: railstask

ecs-cli compose upと。

$ ecs-cli compose \
  --file docker-compose.web_make_admin.yml \
  --ecs-params ecs-param.web.yml \
  --project-name mstdn-fargate-makeadmin \
  --cluster ${CLUSTER_NAME} \
  up --launch-type FARGATE

で、管理メニューアクセスオープン。

レポート - Mastodon 2017-12-03 14-34-44.png

少々手間だが、できるのでよいです。

おわりに

これまではスケール変更を柔軟にしようと思ったら結局ECSクラスタなりk8s、docker swarmなどのノードを確保しておく必要があったことを考えると、リソース配分が目に見える分だけになるのでシンプルです。

Fargateで起動するコンテナと通常のEC2インスタンスを比較すると、数字上で同等のCPU/メモリのインスタンスを維持するよりFargateのほうがちょっと割高になるはずです。
しかし、EC2ではサービス用のプロセス以外にも色々動かしたりケアする場所も多いため、コンテナより余分に性能やらディスクやらが必要になります。Fargateは下限ギリギリを攻めてもよい分、トータルでどっちがよいかは動かしたいものによるでしょう。

スケールアウトの対応速度は比較にならないほどFargateの方がはやいです。※この記事の例での構成ではコンテナイメージの最適化をしてないので少々もたもたしますが。

Fargate専用のSpotFleetでないかなあ。