LoginSignup
0
0

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

Last updated at Posted at 2023-06-05

はじめに

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

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

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

aws_structure.png

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

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

RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その1、Rails基本設定編
RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その2、Rails API編
RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その3、Nuxt.js編
RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その4、TerraformECS前編
RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その5、TerraformECS後編
RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その6、Blue/Greenデプロイ前編
RailsとNuxt3でtodoリストを作ろう[REST-API/Terraform/Fargate]〜その7、Blue/Greenデプロイ後編

bashscriptを作る

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

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で何のリソースかわからないから。

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

main.tf

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

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/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です。

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

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

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/modules/ecr/variables.tf
variable "app_name" {
  type = string
}

iam

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

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/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/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/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/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/modules/network/internet_gateway.tf
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.main.id

  tags = {
    Name = "${var.app_name}-igw"
  }
}
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/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/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/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/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/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/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}/*",
    ]
  }
}
0
0
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
0
0