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

インフラエンジニアの寺岡です。
AWS には「Amazon ECS」というサービスがあり、起動モードとして「Amazon EC2」と「AWS Fargate」の2種類が存在しています。
Fargate はコンテナの実行環境が AWS のフルマネージドで提供されているため、クラスタの管理が不要になるので大変便利ではあるのですが、その仕様上永続ボリュームをコンテナにマウントすることが出来ず、タスクの停止と同時にストレージも削除されてしまいます。
今回検証した WordPress をコンテナで起動した場合、投稿した記事で利用している画像などは、全てローカルボリュームに保存されることになるため、ストレージが削除されてしまっては困ります。
それでも便利だから Fargate を使いたい、そんな方に朗報です。
Fargate のプラットフォームバージョン1.4から、EFS エンドポイントのサポートが開始されました。
● AWS Fargate がプラットフォームバージョン 1.4 をリリース
これであればコンテナ間でデータを共有しつつ、永続データを保持しておくことが出来そうです。
実際にやってみました。
今回検証した構成
今回の内容通りに構築していくと、最終的に以下のような構成になります。
構築は全て 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では、その他にも便利なアップデートが数多くあるので、皆様も一度ご利用されてみてはいかがでしょうか。
1