3
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[AWS]TerraformでECS FargateでAPIの実行環境を作ってみた

Last updated at Posted at 2024-03-20

はじめに

完璧の「璧」って、「壁」じゃなくて「璧」だって知ってました?
僕は知らなくて、最近会社の先輩に言われて初めて気づきました、、、
スマホとかPCで入力するときは勝手に変換してくれているのでいいんですけど、手書きの時は絶対まちがっていただろうなーと思います

はい、では、前回までで、API の作成と GithubActions で CI の構築をしました
せっかくなので、今回は作った API を動かすための AWS 環境を作っていきます

実際に動かす方法について

以下の手順を見てください
https://github.com/Ixy-194/go-api-sample-todo-terraform/blob/main/README.md
docker で動くようにしているので、terraform インストールしなくても、docker さえ入ってれば動かせます👍

今回のゴール

以下構成の AWS 環境を作ります。terraform で
よくある構成な、シングルリージョン、マルチ AZ 、アプリは ECS 、RDS は Aurora です
API は ECS Fargate で動かす予定で、今回 API のデプロイまではしないので、とりあえずコンテナで nginx を動かしてアクセスできるようにするとこまでやります

image.png

Terraform とは?

HashiCorp 社によって開発されたオープンソースの IaC ツールです
AWS, GCP, Azure 等のクラウドサービスのインフラをコードによって構築することができます

Terraformとは、 HashiCorp社によって開発された オープンソースのサービス で、 開発環境を効率的に構築できるIaC(Infrastructure as Code)ツールの一種です。クラウドサーバーなどシステム開発に必要なインフラを、コードを記述することにより自動で構築できます。また、インフラの構成管理などもTerraformのコードによる宣言が可能な事も特徴です

Terraform のメリット

Terraformを導入する一つ目のメリットは、誰が構築しても同一のインフラ構成となることです。複数の開発者が携わるプロジェクトであっても、開発環境の一貫性を保てます
二つ目のメリットは、工数削減効果です。Terraformでは、一度構築したインフラをもとに設計ができるため、同様の環境を構築する際の工数を削減できます。三つ目のメリットは、Gitによるバージョン管理に対応していることです。GitOpsやDevOpsなどさまざまな管理システムを用いて、テスト環境や本番環境を管理できます

詳細はこちらを参照

AWS 環境の構築

ECS Fargate で API の実行環境を作るにあたり、以下の AWS リソースを作成します

  1. VPC
  2. RDS(Aurora)
  3. ALB
  4. ECS
  5. ECR
  6. Bastion(RDSへの踏み台用サーバ)

最終的に出来上がるコードの構成はこんな感じです

.
├── docker-compose.yml
├── env
│   └── dev
│       ├── main.tf
│       └── provider.tf
└── modules
    ├── alb
    │   ├── main.tf
    │   ├── output.tf
    │   └── variables.tf
    ├── bastion
    │   ├── main.tf
    │   └── variables.tf
    ├── ecr
    │   ├── main.tf
    │   ├── output.tf
    │   └── variables.tf
    ├── ecs
    │   ├── main.tf
    │   └── variables.tf
    ├── rds
    │   ├── main.tf
    │   └── variables.tf
    └── vpc
        ├── main.tf
        ├── outputs.tf
        └── variables.tf

では、順々にコードを書いて環境を作っていきます

VPC

まずは VPC の作成です
VPC とは Amazon Virtual Private Cloud のことで、AWS 上に構築する仮想ネットワーク環境のことです

ここでは、VPC リソースの作成に「Terraform-aws-modules/vpc/aws」を使い、以下赤枠内の環境を構築します
image.png

実際のコード

env/dev/main.tf
# 共通的に使用する値を変数として定義
locals {
  env = "dev"

  cidr = "192.168.1.0/24"
  public_subnets = ["192.168.1.64/28", "192.168.1.80/28"]
  private_subnets = ["192.168.1.32/28", "192.168.1.48/28"]
  rds_subnets = ["192.168.1.0/28", "192.168.1.16/28"]
  azs = ["ap-northeast-1a", "ap-northeast-1c"]
  service_name = "go-api-sample-todo"
}

module "vpc" {
  source = "../../modules/vpc"

  cidr            = local.cidr
  public_subnets  = local.public_subnets
  private_subnets = local.private_subnets
  rds_subnets     = local.rds_subnets
  azs             = local.azs
  env             = local.env
}

