2
1

Next.js(AppRouter)×RailsのAWS実行環境を、SSRを考慮して構築する【Terraform】

Last updated at Posted at 2024-01-21

概要

Next.js(App Router)とRails APIで構成されるアプリケーションの実行環境を、AWSで構築する記事です。IaCにはTerraformを利用します。

構築する環境の構成図は以下に示す通りで、アプリの実行環境はECS/Fargateを利用しています。SeverComponentからRailsで実装したAPIを呼び出す際は、CloudMapによるサービスディスカバリを利用してコンテナ間で直接通信するようにしています。
スクリーンショット 2024-01-31 21.05.56.png

※注意点

この記事では、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の内容は以下のように定義します。

.gitignore
**/.terraform/*
*.tfstate
*.tfstate.*
*.tfvars

main.tfの内容を以下のように定義します。
terraformのバージョンの指定、tfstateの保存先の設定、providerの設定(リージョンの指定)を行います。

main.tf
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の内容を以下のように、サービスのプレフィクスやタグの名前で利用する変数を定義します。

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を定義します。

module/network/variables.tf
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のリソースを定義します。

module/network/vpc.tf
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で、各サブネットのリソースを定義します。

module/network/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宛になること以外は、それぞれのサブネットのルートテーブルは内部通信のみを行うようにしています。

module/network/route.tf
#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のリソースを定義します。

module/network/internet-gateway.tf
resource "aws_internet_gateway" "default" {
  vpc_id = aws_vpc.default.id
    tags = {
    Name = "${var.tag_name}-ig"
    group = "${var.tag_group}"
  }
}

outputs.tfで、各リソースのIDを出力します。
これらの値は後から定義されるリソースにおいて参照されるものです。

module/network/outputs.tf
#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モジュールを呼び出す内容を追加します。

main.tf
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のコンテナへのリクエストを許可するセキュリティグループが必要になります、
スクリーンショット 2024-01-31 21.05.09.png

moduleフォルダの下にsecurity-groupフォルダを作成します。

kaku-infrastucture
└── module
    └── security-group
        ├── outputs.tf
        ├── variables.tf
        └── security-group.tf

それぞれの定義内容は以下のようになります。

module/security-group/variables.tf
variable name_prefix {}
variable tag_name {}
variable tag_group {}

#networkモジュールからvpc_idを受け取る
variable vpc_id {}
module/security-group/security-group.tf
# 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"]
}

module/security-group/outputs.tf
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モジュールを呼び出す内容を追加します。

main.tf
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という名前で登録します。
スクリーンショット 2024-01-08 19.59.18.png

登録したら、kaku-infrastuctureフォルダにdata.tfを作成します。

kaku-infrastucture
└── data.tf

以下のように記述することで、独自ドメインをParamater Storeから取得し、また登録されたドメインのホストゾーンと、ACM証明書をTerraformで取得することができます。この後で定義するRoute53のレコードセットの設定などで利用します。

data.tf
# 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

それぞれの定義内容は以下のようになります。

module/alb/variables.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ポートへのリダイレクトを行うようにしています。

module/alb/alb.tf
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"
    }
  }
}

module/alb/outputs.tf
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モジュールを呼び出す内容を追加します。

main.tf
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

それぞれの定義内容は以下のようになります。

module/route53/variables.tf
variable alb_dns_name {}
variable alb_zone_id {}

variable domain_name {}
variable domain_zone_id {}

route53.tfで、Route53のリソースを定義します。
独自ドメインのホストゾーンに、ALBのDNSを指定するAレコードを作成します。こうすることで、独自ドメインにブラウザからアクセスすると、ALBにリクエストが転送されるようになります。

module/route53/route53.tf
# 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モジュールを呼び出す内容を追加します。

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
}

kaku-infrastuctureフォルダで、terraform init、applyを実行して、route53モジュールに定義されたリソースを作成します。

terraform init
terraform apply

RDSモジュールの作成

RDSのリソースを定義するモジュールを作成します。

moduleフォルダの下にrdsフォルダを作成します。

kaku-infrastucture
└── module
    └── rds
        ├── outputs.tf
        ├── variables.tf
        └── rds.tf

それぞれの定義内容は以下のようになります。

module/rds/variables.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が登録されます。

module/rds/rds.tf
#サブネットグループを作成
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
 }
module/rds/outputs.tf
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モジュールを呼び出す内容を追加します。

main.tf
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

それぞれの定義内容は以下のようになります。

module/elasti-cache/variables.tf
variable name_prefix {}
variable tag_name {}
variable tag_group {}

variable vpc_id {}

redis.tfで、Redisのリソースを定義します。

module/elasti-cache/redis.tf
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]
}
module/elasti-cache/outputs.tf
output "redis_endpoint" {
  value = aws_elasticache_cluster.default.cache_nodes.0.address
}

kaku-infrastuctureフォルダのmain.tfに、ElastiCacheモジュールを呼び出す内容を追加します。

main.tf
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に以下の内容を追記します。

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

それぞれの定義内容は以下のようになります。

module/vpc-endpoint/variables.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 {}
module/vpc-endpoint/vpc-endpoint.tf
#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モジュールを呼び出す内容を追加します。

main.tf
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

それぞれの定義内容は以下のようになります。

module/ecr/variables.tf
variable name_prefix {}
variable tag_name {}
variable tag_group {}

ecr.tfで、ECRのリソースを定義します。
Rails(puma)とNext.js(Node.js)の2つのリポジトリを作成しています。また、それぞれのリポジトリに対して、ライフサイクルポリシーを設定しており、最新の3つのイメージのみを保持するようにしています。

module/ecr/ecr.tf
#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"
        }
      }
    ]
  })
}
module/ecr/outputs.tf
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モジュールを呼び出す内容を追加します。

main.tf
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の内容は開発しているアプリケーションの内容によって異なってくると思いますが、以下に最低限必要な内容を記載した例を示します。

kaku-backend/Dockerfile.prod
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タスク定義の環境変数で設定したものを参照するようにしています。

kaku-backend/config/database.yml
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タスク定義の環境変数で設定したものを参照するようにしています。

kaku-backend/config/initializers/cors.rb
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

それぞれの定義内容は以下のようになります。

iam/variables.tf
variable name_prefix {}

iam.tfで、ECSタスク実行ロールとECSタスクロールを定義します。
ECSタスク実行ロールは、ECRからのイメージの取得、CloudWatchLogsへのログの書き込み、SecretManagerからの環境変数の取得を許可するポリシーを定義しています。
ECSタスクロールは、SSMのエージェントを利用するためのポリシーを定義しています。これはECSexecを利用するために必要なポリシーです。

iam/iam.tf
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
}
iam/outputs.tf
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モジュールを呼び出す内容を追加します。

main.tf
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

それぞれの定義内容は以下のようになります。

cloud-watch-logs/variables.tf
variable name_prefix {}
cloud-watch-logs/cloud-watch-logs.tf
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
}
cloud-watch-logs/outputs.tf
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モジュールを呼び出す内容を追加します。

main.tf
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

それぞれの定義内容は以下のようになります。

module/ecs/variables.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を利用できるようにしています。これにより、コンテナ内でコマンドを実行することができます。

module/ecs/ecs-puma.tf
#バックエンドコンテナ用のタスク定義
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にプッシュしたイメージのバージョンを指定してください。

variables.tf
# 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
}
main.tf
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モジュールに以下の内容を追記します。

module/route53/variables.tf
variable service_discovery_domain_name {}
variable service_discovery_sub_domain_name {}
variable vpc_id {}

route53.tfでサービスディスカバリ用のプライベートDNSのホストゾーンを作成します。また、また、サービスディスカバリを定義して、検知したIPアドレスはAレコードで登録するように設定します。

module/route53/route53.tf
# 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"
    }
  }
}
module/route53/outputs.tf
output "service_discovery_arn" {
  value = "${aws_service_discovery_service.default.arn}"
}

main.tfに以下の内容を追記します。

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
}
variables.tf
variable service_discovery_sub_domain_name {
  default = "puma"
}
variable service_discovery_domain_name {
  default = "kaku.local"
}

また、ECSのタスク定義に以下の内容を追記します。

module/ecs/variables.tf
variable service_discovery_arn {}

サービスディスカバリとして定義したARNを、ECSのタスク定義に設定して、サービスディスカバリの対象として設定します。

module/ecs/ecs-puma.tf
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に以下の内容を追記します。

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の内容は開発しているアプリケーションの内容によって異なってくると思いますが、以下に最低限必要な内容を記載した例を示します。

kaku-frontend/Dockerfile.prod
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の内容は以下のようになります。

kaku-frontend/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.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には以下の内容を追記します。

module/ecs/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には以下の内容を定義します。

module/ecs/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には以下の内容を追記します。

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には以下の内容を追記します。

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

それぞれの定義内容は以下のようになります。

module/waf/variables.tf
variable name_prefix {}
variable tag_name {}
variable tag_group {}

variable alb_arn {}

waf.tfでWAFリソースを定義します。
XSS攻撃やSQLインジェクションを防ぐマネージドルールを追加します。

module/waf/waf.tf
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モジュールを呼び出す内容を追加します。

main.tf
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に反映すれば、アプリが正常にデプロイされます。
実際に独自ドメインを通してアクセスして確認してください。

以上となります。

参考文献

本記事は以下の書籍、記事を参考にさせていただきました。

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1