This page looks best with JavaScript enabled

[Terraform][AWS] WordPress 環境を Terraform で構築する #1

 ·   ·  ☕ 9 min read  ·  ✍️ Inomaso

Terraformをチュートリアルレベルまで理解できたので、試しに何か環境を作成することにしました。
今回はWordPress環境をWeb3層構成(ELB, EC2, RDS)と、可用性を考慮したアーキテクチャ設計にて構築しました。

構成図


configure

可用性のポイント


  • EC2はオートスケーリングにより、各AZ毎に1台デプロイされます。
  • WordPressの設定ファイルや画像等(/var/www/html)はEFSに配置し、複数のEC2で共有可能にしました。
  • RDSはマルチAZにしました。

作成手順+ソースコード公開


Githubに作成手順やソースコードを公開しました。
git cloneしてterraformディレクトリへ移動後にリソース作成願います。

今回あえてやったこと


  • terraformコマンドの実行回数を減らすため、リソース毎にフォルダを分けておりません。
  • tfstateファイルのバックエンドはローカル環境にしています。

ソースコード解説


ディレクトリ構造

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
% tree
.
├── aws_ec2.tf
├── aws_efs.tf
├── aws_lb.tf
├── aws_rds.tf
├── aws_security_group_ec2.tf
├── aws_security_group_efs.tf
├── aws_security_group_lb.tf
├── aws_security_group_rds.tf
├── aws_vpc.tf
├── config.tf
├── output.tf
├── script.tpl
├── terraform.tfstate
├── terraform.tfstate.backup
├── wp_ec2 #SSH key pair
└── wp_ec2.pub  #SSH key pair

aws_ec2.tf

AMIはAmazon Linux 2のパブリックイメージを使用しています。
EC2はELBのオートスケーリングにより起動テンプレートの内容で起動されます。
EFSをマウントするにはEFS作成完了後にIDを取得する必要があるので、Template Fileを使用しています。
また、KeyPairは事前に作成した公開鍵を使用します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
####################
# EC2 Launch Template
####################
resource "aws_launch_template" "ec2_launch" {
  name          = "ec2_launch"
  image_id      = "ami-0ce107ae7af2e92b5"
  instance_type = "t2.small"

  key_name = aws_key_pair.wp_ec2.id

  vpc_security_group_ids = [
    aws_security_group.ec2.id
  ]

 //EFS作成後にユーザデータを実行させるためtemplate_file使用
  user_data = base64encode(data.template_file.script.rendered)

  block_device_mappings {
    device_name = "/dev/xvda"

    ebs {
      volume_size           = "30"
      volume_type           = "gp2"
      delete_on_termination = "true"
    }
  }

  tag_specifications {
    resource_type = "instance"

    tags = {
      Name = "wp_dev_ec2"
    }
  }

  tags = {
    Name = "wp_dev_ec2_lt"
  }
}

####################
# User data用 Template File
####################
data "template_file" "script" {
  template = file("script.tpl")
  vars = {
    efs_id = aws_efs_file_system.efs.id
  }
}

####################
# Key Pair
####################
resource "aws_key_pair" "wp_ec2" {
  key_name   = "wp_ec2"
  public_key = file("./wp_ec2.pub")
}

aws_efs.tf

EFSを作成し、AZの1aと1cにマウントします。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
####################
# EFS
####################
resource "aws_efs_file_system" "efs" {
  tags = {
    Name = "wp-dev-efs"
  }
}

####################
# EFS Mount Target 1a
####################
resource "aws_efs_mount_target" "efs_1a" {
  file_system_id  = aws_efs_file_system.efs.id
  subnet_id       = aws_subnet.sub_pub_1a.id
  security_groups = [aws_security_group.efs.id]
}

####################
# EFS Mount Target 1c
####################
resource "aws_efs_mount_target" "efs_1c" {
  file_system_id  = aws_efs_file_system.efs.id
  subnet_id       = aws_subnet.sub_pub_1c.id
  security_groups = [aws_security_group.efs.id]
}

aws_lb.tf