localsに共通的に使用する値(CIDRとか使用する AZとか)を変数として定義し、/modules/vpcを呼び出します

modules/vpcには、以下の3ファイルがあります

  • main.tf
    • 作成する AWS リソースを定義するファイル
  • outputs.tf
    • 戻り値を定義するファイル
      ⇨ここで作成したリソースの情報を他モジュールでも参照・利用できるように変数に格納する
  • variables.tf
    • 引数を定義するファイル

variables.tf では以下のように、env/dev/main.tfから値を受け取るための引数を定義しています

modules/vpc/variables.tf
variable "cidr" {}
variable "public_subnets" {}
variable "private_subnets" {}
variable "rds_subnets" {}
variable "azs" {}
variable "env" {}

main.tf

modules/vpc/main.tf
#################################################################################
# Network
# Ref. https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/5.0.0

#################################################################################

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"
  version = "5.0.0"

  name = "${var.env}-vpc"
  cidr = var.cidr

  azs              = var.azs
  public_subnets   = var.public_subnets
  private_subnets  = var.private_subnets
  database_subnets = var.rds_subnets
  enable_nat_gateway = true
  enable_vpn_gateway = true

  tags = {
    Terraform   = "true"
    Environment = var.env
  }
}

Terraform-aws-modules/vpc/aws」を使うと、これだけの記述で VPC 周りの NW 環境を作れるので便利です

outputs.tf
VPC リソースの情報を他のモジュールでも使えるように変数に格納しておきます。

modules/vpc/main.tf
output "vpc" {
  value = module.vpc
}

これでベースとなるネットワーク環境ができました。

ALB

ALB とは Application Load Balancer のことで、いわゆる ロードバランサーです
リクエストを受けて、背後にあるサービス(今回なら ECS)にいい感じにリクエストを振り分けてルーティングしてくれます
image.png

main.tf

modules/alb/main.tf
# ALB
# ALB
resource "aws_lb" "this" {
  name = "${var.env}-alb"

  internal           = false
  load_balancer_type = "application"
  subnets            = var.subnets

  security_groups = [aws_security_group.this.id]
  tags = {
    Name        = "${var.env}-alb"
    Terraform   = "true"
    Environment = var.env
  }
}

# SecurityGroup
resource "aws_security_group" "this" {
  name   = "${var.env}-sg-alb"
  vpc_id = var.vpc_id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]

  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name        = "${var.env}-sg-alb"
    Terraform   = "true"
    Environment = var.env
  }
}

これで terraform applyをすると、コンソールから ALB が作成されているのがわかります
ただし、現時点では listener(ALBの振り分け先)の指定をしていないので、アクセスすることはできません
次の ECS で listener の作成を行います

ECS

ECS とは Amazon Elastic Container Service (Amazon ECS)のことで、AWS が作成したフルマネージドなコンテナオーケストレーションサービスです
k8s の AWS 版と思っていればいいと思います

ECS は、大きく3つの要素によって構成されています

  • クラスタ
    • タスクとサービスをグループ化するもの
  • サービス
    • タスク定義を元にタスク(コンテナ)を立ち上げて、ロードバランサとの紐付けをするもの
  • タスク
    • 実際に起動するコンテナ
    • タスク定義(task_definition)の内容を元に生成されたコンテナのこと

ここでは、以下のリソースを作成します
image.png
実際のコードはこんな感じ

modules/ecs/main.tf
# クラスター定義
resource "aws_ecs_cluster" "this" {
  name = "${var.env}-ecs-cluster-${var.service_name}"

  tags = {
    Name        = "${var.env}-ecs-cluster-${var.service_name}"
    Terraform   = "true"
    Environment = var.env
  }
}

# サービス定義
resource "aws_ecs_service" "this" {
  name            = "${var.env}-ecs-service-${var.service_name}"
  cluster         = aws_ecs_cluster.this.id
  task_definition = aws_ecs_task_definition.this.arn

  desired_count    = 1
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

  network_configuration {
    subnets         = var.subnets
    security_groups = [aws_security_group.this.id]
  }
  
  # ALB との紐付け
  load_balancer {
    target_group_arn = aws_lb_target_group.this.arn
    container_name   = var.service_name
    container_port   = "80"
  }

  lifecycle {
    ignore_changes = [desired_count, task_definition]
  }

  tags = {
    Name        = "${var.env}-ecs-service-${var.service_name}"
    Terraform   = "true"
    Environment = var.env
  }
}

