AWSにTerraformを使って実践的なWebアプリ環境をリリースする
はじめに
2023年も終盤ということで、AWSとTerraformで曖昧な部分の補完と、振り返りを目的として ECS Fargate を使用したWebアプリケーションの実行環境を作成していきたいと思います。
また、こちらも同時に投稿させていただきました。
合わせてご覧いただけますと幸いです🙇♂
自己紹介(Context)
私はWeb基盤を提供している企業でWebアプリケーションエンジニアをしています。
社内モダナイゼーションの一環で、自社インフラの実行環境からAWS環境にクラウドリフトしました。そのときの経験をもとに作成しています。
Source
Terraformのソースコードです。
READMEを実行することで同じ環境を構築できるはずです。(2023年12月6日 現在)
NGINXのこの画面が表示されると成功です。
※ ドメインは自分で用意する必要があります
クラウドインフラ構成
ドメインはAWS以外で作成することも多いと思うので、Route53のパブリックホストゾーンは事前に作成しておくことにします。
それ以外は全てTerraformで管理します。以下がポイントになります。
- AレコードのエイリアスにALBを指定し、パブリックからのリクエストを受けます。
- ALBをSSLの終端にし、それ以降の処理はHTTPSで ECS Fargate に流します。
- サイドカーにFireLensを配置し、アプリケーション(NGINX)の標準出力をCloudWatchLogsに集約します。
- プライベートサブネットからのインターネットアクセスは NAT Gateway を媒介します。
ディレクトリ構成
Standard Module Structure に則っり main.tf
variables.tf
outputs.tf
をそれぞれのモジュールに含めることにします。
$ tree src/modules/ssl
src/modules/ssl
├── main.tf
├── outputs.tf
└── variables.tf
src/envs
配下に各環境のエントリーポイントを配置します。
$ tree src/envs -L 1
src/envs
├── prod
└── staging
src
配下のアウトラインは以下のようになります。
$ tree src -d
src
├── envs
│ ├── prod
│ └── staging
└── modules
├── dns
├── ecs
├── loadbalancer
├── network
└── ssl
src/modules
各モジュールを関心ごとで分け、それぞれが疎結合となるように作っています。
ここでいう疎結合とは、各モジュールを跨いでのリソースの参照は行わず variables.tf
に定義しているパラメーターで値を受けることを言います。
これにより、各モジュールのポータビリティが向上するのと、そのモジュール単体での使用が可能になります。
また、モジュールごとに低コストでの生成と廃棄が可能(Low-cost disposability)であることはテストの容易性が上がり、現代の開発手法にマッチしていると言えそうです。
src/modules/network
$ tree src/modules/network
src/modules/network
├── data.tf
├── internet_gateway.tf
├── nat_gateway.tf
├── outputs.tf
├── private_subnets.tf
├── publict_subnets.tf
├── variables.tf
└── vpc.tf
vpc.tf
まずはVPCを作ります。#tfsec:ignore:aws-ec2-require-vpc-flow-logs-for-all-vpcs
は、VPCフローログを作成しないと、tfsecに注意されるのでignoreを明示しています。
internet_gateway.tf
インターネットゲートウェイ。インターネットへのエントランスです。
publict_subnets.tf
パブリックサブネットとは、インターネットゲートウェイへのルートが定義されているサブネットのことです。
nat_gateway.tf
NATゲートウェイはパブリックサブネットに配置します。
private_subnets.tf
プライベートサブネットからインターネットアクセスする場合はNATゲートウェイを媒介するのが定石です。
src/modules/dns
$ tree src/modules/dns
src/modules/dns
├── main.tf
├── outputs.tf
└── variables.tf
dns/main.tf
-
aws_route53_zone
を使い、事前に作成していたパブリックホストゾーンからzone_idを取得します。 - Aレコードのエイリアスにロードバランサーを指定します。
src/modules/ssl
$ tree src/modules/ssl
src/modules/ssl
├── main.tf
├── outputs.tf
└── variables.tf
ssl/main.tf
-
aws_route53_zone
を使い、事前に作成していたパブリックホストゾーンからzone_idを取得します。 - AWS Certificate Manager で指定された検証用CNAMEレコードを登録し、SSLを有効化します。
src/modules/loadbalancer
$ tree src/modules/loadbalancer
src/modules/loadbalancer
├── main.tf
├── outputs.tf
├── security_group.tf
└── variables.tf
loadbalancer/main.tf
ロードバランサーです。
- パブリック向けLB(internal=false)の場合、tfsecに注意されるので
#tfsec:ignore:aws-elb-alb-not-public
を明示します。 - 80番ポートに届いたリクエストは443番ポートにリダイレクトさせます。
- 先程 AWS Certificate Manager で作成したSSLのarnを設定します。
security_group.tf
パブリック向けのロードバランサーなのでingress/egress
にcidr_blocks=["0.0.0.0/0"]
を指定しています。このように設定するとtfsecに注意されますが、これをグローバルの設定で許可しています。
.tfsec/config.yml
src/modules/ecs
$ tree src/modules/ecs
src/modules/ecs
├── autoscaling.tf
├── iam.tf
├── main.tf
├── outputs.tf
├── security_group.tf
└── variables.tf
ecs/main.tf
- ECSクラスター
- コンテナインサイトによるモニタリングを有効にします。
- ECSタスク定義
- Fargateを指定。
- 80番ポートでリクエストを受けます。
- nginxは Docker Hub から取得します。
- AWS for Fluent Bit はAmazon ECR 公開ギャラリーから取得します。
- ログドライバーにFireLensを指定し、標準出力(ログ)を Fluent Bit を通してCloudWatchLogsに送信します。
- それぞれのコンテナにヘルスチェックの設定を入れます。
- ECSサービス
- Fargateを指定。
- 維持するタスク数は「2」にします。
- ロードバランサーのターゲットグループと紐づけます。
-
propagate_tags="SERVICE"
を指定して、起動したタスクへのタグの伝搬設定をします。
autoscaling.tf
CPU使用率75%を基準に最大「5」最小「2」でオートスケールするように設定します。
iam.tf
タスクロールとタスク実行ロールで使用する共通でIAMロールを定義しています。機密情報を取り扱うようになったら分けたいところ。
see: 機密情報はECSのvalueFromで環境変数に展開する
security_group.tf
ECSで使用するセキュリティグループです。
src/envs/
$ tree src/envs -a
src/envs
├── prod
│ └── .gitkeep
└── staging
├── README.md
├── backend.tf
├── main.tf
├── outputs.tf
├── provider.tf
└── variables.tf
CICDを組むときは各環境に対応したディレクトリのエントリーポイントを指定します。
横断的設定
src/provider.tf
default_tags
を設定することで、全てのリソースに共通のタグを設定しています。コスト配分タグなどはここで設定するのが良いでしょう。
helpers/backend
共同開発の場合、ステートファイルもクラウド上(S3)で管理するべきでしょう。複数人で同時実行によるコンフリクトを懸念する場合はDynamoDBを使いロックをすることもできますが、今回は作っていません。
scripts/pre-commit.sh
sh scripts/pre-commit.sh
により、フォーマット、脆弱性チェック、ドキュメント生成などを行います。GitHub Action やGitのプレコミットのフックなどに組み込むべきだと思いますが、今回は作っていません。
まとめ
実際には各種データベースのとの接続やマイグレーション、パラメーターストア、BlueGreenデプロイメント、OpenTelemetryによるX-Rayとの連携や、DXやVPNによるオンプレミスとの通信など、実務レベルではまだまだできることはありそうです。(私の実務環境を比較対象にしています)
今回はAWSで扱うWebアプリケーションの基本的な構成について記述しました。ご指導ご鞭撻などコメントでいただけますと幸いです🙇♂
-- おまけ --
.gitignore
.gitignoreの指定はGitHubのignoreテンプレートを使うと良さそうです。Terraform生成物の中には機密情報が含まれているものもあります。
validate
構文やモジュールへのパラメーターの妥当性などをチェックしてくれます。以降で紹介しているtfsecやtflintと組み合わせて使うと良さそうです。
terraform validate
fmt
フォーマット機能です。Goもですが、その言語自体が公式のフォーマットを提供してくれるのはとても好感が持てます。
サブディレクトリも含める場合は以下のようにrecuasiveを指定します。
terraform fmt --recuasive
ツール
ツールを使用することで開発をベストプラクティスに近づけます。
tfsec(⭐⭐⭐)
tfsecはTerraformのコードにセキュリティ上の脆弱性がないかを確認してくれる解析ツールです。
脆弱性が存在した場合は、それの危険度をCritical, High, Middle, Low に分類して教えてくれます。
基本事項、導入方法はこちらの記事がとても参考になります。
.tfsec/config.ymlを作成する
tfsecコマンド実行時、特に指定がない場合はこの設定が適用されます。
ルールを無視する方法
tfsec:ignore:xxxx
を指定します。
tfenv(⭐)
tfenvは手元のTerraformのバージョンをCLIで切り替えることができるツールです。
.terraform-version
このファイルをワークディレクトリに配置しておくと、リポジトリ単位で使用するTerraformのバージョンをチーム内で揃えることができます。
こちらのツールを使用しているチームもありました。
Terraform Docs(⭐)
Terraformのソースコードからモジュール単位で、ドキュメントを生成してくれるツールです。
出力形式はいくつか選べるのですが、Markdownで出力したものはこちらです。
TFLint(検証中)
非推奨の構文や未使用の宣言についての警告などを出してくれる解析ツールです。
(tfsecはセキュリティに特化しているので切り口は別です。)
AWS/Azure/GCPなど、それぞれのプロバイダーに特化したプラグインをインストールすることで、そのプロバイダー固有の問題も検出してくれます。
deep_checkを有効にすると、APIを実行してより厳密に検証を行ってくれます。
こちらの記事が一番良くまとまっていた印象です。
※このツールはTerraformを内部で別に持っているので、開発で使用しているTerraformとTFLintで使用されているTerraformとで、互換性があるバージョンを選択する必要があります。
Discussion