Terraformを用いてEC2およびRDSを立ち上げ、Laravelの環境を構築する


## 構成
Terraformで `ap-northeast-1`リージョンにVPC、1つのパブリックサブネットおよび2つのプライベートサブネットを作成した。さらにパブリックサブネット内にEC2インスタンスを立て、プライベートサブネットにはRDS for MySQLを設置した。ここまでをTerraformで行った。
EC2インスタンス内にはSSH接続でログインし、Laravelの環境を構築した。
![ec2-rds-nginx-laravel.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/a67b5e85-9b4e-8075-4b5e-45fddb99f5e9.jpeg)
[>>](#マイグレーション)
## TerraformでEC2、RDSを構築する
### IAMユーザーを作成する

[AWSコンソール](https://aws.amazon.com/console/)からIAMダッシュボードへ移動する。
![1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/b5fa7568-62d1-38a6-0773-d3f010c97223.jpeg)
**ユーザーを追加**を選択する。
![2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/2877c5dc-ce6a-2fed-8c72-1d73f81af8e4.jpeg)
ユーザー名を入力し、**プログラムによるアクセス**を選択する。
![3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/ceebfd1b-f653-2022-79fb-98a0cea26836.jpeg)
**既存のポリシーを直接アタッチ**を選択し、`AdministratorAccess` を選択する。
![4](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/aca390b8-8f74-4e81-aa6b-1a7dacc1cb54.jpeg)
内容を確認し、**ユーザーの作成**を選択する。
![5](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/4d9a7787-810a-0ae6-44e3-d6f655bb0066.jpeg)
`アクセスキーID`と`シークレットアクセスキー`を控える。
![6](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/e0f596a7-5665-9599-c839-f8ac979a0419.jpeg)

`~/.aws/credentials`に `aws_access_key_id` および `aws_secret_access_key` を記述する。

```sh
% mkdir ~/.aws
% (echo "[default]"; echo "aws_access_key_id = xxxxxxxxxxxxxxxxxxxx"; echo "aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") >> ~/.aws/credentials
```
この状態になる。

```credentials:~/.aws/credentials
[default]
aws_access_key_id = xxxxxxxxxxxxxxxxxxxx
aws_secret_access_key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

### Terraformのファイルを用意する
#### ファイルの構成

```sh
sample
├── aws_db_instance.tf
├── aws_db_parameter_group.tf
├── aws_db_subnet_group.tf
├── aws_instance.tf
├── aws_internet_gateway.tf
├── aws_route_table.tf
├── aws_route_table_association.tf
├── aws_security_group.tf
├── aws_subnet.tf
├── aws_vpc.tf
├── main.tf
├── secret.tfvars
└── terraform.tfvars
```
リソースごとにファイルを分けた。  
今回は13ファイル用意した。

#### ファイルの内容

```tf:main.tf
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}
# Configure the AWS Provider
provider "aws" {
  region = "ap-northeast-1"
}
# variables
variable "app" { type = string }
variable "region" { type = string }
variable "vpc_cidr_block" { type = string }
variable "subnet_cidr_block1" { type = string }
variable "subnet_cidr_block10" { type = string }
variable "subnet_cidr_block11" { type = string }
variable "ssh_cidr_blocks" {
  type = list
  description = "allowed SSH connection"
  default = ["0.0.0.0/0"]
}
variable "db_name" { type = string }
variable "db_username" { type = string }
variable "db_password" { type = string }
```

```tf:aws_vpc.tf
# Create a VPC
resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr_block
  tags = { Name = var.app }
}
```

```tf:aws_subnet.tf
# Create a subnet
resource "aws_subnet" "public" {
  vpc_id = aws_vpc.main.id
  cidr_block = var.subnet_cidr_block1
  availability_zone = "${var.region}a"
  tags = { Name = "${var.app}-public-subnet" }
}
resource "aws_subnet" "private1" {
  vpc_id = aws_vpc.main.id
  cidr_block = var.subnet_cidr_block10
  availability_zone = "${var.region}a"
  tags = { Name = "${var.app}-private1-subnet" }
}
resource "aws_subnet" "private2" {
  vpc_id = aws_vpc.main.id
  cidr_block = var.subnet_cidr_block11
  availability_zone = "${var.region}c"
  tags = { Name = "${var.app}-private2-subnet" }
}
```
```tf:aws_internet_gateway.tf
# Provides a resource to create a VPC Internet Gateway.
resource "aws_internet_gateway" "gw" {
  vpc_id = aws_vpc.main.id
  tags = { Name = var.app }
}
```

```tf:aws_route_table.tf
# Provides a resource to create a VPC routing table.
resource "aws_route_table" "public_route" {
  vpc_id = aws_vpc.main.id
  route {
    cidr_block = "0.0.0.0/0"
    gateway_id = aws_internet_gateway.gw.id
  }
  tags = { Name = "${var.app}-public-route" }
}
```

```tf:aws_route_table_association.tf
# Provides a resource to create an association between a route table and a subnet or a route table and an internet gateway.
resource "aws_route_table_association" "public_route" {
  subnet_id = aws_subnet.public.id
  route_table_id = aws_route_table.public_route.id
}
```

```tf:aws_security_group.tf
# Provides a security group resource.
resource "aws_security_group" "instance" {
  name = var.app
  description = "secutity group for instanse of ${var.app}"
  vpc_id = aws_vpc.main.id
  ingress {
    from_port = 80
    to_port = 80
    protocol = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }
  ingress {
    from_port = 22
    to_port = 22
    protocol = "tcp"
    cidr_blocks = var.ssh_cidr_blocks
  }
  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  tags = { Name = "${var.app}-instance-security-group" }
}

