はじめてのTerraform【リソース生成編】

# はじめに

当記事では、AWSのECS/Fargateインフラを
Terraformでコードから再現する作業をまとめています。

なお、Terraformのインストールなど事前準備は
別記事でまとめていますので、まずはそちらをご確認ください。

https://qiita.com/Shuhei_Nakada/items/422479ad38a2a85f4444

また、Terraformではなく、AWSマネジメントコンソールからGUIで
ECS/Fargateインフラを構築する手順は別記事でまとめていますので
ご興味がございましたら、ご覧ください。

https://qiita.com/Shuhei_Nakada/items/9e4c51bf9ae59f0b1a2b

前提として、ECS/Fargateにホストしているアプリは`Rails`となりますので
その点をご理解ください。



# 使用技術

* Terraform: 1.1.2
* ECS/Fargate: blue/greenデプロイメント
* Rails: 6
* MySQL: 8



# インフラ構成図

下記通り、ECS/Fargateに関連するリソース一式をTerraformでコードから生成します。

![スクリーンショット 2022-01-02 7.57.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/3f461e7e-c3a5-2d18-4db1-f10bb74621ef.png)



# 作業内容

1. 基礎部
2. ネットワーク
3. セキュリティグループ
4. EC2/ RDS
5. ALB/ Route53/ ACM
6. ECS/ SystemsManager
7. CloudFront/ S3　



# 1. 基礎部

まず、Terraform/プロバイダーのバージョン指定や変数などの
基盤部分をmain.tfでまとめます。

```terraform:terraform/main.tf
# -------------------------------------------
# Terraform configuration
# -------------------------------------------
terraform {
  required_version = ">=1.1"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~>3.0"
    }
  }
}

# -------------------------------------------
# Provider
# -------------------------------------------
provider "aws" {
  profile = "terraform"
  region  = "ap-northeast-1"
}

provider "aws" {
  alias   = "virginia"
  profile = "terraform"
  region  = "us-east-1"
}

# -------------------------------------------
# Variables
# -------------------------------------------
variable "tool" {
  type = string
}

variable "project" {
  type = string
}

variable "environment" {
  type = string
}

```
続いて、terraform.tfvarsに、変数に代入する値を
各自でアプリに合わせて入力してください。

私の場合は、`AWSマネジメントコンソールからGUIで作成したECS/Fargateと
新しく作成するTerraformベースのECS/Fargateを区別するために`
toolにterraformという値を設定しています。

```terraform:terraform/terraform.tfvars

tool        = "terraform"
project     = "アプリ名"
environment = "production"

```

また、modulesには、IAMロールだけ書いています。
(後の実装で役立ってきますが、正直２つのIAMロールしか作成しませんので
活用されている訳ではないです。)

module無しで記述しても労力的には変化ないと思いますが
個人的な可読性とmoduleを使ってみたいという気持ちで
採用してますので、ご了承ください。

```terraform:terraform/modules/iam_role/main.tf
variable "name" {}
variable "policy" {}
variable "identifier" {}

resource "aws_iam_role" "default" {
  name               = var.name
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
}

data "aws_iam_policy_document" "assume_role" {
  statement {
    actions = ["sts:AssumeRole"]
    principals {
      type        = "Service"
      identifiers = [var.identifier]
    }
  }
}

resource "aws_iam_policy" "default" {
  name   = var.name
  policy = var.policy
}

resource "aws_iam_role_policy_attachment" "default" {
  role       = aws_iam_role.default.name
  policy_arn = aws_iam_policy.default.arn
}

output "iam_role_arn" {
  value = aws_iam_role.default.arn
}

output "iam_role_name" {
  value = aws_iam_role.default.name
}
```

そして、terraformディレクトリに.gitignoreを新たに配置しています。
複数.gitignoreが存在する場合は、最下層の.gitignoreが優先されます。

コード自体は、gitignore ioでterraformを検索して
テンプレートを貼り付けているだけです。

https://www.toptal.com/developers/gitignore

```:terraform/.gitignore

# Created by https://www.toptal.com/developers/gitignore/api/terraform
# Edit at https://www.toptal.com/developers/gitignore?templates=terraform

### Terraform ###
# Local .terraform directories
**/.terraform/*

# .tfstate files
*.tfstate
*.tfstate.*

# Crash log files
crash.log

# Exclude all .tfvars files, which are likely to contain sentitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
#
*.tfvars

# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Include override files you do wish to add to version control using negated pattern
# !example_override.tf

# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*

# Ignore CLI configuration files
.terraformrc
terraform.rc

# End of https://www.toptal.com/developers/gitignore/api/terraform

```

以上を踏まえて、最終的なディレクトリ構造は下記の通りです。
(これから各リソースのコードを書いていくと同ディレクトリ構造になります。)

```shell:ターミナル

 % tree terraform

terraform
├── acm.tf
├── cloudfront.tf
├── ec2.tf
├── ecs.tf
├── elb.tf
├── main.tf
├── modules
│   ├── iam_role
│   　   └── main.tf
├── network.tf
├── rds.tf
├── route53.tf
├── s3.tf
├── security_group.tf
├── ssm.tf
├── terraform.tfstate
├── terraform.tfstate.backup
└── terraform.tfvars


```


# 2. ネットワーク

では、各リソースの土台となるネットワーク部分から作成していきましょう。

まず、VPC/SubnetなどのリソースをCidrブロックで構想します。


**`VPC：ネットワーク全体`**

| VPC名称 | CIDR | アドレス範囲 |
|:-:|:-:|:-:|
| app-production-vpc | 10.0.0.0/16 | 10.0.0.0 - 10.0.255.255 |


**`サブネット：リソースごとの領域`**

| サブネット名称 | CIDR | アドレス範囲 | region | 用途 | ルートテーブル |
|:-:|:-:|:-:|:-:|:-:|:-:|
| ecs-1a-subnet | 10.0.1.0/24 | 10.0.1.0 - 10.0.1.255 | ap-northeast-1a | ECS(ALB/fargate) | public-rt |
| ecs-1c-subnet | 10.0.2.0/24 | 10.0.2.0 - 10.0.2.255 | ap-northeast-1c | ECS(ALB/fargate) | public-rt |
| db-1a-subnet | 10.0.11.0/24 | 10.0.11.0 - 10.0.11.255 | ap-northeast-1a | DB | private-rt  |
| db-1c-subnet | 10.0.12.0/24 | 10.0.12.0 - 10.0.12.255 | ap-northeast-1c | DB | private-rt  |
| management-ec2-1a-subnet | 10.0.21.0/24 | 10.0.21.0 - 10.0.21.255 | ap-northeast-1a | 管理用EC2 | public-rt |
| management-ec2-1c-subnet | 10.0.22.0/24 | 10.0.22.0 - 10.0.22.255 | ap-northeast-1c | 管理用EC2 | public-rt |


**`インターネットゲートウェイ：インターネットに繋がる唯一の出入り口`**

| インターネットゲートウェイ名称 |
|:-:|
| igw |


**`ルートテーブル：サブネットの通信経路を決める`**

| ルートテーブル名称 | 送信先 | ターゲット |
|:-:|:-:|:-:|
| public-rt | 10.0.0.0/24, 0.0.0.0/0 | local, igw |
| private-rt | 10.0.0.0/24 | local |


それでは、構想したものをterraformコードに落とし込んでいきます。