# タスク定義
resource "aws_ecs_task_definition" "this" {
  family                   = "${var.env}-task-definition-${var.service_name}"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = 256
  memory                   = 512
  container_definitions = jsonencode([
    {
      name = var.service_name
      # 暫定で nginx を立てる
      # 別途 CD でイメージを上書きする
      image = "nginx:latest"
      logConfiguration : {
        logDriver : "awslogs",
        options : {
          awslogs-region : "ap-northeast-1",
          awslogs-stream-prefix : var.service_name,
          awslogs-group : "/ecs/${var.service_name}/${var.env}"
        }

      }
      portMappings = [
        {
          containerPort = 80
        }
      ]
    }
  ])
  task_role_arn      = aws_iam_role.this.arn
  execution_role_arn = aws_iam_role.this.arn

  tags = {
    Name        = "${var.env}-task-definition-${var.service_name}"
    Terraform   = "true"
    Environment = var.env
  }
}

# ターゲットグループの作成
resource "aws_lb_target_group" "this" {
  name = "${var.env}-alb-tg-${var.service_name}"

  port        = 80
  protocol    = "HTTP"
  target_type = "ip"
  vpc_id      = var.vpc_id

  tags = {
    Name        = "${var.env}-alb-tg-${var.service_name}"
    Terraform   = "true"
    Environment = var.env
  }
}

# listener の作成
resource "aws_lb_listener" "http" {
  port     = "80"
  protocol = "HTTP"
  load_balancer_arn = var.lb.arn

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.this.arn
  }

  tags = {
    Name        = "${var.env}-lb-http-listener-${var.service_name}"
    Terraform   = "true"
    Environment = var.env
  }
}

resource "aws_security_group" "this" {
  name        = "${var.env}-sg-${var.service_name}"
  description = "${var.env}-sg-${var.service_name}"
  vpc_id      = var.vpc_id

  ingress {
    from_port       = 80
    to_port         = 80
    protocol        = "tcp"
    security_groups = [var.lb_security_group_id]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name        = "${var.env}-sg-${var.service_name}"
    Terraform   = "true"
    Environment = var.env
  }
}

resource "aws_cloudwatch_log_group" "this" {
  name = "/ecs/${var.service_name}/${var.env}"
}


# 実行ロール
resource "aws_iam_role" "this" {
  name = "${var.env}-ecs-execution-role-${var.service_name}"

  assume_role_policy = <<EOF
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": "sts:AssumeRole",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Effect": "Allow",
      "Sid": ""
    }
  ]
}
EOF
}
resource "aws_iam_role_policy" "this" {
  name = "${var.env}-ecs-execution-role-policy-${var.service_name}"
  role = aws_iam_role.this.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "ssm:GetParameters",
          "secretsmanager:GetSecretValue",
        ]
        Effect   = "Allow"
        Resource = "*"
      },
    ]
  })
}

