Dockerこんにちは、エムスリーエンジニアの園田です。エムスリーテックブログの続きです。エムスリーでは医療・ヘルスケアサイト向けのコンテンツ配信システムであるChuoiというサービスを運用しています。先日のポストで、ElasticBeanstalkからFargateに運用を切り替えたことについて書きました。www.m3tech.blog今回はその実装編で、実際のコードを見ながら説明します。Fargateで運用するために実際にやった作業は大まかに以下の通りです。動作確認までを、弊社事例をサンプルに説明したいと思います。使ってないぜ、という方は次回からご覧いただいて大丈夫です。デプロイパイプラインの構築とTerraformテンプレートについてはおって投稿します。デプロイパイプラインの構築記事を書きました。www.m3tech.blogElixir/PhoenixアプリのDocker化Fargateで動かすためには当然のことながら、Dockerイメージを作成する必要があります。phx.serverというコマンドで起動していました。*1しかし、これだとDocker化するにあたりイメージサイズが肥大化してしまうため、リリースビルドによって生成された配布可能なバイナリを含むDockerイメージを作成しました。このサービスではクリックカウントなど業務メトリクスをログファイルに出力してfluentdでデータレイクに送信しているため、Dockerボリューム上にログを吐き出し、同一タスク内のfluentdコンテナでログを配信可能にする必要がありました。静的コンテンツをnginxのgzip_staticで配信したかったので、nginxのコンテナも作成しました。最終的に作成したDockerfileはアプリ(app)、ウェブ(web)、ログ配信(log)の3つとなりました。Dockerイメージの作成に関連するディレクトリ構成は以下のようになっています。yarn.lockdockerfilesディレクトリに作成するイメージごとのディレクトリを作成し、Dockerfileやentrypoint、設定ファイル等を格納しています。dockerfilesディレクトリ直下にあるシェルはAWSのCodeBuildで実行されるビルドスクリプトで、Dockerイメージには含まれません。*2コンテンツの取得・描画にTypeScriptを利用しているため、tsディレクトリやwebpack.config.jsがありますが、これらはDockerイメージビルド時にトランスパイルされ、Phoenixによってダイジェストが付与されます。コンテナイメージの作成は大きく分けて以下のステップとなります。以下がElixir/PhoenixアプリのDockerfile(dockerfiles/app/Dockerfile)です。$APP_ROOT最終的に生成されるDockerイメージを小さくするため、マルチステージビルドによりビルドイメージとランタイムイメージを分けています。*3各ステップのポイントは以下の通りです。公式にもあるdistilleryを利用しています。MIX_ENV*4などはビルド時に与える必要があります。--build-argsコマンドでビルドプロセスに注入されます。Storeに暗号化されて格納されています。ため、シークレット情報を安全にDockerイメージに埋め込むことが可能です。TypeScriptをトランスパイルして生成されたjsファイルにPhoenixの機能でダイジェストを付与しています。リリースビルド時に生成されるアーカイブファイルに含めるため、生成コンテンツはpriv/staticディレクトリに出力しています。distilleryを使ったリリースバイナリのビルドです。実際にはバイナリや静的ファイル、ライブラリおよび起動スクリプトが同梱されたtarボール(.tar.gz)が生成されます。Erlangのランタイムを含めてビルドできるため、このリリースバイナリの実行イメージにはErlangをインストールする必要がなくなります。これのおかげで、ランタイムイメージは非常にコンパクトなサイズにすることが可能です。*指定しているパスは実際にはバージョン番号が入りますが、Docker上でビルドする場合は常にシングルバージョンとなるため*で問題ありません。後でコピーしやすいようにルートパスに移動しています。ランタイムイメージのベースイメージは、ビルドイメージのベースイメージと同じイメージを利用します。ここでは、elixir:1.6.0-alpineのベースイメージerlang:21-alpineのさらにベースとなるalpine:3.7を指定しています。distilleryで生成される起動スクリプトはbashで実装されているため、ランタイムイメージにbashをインストールする必要があります。opensslはないとエラーになったので入れました。アプリ起動時にGzip圧縮済みの静的ファイルをNginxのドキュメントルートにコピーします。そのため、静的ファイルのディレクトリにはシンボリックリンクでわかりやすい別名を付けています。Nginxのドキュメントルートは、読み取り専用のDockerボリュームとしてWebコンテナ(Nginxコンテナ)にマウントされます。*5出来上がったイメージは40MBで、だいぶコンパクトにできました。webpackビルドもステージを分けようかと思いましたが、pullするイメージサイズが増えてかえって遅くなったのでやめました。環境ごとに異なるパラメータをイメージに埋め込むことはベストプラクティスではないです。セッションに機密性がないシステムであるため、このシステムに限ってはイメージに埋め込んでいます。Managerから取得できるようにした方が良いでしょう。Railsであれば以前紹介した拙作のgemが利用できます。ECRの利用者が限られていて、イメージ利用に関する監査などが行われているのであれば、イメージに埋め込んでも問題ないはずです。コンテナのDockerファイルはオフィシャルのnginxを利用するため、かなりシンプルです。ポイントだけ記載します。開発時やテスト時にも通信可能なようにしました。コンテナもほぼ公式のイメージそのままでシンプルです。設定ファイルで注意しなければいけないのは、コンテナなので終了時にバッファをフラッシュする必要があることです。trueを設定します。ここまででFargate上で動かすためのファイルが揃いました。CodePipelineを構築する前に、実際にFargate上で動作確認を行います。Docker化したアプリをFargateで動かすFargateを構築するまでの手順はかなり多く、大まかに以下の通りです。変わるので、あえて貼っていません。使わず、マネジメントコンソールで作成しました。ここはCLIで作成したほうが楽なので、コマンドも載せておきます。いうタグで登録されました。(ややこしいな)なお、予めイメージを登録しておかないと、ECSサービスを作成した際にタスクが起動せずにエラーになります。エラーが出てもあとから登録すれば勝手に起動してくれるので実害はないですが、エラーが出るのが気持ち悪いので先にイメージを登録しておく手順としました。Fargate(ECSサービスとタスク)用のIAMロールを作成Fargateを実行するために以下の2つのIAMロールを作成します。(ecsTaskExecutionRole)ECSタスクロールECSタスク実行ロールは、ECSサービスに割り当てられるIAMロールで、ECSタスクを実行するための認可を与えます。こちらはマネージドポリシー(AmazonECSTaskExecutionRolePolicy)が用意されているので、IAMロールを作成してポリシーをアタッチすればOKです。IAMロールのAssumeRolePolicyにはecs-tasks.amazonaws.comの信頼関係を設定しておきます。*7ECSタスクロールは、実行されたタスクのコンテナ内部で利用するIAMロールです。書き込み権限などを付与しておきます。こちらもAssumeRolePolicyはecs-tasks.amazonaws.comとなります。作成AWSのマネジメントコンソールで3つのコンテナを起動するタスク定義を作成します。ここでのポイントはボリュームの定義です。以下の3つのボリュームを定義しました。log-tmp静的ファイルはアプリのイメージに含まれているので、アプリの起動コマンドの中でボリュームにコピーするのは前述の通りです。コンテナではそれを読み取り専用でマウントします。コンテナが再起動した際にログを重複送信しないように、bufferファイルとpositionファイルはDockerボリュームに格納します。以下のようになります。シークレット値はコンテナ内のプロセスから取得するようにし、ここには埋め込まない。}セキュリティグループの作成FargateをWeb利用する上で必要なセキュリティグループは以下の2つです。ECSサービスのセキュリティグループALBのセキュリティグループ実現したいことは以下の通りです。上記を実現するためのセキュリティグループを実装します。ECSではサービスに対してセキュリティグループを割り当てるため、コンテナごとにセキュリティグループを分けることはできません。今回作成したセキュリティグループ今回作成したセキュリティグループを例に説明しますが、実現方法は1つではないのであくまでも一例としてとらえてください。セキュリティグループはすでに作成済みで、以下のルールとなっています。アクセス許可それとは別に、以下のセキュリティグループを作成しました。上記3つのセキュリティグループに対し、ECSサービスとALBを以下のように所属させます。sg-ecs-service余談ですが、私はセキュリティグループを作成するときは自身からのアクセス許可のみで作成し、そこにリソースを追加する形式で実装します。することで、セキュリティグループが他のセキュリティグループやリソースに依存しなくなり、Terraformで作成する際にモジュール化しやすくなります。ネットではよくセキュリティグループ間に対して通信許可を与えるパターンを目にしますが、これはTerraform利用においてはアンチパターンです。ECSサービス用のALBとターゲットグループを作成こちらの手順は通常のALB作成手順のため割愛しますが、ターゲットグループはipをターゲットとした空のターゲットグループを作成します。セキュリティグループは前述のとおりに割り当てます。ALBを利用せずにECSサービスをPublicサブネットに配置する場合はこの手順は不要です。動作確認するだけであればALBを使わずにIP直アクセスでも事足りることが多いでしょう。サービスを作成します。動作確認だけなので、タスク数は1で、スケーリングポリシーは設定しません。コンテナを選択ECSサービスを作成すると、自動的にタスクが開始されます。配信されているのでそちらで動作確認を行います。Fargate上で起動した際に発生したエラーRedisにつながらないこれはdistilleryのドキュメントをよく読んでいなかったために発生したエラーです。config.exsの設定がリリースビルド時に静的にコンパイルされますが、それを知らなかったため発生しました。これらはDockerイメージのビルド時に評価されるため、localhostに対して接続しようとしてエラーになっていました。redis_connection_sizeこちらは実行時(コネクション接続時)に環境変数から取得するように関数化しました。以下はRedisコネクションプール作成時の実装(簡易版)です。ファイルの書き込みに失敗していました。実行ユーザをrootに変更したら書き込めるようになりました。以下のように修正して、DockerイメージにCOPYします。/bin/entrypoint.shここらへんはFargateというよりDockerの制約ですね。エラーが発生していました。詳しくは調査していませんが、NginxでのProxy先ホストをlocalhostとしていたため、127.0.0.1だけでなく[::1]にProxyしようとしてエラーになっているようでした。以下のように書き換えました。エラーが発生しなくなり、スループットも向上しました。*9Redisコネクション数が急増してRedisが詰まるこれは単純なオペミスですが、ElasticBeanstalkの旧環境が起動したままFargateを起動したため、一時的にRedisのコネクション数が倍増してRedisが詰まりました。正しい手順として以下のようにデプロイする必要があります。Redisだけでなく、DB(RDS等)でも同様です。(縮退運転)FargateタスクのRedisコネクション数を本来の半分程度にするFargateサービスを作成するRoute53のALIASレコードをElasticBeanstalkからFargateのALBに変更するElasticBeanstalkのRedisコネクション数をさらに少なくするFargateのコネクション数を増やすなお、当然のことながらRedisコネクション数を環境変数等で制御できるようになっている前提です。一時的に縮退運転となるため、トラフィックが安定している時間帯に行います。Redisのコネクション数に余裕があればこの対応は不要です。Fargate云々ではなく、一般的なシステム移行では当たり前のことですね。実行コンテナイメージを軽量化できる。リリースビルドを生成する場合、設定値の環境変数をビルド時に渡すか、実行時評価のための関数に変更する。必要に応じてトラップする。Dockerボリュームもタスク内だけの揮発性。セキュリティグループを作成するときは自身からのアクセス許可のみで作成し、そこにリソースを追加する。自動生成されるリソースも、自前で作成したほうがTerraformフレンドリー。デプロイパイプラインの構築について書きたいと思います。デプロイパイプラインの構築記事を書きました。積極的に利活用しています。カジュアル面談にお越しください。それらのファイルを実行しています*3:最終的なイメージサイズが小さくなればいいため、ビルドイメージはあえてステップを細かく刻んでいます*4:MIX_ENVはElixirプロジェクトの環境を表す変数で、RAILS_ENVのElixir版みたいなものです。*5:こちらもDockerイメージに含めてしまえばよかったのですが、ちょっとイメージの管理が煩雑になりそうだったのでやめました*6:同じやりかたで、worker数なども環境ごとに変更できるようになります。いうロールが自動生成されますが、Terraformフレンドリーではないので自前で作成します*8:どれか1つでもコンテナが落ちたらタスク全体が失敗するように設定するためこの設定は不要かもしれませんが、このシステムではログが重要なため、念のためこうしています。2名によるシステムリニューアル支援(前編)こんにちは。サーバーサイドの足回りの試行錯誤録エンジニアリンググループの矢崎(id:Saiya)です。U...2018-08-20DatadogでDocker監視が幸せになった話こんにちは、エンジニアリングGの安田(@dasoran2)です。月別アーカイブ注目記事はてなブログをはじめよう!m3techさんは、はてなブログを使っています。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。