AWSリソースをterraform化してみよ

# 前書き

今年もアドベントカレンダーの時期がやってまいりました。12月6日が誕生日というだけで指名されてしまうワイです。
今回、しれっと隠れていたのですが某パイセンから指名されたので書くことになってしまいました。
ただ、誕生日は埋まっていたので誕生日イブで書きます！(某パイセン、Iサムさんと誕生日同じです)


# 今回はterraformについて書こうかと思います。

# 目次

- terraformとは
- terraform化したリソース
- terraformの基本
- EC2のterraform化
- SecurityGroupのterraform化
- Route53のterraform化
- まとめ？

---
## terraformとは

terraformとは、ざっくりいうとAWSやGCPなど主要なパブリッククラウドのリソースをIaCできます。
例えばRoute53のDNSレコードをコード化して、レビューしやすくしたり。EC2のインスタンスサイズを一気に変更できたりします。
弊社ではAWSを利用しているのでAWSリソースをterraformにしたときの話を書こうかと思います。
とはいえ、ワイもterraformはそこまで詳しくないので間違ったこと書いていても、笑って見逃してください。

---

## terraform化したリソース

- EC2
- SecurityGroup
- ELB
- Route53
- ECS Fargate
- etc...

上記をterraform化したのですが、全部書くと辛いので王道(かどうかはしらんけど。)のEC2, SecurityGroup, Route53 辺りについて書いていこうかと。。。

---

## terraformの基本

まずはAWSのリソースを作っていく前にterraformの基本を書いていきます。
terraformを書いていくにあたり、どのベストプラクティスに沿って書いていくか決めるのですが、今回はモジュール型と言われる書き方で書いていきます。

モジュール型とは

```
.
├── env                            # env...主にステージで分けられることが多い
│   ├── product
│   │   ├── default_variable.tf
│   │   └── main.tf
│   ├── staging
│   │   ├── default_variable.tf
│   │   └── main.tf
│   └── dev
│       ├── default_variable.tf
│       └── main.tf
├── modules                        # modules ... AWSリソースごとに分けられることが多い
│   ├── Route53
│   │   ├── variable.tf
│   │   └── main.tf
│   ├── ELB
│   │   ├── variable.tf
│   │   ├── alb.tf
│   │   └── nlb.tf
│   ├── EC2
│   │   ├── variable.tf
│   │   └── main.tf
│   ├── other_resource...
.
.
.
```

といったようなenv(環境)とmodules(各リソース)といったディレクトリ構成で書かれているterraformがモジュール型と言われています。
今回書いたのはこれに似た感じではあるのですが、似て非なるもので書きました。(というかいつの間にか似て非なるものになっていた。。。)

##### ↓今回書いてしまったやつのディレクトリ構成

```
.
├── env
│   ├── product
│   │   ├── default_variable.tf
│   │   └── main.tf
│   ├── staging
│   │   ├── default_variable.tf
│   │   └── main.tf
│   └── dev
│       ├── default_variable.tf
│       └── main.tf
├── modules                       # ここもステージごとに作っちゃった。
│   │                             # dev環境にしかないインスタントもあるのでなんかこうなってしまった。
│   ├── product
│   │   ├── alb.tf
│   │   ├── securitygroup.tf
│   │   ├── alb.tf
│   │   ├── variable.tf
│   │   └── other.tf
│   ├── staging
│   │   ├── alb.tf
│   │   ├── securitygroup.tf
│   │   ├── alb.tf
│   │   ├── variable.tf
│   │   └── other.tf
│   ├── dev
│   │   ├── alb.tf
│   │   ├── securitygroup.tf
│   │   ├── alb.tf
│   │   ├── variable.tf
│   │   └── other.tf
.
.
.
```

これはこれで使えるけど、dev,staging,productで似たようなことを何回も書かないといけないのでそれはそれでイケてない。。。
リソースごとに分けるのもdevにしかないインスタンスもあればdevにしかないDNSレコードもあったりとめんどくさいんですよねー…
はい、では次。

./env/dev/main.tf に必要な記述を見ていきます。

```tf
terraform {
    backend "s3" {
        bucket  = "terraform_state"
        key     = "dev.tfstate"
        region  = "ap-northeast-1"
        profile = "terraform-dev"
    }

    required_providers {
        aws = {
            source  = "hashicorp/aws"
            version = "3.72.0"
        }
    }
}

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

module "module_name" {
    source = "../../modules/dev/"

    # 環境ごとに違う部分を定義。variable.tfにて読み込まないとダメ。
    vpc_cidr = xxx.xxx.0.0/16
    subent = xxx.xxx.xxx.0/24
    .
    .
    .
```

順に説明

