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)

## elb

aws_lb_target_groupが複数あるのは、blue/greenデプロイ時、切り替える用に二つづつ作っています。

```terraform:terraform/modules/elb/aws_lb.tf
resource "aws_lb" "alb" {
  name                       = "${var.app_name}-alb"
  internal                   = false
  enable_deletion_protection = false
  load_balancer_type         = "application"
  security_groups            = ["${var.alb_sg_id}"]
  subnets = [
    "${var.subnet_p1a_id}",
    "${var.subnet_p1c_id}"
  ]
  access_logs {
    bucket  = var.s3_bucket_id
    prefix  = "access-log"
    enabled = true
  }
}
```

```terraform:terraform/modules/elb/aws_lb_listener.tf
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    target_group_arn = aws_lb_target_group.web_alb_target_group.arn
    type             = "forward"
  }
}
```

```terraform:terraform/modules/elb/aws_lb_target_group.tf
# LbTargetGroup for api
resource "aws_lb_target_group" "api_blue" {
  name        = "${var.api_app_name}-blue-tg"
  port        = var.api_ports[0].internal
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = var.main_vpc_id

  health_check {
    interval            = 300
    path                = "/"
    port                = var.api_ports[0].internal
    protocol            = "HTTP"
    timeout             = 120
    unhealthy_threshold = 10
    matcher             = "200-299"
  }

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

resource "aws_lb_target_group" "api_green" {
  name        = "${var.api_app_name}-green-tg"
  port        = var.api_ports[0].internal
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = var.main_vpc_id

  health_check {
    interval            = 300
    path                = "/"
    port                = var.api_ports[0].internal
    protocol            = "HTTP"
    timeout             = 120
    unhealthy_threshold = 10
    matcher             = "200-299"
  }

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

# LbTargetGroup for web
resource "aws_lb_target_group" "web_blue" {
  name        = "${var.web_app_name}-blue-tg"
  port        = var.web_ports[0].internal
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = var.main_vpc_id

  health_check {
    interval            = 300
    path                = "/index.html"
    port                = var.web_ports[0].internal
    protocol            = "HTTP"
    timeout             = 120
    unhealthy_threshold = 10
    matcher             = "200-299"
  }

  tags = {
    Name = "${var.app_name}-${var.web_app_name}-blue-tg"
  }
}

resource "aws_lb_target_group" "web_green" {
  name        = "${var.web_app_name}-green-tg"
  port        = var.web_ports[0].internal
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = var.main_vpc_id

  health_check {
    interval            = 300
    path                = "/index.html"
    port                = var.web_ports[0].internal
    protocol            = "HTTP"
    timeout             = 120
    unhealthy_threshold = 10
    matcher             = "200-299"
  }

  tags = {
    Name = "${var.app_name}-${var.web_app_name}-green-tg"
  }
}

```

## ecr

ecsに入れるimageを保存しておく場所です。