resource "aws_iam_role_policy_attachment" "this" {
  role       = aws_iam_role.this.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

これで terraform applyをして、 ALB の URL にアクセスすると nginx の画面が表示されるはずです
ALB の URL はコンソール画面から確認できます
image.png

ALB の URL にアクセスしてこんな画面が表示されれば OK
image.png

RDS(Aurora)

続いて、Amazon Aurora で DB を作ります
image.png

Amazon Aurora は、AWS が開発したフルマネージドなリレーショナルデータベースのことです

terraform:env/dev/main.tf からの呼び出し部分はこんな感じ

env/dev/main.tf
module "rds" {
  source = "../../modules/rds"

  env                      = local.env
  vpc_id                   = module.network.vpc.vpc_id
  azs                      = local.azs
  db_subnet_group_name     = module.network.vpc.database_subnet_group
  access_allow_cidr_blocks = module.network.vpc.private_subnets_cidr_blocks
}

配置する VPC、AZ、サブネットグループの指定と、アクセスを許可する cider(今回は private subnet からのみアクセス許可する)を指定します

modules/rds/main.tf
# 使用する Aurora DB のエンジン、バージョン情報を定義
locals {
  master_username = "admin"
  engine          = "aurora-mysql"
  engine_version  = "8.0.mysql_aurora.3.02.0"
  instance_class  = "db.t4g.medium"
  database_name   = "todo"
}

# RDS
resource "aws_rds_cluster" "this" {
  cluster_identifier = "${var.env}-cluster-${local.database_name}"

  database_name                   = local.database_name
  master_username                 = local.master_username
  master_password                 = random_password.this.result
  availability_zones              = var.azs
  port                            = 3306
  vpc_security_group_ids          = [aws_security_group.this.id]
  db_subnet_group_name            = var.db_subnet_group_name
  db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.this.id
  engine                          = local.engine
  engine_version                  = local.engine_version
  final_snapshot_identifier       = "${var.env}-cluster-final-snapshot-${local.database_name}"
  skip_final_snapshot             = true
  apply_immediately               = true

  tags = {
    Name        = "${var.env}-cluster-${local.database_name}"
    Terraform   = "true"
    Environment = var.env
  }

    lifecycle {
    ignore_changes = [
      availability_zones,
    ]
  }

}

resource "aws_rds_cluster_instance" "this" {
  count              = 1
  identifier         = "${var.env}-${local.database_name}-${count.index}"
  engine             = local.engine
  engine_version     = local.engine_version
  cluster_identifier = aws_rds_cluster.this.id
  instance_class     = local.instance_class
  
  tags = {
    Name        = "${var.env}-${local.database_name}-${count.index}"
    Terraform   = "true"
    Environment = var.env
  }
}

# master_username のパスワードを自動生成
resource "random_password" "this" {
  length           = 12
  special          = true
  override_special = "!#&,:;_"

  lifecycle {
    ignore_changes = [
      override_special
    ]
  }
}


resource "aws_rds_cluster_parameter_group" "this" {
  name   = "${var.env}-rds-cluster-parameter-group-${local.database_name}"
  family = "aurora-mysql8.0"

  parameter {
    name  = "time_zone"
    value = "Asia/Tokyo"
  }
}


# Security Group
resource "aws_security_group" "this" {
  name   = "${var.env}-sg-rds-${local.database_name}"
  vpc_id = var.vpc_id
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name        = "${var.env}-sg-rds-${local.database_name}"
    Terraform   = "true"
    Environment = var.env
  }
}

resource "aws_security_group_rule" "this" {
  type              = "ingress"
  from_port         = 3306
  to_port           = 3306
  protocol          = "tcp"
  cidr_blocks       = var.access_allow_cidr_blocks
  security_group_id = aws_security_group.this.id
}


# 各種パラメータを AWS Systems Manager Parameter Store へ保存
resource "aws_ssm_parameter" "master_username" {
  name      = "/${var.env}/rds/${local.database_name}/master_username"
  type      = "SecureString"
  value     = aws_rds_cluster.this.master_username

  tags = {
    Terraform   = "true"
    environment = var.env
  }
}
resource "aws_ssm_parameter" "master_password" {
  name  = "/${var.env}/rds/${local.database_name}/master_password"
  type  = "SecureString"
  value = aws_rds_cluster.this.master_password

  tags = {
    Terraform   = "true"
    environment = var.env
  }
}

resource "aws_ssm_parameter" "cluster_endpoint" {
  name      = "/${var.env}/rds/${local.database_name}/endpoint_w"
  type      = "SecureString"
  value     = aws_rds_cluster.this.endpoint

  tags = {
    Terraform   = "true"
    environment = var.env
  }
}

resource "aws_ssm_parameter" "cluster_reader_endpoint" {
  name      = "/${var.env}/rds/${local.database_name}/endpoint_r"
  type      = "SecureString"
  value     = aws_rds_cluster.this.reader_endpoint

  tags = {
    Terraform   = "true"
    environment = var.env
  }
}

aws_ssm_parameterでは、生成されたパスワードや RDS のエンドポイントをアプリケーション側から取得できるように、AWS System Manager のParameter Storeに格納しています

これで、terraform applyすると、コンソール画面から クラスターとインスタンスが作成され、Parameter Store にも以下で指定したパラメータが保存されています

# 各種パラメータを AWS Systems Manager Parameter Store へ保存
resource "aws_ssm_parameter" "master_username" {
  name      = "/${var.env}/rds/${local.database_name}/master_username"
  type      = "SecureString"
  value     = aws_rds_cluster.this.master_username

  tags = {
    Terraform   = "true"
    environment = var.env
  }
}
resource "aws_ssm_parameter" "master_password" {
  name  = "/${var.env}/rds/${local.database_name}/master_password"
  type  = "SecureString"
  value = aws_rds_cluster.this.master_password

  tags = {
    Terraform   = "true"
    environment = var.env
  }
}

