【大阪 / 横浜】インフラ / サーバーサイドエンジニア募集中!

【大阪 / 横浜】インフラ / サーバーサイドエンジニア募集中!

【25卒向け】AI×バーチャル面接の募集を開始いたしました!

【25卒向け】AI×バーチャル面接の募集を開始いたしました!

【導入実績 500社以上】AWS 構築・運用保守・監視サービス

【導入実績 500社以上】AWS 構築・運用保守・監視サービス

【CentOS 後継】AlmaLinux OS サーバー構築・移行サービス

【CentOS 後継】AlmaLinux OS サーバー構築・移行サービス

【WordPress 専用】クラウドサーバー『ウェブスピード』

【WordPress 専用】クラウドサーバー『ウェブスピード』

【格安】Webサイト セキュリティ自動診断「クイックスキャナー」

【格安】Webサイト セキュリティ自動診断「クイックスキャナー」

【低コスト】Wasabi オブジェクトストレージ 構築・運用サービス

【低コスト】Wasabi オブジェクトストレージ 構築・運用サービス

【予約システム開発】EDISONE カスタマイズ開発サービス

【予約システム開発】EDISONE カスタマイズ開発サービス

【100URLの登録が0円】Webサイト監視サービス『Appmill』

【100URLの登録が0円】Webサイト監視サービス『Appmill』

【中国現地企業に対応】中国クラウド / サーバー構築・運用保守

【中国現地企業に対応】中国クラウド / サーバー構築・運用保守

【YouTube】ビヨンド公式チャンネル「びよまるチャンネル」

【YouTube】ビヨンド公式チャンネル「びよまるチャンネル」

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

インフラエンジニアの寺岡です。

AWS には「Amazon ECS」というサービスがあり、起動モードとして「Amazon EC2」と「AWS Fargate」の2種類が存在しています。

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

Fargate タスクストレージ

今回検証した 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
読み込み中...
1 票, 平均: 1.00 / 11
7,746
X facebook はてなブックマーク pocket
【2024.6.30 CentOS サポート終了】CentOS サーバー移行ソリューション

【2024.6.30 CentOS サポート終了】CentOS サーバー移行ソリューション

【25卒向け】AI×バーチャル面接の募集を開始いたしました!

【25卒向け】AI×バーチャル面接の募集を開始いたしました!

【大阪 / 横浜】インフラエンジニア・サーバーサイドエンジニア 積極採用中!

【大阪 / 横浜】インフラエンジニア・サーバーサイドエンジニア 積極採用中!

この記事をかいた人

About the author

寺岡佑樹

2016年ビヨンド入社、現在6年目のインフラエンジニア
MSPの中の人として障害対応時のトラブルシューティングを行いながら
AWSなどのパブリッククラウドを用いたインフラの設計/構築も行っている。
最近はDockerやKubernetesなどのコンテナ基盤の構築や
運用自動化の一環としてTerraformやPackerなどのHashicorpツールを扱うことが多く
外部の勉強会やセミナーで登壇するEvangelistの役割も担っている。

・GitHub
https://github.com/nezumisannn

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

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

・所有資格
AWS Certified Solutions Architect - Associate
Google Cloud Professional Cloud Architect