はじめに
AWSにアプリをデプロイして以下の構成でインフラを設計・構築しました。
デプロイ対象のアプリ
インフラ設計・構築がメインなので詳細は割愛します。
db-migrator
データベースマイグレーションを管理・実行するためのアプリ
slack-metrics
Slackのデータを取得してデータベースに永続化するためのアプリ
こだわりポイント
- Organizationsでマルチアカウント運用とし、prdアカウント、stgアカウントを作り、各々の環境が干渉しないようにしています
- Identity Centerでルートユーザー以外のユーザーを作り、環境ごとにロールを与えSSOを実現し、スムーズにアカウントを切り替えられるようにしています
- 1つのサービスにつき1つのセキュリティグループを用意し、自身にアクセスするサービスのセキュリティグループIDを許可することで、各サービス間の連携を明瞭化しています
- 機密情報はSecrets Manager、非機密情報はS3で管理することで、静的な管理としています
- ECRのイメージのタグはGitのコミットハッシュと対応させることでバージョン管理を楽にしつつ、かつ、イミュータブルにしてタグキャッシュによるインシデントを防ぐようにしています
- ECSサービスはプライベートサブネットに置いて、静的IPをオフにしてセキュアにしています
- ECSサービスはオートスケーリングを設定して、負荷の大小により追跡スケーリングでスケールアウト、スケールインするようにしています
- DNS委任設定によりprdアカウントからstgアカウントに権限委任し、stgアカウント側でのサブドメイン設定をセキュアにしています
- プライベートサブネットからインターネットへの通信に関しては、当初NAT Gatewayを使っていましたが、コスト的に個人学習だと割高なので、NATインスタンスとしてEC2を自前で建てる構成に置き換えました。具体的にはSession ManagerからEC2へ接続し、NAT AMIを作成しました。この結果、月7,000円から数百円以下に抑えることができました
- EC2を踏み台サーバとしてポートフォワーディングでローカルからstg環境のRDSを操作できるようにしています。その際、22番ポートを開けずに、SSH経由ではなくSession Manager経由でセキュアなポートフォワーディングを実現しています
- db-migratorとslack-metricsのアプリは、コンテナ起動時のコマンド、環境変数、Dockerイメージ、IAMロールの権限などが違うため、タスク定義を分けて見通しを良くしています
- 初期段階でアプリごとに専用のDBユーザーを作成し、ユーザー名、パスワードを更新しセキュアにしています
- ECSサービスでスケールアウトクールダウン期間、スケールインクールダウン期間を設けて、不必要なスケーリングを防ぎ、システムの安定性を保つようにしています
- 継続運用するslack-metricsについて、stg環境はFargate Spotでコスト削減をしています。この結果、月2,000円から数百円以下に抑えることができました
- バッチ処理はEventBridge Schedulerから単発実行のECSタスクを立ち上げて、日次でSlackのメッセージをDBに同期しています
- バッチ処理の動作確認をする際、EventBridge Schedulerからは即時実行ができないので、まずはローカルからAWS CLIで即時実行し、1回の試行時間をできるだけ短縮することで、デバッグを容易にしています
- stg環境は、EventBridge SchedulerでEC2、ECS、RDSなどのインスタンスを日次で自動停止するスケジュールを組み、コスト削減を図っています。なお、RDSは1週間停止が続くと自動で起動するため、週に1回は自動停止15分前に起動し、その後自動停止をすることでコストの最適化を図っています
- 手動でSlackのメッセージをDBへ同期するために非同期処理を利用しています
- 非同期処理はSQSを使い、Workerコンテナをコスト面、運用面からECSタスク定義の中にサイドカーパターンで動かしています
- 非同期処理の動作確認をする際、まずはローカルでAPIサーバとWorkerサーバを立てて、PostmanからAPIサーバへリクエスト→SQSにメッセージが入る→Workerサーバよりポーリング→SQSからメッセージが除かれる、ことを確認し、デバッグを容易にしています
- APIサーバとWorkerサーバはデプロイやシャットダウンにより、データが中途半端な状態でDBにインサートされるのを防ぐために、グレースフルシャットダウンを導入しています
- デッドレターキューにより、無駄なポーリングを失くし、トラブル発生時の調査を容易にしています
- ECSタスク定義は、必須コンテナとしてコンテナ内のプロセスが落ちている時はデプロイを失敗させることでデプロイを安全にしています
- ECSタスク定義は、読み取り専用としてコンテナ内のルートファイルの書き換えを防ぐことでセキュアにしています
- ECSタスク定義は、コンテナ間でS3から同じ環境変数を参照させつつ、個々のコンテナで必要なところのみValueに上書きすることで、コンテナ間で効率的に環境変数を設定しています
- stg環境はローカルからAWS CLIでECS Execによりコンテナの中に入り、コマンドを実行することでデバッグを容易にしています
- DBマイグレーションは専用のECSタスクを作って、DBに対してマイグレーションをかけています。それをGitHub Actionsからecspressoコマンドで実行するようにCDパイプラインも構築しました
- フロントエンドのアプリはAmplifyにデプロイし、前段に自前のCloudFrontを置いて多段CloudFront構成にすることで、キャッシュやWAFをより柔軟にコントロールできるようにしています
- AmplifyにWAFをアタッチすると各Amplifyごとに料金が発生しますが、CloudFrontにWAFをアタッチした場合はアタッチ料金が不要で、ルール単位での課金のみとなります。そのため、WAFを共通化して利用でき、コスト面でもCloudFront+Amplifyの多段構成が有用と考えています
- CloudFrontで「/static」というパスはS3のオリジンにリクエストを流すようにビヘイビアを設定することで、S3とフロントエンドのアプリを同一サブドメインにしています
- Amplifyが隠蔽しているCloudFrontでもキャッシュの最適化をしており、二重キャッシュによるデバッグは困難を極めるので、CloudFrontのキャッシュポリシーはCashingDisabledとしています
- CloudFront+Amplifyの多段構成では、前段のCloudFrontからhostヘッダーを転送すると、Amplifyに隠蔽されているCloudFrontが前段のCloudFrontに転送し、CloudFrontの無限ループが発生するため、AmplifyのオリジンリクエストポリシーはAllViewerExceptHostHeaderとしてhostヘッダーを転送しないようしています
おわりに
次回はIaCを整理する予定です。