| Name | 説明 |
|---|---|
|backend "s3" | S3にstateファイルを保存するという意味。ローカルなども指定できたはず。 |
|bucket| S3のバケット名を記載 |
|key| stateファイル名を記載。初回実行時はないのが当たり前なのでどんなファイルのstateファイルにしたいかを書く。|
|region| AWSの利用しているリージョンを記載|
|profile| AWSを操作するprofileを指定。IAMでterraform用のユーザなり何なりを作成しなければいけない。|


| Name | 説明 |
|---|---|
|required_providers | どのパブリッククラウドを利用するのかを記載 |
|aws| AWSのリソースを使うと宣言 |
|source| hashicorpのawsを使うという宣言。(だと思う。) |
|version| sourceのバージョンを指定。|


| Name | 説明 |
|---|---|
|provider "aws" | どのパブリッククラウドを利用するのかを記載 |
|region| AWSの利用しているリージョンを記載 |
|profile| AWSを操作するprofileを指定。IAMでterraform用のユーザなり何なりを作成しなければいけない。backend で設定したprofileと同じになると思う。|


| Name | 説明 |
|---|---|
|module "module_name" | module_name はなんでもOK。あとから変えるのはめちゃくちゃめんどいのでちゃんと考えて付けたほうが良い。 |
|source| どのモジュールを実行するかを記載する |
|vpc_cidr| この辺は環境によって差分がある場合、ここで変数定義しておくと後々変更等が楽。|


envの説明はこれくらい。
default_variable.tfについては別に必要ないので書かない。
アプリのバージョンアップとかするためにバージョン番号等をmain.tfから外に出してみただけです。

さて、長くなりましたが、基本はこれくらい。
ようやく各リソースについて書いていきます。

---

## 各リソースの前に、、、

各リソースの前にvariable.tfについて少し説明。
./env/dev/main.tf で定義した変数を module で使うためにvariable.tfというものを作ります。(別に名前は何でもいいけど。)

##### ./modules/dev/variables.tf

```
variable "vpc_cidr" {}
variable "subent" {}

# 他にも ./env/dev/main.tf で定義した変数があれば記載
variable "..." {}
variable "..." {}
variable "..." {}
```
上記の記載をすることで、 var.vpc_cidr という書き方をすればモジュール内で利用することができる。

---

## terraform module 共通の書き方。

基本的に

`resource "AWSリソース名" "ユニークなモジュール名"`
というような宣言の仕方をします。

```tf
resource "aws_instance" "ec2-example-com" {

}
resource "aws_security_group" "access-from-vpc" {

}
```
みたいな感じ。

さてこれを抑えた後で各リソースの書き方に進んでいきましょう。

---

## EC2

```tf
# aws_instance はEC2のことだよ。という宣言的な意味合い。
# resource_nameはterraform内でユニークの値にする必要がある。
# あとで変更も可能だけど、結構めんどくさいのでわかりやすく、被らないようにしたほうが良い。
resource "aws_instance" "ec2-example-com" { 
    ami                         = "ami-xxxxxxxxxxxxxxxxx"         # マーケットプレイスなどのAMI_IDを記載
    availability_zone           = "ap-northeast-1c"               # 作成するAZを記載
    ebs_optimized               = true                            # boolean あとで変更するとインスタンスが作り直されるので注意！
    instance_type               = "t3.small"                      # インスタンスサイズを記載
    monitoring                  = false                           # 詳細モニタリングを有効にするかどうか？かな？わからん
    key_name                    = "key-name"                      # デフォルトユーザでsshするための鍵を記載
    subnet_id                   = var.subnet                      # ./env/dev/main.tf で定義した値を使用
    vpc_security_group_ids      = [
        aws_security_group.hogehoge.id, 
        aws_security_group.fugafuga.id
        ]                                                         # アタッチするセキュリティグループを記載。
                                                                  # aws_security_group.hogehogeみたいな書き方はいい感じにググってください。

    associate_public_ip_address = true                            # boolean PublicIPをアタッチするかどうか
    private_ip                  = "xxx.xxx.xxx.xxx"               # ローカルIPを記載
    source_dest_check           = true                            # よくわからん。

    lifecycle {
        ignore_changes = [associate_public_ip_address]            # associate_public_ip_addressこれが変わっても無視するという設定
    }

    # ここからは無くてもいい
    root_block_device {
        volume_type           = "gp3"
        volume_size           = 20
        delete_on_termination = true

        tags = {
            Name = "dev.example.com"
        }
    }

    tags = {
        "Name" = "dev.example.com"                                 # 主にホスト名とかを記載 
    }
}
```

上記を書けば大体インスタンスは作れます。
また、既存リソースがある場合、上記のような感じでterraformを記載した後、

