RailsのAPIを載せるステージングと本番用のFargate環境をTerraformで作成する機会があったので自分用のメモも兼ねて記事を書くことにしました。
Reactを載せるためのS3などもTerraformで作っております。
利用しているTerraformのバージョンは1.2.0です。
Workspacesを使うかや、Terraform Registryのモジュールを使うかなど悩みましたが、
ひとまず自分の現段階でいいと思えた構成にしています。
ベストプラクティスを示しているわけではないので、より良くなるアドバイスがあれば優しく教えていただけると幸いです。
リポジトリはこちらです
https://github.com/hatsu38/rails-nginx-fargate-infra-template
作る環境
S3とCloudFrontはReactを載せる用のリソースです。
フォルダ構成
environmentsフォルダにstagingとproductionを置いて環境を分けることにしました。
多くのリソースはオリジナルのモジュールを用いて作っています。モジュールはmodulesフォルダに置いています。
Frontend(React)の環境とBackend(Rails API)の環境は互いに影響させたくないため、フォルダを分けました。
変数やproviderを管理するファイルはsharedに置き各フォルダでシンボリックリンクを貼ることで共通化しました。
ファイルの中身については別途記載していきます。
├── README.md
├── environments
│ ├── production
│ │ ├── 10_frontend
│ │ │ ├── main.tf
│ │ │ ├── provider.tf -> ../../../shared/provider.tf
│ │ │ ├── terraform.tfvars
│ │ │ ├── tfstate_backend.tf
│ │ │ ├── variable.tf -> ../../../shared/variable.tf
│ │ │ └── version.tf -> ../../../shared/version.tf
│ │ └── 20_backend
│ │ ├── alb.tf
│ │ ├── ecr.tf
│ │ ├── ecs.tf
│ │ ├── provider.tf -> ../../../shared/provider.tf
│ │ ├── rds.tf
│ │ ├── route53.tf
│ │ ├── ses.tf
│ │ ├── terraform.tfvars
│ │ ├── tfstate_backend.tf
│ │ ├── variable.tf -> ../../../shared/variable.tf
│ │ ├── version.tf -> ../../../shared/version.tf
│ │ └── vpc.tf
│ └── staging
│ ├── 10_frontend
│ │ ├── main.tf
│ │ ├── provider.tf -> ../../../shared/provider.tf
│ │ ├── terraform.tfvars
│ │ ├── tfstate_backend.tf
│ │ ├── variable.tf -> ../../../shared/variable.tf
│ │ └── version.tf -> ../../../shared/version.tf
│ └── 20_backend
│ ├── alb.tf
│ ├── ecr.tf
│ ├── ecs.tf
│ ├── provider.tf -> ../../../shared/provider.tf
│ ├── rds.tf
│ ├── route53.tf
│ ├── ses.tf
│ ├── terraform.tfvars
│ ├── tfstate_backend.tf
│ ├── variable.tf -> ../../../shared/variable.tf
│ ├── version.tf -> ../../../shared/version.tf
│ └── vpc.tf
├── modules
│ ├── alb
│ │ ├── main.tf
│ │ ├── output.tf
│ │ └── variable.tf
│ ├── ecr
│ │ ├── ecr_lifecycle_policy.json
│ │ ├── main.tf
│ │ ├── output.tf
│ │ └── variable.tf
│ ├── ecs
│ │ ├── main.tf
│ │ ├── output.tf
│ │ ├── task_definitions.tpl.json
│ │ └── variable.tf
│ ├── frontend
│ │ ├── main.tf
│ │ ├── output.tf
│ │ ├── provider.tf
│ │ └── variable.tf
│ ├── iam_role
│ │ ├── main.tf
│ │ ├── output.tf
│ │ └── variable.tf
│ ├── rds
│ │ ├── main.tf
│ │ ├── output.tf
│ │ └── variable.tf
│ ├── route53
│ │ ├── main.tf
│ │ ├── output.tf
│ │ └── variable.tf
│ ├── security_group
│ │ ├── main.tf
│ │ ├── output.tf
│ │ └── variable.tf
│ ├── ses
│ │ ├── main.tf
│ │ ├── output.tf
│ │ └── variable.tf
│ ├── subnet
│ │ ├── main.tf
│ │ ├── output.tf
│ │ └── variable.tf
│ └── vpc
│ ├── main.tf
│ ├── output.tf
│ └── variable.tf
└── shared
├── provider.tf
├── variable.tf
└── version.tf
事前準備
terraform applyする前にAWSコンソールで準備しておく作業があります、
- AWS tfstateを保存するためのS3バケットを作成しておきます
- staging-example-resource-tfstate、production-example-resource-tfstateの名前を利用しています。
- terraform コマンドが使えるIAMを作成しておきます。作ったIAMは.aws/configに設定してterraform applyなどに利用します
- Route53でRootドメインのホストゾーンを作成しておきます(本当はterraformで管理したい)
1. ステージングやAPIで利用するサーバのドメインにサブドメインを設定する予定です。
1. 例)以後のterraformリソースではapi.example.com, staging.example.com, staging-api.example.comのようなサブドメインを設定していく予定です - 同RootドメインのACM証明書を作成しておきます
Sharedフォルダ
applyする各フォルダで共通に利用するファイル群をおくフォルダです。
利用するのは以下の3つのファイルです。
version.tf
required_versionとrequired_providersのみを記載するファイルです。
terraform {
required_version = "~> 1.2.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 4.0"
}
}
}
provider.tf
provider "aws" {
region = "ap-northeast-1"
# .aws/configのprofile名
profile = "example-terraform"
default_tags {
tags = local.tags
}
}
variable.tf
各リソースで利用する共通の変数名を置いておくファイル
variable "environment" {
description = "environment common_name"
type = string
}
variable "service" {
description = "service name"
type = string
}
locals {
common_name = "${var.environment}-${var.service}"
# 各サービスが使うルートドメインを記載する
root_domain = "example.com"
tags = {
# terraformで作られたものと明記するため
Managed = "terraform"
# stagingかproductionかを識別する
Environment = var.environment
# アプリケーション名が入る
Service = var.service
# staging-example-backendとかstaging-example-frontendとか
Name = local.common_name
}
}
フロントエンド
10_frontned
provider.tf, variable.tf, version.tfはシンボリックリンクです。
静的ファイルを置くためのS3のバケットと、staging.example.com というドメインのAレコード、CloudFrontでS3とドメインの紐付けを行なっています
└── staging
├── 10_frontend
│ ├── main.tf
│ ├── provider.tf -> ../../../shared/provider.tf
│ ├── terraform.tfvars
│ ├── tfstate_backend.tf
│ ├── variable.tf -> ../../../shared/variable.tf
│ └── version.tf -> ../../../shared/version.tf
terraform.tfvars
タグにつける変数名を記載します
environment = "staging"
service = "example-frontend"
tfstate_backend.tf
tfstateを管理するS3を記載します
terraform {
backend "s3" {
bucket = "staging-example-resource-tfstate"
key = "frontend/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
}
}
main.tf
Frontend(React)を置いてドメインを割り振るための各種リソースを作成する
module "frontend" {
source = "./../../../modules/frontend"
common_name = local.common_name
site_domain = aws_route53_zone.subdomain.name
site_zone_id = aws_route53_zone.subdomain.zone_id
root_domain = local.root_domain
}
data "aws_route53_zone" "root_domain" {
name = local.root_domain
private_zone = false
}
resource "aws_route53_zone" "subdomain" {
# NOTE: ステージングではサブドメインにstagingをつける
name = "staging.example.com"
}
# NSレコードの作成
resource "aws_route53_record" "ns" {
zone_id = data.aws_route53_zone.root_domain.zone_id
name = aws_route53_zone.subdomain.name
type = "NS"
ttl = "30"
records = aws_route53_zone.subdomain.name_servers
}
本番環境用もほとんど同じですので折りたたみで置いておきます。
本番環境はサブドメイン不要(example.com)なので、route53のホストゾーンの作成などは行いません。
詳細はこちら↓
production/10_frontend/**.tf
└── production
├── 10_frontend
│ ├── main.tf
│ ├── provider.tf -> ../../../shared/provider.tf
│ ├── terraform.tfvars
│ ├── tfstate_backend.tf
│ ├── variable.tf -> ../../../shared/variable.tf
│ └── version.tf -> ../../../shared/version.tf
terraform.tfvars
environment = "production"
service = "example-frontend"
tfstate_backend.tf
terraform {
backend "s3" {
bucket = "production-example-resource-tfstate"
key = "frontend/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
}
}
main.tf
module "frontend" {
source = "./../../../modules/frontend"
common_name = local.common_name
site_domain = local.root_domain
site_zone_id = data.aws_route53_zone.root_domain.zone_id
root_domain = local.root_domain
}
data "aws_route53_zone" "root_domain" {
name = local.root_domain
private_zone = false
}
modules/frontend
frontendのリソースを作成するためのモジュールです
modules/frontend
├── main.tf
├── output.tf
├── provider.tf
└── variable.tf
provider.tf
provider "aws" {
region = "us-east-1"
alias = "virginia"
}
variable.tf
variable "common_name" {
description = "common name"
type = string
}
variable "site_domain" {
description = "frontend domain"
type = string
}
variable "site_zone_id" {
description = "frontend domain"
type = string
}
variable "root_domain" {
description = "root domain"
type = string
}
output.tf
# 空
main.tf
# ================
# S3 Bucket
# ================
resource "aws_s3_bucket" "main" {
bucket = var.common_name
}
# バケットはprivateにする
resource "aws_s3_bucket_acl" "main" {
bucket = aws_s3_bucket.main.id
acl = "private"
}
# S3のバージョニングを有効にする
resource "aws_s3_bucket_versioning" "main" {
bucket = aws_s3_bucket.main.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_website_configuration" "main" {
bucket = aws_s3_bucket.main.bucket
index_document {
suffix = "index.html"
}
error_document {
key = "index.html"
}
}
# S3のバケットポリシーを設定
resource "aws_s3_bucket_policy" "bucket" {
bucket = aws_s3_bucket.main.id
policy = data.aws_iam_policy_document.static-www.json
}
data "aws_iam_policy_document" "static-www" {
statement {
sid = "Allow CloudFront"
effect = "Allow"
principals {
type = "AWS"
identifiers = [aws_cloudfront_origin_access_identity.static-www.iam_arn]
}
actions = [
"s3:GetObject"
]
resources = [
"${aws_s3_bucket.main.arn}/*"
]
}
}
# ================
# Cloudfront
# ================
resource "aws_cloudfront_distribution" "static-www" {
# マルチテナントのサービスのためhoge.staging.example.comでも使えるように
# staging.example.com, *.staging.example.comを有効にする
aliases = ["${var.site_domain}", "*.${var.site_domain}"]
origin {
domain_name = aws_s3_bucket.main.bucket_regional_domain_name
origin_id = aws_s3_bucket.main.id
s3_origin_config {
origin_access_identity = aws_cloudfront_origin_access_identity.static-www.cloudfront_access_identity_path
}
}
enabled = true
is_ipv6_enabled = true
comment = var.site_domain
default_root_object = "index.html"
custom_error_response {
# CloudFrontがオリジンにクエリを実行してオブジェクトが更新されているかどうかを確認する前に、
# HTTPエラーコードをCloudFrontキャッシュに保持する最小時間。
error_caching_min_ttl = 360 #(任意)
# 4xx か 5xxを記入。
error_code = 403
# CFがカスタムエラーページとともにビューアに返すHTTPステータスコード。
response_code = 200
# The path of the custom error page (for example,/custom_404.html).
response_page_path = "/index.html"
}
default_cache_behavior {
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
target_origin_id = aws_s3_bucket.main.id
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
viewer_protocol_policy = "redirect-to-https"
min_ttl = 0
default_ttl = 3600
max_ttl = 86400
}
restrictions {
geo_restriction {
restriction_type = "whitelist"
locations = ["JP"]
}
}
viewer_certificate {
cloudfront_default_certificate = false
acm_certificate_arn = aws_acm_certificate.main.arn
ssl_support_method = "sni-only"
minimum_protocol_version = "TLSv1"
}
}
resource "aws_cloudfront_origin_access_identity" "static-www" {
comment = var.site_domain
}
# ================
# Route53 Host Zone
# ================
data "aws_route53_zone" "root_domain" {
name = var.root_domain
private_zone = false
}
# ================
# Route53 Record
# ================
# *.staging.example.comのAレコードを作成
resource "aws_route53_record" "a_wildcard" {
zone_id = var.site_zone_id
name = "*.${var.site_domain}"
type = "A"
alias {
name = aws_cloudfront_distribution.static-www.domain_name
zone_id = aws_cloudfront_distribution.static-www.hosted_zone_id
evaluate_target_health = false
}
}
# staging.example.comのAレコードを作成
resource "aws_route53_record" "a" {
zone_id = var.site_zone_id
name = var.site_domain
type = "A"
alias {
name = aws_cloudfront_distribution.static-www.domain_name
zone_id = aws_cloudfront_distribution.static-www.hosted_zone_id
evaluate_target_health = false
}
}
# ================
# ACM SSL証明書
# ================
resource "aws_acm_certificate" "main" {
provider = aws.virginia
domain_name = var.site_domain
subject_alternative_names = ["*.${var.site_domain}"]
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
resource "aws_route53_record" "certificate" {
for_each = {
for domain_vlidation_option in aws_acm_certificate.main.domain_validation_options : domain_vlidation_option.domain_name => {
name = domain_vlidation_option.resource_record_name
record = domain_vlidation_option.resource_record_value
type = domain_vlidation_option.resource_record_type
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
type = each.value.type
zone_id = var.site_zone_id
ttl = 60
}
resource "aws_acm_certificate_validation" "main" {
provider = aws.virginia
certificate_arn = aws_acm_certificate.main.arn
validation_record_fqdns = [for record in aws_route53_record.certificate : record.fqdn]
}
バックエンド
作るリソースごとにファイルは分けています
environments/staging/20_backend
├── alb.tf
├── ecr.tf
├── ecs.tf
├── provider.tf -> ../../../shared/provider.tf
├── rds.tf
├── route53.tf
├── ses.tf
├── terraform.tfvars
├── tfstate_backend.tf
├── variable.tf -> ../../../shared/variable.tf
├── version.tf -> ../../../shared/version.tf
└── vpc.tf
terraform.tfvars
environment = "staging"
service = "example-backend"
tfstate_backend.tf
terraform {
backend "s3" {
bucket = "staging-example-resource-tfstate"
key = "backend/terraform.tfstate"
region = "ap-northeast-1"
encrypt = true
}
}
vpc.tf
VPCやサブネットやInternetGatewayなどを作成していきます。
module "vpc" {
source = "./../../../modules/vpc"
cidr_block = "172.16.0.0/16"
}
module "subnet" {
source = "./../../../modules/subnet"
vpc_id = module.vpc.id
vpc_cidr_block = module.vpc.cidr_block
}
modules/vpc
VPCを作成するモジュールです
modules/vpc
├── main.tf
├── output.tf
└── variable.tf
modules/vpc/variable.tf
variable "name_tag" {
default = null
description = "Name Tag"
type = string
nullable = true
}
variable "cidr_block" {
description = "cidr_block"
type = string
}
modules/vpc/output.tf
output "id" {
value = aws_vpc.main.id
}
output "cidr_block" {
value = aws_vpc.main.cidr_block
}
modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.cidr_block
enable_dns_support = true
enable_dns_hostnames = true
tags = {
Name = var.name_tag
}
}
modules/subnet
サブネット、インターネットゲートウェイ、ルートテーブル、Elastic IP、NAT Gatewayなどを作成していきます。
module/subnet/variable.tf
variable "availability_zones" {
type = list(string)
# 3つのアベイラビリティーゾーンを指定
default = ["ap-northeast-1a", "ap-northeast-1c", "ap-northeast-1d"]
description = "Availability Zone List"
}
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "vpc_cidr_block" {
description = "VPC CIDR BLOCK"
type = string
}
module/subnet/output.tf
output "public_subnet_ids" {
value = toset([
for subnet in aws_subnet.public : subnet.id
])
}
output "private_subnet_ids" {
value = toset([
for subnet in aws_subnet.private : subnet.id
])
}
module/subnet/main.tf
# ================
# Subnet
# ================
resource "aws_subnet" "public" {
for_each = toset(var.availability_zones)
vpc_id = var.vpc_id
availability_zone = each.value
cidr_block = cidrsubnet(var.vpc_cidr_block, 8, index(var.availability_zones, each.value))
map_public_ip_on_launch = true
tags = {
Public = true
Zone = each.value
}
}
resource "aws_subnet" "private" {
for_each = toset(var.availability_zones)
vpc_id = var.vpc_id
availability_zone = each.value
cidr_block = cidrsubnet(var.vpc_cidr_block, 8, index(var.availability_zones, each.value) + length(aws_subnet.public))
tags = {
Private = true
Zone = each.value
}
}
# ================
# Internet Gateway
# ================
resource "aws_internet_gateway" "main" {
vpc_id = var.vpc_id
}
# ================
# Route Table
# ================
resource "aws_route_table" "public" {
vpc_id = var.vpc_id
tags = {
Public = true
}
}
resource "aws_route_table" "private" {
for_each = toset(var.availability_zones)
vpc_id = var.vpc_id
tags = {
Private = true
Zone = each.value
}
}
# ================
# Elascic IP
# ================
resource "aws_eip" "main" {
for_each = toset(var.availability_zones)
vpc = true
tags = {
Zone = each.value
}
}
# ================
# NatGateway
# ================
resource "aws_nat_gateway" "main" {
for_each = toset(var.availability_zones)
allocation_id = aws_eip.main[each.value].id
subnet_id = aws_subnet.public[each.value].id
depends_on = [aws_internet_gateway.main]
tags = {
Zone = each.value
}
}
# ================
# Route
# ================
####### Public #######
resource "aws_route" "public" {
route_table_id = aws_route_table.public.id
gateway_id = aws_internet_gateway.main.id
destination_cidr_block = "0.0.0.0/0"
}
####### Private #######
resource "aws_route" "private" {
for_each = toset(var.availability_zones)
route_table_id = aws_route_table.private[each.value].id
nat_gateway_id = aws_nat_gateway.main[each.value].id
destination_cidr_block = "0.0.0.0/0"
}
# ================
# Route Table Association
# ================
####### Public #######
resource "aws_route_table_association" "public" {
for_each = toset(var.availability_zones)
subnet_id = aws_subnet.public[each.value].id
route_table_id = aws_route_table.public.id
}
####### Private #######
resource "aws_route_table_association" "private" {
for_each = toset(var.availability_zones)
subnet_id = aws_subnet.private[each.value].id
route_table_id = aws_route_table.private[each.value].id
}
route53.tf
Route53のホストゾーンやAレコードやNSレコード、SSL証明書を作成します。
module "route53" {
source = "./../../../modules/route53"
root_domain = local.root_domain
subdomain = "staging-api.example.com"
common_name = local.common_name
a_recod_alias_name = module.alb.dns_name
a_record_alias_zone_id = module.alb.zone_id
}
# hoge.staging-api.example.comも有効にしたいのでワイルドカードも指定
resource "aws_route53_record" "a_wildcard" {
zone_id = module.route53.subdomain_zone_id
name = "*.staging-api.example.com"
type = "A"
alias {
name = module.alb.dns_name
zone_id = module.alb.zone_id
evaluate_target_health = true
}
}
modules/route53
modules/route53/variable.tf
variable "root_domain" {
description = "root_domain"
type = string
}
variable "subdomain" {
description = "subdomain"
type = string
}
variable "common_name" {
description = "common_name"
type = string
}
variable "a_recod_alias_name" {
description = "A Record Alias Name"
type = string
}
variable "a_record_alias_zone_id" {
description = "A Record Alias Zone Id"
type = string
}
modules/route53/output.tf
output "acm_arn" {
value = aws_acm_certificate.main.arn
}
output "acm_arn_logging" {
value = {}
depends_on = [aws_acm_certificate_validation.main]
}
output "subdomain_zone_id" {
value = aws_route53_zone.subdomain.zone_id
}
modules/route53/main.tf
ホストゾーン、Aレコード、NSレコード、SSL証明書の作成を行うモジュールです
# ================
# Host Zone
# ================
data "aws_route53_zone" "root_domain" {
name = var.root_domain
private_zone = false
}
resource "aws_route53_zone" "subdomain" {
name = var.subdomain
}
# ================
# Route53 Record
# ================
resource "aws_route53_record" "a" {
zone_id = aws_route53_zone.subdomain.zone_id
name = aws_route53_zone.subdomain.name
type = "A"
alias {
name = var.a_recod_alias_name
zone_id = var.a_record_alias_zone_id
evaluate_target_health = true
}
}
resource "aws_route53_record" "ns" {
zone_id = data.aws_route53_zone.root_domain.zone_id
name = aws_route53_zone.subdomain.name
type = "NS"
ttl = "30"
records = aws_route53_zone.subdomain.name_servers
}
# ================
# ACM SSL証明書
# ================
resource "aws_acm_certificate" "main" {
domain_name = aws_route53_zone.subdomain.name
subject_alternative_names = ["*.${aws_route53_zone.subdomain.name}"]
validation_method = "DNS"
lifecycle {
create_before_destroy = true
}
}
resource "aws_route53_record" "certificate" {
for_each = {
for domain_vlidation_option in aws_acm_certificate.main.domain_validation_options : domain_vlidation_option.domain_name => {
name = domain_vlidation_option.resource_record_name
record = domain_vlidation_option.resource_record_value
type = domain_vlidation_option.resource_record_type
}
}
allow_overwrite = true
name = each.value.name
records = [each.value.record]
type = each.value.type
zone_id = aws_route53_zone.subdomain.id
ttl = 60
}
resource "aws_acm_certificate_validation" "main" {
certificate_arn = aws_acm_certificate.main.arn
validation_record_fqdns = [for record in aws_route53_record.certificate : record.fqdn]
}
ecr.tf
Rails用のECRとNginx用のECRを作ります
module "rails_ecr" {
source = "./../../../modules/ecr"
repository_name = "${local.common_name}-rails"
}
module "nginx_ecr" {
source = "./../../../modules/ecr"
repository_name = "${local.common_name}-nginx"
}
modules/ecr
modules/ecr/variable.tf
variable "repository_name" {
description = "ECR Repository Name"
type = string
}
modules/ecr/output.tf
output "arn" {
value = aws_ecr_repository.main.arn
}
modules/ecr/ecr_lifecycle_policy.json
{
"rules": [
{
"rulePriority": 1,
"description": "ECR30個まで保持する",
"selection": {
"tagStatus": "tagged",
"tagPrefixList": ["release"],
"countType": "imageCountMoreThan",
"countNumber": 30
},
"action": {
"type": "expire"
}
}
]
}
modules/ecr/main.tf
ECRを作成するモジュールです
# ================
# ECR
# ================
resource "aws_ecr_repository" "main" {
name = var.repository_name
image_tag_mutability = "IMMUTABLE"
image_scanning_configuration {
scan_on_push = true
}
tags = {
Name = var.repository_name
}
}
resource "aws_ecr_lifecycle_policy" "main" {
repository = aws_ecr_repository.main.name
policy = file("${path.module}/ecr_lifecycle_policy.json")
}
alb.tf
ロードバランサーを作成します。
module "alb" {
source = "./../../../modules/alb/"
common_name = local.common_name
vpc_id = module.vpc.id
public_subnet_ids = module.subnet.public_subnet_ids
aws_acm_certificate_arn = module.route53.acm_arn
acm_depends_on = [module.route53.acm_arn_logging]
}
modules/alb
modules/alb/variable.tf
variable "common_name" {
description = "common_name"
type = string
}
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "public_subnet_ids" {
description = "Public Subnet Ids Form DB Subnet Group"
type = list(string)
}
variable "aws_acm_certificate_arn" {
description = "ACM Arn"
type = string
}
variable "acm_depends_on" {
description = "ACM depends_on"
type = any
}
modules/alb/output.tf
output "target_group_arn" {
value = aws_lb_target_group.main.arn
}
output "dns_name" {
value = aws_lb.main.dns_name
}
output "zone_id" {
value = aws_lb.main.zone_id
}
modules/alb/main.tf
ロードバランサーの作成とロードバランサーのログを保持するS3のバケットを作成します
data "aws_elb_service_account" "main" {}
# ================
# Load Balancer
# ================
resource "aws_lb" "main" {
name = var.common_name
internal = false
load_balancer_type = "application"
idle_timeout = 60
enable_deletion_protection = true
subnets = var.public_subnet_ids
access_logs {
bucket = aws_s3_bucket.alb_log.bucket
enabled = true
}
security_groups = [
module.http_security_group.security_group_id,
module.https_security_group.security_group_id,
module.http_redirect_security_group.security_group_id,
]
}
# ================
# Target Group
# ================
resource "aws_lb_target_group" "main" {
name = var.common_name
vpc_id = var.vpc_id
port = 80
target_type = "ip"
protocol = "HTTP"
deregistration_delay = 300
health_check {
path = "/api/health_checks"
healthy_threshold = 5
unhealthy_threshold = 2
timeout = 5
interval = 30
matcher = 200
port = "traffic-port"
protocol = "HTTP"
}
depends_on = [
aws_lb.main
]
}
# ================
# S3
# ================
### Log Bucket
resource "aws_s3_bucket" "alb_log" {
bucket = "${var.common_name}-alb-log"
}
### ACL
resource "aws_s3_bucket_acl" "alb_log" {
bucket = aws_s3_bucket.alb_log.id
acl = "private"
}
### LifeCycle
resource "aws_s3_bucket_lifecycle_configuration" "alb_log" {
bucket = aws_s3_bucket.alb_log.id
rule {
id = "${var.common_name}-alb-log"
filter {
prefix = "logs/"
}
expiration {
days = 90
}
status = "Enabled"
}
}
# ================
# S3 Bucket Policy
# ================
resource "aws_s3_bucket_policy" "alb_log" {
bucket = aws_s3_bucket.alb_log.id
policy = data.aws_iam_policy_document.alb_log.json
}
data "aws_iam_policy_document" "alb_log" {
statement {
effect = "Allow"
actions = ["s3:PutObject"]
resources = ["arn:aws:s3:::${aws_s3_bucket.alb_log.id}/*"]
principals {
type = "AWS"
identifiers = [data.aws_elb_service_account.main.arn]
}
}
}
# ================
# Security Group
# ================
module "http_security_group" {
source = "./../security_group/"
security_group_name = "${var.common_name}-http"
vpc_id = var.vpc_id
port = 80
cidr_blocks = ["0.0.0.0/0"]
}
module "https_security_group" {
source = "./../security_group/"
security_group_name = "${var.common_name}-https"
vpc_id = var.vpc_id
port = 443
cidr_blocks = ["0.0.0.0/0"]
}
module "http_redirect_security_group" {
source = "./../security_group/"
security_group_name = "${var.common_name}-http_redirect"
vpc_id = var.vpc_id
port = 3000
cidr_blocks = ["0.0.0.0/0"]
}
# ================
# HTTP LoadBalancer Listener
# ================
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = 80
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
status_code = "HTTP_301"
path = "/*"
protocol = "HTTPS"
port = 443
}
}
}
resource "aws_lb_listener_rule" "http" {
listener_arn = aws_lb_listener.http.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.main.arn
}
condition {
path_pattern {
values = ["/*"]
}
}
}
# ================
# HTTPS LoadBalancer Listener
# ================
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = 443
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-2016-08"
certificate_arn = var.aws_acm_certificate_arn
default_action {
type = "fixed-response"
fixed_response {
content_type = "text/plain"
status_code = 200
}
}
depends_on = [var.acm_depends_on]
}
resource "aws_lb_listener_rule" "https" {
listener_arn = aws_lb_listener.https.arn
priority = 100
action {
type = "forward"
target_group_arn = aws_lb_target_group.main.arn
}
condition {
path_pattern {
values = ["/*"]
}
}
}
rds.tf
RDSを作成します。今回はPostgresで作成を行なっています
module "rds" {
source = "./../../../modules/rds/"
common_name = local.common_name
parameter_group_family = "postgres14"
engine = "postgres"
major_engine_version = 14
engine_version = 14.2
db_instance_class = "db.t3.small"
db_name = local.common_name
db_user_name = var.service
multi_az = true
port = 5432
vpc_id = module.vpc.id
cidr_blocks = [module.vpc.cidr_block]
private_subnet_ids = module.subnet.private_subnet_ids
enabled_cloudwatch_logs_exports = ["postgresql", "upgrade"]
}
modules/rds
modules/rds/variable.tf
variable "common_name" {
description = "common_name"
type = string
}
variable "parameter_group_family" {
description = "DB Parameter Group Faimly"
type = string
}
variable "engine" {
description = "DB Engine"
type = string
}
variable "major_engine_version" {
description = "DB Engine"
type = number
}
variable "engine_version" {
description = "DB Engine"
type = number
}
variable "db_instance_class" {
description = "DB Instance Class"
type = string
}
variable "db_name" {
description = "DB Name"
type = string
}
variable "db_user_name" {
description = "DB User Name"
type = string
}
variable "multi_az" {
description = "DB Multi AZ"
type = bool
}
variable "port" {
description = "DB Port"
type = number
}
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "private_subnet_ids" {
description = "Private Subnet Ids Form DB Subnet Group"
type = list(string)
}
variable "cidr_blocks" {
description = "Security Group Cidr Blocks"
type = list(string)
}
variable "enabled_cloudwatch_logs_exports" {
description = "enabled_cloudwatch_logs_exports"
type = list(string)
}
modules/rds/output.tf
output "ssm_db_password_path" {
value = aws_ssm_parameter.db_password.name
}
output "ssm_db_username_path" {
value = aws_ssm_parameter.db_username.name
}
output "ssm_db_port_path" {
value = aws_ssm_parameter.db_port.name
}
output "ssm_db_host_path" {
value = aws_ssm_parameter.db_host.name
}
output "ssm_db_name_path" {
value = aws_ssm_parameter.db_name.name
}
modules/rds/main.tf
RDSを作成するモジュールです。
DBのパスワードやDB名、ホストなどをaws_ssm_parameterに埋め込んでいます。
# ================
# DB Parameter Group
# ================
resource "aws_db_parameter_group" "main" {
name = var.common_name
family = var.parameter_group_family
}
# ================
# DB Option Group
# ================
resource "aws_db_option_group" "main" {
name = var.common_name
engine_name = var.engine
major_engine_version = var.major_engine_version
}
# ================
# DB Subnet Group
# ================
resource "aws_db_subnet_group" "main" {
name = var.common_name
subnet_ids = var.private_subnet_ids
}
# ================
# DB Instance
# ================
resource "aws_db_instance" "main" {
identifier = var.common_name
db_name = replace(var.db_name, "-", "_")
engine = var.engine
engine_version = var.engine_version
instance_class = var.db_instance_class
allocated_storage = 20
max_allocated_storage = 100
storage_type = "gp2"
storage_encrypted = true
username = replace(var.db_user_name, "-", "_")
password = random_password.db.result
multi_az = var.multi_az
publicly_accessible = false
backup_window = "09:10-09:40"
backup_retention_period = 30
maintenance_window = "mon:10:10-mon:10:40"
auto_minor_version_upgrade = true
deletion_protection = true
skip_final_snapshot = false
final_snapshot_identifier = "${var.common_name}-snapshot"
port = var.port
apply_immediately = false
monitoring_interval = 60
monitoring_role_arn = "arn:aws:iam::871107023173:role/rds-monitoring-role"
vpc_security_group_ids = [module.db_security_group.security_group_id]
parameter_group_name = aws_db_parameter_group.main.name
option_group_name = aws_db_option_group.main.name
db_subnet_group_name = aws_db_subnet_group.main.name
enabled_cloudwatch_logs_exports = var.enabled_cloudwatch_logs_exports
lifecycle {
ignore_changes = [password]
}
}
resource "random_password" "db" {
length = 10
special = false
}
# ================
# DB Security Group
# ================
module "db_security_group" {
source = "./../security_group/"
security_group_name = "${var.common_name}-db-security_group"
vpc_id = var.vpc_id
port = var.port
cidr_blocks = var.cidr_blocks
}
# ================
# SSM
# ================
resource "aws_ssm_parameter" "db_username" {
name = "/${var.common_name}/db/username"
type = "SecureString"
value = aws_db_instance.main.username
description = "データーベースユーザー名"
lifecycle {
ignore_changes = [value]
}
}
resource "aws_ssm_parameter" "db_password" {
name = "/${var.common_name}/db/password"
type = "SecureString"
value = aws_db_instance.main.password
description = "データーベースパスワード"
lifecycle {
ignore_changes = [value]
}
}
resource "aws_ssm_parameter" "db_port" {
name = "/${var.common_name}/db/port"
type = "SecureString"
value = aws_db_instance.main.port
description = "データーベースポート"
lifecycle {
ignore_changes = [value]
}
}
resource "aws_ssm_parameter" "db_host" {
name = "/${var.common_name}/db/host"
type = "SecureString"
value = replace(aws_db_instance.main.endpoint, ":${var.port}", "")
description = "データーベースホスト"
lifecycle {
ignore_changes = [value]
}
}
resource "aws_ssm_parameter" "db_name" {
name = "/${var.common_name}/db/name"
type = "SecureString"
value = aws_db_instance.main.name
description = "データーベース名"
lifecycle {
ignore_changes = [value]
}
}
ecs.tf
ECSクラスターやECSタスクを作成します
module "ecs" {
source = "./../../../modules/ecs/"
common_name = local.common_name
vpc_id = module.vpc.id
cidr_blocks = [module.vpc.cidr_block]
private_subnet_ids = module.subnet.private_subnet_ids
target_group_arn = module.alb.target_group_arn
desired_count = 1
cpu = 256
memory = 512
ecs_rails_tag = "LATEST"
ecs_nginx_tag = "LATEST"
rails_ecr_arn = module.rails_ecr.arn
nginx_ecr_arn = module.nginx_ecr.arn
ssm_db_password_path = module.rds.ssm_db_password_path
ssm_db_username_path = module.rds.ssm_db_username_path
ssm_db_port_path = module.rds.ssm_db_port_path
ssm_db_host_path = module.rds.ssm_db_host_path
ssm_db_name_path = module.rds.ssm_db_name_path
ssm_rails_master_key_path = data.aws_ssm_parameter.rails_master_key.name
environment = var.environment
}
data "aws_ssm_parameter" "rails_master_key" {
name = "/${local.common_name}/rails-master-key"
}
modules/ecs
modules/ecs/variable.tf
variable "common_name" {
description = "common_name"
type = string
}
variable "vpc_id" {
description = "VPC ID"
type = string
}
variable "cidr_blocks" {
description = "Cidr Blocks"
type = list(string)
}
variable "private_subnet_ids" {
description = "Private Subnet Ids Form DB Subnet Group"
type = list(string)
}
variable "target_group_arn" {
description = "Target Group Arn"
type = string
}
variable "desired_count" {
description = "ECS Task Count"
type = number
}
variable "cpu" {
description = "ECS CPU"
type = number
}
variable "memory" {
description = "ECS Memory"
type = number
}
variable "ecs_rails_tag" {
description = "ECS Rails TAG"
type = string
}
variable "ecs_nginx_tag" {
description = "ECS Nginx Tag"
type = string
}
variable "rails_ecr_arn" {
description = "Rails ECR Arn"
type = string
}
variable "nginx_ecr_arn" {
description = "Nginx ECR Arn"
type = string
}
variable "ssm_db_password_path" {
description = "aws_ssm_parameter.db_password.name"
type = string
}
variable "ssm_db_username_path" {
description = "aws_ssm_parameter.db_username.name"
type = string
}
variable "ssm_db_port_path" {
description = "aws_ssm_parameter.db_port.name"
type = string
}
variable "ssm_db_host_path" {
description = "aws_ssm_parameter.db_host.name"
type = string
}
variable "ssm_db_name_path" {
description = "aws_ssm_parameter.db_name.name"
type = string
}
variable "ssm_rails_master_key_path" {
description = "data.aws_ssm_parameter.rails_master_key.name"
type = string
}
variable "environment" {
description = "Rails Environment"
type = string
}
modules/ecs/output.tf
# 空
modules/ecs/task_definitions.tpl.json
[
{
"name": "rails",
"image": "${rails_ecr_arn}:${rails_tag}",
"memoryReservation": 512,
"essential": true,
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/${service_name}",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "ecs/${service_name}"
}
},
"entryPoint": [],
"portMappings": [
{
"hostPort": 3000,
"protocol": "tcp",
"containerPort": 3000
}
],
"command": [
"/app/entrypoint.sh"
],
"healthCheck": {
"retries": 10,
"command": [
"CMD-SHELL",
"curl localhost:3000/api/health_check",
"\"|| exit 1\""
],
"timeout": 30,
"interval": 5,
"startPeriod": 30
},
"environment": [
{
"name": "RAILS_ENV",
"value": "${environment}"
},
{
"name": "RAILS_LOG_TO_STDOUT",
"value": "true"
},
{
"name": "RAILS_SERVE_STATIC_FILES",
"value": "true"
}
],
"secrets": [
{
"valueFrom": "${ssm_db_host_path}",
"name": "DB_HOST"
},
{
"valueFrom": "${ssm_db_password_path}",
"name": "DB_PASSWORD"
},
{
"valueFrom": "${ssm_db_username_path}",
"name": "DB_USERNAME"
},
{
"valueFrom": "${ssm_rails_master_key_path}",
"name": "RAILS_MASTER_KEY"
}
]
},
{
"name": "nginx",
"image": "${nginx_ecr_arn}:${nginx_tag}",
"essential": true,
"portMappings": [
{
"hostPort": 80,
"protocol": "tcp",
"containerPort": 80
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/${service_name}",
"awslogs-region": "ap-northeast-1",
"awslogs-stream-prefix": "ecs/${service_name}"
}
},
"dependsOn": [
{
"containerName": "rails",
"condition": "HEALTHY"
}
],
"healthCheck": {
"command": [
"CMD-SHELL",
"curl -f http://localhost/",
"\"|| exit 1\""
]
}
}
]
modules/ecs/main.tf
ECSタスクではRailsとNginxのタスクを動かすようにしています
data "aws_caller_identity" "current" {}
# ================
# ECS Cluster
# ================
resource "aws_ecs_cluster" "main" {
name = var.common_name
setting {
name = "containerInsights"
value = "enabled"
}
}
# ================
# ECS AutoScaring
# ================
### Capacity Provider
resource "aws_ecs_cluster_capacity_providers" "main" {
cluster_name = aws_ecs_cluster.main.name
capacity_providers = ["FARGATE"]
default_capacity_provider_strategy {
base = 1
capacity_provider = "FARGATE"
}
}
# ================
# ECS Task Definition
# ================
resource "aws_ecs_task_definition" "main" {
family = var.common_name
cpu = var.cpu
memory = var.memory
network_mode = "awsvpc"
requires_compatibilities = ["FARGATE"]
execution_role_arn = module.ecs_task_execution_role.iam_role_arn
task_role_arn = module.ecs_task_execution_role.iam_role_arn
container_definitions = templatefile("${path.module}/task_definitions.tpl.json", {
service_name = var.common_name,
rails_tag = var.ecs_rails_tag,
nginx_tag = var.ecs_nginx_tag,
rails_ecr_arn = var.rails_ecr_arn,
nginx_ecr_arn = var.nginx_ecr_arn,
ssm_db_password_path = var.ssm_db_password_path,
ssm_db_username_path = var.ssm_db_username_path,
ssm_db_port_path = var.ssm_db_port_path,
ssm_db_host_path = var.ssm_db_host_path,
ssm_db_name_path = var.ssm_db_name_path,
ssm_rails_master_key_path = var.ssm_rails_master_key_path,
environment = var.environment,
})
}
# ================
# ECS Task IAM
# ================
data "aws_iam_policy" "ecs_task_execution_role_policy" {
arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
data "aws_iam_policy_document" "ecs_task_execution" {
source_policy_documents = [data.aws_iam_policy.ecs_task_execution_role_policy.policy]
statement {
effect = "Allow"
actions = [
"ssm:GetParameters",
"kms:Decrypt",
"ssmmessages:CreateControlChannel",
"ssmmessages:CreateDataChannel",
"ssmmessages:OpenControlChannel",
"ssmmessages:OpenDataChannel",
]
resources = ["*"]
}
statement {
effect = "Allow"
actions = ["iam:PassRole"]
resources = ["arn:aws:iam::871107023173:role/ecsTaskExecutionRole"]
}
statement {
effect = "Allow"
actions = ["ecs:ExecuteCommand"]
resources = ["arn:aws:iam::871107023173:role/ecsTaskExecutionRole"]
}
}
module "ecs_task_execution_role" {
source = "./../iam_role/"
name = "${var.common_name}-ecs-task-execution"
identifier = "ecs-tasks.amazonaws.com"
policy = data.aws_iam_policy_document.ecs_task_execution.json
}
# ================
# ECS Service
# ================
resource "aws_ecs_service" "main" {
name = var.common_name
cluster = aws_ecs_cluster.main.arn
task_definition = aws_ecs_task_definition.main.arn
desired_count = var.desired_count
launch_type = "FARGATE"
platform_version = "LATEST"
health_check_grace_period_seconds = 300
enable_execute_command = true
network_configuration {
assign_public_ip = false
security_groups = [module.nginx_security_group.security_group_id]
subnets = var.private_subnet_ids
}
load_balancer {
target_group_arn = var.target_group_arn
container_name = "nginx"
container_port = 80
}
lifecycle {
ignore_changes = [task_definition]
}
}
module "nginx_security_group" {
source = "./../security_group/"
security_group_name = "${var.common_name}-nginx"
vpc_id = var.vpc_id
port = 80
cidr_blocks = var.cidr_blocks
}
# ================
# CloudWatch Log Group
# ================
resource "aws_cloudwatch_log_group" "main" {
name = "/ecs/${var.common_name}"
retention_in_days = 180
}
ses.tf
Railsでメールを送る際にAmazon SESを利用します
module "ses" {
source = "./../../../modules/ses/"
domain_name = "staging.example.com"
}
modules/ses
modules/ses/variable.tf
variable "domain_name" {
description = "SES Domain"
type = string
}
modules/ses/output.tf
# 空
modules/ses/main.tf
SESで利用するドメインに関するDNSレコードの作成も行います
# ================
# SES Domain Verification
# ================
# NOTE: SESのドメイン認証確認をしようとするとDKIMの認証に最長72時間かかるため無効化する
resource "aws_ses_domain_identity_verification" "ses-identify-verification" {
domain = aws_ses_domain_identity.ses.domain
depends_on = [aws_route53_record.ses-verification-record]
}
data "aws_route53_zone" "ses" {
name = var.domain_name
}
# ================
# SES Domain
# ================
resource "aws_ses_domain_identity" "ses" {
domain = var.domain_name
}
# ================
# SES Route53 TXT Record
# ================
resource "aws_route53_record" "ses-verification-record" {
zone_id = data.aws_route53_zone.ses.zone_id
name = "_amazonses.${aws_ses_domain_identity.ses.domain}"
type = "TXT"
ttl = "600"
records = [aws_ses_domain_identity.ses.verification_token]
}
# ================
# DKIM
# ================
resource "aws_ses_domain_dkim" "domain-dkim" {
domain = aws_ses_domain_identity.ses.domain
}
# ================
# DKIM Route53 CNAME Record
# ================
resource "aws_route53_record" "ses-amazonses-verification-record" {
# NOTE: for_eachは、aws_ses_domain_dkim.domain-dkim.dkim_tokensを作ってからでないと使えのでcountを使用している
count = 3
zone_id = data.aws_route53_zone.ses.zone_id
name = "${element(aws_ses_domain_dkim.domain-dkim.dkim_tokens, count.index)}._domainkey.${aws_ses_domain_identity.ses.domain}"
type = "CNAME"
ttl = "600"
records = ["${element(aws_ses_domain_dkim.domain-dkim.dkim_tokens, count.index)}.dkim.amazonses.com"]
}
# ================
# SPF
# ================
resource "aws_ses_domain_mail_from" "spf" {
domain = aws_ses_domain_identity.ses.domain
mail_from_domain = "mail.${aws_ses_domain_identity.ses.domain}"
}
# ================
# SPF Route53 MX Record
# ================
resource "aws_route53_record" "mx-record-primary" {
zone_id = data.aws_route53_zone.ses.id
name = aws_ses_domain_mail_from.spf.mail_from_domain
type = "MX"
ttl = "600"
records = ["10 feedback-smtp.ap-northeast-1.amazonses.com"]
}
# ================
# SPF Route53 TXT Record
# ================
resource "aws_route53_record" "txt_mail" {
zone_id = data.aws_route53_zone.ses.zone_id
name = aws_ses_domain_mail_from.spf.mail_from_domain
type = "TXT"
ttl = "600"
records = ["v=spf1 include:amazonses.com ~all"]
}
# ================
# DMARC Route53 TXT Record
# ================
resource "aws_route53_record" "txt_dmarc" {
zone_id = data.aws_route53_zone.ses.zone_id
name = "_dmarc.${aws_ses_domain_identity.ses.domain}"
type = "TXT"
ttl = "600"
records = ["v=DMARC1;p=quarantine;pct=25;rua=mailto:dmarcreports@${aws_ses_domain_identity.ses.domain}"]
}
security_group
セキュリティグループの作成を行うモジュールです
modules/security_group/variable.tf
variable "security_group_name" {
type = string
description = "SecurityGroup Name"
}
variable "vpc_id" {
type = string
description = "VPC Id"
}
variable "port" {
type = number
description = "通信を許可するポート番号"
}
variable "cidr_blocks" {
type = list(string)
description = "CIDR BLOCKの配列"
}
modules/security_group/output.tf
output "security_group_id" {
value = aws_security_group.main.id
}
modules/security_group/main.tf
resource "aws_security_group" "main" {
name = var.security_group_name
description = var.security_group_name
vpc_id = var.vpc_id
}
resource "aws_security_group_rule" "ingress" {
type = "ingress"
from_port = var.port
to_port = var.port
protocol = "tcp"
cidr_blocks = var.cidr_blocks
security_group_id = aws_security_group.main.id
}
resource "aws_security_group_rule" "egress" {
type = "egress"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
security_group_id = aws_security_group.main.id
}
iam
modules/iam_role/variable.tf
variable "name" {}
variable "policy" {}
variable "identifier" {}
modules/iam_role/output.tf
output "iam_role_arn" {
value = aws_iam_role.main.arn
}
output "iam_role_name" {
value = aws_iam_role.main.name
}
modules/iam_role/main.tf
IAMロールを作成するモジュールです
resource "aws_iam_role" "main" {
name = var.name
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
data "aws_iam_policy_document" "assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = [var.identifier]
}
}
}
resource "aws_iam_policy" "main" {
name = var.name
policy = var.policy
}
resource "aws_iam_role_policy_attachment" "main" {
role = aws_iam_role.main.name
policy_arn = aws_iam_policy.main.arn
}
terraform applyとその事後手順
上記のコードができたら、terraform applyをしていきます。
10_frontendフォルダへ移動してterraform apply
20_backendフォルダへ移動してterraform apply
をしていくと各リソースが作成されます。
apply後、SSMパラメータストアにRAILS_MASTER_KEYをセットしていきます。
これはAWSコンソールで手入力して作成します。
これでTerraformでRailsを載せるECS Fargate環境とReactを載せるS3環境の作成が完了しました。
Railsで用いたNginxの設定などはこちらの記事にも記載しているので、詰まったら参考にするといいかもしれません。
https://qiita.com/hatsu/items/22e11e94a0a981d78efa