```terraform:terraform/modules/ecr/ecr.tf
# EcrRepository for api
resource "aws_ecr_repository" "api_repository" {
  name         = var.api_app_name
  force_delete = true

  image_scanning_configuration {
    scan_on_push = true
  }

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

# EcrRepository for web
resource "aws_ecr_repository" "web_repository" {
  name         = var.web_app_name
  force_delete = true

  image_scanning_configuration {
    scan_on_push = true
  }

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

## Null Resource

terraform apllyした時に一緒にbashscriptを実行します。
内容は、ecrにimageをpushしたことです。

```terraform:terraform/modules/bash/null_resource.tf
resource "null_resource" "default" {
  provisioner "local-exec" {
    command = "aws ecr get-login-password --region ${var.region} | docker login --username AWS --password-stdin ${data.aws_caller_identity.self.account_id}.dkr.ecr.ap-northeast-1.amazonaws.com"
  }

  provisioner "local-exec" {
    command = "docker build -t ${var.api_repository_url}:latest --file ../${var.api_app_dir_name}/Dockerfile ../${var.api_app_dir_name}/"
  }

  provisioner "local-exec" {
    command = "docker build -t ${var.web_repository_url}:latest --file ../${var.web_app_dir_name}/Dockerfile ../${var.web_app_dir_name}/"
  }

  provisioner "local-exec" {
    command = "docker push ${var.api_repository_url}:latest"
  }

  provisioner "local-exec" {
    command = "docker push ${var.web_repository_url}:latest"
  }
}
```

## rds

```terraform:terraform/modules/rds/aws_db_instance.tf
resource "aws_db_instance" "db-setting" {
  engine         = "mysql"
  engine_version = "8.0.31"

  identifier = "db-mysql"

  db_name  = var.db_name
  username = var.db_username
  password = var.db_password

  instance_class = "db.t2.micro"

  allocated_storage     = 20
  max_allocated_storage = 50
  storage_type          = "gp2"
  storage_encrypted     = false

  multi_az               = false
  availability_zone      = "ap-northeast-1a"
  db_subnet_group_name   = var.db_sbg_name
  vpc_security_group_ids = ["${var.sg_rds_sg_id}"]
  publicly_accessible    = false
  port                   = var.db_ports[0].external

  parameter_group_name = aws_db_parameter_group.db-pg.name
  option_group_name    = aws_db_option_group.main_optiongroup.name

  backup_window              = "04:00-05:00"
  backup_retention_period    = 7
  maintenance_window         = "Mon:05:00-Mon:08:00"
  auto_minor_version_upgrade = false

  deletion_protection = false
  skip_final_snapshot = true

  apply_immediately = true

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

```terraform:terraform/modules/rds/db_parameter_group.tf
resource "aws_db_parameter_group" "db-pg" {
  name   = "${var.app_name}-db-pg"
  family = "mysql8.0"

  parameter {
    name  = "character_set_database"
    value = "utf8mb4"
  }

  parameter {
    name  = "character_set_server"
    value = "utf8mb4"
  }
}
```

```terraform:terraform/modules/rds/aws_db_option_group.tf
resource "aws_db_option_group" "main_optiongroup" {
  name                 = "${var.app_name}-optiongroup"
  engine_name          = "mysql"
  major_engine_version = "8.0"
}
```

## ecs

dockerコンテナをデブロイできます。


```terraform:terraform/modules/ecs/ecs.tf
resource "aws_ecs_cluster" "cluster" {
  name = "${var.app_name}-cluster"

  setting {
    name  = "containerInsights"
    value = "enabled"
  }

  tags = {
    Name = "${var.app_name}-cluster"
  }
}

resource "aws_ecs_cluster_capacity_providers" "capacity_provider" {
  cluster_name       = "${var.app_name}-cluster"
  capacity_providers = ["FARGATE"]
}
```

```terraform:terraform/modules/ecs/aws_lb_listener_rule.tf
# LbListenerRule for api
resource "aws_lb_listener_rule" "api-blue" {
  listener_arn = var.http_arn
  priority     = 1
  action {
    type             = "forward"
    target_group_arn = var.api_blue_arn
  }
  condition {
    path_pattern {
      values = ["/api/*"]
    }
  }
}


# LbListenerRule for web
resource "aws_lb_listener_rule" "web-blue" {
  listener_arn = var.http_arn
  priority     = 3
  action {
    type             = "forward"
    target_group_arn = var.web_blue_arn
  }
  condition {
    path_pattern {
      values = ["*"]
    }
  }
}

```

```terraform:terraform/modules/ecs/ecs_service.tf
# EcsService for Fargate api
resource "aws_ecs_service" "api-service" {
  name            = "${var.api_app_name}-service"
  cluster         = aws_ecs_cluster.cluster.id
  depends_on      = [aws_lb_listener_rule.api-blue]
  task_definition = aws_ecs_task_definition.api-definition.arn
  desired_count   = 1
  # ecs exec
  enable_execute_command = true

  network_configuration {
    subnets          = ["${var.subnet_p1a_id}"]
    security_groups  = ["${var.apserver_sg_id}"]
    assign_public_ip = true
    #assign_public_ip = false
  }

  load_balancer {
    target_group_arn = var.api_alb_target_group_arn
    container_name   = var.api_app_name
    container_port   = var.api_ports[0].internal
  }
  deployment_controller {
    type = "CODE_DEPLOY"
  }

  capacity_provider_strategy {
    base              = 1
    weight            = 100
    capacity_provider = "FARGATE"
  }

  lifecycle {
    ignore_changes = [desired_count, task_definition, load_balancer]
  }
}

# EcsService for Fargate web
resource "aws_ecs_service" "web-service" {
  name            = "${var.web_app_name}-service"
  cluster         = aws_ecs_cluster.cluster.id
  depends_on      = [aws_lb_listener_rule.web-blue, aws_lb_listener_rule.api-blue]
  task_definition = aws_ecs_task_definition.web-definition.arn
  desired_count   = 1

  load_balancer {
    target_group_arn = var.web_alb_target_group_arn
    container_name   = var.web_app_name
    container_port   = var.web_ports[0].internal
  }

  network_configuration {
    subnets          = ["${var.subnet_p1c_id}"]
    security_groups  = ["${var.webserver_sg_id}"]
    assign_public_ip = true
    #assign_public_ip = false
  }
  deployment_controller {
    type = "CODE_DEPLOY"
  }

  capacity_provider_strategy {
    base              = 1
    weight            = 100
    capacity_provider = "FARGATE"
  }

  lifecycle {
    ignore_changes = [desired_count, task_definition, load_balancer]
  }
}
```

```terraform:terraform/modules/ecs/ecs_task_definition.tf
# TaskDefinition for Fargate api
resource "aws_ecs_task_definition" "api-definition" {
  family                   = "${var.api_app_name}-definition"
  requires_compatibilities = ["FARGATE"]
  cpu                      = 256
  memory                   = 512
  execution_role_arn       = var.ecs_main_role
  task_role_arn            = var.ecs_main_role
  network_mode             = "awsvpc"
  container_definitions = jsonencode([
    {
      name      = "${var.api_app_name}"
      image     = "${var.api_repository_url}:latest"
      cpu       = 10
      memory    = 256
      essential = true
      portMappings = [
        {
          hostPort      = var.api_ports[0].internal
          containerPort = var.api_ports[0].external
        }
      ]
      environment = [
        {
          name  = "HOST"
          value = split(":", "${var.db_endpoint}")[0]
        },
        {
          name  = "DBNAME"
          value = "${var.db_name}"
        },
        {
          name  = "USERNAME"
          value = "${var.db_username}"
        },
        {
          name  = "PASSWORD"
          value = "${var.db_password}"
        },
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-region        = "ap-northeast-1"
          awslogs-stream-prefix = "${var.api_app_name}"
          awslogs-create-group  = "true"
          awslogs-group         = "/fargate/${var.app_name}/dev/${var.api_app_name}"
        }
      }
    }
  ])
  runtime_platform {
    operating_system_family = "LINUX"
    cpu_architecture        = "X86_64"
  }

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

# TaskDefinition for Fargate web
resource "aws_ecs_task_definition" "web-definition" {
  family                   = "${var.web_app_name}-definition"
  requires_compatibilities = ["FARGATE"]
  cpu                      = 256
  memory                   = 512
  execution_role_arn       = var.ecs_main_role
  task_role_arn            = var.ecs_main_role
  network_mode             = "awsvpc"
  container_definitions = jsonencode([
    {
      name      = "${var.web_app_name}"
      image     = "${var.web_repository_url}:latest"
      cpu       = 10
      memory    = 256
      essential = true
      portMappings = [
        {
          containerPort = var.web_ports[0].internal
          hostPort      = var.web_ports[0].external
        }
      ]
      logConfiguration = {
        logDriver = "awslogs"
        options = {
          awslogs-region        = "ap-northeast-1"
          awslogs-stream-prefix = "${var.web_app_name}"
          awslogs-create-group  = "true"
          awslogs-group         = "/fargate/${var.app_name}/dev/${var.web_app_name}"
        }
      }
    }
  ])
  runtime_platform {
    operating_system_family = "LINUX"
    cpu_architecture        = "X86_64"
  }
  tags = {
    Name = "${var.app_name}-${var.web_app_name}-template"
  }
}

```

# ecs execをする

今回は
クラスター名=todoproject
コンテナ名=apiserver
です。

タスクIDはaws consoleから、ecs→サービス→タスクで確認できます。


```console
aws ecs execute-command --task=【タスクID】 --interactive \
--cluster=【クラスター名】 --container=【コンテナ名】 --command /bin/bash
```

マイグレーションを実行します。

```console
rails db:migrate
```

# 結果

![Screenshot 2023-05-21 at 10.10.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731268/28ddeabd-c090-afd3-ba0f-21c98d8221a2.png)


## その後

その後のためにmigrationは自動化しておきます。


```terraform:terraform/modules/ecs/null_resource.tf
resource "null_resource" "init_db" {
  triggers = {
    cluster_arn         = aws_ecs_cluster.cluster.arn
    task_definition_arn = aws_ecs_task_definition.api-definition.arn
  }
  provisioner "local-exec" {
    command = <<EOT
    aws ecs run-task \
      --region $AWS_DEFAULT_REGION \
      --launch-type FARGATE \
      --network-configuration "awsvpcConfiguration={subnets=[$SUBNET_ID],securityGroups=[$SECURITY_GROUP_ID],assignPublicIp=ENABLED}" \
      --cluster $CLUSTER_NAME \
      --task-definition $TASK_NAME \
      --overrides "{\"containerOverrides\": [{\"name\": \"apiserver\",\"command\": [\"rails\", \"db:migrate\"]}]}"
EOT


    interpreter = ["/bin/bash", "-c"]

    environment = {
      AWS_DEFAULT_REGION = "ap-northeast-1"
      SUBNET_ID          = var.subnet_p1a_id
      SECURITY_GROUP_ID  = var.apserver_sg_id
      CLUSTER_NAME       = aws_ecs_cluster.cluster.name
      TASK_NAME          = aws_ecs_task_definition.api-definition.family
    }
  }

  depends_on = [
    aws_ecs_cluster.cluster,
    aws_ecs_task_definition.api-definition
  ]
}
```