terraformコードは、HCL（HashiCorpLanguage）という文法に基づいています。

HCLについて、全ては解説しませんので、ご了承ください。

resourceブロックでは、`resourceに続く、`
`１つ目の””で囲われた文字列は生成するAWSリソースを`
`２つ目の””で囲われた文字列はterraform内の識別子を`
記述しています。

`※terraform内の識別子は、自分の好きな名称を決められます。`

なお、resourceブロックで定義したリソースは他のresourceブロックから参照できます。
例えば、ECS用サブネットに着目すると、
`vpc_id =`の後に`aws_vpc.vpc.id`とVPCのリソースを参照しています。

さらに`id`の箇所は属性といい、各リソースによって使用できるものが異なります。
詳細を知りたい場合は、Terraformの公式ページから確認します。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs

一例ではありますが、VPCを生成する`aws_vpcというリソース`であれば
下記のように使える属性を調べます。

![スクリーンショット 2022-01-02 14.59.58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/73766004-3d20-4674-6631-440bf434a903.png)

では、AWSマネジメントコンソールからGUIで
ネットワークの各リソースを関連づけしていたように
terraformコードで、各リソースを紐付けるように記述していきます。


```terraform:terraform/network.tf
# -------------------------------------------
# VPC
# -------------------------------------------

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

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-vpc"
  }
}

# -------------------------------------------
# Subnet (マルチAZ)
# -------------------------------------------

### ECS用サブネット
resource "aws_subnet" "ecs_public_subnet_1a" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-1a"
  cidr_block              = "10.0.1.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-ecs-public-subnet-1a"
  }
}

resource "aws_subnet" "ecs_public_subnet_1c" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-1c"
  cidr_block              = "10.0.2.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-ecs-public-subnet-1c"
  }
}

### DB用サブネット
resource "aws_subnet" "db_private_subnet_1a" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-1a"
  cidr_block              = "10.0.11.0/24"
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-db-private-subnet-1a"
  }
}

resource "aws_subnet" "db_private_subnet_1c" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-1c"
  cidr_block              = "10.0.12.0/24"
  map_public_ip_on_launch = false

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-db-private-subnet-1c"
  }
}

### マネジメントEC2用サブネット
resource "aws_subnet" "management_ec2_public_subnet_1a" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-1a"
  cidr_block              = "10.0.21.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-management-ec2-public-subnet-1a"
  }
}

resource "aws_subnet" "management_ec2_public_subnet_1c" {
  vpc_id                  = aws_vpc.vpc.id
  availability_zone       = "ap-northeast-1c"
  cidr_block              = "10.0.22.0/24"
  map_public_ip_on_launch = true

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-management-ec2-public-subnet-1c"
  }
}

# -------------------------------------------
# InternetGateway
# -------------------------------------------

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

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-igw"
  }
}

# -------------------------------------------
# RootTable
# -------------------------------------------

### Public
resource "aws_route_table" "public_rt" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-public-rt"
  }
}

# Publicには、インターネットゲートウェイに繋がるルートを追加する
resource "aws_route" "public_rt_igw_r" {
  route_table_id         = aws_route_table.public_rt.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw.id
}

resource "aws_route_table_association" "ecs_public_rt_1a" {
  route_table_id = aws_route_table.public_rt.id
  subnet_id      = aws_subnet.ecs_public_subnet_1a.id
}
resource "aws_route_table_association" "ecs_public_rt_1c" {
  route_table_id = aws_route_table.public_rt.id
  subnet_id      = aws_subnet.ecs_public_subnet_1c.id
}
resource "aws_route_table_association" "management_ec2_public_rt_1a" {
  route_table_id = aws_route_table.public_rt.id
  subnet_id      = aws_subnet.management_ec2_public_subnet_1a.id
}
resource "aws_route_table_association" "management_ec2_public_rt_1c" {
  route_table_id = aws_route_table.public_rt.id
  subnet_id      = aws_subnet.management_ec2_public_subnet_1c.id
}

### Private
resource "aws_route_table" "private_rt" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-private-rt"
  }
}

resource "aws_route_table_association" "db_private_rt_1a" {
  route_table_id = aws_route_table.private_rt.id
  subnet_id      = aws_subnet.db_private_subnet_1a.id
}
resource "aws_route_table_association" "db_private_rt_1c" {
  route_table_id = aws_route_table.private_rt.id
  subnet_id      = aws_subnet.db_private_subnet_1c.id
}
```

ここまで、記述できたらAWSリソース生成のために
ターミナルから下記terraformコマンドを実行します。
(次章以降、当コマンドの実行は重複するため原則省略します。)

```shell:ターミナル

# Terraformの初期化
% terraform init

# コードを整列するようフォーマットする
% terraform fmt

# 現コードで生成されるリソースを確認
% terraform plan

# リソース生成の実行
% terraform apply -auto-approve

```

問題なく処理が完了したら、マネジメントコンソール画面から確認してみましょう。

![スクリーンショット 2022-01-02 15.31.52.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/0a32f952-e91c-333f-4130-96cf852136f9.png)



# 3. セキュリティグループ

続いて、AWSサービスのファイアーウォールにあたる
セキュリティグループを設定していきます。

セキュリティグループでは、リソースへの接続を制限する
インバウンドルールに注意して設定していきます。
（アウトバウンドルールは制限をかけていません。）

| セキュリティグループ名称 | タイプ | ソース | 用途 |
|:-:|:-:|:-:|:-:|
| ecs-sg | HTTP, HTTPS, カスタムTCP | 0.0.0.0, 0.0.0.0, management-ec2-sg | ALB/fargate |
| db-sg | MySQL | ecs-sg, management-ec2-sg | DB |
| management-ec2-sg  | SSH | 0.0.0.0 | 管理用EC2 |

ECSは、ユーザーからのアクセスが想定されるために、
HTTP/HTTPSのプロトコルで全IPアドレスを許可する設定にします。

DBは、ECSと管理用EC2のみアクセスできるように
２つのセキュリティグループに限定して許可する設定とします。

管理用EC2には、SSHプロトコルのみ接続が可能とします。

```terraform:terraform/security_group.tf
# -------------------------------------------
# SecurtyGroup
# -------------------------------------------

### 1.ECS用 ###
resource "aws_security_group" "ecs_sg" {
  name   = "ecs-sg"
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-ecs-sg"
  }
}

resource "aws_security_group_rule" "ecs_ingress_http" {
  type              = "ingress"
  from_port         = 80
  to_port           = 80
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.ecs_sg.id
}

resource "aws_security_group_rule" "ecs_ingress_https" {
  type              = "ingress"
  from_port         = 443
  to_port           = 443
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.ecs_sg.id
}

resource "aws_security_group_rule" "ecs_ingress_custom_tcp" {
  type                     = "ingress"
  from_port                = 10080
  to_port                  = 10080
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.management_ec2_sg.id
  security_group_id        = aws_security_group.ecs_sg.id
}

resource "aws_security_group_rule" "ecs_egress" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.ecs_sg.id
}

### 2. DB用 ###
resource "aws_security_group" "db_sg" {
  name   = "db-sg"
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-db-sg"
  }
}

resource "aws_security_group_rule" "db_ingress_mysql_from_ecs" {
  type                     = "ingress"
  from_port                = 3306
  to_port                  = 3306
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.ecs_sg.id
  security_group_id        = aws_security_group.db_sg.id
}

resource "aws_security_group_rule" "db_ingress_mysql_from_management_ec2" {
  type                     = "ingress"
  from_port                = 3306
  to_port                  = 3306
  protocol                 = "tcp"
  source_security_group_id = aws_security_group.management_ec2_sg.id
  security_group_id        = aws_security_group.db_sg.id
}

resource "aws_security_group_rule" "db_egress" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.db_sg.id
}

### 3.マネジメントEC2用 ###
resource "aws_security_group" "management_ec2_sg" {
  name   = "management-ec2-sg"
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-management-ec2-sg"
  }
}

resource "aws_security_group_rule" "management_ec2_ingress_ssh" {
  type              = "ingress"
  from_port         = 22
  to_port           = 22
  protocol          = "tcp"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.management_ec2_sg.id
}

resource "aws_security_group_rule" "management_ec2_egress" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
  security_group_id = aws_security_group.management_ec2_sg.id
}

```