Webアクセスのフロント用にALBを作成します。
起動テンプレートをもとに、Auto ScalingでEC2を起動します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
####################
# ELB
####################
resource "aws_lb" "elb" {
  name               = "wp-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups = [
    aws_security_group.elb.id
  ]
  subnets = [
    aws_subnet.sub_pub_1a.id,
    aws_subnet.sub_pub_1c.id
  ]
}

####################
# ELB Listener
####################
resource "aws_lb_listener" "elb_http" {
  load_balancer_arn = aws_lb.elb.arn
  port              = "80"
  protocol          = "HTTP"
  default_action {
    target_group_arn = aws_lb_target_group.ec2_http.arn
    type             = "forward"
  }
}

####################
# ELB Target Group
####################
resource "aws_lb_target_group" "ec2_http" {
  name     = "wp-dev-alb-tg-http"
  port     = 80
  protocol = "HTTP"
  vpc_id   = aws_vpc.vpc.id

  health_check {
    interval            = 10
    path                = "/"
    port                = "traffic-port"
    protocol            = "HTTP"
    timeout             = 5
    healthy_threshold   = 3
    unhealthy_threshold = 3
  }

  stickiness {
    cookie_duration = 1800
    enabled         = true
    type            = "lb_cookie"
  }
}

####################
# Auto Scaling Group
####################
resource "aws_autoscaling_group" "ec2_ag" {
  name                      = "wp_dev_ec2_ag"
  max_size                  = 4
  min_size                  = 2
  health_check_grace_period = 300
  health_check_type         = "EC2"
  desired_capacity          = 2

  vpc_zone_identifier = [
    aws_subnet.sub_pub_1a.id,
    aws_subnet.sub_pub_1c.id
  ]

  launch_template {
    id      = aws_launch_template.ec2_launch.id
    version = "$Latest"
  }

  target_group_arns = [aws_lb_target_group.ec2_http.arn]
}

aws_rds.tf

WordPressの動的コンテンツ作成用に、RDS for MySQLを作成します、

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
####################
# RDS DB Instance
####################
resource "aws_db_instance" "rds" {
  allocated_storage       = 20
  storage_type            = "gp2"
  engine                  = "mysql"
  engine_version          = "5.7"
  instance_class          = "db.t2.micro"
  name                    = "wpdb"
  username                = "dbadmin"
  password                = "SuperSecret"
  parameter_group_name    = aws_db_parameter_group.db_pg.name
  option_group_name       = aws_db_option_group.db_og.name
  multi_az                = true
  db_subnet_group_name    = aws_db_subnet_group.db_sub_gp.name
  vpc_security_group_ids  = [aws_security_group.rds.id]
  backup_retention_period = "7"
  backup_window           = "22:29-22:59"
  //DB削除前にスナップショットを作成しない
  skip_final_snapshot = true
  //自動スケーリング上限
  max_allocated_storage = 200
  //接続エンドポイント
  identifier = "wpdb"

  tags = {
    Name = "wp_dev_rds"
  }
}

####################
# RDS DB Option Group
####################
resource "aws_db_option_group" "db_og" {
  name                 = "wp-dev-db-og"
  engine_name          = "mysql"
  major_engine_version = "5.7"
}

####################
# RDS DB Parameter Group
####################
resource "aws_db_parameter_group" "db_pg" {
  name   = "wp-dev-db-pg"
  family = "mysql5.7"
}

aws_security_group_ec2.tf

EC2用のセキュリティグループです。
HTTP(80ポート)はELBからしか許可しません。
インバウンドルールを複数定義するためにaws_security_group_ruleを利用しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
####################
# EC2 Security Group
####################
resource "aws_security_group" "ec2" {
  name        = "wp_dev_sg_ec2"
  description = "wp_dev_sg_ec2"
  vpc_id      = aws_vpc.vpc.id
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "wp_dev_sg_ec2"
  }
}

//80番ポート(http)許可のインバウンドルール
resource "aws_security_group_rule" "inbound_http" {
  type      = "ingress"
  from_port = 80
  to_port   = 80
  protocol  = "tcp"
  //elbセキュリティグループが紐づくリソースにアクセス許可
  source_security_group_id = aws_security_group.elb.id

  //ec2セキュリティグループに紐付け
  security_group_id = aws_security_group.ec2.id
}

