FargateでEFSがサポートされたのでWordPressの環境をTerraformで構築する


インフラエンジニアの寺岡です。
AWSにはECSというサービスがあり
起動モードとしてEC2とFargateの2種類が存在しています。

Fargateはコンテナの実行環境がAWSのフルマネージドで提供されているため
クラスタの管理が不要になるので大変便利ではあるのですが
その仕様上永続ボリュームをコンテナにマウントすることが出来ず
タスクの停止と同時にストレージも削除されてしまいます。

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/fargate-task-storage.html

今回検証したWordPressをコンテナで起動した場合
投稿した記事で利用している画像などは全てローカルボリュームに保存されることになるため
ストレージが削除されてしまっては困ります。

それでも便利だからFargateを使いたい、そんな方に朗報です。
Fargateのプラットフォームバージョン1.4からEFSエンドポイントのサポートが開始されました。

https://aws.amazon.com/jp/about-aws/whats-new/2020/04/aws-fargate-launches-platform-version-14/

これであればコンテナ間でデータを共有しつつ永続データを保持しておくことが出来そうです。
実際にやってみました。

今回検証した構成

今回の内容通りに構築していくと最終的に以下のような構成になります。

構築は全てTerraformで行っているので以下にコードを記載します。
Terraformのバージョンは「0.12.24」です。
参考になされる場合はご注意ください。

書いたコード

ディレクトリ構造は以下です。

$ tree
.
├── README.md
├── alb.tf
├── efs.tf
├── fargate.tf
├── iam.tf
├── provider.tf
├── rds.tf
├── roles
│   ├── fargate_task_assume_role.json
│   └── fargate_task_execution_policy.json
├── securitygroup.tf
├── ssm.tf
├── tasks
│   └── container_definitions.json
├── terraform.tfstate
├── terraform.tfstate.backup
├── variables.tf
└── vpc.tf

2 directories, 16 files

provider.tf

provider "aws" {
  access_key = var.access_key
  secret_key = var.secret_key
  region     = var.region

  assume_role {
    role_arn = var.role_arn
  }
}

variables.tf

####################
# Provider
####################
variable "access_key" {
  description = "AWS Access Key"
}

variable "secret_key" {
  description = "AWS Secret Key"
}

variable "role_arn" {
  description = "AWS Role Arn"
}

variable "region" {
  default = "ap-northeast-1"
}

vpc.tf

####################
# VPC
####################
resource "aws_vpc" "vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "vpc-fargate-efs"
  }
}

####################
# Subnet
####################
resource "aws_subnet" "public_1a" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "${var.region}a"
  cidr_block              = "10.0.10.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet-fargate-efs-public-1a"
  }
}

resource "aws_subnet" "public_1c" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "${var.region}c"
  cidr_block              = "10.0.11.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet-fargate-efs-public-1c"
  }
}

resource "aws_subnet" "dmz_1a" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "${var.region}a"
  cidr_block              = "10.0.20.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet-fargate-efs-dmz-1a"
  }
}

resource "aws_subnet" "dmz_1c" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "${var.region}c"
  cidr_block              = "10.0.21.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet-fargate-efs-dmz-1c"
  }
}

resource "aws_subnet" "private_1a" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "${var.region}a"
  cidr_block              = "10.0.30.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet-fargate-efs-private-1a"
  }
}

resource "aws_subnet" "private_1c" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "${var.region}c"
  cidr_block              = "10.0.31.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "subnet-fargate-efs-private-1c"
  }
}

####################
# Route Table
####################
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "route-fargate-efs-public"
  }
}

resource "aws_route_table" "dmz" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "route-fargate-efs-dmz"
  }
}

resource "aws_route_table" "private" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "route-fargate-efs-private"
  }
}

####################
# IGW
####################
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "igw-fargate-efs"
  }
}

####################
# NATGW
####################
resource "aws_eip" "natgw" {
  vpc = true

  tags = {
    Name = "natgw-fargate-efs"
  }
}

resource "aws_nat_gateway" "natgw" {
  allocation_id = aws_eip.natgw.id
  subnet_id     = aws_subnet.public_1a.id

  tags = {
    Name = "natgw-fargate-efs"
  }

  depends_on = [aws_internet_gateway.igw]
}