では、terraformコマンドを実行して、リソースが追加で
生成できたかをマネジメントコンソールで確認します。



# 4. EC2/ RDS

次は、EC2とRDSをネットワークに配置していきましょう。

## ⑴ EC2

では、EC2から着手していきます。

EC2本体のコードを書く前に

* AMI
* key pair

を先に準備していきます。

はじめに**`AMI`**から準備します。

ただ、AMIはマネジメントコンソールからAMIを特定するIDをコピーして
直接EC2のamiに貼り付けても問題はありません。

![スクリーンショット 2022-01-02 16.10.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/a7cb0275-a2f4-4ae8-18b7-75eb1cc3b785.png)

当記事では、dataブロックを用いて
AWS外部データにアクセスして、指定条件からamiを特定し
それをEC2のamiに紐づける方法で実装しています。

最初にamiの詳細情報を調べるためにaws cliを使います。
ターミナルから下記コマンドを実行して詳細情報を確認しましょう。

```shell:ターミナル

# acmの詳細情報を表示する
% aws ec2 describe-images --image-ids ami-0218d08a1f9dac831


{
    "Images": [
        {
           ----省略----

            ### nameフィルターで使用
            "Description": "Amazon Linux 2 Kernel 5.10 AMI 2.0.20211201.0 x86_64 HVM gp2",
            "EnaSupport": true,
            "Hypervisor": "xen",
            "ImageOwnerAlias": "amazon",
            "Name": "amzn2-ami-kernel-5.10-hvm-2.0.20211201.0-x86_64-gp2",
            "RootDeviceName": "/dev/xvda",
            ### root-device-typeフィルターで使用
            "RootDeviceType": "ebs",
            "SriovNetSupport": "simple",
            ### virtualization-typeフィルターで使用
            "VirtualizationType": "hvm"
        }
    ]
}

```

では、さきほど調べた３つの情報を各filterに設定していきます。

なお、補足ですが
most_recentは最新版を選択する設定で、
更に３つあるfilterのうち、nameは
*（ワイルドカード）を用いて、どの日付でもヒットするよう条件を緩めています。

これによって、`複数ヒットするamiから最新版を選んでくれる`
と設定になります。


```terraform:terraform/ec2.tf

### AMI ###
data "aws_ami" "management_ec2_ami" {
  most_recent = true
  owners      = ["self", "amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-kernel-5.10-hvm-2.0.*-x86_64-gp2"]
  }
  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }
  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

```

続いて、**`key pair`**です。

`マネジメントコンソールからGUI操作で構築していた時`は
最初に、AWS側にキーペア（公開鍵＋秘密鍵）を生成し、
そのキーペアのうち、秘密鍵（~.pem）をローカルに渡して
ローカルからAWSリソースへSSHで接続可能としていました。

`Terraformから生成する場合`は
ローカルでキーペア(公開鍵＋秘密鍵)を作成して
キーペアのうち、公開鍵(~.pub)をAWSに登録します。

いずれも最終的には

* AWS: 公開鍵
* ローカル: 秘密鍵

という状態になります。

では、最初にターミナルからキーペアを作成します。

```shell:ターミナル

# 既存のSSHキーが存在するか確認
% ls -al ~/.ssh

# 未使用の無駄なSSHキーがあれば削除しておく
% rm -rf ~/.ssh/未使用のキー

# RSA4096ビット形式のSSHキーを作成する
% ssh-keygen -t rsa -b 4096

 ↓ 質問に答えていく
# SSHキーの保存先パスとファイル名を決める
#  -> そのままEnterと押すと()内のデフォルト値で生成される 
#    ※ 今回は「keypair_aws」としている
Enter file in which to save the key (/Users/ユーザー名/.ssh/id_rsa): /Users/ユーザー名/.ssh/keypair_aws

# SSHキーのパスフレーズ設定
Enter passphrase (empty for no passphrase): 任意のパスワードを入力

# パスフレーズの再確認
Enter same passphrase again: 再度パスワードを入力

 ↓　入力が完了すると、秘密鍵と公開鍵が生成される

Your identification has been saved in /Users/ユーザー名/.ssh/keypair_aws.
Your public key has been saved in /Users/ユーザー名/.ssh/keypair_aws.pub.

# 生成されたSSHキーの確認
% ls -al ~/.ssh

```

lsコマンドで表示されるファイルのうち
`.pubの拡張子がついているファイルは公開鍵を指します。
何もついていない鍵は秘密鍵ですので、拡張子「.pem」を付けておきましょう。`

それでは、生成したキーペアの公開鍵（~.pub）を
terraformコードで、AWSに登録していきます。

```terraform:terraform/ec2.tf
### key pair ###
resource "aws_key_pair" "keypair" {
  key_name   = "${var.tool}-${var.project}-${var.environment}-management-ec2-keypair"
  public_key = file("~/.ssh/keypair_aws.pub")

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-management-ec2-keypair"
  }
}
```

では、EC2本体を実装していきます。

先ほど設定したAMIとkeypairをEC2に紐づける他に、aws_eipで
`ElasticIPをEC2に関連づけてIPアドレスを固定化させています。`

最後のoutputは、自動生成されたEC2のパブリックIPアドレスを出力しています。
SSH接続時に必要なIPアドレスをコードからも確認できる仕組みとしています。

```terraform:terraform/ec2.tf

# -------------------------------------------
# Management EC2
# -------------------------------------------
### AMI ###
data "aws_ami" "management_ec2_ami" {
  most_recent = true
  owners      = ["self", "amazon"]

  filter {
    name   = "name"
    values = ["amzn2-ami-kernel-5.10-hvm-2.0.*-x86_64-gp2"]
  }
  filter {
    name   = "root-device-type"
    values = ["ebs"]
  }
  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}

### key pair ###
resource "aws_key_pair" "keypair" {
  key_name   = "${var.tool}-${var.project}-${var.environment}-management-ec2-keypair"
  public_key = file("~/.ssh/keypair_aws.pub")

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-management-ec2-keypair"
  }
}

### EC2 instance ###
resource "aws_instance" "management_ec2" {
  ami                         = data.aws_ami.management_ec2_ami.id
  instance_type               = "t2.micro"
  subnet_id                   = aws_subnet.management_ec2_public_subnet_1a.id
  associate_public_ip_address = true
  vpc_security_group_ids      = [aws_security_group.management_ec2_sg.id]
  key_name                    = aws_key_pair.keypair.key_name

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-management-ec2"
  }
}

### Elastic IP ###
resource "aws_eip" "management_ec2_eip" {
  vpc      = true
  instance = aws_instance.management_ec2.id

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-management-ec2-eip"
  }
}

output "ElasticIP" {
  value = aws_eip.management_ec2_eip.public_ip
}

```

