LoginSignup
37
30

More than 1 year has passed since last update.

AWS Fargate サービスを Terraform で構築、 コマンドラインからデプロイ

Last updated at Posted at 2020-09-05

動機

【AWS】 Fargate CLI + Terraform で Docker コンテナを動かす簡単なチュートリアル というのを書いたんですが、下記の問題点を感じました。

  • Fargate CLI のインストールが若干手間。
  • Fargate CLI を使うと、 Terraform だけで完結しないため、一部ハードコーディングが必要になる。
  • Fargate CLI は冪等性が無い。
  • SSL 証明書や Route53 周りは Terraform で設定したいが、 Fargate CLI で設定した値の取得には結局 aws-cli を叩く必要がある。
    • Fargate CLI でも設定可能だが、事前に Route53 で設定とかしないとうまく動いてくれなかった記憶があり、結局あんま信頼できなかった

よって、最近は Fargate CLI は使わずに、

  • インフラ構築は Terraform に全て任せる
  • デプロイは AWS CLI でやる
  • 環境変数は .env 経由で読み込む

としています。

Fargate の更新など、 Mutable な変更は Terraform は向いていないので、そこは AWS CLI から更新しに行く方が安心ですね。

また Terraform は長くなるので aws-cdk とかに移行しようとしたのですが、 CloudFormation は ECS に適用するのは難しいことが分かったので、 Terraform の方が確実だとなりました。

※ 環境変数に関して、 API キーなど秘匿が必要な情報に関しては、適切な権限を付与した S3 バケットに 設定ファイルを保存し、アプリケーション起動時にそれを読み込むのが、セキュアな方法です。 KMS などを使う方法でも良いです。

Terraform

"subnet-xxxxx" だけ手抜きでハードコーディングしてるんで、そこだけ変えてもらえれば動くと思います。 → Subnet も自動的にデフォルトを指定する形に変更しました

ポート番号 var.port などは変数で適当に変更する形です。

また HTTPS の設定はしていないですが、 CloudFront を通すなり、 ALB に証明書設定するなり、 Cloudflare でプロキシするなり、組み合わせは色々です。一見 Cloudflare が一番手間が少なくて良いかなと思いましたが、 WebSocket などを使う場合に問題があり、 ALB の方が良かったです。

[追記]
2021年現在でのベストプラクティスは CloudFront を使うことのようです。セキュリティとパフォーマンスの両面で改善が期待できるためのようです。この記事は使っていませんが、 CloudFront を別途用意して Origin を ALB にするのが良いです。
[/追記]

fargate.tf
resource "aws_default_vpc" "default" {}

data "aws_subnet_ids" "default" {
  vpc_id = aws_default_vpc.default.id
}

variable "port" {
  type = number
}

######################
# ALB
######################

resource "aws_alb_target_group" "main" {
  port        = 3000
  protocol    = "HTTP"
  vpc_id      = aws_default_vpc.default.id
  target_type = "ip"

  health_check {
    path = "/"
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_alb_listener" "main" {
  port              = "80"
  protocol          = "HTTP"
  load_balancer_arn = aws_alb.main.arn

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

resource "aws_alb" "main" {
  security_groups    = [aws_security_group.alb.id]
  load_balancer_type = "application"
  subnets            = data.aws_subnet_ids.default.ids
}

######################
# Security Group
######################

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

  ingress {
    from_port       = var.port
    to_port         = var.port
    protocol        = "tcp"
    security_groups = [aws_security_group.alb.id]
  }
}


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

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

######################
# ECS
######################

resource "aws_ecr_repository" "main" {
  name = "myapp"
}

resource "aws_ecs_cluster" "main" {
  name = "myapp"
}

resource "aws_ecs_task_definition" "main" {
  family = "myapp"
  container_definitions = templatefile("./ecs-task-definition.json", {
    image_uri = aws_ecr_repository.main.repository_url
  })
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = "256"
  memory                   = "512"
  task_role_arn            = aws_iam_role.ecs_task_execution_role.arn
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
}

resource "aws_ecs_service" "main" {
  name    = "myapp"
  cluster = aws_ecs_cluster.main.id

  task_definition = aws_ecs_task_definition.main.arn
  desired_count   = 2
  launch_type     = "FARGATE"

  depends_on = [aws_alb_listener.main]

  load_balancer {
    target_group_arn = aws_alb_target_group.main.arn
    container_name   = "myapp"
    container_port   = var.port
  }

  network_configuration {
    subnets          = data.aws_subnet_ids.default.ids
    security_groups  = [aws_security_group.fargate_service.id]
    assign_public_ip = true
  }

  lifecycle {
    ignore_changes = [desired_count, task_definition]
  }
}

######################
# IAM Role
######################

resource "aws_iam_role" "ecs_task_execution_role" {
  name               = "myapp-iam-role-ecs"
  assume_role_policy = data.aws_iam_policy_document.assume_role_policy.json
}

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

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

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

######################
# Cloudwatch
######################

resource "aws_cloudwatch_log_group" "main" {
  name = "/ecs/myapp"
}
ecs-task-definition.json
[
  {
    "name": "myapp",
    "image": "${image_uri}",
    "essential": true,
    "portMappings": [
      {
        "containerPort": 3000,
        "hostPort": 3000
      }
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "/ecs/myapp",
        "awslogs-region": "us-west-1",
        "awslogs-stream-prefix": "ecs"
      }
    }
  }
]

デプロイ

bin/update-fargate-service.sh
#!/bin/bash

set -eu

test -n "$NAME"
test -n "$DOCKER_REGISTRY"
test -n "$SOURCE"

docker build -t $DOCKER_REGISTRY $SOURCE
aws ecr get-login-password | docker login --username AWS --password-stdin $DOCKER_REGISTRY
docker push $DOCKER_REGISTRY

# 前と同じ設定を適用する
# @see https://github.com/aws/aws-cli/issues/3064#issuecomment-638751296
NEW_TASK_DEFINTION=$(aws ecs describe-task-definition --task-definition $NAME \
      --query '{  containerDefinitions: taskDefinition.containerDefinitions,
                  family: taskDefinition.family,
                  taskRoleArn: taskDefinition.taskRoleArn,
                  executionRoleArn: taskDefinition.executionRoleArn,
                  networkMode: taskDefinition.networkMode,
                  volumes: taskDefinition.volumes,
                  placementConstraints: taskDefinition.placementConstraints,
                  requiresCompatibilities: taskDefinition.requiresCompatibilities,
                  cpu: taskDefinition.cpu,
                  memory: taskDefinition.memory}')

GIT_TAG=`git log | head -n 1 | awk '{print $2}'`

aws ecs register-task-definition \
  --family $NAME \
  --tags key=GIT_TAG,value=$GIT_TAG \
  --cli-input-json "$NEW_TASK_DEFINTION"

aws ecs update-service \
  --cluster $NAME \
  --service $NAME \
  --task-definition $NAME

こうしておけば、 CI からデプロイするのも簡単ですね。

$ env \
        NAME=myapp \
        DOCKER_REGISTRY=123456789.dkr.ecr.us-west-1.amazonaws.com/core-server \
        SOURCE=myapp \
        bin/update-fargate-service.sh

37
30
1

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
37
30