####################
# Route
####################
resource "aws_route" "public" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw.id
  depends_on             = [aws_route_table.public]
}

resource "aws_route" "dmz" {
  route_table_id         = aws_route_table.dmz.id
  destination_cidr_block = "0.0.0.0/0"
  nat_gateway_id         = aws_nat_gateway.natgw.id
  depends_on             = [aws_route_table.dmz]
}

####################
# Route Association
####################
resource "aws_route_table_association" "public_1a" {
  subnet_id      = aws_subnet.public_1a.id
  route_table_id = aws_route_table.public.id
}

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

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

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

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

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

securitygroup.tf

####################
# Security Group
####################
resource "aws_security_group" "alb" {
  name        = "alb-sg"
  description = "for ALB"
  vpc_id      = aws_vpc.vpc.id
}

resource "aws_security_group" "fargate" {
  name        = "fargate-sg"
  description = "for Fargate"
  vpc_id      = aws_vpc.vpc.id
}

resource "aws_security_group" "efs" {
  name        = "efs-sg"
  description = "for EFS"
  vpc_id      = aws_vpc.vpc.id
}

resource "aws_security_group" "rds" {
  name        = "rds-sg"
  description = "for RDS"
  vpc_id      = aws_vpc.vpc.id
}

#####################
# Security Group Rule
#####################
resource "aws_security_group_rule" "allow_http_for_alb" {
  security_group_id = aws_security_group.alb.id
  type              = "ingress"
  protocol          = "tcp"
  from_port         = 80
  to_port           = 80
  cidr_blocks       = ["0.0.0.0/0"]
  description       = "allow_http_for_alb"
}

resource "aws_security_group_rule" "from_alb_to_fargate" {
  security_group_id        = aws_security_group.fargate.id
  type                     = "ingress"
  protocol                 = "tcp"
  from_port                = 80
  to_port                  = 80
  source_security_group_id = aws_security_group.alb.id
  description              = "from_alb_to_fargate"
}

resource "aws_security_group_rule" "from_fargate_to_efs" {
  security_group_id        = aws_security_group.efs.id
  type                     = "ingress"
  protocol                 = "tcp"
  from_port                = 2049
  to_port                  = 2049
  source_security_group_id = aws_security_group.fargate.id
  description              = "from_fargate_to_efs"
}

resource "aws_security_group_rule" "from_fargate_to_rds" {
  security_group_id        = aws_security_group.rds.id
  type                     = "ingress"
  protocol                 = "tcp"
  from_port                = 3306
  to_port                  = 3306
  source_security_group_id = aws_security_group.fargate.id
  description              = "from_fargate_to_rds"
}

resource "aws_security_group_rule" "egress_alb" {
  security_group_id = aws_security_group.alb.id
  type              = "egress"
  protocol          = "-1"
  from_port         = 0
  to_port           = 0
  cidr_blocks       = ["0.0.0.0/0"]
  description       = "Outbound ALL"
}

resource "aws_security_group_rule" "egress_fargate" {
  security_group_id = aws_security_group.fargate.id
  type              = "egress"
  protocol          = "-1"
  from_port         = 0
  to_port           = 0
  cidr_blocks       = ["0.0.0.0/0"]
  description       = "Outbound ALL"
}

resource "aws_security_group_rule" "egress_efs" {
  security_group_id = aws_security_group.efs.id
  type              = "egress"
  protocol          = "-1"
  from_port         = 0
  to_port           = 0
  cidr_blocks       = ["0.0.0.0/0"]
  description       = "Outbound ALL"
}

resource "aws_security_group_rule" "egress_rds" {
  security_group_id = aws_security_group.rds.id
  type              = "egress"
  protocol          = "-1"
  from_port         = 0
  to_port           = 0
  cidr_blocks       = ["0.0.0.0/0"]
  description       = "Outbound ALL"
}

alb.tf

####################
# ALB
####################
resource "aws_lb" "alb" {
  name               = "alb-fargate-efs"
  internal           = false
  load_balancer_type = "application"
  security_groups = [
    aws_security_group.alb.id
  ]
  subnets = [
    aws_subnet.public_1a.id,
    aws_subnet.public_1c.id
  ]
}

