これまでもコンテナ関連の記事はそれなりに書いてきましたが、改めて最新事情に合わせて練り直したり見渡してみると、大きなところから小さなところまで選択肢が多すぎると感じました。
コンテナ系アーキテクチャを丸っと他所の構成で真似することって、おそらくほとんどなくて、参考にしつつ自分流に築き上げていくでしょうから、今回は築くにあたってどういう選択肢があるのかにフォーカスした変化系で攻めてみようと思った次第です:-)
目次
今年一発目の長いやつです。半分は学習教材用、半分は道楽なテイストです。- はじめに
- 基盤
- ロードバランサー
- ECS / EKS
- デプロイ:Blue/Green or RollingUpdate
- デプロイ:一括 or 徐々に
- デプロイ:減少 → 増加 or 増加 → 減少
- ヘルスチェック
- 名前解決
- Network:Public or Private NAT G/W or PrivateLink
- vCPU と Memory
- ECR Private or ECR Public or DockdrHub
- イメージタグ:latest or ID
- Task / Container
- ログ/監視
- Code系デプロイ
- Source CodeCommit のトリガー
- Pipeline 通知:CodeStar or EventBridge
- CodeBuild:EC2 or Lambda
- 承認フェーズ:要不要
- ビルド用ファイルの置き場所
- おわりに
はじめに
今回だしていく例は、社内用の外道塾のために作成した教科書や、基本構成となるコードをザッと眺めていって、選択の判断をしていったっぽい箇所を取り上げています。また、基盤は AWS かつ ECS 主体なので、物足りない部分も出るでしょうが、そのへんはご愛嬌ということで。あと、コンテナとしての原理原則というか理想みたいな部分は、基本守りつつあまり触れないので、そういう基礎から入りたい人は ↓ こういうドキュメントとか見つつ、
- コンテナ構築のおすすめの方法 | Cloud アーキテクチャ センター | Google Cloud
- コンテナ運用のベスト プラクティス | Cloud アーキテクチャ センター | Google Cloud
ちらほら置いておく私見などは、音楽性の違いとか宗教上の理由でそうしたんやろな、俺はこうするけど、くらいの感じで楽しく真面目に読んでもらえれば幸いでございます。
基盤
まずは、というかイキナリ大きなところとして、コンテナを稼働させる基盤カテゴリからチョイスしていきましょう。わりと未来を決める分水嶺になりますが、なるたけサクサク感多めで進行します。インスタンス or コンテナ
当然ですが、旧来の EC2 で推して参るのか、ECS/EKS のコンテナに舵を切るのか、が最初の分岐点になります。ぶっちゃけ、サービスを稼働させるという結果や、基盤としてのコスパ自体に、そこまで大きな差があるとは言えないと思います。特に差があるのは学習量で、コンテナの方が間違いなく数倍の学習と選択の量を必要とします。
既存の EC2 から無理にコンテナに切り替えるのは考えどころですが、新規ならコンテナでいくべき時代だよ、と言いたいところです。ですが、もしコンテナ経験者が少ない環境でゼロからなら、ちょっと序盤は厳しい時期が続くかもしれない、という感じなので、だからコレを書いているという側面もあります。
今の時代はコード化と自動化を是とするので、どちらの環境にしたとしても可能な限りそれを適用することにはなるのですが、コンテナにした方がよりキッチリ・カッチリとした仕上がりと運用になるだろう、と感じています。
インスタンス・ベースだと、どうしても細かいシステムや手順が、いつの間にか属人化したり再構築が難しくなったりしますが、コンテナ・ベースだとおそらく全てをコードとして残せるので、最悪でもコードを追えばなんとでもできるし、それすなわちシステムとしての安定した稼働・運用にも繋がる、のではなかろうかと思われます。
ECS or EKS
過去に Kubernetes、やめました | 外道父の匠 を書いた上での現状としては、普通に EKS を使っているサービスもあるし、別に嫌いではないけども、あんまり率先して選択するものではないな、と今でも思っています。しいて意見を述べるならば、基本的には ECS とし、Kubernetes でどうしても使った方が効率的になる機能があるならば EKS もありえる、という感じが程よいです。
コンテナ・アーキテクチャ全体から見れば、ECS/EKS の部分は大きいとはいえ一部でしかないので、それ以外の部分をそのままに入れ替えてもイケるやん!くらいに整理したり、両方動かしてみてから決めてもいいし、時間不足や決定力不足で迷うなら ECS が無難でイィと思います。
on EC2 or FARGATE
ECS/EKS を EC2 も管理しながら動かすか、FARGATE としてコンテナ(というかタスク)だけを管理するか、の違いです。EC2 も管理すると面倒が増える分、最新インスタンスタイプを使えたり、普通にスポットを使えたり、細かいやれることも増えたりもします。その代わりちゃんとやらないと、1台分のリソースをフルに使えなくて余剰リソースを大きくしてコスパを悪くしたり、など難しい部分も出てきます。
FARGATE は扱いが楽な分、昔はコスパが悪かったのですが、最近はCPUが強くなってだいぶコスパが良くなってきました。
個人的には ARM64 にするなら FARGATE 推しですが、常に真の最適化を求めるなら on EC2 になるので、そこは好みと運用加減次第、ということになります。
X86 or ARM64
FARGATE を選ぶなら、ARM64 必須としたい気持ちは 続・AWS ECS Fargate のCPU性能と特徴 2023年版 | 外道父の匠 の通りです。ミドルウェアやソフトウェアの都合があるのは承知の上で、それをなんとか解決してでも ARM64 にする価値があります。EC2 の場合も同様で、単純に同世代なら Graviton の方が安定してコスパが良い時代になってきているので、ARM64 推しになります。
ただし、時期によって優位性が異なるので、構築時には両対応とした構成を心がけて、いつでも切り替えていける姿勢でいるのが最もイケてると思われます。
ロードバランサー
ちょっと話が飛びますが、上から順にということで。メンテナンス:ALB or ECS Service
サービスをメンテナンス・モードに入れる仕組みは色々考えられますし、コンテナに限った話ではないのですが、何が最も適しているかを一度は考えて損はない箇所です。古い仕組みだと、WEB/APPサーバーの全台に対してデプロイすることで、メンテモードに切り替えたりしているかもしれません。しかしコンテナは後から手を加えることは良しとしませんし、全台が完全同時に切り替わるわけではないことや、そのタイミングに Autoscaling の Increase が発動しないように気を配る必要もあり、あまり美しいとは言えません。
そこでまずは ALB 自体がメンテ・レスポンスを返す方法を考えます。
ルールの優先度を入れ替えるだけで、約10秒 で確実に切り替わるのがメリットですが、レスポンスの要件で機能不足な場合は別の手段を考える必要があります。
別案として、メンテ用 ECS Service を用意し、それ用の TargetGroup と紐づけます。通常のアプリケーションと同じ内容で起動しますが、唯一メンテ用の環境変数を付与するところだけ変更します。アプリはそれを見て任意のパターンでメンテ用レスポンスを返すようにするだけです。
通常時はタスク数はゼロで、メンテ前に数タスク起動し、あとはメンテ10秒前にルールを切り替えるところは同じです。メンテ用イメージを作ったりせず、環境変数だけで状態を変更できるようにする、こういう仕組みが醍醐味といえるのではないでしょうか。
共有 or 1環境毎
ALB + ACM で扱う場合、DNSゾーンが共通していれば1つの ALB で複数の FQDN を複数の TargetGroup に振り分けることが可能です。また、1つの FQDN に1つの ALB を使うことももちろん可能です。それは場合によりけりではあるものの、開発環境などのために1つ1つの環境毎に ALB を作成していては、コスパが非常に悪いですし、2月からは IPv4 課金が始まってそれがより顕著になります。
そのため、少なくとも本番以外は可能な限りゾーンを共通化し、少ない ALB を共通利用するべきです。
本番はそもそもドメインが変わるでしょうから、別 ALB になるとして、問題は複数 FQDN がある場合に 1 ALB にするか、分けるべきか、と考えるでしょう。1台にした場合、1つの FQDN に対するトラフィックが急増した場合に、他の FQDN にも影響することになるからです。
私見としては、多くの場合は1台で共通化してよいです。ALB がトラフィック増加によって、最初の自動拡張が発動して数十秒のエラーが発生するのは、およそ 5000~6000 Requests/sec あたりなので、その付近をうろつく機会がなければ気にする必要はないからです。その辺を考慮して、安定のためやメトリクスを分けるなどで必要があれば、本番は複数の ALB に分けるとよいでしょう。
アクセスログ:ALB or WEBサーバー
いわゆるWEBサーバーのよくあるアクセスログを、丁寧に保管して残す必要があるサービスもあると思います。コンテナのログはストレージに不必要に蓄積させず、ストリームで吐き出すのが基本ですが、そもそもアクセスログは Apache や Nginx のような箇所で採取すべきなのかどうかって話です。もしかしたら、WEBサーバーすら置かずに直アプリケーション・サーバーなシステムもあるかもですしね。
もし ALB のアクセスログで内容が十分ならば、コンテナで fluentd を通すようなことをせず、ALB で S3 に直接残してしまえば良いでしょう。
WEBサーバーじゃないと採れないデータって WEB <-> APP の間の出来事や、任意のヘッダ値とか、そう多くないはずなので、何が必要なのかを整理した上で楽な方法を選ぶと良いでしょう。
ECS / EKS
ECS用のコードを眺めて書いているので基本 ECS の話ですが、EKS でも共通な部分もあるので、そのへんはザックリ感でお願いします。デプロイ:Blue/Green or RollingUpdate
デプロイの話で、大雑把に2種類あり、さらにそれぞれで細かい挙動を選択することになります。大きな違いは B/G は ALB TargetGroup が2つ、Rolling は1つかゼロで運用するところで、説明はこの辺です。- ローリング更新 – Amazon ECS
- CodeDeploy による ブルー/グリーンデプロイ – Amazon ECS
- AWS ECS + CodeDeploy – blue/green デプロイ と Canaryデプロイの違い ( ECSAllAtOnce とその他のデプロイ設定 ) #AWS – Qiita
B/G の ECSAllAtOnce は新旧を1発で完全に切り替え、それ以外は新旧が一時的に混在する形になるので、そもそも混在が許されるデプロイ内容なのか、が根本的な判断材料となり、アプリケーション開発者の希望を汲む必要があります。
色々な機能の ECS Service を作ることになるでしょうが、基本的には ALB 配下となる Service は Blue/Green で、それ以外は RollingUpdate を選択するのが無難でしょう。その上で、デプロイ中のリソース増減の流れを整理し、動作やデータに不整合などの不都合が起きないかを第一に考慮して、それを確保してからの運用し安さも考慮していくとよいです。
デプロイ:一括 or 徐々に
Blue/Greenデプロイにおける新旧リソースの入れ替えでは、一括での入れ替えと、線形やカナリアと呼ばれる徐々に新を100% にまで増やしていくタイプがあります。徐々に増やすことで悪影響が出たときに影響範囲を小さく留めてロールバックできたりしますが、新旧混在を許容する更新内容とする必要があり、かつデプロイ時間が長くなるので、効果的に運用するには相応の設計が必要です。
そもそもの話としては、新バージョンへの実際のアクセスでの動作確認って、最初はおそらく内部の人間に限定してやりたいことも多いでしょうから、そうなると構成がシンプルでなくなるので、事前のテストを含めた運用の一連の流れをどう最適化していくかっていう重めの話になっていくでしょう。
デプロイ:減少 → 増加 or 増加 → 減少
RollingUpdate では基本リソース量を 100% として、デプロイ時に最大 200% まで許容すると新Verを増加させてから旧Verを削除する流れにできます。逆に最小を 50% まで許容すると減少してから増加することになります。この考え方はコンテナに限らず EC2 等でも機能更新されていて、基本的には増やしてから減らすことでリソース不足となる可能性の瞬間をなくすことが推奨されると思われます。
もちろん費用的には若干増加しますが、その増加額とリスクを天秤にかけた時、インフラ業務としてどちらが優位かを迷うことは少ないでしょうが、一応これも選択の1つということで。
ヘルスチェック
これは選択というより調整ですが、ヘルスチェックはタスク定義内のコンテナ毎のモノと、ALB TargetGroup からタスクに対しての、2箇所に存在します。どちらも、何秒間隔で何回連続で成功したら OK、失敗したら NG とするので、二重構成をそれぞれ最長で計算すると、起動完了からリソース群に参加するまでが案外長くなったりします。
条件が厳しいとデプロイが長くなったり、負荷対策が遅くなったりするので、条件をちょうどよく緩めるわけですが、あまり短すぎると今度はトラフィック経路の瞬断や高負荷時のレスポンス遅延で NG 判定になりやすくなってしまいます。
具体的な数値は示しませんが、大事なのはヘルスチェックの条件を正しく認識することと、1度はそれについて考えて最適化しようとすることです。
名前解決
ECS Service 間で接続したい時── 例えば、開発用途で DB を RDS でなく Service のコンテナにしたり、ログの Agent を分けて Appサーバーから送信したり、です。そういう時は Service に FQDN を持たせて名前解決できるようにするのですが、方法が複数あるので違いを理解して選択する必要があります。
- サービス検出 – Amazon Elastic Container Service
- Service Connect – Amazon Elastic Container Service
- AWS App Mesh とAmazon ECS の開始方法 – AWS App Mesh
- 新しい “Service Connect” 機能と「サービス検出」の相違点 – サーバーワークスエンジニアブログ
正解は特になくて、何を目的とするか(何ができればよいか)を整理して、最小限で実現できるモノを選べばいいと思います。
Network:Public or Private NAT G/W or PrivateLink
コンテナはイメージのダウンロードを多く必要とするので、その転送費用が無駄に高くならないかは考える必要があります。特に FARGATE は ECR から毎回必ずダウンロードするので、考察を避けては通れません。Autoscaling で日々コンテナを増減させると、増加のたびにダウンロードが走りますし、タスク当たりの割当リソース量が小さいほど必要タスク数が多くなるので、vCPU/Memory の調整も関わってきます。
ザックリと Public は Input 無料+PublicIPv4 有料。ECR は IPv6 で通信できないので Private は NAT G/W 必須となりその存在と通信料。PrivateLink は通信料が安くも、ちゃんと構成すると存在自体がそれなりに高くなる、という感じです。
それぞれ計算してみると、主に以下の3つが変数として関わってくることがわかり、環境によって最適な選択は変わってくると思われます。
- 1タスクあたりのダウンロード容量
- 1日あたりのタスク起動回数
- 1日あたりの平均タスク数
それでもあえて言うならば、リソース量を大きくしてタスク数を抑えた Public での PublicIP 持ち、が最も扱いやすく、異常に高くなりすぎることもなく、安定するのではと考えています。
vCPU と Memory
リソース量の調整は2箇所で考えることがあり、1つはタスク単位、もう1つは on EC2 の場合のインスタンス単位です。↑ でも書いた通り、同じトラフィックを捌くにしても、1タスク当たりを小さくするとタスク数が多くなり、イメージのダウンロード回数が増えますし、なにより管理数が多いのはよくないので、1タスクあたり 8~16vCPU (8192~16384) を基準に設計したら良いです。FARGATE の場合は、1vCPU あたりメモリ 2GB の比率が最低基準となり、メモリだけ増やすことができるのは良いメリットです。
ただそれにはアプリケーション・サーバーが Multi Process (Worker) や Multi Thread で動作できる必要があるので、Single でしか動けないミドルウェアの場合は正直、選定し直した方がいいくらいには不利に働くと思った方がよいです。
あとは 1vCPU 未満にした時にどうなるか、をちゃんと理解しておく必要はあります。0.25vCPU だと、本来1秒で終わる処理が4秒になるので、アクセス頻度が低い開発環境には遅延影響少なくコスト節約で良い選択ですが、本番では絶対にやっちゃいけないやつです。
もう1つはインスタンスへの詰め込み具合の話です。on EC2 の場合はインスタンス・タイプが持つ vCPU / Memory 容量に対して、親OSの予約領域を差し引き、タスクに割り当てていきます。当然、不足するとそこにはタスクは起動できないので、リソース数値の設計次第では、大きな空洞リソースができる可能性があります。
インスタンスが 64 GB だからと、シンプルに 16 GB のタスクを4つ起動できるわけではないので、タスクを増加させていった時に、どのように詰め込まれ、どのようにインスタンスが増加し、どのくらい空洞があるのか、などを丁寧に見ておくとよいです。
- Amazon ECS の CPU 割り当てについて理解する | AWS re:Post
- 詳解: Amazon ECS による CPU とメモリのリソース管理 | Amazon Web Services ブログ
ECR Private or ECR Public or DockerHub
コンテナ・イメージは必ずコンテナ・レジストリに保存する必要があります。元イメージに手を加えるビルドをし、ECR Private に保存する場合もあれば、完成品のまま使うモノもあります。どちらにせよ守りたいのは、AWS圏内で構成を済ませるべき、ということです。
DockerHub を筆頭に外部レジストリを指定している場合、その接続障害を原因としてコンテナを起動できなくなってしまう可能性があるからです。せっかくイメージをビルドして、即ミドルウェアを起動するだけに構成しても、イメージ取得が外部要因で障害になってしまっては元も子もありません。
タスク定義で指定するイメージは必ず ECR とし、ECR に障害が起きたらおそらく AWS の大規模障害となるので天災として諦める、くらいの感じにしておきましょう。
イメージタグ:latest or ID
ECRにイメージを保存する際のタグに、よく latest が使われる例があります。そして、latest は使うべきではないというコンテナお作法みたいのも存在します。私としてはこの境界線はハッキリしていて、更新頻度が低いミドルウェアとしてのイメージは latest 系だけでもよく、更新頻度が高くロールバックも重要となるアプリケーションは、latest 系と共に Build ID や Commit ID などをつけたら良いと思っています。そうすれば、IDで判別できつつ、最新もわかりやすくなるからです。
Task / Container
ここは Docker 寄りの話になります。コンテナの OS
インスタンスだと組織文化や要件で選択してきた OS ですが、コンテナは基本的には元となる既存イメージを利用して開発していくことになります。まずは必要とするミドルウェアやプログラミング言語を、ECR Public Gallery で検索してみるところから始めるとよいです。多くのイメージは Debian 12 (bookworm) を採用しているため、統一するならそれが最も現実的選択ですし、Debian は昔から安定していることや、yum が apt に変わっても大した問題ではなくすぐ慣れます。
昔は容量が少ない方が正義という意味で alpine がよく出る単語な時期もありましたが、私は全然オススメしません。おそらく使いづらさなど、色々失うものの方が大きいです。
あとは CPUアーキテクチャの選択で、1つのイメージで専用だったり複数に対応していたりします。できれば複数対応のモノが使いやすいですが、標準的なイメージはたいてい X86 と ARM64 に対応してくれています。
ミドルウェアとコンテナの構成
コンテナは ENTRYPOINT や CMD があれば、それを起動時に実行し、終了すればコンテナも終了。フォアグラウンドで永続化すればコンテナも起動し続けます。極端な例だと、tail -f /dev/null しておけば、そのコンテナは永続化するので、その前にバックグラウンドでミドルウェアなどを動かしておけば、複数のミドルウェアを動かすことも可能で、要はなんでもできるわけです。1タスク1コンテナでもいいし、1タスクに複数コンテナで分けることも可能です。
それゆえに例えば Nginx と Rails の puma で動かす時、どういう構成にするかを悩むことになります。コンテナを分けるとイメージも分かれるし、OS が分かれるほど若干とはいえ OS 分のオーバーヘッドが発生するとも言えるし、socket ファイルを共有するために volume 設定が必要にもなります。
イメージのビルド時に1つにまとまるよう構築も可能で、分けるほうが面倒に思えそうですが、その辺は感覚で決めるよりはルールとして決めてしまう方がよくて、
ミドルウェア : コンテナ : イメージ : Gitリポジトリ = 1 : 1 : 1 : 1
を貫く方が、結果的にキレイにわかりやすく運用できると感じています。この場合は、1タスクに Nginx, Puma, fluentd の3コンテナを起動する、ような感じです。
自由度による好みが入りがちな部分がゆえに、逆に最も厳格な構成を選ぶ方が、コードのシンプルさと、開発者の意思統一がしやすくて良いのではなかろうか、という思いです。
ENTRYPOINT と CMD
こららの使い方も最初は正解がわかりづらいので、まずはちゃんと理解しておきつつ、- [docker] CMD とENTRYPOINT の違いを試してみた #Docker – Qiita
- ENTRYPOINTは「必ず実行」、CMDは「(デフォルトの)引数」 ‣ Pocketstudio.Net
- DockerfileのCMDとENTRYPOINTを改めて解説する #Docker – Qiita
色んな公開イメージの Dockerfile を見てみると良いです。多くのミドルウェアは、ENTRYPOINT に entrypoint.sh に似たスクリプトを指定し、その最後に exec “$@” を書くことで CMD を引数としているので、自分で記述するときもそれに習うのがよいでしょう。モノによっては、/etc/default/*** にファイルを置くことで実行するように仕込まれていたりもします。
自前のアプリケーション・イメージで、ミドルウェア実行前に色々処理をしたければ、こんな感じの ENTRYPOINT 内容にすることで、entrypoint.d/* のファイルを実行しておくと便利です。メタデータの取得や情報整理などを気軽に分けて追加していくことができます。
1 2 3 4 5 6 7 8 9 |
#!/bin/bash SCRIPT_DIR=$(cd $(dirname $0) && pwd) for f in $(find ${SCRIPT_DIR}/entrypoint.d/ -follow -type f -print | sort -V); do [ ! -r $f ] && continue . $f done exec "$@" |
環境変数の扱い
コンテナの挙動は環境変数の値で決まるように構成する、という基本があります。全く同じイメージを使っても、値を変えるだけで手元の Docker Compose 開発環境で動かしたり、AWS 上の本番環境とするように切り替えられるようにします。わかりやすいところだと、DB接続先などの変数となりうる項目をハードコーディングせず、環境変数として取り出していく感じです。通常はタスク定義の environment に環境変数を設定しますが、パスワード系は Git 等に残すのは良くないので、secrets に書いて SSMパラメータから読み込むようにします。
コンテナ内では、その環境変数を使って設定を書き換えるわけですが、モノによっては設定ファイル内に環境変数をテンプレート的に埋め込めたり、できなかったりします。
Rails で言えば config/database.yml に埋め込むことは可能ですが、もしそれが不可能なら config/databases/production.yml , config/databsaes/staging.yml などに分けて、接続先はハードコーディングして RAILS_ENV で対象ファイルをコピー上書きする、みたいにするかもしれません。
この辺から先をどうコーディングするかは開発者次第になってくるところで、環境変数だけで表現することを当然とするかもしれないし、埋め込み不可や環境変数が多くなりすぎるなら事前に用意した環境別ファイルを読み込む方法も併用するかもしれません。
大切なのは頑なに理想を追求することではなく、わかりやすさ・運用しやすさだと思いますので、現場に応じてある程度は柔軟に対応する構えがよいかと思われます。
静的環境変数 と 動的環境変数
これらはココでの造語ですが、DB や KVS の接続先は環境ごとに固定値となるので静的と表現しました。動的は何かというと、環境変数として埋め込むけども自動的に決定した方が楽なものを、そう表現してみました。例としては vCPU と メモリ容量です。アプリケーション・サーバーの Worker / Thread 数は、vCPU を基準に 1.5~2.0倍 などに設定しつつ、1 Worker あたりのメモリ容量次第で調整したりします。
Worker数をタスク定義の環境変数で静的に指定すると、CPU / Memory の値の変更と共に編集することになりますが、ENTRYPOINT の中で自動的に取得と計算をして export すれば、リソース量の調整を気兼ねなく行うことができます。
1 2 3 4 5 6 7 8 9 |
#!/bin/bash INIT_ENV=$(strings /proc/1/environ) ECS_METADATA_TASK_URI="${ECS_CONTAINER_METADATA_URI_V4}/task" METADATA_TASK=$(curl -s $ECS_METADATA_TASK_URI) export TASK_CPU=$(echo "$METADATA_TASK" | jq .Limits.CPU) export TASK_MEMORY=$(echo "$METADATA_TASK" | jq .Limits.Memory) export PUMA_WORKERS=$(awk "BEGIN { W=int($TASK_CPU * 2); if(W < 1) { W=1 }; print W }") |
コンテナにおける /proc/cpuinfo やコンテナメタデータの情報は、1vCPU = 1024 とは全く一致しないので、タスクメタデータから取得して使います。他にも自動設定したら楽になる箇所があるかもなので、メタデータは一通り確認しておいた方がよいでしょう。
ログ/監視
どんなシステムでもログと監視はトップクラスに重要なので、サボらず使いやすく整理していきましょう。CloudWatchLogs or Firelens(fluentd)
コンテナのログは常時ストリームで外に送って保存しましょう、というのが基本です。障害でダウンしてもログが残りますし、ストレージ容量を気にする必要をなくせます。ログの送信は主に2種類あって、CloudWatchLogs に直接送るのと、Firelens で fluentd に送ってその機能で仕分ける方法です。
- awslogs ログドライバーを使用する – Amazon Elastic Container Service
- カスタムログルーティング – Amazon ECS
- 詳解 FireLens – Amazon ECS タスクで高度なログルーティングを実現する機能を深く知る | Amazon Web Services ブログ
- AWS ECS on Fargate + FireLens で大きなログが扱いやすくなった話 | BLOG – DeNA Engineering
これも正解があるわけではない話ですが、大切なのは使いやすいログの扱いにすることと、不要なログを保存しないことです。見づらいログはほぼゴミですし、保存費用も馬鹿にならないものです。
どのようなログの種類があり、環境ごとの要不要・容量・使用頻度・内容の重複、などを整理した上で、Logs or S3 or 捨てる を最適化していきます。
特にやっちゃいけないのは、楽だからとなんでもかんでも Logs に保存することです。Logs は保存容量自体は高くないですが、送信容量が結構高かったりするので、費用と活用が見合わないことになったりします。
アプリケーション・エラーログ
アプリケーションが吐くエラーログは重要度が高いので、生ログをゴチャッと保存するのはよくありません。Sentry や Errbit のようなシステムを活用することで、内容や発生頻度の把握、発覚しやすくするために、何がサービスに適しているかを調べて選択します。
Container Insight
メトリクスは Cluster, Service, Task 単位で採取されますが、おそらく普通に利用するのは Service 単位になるでしょう。通常の CloudWatch メトリクスと、より詳細な Container Insights メトリクスがあり、まずは内容の把握をしておきます。
- Amazon ECS CloudWatch メトリクス – Amazon Elastic Container Service
- Amazon ECS Container Insights メトリクス – Amazon CloudWatch
通常のみだと、CPU と Memory しか無いので、少なくとも本番環境のクラスタでは Container Insights を有効にして、各カテゴリごとにまとめて見やすいグラフにしておいた方がよいです。
Container Insights は微妙に費用が高いので、ステージングや開発環境などでは無効にするなど、不要箇所で費用が膨れないように判断していきましょう。
DataDog や NewRelic
パフォーマンス監視ツールな SaaS は何かしら導入すると思われますが、どのくらいデータ採取するかを2つの観点から判断していきます。1つは費用です。サービスによって形態は異なるものの、基本はホスト数に依存するので、もし全ホストで有効にする必要がなく、サンプリング程度の数で済むのであれば、一部のホストでのみログエージェントを有効にすることで費用削減ができます。
もう1つは負荷です。ログエージェントを動かすと、そのサーバーの負荷が通常と比べて 1.5~2.0倍 くらいに上がることがあります。負荷分散のリソース群に均等に割り振られた場合、重いサーバーでエラーが返ったりレスポンスが遅くなる恐れがあります。
これらを解決する方法は人それぞれでしょうが、両方を解決する1つのヒントとして、デフォルトで全ホストのログ送信を無効にしておいて、Lambda などの外部から任意の台数分だけ有効にすることが可能です。また有効にする処理をタスクあたり1回しか実行しなければ、複数 worker のうちの1つしか有効にならないので、16 worker で動いていれば 1/16 分しか負荷が上がらなくなります。
仕組みづくりと調整が大変ですが、サービスの最適化に必要な最小限のデータとコストで実装できるのが理想なので、現状から理想に向かって良い落とし所を考えてみるとよいでしょう。
Code系デプロイ
最後に、自動デプロイで CodePipeline を通っていきます。Source CodeCommit のトリガー
Pipeline 通知:CodeStar or EventBridge
どちらも EventRule を使ってイィ感じにしましょう、というのを最近まとめて書きましたので、ご参考まで。CodeBuild:EC2 or Lambda
CodeBuild の実行を Lambda にすると、docker build などはできなくなる代わりに、初期化を 50秒 短縮できます。ちょっとした軽量言語スクリプトとか API 実行など簡素な処理の場合は Lambda にした方がよいです。Terraform では aws 5.32.0 で使えるようになるっぽい流れをぶら下げてあります。
CodeBuild の Lambda化、Build さえ通るなら速い安いARM有るになるから、Terraform が対応したら即試すかな https://t.co/vsq6Rqglvd
— 外道父 | Noko (@GedowFather) November 11, 2023
承認フェーズ:要不要
Pipeline のビルドが終わったあたりで、すぐデプロイに行くか、手動承認フェーズを挟んでから行くか、です。これはアプリのツクリによるところで、スピード重視にできるのか、それとも手動にすることにメリットがあるか、またはそうしないとダメなのかで選択することになります。
ビルド用ファイルの置き場所
Dockerfile や buildspec.yml , appspec.yml など、デフォルト名があるビルド用ファイルは Git リポジトリのトップに置くのが楽ですが、多くなるとわかりづらくなる場合があります。好みの問題ではありますが、ミドルウェアのビルド程度ならトップに置いて、アプリケーションコードのリポジトリは1つディレクトリを作って、その下にまとめるくらいが程よいと感じています。唯一、compose.yaml だけはトップに置きたいかな、というのはあります。
多少パスが変わっても、buildspec.yml の指定を変えつつ、その中の処理でなんとでもできるので、ルールだけ最初に決めてやりくりしたらいいと思います。
おわりに
細かい工夫とか悩みを含めてたらキリがないのでこの辺にしておきます。時期によって正着が変わるものや、ドキュメントや記事を読み込んでから判断するもの、サービスの運用事情を汲んで仕上げるもの、など様々あることがわかると思います。コンテナでシステムを動かすぞと決めて、ストレートに組んで稼働させるだけなら、そこまで苦労するわけではない感じはあります。しかしコスパや運用しやすさまで最適化しようとすると、これらに関わる知識を整理し、経験を含めて最適を判断し、周りも含めて納得するよう組み上げていく実行力、すなわち技術力が求められる、そんなお話でした。
(参考:エンジニアの勉強と技術力と育児 | 外道父の匠)
言ってしまえばコンテナとか関係なく、システムを理解しようとして適切に組んで扱えるのかって部分が大半です。ただコンテナ関連は最適化のために考えることが多く、上手に仕上げるほどに、それに応えるように品質の高いシステムになってくれる感覚があるので、やりがいと安心を感じれて面白いです。
こういったことを進めるにあたって大切なのは「使いたい」「こうしたい」という欲求ベースではなく、「こうしたら効率的」「こうあるべき」という観点だと思っています。
欲望がパワーになることもありますが、ちょくちょく一歩離れて第三者視点で見て、その判断の姿勢やバランスが崩れていないか、を感じながら進行すると、より幸せなシステムが出来上がるのではないでしょうか:-)