## ⑵ RDS

続いて、RDSです。

一部のみ解説いたします。

パラメータグループは、MySQLのデフォルトでセットされていない
日本語に対応させるため、文字コードにutf8mb4を指定しています。

サブネットグループは、RDS生成時には必須である為
作成済みのサブネットをまとめる設定をしています。

random_stringは、ランダムな文字列を生成できるTerraformのツールです。
ここでは、記号を使用しない16文字で生成するルールとしています。
パスワードはgitignoreで無視させているterraform.tfstateに出力されます。

**`identifier`**は、**`AWSで生成されるDBの名称`**で
**`name`**は、**`MySQLで生成されるDBの名称`**です。
`(RailsがMySQLに接続する時はnameで定めた文字列をdatabase.ymlに定義しておく必要がある）`

deletion_protectionは削除保護になりますので
むやみにDBを削除させない為には`true`としておいた方が安全でしょう。
私の場合は、まだポートフォリオが完成していないので
削除しやすいように`false`としています。

```terraform:terraform/rds.tf
# -------------------------------------------
# ParameterGroup
# -------------------------------------------

resource "aws_db_parameter_group" "mysql_parametergroup" {
  name   = "${var.tool}-${var.project}-${var.environment}-mysql-parametergroup"
  family = "mysql8.0"

  parameter {
    name  = "character_set_database"
    value = "utf8mb4"
  }

  parameter {
    name  = "character_set_server"
    value = "utf8mb4"
  }
}

# -------------------------------------------
# SubnetGroup
# -------------------------------------------

resource "aws_db_subnet_group" "mysql_subnet_group" {
  name = "${var.tool}-${var.project}-${var.environment}-mysql-subnetgroup"
  subnet_ids = [
    aws_subnet.db_private_subnet_1a.id,
    aws_subnet.db_private_subnet_1c.id
  ]

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-mysql-subnetgroup"
  }
}

# -------------------------------------------
# RDS instance
# -------------------------------------------

resource "random_string" "db_password" {
  length  = 16
  special = false
}

resource "aws_db_instance" "mysql" {
  engine         = "mysql"
  engine_version = "8.0.27"
  # AWSで作成されるデータベース名
  identifier     = "${var.tool}-${var.project}-${var.environment}-mysql"
  username       = "admin"
  password       = random_string.db_password.result
  instance_class = "db.t2.micro"

  allocated_storage     = 20
  max_allocated_storage = 50
  storage_type          = "gp2"
  storage_encrypted     = false

  multi_az               = false
  availability_zone      = "ap-northeast-1a"
  db_subnet_group_name   = aws_db_subnet_group.mysql_subnet_group.name
  vpc_security_group_ids = [aws_security_group.db_sg.id]
  publicly_accessible    = false
  port                   = 3306

  # MySQLで作成されるデータベース名
  name                 = "${var.project}_db"
  parameter_group_name = aws_db_parameter_group.mysql_parametergroup.name

  backup_window              = "04:00-05:00"
  backup_retention_period    = 7
  maintenance_window         = "Mon:05:00-Mon:06:00"
  auto_minor_version_upgrade = false

  deletion_protection = false
  skip_final_snapshot = true
  apply_immediately   = true

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-mysql"
  }
}

output "rds_endpoint" {
  value = aws_db_instance.mysql.address
}
```

ここまで完了できたら、これまでと同様にterraformコマンドで
現時点のリソースを反映させましょう。



# 5. ALB/ Route53/ ACM

続いて、
・通信をリスナーごとに振り分けるロードバランサーのALB
・名前解決を行うDNSサーバーのRoute53
・HTTPS通信で必要になる証明書のACM
を生成していきます。

## ⑴ ALB
では、まずALBから実装していきましょう。

ここでも一部のみ解説します。

enable_deletion_protectioは削除保護をする設定ですが
今回はコメントアウトで設定していませんので
terraform destroyコマンドで消しやすいようにしています。

access_logsは、後で実装予定のアクセスログ用S3が関わってきますので
現時点はコメントアウトしています。

そして、ポイントとなる`リスナーとターゲットグループ`は
以下の通りで作成していきます。

| リスナー名称 | ポート番号 | デフォルトアクション | 用途 | 備考 |
|:-:|:-:|:-:|:-:|:-:|
| http | 80 |　redirect　| リダイレクト専用 | 80ポートを443ポートにリダイレクトさせる |
| https | 443 |　forward　| 本番用リスナー | 443ポートからアクセスしたユーザー |
| custom_10080 | 10080 |　forward　| テスト用リスナー | 10080ポートからアクセスしたテストユーザー |

| ターゲットグループ名称 | ポート番号 | 用途 |
|:-:|:-:|:-:|
| blue_tg | 443 | blue/greenデプロイメントの転送先　|
| green_tg | 10080 | blue/greenデプロイメントの転送先　 |

通信の流れを理解する為に
`リスナー：　一般/テストユーザーなどの外から通信を受け付ける対象`
`ターゲットグループ：　受け取った通信を流すネットワーク内の場所`
でイメージを捉えておくと良いと思います。

なお、health_checkのpathは、アプリ構成によって異なる箇所です。
ご自身のヘルスチェックを行うパスに合わせましょう。

```terraform:terraform/elb.tf

# -------------------------------------------
# ALB
# -------------------------------------------

### ALB本体 ###

resource "aws_lb" "alb" {
  name               = "${var.tool}-${var.project}-${var.environment}-alb"
  load_balancer_type = "application"
  internal           = false
  # 削除保護は開発時には設定しない
  # enable_deletion_protection = true

  subnets = [
    aws_subnet.ecs_public_subnet_1a.id,
    aws_subnet.ecs_public_subnet_1c.id,
  ]

  ### アクセスログ用のS3
  #access_logs {
  #  bucket  = aws_s3_bucket.alb_accesslogs_bucket.id
  #  enabled = true
  #}

  security_groups = [
    aws_security_group.ecs_sg.id
  ]
}

output "alb_dns_name" {
  value = aws_lb.alb.dns_name
}

### リスナー ###

## 1.HTTPリスナー ##
resource "aws_lb_listener" "http" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type = "redirect"

    redirect {
      port        = "443"
      protocol    = "HTTPS"
      status_code = "HTTP_301"
    }
  }
}

## 2.プロダクションリスナー(HTTPS) ##
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = aws_acm_certificate.tokyo_cert.arn

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

## 3.テストリスナー(カスタムTCP：10080) ##
resource "aws_lb_listener" "custom_10080" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "10080"
  protocol          = "HTTP"

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


# -------------------------------------------
# TargetGroup
# -------------------------------------------

### 1.blue target group ###
resource "aws_lb_target_group" "blue_tg" {
  name                 = "${var.tool}-${var.project}-${var.environment}-blue-tg"
  target_type          = "ip"
  vpc_id               = aws_vpc.vpc.id
  port                 = 80
  protocol             = "HTTP"
  deregistration_delay = 300

  health_check {
    path                = "/api/healthcheck"
    healthy_threshold   = 3
    unhealthy_threshold = 2
    timeout             = 5
    interval            = 15
    matcher             = 200
    port                = "traffic-port"
    protocol            = "HTTP"
  }

  depends_on = [aws_lb.alb]
}

### 2.green target group ###
resource "aws_lb_target_group" "green_tg" {
  name                 = "${var.tool}-${var.project}-${var.environment}-green-tg"
  target_type          = "ip"
  vpc_id               = aws_vpc.vpc.id
  port                 = 10080
  protocol             = "HTTP"
  deregistration_delay = 300

  health_check {
    path                = "/api/healthcheck"
    healthy_threshold   = 3
    unhealthy_threshold = 2
    timeout             = 5
    interval            = 15
    matcher             = 200
    port                = "traffic-port"
    protocol            = "HTTP"
  }

  depends_on = [aws_lb.alb]
}

```

