こんにちは。WEARリプレイスチームの id:takanamito です。
先日、社内で初めてAWS Fargate上でRailsを動かす環境をつくったので、その事例報告をしようと思います。
Fargate導入のきっかけ
WEARでは先日RubyKaigi 2019のスポンサーセッションでお話したように、Ruby on Railsへのシステムリプレイス作業を進めています。
そんな中、手作業で行っている運用を管理画面上でツール化したいという要望が上がってきました。リプレイス作業中であるため、できれば新機能はRailsで実装をしたいところです。しかし管理画面に相当するアプリケーションをデプロイするインフラはまだありませんでした。
WEARリプレイスでは、インフラの構築先としてAWSを採用しています。AWS上で新規にRailsを動かす環境を作るとすると有力な選択肢は次の3つになるかと思います。
- EC2上に環境をつくる
- ECS on EC2でコンテナを動かす
- Fargateでコンテナを動かす
リプレイスプロジェクトでは、私たちアプリケーションエンジニアがインフラ構築/運用に参加する事を踏まえ、運用作業をできるだけ少なくしたいという想いがありました。
SREチームとも相談した結果、よりマネージドな環境でコンテナを運用するためFargateでRailsアプリケーションのコンテナを動かす方式を採用することにしました。
運用工数の削減については弊社の塩崎が以前に書いた記事で言及されているので合わせてご参照ください。
今回の記事では実際に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 AppUsing 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アプリケーションの開発/運用改善に興味がある方を募集しています。興味のある方はぜひ以下のリンクからご応募ください。