resource "aws_security_group" "db_instance" {
  name = "${var.app}-db"
  description = "security group for db instance of ${var.app}"
  vpc_id = aws_vpc.main.id
  ingress {
    from_port = 3306
    to_port = 3306
    protocol = "tcp"
    security_groups = [aws_security_group.instance.id]
  }
  egress {
    from_port = 0
    to_port = 0
    protocol = "-1"
  }
  tags = { Name = "${var.app}-db-instance-security-group" }
}
```

```tf:aws_instance.tf
# Provides an EC2 instance resource.
resource "aws_instance" "web" {
  ami = data.aws_ssm_parameter.amzn2_ami.value
  instance_type = "t2.micro"
  associate_public_ip_address = true
  subnet_id = aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.instance.id]
  key_name = "sample"
  tags = { Name = var.app }
}
# Provides an SSM Parameter data source.
data "aws_ssm_parameter" "amzn2_ami" {
  name = "/aws/service/ami-amazon-linux-latest/amzn2-ami-hvm-x86_64-gp2"
}
```
[`aws_ssm_parameter` のData Source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/ssm_parameter)でAmazon Linux 2のAMI IDを取得している。
[この記事](https://dev.classmethod.jp/articles/launch-ec2-from-latest-amazon-linux2-ami-by-terraform/)が参考になった。  
本記事内では `key_name` は `sample` としている。  
後ほど [`sample` という名前のキーペアを作成する](#shh接続用のキーペアを作成する)。

```tf:aws_db_subnet_group.tf
# Provides an RDS DB subnet group resource.
resource "aws_db_subnet_group" "default" {
  name = "${var.app}-db-subnet-group"
  description = "${var.app}-db-subnet-group"
  subnet_ids = [ aws_subnet.private1.id, aws_subnet.private2.id ]
  tags = { Name = "${var.app}-db-subnet-group" }
}
```

```tf:aws_db_parameter_group.tf
# Provides an RDS DB parameter group resource.
resource "aws_db_parameter_group" "default" {
  name_prefix = var.app
  family = "mysql8.0"
  description = "${var.app} parameter group for mysql8.0"
  parameter {
    name = "time_zone"
    value = "Asia/Tokyo"
  }
  tags = { Name = var.app }
}
```

```tf:aws_db_instance.tf
# Provides an RDS instance resource.
resource "aws_db_instance" "default" {
  allocated_storage = 20
  storage_type = "gp2"
  engine = "mysql"
  engine_version = "8.0"
  instance_class = "db.t2.micro"
  db_subnet_group_name = aws_db_subnet_group.default.id
  vpc_security_group_ids  = [ aws_security_group.db_instance.id ]
  parameter_group_name = aws_db_parameter_group.default.id
  name = var.db_name
  username = var.db_username
  password = var.db_password
  skip_final_snapshot = true
  tags = { Name = "${var.app}-rds" }
}
```
##### 変数ファイル

```tfvars:terraform.tfvars
app = "sample"
region = "ap-northeast-1"
vpc_cidr_block = "10.1.0.0/16"
subnet_cidr_block1 = "10.1.1.0/24"
subnet_cidr_block10 = "10.1.10.0/24"
subnet_cidr_block11 = "10.1.11.0/24"
```

`terraform.tfvars` ファイルを作って変数に値を渡す。

```tfvars:secret.tfvars
ssh_cidr_blocks = ["x.x.x.x/32"] // ← allowed SSH connection
db_name = "sample"
db_username = "xxxxxx"
db_password = "xxxxxx"
```

`secret.tfvars` などのように `terraform.tfvars` 以外の名前を使う場合は `-var-file` を使う。
クレデンシャル情報を書く場合は、同ファイルをバージョン管理の対象外としておく。
今回は `secret.tfvars` にクレデンシャル情報を書いた。
`db_name`、`db_username` および `db_password` の値は [Laravelの環境変数を設定する際に使用する](#データベースの環境変数を設定する) 。

### terraform を実行する

```sh
% terraform init
```

terraformの作業ディレクトリを初期化している。

```sh
% terraform plan -var-file="secret.tfvars"
```
`terraform plan` 差分を確認する。
`-var-file="secret.tfvars"` で変数を渡している。
`terraform.tfvars` に書いた変数は `-var-file` 無しで渡せる。

```sh
% terraform apply -var-file="secret.tfvars"
```
`terraform apply` でAWSにサービスを構築する。  
これでAWS上で下図の状態になる。
![ec2-rds.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/905a8d0b-c398-0470-1b0e-5bcbe0f1a318.jpeg)
あとはSSH接続し、EC2インスタンス内に必要なものをインストールしていく。

もし構築したものをすべて壊すときは `terraform destroy -var-file="secret.tfvars"` を実行する。
### SHH接続用のキーペアを作成する  
[AWSコンソール](https://aws.amazon.com/console/)からEC2ダッシュボードへ移動する。
![1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/4baff7f5-c76f-f8a3-2cbd-d26d8ba419e2.jpeg)
**キーペア** を選択する。
![2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/5e7548bf-194e-4c5e-5cc8-35c706448307.jpeg)
**キーペアを作成** を選択する。
![3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/c6d7dcaf-89e1-aad9-1866-628b864de3d0.jpeg)
キーペアを作成する。
![4](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/79ba6653-9bc1-7e2d-17f0-3528339040f5.jpeg)
キーペアが作成される。
![5](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/1c5f005d-0f52-e41f-7443-ff3500019e56.jpeg)
キーペアを作成すると秘密鍵がダウンロードされる。
秘密鍵は `~/.ssh` へ移動させて、権限を変更してユーザーのみ読み込み可能にしておく。

```sh
% mv ~/Downloads/sample.pem ~/.ssh/
% chmod 400 ~/.ssh/sample.pem 
```

[Amazon EC2 キーペアと Linux インスタンス | オプション 1: Amazon EC2 を使用してキーペアを作成する - Amazon Elastic Compute Cloud](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/ec2-key-pairs.html#having-ec2-create-your-key-pair)

## EC2インスタンス内にLaravelの環境を構築する
[AWSコンソール](https://aws.amazon.com/console/)からEC2ダッシュボードへ移動する。
![1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/aaf5262c-7ccb-cebe-53e6-606429864f1c.jpeg)
インスタンスを選択し、パブリックIPアドレスをコピーする。

### SSHクライアントでEC2インスタンスに接続する
```ｓｈ
% ssh -i ~/.ssh/sample.pem ec2-user@x.x.x.x
```
SSH接続でEC2にログインする。

```
       __|  __|_  )
       _|  (     /   Amazon Linux 2 AMI
      ___|\___|___|

https://aws.amazon.com/amazon-linux-2/
No packages needed for security; 3 packages available
Run "sudo yum update" to apply all updates.
```

### nginxとphpをインストールする
```
$ amazon-linux-extras 
```
利用可能なパッケージを確認する。
今回は`nginx1`と`php7.4`を利用する。

```
$ sudo amazon-linux-extras install -y php7.4 nginx1
```
インストールする。
必要なパッケージも同時にインストールされる。

```
$ sudo systemctl start nginx php-fpm
```
nginxおよびphp-fpmを起動させる。
nginxを起動させてから http://x.x.x.x にアクセスすると
![screenshot](https://i.gyazo.com/45563b847e5c11a56b6bd72755933f8a.png)
このように表示される。

```sh
$ systemctl status nginx php-fpm
```
で起動出来ているかどうかの確認ができる。

### 必要なパッケージのインストール
[Laravel 8.x インストール](https://readouble.com/laravel/8.x/ja/installation.html)によると、
>
以下の要件を満たす必要があります。
>
PHP >= 7.3
BCMath PHP拡張
Ctype PHP拡張
Fileinfo PHP拡張
JSON PHP拡張
Mbstring PHP拡張
OpenSSL PHP拡張
PDO PHP拡張
Tokenizer PHP拡張
XML PHP拡張

従って、必要なパッケージをインストールする。

```sh
$ sudo yum install -y php-bcmath php-ctype php-fileinfo php-json php-mbstring php-openssl php-pdo php-tokenizer php-xml
```

### Composerをインストールする

```sh
$ curl -sS https://getcomposer.org/installer | php
```
Composerをインストールする。

```sh
$ sudo mv composer.phar /usr/local/bin/composer
```
移動させてパスを通す。

### nginxとphpの設定
#### nginxの設定

```sh
$ sudo vi /etc/nginx/conf.d/default.conf 
```
`/etc/nginx/conf.d/default.conf` を作成し、以下の内容に変更する。

```conf:/etc/nginx/conf.d/default.conf 
server {
    listen 80;
    server_name _;
    root /var/www/sample/public;

    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-XSS-Protection "1; mode=block";
    add_header X-Content-Type-Options "nosniff";

    index index.php;

    charset utf-8;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location ~ \.php$ {
        fastcgi_pass unix:/run/php-fpm/php-fpm.sock;
        fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}
```
内容は[デプロイ 8.x Laravel](https://readouble.com/laravel/8.x/ja/deployment.html#nginx)を参考にしている。

#### php-fpmの設定

```sh
$ sudo vi /etc/php-fpm.d/www.conf
```
`/etc/php-fpm.d/www.conf` の以下の値を変更する。  

```conf:/etc/php-fpm.d/www.conf
user = nginx
group = nginx

listen = /run/php-fpm/php-fpm.sock

listen.owner = nginx
listen.group = nginx
listen.mode = 0660
```

```
$ sudo systemctl restart nginx php-fpm
```
再起動する。

```sh
$ sudo mkdir /var/www && sudo chmod 777 /var/www
```
`/var/www` ディレクトリを作成し、全権限を与える。

### Laravelをインストールする
```sh
$ composer create-project --prefer-dist laravel/laravel /var/www/sample
```
インストールする。

```
$ sudo chmod -R 777 /var/www/sample/{storage,bootstrap/cache/}
```
`/var/www/sample/storage/`および`/var/www/sample/bootstrap/cache/`の権限を変更し、全権限を与える。

#### データベースの環境変数を設定する

```sh
$ vi /var/www/sample/.env
```

```.env:/var/www/sample/.env
DB_CONNECTION=mysql
DB_HOST=エンドポイント
DB_PORT=3306
DB_DATABASE=データベース名
DB_USERNAME=ユーザー名
DB_PASSWORD=パスワード
```
`エンドポイント`はRDSダッシュボードから確認する。

①![1](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/65e492d8-748d-bd7c-7ed4-3cceadfcf123.jpeg)
②![2](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/2cd2bfb8-cd98-4336-22bf-12c8db1828d2.jpeg)
③![3](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/6ec3940c-e7fb-a777-202f-1d09d136ab9a.jpeg)
④![4](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/e36dbf71-80ce-c981-690e-445a14ade118.jpeg)
`データベース名`、`ユーザー名`、および`パスワード` は[terraformに変数で渡した値](#変数ファイル)。


#### マイグレーション
```
$ cd /var/www/laravel
$ php artisan migrate:fresh --seed
```

成功すれば、LaravelとMySQLの接続も問題ない。
EC2インスタンス内にも必要なものが揃い、[目的の構成](#構成)の状態になった。


### npmを使うためには
```sh
$ curl -sL https://rpm.nodesource.com/setup_14.x | sudo bash - && sudo yum install -y nodejs
```

インストールする。

### gitが必要な場合
```sh
$ sudo yum install -y git
```
インストールする。

### mysqlにログインする場合
```sh
$ sudo yum install -y mysql
```
インストールする。

```sh
$ mysql -h エンドポイント -u ユーザー名 -p
```
ログインする。

## あとがき
もともと ECS、ECRを使って下図のような構成にしようと思っていました。

![screenshot](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/230954/dffa55c4-78d5-d923-3061-b992ae7bd1f8.jpeg)


しかし、おそらくタスク定義に問題があり、ローカルでは問題無いもののAWS上ではnginxとphp-fpmの接続がうまくいきませんでした。
[どなたかわかる方が居りましたら教えてください。](https://stackoverflow.com/questions/64941180/docker-volumes-in-ecs-how-to-connect-nginx-with-php-fpm-using-unix-socket)


## 参考

- [Terraformで最新のAmazon Linux 2のAMI IDを取得してEC2を構築する](https://dev.classmethod.jp/articles/launch-ec2-from-latest-amazon-linux2-ami-by-terraform/)
- [AWSでTerraformに入門](https://dev.classmethod.jp/articles/terraform-getting-started-with-aws/)
- [Laravel 環境構築 AWS EC2(nginx + PHP) + RDS - Qiita](https://qiita.com/ntm718/items/f896c8e4fb801777954b)
- [How To Install Node.js 14|12|10 on Amazon Linux 2 | TechViewLeo](https://techviewleo.com/how-to-install-nodejs-on-amazon-linux/)