## ⑵ Route53

次はRoute53の実装です。

私は、`AWSのRoute53からドメインを購入しています`。

もし`異なるレジストラからドメインを取得している方は
ホストゾーンなど少しだけ実装が違うかもしれません。`

ここでは、
`DNSサーバーを担うAWSサービスのRoute53`に
`ドメイン名をもとにアクセスしてきた通信をALBに向ける`という
`名前解決`の設定を行います。

なお、a_record_cloudfrontは、後の実装で関わってくる
CloudFront用のレコードなので、現時点ではコメントアウトしています。

表でまとめると下記となります。

| レコード名称 | ドメイン | 対象リソース |
|:-:|:-:|:-:|
| a_record_alb | Route53で取得したドメイン名 | ALB |
| a_record_cloudfront | ホスト名.Route53で取得したドメイン名 | CloudFront |


また、route53_acm_dns_resolveは
証明書ACMで使うDNS検証用のレコード設定です。
次章で説明しますが、terraform公式コードを参考にしています。

```terraform:terraform/route53.tf

# -------------------------------------------
# Route53
# -------------------------------------------

### ドメインの取得(レジストリ：Route53) ###
data "aws_route53_zone" "hostzone" {
  name = "Route53で取得したドメイン名"
}

### Aレコード：ALB ###
resource "aws_route53_record" "a_record_alb" {
  zone_id = data.aws_route53_zone.hostzone.zone_id
  name    = data.aws_route53_zone.hostzone.name
  type    = "A"

  alias {
    name                   = aws_lb.alb.dns_name
    zone_id                = aws_lb.alb.zone_id
    evaluate_target_health = true
  }
}

output "domain_name" {
  value = aws_route53_record.a_record_alb.name
}

### 後の実装
### Aレコード：CloudFront ###
#resource "aws_route53_record" "a_record_cloudfront" {
#  zone_id = data.aws_route53_zone.hostzone.zone_id
#  name    = "ホスト名.Route53で取得したドメイン名"
#  type    = "A"

#  alias {
#    name                   = aws_cloudfront_distribution.cf.domain_name
#    zone_id                = aws_cloudfront_distribution.cf.hosted_zone_id
#    evaluate_target_health = true
#  }
#}

### 証明書のDNS検証用レコード ###
resource "aws_route53_record" "route53_acm_dns_resolve" {
  for_each = {
    for dvo in aws_acm_certificate.tokyo_cert.domain_validation_options : dvo.domain_name => {
      name   = dvo.resource_record_name
      type   = dvo.resource_record_type
      record = dvo.resource_record_value
    }
  }

  allow_overwrite = true
  zone_id         = data.aws_route53_zone.hostzone.zone_id
  name            = each.value.name
  type            = each.value.type
  records         = [each.value.record]
  ttl             = 60
}

```

## ⑶ ACM

続いて、ACMの作成です。

ACM証明書は、以下の通りで２つ発行します。

| ACM名称 | リージョン | 用途 | 備考 |
|:-:|:-:|:-:|:-:|
| tokyo_cert | ap-northeast-1 | ALB | デフォルトのリージョン |
| virginia_cert | us-east-1 | CloudFront | providerのエイリアスを用いてリージョン切り替え |

CloudFront用のvirginia_certは、東京リージョンでは作成できません。
そこでmain.tfに記述しているproviderのaliasで工夫し、
リージョンを切替しています。

なお、ACM証明書のDNS検証については
Terraform公式ページにコードが記述されていますので、ご確認ください。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate_validation


```terraform:terraform/main.tf
----省略----

# -------------------------------------------
# Provider
# -------------------------------------------
provider "aws" {
  profile = "terraform"
  region  = "ap-northeast-1"
}

provider "aws" {
  alias   = "virginia"
  profile = "terraform"
  region  = "us-east-1"
}

----省略----

```

```terraform:terraform/acm.tf

# -------------------------------------------
# AWS Certificate Manager
# -------------------------------------------

### 1、ALB用のHTTPS証明書 ###
resource "aws_acm_certificate" "tokyo_cert" {
  domain_name       = data.aws_route53_zone.hostzone.name
  validation_method = "DNS"

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-alb-sslcert"
  }

  lifecycle {
    create_before_destroy = true
  }
}

### 証明書のDNS検証 ###
resource "aws_acm_certificate_validation" "cert_valid" {
  certificate_arn         = aws_acm_certificate.tokyo_cert.arn
  validation_record_fqdns = [for record in aws_route53_record.route53_acm_dns_resolve : record.fqdn]
}

### 2、CloudFront用のHTTPS証明書 ###
resource "aws_acm_certificate" "virginia_cert" {
  provider          = aws.virginia
  domain_name       = "*.取得したドメイン名"
  validation_method = "DNS"

  tags = {
    Name = "${var.tool}-${var.project}-${var.environment}-cloudfront-sslcert"
  }

  lifecycle {
    create_before_destroy = true
  }
}

```

ここまでできたら、terraformコマンドを実行してリソースを生成します。

`参考まで：（検証作業）`

なお、リソース生成後、リスナー側を検証したい場合
defaullt_actionをfixed-responseにして
プレーンテキストが返されるか見てみるのも良いと思います。

検証は、ターミナルからcurlコマンドを実行すると出来ます。

```terraform:terraform/elb.tf
### リスナー ###

## 2.プロダクションリスナー(HTTPS) ##
resource "aws_lb_listener" "https" {
  load_balancer_arn = aws_lb.alb.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"
  certificate_arn   = aws_acm_certificate.tokyo_cert.arn

  ## 一時的にコメントアウト
  #default_action {
  #  type             = "forward"
  #  target_group_arn = aws_lb_target_group.blue_tg.arn
  #}

  ## 検証：　プレーンテキストがレスポンスされるか
  default_action {
    type = "fixed-response"

    fixed_response {
      content_type = "text/plain"
      message_body = "HTTPのテスト"
      status_code  = "200"
    }
  }
}

```

```shell:ターミナル

% curl https://ドメイン名

```



# 6. SystemsManager/ ECS

それでは、
・秘匿情報を管理するSystemsManager
・コンテナを制御するECS(cluster, service, task-def)
を作成していきましょう。

## ⑴ SystemsManager

まず、SystemsManagerから実装します。

ここでは、RailsからMySQLに接続するに必要な3つの情報を
SystemsManagerに登録しています。