####################
# Target Group
####################
resource "aws_lb_target_group" "alb" {
  name                 = "fargate-efs-tg"
  port                 = "80"
  protocol             = "HTTP"
  target_type          = "ip"
  vpc_id               = aws_vpc.vpc.id
  deregistration_delay = "60"

  health_check {
    interval            = "10"
    path                = "/"
    port                = "traffic-port"
    protocol            = "HTTP"
    timeout             = "4"
    healthy_threshold   = "2"
    unhealthy_threshold = "10"
    matcher             = "200-302"
  }
}

####################
# Listener
####################
resource "aws_lb_listener" "alb" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"

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

efs.tf

####################
# EFS
####################
resource "aws_efs_file_system" "efs" {
  creation_token                  = "fargate-efs"
  provisioned_throughput_in_mibps = "50"
  throughput_mode                 = "provisioned"

  tags = {
    Name = "fargate-efs"
  }
}

####################
# Mount Target
####################
resource "aws_efs_mount_target" "dmz_1a" {
  file_system_id = aws_efs_file_system.efs.id
  subnet_id      = aws_subnet.dmz_1a.id
  security_groups = [
    aws_security_group.efs.id
  ]
}

resource "aws_efs_mount_target" "dmz_1c" {
  file_system_id = aws_efs_file_system.efs.id
  subnet_id      = aws_subnet.dmz_1c.id
  security_groups = [
    aws_security_group.efs.id
  ]
}

rds.tf

####################
# Parameter Group
####################
resource "aws_db_parameter_group" "rds" {
  name        = "fargate-efs-pg"
  family      = "mysql5.7"
  description = "for RDS"
}

####################
# Subnet Group
####################
resource "aws_db_subnet_group" "rds" {
  name        = "fargate-efs-sg"
  description = "for RDS"
  subnet_ids = [
    aws_subnet.private_1a.id,
    aws_subnet.private_1c.id
  ]
}

####################
# Instance
####################
resource "aws_db_instance" "rds" {
  identifier                = "fargate-efs-db01"
  engine                    = "mysql"
  engine_version            = "5.7"
  instance_class            = "db.t3.micro"
  storage_type              = "gp2"
  allocated_storage         = "50"
  max_allocated_storage     = "100"
  username                  = "root"
  password                  = "password"
  final_snapshot_identifier = "fargate-efs-db01-final"
  db_subnet_group_name      = aws_db_subnet_group.rds.name
  parameter_group_name      = aws_db_parameter_group.rds.name
  multi_az                  = false
  vpc_security_group_ids = [
    aws_security_group.rds.id
  ]
  backup_retention_period = "7"
  apply_immediately       = true
}

fargate.tf

