Fargate x Railsで考慮したassets配信・ログ・秘匿情報管理・モニタリングについて

こんにちは。WEARリプレイスチームの id:takanamito です。
先日、社内で初めてAWS Fargate上でRailsを動かす環境をつくったので、その事例報告をしようと思います。

f:id:takanamito:20190523175756p:plain

Fargate導入のきっかけ

WEARでは先日RubyKaigi 2019のスポンサーセッションでお話したように、Ruby on Railsへのシステムリプレイス作業を進めています。

f:id:takanamito:20190419100636j:plain

そんな中、手作業で行っている運用を管理画面上でツール化したいという要望が上がってきました。リプレイス作業中であるため、できれば新機能はRailsで実装をしたいところです。しかし管理画面に相当するアプリケーションをデプロイするインフラはまだありませんでした。
WEARリプレイスでは、インフラの構築先としてAWSを採用しています。AWS上で新規にRailsを動かす環境を作るとすると有力な選択肢は次の3つになるかと思います。

  1. EC2上に環境をつくる
  2. ECS on EC2でコンテナを動かす
  3. Fargateでコンテナを動かす

リプレイスプロジェクトでは、私たちアプリケーションエンジニアがインフラ構築/運用に参加する事を踏まえ、運用作業をできるだけ少なくしたいという想いがありました。
SREチームとも相談した結果、よりマネージドな環境でコンテナを運用するためFargateでRailsアプリケーションのコンテナを動かす方式を採用することにしました。
運用工数の削減については弊社の塩崎が以前に書いた記事で言及されているので合わせてご参照ください。

techblog.zozo.com

今回の記事では実際にRailsを動かしていくにあたって考慮したことについて詳細をお話します。

コンテナ環境で動かすにあたって考慮したこと

弊社ではIQONなど既に運用中のRailsアプリケーションがありますが、コンテナは使っておらずEC2インスタンス上にChefで環境を構築しています。
今回、社内で初めてコンテナで動かすRails環境を構築することになったため、以下のような点で新しい仕組みを考える必要がありました。

  • assets配信
  • ログ出力
  • 秘匿情報の注入
  • リソース監視

現在運用中のRailsアプリケーションと、今回つくるFargate環境との比較を以下の表にまとめました。詳細をこの後に書いていきます。

非コンテナ(EC2) Fargate
assets配信 Nginxからstaticに配信 Railsから直接配信
ログ出力 ファイルに出力 CloudWatch Logsに連携
秘匿情報の注入 インスタンスの環境変数として渡す コンテナ内に環境変数として渡す

本来、新規開発のアプリケーションであればデータベースマイグレーションの仕組みを考える必要がありますが、マイグレーションは既にRails以外の別の方法で行われているため考慮から外しました。

assets配信

よくあるRailsアプリケーションの構成ではNginxなどのリバースプロキシからassets配信をすると思いますが、今回はRailsから直接assets配信をすることにしました。
assets配信を考える際には以下の項目を考慮しました。

  • そもそも社内限定の管理ツールのためトラフィックが極めて少ない
  • The Twelve-Factor Appに従いassetsを含めコンテナで完結させたい

リバースプロキシを使わずにRailsでassets配信をするためCDNの利用も検討しましたが、そもそも社員しか使わないアプリケーションでトラフィックが極めて少なくレスポンスタイムも問題になるほど遅くなかったので導入は避けました。

また同時にasset_syncなどのgemを使ってS3にホスティングすることも考えましたが、Herokuのドキュメントでも触れられているようにThe Twelve-Factor Appの考えに従いこちらも導入は避けました。

The twelve-factor app is completely self-contained and does not rely on runtime injection of a webserver into the execution environment to create a web-facing service.
The Twelve-Factor App

Using Asset Sync can cause failures. Heroku recommends using a a CDN instead of asset_sync whenever possible.
Please Do Not Use Asset Sync | Heroku Dev Center

ログ出力

実装した当時、FargateではawslogsドライバーのみがサポートされていたためRailsログはログドライバーを通じてCloudWatch Logsに送信されます。
デフォルトのRailsのログは複数行に改行されてしまい、CloudWatch Logs insightsでうまく検索できないため lograge gemを使ってjson形式に変換しています。

# config/environments/production.rb
Rails.application.configure do
  config.lograge.enabled = true
  config.lograge.formatter = Lograge::Formatters::Json.new
  config.lograge.custom_options = lambda do |event|
    exceptions = %w(controller action format authenticity_token)
    {
        host: event.payload[:host],
        timestamp: Time.zone.now,
        params: event.payload[:params].except(*exceptions),
        exception: event.payload[:exception],
        exception_object: event.payload[:exception_object],
    }
  end
  ...
end

秘匿情報の注入

DBのパスワードなどの秘匿情報をどのようにコンテナ内部に渡すかも課題でした。
例えばRailsのEncrypted Credentialsを使うのも選択肢のひとつですが、弊社では以前からAWS Systems Managerパラメータストアを使った秘匿情報の管理運用をしていたためそれに合わせた仕組みを採用しました。
Fargateプラットフォームバージョン1.3からシークレットを扱えるようになっていますが今回は採用を見送りました。理由は後述します。

具体的にはdocker entrypointに指定しているシェルスクリプトで、パラメータストアから取得した値をコンテナの環境変数に埋めています。

# Dockerfile
FROM ruby:2.6.3

RUN apt-get update -qq && apt-get install -y build-essential nodejs awscli

# ...中略