| 名称 | Key値 | Value値 | 備考 |
|:-:|:-:|:-:|:-:|
| db_username | "/container-param/db-username" | admin | そのまま使用 |
| db_password | "/container-param/db-password" | uninitialized | aws cliで書き換え |
| db_host | "/container-param/db-host" | uninitialized | aws cliで書き換え |

全てタイプは`SecureString`です。

Value値について補足ですが、
**`db_username`**は**`「admin」をそのままの値として利用し、`**
**`db_password, db_host`**は**`仮の値として「uninitialized」とし、
SystemsManager登録後に、ターミナルからaws cliで値を上書きします。`**

```terraform:terraform/ssm.tf

# -------------------------------------------
# Systems Manager
# -------------------------------------------

resource "aws_ssm_parameter" "db_username" {
  name        = "/container-param/db-username"
  value       = "admin"
  type        = "SecureString"
  description = "MySQLのユーザー名"

  lifecycle {
    ignore_changes = [value]
  }
}

resource "aws_ssm_parameter" "db_password" {
  name        = "/container-param/db-password"
  value       = "uninitialized"
  type        = "SecureString"
  description = "AWSCLIを用いて初期値から変更"

  lifecycle {
    ignore_changes = [value]
  }
}

resource "aws_ssm_parameter" "db_host" {
  name        = "/container-param/db-host"
  value       = "uninitialized"
  type        = "SecureString"
  description = "AWSCLIを用いて初期値から変更"

  lifecycle {
    ignore_changes = [value]
  }
}

```

ここまでで、terraformコマンドを実行してリソースを生成しましょう。

リソース生成後に、`ターミナルからaws cliを実行して
SystemsManagerのパラメーターストアに登録している２つ値を上書きします。`

なお、`RDSパスワードはrandom_stringが自動生成しています。`
その為、`VScode`で`terraform.tfstate`ファイル内を
`password`で検索をかければ、パスワードが表示できるはずです。

`RDSエンドポイントは、マネジメントコンソールでもterraform.tfstateでも
どちらでもいいのでコピペしてください。`

```shell:ターミナル

% aws ssm put-parameter --name '/container-param/db-password' --type SecureString --value 'RDSパスワードの値' --overwrite

% aws ssm put-parameter --name '/container-param/db-host' --type SecureString --value 'RDSエンドポイントの値' --overwrite

```

それでは、値が更新できているかをマネジメントコンソールの
SystemsManagerのパラメータストアから確認しましょう。

db-host, db-passwordをクリックして、保存されている値が
先ほど、ターミナルから入力した値を合致していればOKです。

![スクリーンショット 2022-01-03 11.04.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/b88ad5d7-9104-89ed-1a62-c21abd0de5a9.png)


## ⑵ ECS

ここから、ECSの設定に入ります。

コードがかなり膨大なので、一部だけ解説します。

desired_countは、ECSで起動させるタスク数です。
当記事では1としておりますが、このままでは障害が発生した時に
再起動まで時間が掛かる為、可用性を意識するなら2以上が望ましいでしょう。

deployment_controllerは、blue/greenデプロイメントを実現する為に
AWSのCodeDeployを指定しています。
Terraform公式ページにコードが載っていますので、ご確認ください。

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codedeploy_app

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/codedeploy_deployment_group

container_definitionsは、
`file参照ではなく、jsonencodeで各項目を直接記述しています。`
secretsは、
`先ほどSystemsManagerに登録した情報を読み取り、
コンテナ（Rails）の環境変数に埋め込んでいます。`

ecs_task_execution_role　と　ecs_code_deploy_role　は
IAMロールで、モジュールを用いて、それぞれ作成しています。
`1つ目のdataブロックでAWS公式のIAMロールを参照し、`
`2つ目のdataブロックでポリシードキュメントとして定義しておき`
`最後にmoduleで同ポリシードキュメントを紐づけています。`
`（タスク実行ロールは、statementでSystemsManagerへのアクセス権限を付けています。）`

```terraform:terraform/ecs.tf
# -------------------------------------------
# ECS
# -------------------------------------------

### クラスター ###
resource "aws_ecs_cluster" "cluster" {
  name = "${var.tool}-${var.project}-${var.environment}-ecs-backend-rails-cluster"
}

### サービス ###
resource "aws_ecs_service" "service" {
  name            = "${var.tool}-${var.project}-${var.environment}-ecs-backend-rails-service"
  cluster         = aws_ecs_cluster.cluster.arn
  task_definition = aws_ecs_task_definition.task_def.arn
  # 最終的に可用性を上げるために2に変更する
  desired_count                     = 1
  launch_type                       = "FARGATE"
  platform_version                  = "1.4.0"
  health_check_grace_period_seconds = 180

  network_configuration {
    assign_public_ip = true
    security_groups  = [aws_security_group.ecs_sg.id]

    subnets = [
      aws_subnet.ecs_public_subnet_1a.id,
      aws_subnet.ecs_public_subnet_1c.id
    ]
  }

  load_balancer {
    target_group_arn = aws_lb_target_group.blue_tg.arn
    container_name   = "自分のコンテナ名"
    container_port   = 80
  }

  deployment_controller {
    type = "CODE_DEPLOY"
  }

  lifecycle {
    ignore_changes = [task_definition, load_balancer]
  }

  depends_on = [aws_db_instance.mysql]
}

### タスク定義 ###
resource "aws_ecs_task_definition" "task_def" {
  family                   = "${var.tool}-${var.project}-${var.environment}-ecs-backend-rails-task-def"
  cpu                      = 256
  memory                   = 512
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  execution_role_arn       = module.ecs_task_execution_role.iam_role_arn

  container_definitions = jsonencode([
    {
      name      = "自分のコンテナ名"
      image     = aws_ecr_repository.backend.repository_url
      essential = true
      portMappings = [
        {
          containerPort = 80
          hostPort      = 80
        }
      ]
      logConfiguration = {
        logDriver : "awslogs",
        options : {
          awslogs-region : "ap-northeast-1",
          awslogs-stream-prefix : "backend",
          awslogs-group : aws_cloudwatch_log_group.ecs_backend.name
        }
      },
      secrets = [
        {
          name      = "DB_USERNAME",
          valueFrom = "/container-param/db-username"
        },
        {
          name      = "DB_PASSWORD",
          valueFrom = "/container-param/db-password"
        },
        {
          name      = "DB_HOST",
          valueFrom = "/container-param/db-host"
        }
      ]
    }
  ])
}

### 1.IAMロール：タスク実行ロール ###
data "aws_iam_policy" "ecs_task_execution_role_policy" {
  arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

data "aws_iam_policy_document" "ecs_task_execution" {
  source_json = data.aws_iam_policy.ecs_task_execution_role_policy.policy

  statement {
    effect    = "Allow"
    actions   = ["ssm:GetParameters"]
    resources = ["*"]
  }
}

module "ecs_task_execution_role" {
  source     = "./modules/iam_role"
  name       = "ecs-task-execution"
  identifier = "ecs-tasks.amazonaws.com"
  policy     = data.aws_iam_policy_document.ecs_task_execution.json
}

### 2.IAMロール：CodeDeployロール ###
data "aws_iam_policy" "ecs_code_deploy_role_policy" {
  arn = "arn:aws:iam::aws:policy/AWSCodeDeployRoleForECS"
}

data "aws_iam_policy_document" "ecs_code_deploy" {
  source_json = data.aws_iam_policy.ecs_code_deploy_role_policy.policy
}

module "ecs_code_deploy_role" {
  source     = "./modules/iam_role"
  name       = "ecs-code-deploy"
  identifier = "codedeploy.amazonaws.com"
  policy     = data.aws_iam_policy_document.ecs_code_deploy.json
}

### CodeDeploy ###

resource "aws_codedeploy_app" "codedeploy_app" {
  compute_platform = "ECS"
  name             = "${var.tool}-${var.project}-${var.environment}-codedeploy-app"
}

resource "aws_codedeploy_deployment_group" "codedeploy_dg" {
  app_name               = aws_codedeploy_app.codedeploy_app.name
  deployment_group_name  = "${var.tool}-${var.project}-${var.environment}-codedeploy-dg"
  service_role_arn       = module.ecs_code_deploy_role.iam_role_arn
  deployment_config_name = "CodeDeployDefault.ECSAllAtOnce"

  auto_rollback_configuration {
    enabled = true
    events  = ["DEPLOYMENT_FAILURE"]
  }

  blue_green_deployment_config {
    deployment_ready_option {
      action_on_timeout = "CONTINUE_DEPLOYMENT"
    }

    terminate_blue_instances_on_deployment_success {
      action                           = "TERMINATE"
      termination_wait_time_in_minutes = 5
    }
  }

  deployment_style {
    deployment_option = "WITH_TRAFFIC_CONTROL"
    deployment_type   = "BLUE_GREEN"
  }

  ecs_service {
    cluster_name = aws_ecs_cluster.cluster.name
    service_name = aws_ecs_service.service.name
  }

  load_balancer_info {
    target_group_pair_info {
      prod_traffic_route {
        listener_arns = [aws_lb_listener.https.arn]
      }

      target_group {
        name = aws_lb_target_group.blue_tg.name
      }

      target_group {
        name = aws_lb_target_group.green_tg.name
      }
    }
  }
}

### ECR ###
resource "aws_ecr_repository" "backend" {
  name = "${var.tool}-${var.project}-${var.environment}-backend-rails-repo"
}

### CloudWatch Logs ###
resource "aws_cloudwatch_log_group" "ecs_backend" {
  name              = "/${var.tool}-${var.project}-${var.environment}/ecs/backend"
  retention_in_days = 180
}

```