####################
# Cluster
####################
resource "aws_ecs_cluster" "cluster" {
  name = "cluster-fargate-efs"

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

####################
# Task Definition
####################
resource "aws_ecs_task_definition" "task" {
  family                = "task-fargate-wordpress"
  container_definitions = file("tasks/container_definitions.json")
  cpu                   = "256"
  memory                = "512"
  network_mode          = "awsvpc"
  execution_role_arn    = aws_iam_role.fargate_task_execution.arn

  volume {
    name = "fargate-efs"

    efs_volume_configuration {
      file_system_id = aws_efs_file_system.efs.id
      root_directory = "/"
    }
  }

  requires_compatibilities = [
    "FARGATE"
  ]
}

####################
# Service
####################
resource "aws_ecs_service" "service" {
  name             = "service-fargate-efs"
  cluster          = aws_ecs_cluster.cluster.arn
  task_definition  = aws_ecs_task_definition.task.arn
  desired_count    = 2
  launch_type      = "FARGATE"
  platform_version = "1.4.0"

  load_balancer {
    target_group_arn = aws_lb_target_group.alb.arn
    container_name   = "wordpress"
    container_port   = "80"
  }

  network_configuration {
    subnets = [
      aws_subnet.dmz_1a.id,
      aws_subnet.dmz_1c.id
    ]
    security_groups = [
      aws_security_group.fargate.id
    ]
    assign_public_ip = false
  }
}

container_definition.json

[
    {
        "name": "wordpress",
        "image": "wordpress:latest",
        "essential": true,
        "portMappings": [
            {
                "containerPort": 80,
                "hostPort": 80
            }
        ],
        "mountPoints": [
            {
                "containerPath": "/var/www/html",
                "sourceVolume": "fargate-efs"
            }
        ],
        "secrets": [
            {
                "name": "WORDPRESS_DB_HOST",
                "valueFrom": "WORDPRESS_DB_HOST"
            },
            {
                "name": "WORDPRESS_DB_USER",
                "valueFrom": "WORDPRESS_DB_USER"
            },
            {
                "name": "WORDPRESS_DB_PASSWORD",
                "valueFrom": "WORDPRESS_DB_PASSWORD"
            },
            {
                "name": "WORDPRESS_DB_NAME",
                "valueFrom": "WORDPRESS_DB_NAME"
            }
        ]
    }
]

iam.tf

####################
# IAM Role
####################
resource "aws_iam_role" "fargate_task_execution" {
  name               = "role-fargate_task_execution"
  assume_role_policy = file("./roles/fargate_task_assume_role.json")
}

####################
# IAM Role Policy
####################
resource "aws_iam_role_policy" "fargate_task_execution" {
  name   = "execution-policy"
  role   = aws_iam_role.fargate_task_execution.name
  policy = file("./roles/fargate_task_execution_policy.json")
}

fargate_task_assume_role.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "ecs-tasks.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

fargate_task_execution_policy.json

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "ssm:GetParameters",
            "Resource": "*"
        }
    ]
}

ssm.tf

####################
# Parameter
####################
resource "aws_ssm_parameter" "wordpress_db_host" {
  name        = "WORDPRESS_DB_HOST"
  description = "WORDPRESS_DB_HOST"
  type        = "String"
  value       = aws_db_instance.rds.address
}

resource "aws_ssm_parameter" "wordpress_db_user" {
  name        = "WORDPRESS_DB_USER"
  description = "WORDPRESS_DB_USER"
  type        = "String"
  value       = "wordpress"
}

resource "aws_ssm_parameter" "wordpress_db_password" {
  name        = "WORDPRESS_DB_PASSWORD"
  description = "WORDPRESS_DB_PASSWORD"
  type        = "String"
  value       = "password"
}

resource "aws_ssm_parameter" "wordpress_db_name" {
  name        = "WORDPRESS_DB_NAME"
  description = "WORDPRESS_DB_NAME"
  type        = "String"
  value       = "wordpress"
}

RDSの初期設定

WordPress用にデータベースとユーザーを作成しておきましょう。

$ create database wordpress;
$ CREATE USER 'wordpress'@'%' IDENTIFIED WITH mysql_native_password BY 'password';
$ grant all privileges on wordpress.* to wordpress@'%';

動作確認

terraform applyを実行した後にFargateのタスクが2つ立ち上がるので
ALBのエンドポイントにアクセスします。

WordPressの画面が表示されるので初期設定を行うと以下のようにサイトが表示されました。
ここまではまだ問題にはならないですね。

ここからが本題、画像付きの記事を管理画面から投稿してみます。

投稿ボタンを押したら記事のページに遷移してひたすらF5キーを連打して更新してみます。

EFSが正しく認識されていない場合画面のリロードによって画像が表示されるときとされないときが起こり得るのですが
100回くらい無心で画面をリロードしたところ特に表示が崩れることはありませんでしたので大丈夫そうです。

まとめ

FargateとEFSの連携が可能になったことで構築する際の設計の幅が大きく広がりました。
プラットフォームバージョン1.4ではその他にも便利なアップデートが数多くあるので
皆様も一度ご利用されてみてはいかがでしょうか。


この記事をかいた人

About the author

寺岡佑樹

2016年新卒入社、現在5年目。
SREとして、社内の運用業務の仕組みの検討・実装をしつつ
社外で技術的な登壇を行うエバンジェリスト的な側面も持つ。

・GitHub
https://github.com/nezumisannn

・登壇経歴
https://github.com/nezumisannn/my-profile

・発表資料(SpeakerDeck)
https://speakerdeck.com/nezumisannn