WORKDIR /wear
COPY Gemfile /wear/Gemfile
COPY Gemfile.lock /wear/Gemfile.lock
RUN bundle install
COPY . /wear
RUN bundle exec rails assets:precompile

ENTRYPOINT ["./docker-entrypoint.sh", "bundle", "exec"]

環境変数は次のようにして埋めています。

#!/bin/bash

set -e

export DB_USER=$(aws ssm get-parameters --names "/db_user")
export DB_PASSWORD=$(aws ssm get-parameters --names "/db_password")
export SECRET_KEY_BASE=$(aws ssm get-parameters --names "/secret_key_base")

exec "$@"

パラメータストアを使うことで秘匿情報にアクセスできる開発者を限定できる + CloudFormation(CFn)を使ってコードで権限管理が可能なところは利点かと思います。
そのため弊社ではAWSのオーケストレーションツールとしてCFnを使用しています。
Fargateプラットフォームバージョン1.3でサポートされたシークレットの仕組みを採用したかったのですが、今回のインフラ構築をしていた時点ではまだCFnにSecretの機能が反映されていなかったため、APIを通じてパラメータストアから値を取得する方法を選択しています。

開発状況はこのissueで報告されており、現在開発が進行中とのことです。
[ECS] [CloudFormation]: CloudFormation support for Secrets · Issue #97 · aws/containers-roadmap · GitHub

リソース監視

コンテナのメトリクス監視にはDatadogを使用しています。こちらは先述の塩崎の記事で紹介されていたのと同様、サイドカーパターンでコンテナのメトリクスを収集しています。

苦労した点

今回、自分で構築するコンテナデプロイ環境の構築が初めてだったため、かなり色んなハマりポイントにつまずきながらの作業になってしまいました。

Fargateは通常sshできない環境での作業を強いられるためそれによる確認作業の進めにくさは感じました。
特にコンテナ起動で試行錯誤している最中に「環境変数が設定できているか」など、インスタンスに入ってコマンドを叩けば一瞬で終わるはずの作業ができないのはストレスでした。
またAWSマネージドサービスを多用しているため、書いたコードが悪いのか、IAMの権限不足が悪いのかなど原因の切り分けにも苦労する場面が多かったです。

私は最終的にsshが可能なコンテナイメージを用意して確認をしました。これからコンテナデプロイ環境を構築される方は最初に用意しておくと確認作業が捗るかもしれません。

一部ではありますが、今回の環境構築で使用したCFnのymlファイルを置いておきます。

  ECSTaskDefinitionRailsApplication:
    Type: 'AWS::ECS::TaskDefinition'
    Properties:
      RequiresCompatibilities:
        - 'FARGATE'
      Cpu: 512
      Memory: 1024
      NetworkMode: 'awsvpc'
      TaskRoleArn: !GetAtt IAMRoleForRailsApplicationTaskRole.Arn # パラメータストアへのアクセス権限を付与
      ExecutionRoleArn: !GetAtt IAMRoleForRailsApplicationTaskExecution.Arn
      ContainerDefinitions:
        - Image: !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryRailsApplication}:latest
          Name: 'rails_application'
          Cpu: 256
          Memory: 512
          Command:
            - 'puma'
            - '-e'
            - !GetAtt SSMParameterRailsEnv.Value
          Environment:
            - Name: 'RAILS_ENV'
              Value: !GetAtt SSMParameterRailsEnv.Value
          PortMappings:
            - ContainerPort: 80
              HostPort: 80
              Protocol: 'tcp'
          LogConfiguration:
            LogDriver: 'awslogs'
            Options:
              awslogs-group: !Ref LogsLogGroupForRailsApplication
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: 'rails_application'
        - Image: datadog/agent:latest
          Name: 'datadog'
          Cpu: 256
          Memory: 512
          Environment:
            - Name: 'DD_API_KEY'
              Value: !Ref DatadogAPIKey
            - Name: 'ECS_FARGATE'
              Value: 'true'
          LogConfiguration:
            LogDriver: 'awslogs'
            Options:
              awslogs-group: !Ref LogsLogGroupForRailsApplication
              awslogs-region: !Ref AWS::Region
              awslogs-stream-prefix: 'rails_application-datadog'
              
  ECSServiceRailsApplication:
    Type: 'AWS::ECS::Service'
    DependsOn:
      - IAMServiceLinkedRoleForECSRailsApplicationService
    Properties:
      Cluster: !Ref ECSClusterRailsApplication
      DesiredCount: 2
      LaunchType: 'FARGATE'
      LoadBalancers:
        - ContainerName: 'rails_application'
          ContainerPort: 80
          TargetGroupArn: !Ref ElasticLoadBalancingV2TargetGroupExternalRailsApplication
      NetworkConfiguration:
        AwsvpcConfiguration:
          AssignPublicIp: 'DISABLED'
          SecurityGroups:
            - !Ref EC2SecurityGroupECSRailsApplication
          Subnets:
            - !Ref EC2SubnetPrivateApplicationAZ1
            - !Ref EC2SubnetPrivateApplicationAZ2
      TaskDefinition: !Ref ECSTaskDefinitionRailsApplication

まとめ

初期の環境構築に今までと一味違った苦戦があったことは事実ですが、EC2インスタンスの管理から解放されるメリットはやはり大きいと感じています。

弊社では今までの仕組みにとらわれず新しい事例を作ることに興味がある方、Webアプリケーションの開発/運用改善に興味がある方を募集しています。興味のある方はぜひ以下のリンクからご応募ください。

www.wantedly.com

カテゴリー