18
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TerraformでさくっとFargateを構築する

Posted at

はじめに

コンテナの実行環境をAWSでサクッと構築したいよね...ということから今回Terraformを用いて、さらのAWS上にECSを構築することにします。

構成図

今回以下のような環境を構築することを目指します。

terraform.png

構築

まずはベースになるネットワークから構築しましょう。

VPCネットワークの構築

構成要素は以下になります。ネットワーク制御に関しては、今回はセキュリティグループのみで制御し、ネットワークACLは使わないことにします。

  • VPC
  • サブネット
  • インターネットゲートウェイ
  • ルートテーブル
  • NATゲートウェイ
  • セキュリティグループ
  • (ネットワークACL)

VPC

VPCです。IPアドレスの構築規約は適当ですがVPCはCIDRの/16で構築し、サブネットは/24で構築することとします。

resource "aws_vpc" "example" {
  cidr_block = "10.0.0.0/16"

  tags = {
    Name = "sample"
  }
}

サブネット

続いてサブネットの構築です。Multi-AZ環境とするため、publicとprivateでそれぞれap-northeast-1aとap-northeast-1cに合わせて2つのサブネットを構築します。

resource "aws_subnet" "public_0" {
  cidr_block        = "10.0.1.0/24"
  vpc_id            = aws_vpc.example.id
  availability_zone = "ap-northeast-1a"
}

resource "aws_subnet" "public_1" {
  cidr_block        = "10.0.2.0/24"
  vpc_id            = aws_vpc.example.id
  availability_zone = "ap-northeast-1c"
}

resource "aws_subnet" "private_0" {
  vpc_id            = aws_vpc.example.id
  cidr_block        = "10.0.101.0/24"
  availability_zone = "ap-northeast-1a"
}

resource "aws_subnet" "private_1" {
  vpc_id            = aws_vpc.example.id
  cidr_block        = "10.0.102.0/24"
  availability_zone = "ap-northeast-1c"
}

インターネットゲートウェイ

VPCとインターネットを接続するためにインターネットゲートウェイを構築します。

resource "aws_internet_gateway" "example" {
  vpc_id = aws_vpc.example.id
}

NATゲートウェイ

インターネットからprivateサブネットから直接通信できません。しかし、privateサブネットからインターネットへ通信したいことがあります。privateサブネットからインターネットに通信するためにNATゲートウェイを構築します。NATゲートウェイにはElastic IPをソースアドレスとして使用します。

resource "aws_eip" "nat_gateway_0" {
  vpc        = true
  depends_on = [aws_internet_gateway.example]
}

resource "aws_eip" "nat_gateway_1" {
  vpc        = true
  depends_on = [aws_internet_gateway.example]
}

resource "aws_nat_gateway" "nat_gateway_0" {
  allocation_id = aws_eip.nat_gateway_0.id
  subnet_id     = aws_subnet.public_0.id
  depends_on    = [aws_internet_gateway.example]
}

resource "aws_nat_gateway" "nat_gateway_1" {
  allocation_id = aws_eip.nat_gateway_1.id
  subnet_id     = aws_subnet.public_1.id
  depends_on    = [aws_internet_gateway.example]
}

ルートテーブル

サブネットのルーティングの設定をしましょう。ルートテーブルはVPC内部の通信はlocalというローカルルートで通信します。publicサブネットのデフォルトゲートウェイをインターネットゲートウェイに設定します。

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.example.id
}

resource "aws_route" "public" {
  route_table_id         = aws_route_table.public.id
  gateway_id             = aws_internet_gateway.example.id
  destination_cidr_block = "0.0.0.0/0"
}

privateサブネットの場合はデフォルトゲートウェイはNATゲートウェイにします。

resource "aws_route_table" "private_0" {
  vpc_id = aws_vpc.example.id
}

resource "aws_route_table" "private_1" {
  vpc_id = aws_vpc.example.id
}

resource "aws_route" "private_0" {
  route_table_id         = aws_route_table.private_0.id
  nat_gateway_id         = aws_nat_gateway.nat_gateway_0.id
  destination_cidr_block = "0.0.0.0/0"
}

resource "aws_route" "private_1" {
  route_table_id         = aws_route_table.private_1.id
  nat_gateway_id         = aws_nat_gateway.nat_gateway_1.id
  destination_cidr_block = "0.0.0.0/0"
}

作成したルートテーブルをどのサブネットで使用するか設定します。

resource "aws_route_table_association" "public_0" {
  route_table_id = aws_route_table.public.id
  subnet_id      = aws_subnet.public_0.id
}

