概要
Next.js(App Router)とRails APIで構成されるアプリケーションの実行環境を、AWSで構築する記事です。IaCにはTerraformを利用します。
構築する環境の構成図は以下に示す通りで、アプリの実行環境はECS/Fargateを利用しています。SeverComponentからRailsで実装したAPIを呼び出す際は、CloudMapによるサービスディスカバリを利用してコンテナ間で直接通信するようにしています。
※注意点
この記事では、ECS/Fargate環境におけるISRの実現方法については触れていません。今回の構成で考慮しているのはSSR(cache: no-store)のみです。
ECS/FargateにおけるISRの実現方法についてお探しの方は、以下の記事が参考になるかと思います。
前提
AWS CLIのインストールや、認証情報の設定、またterraformのインストール、初期セットアップは既に完了しているものとします。
※CLIで利用する認証アカウントのIAMロールには、適宜必要なポリシーを割り振ってください。 セキュリティの問題に関わるため推奨できませんが、「PowerUserAccess」ポリシーがあれば、この記事で記載している内容は全て実行できます。
アプリケーションのソースコードは、以下のような構成になっているものとします。
kaku-projectフォルダをルートディレクトして、kaku-backendフォルダがRails用、kaku-frontendがNext.js用のフォルダとします。
ヘルスチェック用のAPIは以下のようにRails、Next.jsそれぞれのアプリケーションに実装しています。
kaku-project
├── kaku-backend
| ├── Dockerfile.prod
| ├── app
| | └── controllers
| | └── api/v1
| | └── health_check_controller.rb
| ├── bin
| ├── config
| ├── db
| └── ...
└── kaku-frontend
├── Dockerfile.prod
├── node_modules
├── public
├── src
| └── app
| └── next-api
| └── health-check
| └── route.ts
├── package.json
└── ...
本番環境と開発環境でDockerfileを使い分けることを想定して、Dockerfile.prodという名前でDockerfileを作成しています。
また、コンテナ間の疎通確認を行う必要があるため、SCでRailsのAPIを呼び出すページを事前に用意していることを想定しています。
Terraform用のディレクトリの作成、初期セットアップ
Terraform用のディレクトリを以下のように作成して、ルートフォルダにmain.tf、variables.tfを作成します。またバージョン管理をする場合は、.gitignoreファイルも作成しておきます。
kaku-project
└── kaku-infrastucture
├── .gitignore
├── main.tf
└── variables.tf
.gitignoreの内容は以下のように定義します。
**/.terraform/*
*.tfstate
*.tfstate.*
*.tfvars
main.tfの内容を以下のように定義します。
terraformのバージョンの指定、tfstateの保存先の設定、providerの設定(リージョンの指定)を行います。
terraform {
required_version = "1.6.3"
backend "s3" {
bucket = "kaku-tfstate"
key = "kaku-infrastructure/terraform.tfstate"
region = "ap-northeast-1"
}
}
provider "aws" {
region = "ap-northeast-1"
}
上記で指定したtfstateの保存先のS3バケットを作成します。
tfstateの保存先のS3バケットは、今回定義するterraformのグループとは別で定義することが推奨されているので、CLI、もしくはコンソール等を利用して別途作成してください。
S3バケットを作成する際は、公開範囲をプライベートにして、S3バケットの名前は上記で指定したkaru-tfstateとしてください。
variables.tfの内容を以下のように、サービスのプレフィクスやタグの名前で利用する変数を定義します。
variable name_prefix {
default = "kaku"
}
variable tag_name {
default = "kaku"
}
variable tag_group {
default = "kaku"
}
kaku-infrastuctureフォルダで、terraform initを実行して、初期セットアップは完了です。
terraform init
networkモジュールの作成
VPCやサブネット、ルートテーブル、IGといったネットワーク周りのリソースを定義するモジュールを作成します。
moduleフォルダを作成して、その下にnetworkフォルダを作成します。
また、networkフォルダの下にvariables.tf、outputs.tfと、各種リソース用のファイルを作成します。
kaku-infrastucture
└── module
└── network
├── outputs.tf
├── variables.tf
├── vpc.tf
├── subnet.tf
├── route.tf
└── internet-gateway.tf
variables.tfでは、マルチAZで利用するリージョンと、VPCのCIDR、各サブネットのCIDRを定義します。
variable name_prefix {}
variable tag_name {}
variable tag_group {}
locals {
vpc_cidr = "10.0.0.0/16"
az_1 = "ap-northeast-1a"
az_2 = "ap-northeast-1c"
ingress_subnet_1_cidr = "10.0.1.0/24"
ingress_subnet_2_cidr = "10.0.2.0/24"
nodejs_subnet_1_cidr = "10.0.3.0/24"
nodejs_subnet_2_cidr = "10.0.4.0/24"
puma_subnet_1_cidr = "10.0.5.0/24"
puma_subnet_2_cidr = "10.0.6.0/24"
mysql_subnet_1_cidr = "10.0.7.0/24"
mysql_subnet_2_cidr = "10.0.8.0/24"
redis_subnet_1_cidr = "10.0.9.0/24"
redis_subnet_2_cidr = "10.0.10.0/24"
vpc_endpoint_subnet_1_cidr = "10.0.11.0/24"
vpc_endpoint_subnet_2_cidr = "10.0.12.0/24"
}
vpc.tfで、VPCのリソースを定義します。
resource "aws_vpc" "default" {
cidr_block = local.vpc_cidr
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = "${var.name_prefix}-vpc"
group = "${var.tag_group}"
}
}
subnet.tfで、各サブネットのリソースを定義します。
#ingress用のpublicサブネット
resource "aws_subnet" "public_subnet_ingress_1" {
cidr_block = local.ingress_subnet_1_cidr
vpc_id = aws_vpc.default.id
availability_zone = local.az_1
map_public_ip_on_launch = true
tags = {
Name = "${var.tag_name}-public-subnet-ingress-1"
group = "${var.tag_group}"
}
}
resource "aws_subnet" "public_subnet_ingress_2" {
cidr_block = local.ingress_subnet_2_cidr
vpc_id = aws_vpc.default.id
availability_zone = local.az_2
map_public_ip_on_launch = true
tags = {
Name = "${var.tag_name}-public-subnet-ingress-2"
group = "${var.tag_group}"
}
}
#node.js用のprivateサブネット
resource "aws_subnet" "private_subnet_nodejs_1" {
cidr_block = local.nodejs_subnet_1_cidr
vpc_id = aws_vpc.default.id
availability_zone = local.az_1
map_public_ip_on_launch = false
tags = {
Name = "${var.tag_name}-private-subnet-nodejs-1"
group = "${var.tag_group}"
}
}
resource "aws_subnet" "private_subnet_nodejs_2" {
cidr_block = local.nodejs_subnet_2_cidr
vpc_id = aws_vpc.default.id
availability_zone = local.az_2
map_public_ip_on_launch = false
tags = {
Name = "${var.tag_name}-private-subnet-nodejs-2"
group = "${var.tag_group}"
}
}
#puma用のprivateサブネット
resource "aws_subnet" "private_subnet_puma_1" {
cidr_block = local.puma_subnet_1_cidr
vpc_id = aws_vpc.default.id
availability_zone = local.az_1
map_public_ip_on_launch = false
tags = {
Name = "${var.tag_name}-private-subnet-puma-1"
group = "${var.tag_group}"
}
}
resource "aws_subnet" "private_subnet_puma_2" {
cidr_block = local.puma_subnet_2_cidr
vpc_id = aws_vpc.default.id
availability_zone = local.az_2
map_public_ip_on_launch = false
tags = {
Name = "${var.tag_name}-private-subnet-puma-2"
group = "${var.tag_group}"
}
}
#mysql用のprivateサブネット
resource "aws_subnet" "private_subnet_mysql_1" {
cidr_block = local.mysql_subnet_1_cidr
vpc_id = aws_vpc.default.id
availability_zone = local.az_1
map_public_ip_on_launch = false
tags = {
Name = "${var.tag_name}-private-subnet-mysql-1"
group = "${var.tag_group}"
}
}
resource "aws_subnet" "private_subnet_mysql_2" {
cidr_block = local.mysql_subnet_2_cidr
vpc_id = aws_vpc.default.id
availability_zone = local.az_2
map_public_ip_on_launch = false
tags = {
Name = "${var.tag_name}-private-subnet-mysql-2"
group = "${var.tag_group}"
}
}
#redis用のprivateサブネット
resource "aws_subnet" "private_subnet_redis_1" {
cidr_block = local.redis_subnet_1_cidr
vpc_id = aws_vpc.default.id
availability_zone = local.az_1
map_public_ip_on_launch = false
tags = {
Name = "${var.tag_name}-private-subnet-redis-1"
group = "${var.tag_group}"
}
}
resource "aws_subnet" "private_subnet_redis_2" {
cidr_block = local.redis_subnet_2_cidr
vpc_id = aws_vpc.default.id
availability_zone = local.az_2
map_public_ip_on_launch = false
tags = {
Name = "${var.tag_name}-private-subnet-redis-2"
group = "${var.tag_group}"
}
}
#interface VPC endpoint用のprivateサブネット
resource "aws_subnet" "private_subnet_vpc_endpoint_1" {
cidr_block = local.vpc_endpoint_subnet_1_cidr
vpc_id = aws_vpc.default.id
availability_zone = local.az_1
map_public_ip_on_launch = false
tags = {
Name = "${var.tag_name}-private-subnet--vpc-endpoint-1"
group = "${var.tag_group}"
}
}
resource "aws_subnet" "private_subnet_vpc_endpoint_2" {
cidr_block = local.vpc_endpoint_subnet_2_cidr
vpc_id = aws_vpc.default.id
availability_zone = local.az_2
map_public_ip_on_launch = false
tags = {
Name = "${var.tag_name}-private-subnet--vpc-endpoint-2"
group = "${var.tag_group}"
}
}
route.tfで、ルートテーブルのリソースを定義します。
ALB(ingress)用のサブネットののルートテーブルがIG宛になること以外は、それぞれのサブネットのルートテーブルは内部通信のみを行うようにしています。
#publicサブネット用のルートテーブルを定義
resource "aws_route_table" "route-ingress" {
vpc_id = aws_vpc.default.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.default.id
}
tags = {
Name = "${var.tag_name}-route-ingress"
group = "${var.tag_group}"
}
}
resource "aws_route_table_association" "public_subnet_ingress_1_route" {
subnet_id = aws_subnet.public_subnet_ingress_1.id
route_table_id = aws_route_table.route-ingress.id
}
resource "aws_route_table_association" "public_subnet_ingress_2_route" {
subnet_id = aws_subnet.public_subnet_ingress_2.id
route_table_id = aws_route_table.route-ingress.id
}
#node.js用のprivateサブネット用のルートテーブルを定義
resource "aws_route_table" "route_nodejs" {
vpc_id = aws_vpc.default.id
tags = {
Name = "${var.tag_name}-route-nodejs"
group = "${var.tag_group}"
}
}
resource "aws_route_table_association" "private_subnet_nodejs_1_route" {
subnet_id = aws_subnet.private_subnet_nodejs_1.id
route_table_id = aws_route_table.route_nodejs.id
}
resource "aws_route_table_association" "private_subnet_nodejs_2_route" {
subnet_id = aws_subnet.private_subnet_nodejs_2.id
route_table_id = aws_route_table.route_nodejs.id
}
#puma用のprivateサブネット用のルートテーブルを定義
resource "aws_route_table" "route_puma" {
vpc_id = aws_vpc.default.id
tags = {
Name = "${var.tag_name}-route-puma"
group = "${var.tag_group}"
}
}
resource "aws_route_table_association" "private_subnet_puma_1_route" {
subnet_id = aws_subnet.private_subnet_puma_1.id
route_table_id = aws_route_table.route_puma.id
}
resource "aws_route_table_association" "private_subnet_puma_2_route" {
subnet_id = aws_subnet.private_subnet_puma_2.id
route_table_id = aws_route_table.route_puma.id
}
#mysql用のprivateサブネット用のルートテーブルを定義
resource "aws_route_table" "route-mysql" {
vpc_id = aws_vpc.default.id
tags = {
Name = "${var.tag_name}-route-mysql"
group = "${var.tag_group}"
}
}
resource "aws_route_table_association" "private_subnet_mysql_1_route" {
subnet_id = aws_subnet.private_subnet_mysql_1.id
route_table_id = aws_route_table.route-mysql.id
}
resource "aws_route_table_association" "private_subnet_mysql_2_route" {
subnet_id = aws_subnet.private_subnet_mysql_2.id
route_table_id = aws_route_table.route-mysql.id
}
#redis用のprivateサブネット用のルートテーブルを定義
resource "aws_route_table" "route-redis" {
vpc_id = aws_vpc.default.id
tags = {
Name = "${var.tag_name}-route-redis"
group = "${var.tag_group}"
}
}
resource "aws_route_table_association" "private_subnet_redis_1_route" {
subnet_id = aws_subnet.private_subnet_redis_1.id
route_table_id = aws_route_table.route-redis.id
}
resource "aws_route_table_association" "private_subnet_redis_2_route" {
subnet_id = aws_subnet.private_subnet_redis_2.id
route_table_id = aws_route_table.route-redis.id
}
internet-gateway.tfで、IGのリソースを定義します。
resource "aws_internet_gateway" "default" {
vpc_id = aws_vpc.default.id
tags = {
Name = "${var.tag_name}-ig"
group = "${var.tag_group}"
}
}
outputs.tfで、各リソースのIDを出力します。
これらの値は後から定義されるリソースにおいて参照されるものです。
#VPCのIDを出力
output "vpc_id" {
value = "${aws_vpc.default.id}"
}
#各サブネットのIDを出力
output "public_subnet_ingress_1_id" {
value = "${aws_subnet.public_subnet_ingress_1.id}"
}
output "public_subnet_ingress_2_id" {
value = "${aws_subnet.public_subnet_ingress_2.id}"
}
output "private_subnet_nodejs_1_id" {
value = "${aws_subnet.private_subnet_nodejs_1.id}"
}
output "private_subnet_nodejs_2_id" {
value = "${aws_subnet.private_subnet_nodejs_2.id}"
}
output "private_subnet_puma_1_id" {
value = "${aws_subnet.private_subnet_puma_1.id}"
}
output "private_subnet_puma_2_id" {
value = "${aws_subnet.private_subnet_puma_2.id}"
}
output "private_subnet_mysql_1_id" {
value = "${aws_subnet.private_subnet_mysql_1.id}"
}
output "private_subnet_mysql_2_id" {
value = "${aws_subnet.private_subnet_mysql_2.id}"
}
output "private_subnet_redis_1_id" {
value = "${aws_subnet.private_subnet_redis_1.id}"
}
output "private_subnet_redis_2_id" {
value = "${aws_subnet.private_subnet_redis_2.id}"
}
output "private_subnet_vpc_endpoint_1_id" {
value = "${aws_subnet.private_subnet_vpc_endpoint_1.id}"
}
output "private_subnet_vpc_endpoint_2_id" {
value = "${aws_subnet.private_subnet_vpc_endpoint_2.id}"
}
#ECS用のサブネット紐付けたルートテーブルのIDを出力
output "route_nodejs_id" {
value = "${aws_route_table.route_nodejs.id}"
}
output "route_puma_id" {
value = "${aws_route_table.route_puma.id}"
}
kaku-infrastuctureフォルダのmain.tfに、networkモジュールを呼び出す内容を追加します。
module "network" {
source = "./module/network"
name_prefix = var.name_prefix
tag_name = var.tag_name
tag_group = var.tag_group
}
kaku-infrastuctureフォルダで、terraform init、applyを実行して、networkモジュールに定義されたリソースを作成します。
terraform init
terraform apply
securityモジュールの作成
セキュリティグループのリソースを定義するモジュールを作成します。
まだセキュリティグループを紐づけるリソースを作成していませんが、イメージとしては以下のようなリクエストを許可するセキュリティグループを定義します。
注意点として、サービスディスカバリ経由でコンテナ間での通信が生じるので、NextjsからRailsのコンテナへのリクエストを許可するセキュリティグループが必要になります、
moduleフォルダの下にsecurity-groupフォルダを作成します。
kaku-infrastucture
└── module
└── security-group
├── outputs.tf
├── variables.tf
└── security-group.tf
それぞれの定義内容は以下のようになります。
variable name_prefix {}
variable tag_name {}
variable tag_group {}
#networkモジュールからvpc_idを受け取る
variable vpc_id {}
# ALB用のセキュリティグループを定義
resource "aws_security_group" "sg_alb" {
name = "${var.name_prefix}-sg-alb"
vpc_id = var.vpc_id
tags = {
Name = "${var.tag_name}-sg-alb"
group = "${var.tag_group}"
}
}
resource "aws_security_group_rule" "sg_alb_ingress_http" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
security_group_id = aws_security_group.sg_alb.id
cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_security_group_rule" "sg_alb_ingress_https" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
security_group_id = aws_security_group.sg_alb.id
cidr_blocks = ["0.0.0.0/0"]
}
resource "aws_security_group_rule" "sg_alb_egress_all" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
security_group_id = aws_security_group.sg_alb.id
cidr_blocks = ["0.0.0.0/0"]
}
#node.js用のセキュリティグループを定義
resource "aws_security_group" "sg_nodejs" {
name = "${var.name_prefix}-sg-nodejs"
vpc_id = var.vpc_id
tags = {
Name = "${var.tag_name}-sg-nodejs"
group = "${var.tag_group}"
}
}
resource "aws_security_group_rule" "sg_nodejs_ingress_http_from_alb" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
security_group_id = aws_security_group.sg_nodejs.id
source_security_group_id = aws_security_group.sg_alb.id
}
resource "aws_security_group_rule" "sg_nodejs_egress_all" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
security_group_id = aws_security_group.sg_nodejs.id
cidr_blocks = ["0.0.0.0/0"]
}
#puma用のセキュリティグループを定義
resource "aws_security_group" "sg_puma" {
name = "${var.name_prefix}-sg-puma"
vpc_id = var.vpc_id
tags = {
Name = "${var.tag_name}-sg-puma"
group = "${var.tag_group}"
}
}
resource "aws_security_group_rule" "sg_puma_ingress_http_from_alb" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
security_group_id = aws_security_group.sg_puma.id
source_security_group_id = aws_security_group.sg_alb.id
}
resource "aws_security_group_rule" "sg_puma_ingress_http_from_nodejs" {
type = "ingress"
from_port = 80
to_port = 80
protocol = "tcp"
security_group_id = aws_security_group.sg_puma.id
source_security_group_id = aws_security_group.sg_nodejs.id
}
resource "aws_security_group_rule" "sg_puma_egress_all" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
security_group_id = aws_security_group.sg_puma.id
cidr_blocks = ["0.0.0.0/0"]
}
#mysql用のセキュリティグループを定義
resource "aws_security_group" "sg_mysql" {
name = "${var.name_prefix}-sg-mysql"
vpc_id = var.vpc_id
tags = {
Name = "${var.tag_name}-sg-mysql"
group = "${var.tag_group}"
}
}
resource "aws_security_group_rule" "sg_mysql_ingress_mysql_from_puma" {
type = "ingress"
from_port = 3306
to_port = 3306
protocol = "tcp"
security_group_id = aws_security_group.sg_mysql.id
source_security_group_id = aws_security_group.sg_puma.id
}
resource "aws_security_group_rule" "sg_mysql_egress_all" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
security_group_id = aws_security_group.sg_mysql.id
cidr_blocks = ["0.0.0.0/0"]
}
#redis用のセキュリティグループを定義
resource "aws_security_group" "sg_redis" {
name = "${var.name_prefix}-sg-redis"
vpc_id = var.vpc_id
tags = {
Name = "${var.tag_name}-sg-redis"
group = "${var.tag_group}"
}
}
resource "aws_security_group_rule" "sg_redis_ingress_redis_from_puma" {
type = "ingress"
from_port = 6379
to_port = 6379
protocol = "tcp"
security_group_id = aws_security_group.sg_redis.id
source_security_group_id = aws_security_group.sg_puma.id
}
resource "aws_security_group_rule" "sg_redis_egress_all" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
security_group_id = aws_security_group.sg_redis.id
cidr_blocks = ["0.0.0.0/0"]
}
#vpcendpoint用のセキュリティグループを定義
resource "aws_security_group" "sg_vpc_endpoint" {
name = "${var.name_prefix}-sg-vpc-endpoint"
vpc_id = var.vpc_id
tags = {
Name = "${var.tag_name}-sg-vpc-endpoint"
group = "${var.tag_group}"
}
}
resource "aws_security_group_rule" "sg_vpc_endpoint_ingress_https_from_nodejs" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
security_group_id = aws_security_group.sg_vpc_endpoint.id
source_security_group_id = aws_security_group.sg_nodejs.id
}
resource "aws_security_group_rule" "sg_vpc_endpoint_ingress_https_from_puma" {
type = "ingress"
from_port = 443
to_port = 443
protocol = "tcp"
security_group_id = aws_security_group.sg_vpc_endpoint.id
source_security_group_id = aws_security_group.sg_puma.id
}
resource "aws_security_group_rule" "sg_vpc_endpoint_egress_all" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
security_group_id = aws_security_group.sg_vpc_endpoint.id
cidr_blocks = ["0.0.0.0/0"]
}
output "sg_alb_id" {
value = "${aws_security_group.sg_alb.id}"
}
output "sg_nodejs_id" {
value = "${aws_security_group.sg_nodejs.id}"
}
output "sg_puma_id" {
value = "${aws_security_group.sg_puma.id}"
}
output "sg_mysql_id" {
value = "${aws_security_group.sg_mysql.id}"
}
output "sg_redis_id" {
value = "${aws_security_group.sg_redis.id}"
}
output "sg_vpc_endpoint_id" {
value = "${aws_security_group.sg_vpc_endpoint.id}"
}
kaku-infrastuctureフォルダのmain.tfに、securityモジュールを呼び出す内容を追加します。
module "security-group" {
source = "./module/security-group"
name_prefix = var.name_prefix
tag_name = var.tag_name
tag_group = var.tag_group
vpc_id = module.network.vpc_id
}
kaku-infrastuctureフォルダで、terraform init、applyを実行して、securityモジュールに定義されたリソースを作成します。
terraform init
terraform apply
独自ドメインの取得と、ACM証明書の作成
ALBに紐づける独自ドメインと、ACM証明書を作成します。
独自ドメインの取得と、ACMによる証明書の作成については一度きりの作業なので、IaCで定義することはせず、コンソールから行います。
独自ドメインの取得、ACMによる証明書の作成の作業については、以下の記事などを参考に行なってください。
上記の作業が完了したら、Terraformで参照形式で独自ドメインを取得するために、独自ドメインをParamater Storeに登録します。こちらはIaCで登録することは可能ですが、ParamaterStoreは機密保持の観点からも利用するため、IaCにハードコードすることは避け、コンソールから登録します。
図のようにdomain-nameという名前で登録します。
登録したら、kaku-infrastuctureフォルダにdata.tfを作成します。
kaku-infrastucture
└── data.tf
以下のように記述することで、独自ドメインをParamater Storeから取得し、また登録されたドメインのホストゾーンと、ACM証明書をTerraformで取得することができます。この後で定義するRoute53のレコードセットの設定などで利用します。
# Paramater Storeに登録したドメイン名を取得
data "aws_ssm_parameter" "domain_name" {
name = "domain-name"
}
# domainのホストゾーンを取得
data "aws_route53_zone" "default" {
name = data.aws_ssm_parameter.domain_na Ame.value
private_zone = false
}
#acm証明書を取得
data "aws_acm_certificate" "default" {
domain = data.aws_ssm_parameter.domain_name.value
}
ALBモジュールの作成
ALBのリソースを定義するモジュールを作成します。
moduleフォルダの下にalbフォルダを作成します。
kaku-infrastucture
└── module
└── alb
├── outputs.tf
├── variables.tf
└── alb.tf
それぞれの定義内容は以下のようになります。
variable name_prefix {}
variable tag_name {}
variable tag_group {}
variable vpc_id {}
variable subnet_ingress_1_id {}
variable subnet_ingress_2_id {}
variable sg_alb_id {}
variable certificate_arn {}
alb.tfで、ALBのリソースを定義します。
ポイントとしては、ターゲットグループでRailsとNextjsのコンテナを指定して、Railsへのリクエストの転送は、/api/*のパスで、それ以外のリクエストはNextjsへ転送するようにしています。また、それぞれのヘルスチェックのパスについても指定しています。
ACMの証明書を紐づけることで、HTTPSでの通信を可能とます。また、HTTPリクエストについては、80ポートから443ポートへのリダイレクトを行うようにしています。
resource "aws_lb" "default" {
name = "${var.name_prefix}-alb"
load_balancer_type = "application"
internal = false
idle_timeout = 60
enable_deletion_protection = false
subnets= [
var.subnet_ingress_1_id,
var.subnet_ingress_2_id
]
security_groups = [var.sg_alb_id]
tags = {
Name = "${var.tag_name}-alb"
group = "${var.tag_group}"
}
}
#node.jsへのターゲットグループを定義
resource "aws_lb_target_group" "tg_nodejs" {
name = "${var.name_prefix}-alb-tg-nodejs"
target_type = "ip"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
healthy_threshold = 3
unhealthy_threshold = 2
timeout = 5
path = "/next-api/health-check"
port = "traffic-port"
protocol = "HTTP"
matcher = "200"
interval = 30
}
tags = {
Name = "${var.tag_name}-alb-tg-nodejs"
group = "${var.tag_group}"
}
}
#pumaへのターゲットグループを定義
resource "aws_lb_target_group" "tg_puma" {
name = "${var.name_prefix}-alb-tg-puma"
target_type = "ip"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
healthy_threshold = 3
unhealthy_threshold = 2
timeout = 5
path = "/api/v1/health_check"
port = "traffic-port"
protocol = "HTTP"
matcher = "200"
interval = 30
}
tags = {
Name = "${var.tag_name}-alb-tg-puma"
group = "${var.tag_group}"
}
}
#port443でのnode.js用のターゲットグループへのリスナー(ルール)を定義
resource "aws_lb_listener" "alb_listener_https" {
load_balancer_arn = aws_lb.default.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = var.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.tg_nodejs.arn
}
}
#port443のpuma用のターゲットグループへのリスナールールを定義
resource "aws_lb_listener_rule" "https-rule-puma" {
listener_arn = aws_lb_listener.alb_listener_https.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.tg_puma.arn
}
condition {
path_pattern {
values = ["/api/*"]
}
}
}
#port80へのリクエストをport443へリダイレクトするリスナーを定義
resource "aws_lb_listener" "alb_listener_http" {
load_balancer_arn = aws_lb.default.arn
port = 80
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
output "alb_dns_name" {
value = "${aws_lb.default.dns_name}"
}
output "alb_zone_id" {
value = "${aws_lb.default.zone_id}"
}
output "tg_puma_arn" {
value = "${aws_lb_target_group.tg_puma.arn}"
}
output "tg_nodejs_arn" {
value = "${aws_lb_target_group.tg_nodejs.arn}"
}
kaku-infrastuctureフォルダのmain.tfに、albモジュールを呼び出す内容を追加します。
module "alb" {
source = "./module/alb"
name_prefix = var.name_prefix
tag_name = var.tag_name
tag_group = var.tag_group
vpc_id = module.network.vpc_id
subnet_ingress_1_id = module.network.public_subnet_ingress_1_id
subnet_ingress_2_id = module.network.public_subnet_ingress_2_id
sg_alb_id = module.security-group.sg_alb_id
certificate_arn = data.aws_acm_certificate.default.arn
}
kaku-infrastuctureフォルダで、terraform init、applyを実行して、albモジュールに定義されたリソースを作成します。
terraform init
terraform apply
Route53モジュールの作成
Route53のリソースを定義するモジュールを作成します。
moduleフォルダの下にroute53フォルダを作成します。
kaku-infrastucture
└── module
└── route53
├── variables.tf
└── route53.tf
それぞれの定義内容は以下のようになります。
variable alb_dns_name {}
variable alb_zone_id {}
variable domain_name {}
variable domain_zone_id {}
route53.tfで、Route53のリソースを定義します。
独自ドメインのホストゾーンに、ALBのDNSを指定するAレコードを作成します。こうすることで、独自ドメインにブラウザからアクセスすると、ALBにリクエストが転送されるようになります。
# Route53にALBのDNSと紐づけるAレコードを作成
resource "aws_route53_record" "default" {
zone_id = var.domain_zone_id
name = var.domain_name
type = "A"
alias {
name = var.alb_dns_name
zone_id = var.alb_zone_id
evaluate_target_health = true
}
}
kaku-infrastuctureフォルダのmain.tfに、route53モジュールを呼び出す内容を追加します。
module "route53" {
source = "./module/route53"
alb_dns_name = module.alb.alb_dns_name
alb_zone_id = module.alb.alb_zone_id
domain_name = data.aws_ssm_parameter.domain_name.value
domain_zone_id = data.aws_route53_zone.default.zone_id
}
kaku-infrastuctureフォルダで、terraform init、applyを実行して、route53モジュールに定義されたリソースを作成します。
terraform init
terraform apply
RDSモジュールの作成
RDSのリソースを定義するモジュールを作成します。
moduleフォルダの下にrdsフォルダを作成します。
kaku-infrastucture
└── module
└── rds
├── outputs.tf
├── variables.tf
└── rds.tf
それぞれの定義内容は以下のようになります。
variable name_prefix {}
variable tag_name {}
variable tag_group {}
variable vpc_id {}
variable subnet_mysql_1_id {}
variable subnet_mysql_2_id {}
variable sg_mysql_id {}
rds.tfで、RDSのリソースを定義します。
サブネットグループの作成、Auroraのインスタンス作成、プライマリインスタンスの作成、リードレプリカの作成という順で定義しています。
クラスターの定義のおいて、manage_master_user_passwordをtrueとすると、SecretManagerにusernameとpasswordが登録されます。
#サブネットグループを作成
resource "aws_db_subnet_group" "default" {
name = "${var.name_prefix}_rds_subnet_group"
subnet_ids = [
var.subnet_mysql_1_id,
var.subnet_mysql_2_id
]
tags = {
Name = "${var.tag_name}-rds-subnet-group"
}
}
resource "aws_rds_cluster" "default" {
cluster_identifier = "${var.name_prefix}-rds-cluster"
engine = "aurora-mysql"
engine_version = "8.0.mysql_aurora.3.04.1"
database_name = "${var.name_prefix}_db"
master_username = "admin"
manage_master_user_password = true
db_subnet_group_name = aws_db_subnet_group.default.name
vpc_security_group_ids = [var.sg_mysql_id]
backup_retention_period = 5
preferred_backup_window = "07:00-09:00"
preferred_maintenance_window = "sun:04:00-sun:06:00"
skip_final_snapshot = true
network_type = "IPV4"
port = 3306
}
#RDS Aurora プライマリインスタンスの定義
resource "aws_rds_cluster_instance" "primary_instances" {
identifier = "${var.name_prefix}-rds-cluster-primary"
cluster_identifier = aws_rds_cluster.default.id
instance_class = "db.t3.medium"
engine = aws_rds_cluster.default.engine
engine_version = aws_rds_cluster.default.engine_version
publicly_accessible = false
}
# RDS Aurora リードレプリカの定義
resource "aws_rds_cluster_instance" "read_instances_1" {
identifier = "${var.name_prefix}-rds-cluster-read-instances-1"
cluster_identifier = aws_rds_cluster.default.id
instance_class = "db.t3.medium"
engine = aws_rds_cluster.default.engine
engine_version = aws_rds_cluster.default.engine_version
publicly_accessible = false
}
output "primary_db_host" {
value = aws_rds_cluster.default.endpoint
}
output "reader_db_host" {
value = aws_rds_cluster.default.reader_endpoint
}
output "db_name" {
value = aws_rds_cluster.default.database_name
}
kaku-infrastuctureフォルダのmain.tfに、rdsモジュールを呼び出す内容を追加します。
module "rds" {
source = "./module/rds"
name_prefix = var.name_prefix
tag_name = var.tag_name
tag_group = var.tag_group
vpc_id = module.network.vpc_id
subnet_mysql_1_id = module.network.private_subnet_mysql_1_id
subnet_mysql_2_id = module.network.private_subnet_mysql_2_id
sg_mysql_id = module.security-group.sg_mysql_id
}
kaku-infrastuctureフォルダで、terraform init、applyを実行して、rdsモジュールに定義されたリソースを作成します。
terraform init
terraform apply
ElastiCacheモジュールの作成
ElastiCacheのリソースを定義するモジュールを作成します。
moduleフォルダの下にelasti-cacheフォルダを作成します。
リソースを定義するファイル名は、redis.tfとしています。
kaku-infrastucture
└── module
└── elasti-cache
├── outputs.tf
├── variables.tf
└── redis.tf
それぞれの定義内容は以下のようになります。
variable name_prefix {}
variable tag_name {}
variable tag_group {}
variable vpc_id {}
redis.tfで、Redisのリソースを定義します。
resource "aws_elasticache_subnet_group" "default" {
name = "${var.name_prefix}-redis"
subnet_ids = [var.subnet_redis_1_id, var.subnet_redis_2_id]
}
resource "aws_elasticache_cluster" "default" {
cluster_id = "${var.name_prefix}-redis"
engine = "redis"
engine_version = "7.0"
node_type = "cache.t2.micro"
num_cache_nodes = 1
port = 6379
subnet_group_name = aws_elasticache_subnet_group.default.name
security_group_ids = [var.sg_redis_id]
}
output "redis_endpoint" {
value = aws_elasticache_cluster.default.cache_nodes.0.address
}
kaku-infrastuctureフォルダのmain.tfに、ElastiCacheモジュールを呼び出す内容を追加します。
module "elasti-cashe" {
source = "./module/elasti-cashe"
name_prefix = var.name_prefix
tag_name = var.tag_name
tag_group = var.tag_group
subnet_redis_1_id = module.network.private_subnet_redis_1_id
subnet_redis_2_id = module.network.private_subnet_redis_2_id
sg_redis_id = module.security-group.sg_redis_id
}
kaku-infrastuctureフォルダで、terraform init、applyを実行して、rdsモジュールに定義されたリソースを作成します。
terraform init
terraform apply
SecretManagerからRDSのusernameとpasswordをterraform内で取得する
RDSモジュールを反映したら、コンソール上からSecretManagerを確認してください。RDSのusernameとpasswordが登録されているはずです。
このusernameとpasswordをTerraform内で取得するために、kaku-infrastuctureフォルダのdata.tfに以下の内容を追記します。
#secret managerのメタ情報を取得
data "aws_secretsmanager_secret" "db_secret" {
name = #SecretManagerに登録されたsecretの名前
}
#メタ情報をもとにsecretのARNを取得
data "aws_secretsmanager_secret_version" "db_secret_id" {
secret_id = data.aws_secretsmanager_secret.db_secret.id
}
このdataは、後で定義するECSのタスク定義で利用します。
VPCエンドポイントモジュールの作成
VPCエンドポイントのリソースを定義するモジュールを作成します。
moduleフォルダの下にvpc-endpointフォルダを作成します。
kaku-infrastucture
└── module
└── vpc-endpoint
├── variables.tf
└── vpc-endpoint.tf
それぞれの定義内容は以下のようになります。
variable name_prefix {}
variable tag_name {}
variable tag_group {}
variable vpc_id {}
variable subnet_vpc_endpoint_1_id {}
variable subnet_vpc_endpoint_2_id {}
variable sg_vpc_endpoint_id {}
variable route_nodejs_id {}
variable route_puma_id {}
#ECRとの通信のためのエンドポイント_api
resource "aws_vpc_endpoint" "vpc_endpoint_ecr_api" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.ap-northeast-1.ecr.api"
vpc_endpoint_type = "Interface"
subnet_ids = [var.subnet_vpc_endpoint_1_id,var.subnet_vpc_endpoint_2_id]
security_group_ids = [var.sg_vpc_endpoint_id]
private_dns_enabled = true
tags = {
Name = "${var.tag_name}-vpc-endpoint-ecr-api"
group = "${var.tag_group}"
}
}
#ECRとの通信のためのエンドポイント_dkr
resource "aws_vpc_endpoint" "vpc_endpoint_ecr_dkr" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.ap-northeast-1.ecr.dkr"
vpc_endpoint_type = "Interface"
subnet_ids = [var.subnet_vpc_endpoint_1_id,var.subnet_vpc_endpoint_2_id]
security_group_ids = [var.sg_vpc_endpoint_id]
private_dns_enabled = true
tags = {
Name = "${var.tag_name}-vpc-endpoint-ecr-dkr"
group = "${var.tag_group}"
}
}
# CloudWatchLogsとの通信のためのエンドポイント
resource "aws_vpc_endpoint" "vpc_endpoint_logs" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.ap-northeast-1.logs"
vpc_endpoint_type = "Interface"
subnet_ids = [var.subnet_vpc_endpoint_1_id,var.subnet_vpc_endpoint_2_id]
security_group_ids = [var.sg_vpc_endpoint_id]
private_dns_enabled = true
tags = {
Name = "${var.tag_name}-vpc-endpoint-logs"
group = "${var.tag_group}"
}
}
# SecretsManagerとの通信のためのエンドポイント
resource "aws_vpc_endpoint" "vpc_endpoint_secretsmanager" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.ap-northeast-1.secretsmanager"
vpc_endpoint_type = "Interface"
subnet_ids = [var.subnet_vpc_endpoint_1_id,var.subnet_vpc_endpoint_2_id]
security_group_ids = [var.sg_vpc_endpoint_id]
private_dns_enabled = true
tags = {
Name = "${var.tag_name}-vpc-endpoint-secretsmanager"
group = "${var.tag_group}"
}
}
# SSM エンドポイントの設定
resource "aws_vpc_endpoint" "vpc_endpoint_ssm" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.ap-northeast-1.ssm"
vpc_endpoint_type = "Interface"
subnet_ids = [var.subnet_vpc_endpoint_1_id,var.subnet_vpc_endpoint_2_id]
security_group_ids = [var.sg_vpc_endpoint_id]
private_dns_enabled = true
tags = {
Name = "${var.tag_name}-vpc-endpoint-ssm"
group = "${var.tag_group}"
}
}
# SSM Messages エンドポイントの設定
resource "aws_vpc_endpoint" "vpc_endpoint_ssm_messages" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.ap-northeast-1.ssmmessages"
vpc_endpoint_type = "Interface"
subnet_ids = [var.subnet_vpc_endpoint_1_id,var.subnet_vpc_endpoint_2_id]
security_group_ids = [var.sg_vpc_endpoint_id]
private_dns_enabled = true
tags = {
Name = "${var.tag_name}-vpc-endpoint-ssmmessages"
group = "${var.tag_group}"
}
}
# S3との通信のためのエンドポイント
# ECRに格納されるイメージはS3に格納されるため、S3との通信のためのエンドポイントを作成する
resource "aws_vpc_endpoint" "vpc_endpoint_s3" {
vpc_id = var.vpc_id
service_name = "com.amazonaws.ap-northeast-1.s3"
vpc_endpoint_type = "Gateway"
route_table_ids = [var.route_nodejs_id,var.route_puma_id]
tags = {
Name = "${var.tag_name}-vpc-endpoint-s3"
group = "${var.tag_group}"
}
}
kaku-infrastuctureフォルダのmain.tfに、vpc-endpointモジュールを呼び出す内容を追加します。
module "vpc-endpoint" {
source = "./module/vpc-endpoint"
name_prefix = var.name_prefix
tag_name = var.tag_name
tag_group = var.tag_group
vpc_id = module.network.vpc_id
subnet_vpc_endpoint_1_id = module.network.private_subnet_vpc_endpoint_1_id
subnet_vpc_endpoint_2_id = module.network.private_subnet_vpc_endpoint_2_id
sg_vpc_endpoint_id = module.security-group.sg_vpc_endpoint_id
route_nodejs_id = module.network.route_nodejs_id
route_puma_id = module.network.route_puma_id
}
kaku-infrastuctureフォルダで、terraform init、applyを実行して、vpc-endpointモジュールに定義されたリソースを作成します。
terraform init
terraform apply
ECRモジュールの作成
ECRのリソースを定義するモジュールを作成します。
moduleフォルダの下にecrフォルダを作成します。
kaku-infrastucture
└── module
└── ecr
├── outputs.tf
├── variables.tf
└── ecr.tf
それぞれの定義内容は以下のようになります。
variable name_prefix {}
variable tag_name {}
variable tag_group {}
ecr.tfで、ECRのリソースを定義します。
Rails(puma)とNext.js(Node.js)の2つのリポジトリを作成しています。また、それぞれのリポジトリに対して、ライフサイクルポリシーを設定しており、最新の3つのイメージのみを保持するようにしています。
#nodejsのECRリポジトリを作成
resource "aws_ecr_repository" "repository_nodejs" {
name = "${var.name_prefix}-nodejs"
image_tag_mutability = "IMMUTABLE"
image_scanning_configuration {
scan_on_push = true
}
encryption_configuration {
encryption_type = "KMS"
}
tags = {
Name = "${var.name_prefix}-nodejs"
group = "${var.tag_group}"
}
}
resource "aws_ecr_lifecycle_policy" "lifecycle_policy_nodejs" {
repository = aws_ecr_repository.repository_nodejs.name
policy = jsonencode({
rules = [
{
rulePriority = 1,
description = "Keep at most 3 images",
selection = {
tagStatus = "any",
countType = "imageCountMoreThan",
countNumber = 3
},
action = {
type = "expire"
}
}
]
})
}
#pumaのECRリポジトリを作成
resource "aws_ecr_repository" "repository_puma" {
name = "${var.name_prefix}-puma"
image_tag_mutability = "IMMUTABLE"
image_scanning_configuration {
scan_on_push = true
}
encryption_configuration {
encryption_type = "KMS"
}
tags = {
Name = "${var.name_prefix}-puma"
group = "${var.tag_group}"
}
}
resource "aws_ecr_lifecycle_policy" "lifecycle_policy_puma" {
repository = aws_ecr_repository.repository_puma.name
policy = jsonencode({
rules = [
{
rulePriority = 1,
description = "Keep at most 3 images",
selection = {
tagStatus = "any",
countType = "imageCountMoreThan",
countNumber = 3
},
action = {
type = "expire"
}
}
]
})
}
output "nodejs_repository" {
value = "${aws_ecr_repository.repository_nodejs.repository_url}"
}
output "puma_repository" {
value = "${aws_ecr_repository.repository_puma.repository_url}"
}
kaku-infrastuctureフォルダのmain.tfに、ecrモジュールを呼び出す内容を追加します。
module "ecr" {
source = "./module/ecr"
name_prefix = var.name_prefix
tag_name = var.tag_name
tag_group = var.tag_group
}
kaku-infrastuctureフォルダで、terraform init、applyを実行して、ecrモジュールに定義されたリソースを作成します。
terraform init
terraform apply
RailsのDockerfileを作成、ビルドしてECRにプッシュする
RailsのDockerイメージをECRで管理するために、Dockerfileを作成、ビルドしてECRにプッシュします。
Dockerfileの内容は開発しているアプリケーションの内容によって異なってくると思いますが、以下に最低限必要な内容を記載した例を示します。
FROM --platform=linux/x86_64 ruby:3.1.2
ENV RAILS_ENV=production
ENV PORT=80
RUN mkdir /app
WORKDIR /app
COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock
RUN bundle install
COPY . /app
EXPOSE 80
# 本番環境でpumaを起動する
ENTRYPOINT ["bundle", "exec" ,"puma", "-C", "config/puma.rb"]
またビルドする前に、アプリケーション側でdatabase.ymlと、CORSの設定を本番環境用に調整する必要があります。
database.ymlにRDSへ接続するための設定を、以下のように記述します。
databseやusername、password、hostは、後述で定義するECSタスク定義の環境変数で設定したものを参照するようにしています。
default: &default
adapter: mysql2
encoding: utf8mb4
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
username: root
port: 3306
production:
<<: *default
database: <%= ENV.fetch("DATABASE_NAME" ) { } %>
username: <%= ENV.fetch("DATABASE_USERNAME"){ } %>
password: <%= ENV.fetch("DATABASE_PASSWORD"){ } %>
host: <%= ENV.fetch("DATABASE_HOST"){ } %>
CORSは、独自ドメインからのアクセスを許可する必要があるため、以下のように記述します。こちらも後述で定義するECSタスク定義の環境変数で設定したものを参照するようにしています。
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins ENV.fetch("FRONT_DOMAIN", "http://localhost:8000")
resource "*",
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end
上記の内容を定義したら、DockerfileをビルドしてECRにプッシュします。
ECSと紐づけるIAMロールを作成
ECSと紐づけるIAMロールを作成します。
kaku-infrastuctureフォルダの下にiamフォルダを作成します。
kaku-infrastucture
└── iam
├── outputs.tf
├── variables.tf
└── iam.tf
それぞれの定義内容は以下のようになります。
variable name_prefix {}
iam.tfで、ECSタスク実行ロールとECSタスクロールを定義します。
ECSタスク実行ロールは、ECRからのイメージの取得、CloudWatchLogsへのログの書き込み、SecretManagerからの環境変数の取得を許可するポリシーを定義しています。
ECSタスクロールは、SSMのエージェントを利用するためのポリシーを定義しています。これはECSexecを利用するために必要なポリシーです。
resource "aws_iam_policy" "ecs_task_execution_role_policy" {
name = "${var.name_prefix}-ecs-task-execution-role-policy"
policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchCheckLayerAvailability",
"ecr:GetDownloadUrlForLayer",
"ecr:BatchGetImage",
"logs:CreateLogStream",
"logs:PutLogEvents",
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret"
],
"Resource": "*"
}
]
})
}
resource "aws_iam_role" "ecs_task_execution_role" {
name = "${var.name_prefix}-ecs-task-execution-role"
assume_role_policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Sid": "",
"Effect": "Allow",
"Principal": {
"Service": [
"ecs-tasks.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy_attachment" "ecs_task_execution_role_attach" {
role = aws_iam_role.ecs_task_execution_role.name
policy_arn = aws_iam_policy.ecs_task_execution_role_policy.arn
}
resource "aws_iam_policy" "ecs_task_role_policy" {
name = "${var.name_prefix}-ecs-task-role-policy"
policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel"
],
"Resource": "*"
}
]
})
}
resource "aws_iam_role" "ecs_task_role" {
name = "${var.name_prefix}-ecs-task-role"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Action = "sts:AssumeRole",
Effect = "Allow",
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
}
resource "aws_iam_role_policy_attachment" "ecs_task_role_attach" {
role = aws_iam_role.ecs_task_role.name
policy_arn = aws_iam_policy.ecs_task_role_policy.arn
}
output "ecs_task_execution_role_arn" {
value = "${aws_iam_role.ecs_task_execution_role.arn}"
}
output "ecs_task_role_arn" {
value = "${aws_iam_role.ecs_task_role.arn}"
}
kaku-infrastuctureフォルダのmain.tfに、iamモジュールを呼び出す内容を追加します。
module "iam" {
source = "./module/iam"
name_prefix = var.name_prefix
}
kaku-infrastuctureフォルダで、terraform init、applyを実行して、iamモジュールに定義されたリソースを作成します。
terraform init
terraform apply
ECSに紐づけるCloudWatchLogsのロググループを作成
ECSに紐づけるCloudWatchLogsのロググループを作成します。
kaku-infrastuctureフォルダの下にcloud-watch-logsフォルダを作成します。
kaku-infrastucture
└── cloud-watch-logs
├── outputs.tf
├── variables.tf
└── cloud-watch-logs.tf
それぞれの定義内容は以下のようになります。
variable name_prefix {}
resource "aws_cloudwatch_log_group" "puma-log" {
name = "/${var.name_prefix}/puma"
retention_in_days = 3
}
resource "aws_cloudwatch_log_group" "nodejs-log" {
name = "/${var.name_prefix}/nodejs"
retention_in_days = 3
}
output "puma_log_group" {
value = "${aws_cloudwatch_log_group.puma-log.name}"
}
output "nodejs_log_group" {
value = "${aws_cloudwatch_log_group.nodejs-log.name}"
}
kaku-infrastuctureフォルダのmain.tfに、cloud-watch-logsモジュールを呼び出す内容を追加します。
module "cloud-watch-logs" {
source = "./module/cloud-watch-logs"
name_prefix = var.name_prefix
}
kaku-infrastuctureフォルダで、terraform init、applyを実行して、cloud-watch-logsモジュールに定義されたリソースを作成します。
terraform init
terraform apply
Rails用のECSモジュールの作成
ECSのリソースを定義するモジュールを作成します。
moduleフォルダの下にecsフォルダを作成します。
kaku-infrastucture
└── module
└── ecs
├── variables.tf
└── ecs-puma.tf
それぞれの定義内容は以下のようになります。
variable name_prefix {}
variable tag_name {}
variable tag_group {}
variable vpc_id {}
variable subnet_puma_1_id {}
variable subnet_puma_2_id {}
variable sg_puma_id {}
variable image_puma {}
variable image_puma_version {}
variable execution_role_arn {}
variable task_role_arn {}
variable cloudwatch_log_group_arn_puma {}
variable tg_puma_arn {}
variable primary_db_host {}
variable db_name {}
variable redis_host {}
variable db_secret_username {}
variable db_secret_password {}
variable domain_name {}
variable task_cpu_puma {}
variable task_memory_puma {}
variable task_container_memory_reservation_puma {}
variable task_container_memory_puma {}
variable task_container_cpu_puma {}
variable task_count_puma {}
variable task_health_check_grace_period_seconds_puma {}
ecs-puma.tfで、Railsコンテナ用のタスク定義、ECSクラスター、バックエンドのECSサービスを定義します。
タスク定義の環境変数は、SecretManagerから取得したusernameとpasswordを参照しており、またParamater Storeから取得したドメイン名を参照しています。DATABASE_HOSTと、DATABASE_NAMEは、RDSモジュールを参照して設定します。
サービスの設定では、enable_execute_commandをtrueにしておくことで、ECSexecを利用できるようにしています。これにより、コンテナ内でコマンドを実行することができます。
#バックエンドコンテナ用のタスク定義
resource "aws_ecs_task_definition" "task_puma" {
family = "${var.name_prefix}-puma"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = "${var.task_cpu_puma}"
memory = "${var.task_memory_puma}"
execution_role_arn = "${var.execution_role_arn}"
task_role_arn = "${var.task_role_arn}"
container_definitions = jsonencode([{
name = "kaku_puma",
image = "${var.image_puma}:${var.image_puma_version}",
essential: true,
memoryReservation = "${var.task_container_memory_reservation_puma}"
memory = "${var.task_container_memory_puma}",
cpu = "${var.task_container_cpu_puma}",
portMappings: [
{
protocol: "tcp",
containerPort: 80
}
],
logConfiguration = {
logDriver = "awslogs",
options = {
"awslogs-group" = "${var.cloudwatch_log_group_arn_puma}",
"awslogs-region" = "ap-northeast-1",
"awslogs-stream-prefix" = "${var.name_prefix}-puma-task"
}
},
secrets = [
{
name= "DATABASE_USERNAME",
valueFrom = "${var.db_secret_username}"
},
{
name= "DATABASE_PASSWORD",
valueFrom= "${var.db_secret_password}"
}
],
environment = [
{
name = "DATABASE_HOST",
value = "${var.primary_db_host}"
},
{
name = "DATABASE_NAME",
value = "${var.db_name}"
},
{
name = "FRONT_DOMAIN",
value = "https://${var.domain_name}"
},
{
name = "REDIS_HOST",
value = "${var.redis_host}"
}
]
}])
}
#バックエンドのecrクラスターを定義
resource "aws_ecs_cluster" "cluster_puma" {
name = "${var.name_prefix}-puma"
setting {
name = "containerInsights"
value = "enabled"
}
}
resource "aws_ecs_service" "service_puma" {
name = "${var.name_prefix}-puma"
launch_type = "FARGATE"
platform_version = "1.4.0"
task_definition = aws_ecs_task_definition.task_puma.arn
desired_count = "${var.task_count_puma}"
health_check_grace_period_seconds = "${var.task_health_check_grace_period_seconds_puma}"
cluster = aws_ecs_cluster.cluster_puma.id
enable_execute_command = true
network_configuration {
subnets = [var.subnet_puma_1_id, var.subnet_puma_2_id]
security_groups = [var.sg_puma_id]
assign_public_ip = false
}
load_balancer {
target_group_arn = var.tg_puma_arn
container_name = "kaku_puma"
container_port = "80"
}
}
kaku-infrastuctureフォルダのvariables.tfと、main.tfに、ecsモジュールを呼び出す内容を追加します。
variables.tfには、pumaのタスク定義用の変数を追加します。image_puma_versionに関しては、ECRにプッシュしたイメージのバージョンを指定してください。
# pumaのタスク定義用
variable image_puma_version {
default = "latest"
}
variable task_cpu_puma {
default = 256
}
variable task_memory_puma {
default = 512
}
variable task_container_memory_reservation_puma {
default = 512
}
variable task_container_memory_puma {
default = 512
}
variable task_container_cpu_puma {
default = 256
}
variable task_count_puma {
default = 1
}
variable task_health_check_grace_period_seconds_puma {
default = 60
}
module "ecs" {
source = "./module/ecs"
name_prefix = var.name_prefix
tag_name = var.tag_name
tag_group = var.tag_group
vpc_id = module.network.vpc_id
subnet_puma_1_id = module.network.private_subnet_puma_1_id
subnet_puma_2_id = module.network.private_subnet_puma_2_id
primary_db_host = module.rds.primary_db_host
db_name = module.rds.db_name
redis_host = module.elasti-cashe.redis_endpoint
db_secret_username = "${data.aws_secretsmanager_secret_version.db_secret_id.arn}:username::"
db_secret_password = "${data.aws_secretsmanager_secret_version.db_secret_id.arn}:password::"
domain_name = data.aws_ssm_parameter.domain_name.value
#pumaのタスク定義用
sg_puma_id = module.security-group.sg_puma_id
image_puma = module.ecr.puma_repository
image_puma_version = var.image_puma_version
execution_role_arn = module.iam.ecs_task_execution_role_arn
task_role_arn = module.iam.ecs_task_role_arn
cloudwatch_log_group_arn_puma = module.cloud-watch-logs.puma_log_group
tg_puma_arn = module.alb.tg_puma_arn
task_cpu_puma = var.task_cpu_puma
task_memory_puma = var.task_memory_puma
task_container_memory_reservation_puma = var.task_container_memory_reservation_puma
task_container_memory_puma = var.task_container_memory_puma
task_container_cpu_puma = var.task_container_cpu_puma
task_count_puma = var.task_count_puma
task_health_check_grace_period_seconds_puma = var.task_health_check_grace_period_seconds_puma
}
kaku-infrastuctureフォルダで、terraform init、applyを実行して、ecsモジュールに定義されたリソースを作成します。
terraform init
terraform apply
ECS execにより、DBのセットアップをする
上記の手順で、ECSにタスクが起動した状態では、DBのcreateやmigrateが実行されていないため、Railsのアプリケーションが正常に動作しません。そのためECS execを利用して、DBのセットアップを行う必要があります。
AWS CLIを利用して、ECS execを実行するために、以下のコマンドを実行します。
※事前にSSMのプラグインをインストールしておく必要があります。
aws ecs execute-command \
--region ap-northeast-1 \
--cluster kaku-puma\
--task (タスクNO) \
--container kaku-puma \
--command "bundle execrails db:create" \
--interactive
aws ecs execute-command \
--region ap-northeast-1 \
--cluster kaku-puma\
--task (タスクNO) \
--container kaku-puma \
--command "bundle execrails db:migrate" \
--interactive
必要に応じて、seedデータの投入なども行ってください。
実際にECSにタスクが起動しているか確認するために、ブラウザから独自ドメインを介して、RailsのヘルスチェックAPIにアクセスしてみてください。
https://<独自ドメイン>/api/v1/health_check
正常にアクセスできれば、DBのセットアップが完了して、ECSにタスクが正常に作用していることになります。
サービスディスカバリを利用して、SSRで呼び出すAPIのドメインを取得する
SSRでは上記で定義したALBを介さずに、サービスディスカバリにより取得したドメインを利用して、Railsコンテナと通信します。
ALBに直接アクセスするためにNat Gatewayを利用したり、ALBのローカルドメインを参照するという手法もあるのですが、構成として不自然なものになるため、サービスディスカバリを利用して、Railsコンテナと通信するようにします。
また、サービスディスカバリを利用せずとも、内部ALBを設置する自然な構成でコンテナ間の通信も実現できます。今回は、コスト面を考慮して、サービスディスカバリを利用することにします。
(また、未検証ですが、サービスディスカバリを利用することで、コンテナ間の通信が直接できるため、通信速度が速くなると考えられます。)
route53モジュールに以下の内容を追記します。
variable service_discovery_domain_name {}
variable service_discovery_sub_domain_name {}
variable vpc_id {}
route53.tfでサービスディスカバリ用のプライベートDNSのホストゾーンを作成します。また、また、サービスディスカバリを定義して、検知したIPアドレスはAレコードで登録するように設定します。
# Route53にService Discovery用のプライベートDNSのホストゾーンを作成
resource "aws_service_discovery_private_dns_namespace" "default" {
name = var.service_discovery_domain_name
vpc = var.vpc_id
}
resource "aws_service_discovery_service" "default" {
name = var.service_discovery_sub_domain_name
dns_config {
namespace_id = aws_service_discovery_private_dns_namespace.default.id
dns_records {
ttl = 10
type = "A"
}
}
}
output "service_discovery_arn" {
value = "${aws_service_discovery_service.default.arn}"
}
main.tfに以下の内容を追記します。
module "route53" {
source = "./module/route53"
alb_dns_name = module.alb.alb_dns_name
alb_zone_id = module.alb.alb_zone_id
domain_name = data.aws_ssm_parameter.domain_name.value
domain_zone_id = data.aws_route53_zone.default.zone_id
+ vpc_id = module.network.vpc_id
+ service_discovery_domain_name = var.service_discovery_domain_name
+ service_discovery_sub_domain_name = var.service_discovery_sub_domain_name
}
variable service_discovery_sub_domain_name {
default = "puma"
}
variable service_discovery_domain_name {
default = "kaku.local"
}
また、ECSのタスク定義に以下の内容を追記します。
variable service_discovery_arn {}
サービスディスカバリとして定義したARNを、ECSのタスク定義に設定して、サービスディスカバリの対象として設定します。
resource "aws_ecs_service" "service_puma" {
name = "${var.name_prefix}-puma"
launch_type = "FARGATE"
platform_version = "1.4.0"
...
+ service_registries {
+ registry_arn = var.service_discovery_arn
+ }
}
main.tfに以下の内容を追記します。
module "ecs" {
source = "./module/ecs"
...
#pumaのタスク定義用
sg_puma_id = module.security-group.sg_puma_id
image_puma = module.ecr.puma_repository
...
+ service_discovery_arn = module.route53.service_discovery_arn
}
Next.jsのDockerfileを作成、ビルドしてECRにプッシュする
Next.jsのDockerイメージをECRで管理するために、Dockerfileを作成、ビルドしてECRにプッシュします。
Dockerfileの内容は開発しているアプリケーションの内容によって異なってくると思いますが、以下に最低限必要な内容を記載した例を示します。
FROM --platform=linux/x86_64 node:18.17.1
ENV NODE_ENV production
ENV PORT 80
RUN mkdir /app
WORKDIR /app
COPY ./package.json ./
COPY ./package-lock.json ./
RUN npm install
COPY . .
COPY entrypoint.prod.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.prod.sh
ENTRYPOINT ["entrypoint.prod.sh"]
entrypoint.prod.shの内容は以下のようになります。
#!/bin/bash
set -e
#ECSタスク上でビルドする
echo "npm build"
npm run build
echo "npm start"
npm run start
注意点として、ビルドをするのはECSタスク上になります。これは、App RouterのNext.jsにおいてSCで呼び出されるAPIはno-storeを指定していても必ず実行されて、通信ができなかった場合は失敗するためです。(ビルド時にAPIの実行をスキップする方法があれば、ご教授ください。)
※本来はローカル環境でビルドすることにより、マルチステージビルドを利用して、イメージのサイズを小さくするべきですが、App Routerで実装したNext.jsをECS/Fargate上で動かす場合は、このような形式を取る必要があります。
もしくはCloud9などを利用して、ビルド時にプライベートサブネットに存在するRailsサーバに直接アクセスできるようにすることで、マルチステージビルドを実現するといった方法も考えられます。
APIを呼び出す際のドメイン名はECSのタスク定義で設定した環境変数を利用するため、必要に応じてAPIを呼び出す実装の記述を環境変数で修正してください。
CSRで利用する環境変数は「NEXT_PUBLIC_RAILS_API_URL」、SSRで利用する環境変数は「NEXT_PRIVATE_RAILS_API_URL」としています。
例としてAPIの呼び出しは以下のようなコードになることを想定しています。
// CCで呼び出す場合
const response = await fetch(process.env.NEXT_PUBLIC_RAILS_API_URL + '/v1/products');
//SC(SSR)で呼び出す場合
const response = await fetch(process.env.NEXT_PRIVATE_RAILS_API_URL + '/v1/products', { cache: 'no-store' });
上記の内容を定義したら、DockerfileをビルドしてECRにプッシュします。
Next.js用のECSモジュールの作成
ECSモジュールの設定の前にParamater StoreにAPIの呼び出し先のドメイン名を登録します。
「next-public-rails-api-url」という名前で「https://(独自ドメイン)/api」を登録。
「next-private-rails-api-url」という名前で「http://puma.kaku.local/api」を登録。これは、CloudMapのサービスディスカバリで指定したドメインです。
そしてdata.tfに以下の内容を追記します。
data "aws_ssm_parameter" "next_public_rails_api_url" {
name = "next-public-rails-api-url"
}
data "aws_ssm_parameter" "next_private_rails_api_url" {
name = "next-private-rails-api-url"
}
ECSのリソースを定義するモジュールを作成します。
module/ecsフォルダに、ecs-nodejs.tfを作成します。
kaku-infrastucture
└── module
└── ecs
├── variables.tf
├── ecs-puma.tf
└── ecs-nodejs.tf
それぞれの定義内容は以下のようになります。
variables.tfには以下の内容を追記します。
variable subnet_node_1_id {}
variable subnet_node_2_id {}
variable sg_nodejs_id {}
variable image_nodejs {}
variable image_nodejs_version {}
variable cloudwatch_log_group_arn_nodejs {}
variable tg_nodejs_arn {}
variable task_cpu_nodejs {}
variable task_memory_nodejs {}
variable task_container_memory_reservation_nodejs {}
variable task_container_memory_nodejs {}
variable task_container_cpu_nodejs {}
variable task_count_nodejs {}
variable task_health_check_grace_period_seconds_nodejs {}
ecs-nodejs.tfには以下の内容を定義します。
resource "aws_ecs_task_definition" "task_nodejs" {
family = "${var.name_prefix}-nodejs"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = "${var.task_cpu_nodejs}"
memory = "${var.task_memory_nodejs}"
execution_role_arn = "${var.execution_role_arn}"
container_definitions = jsonencode([{
name = "kaku_nodejs",
image = "${var.image_nodejs}:${var.image_nodejs_version}",
essential: true,
memoryReservation = "${var.task_container_memory_reservation_nodejs}",
memory = "${var.task_container_memory_nodejs}",
cpu = "${var.task_container_cpu_nodejs}",
portMappings: [
{
protocol: "tcp",
containerPort: 80
}
],
logConfiguration = {
logDriver = "awslogs",
options = {
"awslogs-group" = "${var.cloudwatch_log_group_arn_nodejs}",
"awslogs-region" = "ap-northeast-1",
"awslogs-stream-prefix" = "${var.name_prefix}-nodejs-task"
}
}
environment = [
{
name = "NEXT_PUBLIC_RAILS_API_URL",
value = "${var.public_rails_api_url}"
},
{
name = "NEXT_PRIVATE_RAILS_API_URL",
value = "${var.private_rails_api_url}"
}
]
}])
}
resource "aws_ecs_cluster" "cluster_nodejs" {
name = "${var.name_prefix}-nodejs"
setting {
name = "containerInsights"
value = "enabled"
}
}
resource "aws_ecs_service" "service_nodejs" {
name = "${var.name_prefix}-nodejs"
launch_type = "FARGATE"
platform_version = "1.4.0"
task_definition = aws_ecs_task_definition.task_nodejs.arn
desired_count = "${var.task_count_nodejs}"
health_check_grace_period_seconds = "${var.task_health_check_grace_period_seconds_nodejs}"
cluster = aws_ecs_cluster.cluster_nodejs.id
network_configuration {
subnets = [var.subnet_node_1_id, var.subnet_node_2_id]
security_groups = [var.sg_nodejs_id]
assign_public_ip = false
}
load_balancer {
target_group_arn = var.tg_nodejs_arn
container_name = "kaku_nodejs"
container_port = "80"
}
}
variables.tfには以下の内容を追記します。
# nodejsのタスク定義用
variable image_nodejs_version {
default = "latest"
}
variable task_cpu_nodejs {
default = 256
}
variable task_memory_nodejs {
default = 512
}
variable task_container_memory_reservation_nodejs {
default = 512
}
variable task_container_memory_nodejs {
default = 512
}
variable task_container_cpu_nodejs {
default = 256
}
variable task_count_nodejs {
default = 0
}
variable task_health_check_grace_period_seconds_nodejs {
default = 120
}
main.tfには以下の内容を追記します。
module "ecs" {
source = "./module/ecs"
,,,
+ #nodejsのタスク定義用
+ sg_nodejs_id = module.security-group.sg_nodejs_id
+ image_nodejs = module.ecr.nodejs_repository
+ image_nodejs_version = var.image_nodejs_version
+ cloudwatch_log_group_arn_nodejs = module.cloud-watch-logs.nodejs_log_group
+ tg_nodejs_arn = module.alb.tg_nodejs_arn
+ task_cpu_nodejs = var.task_cpu_nodejs
+ task_memory_nodejs = var.task_memory_nodejs
+ task_container_memory_reservation_nodejs = var.task_container_memory_reservation_nodejs
+ task_container_memory_nodejs = var.task_container_memory_nodejs
+ task_container_cpu_nodejs = var.task_container_cpu_nodejs
+ task_count_nodejs = var.task_count_nodejs
+ task_health_check_grace_period_seconds_nodejs = var.task_health_check_grace_period_seconds_nodejs
+ public_rails_api_url = data.aws_ssm_parameter.next_public_rails_api_url.value
+ private_rails_api_url = data.aws_ssm_parameter.next_private_rails_api_url.value
}
kaku-infrastuctureフォルダで、terraform init、applyを実行して、ecsモジュールに定義されたリソースを作成します。
terraform init
terraform apply
WAFモジュールの作成
WAFのリソースを定義するモジュールを作成します。
moduleフォルダの下にwafフォルダを作成します。
kaku-infrastucture
└── module
└── waf
├── variables.tf
└── waf.tf
それぞれの定義内容は以下のようになります。
variable name_prefix {}
variable tag_name {}
variable tag_group {}
variable alb_arn {}
waf.tfでWAFリソースを定義します。
XSS攻撃やSQLインジェクションを防ぐマネージドルールを追加します。
resource "aws_wafv2_web_acl" "default" {
name = "${var.name_prefix}-waf"
description = "WAF for ${var.name_prefix}"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "AWSManagedRulesCommonRuleSet"
priority = 10
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${var.name_prefix}-AWSManagedRulesCommonRuleSet"
sampled_requests_enabled = false
}
}
rule {
name = "AWSManagedRulesAmazonIpReputationList"
priority = 20
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesAmazonIpReputationList"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${var.name_prefix}-AWSManagedRulesAmazonIpReputationList"
sampled_requests_enabled = false
}
}
rule {
name = "AWSManagedRulesKnownBadInputsRuleSet"
priority = 30
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesKnownBadInputsRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${var.name_prefix}-AWSManagedRulesKnownBadInputsRuleSet"
sampled_requests_enabled = false
}
}
rule {
name = "AWSManagedRulesSQLiRuleSet"
priority = 40
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesSQLiRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${var.name_prefix}-AWSManagedRulesSQLiRuleSet"
sampled_requests_enabled = false
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${var.name_prefix}-waf"
sampled_requests_enabled = true
}
}
resource "aws_wafv2_web_acl_association" "default" {
resource_arn = "${var.alb_arn}"
web_acl_arn = aws_wafv2_web_acl.default.arn
}
kaku-infrastuctureフォルダのmain.tfに、wafモジュールを呼び出す内容を追加します。
module "waf" {
source = "./module/waf"
name_prefix = var.name_prefix
tag_name = var.tag_name
tag_group = var.tag_group
alb_arn = module.alb.alb_arn
}
kaku-infrastuctureフォルダで、terraform init、applyを実行して、route53モジュールに定義されたリソースを作成します。
terraform init
terraform apply
デプロイの確認
上記のリソースをAWSに反映すれば、アプリが正常にデプロイされます。
実際に独自ドメインを通してアクセスして確認してください。
以上となります。
参考文献
本記事は以下の書籍、記事を参考にさせていただきました。
- AWSではじめるインフラ構築入門 第2版 安全で堅牢な本番環境のつくり方
- AWSコンテナ設計・構築[本格]入門
- AWSの薄い本 IAMのマニアックな話
- AWSで実現するモダンアプリケーション入門 〜サーバーレス、コンテナ、マイクロサービスで何ができるのか
- 本番で使えるFargate環境構築
- あなたの組織に最適なECSデプロイ手法の考察
- ECS FargateでVPCエンドポイントを使用する
- 実行中のコンテナに乗り込んでコマンドを実行できる「ECS Exec」が公開されました
- modern-frontend-design-pattern
- 実践Terraform AWSにおけるシステム設計とベストプラクティス
- 【Terraform + ECS + RDS】Terraform で ECS環境構築してみた
- Terraform で ECS 環境を構築する
- Terraform で秘密情報を扱う
- Terraform で AWS に DB を構築するとき manage_master_user_password を使っていますか?
- Terraform プロジェクトの効果的なディレクトリ構成パターン
- Rails×Next.js×AWSハンズオン解説
- キャッシュで理解するNext.js App Routerのデータ取得
- 個人ブログの Next.js v13 移行でやったことまとめ
- Next.js App Router でヘルスチェックをする
- Next.js 13のApp Routerでの状態管理