//22番ポート(ssh)許可のインバウンドルール
resource "aws_security_group_rule" "inbound_ssh" {
  type      = "ingress"
  from_port = 22
  to_port   = 22
  protocol  = "tcp"
  cidr_blocks = [
    "0.0.0.0/0"
  ]

  //ec2セキュリティグループに紐付け
  security_group_id = aws_security_group.ec2.id
}

aws_security_group_efs.tf

EFS用のセキュリティグループです。
EC2からのアクセスしか許可しません。
こちらはインバウンドルールが一つなので、ingressで定義しています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
####################
# EFS Security Group
####################
resource "aws_security_group" "efs" {
  name        = "wp_dev_sg_efs"
  description = "wp_dev_sg_efs"
  vpc_id      = aws_vpc.vpc.id
  ingress {
    from_port = 2049
    to_port   = 2049
    protocol  = "tcp"
    //ec2セキュリティグループが紐づくリソースにアクセス許可
    security_groups = [aws_security_group.ec2.id]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "wp_dev_sg_efs"
  }
}

aws_security_group_lb.tf

ELB用のセキュリティグループです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
####################
# ELB Security Group
####################
resource "aws_security_group" "elb" {
  name        = "wp_dev_sg_alb"
  description = "wp_dev_sg_alb"
  vpc_id      = aws_vpc.vpc.id
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "wp_dev_sg_alb"
  }
}

aws_security_group_rds.tf

RDS用のセキュリティグループです。
EC2からのアクセスしか許可しません。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
####################
# RDS Security Group
####################
resource "aws_security_group" "rds" {
  name        = "wp_dev_sg_rds"
  description = "wp_dev_sg_rds"
  vpc_id      = aws_vpc.vpc.id
  ingress {
    from_port = 3306
    to_port   = 3306
    protocol  = "tcp"
    //ec2セキュリティグループが紐づくリソースにアクセス許可
    security_groups = [aws_security_group.ec2.id]
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "wp_dev_sg_rds"
  }
}

aws_vpc.tf

VPC関連を作成します。
DBサブネットもこちらで定義します。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
####################
# VPC
####################
resource "aws_vpc" "vpc" {
  cidr_block           = "10.0.0.0/16"
  instance_tenancy     = "default"
  enable_dns_support   = true
  enable_dns_hostnames = true

  tags = {
    Name = "wp_dev_vpc"
  }
}

####################
# Subnet
####################
resource "aws_subnet" "sub_pub_1a" {
  vpc_id     = aws_vpc.vpc.id
  cidr_block = "10.0.0.0/24"
  //パブリック IPv4 アドレスの自動割り当て
  map_public_ip_on_launch = true
  availability_zone       = "ap-northeast-1a"

  tags = {
    Name = "wp_dev_sub_pub_1a"
  }
}

resource "aws_subnet" "sub_pub_1c" {
  vpc_id     = aws_vpc.vpc.id
  cidr_block = "10.0.1.0/24"
  //パブリック IPv4 アドレスの自動割り当て
  map_public_ip_on_launch = true
  availability_zone       = "ap-northeast-1c"

  tags = {
    Name = "wp_dev_sub_pub_1c"
  }
}

resource "aws_subnet" "sub_db_pri_1a" {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = "10.0.20.0/24"
  availability_zone = "ap-northeast-1a"

  tags = {
    Name = "wp_dev_sub_db_pri_1a"
  }
}

resource "aws_subnet" "sub_db_pri_1c" {
  vpc_id            = aws_vpc.vpc.id
  cidr_block        = "10.0.21.0/24"
  availability_zone = "ap-northeast-1c"

  tags = {
    Name = "wp_dev_sub_db_pri_1c"
  }
}

####################
# DB Subnet
####################
resource "aws_db_subnet_group" "db_sub_gp" {
  name       = "dbsubnet"
  subnet_ids = [aws_subnet.sub_db_pri_1a.id, aws_subnet.sub_db_pri_1c.id]

  tags = {
    Name = "wp_dev_sub_gp"
  }
}

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

  tags = {
    Name = "wp_dev_igw"
  }
}

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

  //インターネットゲートウェイ向けのルート追加
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.igw.id
  }

  tags = {
    Name = "wp_dev_pub_rt"
  }
}

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

  tags = {
    Name = "wp_dev_db_pri_rt"
  }
}