resource "aws_route_table_association" "public_1" {
  route_table_id = aws_route_table.public.id
  subnet_id      = aws_subnet.public_1.id
}

resource "aws_route_table_association" "private_0" {
  subnet_id      = aws_subnet.private_0.id
  route_table_id = aws_route_table.private_0.id
}

resource "aws_route_table_association" "private_1" {
  subnet_id      = aws_subnet.private_1.id
  route_table_id = aws_route_table.private_1.id
}

セキュリティグループ

セキュリティグループの設定をします。ひとまず以下のような通信を許可するセキュリティグループを作成します。
ingressの通信はHTTPとHTTPSのみ許可し、egressの通信はすべてのIPアドレスに通信可能とします。

通信の向き ポート プロトコル IPアドレス
ingress 80 tcp 0.0.0.0/0
ingress 443 tcp 0.0.0.0/0
egress x x 0.0.0.0/0
resource "aws_security_group" "example" {
  name   = "example"
  vpc_id = aws_vpc.dev-example.id
}

resource "aws_security_group_rule" "ingress_example_http" {
  from_port         = "80"
  to_port           = "80"
  protocol          = "tcp"
  security_group_id = aws_security_group.example.id
  type              = "ingress"
  cidr_blocks       = ["0.0.0.0/0"]
}

resource "aws_security_group_rule" "ingress_example_https" {
  from_port         = "443"
  to_port           = "443"
  protocol          = "tcp"
  security_group_id = aws_security_group.example.id
  type              = "ingress"
  cidr_blocks       = ["0.0.0.0/0"]
}

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

ALBの作成

ユーザからリクエストするエンドポイントはHTTPSで通信することにします。まずはALBに紐付けるSSL証明書を取得するためにACMで証明書の取得とDNSでドメインを取得しましょう。

ALB本体は先に作成しておきます。

resource "aws_alb" "example" {
  name                       = "web"
  load_balancer_type         = "application"
  internal                   = false
  idle_timeout               = 60
  enable_deletion_protection = false

  subnets = [
    aws_subnet.public_0.id,
    aws_subnet.public_1.id,
  ]
  security_groups = [aws_security_group.example.id]
}

DNS

ドメインを新規に取得することにします。管理コンソールから手動で取得することにします。新規に取得するのでお金がかかります。適当なドメインを取得し、ドメインが登録できたものとします。本記事ではドメイン名を sample.com としておきます。※実際に sample.com を取得しようとすると、このドメインはすでに登録されているため登録できません。別のドメインで登録します。

data "aws_route53_zone" "example" {
  name = "sample.com"
}

ACM

取得したゾーンからACMで証明書を作成します。

