優先度が高い仕事が落ち着いてきたため、ずっとやりたかった本番環境コンテナ化に着手しました。
たぶんお休みで全部忘れるので社内に説明用も兼ねてメモ。
下記の記事のように現在のシニアジョブに入社してすぐ、開発環境はDocker化していました。
シニアジョブに入社して1ヶ月でやったこととこれから - GAミント至上主義
現在、本番環境はElastic Beanstalkで動いていますが、Dockerfileで管理できるため、いつかは揃えてコンテナ化したいなぁと思っていました。
AWS Fargateとは
AWS ECSからEC2の管理を(でき)なくしたサーバーレス的なもの・・・という雑な認識。詳細は公式。
AWS Fargate(サーバーやクラスターの管理が不要なコンテナの使用)| AWS
専用ページはあるものの、コンソールの中だと、ECSやEKSの起動タイプとして存在してるだけで意外と地味な存在でした。
なぜFargateを使うか
コンテナ化に際しては、タイトルのFargateの他、下記の選択肢がありました。
- AWS ECS (Elastic Container Service) の起動タイプにEC2
- AWS EKS (Elastic Kubernetes Service)
- GCP GKE (Google Kubernetes Engine)
下記がFargateにした理由です。
AWSのいろいろなサービスを使っていたから(GCPを使わない理由)
シニアジョブでは、現在のシステムが動いた当初(2016年ごろ?)からAWS上で求人サイトや管理システムも動かしていました。
そのためRDS(MySQL)をはじめ、Elasticsearch、DynamoDB、SQSなども使用しています。
そのため、個人的にはGCPのGKEは使い慣れていたので使いたい気持ちはあるものの、わざわざGCPに移すという選択肢はありませんでした。
そんなに必要なコンテナ数が多くない(Kubernetesを使わない理由)
これまで触ったことはないものの、ネット上の記事とかの印象だと、AWSでそこまで大規模じゃないシステムだったらFargateかなぁというイメージでした。
シニアジョブのシステムで必要になりそうなコンテナは下記3つだけで、ほかはRDS, RedisなどはAWSのマネージドで済みます。
- アプリケーション (Laravel)
- ワーカー (php artisan queue:workerするやつ) (Laravel)
- WEBサーバー(Nginx)
これだけだとKubernetesの機能はちょっとオーバーかなぁという印象。あとAWSでKubernetesはGKEと比較すると高くつくというのもあります。
実際構築してみた感じ、ハードウェアリソースも柔軟で、Fargateで十分でした。
管理が楽そうだから(EC2タイプを使わない理由)
EC2タイプを使ったことはないので比較はできないのですが、EC2を意識せずに使えるというのは、エンジニア3人で何とかしている現状では、メンテナンスコストを削減できそうなので、大きな価値がありました。
サーバーレスとあるので、GCPでいうCloud Runみたいな感じかなぁと思ってたら、どちらかというとKubernetesで最近出たAutopilot的なものでした。
Fargate化するために必要な登場人物
GCPではいろいろ構築したことがあるもののAWSで複数サーバーのシステム構築は初めてでした。
そのためFargateを使用するにあたり、AWSの他のサービス、機能を知る必要がありました。
ECR (Elastic Container Registry)
まずはDockerで作ったコンテナをAWSに置いておく必要があります。その置き場所がECR。
Amazon ECR(Docker イメージの保存と取得)| AWS
最初1回だけ手作業でローカルからビルド&プッシュしましたが、面倒なのでCircle CIで自動化しました(後述)
ECS (Elastic Container Service)
これが今回使うメインのサービスになります。
Amazon ECS(Docker コンテナを実行および管理)| AWS
ECSの中に下記があります
ECS クラスター
ECSの一番外側、EC2をひとまとめにしたもの。Kubernetesのクラスターと同じ感じですね。
今回はFargateを使うので、ネットワーキングのみを使用します。
ECS サービス
後述するタスクを管理するもので、タスク定義や、起動数を指定し、その状態に変更、維持してくれます。
1サービスに対し、1タスク定義っぽい。
また後述するロードバランサーもサービスと紐づけることになります。
Amazon ECS サービス - Amazon ECS
作成後に変更できない設定が多々あり、5回ぐらいは作り直した記憶があります。
タスク定義
タスクの設計書のようなもので、ここに使用するコンテナ(複数可)や、コンテナごとの環境変数などの設定、必要なリソース(CPU、メモリ等)を指定します。
こちらも作成時に起動タイプにFargateを指定します(もう一つはEC2)。
更新の際は、上書きではなく、リビジョンを増やしていく形になります。
Amazon ECS タスク定義 - Amazon Elastic Container Service
コンテナ定義
具体的なコンテナ定義はタスク定義内にあります。開けるポートの指定もここ。
同じタスク定義内であれば、127.0.0.1で通信できるようなので、nginxからappへのプロキシ設定では127.0.0.1を使用しました。
KubernetesだとServiceを作る必要があったり面倒なので、今回はこれで十分だし、シンプルに感じます。
※あとでqueueは別サービスに分けました
サービスでのロードバランサー設定は、ここで設定したnginxコンテナのポート80を指定しました。
タスク
上記のタスクをサービスが起動したもの。プログラミングだとタスク定義がクラス、タスクがインスタンス、みたいなイメージ?
変更の際はタスク定義のリビジョンを指定して更新します。
スケールさせるのもこのタスク単位
頻繁に消したり、作ったりするものなので、タスクIDは
32034c0a56d84d5eb95908090cdec44c
みたいなランダムな16進文字列になります(後述のシェルで入るときに使う)。
サービスを更新すると新しいのができたり古いのが消えたりします。
ECS以外
ECSと関係するのが以下のサービス&機能になります。
VPC (Virtual Private Cloud)
Amazon VPC(仮想ネットワーク内での AWS リソースの起動)| AWS
クラスター使用時に選択、または作成します。仮想のネットワークで、セキュリティ設定などに影響します。
新規のときは一緒に作ればいいですが、既存のシステムがあるときは同じものを使用しないと、いろいろ繋げるのが面倒になるので、注意が必要でした。
Route 53
Amazon Route 53(スケーラブルなドメインネームシステム (DNS))| AWS
言わずとしれたDNSサービス。
上記ロードバランサーでは
foobar-4580172169.ap-northeast-1.elb.amazonaws.com
のような長めのURLが振られるため、独自ドメインを使用する際はRoute53で紐付けます。
Aレコードから選択形式で簡単に指定できます。
ターゲットグループ
ロードバランサーから今回のECSのサービスに繋げる転送先(実体はタスクのローカルIP)に使用されます。
ロードバランサーの新規作成時や、ECSでサービスを作る際に自動でできてしまうので、サービスを作り直したりすると、余計なものが残っていて邪魔になり消すのが面倒でした。
ECSのサービスは、タスクが更新され、ヘルスチェックが通ると、このターゲットグループのIPを自動で変更することで、ユーザー側へも変更されます。
セキュリティグループ
ECSのサービスが持ちます。
IPは変わってしまうので、これを使って、VPC内の通信許可などを行います。
現環境では既存のセキュリティグループに、サービスからRDSやRedisへの許可を追加しました。
Secrets Manager
コンテナで使うパスワード等の機密情報を保持します。
とりあえずLaravelで.envファイルで使用していた項目を移しました。
AWS Secrets Manager(シークレットのローテーション、管理、取得)| AWS
タスク定義で、コンテナごとに設定し、環境変数として使うことができます。
JSON形式のオブジェクト形式で複数持つことができますが、自動で環境変数に展開することはできないようなので
valueFromで一つずつ
arn:aws:secretsmanager:ap-northeast-1:user-id:secret:secret-name-AAAAAA:YOURENV::
のように指定して読み込む必要がありました。
このチュートリアルのようにIAMロールの追加も必要です。
チュートリアル: Secrets Manager シークレットを使用した機密データの指定 - Amazon Elastic Container Service
その他
動かすためにいろんな設定を理解、修正する必要があり、独自ドメインからコンテナのWEBアプリケーションまで繋がるのに幾度となくはまり、3日ぐらいかかりましたがなんとか動かくことができました。
Circle CIでECRにプッシュ&アップデート
コンテナ変更のたびに手作業でビルド、プッシュするのは現実的ではないのでなるべく早い段階でやった方がよさそうです。
シニアジョブではCircle CIを使っていたので、そのまま利用しました。GitHub Actionsでもやってみたいなぁ。
Circle CIのいろんな処理をまとめたOrbsを使うとめっちゃ簡単でした。
ほぼ公式まんまですが、service-nameだけ公式ドキュメントになく、デフォルト値ではうまくいかなかったので、orbのソースを見て追加しました。
またfamilyがわかりにくいですが、タスク定義をJSONで確認するとfamilyの項目が見つかります。
まだステージング環境だけなのでtagの先頭にstageを固定でつけてます。
.circleci/config.yml
version: 2.1 orbs: aws-ecr: circleci/aws-ecr@6.15.3 aws-ecs: circleci/aws-ecs@2.0.0 workflows: build_and_push_image_app: jobs: - aws-ecr/build-and-push-image: account-url: AWS_ECR_ACCOUNT_URL aws-access-key-id: AWS_ACCESS_KEY_ID aws-secret-access-key: AWS_SECRET_ACCESS_KEY dockerfile: ./Dockerfiles/app/Dockerfile path: . region: AWS_REGION repo: senior-job/app create-repo: true tag: "stage-$CIRCLE_SHA1" - aws-ecs/deploy-service-update: cluster-name: 'example' service-name: 'example-service' container-image-name-updates: 'container=app-container,tag=stage-${CIRCLE_SHA1}' family: 'example' requires: - aws-ecr/build-and-push-image - aws-ecs/deploy-service-update: cluster-name: 'example' service-name: 'example-service' container-image-name-updates: 'container=queue-container,tag=stage-${CIRCLE_SHA1}' family: 'example' requires: - aws-ecr/build-and-push-image
コンテナにシェルで入る
なぜかうまく行かない時は、中で環境変数がどうなってるかとか、ファイルちゃんとあるかとか、やっぱりコンテナの中に入って確認したいもの。
Kubernetesだと`kubectl exec -it pod_name bash`みたいにサラっと入れますが、一筋縄では行きませんでした。
特にFargateはEC2を触れないことから、難しかったみたいですが最近(2021/3)公式な手段が準備されたようです。
このプロジェクトが2ヶ月早かったら詰んでたかも。5年以上前でもexecはできてたので、全体的な完成度としてはKubernetesがやっぱ進んでるなぁと思ったできごとでした。
New – Amazon ECS Exec による AWS Fargate, Amazon EC2 上のコンテナへのアクセス | Amazon Web Services ブログ
権限付与、サービスの設定変更、CLIのアップデートなどいろいろ面倒でしたが、こちらの記事がまとまっていてわかりやすかったです。
[アップデート] 実行中のコンテナに乗り込んでコマンドを実行できる「ECS Exec」が公開されました | DevelopersIO
これから
まだステージング環境が動いただけなので、いくつかまだたりないデプロイプロセスの追加や、死活監視関係、本番でのリソース調整やオートスケール設定など、まだまだたくさんありそうです。
あとAWSだけだと自動化つらいのでTerraformも使った方がいいのかなぁとか。
2021/4/15 追記
appのサービス(php-fpmとnginx)とqueueを実行するサービス&タスクは分けた方が良いと思った