resource "aws_ssm_parameter" "cluster_endpoint" {
  name      = "/${var.env}/rds/${local.database_name}/endpoint_w"
  type      = "SecureString"
  value     = aws_rds_cluster.this.endpoint

  tags = {
    Terraform   = "true"
    environment = var.env
  }
}

resource "aws_ssm_parameter" "cluster_reader_endpoint" {
  name      = "/${var.env}/rds/${local.database_name}/endpoint_r"
  type      = "SecureString"
  value     = aws_rds_cluster.this.reader_endpoint

  tags = {
    Terraform   = "true"
    environment = var.env
  }
}

画面から確認するとこんな感じ

image.png

Bastion

EC2 を使って踏み台サーバを構築します
ここでの用途としては、 DB にアクセスするための踏み台サーバです
踏み台といっても外部に公開するのは嫌なので、private subnet に配置し、ssh ではなく、コンソール画面からのみ接続できるようにします

実際のソースコードはこんな感じ

modules/bastion/main.tf
# EC2
## 最新の AmazonLinux2 の AMI の ID を取得
data "aws_ssm_parameter" "this" {
  name = "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
}

resource "aws_instance" "centos" { 
  ami                  = data.aws_ssm_parameter.this.value
  instance_type        = "t2.nano"
  subnet_id            = var.subnet_id
  iam_instance_profile = aws_iam_instance_profile.this.name

  vpc_security_group_ids = [aws_security_group.this.id]
  key_name = "centos-bastion"

  tags = {
    Name        = "${var.env}-bastion"
    Terraform   = "true"
    Environment = var.env
  }
}

resource "aws_instance" "this" {
  ami                  = data.aws_ssm_parameter.this.value
  instance_type        = "t2.nano"
  subnet_id            = var.subnet_id
  iam_instance_profile = aws_iam_instance_profile.this.name

  vpc_security_group_ids = [aws_security_group.this.id]

  tags = {
    Name        = "${var.env}-aws-bastion"
    Terraform   = "true"
    Environment = var.env
  }
}


# Security Group
## コンソール画面からしかアクセスしないので、 ingress は設定しない
resource "aws_security_group" "this" {
  name   = "${var.env}-sg-bastion"
  vpc_id = var.vpc_id

  ingress {
    description = "from rivate"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name        = "${var.env}-sg-bastion"
    Terraform   = "true"
    Environment = var.env
  }
}

# IAM 
## コンソールからセッションマネージャーでアクセスできるように、IAMロールとIAMポリシーを設定
  resource "aws_iam_role" "this" {
    name = "${var.env}-iam-role-bastion"

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

resource "aws_iam_instance_profile" "this" {
  name = "${var.env}-iam-instance-profile-bastion"
  role = aws_iam_role.this.name
}

data "aws_iam_policy" "this" {
  arn = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore"
}

resource "aws_iam_role_policy_attachment" "this" {
  role       = aws_iam_role.this.name
  policy_arn = data.aws_iam_policy.this.arn
}

これでわざわざローカルから SSH で接続しなくても、コンソール画面から接続できるので便利ですね

ECR

最後に ECR の作成です
ECR とは Amazon Elastic Container Registry のことで、AWS が提供するフルマネージドなコンテナイメージレジストリです
docker のコンテナイメージの管理に使います
実際のコードはこんな感じ

modules/ecr/main.tf
resource "aws_ecr_repository" "this" {
  name                 = var.service_name
  image_tag_mutability = "MUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }

  tags = {
    Name        = "${var.env}-ecr-${var.service_name}"
    Terraform   = "true"
    Environment = var.env
  }
}

これで terraform applyするだけでリポジトリが作れます
簡単ですね

最後に

こんな感じで AWS 環境を作ってみました
実際のソースコードはこちら

正直、コンソール画面からぽちぽち設定するよりも、 terraform とかで環境をコード化できるっていうのは素晴らしいと思う
作り直しが簡単だし、どこかの手順をが抜けてた!なんてこともないですし
わざわざエクセルとかで手順書なんて作りたくないですからねー

次回は、 この GithubActions でこの terraform のコードを CI/CD してみようと思います
ではでは

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?