```bash
terraform import [モジュール名] [インスタンスID]`
# 例)
# terraform import module.module_name.aws_instance.ec2-example-com i-xxxxxxxx
# という感じ。
```

### SecurityGroup

```tf
resource "aws_security_group" "default-securitygroup" {
    name        = "hogehoge-securitygroup"                              # SecurityGroup名を記載。確かあとで変更しようと思うと作り直されるような気がするので注意
    description = "attach all ec2"                                      # 説明を書く
    vpc_id      = "var.vpc_id"                                          # 変数化してると楽。

    # ingress は外からの通信(inbound)の許可設定
    ingress {
        description     = ""                                            # 説明
        from_port       = 22                                            # 何番ポートから何番ポートまで空けるのかを指定
        to_port         = 22                                            # 今回でいうと22番ポートのみ空けている
        protocol        = "tcp"
        cidr_blocks     = [ xxx.xxx.xxx.xxx/32, yyy.yyy.yyy.0/24 ]      # Cidrで指定したり1つのIPを指定して空けることができる
    }

    # egressは内からの通信(outbound)許可設定
    # 基本的にingressと似ているので割愛
    egress {
        from_port       = 0
        to_port         = 0
        protocol        = "-1"
        cidr_blocks     = ["0.0.0.0/0"]
    }

    # なくてもいい
    tags = {
        "Name" = "hogehoge"
    }
}
```

上記を書けば大体セキュリティグループは作れます。
また、既存リソースがある場合、上記のような感じでterraformを記載した後、

```bash
terraform import [モジュール名] [セキュリティグループID]
# 例)
# terraform import module.module_name.aws_security_group.default-securitygroup sg-xxxxxxxx
# という感じ。
```

---

### Route53

```tf 
# Aliasレコード
resource "aws_route53_record" "ec2-example-com-Alias" {
    zone_id = "xxxxxxxxxxxxxxxxxxxx"                               # ホストゾーンIDを記載
    name    = "alias.ec2.example.com"                              # 登録したいレコード名
    type    = "A"                                                  # AliasレコードのタイプはAレコードなのでAを書く。

    alias {
        name    = "dualstack.hogehoge.ap-northeast-1.elb.amazonaws.com"  # ELBやCloudFrontなどのDNSレコードを記載
        zone_id = "xxxxxxxxxxxxx"                                        # ELBとかのホストゾーンは(確か)固定なのでググって調べる
        evaluate_target_health = true
    }
}
# Aレコード
resource "aws_route53_record" "ec2-example-com-A" {
    zone_id = "xxxxxxxxxxxxxxxxxxxx"                               # ホストゾーンIDを記載
    name    = "a.ec2.example.com"                                  # 登録したいレコード名
    type    = "A"                                                  # AレコードなのでAを書く。
    records = ["xxx.xxx.xxx.xxx"]                                  # IPアドレスを書く
    ttl     = "300"

}
# CNAMEレコード
resource "aws_route53_record" "ec2-example-com-CNAME" {
    zone_id = "xxxxxxxxxxxxxxxxxxxx"                               # ホストゾーンIDを記載
    name    = "cname.ec2.example.com"                              # 登録したいレコード名
    type    = "CNAME"                                              # CNAMEレコードなのでCNAMEを書く。
    records = ["a.ec2.example.com"]                                # CNAME先を書く
    ttl     = "300"

}
# MXレコード
resource "aws_route53_record" "ec2-example-com-MX-0" {
    zone_id = "xxxxxxxxxxxxxxxxxxxx"                               # ホストゾーンIDを記載
    name    = "a.ec2.example.com"                                  # 登録したいレコード名
    type    = "MX"                                                 # MXレコードなのでMXを書く。
    records = ["0 mail.ec2.example.com"]                           # Priorityとレコードを書く
    ttl     = "300"

}
```

上記を書けば大体セキュリティグループは作れます。
また、既存リソースがある場合、上記のような感じでterraformを記載した後、

```bash
terraform import [モジュール名] [ゾーンID_レコード名_レコードタイプ_]
# 例)
# terraform import module.module_name.aws_route53_record.ec2-example-com-Alias Z4KAPRWWNC7JR_dev.example.com_Alias
# という感じ。
# Route53のimportが一番めんどくさい。。。
```

---

### まとめ

といった感じでかなり適当にterraformの書き方について書いてみました。
これからterraformでガシガシIaCしていくぜ！という方のお役に立てれば嬉しいです。
AWSとかパブリッククラウドのリソースの変更が結構レビューしずらい。スクリーンショットでしかバックアップ取れなかったり。
IaC化していって、オペミスや効率化をどんどんしていきたいなぁ～思う誕生日前日でした。

---

参照

[terraform](https://registry.terraform.io/providers/hashicorp/aws/latest/docs)