RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その４、TerraformECS前編

# はじめに

RailsとNuxt3でtodoリストの作り方を
初めから丁寧に説明したいと思います。

使用pcはmacを想定しています。

完成した構成図は以下の通りです。

![aws_structure.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/362a1ef8-00e1-75d0-e923-ad2e59eca033.png)

また、githubレポジトリはこちらです。

https://github.com/sugiyama404/ruby_and_nuxt

各シリーズは以下の通りです。

[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その１、Rails基本設定編](https://qiita.com/sugiyama404/items/637900df3b533fd149f8)
[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その２、Rails API編](https://qiita.com/sugiyama404/items/acea4cc38fe7f155ab54)
[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その３、Nuxt.js編](https://qiita.com/sugiyama404/items/4e74c4446add5efc3669)
[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その４、TerraformECS前編](https://qiita.com/sugiyama404/items/4e19ac1f9be6b8213f55)
[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その５、TerraformECS後編](https://qiita.com/sugiyama404/items/d0e0e74628d38842c9bd)
[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その６、Blue/Greenデプロイ前編](https://qiita.com/sugiyama404/items/1ffb4334b3a1546b4e23)
[RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その７、Blue/Greenデプロイ後編](https://qiita.com/sugiyama404/items/ea57e8c08156bf6eca46)

# bashscriptを作る

terraformでいつも実行するコマンドはbashscriptを作りまとめておきます。

```bash:bin/terraform_apply
#!/usr/bin/env bash

set -euo pipefail

function terraform_apply() {
  local cwd="$1"
  cd "${cwd}"
  cd terraform

  terraform init
  terraform plan
  terraform apply --auto-approve

}

function main() {
  local cwd
  cwd="$(cd "$(dirname "$0")/.." && pwd)"
  terraform_apply "${cwd}"

}

if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
  main "$@"
fi
```

# terraformを書く

tagはつけた方がいいです。
aws consoleで何のリソースかわからないから。


以下のサイトを参考に作ります。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table

## main.tf

main.tfを読み込んで、terraformを実行します。
読み出すmoduleを書きます。
source以下は、moduleに渡す変数です。

```terraform:terraform/main.tf
terraform {
  required_version = "=1.0.6"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 4.0"
    }
  }
}

provider "aws" {
  region = var.region
}

# IAM
module "iam" {
  source   = "./modules/iam"
  app_name = var.app_name
}

# cloudwatch
module "cloudwatch" {
  source       = "./modules/cloudwatch"
  app_name     = var.app_name
  web_app_name = var.web_app_name
  api_app_name = var.api_app_name
}

# network
module "network" {
  source       = "./modules/network"
  app_name     = var.app_name
  web_app_name = var.web_app_name
  api_app_name = var.api_app_name
  http_ports   = var.http_ports
  https_ports  = var.https_ports
  web_ports    = var.web_ports
  api_ports    = var.api_ports
  db_ports     = var.db_ports
}

# s3
module "s3" {
  source   = "./modules/s3"
  app_name = var.app_name
}

# elb
module "elb" {
  source        = "./modules/elb"
  app_name      = var.app_name
  web_app_name  = var.web_app_name
  api_app_name  = var.api_app_name
  main_vpc_id   = module.network.main_vpc_id
  subnet_p1a_id = module.network.subnet_public_1a_id
  subnet_p1c_id = module.network.subnet_public_1c_id
  alb_sg_id     = module.network.alb_sg_id
  web_ports     = var.web_ports
  api_ports     = var.api_ports
  s3_bucket_id  = module.s3.s3_bucket_id
}

# ECR
module "ecr" {
  source       = "./modules/ecr"
  app_name     = var.app_name
  web_app_name = var.web_app_name
  api_app_name = var.api_app_name
}

# Null Resource
module "after_ecr" {
  source             = "./modules/bash"
  region             = var.region
  web_app_name       = var.web_app_name
  api_app_name       = var.api_app_name
  web_app_dir_name   = var.web_app_dir_name
  api_app_dir_name   = var.api_app_dir_name
  api_repository_url = module.ecr.api_repository_url
  web_repository_url = module.ecr.web_repository_url
}

# rds
module "rds" {
  source       = "./modules/rds"
  db_sbg_name  = module.network.db_sbg_name
  sg_rds_sg_id = module.network.sg_rds_sg_id
  db_ports     = var.db_ports
  app_name     = var.app_name
  db_name      = var.db_name
  db_username  = var.db_username
  db_password  = var.db_password
}

# ECS
module "ecs" {
  source                   = "./modules/ecs"
  app_name                 = var.app_name
  web_app_name             = var.web_app_name
  api_app_name             = var.api_app_name
  apserver_sg_id           = module.network.apserver_sg_id
  subnet_p1a_id            = module.network.subnet_public_1a_id
  webserver_sg_id          = module.network.webserver_sg_id
  subnet_p1c_id            = module.network.subnet_public_1c_id
  ecs_main_role            = module.iam.ecs_main_role
  db_endpoint              = module.rds.db_endpoint
  db_name                  = var.db_name
  db_username              = var.db_username
  db_password              = var.db_password
  api_alb_target_group_arn = module.elb.api_alb_target_group_arn
  web_alb_target_group_arn = module.elb.web_alb_target_group_arn
  web_ports                = var.web_ports
  api_ports                = var.api_ports
  http_arn                 = module.elb.http_arn
  api_repository_url = module.ecr.api_repository_url
  web_repository_url = module.ecr.web_repository_url

}
```

## variables.tf

親元の変数です。間違えやすいport番号も登録します。
main.tfを通して、moduleに渡します。

```terraform:terraform/variables.tf
variable "app_name" {
  description = "application name"
  type        = string
  default     = "todolist"
}

variable "web_app_name" {
  description = "webserver name"
  type        = string
  default     = "webserver"
}

variable "api_app_name" {
  description = "apiserver name"
  type        = string
  default     = "apiserver"
}

variable "web_app_dir_name" {
  description = "webserver directory name"
  type        = string
  default     = "webserver"
}

variable "api_app_dir_name" {
  description = "apiserver directory name"
  type        = string
  default     = "apserver"
}

variable "region" {
  description = "AWS region to create resources in"
  type        = string
  default     = "ap-northeast-1"
}

variable "http_ports" {
  type = list(object({
    internal = number
    external = number
    protocol = string
  }))
  default = [
    {
      internal = 80
      external = 80
      protocol = "tcp"
    }
  ]
}

variable "https_ports" {
  type = list(object({
    internal = number
    external = number
    protocol = string
  }))
  default = [
    {
      internal = 443
      external = 443
      protocol = "tcp"
    }
  ]
}

variable "web_ports" {
  type = list(object({
    internal = number
    external = number
    protocol = string
  }))
  default = [
    {
      internal = 3000
      external = 3000
      protocol = "tcp"
    }
  ]
}

variable "api_ports" {
  type = list(object({
    internal = number
    external = number
    protocol = string
  }))
  default = [
    {
      internal = 8080
      external = 8080
      protocol = "tcp"
    }
  ]
}

variable "db_ports" {
  type = list(object({
    internal = number
    external = number
    protocol = string
  }))
  default = [
    {
      internal = 3306
      external = 3306
      protocol = "tcp"
    }
  ]
}

variable "db_name" {
  type        = string
  description = "database name"
  default     = "todoproject"
}

variable "db_username" {
  type        = string
  description = "database user name"
  default     = "rubyruby"
}

variable "db_password" {
  type        = string
  description = "database password"
  default     = "rubyruby"
}

```

## moduleを理解する

以下はecrのmodlueです。

```text
.
├── ecr.tf
├── outputs.tf
└── variables.tf
```

outputs.tfは他のmodlueに変数を渡すためにあります。


```terraform:terraform/modules/ecr/outputs.tf
output "api_repository_url" {
  description = "The URL of the api image repository."
  value       = aws_ecr_repository.api_repository.repository_url
}
```

variables.tfは他のmodlueから変数を受けるためにあります。


```terraform:terraform/modules/ecr/variables.tf
variable "app_name" {
  type = string
}
```

## iam

ecsに他のawsサービスと連携するためにiamを許可します。

```terraform:terraform/modules/iam/aws_iam_role.tf
resource "aws_iam_role" "main_role" {
  name = "${var.app_name}_role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid = ""
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ecs-tasks.amazonaws.com"
        }
      },
    ]
  })

  tags = {
    Name = "${var.app_name}-app-iam-role"
  }
}
```

以下のようにpolicyを自作する必要はないです。
すでにあるpolicyのarnをコピーしとけばokです。
今回は自作します。

```terraform:terraform/modules/iam/aws_iam_policy.tf
resource "aws_iam_policy" "ecr_policy" {
  name = "${var.app_name}_ecr_policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "ecr:*"
        ]
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  })
}

resource "aws_iam_policy" "cloudwatch_policy" {
  name = "${var.app_name}_cloudwatch_policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "logs:*"
        ]
        Effect   = "Allow"
        Resource = "arn:aws:logs:*:*:*"
      },
    ]
  })
}

resource "aws_iam_policy" "ecs_task_role_policy" {
  name = "${var.app_name}_ecs_task_role_policy"

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "ssmmessages:CreateControlChannel",
          "ssmmessages:CreateDataChannel",
          "ssmmessages:OpenControlChannel",
          "ssmmessages:OpenDataChannel"
        ]
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  })
}
```

policyをアタッチします。

```terraform:terraform/modules/iam/aws_iam_policy_attachment.tf
resource "aws_iam_policy_attachment" "ecr_attach" {
  name       = "${var.app_name}-ecr_attach"
  roles      = ["${aws_iam_role.main_role.name}"]
  policy_arn = aws_iam_policy.ecr_policy.arn
}

resource "aws_iam_policy_attachment" "cloudwatch_attach" {
  name       = "${var.app_name}_cloudwatch_attach"
  roles      = ["${aws_iam_role.main_role.name}"]
  policy_arn = aws_iam_policy.cloudwatch_policy.arn
}

resource "aws_iam_policy_attachment" "ecs_task_role_attach" {
  name       = "${var.app_name}_ecs_task_role_attach"
  roles      = ["${aws_iam_role.main_role.name}"]
  policy_arn = aws_iam_policy.ecs_task_role_policy.arn
}
```

## CloudWatch

CloudWatchはecsのエラーログを吐き出すために必要です。


```terraform:terraform/modules/cloudwatch/aws_cloudwatch_log_group.tf
# CloudWatchLog for Fargate api
resource "aws_cloudwatch_log_group" "app" {
  name              = "/fargate/${var.app_name}/dev/${var.api_app_name}"
  retention_in_days = 1

  tags = {
    Name = "${var.app_name}-${var.api_app_name}-cloudwatch-log"
  }
}

# CloudWatchLog for Fargate web
resource "aws_cloudwatch_log_group" "web" {
  name              = "/fargate/${var.app_name}/dev/${var.web_app_name}"
  retention_in_days = 1

  tags = {
    Name = "${var.app_name}-${var.api_app_name}-cloudwatch-log"
  }
}
```

## network

```terraform:terraform/modules/network/vpc.tf
resource "aws_vpc" "main" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true
  instance_tenancy     = "default"

  tags = {
    Name = "${var.app_name}-main-vpc"
  }
}
```

```terraform:terraform/modules/network/internet_gateway.tf
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.app_name}-igw"
  }
}
```

```terraform:terraform/modules/network/subnet.tf
resource "aws_subnet" "public_subnet_1a" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.11.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.app_name}-public-1a"
  }
}

resource "aws_subnet" "public_subnet_1c" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.12.0/24"
  availability_zone       = "ap-northeast-1c"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.app_name}-public-1c"
  }
}

resource "aws_subnet" "private_subnet_1a" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.21.0/24"
  availability_zone       = "ap-northeast-1a"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.app_name}-private-1a"
  }
}

resource "aws_subnet" "private_subnet_1c" {
  vpc_id                  = aws_vpc.main.id
  cidr_block              = "10.0.22.0/24"
  availability_zone       = "ap-northeast-1c"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.app_name}-private-1c"
  }
}

resource "aws_db_subnet_group" "db-sg" {
  name       = "db-sg"
  subnet_ids = [aws_subnet.private_subnet_1a.id, aws_subnet.private_subnet_1c.id]
}
```

```terraform:terraform/modules/network/route_table.tf
resource "aws_route_table" "public_rt" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }
  tags = {
    Name = "${var.app_name}-public-rt"
  }
}

resource "aws_route_table_association" "public_rt_1a" {
  route_table_id = aws_route_table.public_rt.id
  subnet_id      = aws_subnet.public_subnet_1a.id
}

resource "aws_route_table_association" "public_rt_1c" {
  route_table_id = aws_route_table.public_rt.id
  subnet_id      = aws_subnet.public_subnet_1c.id
}

resource "aws_route_table" "private_rt" {
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "${var.app_name}-private-rt"
  }
}

resource "aws_route_table_association" "private_rt_1a" {
  route_table_id = aws_route_table.private_rt.id
  subnet_id      = aws_subnet.private_subnet_1a.id
}

resource "aws_route_table_association" "private_rt_1c" {
  route_table_id = aws_route_table.private_rt.id
  subnet_id      = aws_subnet.private_subnet_1c.id
}
```


```terraform:terraform/modules/network/security_group.tf
# SecurityGroup for Fargate alb
resource "aws_security_group" "alb_sg" {
  name   = "${var.app_name}-alb-sg"
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.app_name}-alb-sg"
  }
}

# SecurityGroup for Fargate api
resource "aws_security_group" "apserver_sg" {
  name   = "${var.api_app_name}-sg"
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.app_name}-${var.api_app_name}-sg"
  }
}

# SecurityGroup for Fargate web
resource "aws_security_group" "webserver_sg" {
  name   = "${var.web_app_name}-sg"
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "${var.app_name}-${var.web_app_name}-sg"
  }
}

# SecurityGroup for RDS
resource "aws_security_group" "rds_sg" {
  name   = "rds-sg"
  vpc_id = aws_vpc.main.id
  tags = {
    Name = "${var.app_name}-rds-sg"
  }
}
```

```terraform:terraform/modules/network/security_group_rule.tf
# SecurityGroupRules for ALB
resource "aws_security_group_rule" "alb_in_http" {
  type              = "ingress"
  from_port         = var.http_ports[0].internal
  to_port           = var.http_ports[0].external
  protocol          = var.http_ports[0].protocol
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.alb_sg.id
}

resource "aws_security_group_rule" "alb_out_full" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.alb_sg.id
}

# SecurityGroupRules for Fargate api
resource "aws_security_group_rule" "apserver_in_alb" {
  type              = "ingress"
  from_port         = var.api_ports[0].internal
  to_port           = var.api_ports[0].external
  protocol          = var.api_ports[0].protocol
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.apserver_sg.id
}

resource "aws_security_group_rule" "apserver_out_db" {
  type              = "egress"
  from_port         = var.db_ports[0].internal
  to_port           = var.db_ports[0].external
  protocol          = var.db_ports[0].protocol
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.apserver_sg.id
}

resource "aws_security_group_rule" "apserver_out_http" {
  type              = "egress"
  from_port         = var.http_ports[0].internal
  to_port           = var.http_ports[0].external
  protocol          = var.http_ports[0].protocol
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.apserver_sg.id
}

resource "aws_security_group_rule" "apserver_out_https" {
  type              = "egress"
  from_port         = var.https_ports[0].internal
  to_port           = var.https_ports[0].external
  protocol          = var.https_ports[0].protocol
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.apserver_sg.id
}

# SecurityGroupRules for Fargate web
resource "aws_security_group_rule" "webserver_in_alb" {
  type              = "ingress"
  from_port         = var.web_ports[0].internal
  to_port           = var.web_ports[0].external
  protocol          = var.web_ports[0].protocol
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.webserver_sg.id
}

resource "aws_security_group_rule" "webserver_out_http" {
  type              = "egress"
  from_port         = var.http_ports[0].internal
  to_port           = var.http_ports[0].external
  protocol          = var.http_ports[0].protocol
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.webserver_sg.id
}

resource "aws_security_group_rule" "webserver_out_https" {
  type              = "egress"
  from_port         = var.https_ports[0].internal
  to_port           = var.https_ports[0].external
  protocol          = var.https_ports[0].protocol
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.webserver_sg.id
}

# SecurityGroupRules for db
resource "aws_security_group_rule" "rds_in_api" {
  type                     = "ingress"
  from_port                = var.db_ports[0].internal
  to_port                  = var.db_ports[0].external
  protocol                 = var.db_ports[0].protocol
  source_security_group_id = aws_security_group.apserver_sg.id
  security_group_id        = aws_security_group.rds_sg.id
}
```

## s3

elbのacesslogをs3に貯めます。


```terraform:terraform/modules/s3/aws_s3_bucket.tf
resource "aws_s3_bucket" "alb_access_log" {
  bucket        = "access-log-${random_string.s3_unique_key.result}"
  force_destroy = true

  tags = {
    Name        = "${var.app_name}-s3-access-log"
    Environment = "Dev"
  }

}

resource "random_string" "s3_unique_key" {
  length  = 6
  upper   = false
  lower   = true
  numeric = true
  special = false
}
```

```terraform:terraform/modules/s3/aws_s3_bucket_versioning.tf
resource "aws_s3_bucket_versioning" "versioning" {
  bucket = aws_s3_bucket.alb_access_log.id
  versioning_configuration {
    status = "Enabled"
  }
}
```

```terraform:terraform/modules/s3/aws_s3_bucket_policy.tf
locals {
  alb_root_account_id = "582318560864"
  # ロードバランサーのリージョンに対応するAWSアカウントID
  # https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/enable-access-logs.html#attach-bucket-policy
}

resource "aws_s3_bucket_policy" "allow_access" {
  bucket = aws_s3_bucket.alb_access_log.id
  policy = data.aws_iam_policy_document.allow_access.json
}

data "aws_iam_policy_document" "allow_access" {
  statement {
    effect = "Allow"
    principals {
      type        = "AWS"
      identifiers = ["${local.alb_root_account_id}"]
    }
    actions = ["*"]
    resources = [
      aws_s3_bucket.alb_access_log.arn,
      "${aws_s3_bucket.alb_access_log.arn}/*",
    ]
  }
}
```