LoginSignup
5
5

More than 1 year has passed since last update.

TerraformでRailsを載せるECS Fargate環境とReactを載せるS3環境を作成する

Posted at

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を載せる用のリソースです。
ECS Fargate For Insight-本番環境.drawio (1).png

フォルダ構成

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コンソールで準備しておく作業があります、

  1. AWS tfstateを保存するためのS3バケットを作成しておきます
    1. staging-example-resource-tfstate、production-example-resource-tfstateの名前を利用しています。
  2. terraform コマンドが使えるIAMを作成しておきます。作ったIAMは.aws/configに設定してterraform applyなどに利用します
  3. Route53でRootドメインのホストゾーンを作成しておきます(本当はterraformで管理したい)
      1. ステージングやAPIで利用するサーバのドメインにサブドメインを設定する予定です。
      1. 例)以後のterraformリソースではapi.example.com, staging.example.com, staging-api.example.comのようなサブドメインを設定していく予定です
  4. 同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

5
5
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
5
5