ここまでできたら、同じようにterraformコマンドを実行してみましょう。

リソース生成後の動作検証は**[ECS起動の確認作業](https://qiita.com/Shuhei_Nakada/items/68bb082c0e5085b22a16#ecs%E8%B5%B7%E5%8B%95%E3%81%AE%E7%A2%BA%E8%AA%8D%E4%BD%9C%E6%A5%AD)**を実施してください。



### (番外) ECS Exec

ECS/Fargate構成で生成したコンテナをローカルから操作する為の
追加実装なので、こちらは必要であれば実装してください。

私はコンテナからRaisのマイグレーション作業など試したかったので
実装しました。

それでは、最初はECSのサービスで
enable_execute_commandをtrueとして
execute_commandコマンドが使用できるように有効化させます。

続いて、ECSのタスク定義で
task_role_arnに新しく作成するIAMロールを紐付けます。

最後にECSのタスク定義に紐付けるIAMロールを作成します。
ここではstatementでjsonコードを直接記述し、ポリシーを定めています。
(dataブロックでベースとなるポリシーは取得せずにコードで書いています。)

■ポリシー

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-exec.html#ecs-exec-required-iam-permissions


```terraform:terraform/ecs.tf
### サービス ###
resource "aws_ecs_service" "service" {
  ----省略----
  enable_execute_command            = true
  ----省略----
}

### タスク定義 ###
resource "aws_ecs_task_definition" "task_def" {
  ----省略----
  task_role_arn            = module.ecs_task_role.iam_role_arn
  ----省略----
}

### 1.IAMロール：タスク実行ロール ###
----省略----

### 2.IAMロール：タスクロール ###
data "aws_iam_policy_document" "ecs_task" {
  version = "2012-10-17"
  statement {
    effect = "Allow"
    actions = [
      "ssmmessages:CreateControlChannel",
      "ssmmessages:CreateDataChannel",
      "ssmmessages:OpenControlChannel",
      "ssmmessages:OpenDataChannel"
    ]
    resources = ["*"]
  }
}

module "ecs_task_role" {
  source     = "./modules/iam_role"
  name       = "ecs-task"
  identifier = "ecs-tasks.amazonaws.com"
  policy     = data.aws_iam_policy_document.ecs_task.json
}

### 3.IAMロール：CodeDeployロール ###
----省略----

```




# 7. CloudFront/ S3　

続いて、
・コンテンツを高速配信するCloudFront
・画像やログを保管するS3
を作成します。

## ⑴ CloudFront

まずは、CloudFrontから実装します。

ここでも、一部のみの解説となります。

originのdomain_nameとorigin_idでは、接続先となる
パブリックアクセス用S3バケットを紐づけています。

origin_access_identityはS3にアクセスする為に設定が必要なものです。
別リソースaws_cloudfront_origin_access_identityで
定義しているものを参照しています。

default_cache_behaviorでは、
**`どういうURLを受け付けて、どこに振り分けるか`**を設定する項目です。
ここでは、`S3による静的コンテンツの表示を目的としている為
query_string, headers, cookies の情報は転送しないようにしており、
min_ttl以降でキャッシュの設定をしています。`

restrictionsでは、ロケーションを指定してアクセスを制限できます。
ですが、ここでは制限をかけずに明記だけしています。

aliasesでは、取得したドメインにホスト名を付して定義しています。

viewer_certificateは、証明書の設定です。
以前にACMで作成した証明書virginia_certを関連付けしています。

```terraform:terraform/cloudfront.tf
# -------------------------------------------
# CloudFront
# -------------------------------------------
resource "aws_cloudfront_distribution" "cf" {
  enabled     = true
  price_class = "PriceClass_All"

  origin {
    domain_name = aws_s3_bucket.public_access_bucket.bucket_regional_domain_name
    origin_id   = aws_s3_bucket.public_access_bucket.id

    s3_origin_config {
      origin_access_identity = aws_cloudfront_origin_access_identity.cf_s3_origin_access_identity.cloudfront_access_identity_path
    }
  }

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = aws_s3_bucket.public_access_bucket.id

    forwarded_values {
      query_string = false
      headers      = []
      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 86400
    max_ttl                = 31536000
    compress               = true
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  aliases = ["ホスト名.ドメイン名"]

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.virginia_cert.arn
    minimum_protocol_version = "TLSv1.2_2019"
    ssl_support_method       = "sni-only"
  }
}

resource "aws_cloudfront_origin_access_identity" "cf_s3_origin_access_identity" {
  comment = "public access bucket identity"
}

```




## ⑵ S3

最後に、S3バケットを作成します。

作成するS3は次の２つです。

1. Railsからの画像投稿を保存するS3
2. ALBのアクセスログを保管するS3

1.は、Railsでcarrier_waveを使っている前提となりますが、
`config.fog_directoryで、パブリックアクセス用のS3名`
`config.asset_hostで、保存先のURL名`
を設定します。


```ruby:config/initializers/carrier_wave.rb

if Rails.env.production?
  CarrierWave.configure do |config|
    config.fog_credentials = {
      provider: 'AWS',
      aws_access_key_id: Rails.application.credentials.dig(:aws, :access_key_id),
      aws_secret_access_key: Rails.application.credentials.dig(:aws, :secret_access_key),
      region: 'ap-northeast-1'
    }
    config.fog_directory = 'パブリックアクセス用のS3名'
    config.asset_host = 'https://ホスト名.ドメイン名'
  end
end

```

2.は、elb.tfでコメントアウトしていたものをコメントインさせて
ALBにS3を紐付けして、アクセスログ用のS3も設定します。

```terraform:terraform/elb.tf

# -------------------------------------------
# ALB
# -------------------------------------------

----省略----

  ### コメントイン
  ## アクセスログ用のS3
  access_logs {
    bucket  = aws_s3_bucket.alb_accesslogs_bucket.id
    enabled = true
  }
}
```

S3の設定は以下のように行います。

```terraform:terraform/s3.tf
# -------------------------------------------
# Public Access Bucket
# -------------------------------------------
resource "aws_s3_bucket" "public_access_bucket" {
  bucket        = "${var.tool}-${var.project}-${var.environment}-public-access-bucket"
  force_destroy = true
  acl           = "public-read"

  cors_rule {
    allowed_origins = ["https://ドメイン名"]
    allowed_methods = ["GET"]
    allowed_headers = ["*"]
    max_age_seconds = 3600
  }
}

# -------------------------------------------
# ALB AccessLogs Bucket
# -------------------------------------------
resource "aws_s3_bucket" "alb_accesslogs_bucket" {
  bucket        = "${var.tool}-${var.project}-${var.environment}-alb-accesslogs-bucket"
  force_destroy = true

  lifecycle_rule {
    enabled = true
    expiration {
      days = "180"
    }
  }
}

resource "aws_s3_bucket_policy" "alb_log" {
  bucket = aws_s3_bucket.alb_accesslogs_bucket.id
  policy = data.aws_iam_policy_document.alb_log.json
}

data "aws_iam_policy_document" "alb_log" {
  statement {
    effect    = "Allow"
    actions   = ["s3:PutObject"]
    resources = ["arn:aws:s3:::${aws_s3_bucket.alb_accesslogs_bucket.id}/*"]

    principals {
      type        = "AWS"
      identifiers = ["582318560864"]
    }
  }
}
```

ここまで実装できたら、今までと同様でterraformコマンドを実行して
リソースを生成します。

これでRailsの画像投稿も機能しますので完成です！！



# ECS起動の確認作業

Terraformの実装は完了しましたが、
現状はECRにイメージが存在しない状態です。

その為、ECRのタスクでコンテナ生成が上手くいきません。

そこで、当記事ではターミナルから手動で
ローカルからECRにイメージをpushします。

マネジメントコンソールのECR画面から
対象リポジトリにチェックを入れて
`プッシュコマンドの表示`をクリックすると
イメージpushに関わる実行コマンドを表示されます。

**`<手動パターン>`**

```shell:ターミナル

# 1.Dockerコマンドが使用できるようにAWSに認証させる
% aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com

# 2.本番用のDockerfile.productionをもとにイメージを生成する
% docker build -f Dockerfile.production -t ECRレポジトリ名 .

# 3.ECRに保存するためにAWS指定のタグ付けを行う
% docker tag ECRレポジトリ名:latest AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/ECRレポジトリ名:latest

# 4.指定タグをつけたイメージをECRに保存する
% docker push AWSアカウントID.dkr.ecr.ap-northeast-1.amazonaws.com/ECRレポジトリ名:latest

```

もし、CircleCIのOrbsを用いて、自動デプロイを実装していれば
ECR/ECSに関わるAWSリソース名を全て変更すれば機能します。
参考までにCircleCIの設定ファイルを載せておきます。

**`<CircleCIで自動パターン>`**

```yml:.circleci/config.yml
version: 2.1
jobs:
  build-and-test:
    docker:
      - image: circleci/ruby:2.7.3-node-browsers
        environment:
          RAILS_ENV: 'test'
      - image: circleci/mysql:8.0
        command: mysqld --default-authentication-plugin=mysql_native_password
        environment:
          MYSQL_ALLOW_EMPTY_PASSWORD: 'true'
          MYSQL_ROOT_HOST: '%'
    steps:
      - checkout
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            - v1-dependencies-
      - run:
          name: install dependencies
          command: |
            bundle install --jobs=4 --retry=3 --path vendor/bundle
      - save_cache:
          paths:
          - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
      - run: yarn add @fortawesome/fontawesome-free
      - run: mv config/database.yml.ci config/database.yml
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load
      # rubocopのコード解析は一旦保留にする
      # - run:
      #     name: Rubocop
      #     command: bundle exec rubocop
      - run:
          name: RSpec
          command: |
            mkdir /tmp/test-results
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
              circleci tests split --split-by=timings)"
            bundle exec rspec \
              --format progress --format RspecJunitFormatter \
              --out /tmp/test-results/rspec.xml \
              $TEST_FILES
      - store_test_results:
          path: /tmp/test-results
      - store_artifacts:
          path: tmp/screenshots
          destination: test-screenshots

orbs:
  aws-ecr: circleci/aws-ecr@7.3.0
  aws-ecs: circleci/aws-ecs@2.2.1
workflows:
  build-and-deploy:
    jobs:
      - build-and-test
      - aws-ecr/build-and-push-image:
          filters:
            branches:
              only: master
          extra-build-args: '--build-arg RAILS_MASTER_KEY=${RAILS_MASTER_KEY}'
          ### terraformで作成したECR名称に変更 
          repo: ECRレポジトリ名
          dockerfile: Dockerfile.production
      - aws-ecs/deploy-service-update:
          filters:
            branches:
              only: master
          requires:
            - aws-ecr/build-and-push-image
          ### terraformで作成したリソース名に全て変更
          cluster-name: 'クラスター名'
          service-name: 'サービス名'
          family: 'タスク定義名'
          deployment-controller: 'CODE_DEPLOY'
          codedeploy-application-name: 'CodeDeployのアプリケーション名'
          codedeploy-deployment-group-name: 'CodeDeployのデプロイメントグループ名'
          codedeploy-load-balanced-container-name: 'コンテナ名'

```

上記作業を実施後に
マネジメントコンソールのサービス内のイベントタブで
`「service サービス名 has reached a steady state.」`
のメッセージが確認できれば成功です。

![スクリーンショット 2022-02-14 17.48.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1380401/5b420332-8f56-996e-6077-f67bac6a23b3.png)


# 参考動画/書籍

https://www.udemy.com/course/iac-with-terraform/

https://www.amazon.co.jp/dp/B07XT7LJLC/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1



# 参考記事

https://qiita.com/tomokon/items/eea60082e3210a2cf6b6#%E3%82%B9%E3%83%86%E3%83%83%E3%83%973-ecs

https://qiita.com/marnie_ms4/items/202deb8f587233a17cca

https://registry.terraform.io/providers/hashicorp/aws/latest/docs



# 終わりに

一度、Terraformでインフラが構築できれば、後から
部分的に構成を変更したり、削除したり、追加したりも容易になるし
terraform最高！

ただ、正直なところ、細かい部分は設定できていないし、
これから加筆修正も加えないとな…

長い記事となりましたが、最後までお読み頂き
ありがとうございました。