####################
# Route Association
####################
resource "aws_route_table_association" "sub_pub_1a_rt_assocication" {
  subnet_id      = aws_subnet.sub_pub_1a.id
  route_table_id = aws_route_table.pub_rt.id
}

resource "aws_route_table_association" "sub_pub_1c_rt_assocication" {
  subnet_id      = aws_subnet.sub_pub_1c.id
  route_table_id = aws_route_table.pub_rt.id
}

resource "aws_route_table_association" "sub_db_pri_1a_rt_assocication" {
  subnet_id      = aws_subnet.sub_db_pri_1a.id
  route_table_id = aws_route_table.db_pri_rt.id
}

resource "aws_route_table_association" "sub_db_pri_1c_rt_assocication" {
  subnet_id      = aws_subnet.sub_db_pri_1c.id
  route_table_id = aws_route_table.db_pri_rt.id
}

config.tf

AWSリージョンや、Terraform周りのバージョンを固定します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
provider "aws" {
  //バージョンを固定
  version = "3.14.1"
  //東京リージョン
  region = "ap-northeast-1"
}

terraform {
  //バージョンを固定
  required_version = "0.13.5"
}

output.tf

リソース作成後のアクセス確認や設定で使用する値をコンソールで表示します。

1
2
3
4
5
6
7
8
9
//Webアクセス用にコンソール表示
output "elb_dns_name" {
  value = aws_lb.elb.dns_name
}

//WordPress設定用にコンソール表示
output "rds_endpoint" {
	value = aws_db_instance.rds.endpoint
}

script.tpl

EC2のユーザデータ用のスクリプトになります。
WordPress設定全般について記述しています。
WordPressは最新版をインストールしたかったのですが、
PHPバージョンと互換性がなかったため特定バージョンをインストールしています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#!/bin/bash
yum update -y

#EFSマウント設定
yum install -y amazon-efs-utils
efs_id="${efs_id}"
echo "$efs_id:/ /var/www/html efs defaults,_netdev 0 0" >> /etc/fstab

#RDS for MySQL接続クライアントインストール
yum install -y php php-dom php-gd php-mysql
yum localinstall https://dev.mysql.com/get/mysql80-community-release-el7-1.noarch.rpm -y
yum-config-manager --disable mysql80-community
yum-config-manager --enable mysql57-community
yum -y install mysql-community-client.x86_64

#/etc/fstabに記載がある全てのデバイスをマウント
mount -a

#WordPress初期化
#WordPress関連のフォルダが存在しなければ配置
if [ ! -e /var/www/html/wp-admin ]; then   
  cd /tmp
  wget https://wordpress.org/wordpress-5.0.7.tar.gz
  tar xzvf /tmp/wordpress-5.0.7.tar.gz --strip 1 -C /var/www/html
  rm /tmp/wordpress-5.0.7.tar.gz
fi

#ファイルやディレクトリのユーザやグループを変更
chown -R apache:apache /var/www/html

#Apache自動起動を有効化
systemctl enable httpd

#Apache起動
systemctl start httpd

改善点


今回は最低限の可用性を考慮した設計なので、セキュリティやパフォーマンスを考慮するとまだまだ改善点があります。

  • EC2をプライベートサブネットに配置しNatGateway導入する。
  • EC2のセッション管理用にElastiCacheを構築する。
  • EC2へのSSHアクセス用にSSMセッションマネージャを追加する。
  • MySQLと互換性のあるAuroraを導入しパフォーマンスを向上させる。
  • AWS WAFを導入しアプリケーション脆弱性リスクを軽減する。
  • GuardDuty有効化し脅威検出可能にする。
  • EFSのAWS Backup有効化する。
  • ALBやVPC、ClouTrail等のログを有効化する。
  • Fargateを導入し、EC2のIDS/IPSやウィルス検知を不要にする。
  • etc…

参考


まとめ


実際に手を動かしてみると想定どおりにリソースが作成できなく色々とてこずりました。
特にEFSは何回やってもEC2へのマウントが失敗したので、解決までに時間がかかりました。

改善点を対応したら、また記事にしてみようと思います。

Share on

comments powered by Disqus