resource "aws_acm_certificate" "example" {
  domain_name               = data.aws_route53_zone.example.name
  subject_alternative_names = []
  validation_method         = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

Route53

登録したドメインを利用できるように data でゾーンを取得しておきます。

data "aws_route53_zone" "example" {
  name = "sample.com"
}

resource "aws_route53_record" "example" {
  name    = data.aws_route53_zone.example.name
  zone_id = data.aws_route53_zone.example.id
  type    = "A"

  alias {
    evaluate_target_health = true
    name                   = aws_alb.example.dns_name
    zone_id                = aws_alb.example.zone_id
  }
}

検証用のDNSレコードの作成と検証をします。

# 検証用DNSレコードの作成
resource "aws_route53_record" "example" {
  name    = aws_acm_certificate.example.domain_validation_options[0].resource_record_name
  type    = aws_acm_certificate.example.domain_validation_options[0].resource_record_type
  records = [aws_acm_certificate.example.domain_validation_options[0].resource_record_value]
  zone_id = data.aws_route53_zone.example.id
  ttl     = 60
}

# DNSレコードの検証
resource "aws_acm_certificate_validation" "example" {
  certificate_arn         = aws_acm_certificate.example.arn
  validation_record_fqdns = [aws_route53_record.example.fqdn]
}

ALB

リスナーとターゲットグループを作成します。

リスナー

リスナーとは外部からALBが接続を待ち受けるポート/プロトコルをチェックするプロセスです。HTTPSのみのリクエストを待ち受けることにします。

resource "aws_alb_listener" "example" {
  load_balancer_arn = aws_alb.example.arn
  port              = "443"
  protocol          = "HTTPS"
  certificate_arn   = aws_acm_certificate.example.arn
  ssl_policy        = "ELBSecurityPolicy-2016-08"

  default_action {
    type             = "forward"
    target_group_arn = aws_alb_target_group.example.arn
  }
}

ターゲットグループ

ALBで振り分け先となるターゲットグループを作成します。

resource "aws_alb_target_group" "example" {
  name                 = "example-target"
  vpc_id               = aws_vpc.example.id
  target_type          = "ip"
  port                 = 80
  protocol             = "HTTP"
  deregistration_delay = 300

  health_check {
    path                = "/"
    healthy_threshold   = 5
    unhealthy_threshold = 2
    timeout             = 5
    interval            = 30
    matcher             = 200
    port                = "traffic-port"
    protocol            = "HTTP"
  }

  depends_on = [aws_alb.example]
}

リスナールール

リスナールールによってマッチしたURLのパスに応じたリクエストの振り分けをすることができます。

resource "aws_alb_listener_rule" "example" {
  listener_arn = aws_alb_listener.example.arn
  priority     = 100
  action {
    type             = "forward"
    target_group_arn = aws_alb_target_group.example.arn
  }

  condition {
    field  = "path-pattern"
    values = ["/*"]
  }
}

ECSの作成

準備が整ったのでECSの構築をします。

クラスタの作成

まずはECSクラスタを作成します。

resource "aws_ecs_cluster" "example" {
  name = "nginx-cluster"
}

タスク定義

続いてタスク定義です。実態は example_task_definitions.json のファイルに記述していきます。

resource "aws_ecs_task_definition" "example" {
  container_definitions    = file("./example_task_definitions.json")
  family                   = "example"
  cpu                      = 256
  memory                   = 512
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = aws_iam_role.example.arn
}

ログを CloudWatch Logsで参照します。logConfigurationのlogDriverにawslogsを指定することで、ログ出力先をCloudWatch Logsに設定することができます。

example_task_definitions.json
[
  {
    "name": "example",
    "image": "nginx:latest",
    "essential": true,
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-region": "ap-northeast-1",
        "awslogs-stream-prefix": "ecs",
        "awslogs-group": "/ecs/example"
      }
    },
    "portMappings": [
      {
        "protocol": "tcp",
        "containerPort": 80
      }
    ]
  }
]

サービス

resource "aws_ecs_service" "example-nginx-service" {
  name                              = "example"
  cluster                           = aws_ecs_cluster.example.arn
  task_definition                   = aws_ecs_task_definition.example.arn
  desired_count                     = 2
  launch_type                       = "FARGATE"
  platform_version                  = "1.3.0"
  health_check_grace_period_seconds = 60

  network_configuration {
    assign_public_ip = false
    security_groups  = [aws_security_group.example.id]

    subnets = [
      aws_subnet.private_0.id,
      aws_subnet.private_1.id,
    ]
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.target.arn
    container_name   = "example"
    container_port   = 80
  }

  lifecycle {
    ignore_changes = [task_definition]
  }
}

ログ

CloudWatch Logs

resource "aws_cloudwatch_log_group" "for_ecs" {
  name              = "/ecs/nginx/example"
  retention_in_days = 7
}

IAM

ECSに権限を付与するために、IAMロールを作成します。タスク実行IAMロールとして AmazonECSTaskExecutionRolePolicy を用いることができます。

このロールにパラメータストアからパラメータを取得する権限と、KMSから復号化するための権限を付与しておきます。

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_json = data.aws_iam_policy.ecs_task_execution_role_policy.policy

  statement {
    effect    = "Allow"
    actions   = ["ssm:GetParameters", "kms:Decrypt"]
    resources = ["*"]
  }
}

resource "aws_iam_policy" "example" {
  name   = "example-policy-ecs-task-execution"
  policy = data.aws_iam_policy_document.ecs_task_execution.json
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]

    principals {
      type        = "Service"
      identifiers = ["ecs-tasks.amazonaws.com"]
    }
  }
}

resource "aws_iam_role" "example" {
  name               = "example-role-ecs-task-execution"
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

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

まとめ

Terraformを用いてFargateを構築しました。
今回はECRを用いずにDockerHubにあるイメージで簡易的なnginxが動く環境を構築しました。
次は、ECRに構築したイメージからタスクを実行したり、GitHubと連携してWebhookでCI/CDを回したり、ECSのAutoScalingをしたり...といったことについて書きたいと思います。

今回使用したファイルは以下にあります。

なお、今回はサンプルとして構築したので1ファイルにごっそり書いてますが、tfファイル構成などは Terraformのベストなプラクティスってなんだろうか が参考になります。

18
11
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
18
11

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?