terraformの第一歩

# 概要
terraformがどのようなものか感覚的に理解することを目的としたページです。
terraformについての詳細は下記を参照してください。

https://www.lac.co.jp/lacwatch/service/20200903_002270.html

terraformはクラウド環境をコードで構築/管理するためのツールです。AWSに限らずGCPやAzureのリソースにも対応しています。
DevOpsの現場では問題が起きたサーバーをterraformを使って破棄→再構築ですぐさまサービスを復旧させているところもあるそうです。ライセンス等の都合もあるので一概に何でも同じことができるとは言えませんがコードで構成を管理できるだけでも大きな利点となります。

## どう使っているのか
terraformは.tfファイルに記載されたリソース情報を基に作成します。
provider.tfは必須ですが基本的に.tfファイルの命名規則などは無く、initを実行したディレクトリの.tfファイルを片っ端等から読み込むため基本的に記載するリソースに応じて.tfファイルの名前は決まります。

下記はvpcからec2まで新規に立ち上げた際に私が適当に命名していたディレクトリの状況です。

```
yamanashi@DESKTOP-9KCV2CQ:~/studymyself/terraform/aws/make_ec2_01$ tree ./
./
├── ec2.tf
├── igw.tf
├── provider.tf
├── rt.tf
├── sg.tf
├── terraform.tfstate
├── terraform.tfstate.backup
└── vpc.tf
```

## terraformによる構成管理
terraformはコードを基にリソースを作成することができますが、本質的なところは作成ではなく管理になります。
というのも単純にリソースを作成するだけであればterraformではなく各クラウドが配布しているazcliやgcloud,awscliで事足ります。

この項目ではterraformの構成管理がどのようなものか、そしてその注意点について記載していきます。


### .tfstateファイル
terraformでリソースを作成するとterraform.tfstateというファイルが作成されます。
このterraform.tfstateは.tfファイルを基に作成したリソースの情報や構成を管理しているファイルです。applyやplanを実行した際にはクラウドのリソースの状態とこのterraform.tfstateに記録されたリソースの構成が比較されます。

そしてその内容に差分があった場合、その差分を修正する為にリソースの削除や作成を行うことができます。そのため、リソースを作成する`terraform apply`を連続で複数回実行した場合でも2回目以降は下記のような表示になり、リソースが複数作られるということはありません。

```
yamanashi@DESKTOP-9KCV2CQ:~/studymyself/terraform/aws/make_ec2_01$ terraform apply
aws_vpc.test-vpc: Refreshing state... [id=vpc-02c30b828453c8908]
aws_internet_gateway.test-igw: Refreshing state... [id=igw-07016852b089d2d1a]
aws_route_table.test-routetable: Refreshing state... [id=rtb-0c9ce935c7c847ec4]
aws_security_group.test-sg: Refreshing state... [id=sg-0fb49bf714c72a67b]
aws_subnet.test-subnet-a: Refreshing state... [id=subnet-03f06d9427892f253]
aws_security_group_rule.outbound_all: Refreshing state... [id=sgrule-3759350995]
aws_security_group_rule.inbound_ssh: Refreshing state... [id=sgrule-1405075988]
aws_security_group_rule.inbound_icmp: Refreshing state... [id=sgrule-1057746502]
aws_instance.terraform-ec2-01: Refreshing state... [id=i-0804f19757d0f5bdd]
aws_route_table_association.rt_association: Refreshing state... [id=rtbassoc-0754805fc19a3f01a]
aws_route.route-to-igw: Refreshing state... [id=r-rtb-0c9ce935c7c847ec41080289494]

Warning: Version constraints inside provider configuration blocks are deprecated

  on provider.tf line 2, in provider "aws":
   2:   version    = "~> 2.0"

Terraform 0.13 and earlier allowed provider version constraints inside the
provider configuration block, but that is now deprecated and will be removed
in a future version of Terraform. To silence this warning, move the provider
version constraint into the required_providers block.


Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
```


では差分がある場合はどうなるのでしょうか。

試しに`terraform apply`で作成した後にec2のtag(Name)を手動で変更してみます。
元はterraform-ec2-01という名前を付けていました。
![01.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1206561/57ba4ad7-4ffc-96cb-6ec8-4d2ee76fd300.png)

この状態で再度`terraform apply`を実行してみます。

```
yamanashi@DESKTOP-9KCV2CQ:~/studymyself/terraform/aws/make_ec2_01$ terraform apply
aws_vpc.test-vpc: Refreshing state... [id=vpc-02c30b828453c8908]
aws_subnet.test-subnet-a: Refreshing state... [id=subnet-03f06d9427892f253]
aws_route_table.test-routetable: Refreshing state... [id=rtb-0c9ce935c7c847ec4]
aws_internet_gateway.test-igw: Refreshing state... [id=igw-07016852b089d2d1a]
aws_security_group.test-sg: Refreshing state... [id=sg-0fb49bf714c72a67b]
aws_security_group_rule.inbound_icmp: Refreshing state... [id=sgrule-1057746502]
aws_security_group_rule.inbound_ssh: Refreshing state... [id=sgrule-1405075988]
aws_security_group_rule.outbound_all: Refreshing state... [id=sgrule-3759350995]
aws_instance.terraform-ec2-01: Refreshing state... [id=i-0804f19757d0f5bdd]
aws_route_table_association.rt_association: Refreshing state... [id=rtbassoc-0754805fc19a3f01a]
aws_route.route-to-igw: Refreshing state... [id=r-rtb-0c9ce935c7c847ec41080289494]

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # aws_instance.terraform-ec2-01 will be updated in-place
  ~ resource "aws_instance" "terraform-ec2-01" {
        id                           = "i-0804f19757d0f5bdd"
      ~ tags                         = {
          ~ "Name" = "terraform-ec2-test" -> "terraform-ec2-01"
        }
        # (25 unchanged attributes hidden)



        # (3 unchanged blocks hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.


Warning: Version constraints inside provider configuration blocks are deprecated

  on provider.tf line 2, in provider "aws":
   2:   version    = "~> 2.0"

Terraform 0.13 and earlier allowed provider version constraints inside the
provider configuration block, but that is now deprecated and will be removed
in a future version of Terraform. To silence this warning, move the provider
version constraint into the required_providers block.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:
```
terraformはterraform.tfstateに記録した構成と現在のAWSの構成を比較して差分があった場合にはこのように変更点を明示して修正するか選択することができます。yesを入力してみます。

```
~snip~
aws_instance.terraform-ec2-01: Modifying... [id=i-0804f19757d0f5bdd]
aws_instance.terraform-ec2-01: Still modifying... [id=i-0804f19757d0f5bdd, 10s elapsed]
aws_instance.terraform-ec2-01: Modifications complete after 13s [id=i-0804f19757d0f5bdd]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.
```
![02.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1206561/9388b8fc-9609-dcb9-9920-07712bb2024d.png)

問題なく修正されました。


この機能により、terraformを用いて構築した環境は例え手動操作で変更が加えられていたとしても変更点の洗い出しと修正を簡単に行うことができます。

.tfファイルを作り込み、planやapplyだけで構成の差分を完全に洗い出せるようにするのがterraformを用いた構成管理の一つの目標になります。


### 構成管理に当たって抱えている問題
実はリソースを作成できるだけのtfコードと、前述の通り構成管理まで問題なく実行できるコードはかなり差があります。

というのも何故か再作成する挙動をとるリソースというのがいくつもあります。面倒なことに再作成する挙動をとるリソースに関係のあるリソースも巻き添えで再作成になったりするためその範囲は実際にやってみないとわからないというところです。

例として、ElasticIPアドレスを取得してec2インスタンスに紐づけたとき、何も対策を講じないとapplyを実行するたびに何故かec2インスタンスが再作成されてしまいます。もちろんec2インスタンス自体はElasticIPアドレスを紐づけない限りこのような挙動は取りません。

実際に見てみましょう。

外部から接続できるようにec2インスタンス(terraform-ec2-01)にElastic IPアドレスを割り振るように.tfファイルを更新しました。この状態で一度applyし、再度applyを実行すると下記の結果になります。

```
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  ~ update in-place
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # aws_eip.ec2-eip will be updated in-place
  ~ resource "aws_eip" "ec2-eip" {
        id                = "eipalloc-039cc86c08d71bcc1"
      ~ instance          = "i-0804f19757d0f5bdd" -> (known after apply)
        tags              = {}
        # (9 unchanged attributes hidden)
    }

  # aws_instance.terraform-ec2-01 must be replaced
-/+ resource "aws_instance" "terraform-ec2-01" {
      ~ arn                          = "arn:aws:ec2:ap-northeast-1:788373527495:instance/i-0804f19757d0f5bdd" -> (known after apply)
      ~ associate_public_ip_address  = true -> false # forces replacement
      ~ cpu_core_count               = 1 -> (known after apply)
      ~ cpu_threads_per_core         = 1 -> (known after apply)
      - disable_api_termination      = false -> null
      - ebs_optimized                = false -> null
      - hibernation                  = false -> null
      + host_id                      = (known after apply)
      ~ id                           = "i-0804f19757d0f5bdd" -> (known after apply)
      ~ instance_state               = "running" -> (known after apply)
      ~ ipv6_address_count           = 0 -> (known after apply)
      ~ ipv6_addresses               = [] -> (known after apply)
      - monitoring                   = false -> null
      + network_interface_id         = (known after apply)
      + outpost_arn                  = (known after apply)
      + password_data                = (known after apply)
      + placement_group              = (known after apply)
      ~ primary_network_interface_id = "eni-0a280fb6ac9fe2a41" -> (known after apply)
      ~ private_dns                  = "ip-10-40-0-10.ap-northeast-1.compute.internal" -> (known after apply)
      ~ public_dns                   = "ec2-54-238-91-142.ap-northeast-1.compute.amazonaws.com" -> (known after apply)
      ~ public_ip                    = "54.238.91.142" -> (known after apply)
      ~ security_groups              = [] -> (known after apply)
        tags                         = {
            "Name" = "terraform-ec2-01"
        }
      ~ tenancy                      = "default" -> (known after apply)
      ~ volume_tags                  = {} -> (known after apply)
        # (9 unchanged attributes hidden)

      - credit_specification {
          - cpu_credits = "standard" -> null
        }

      + ebs_block_device {
          + delete_on_termination = (known after apply)
          + device_name           = (known after apply)
          + encrypted             = (known after apply)
          + iops                  = (known after apply)
          + kms_key_id            = (known after apply)
          + snapshot_id           = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + ephemeral_block_device {
          + device_name  = (known after apply)
          + no_device    = (known after apply)
          + virtual_name = (known after apply)
        }

      ~ metadata_options {
          ~ http_endpoint               = "enabled" -> (known after apply)
          ~ http_put_response_hop_limit = 1 -> (known after apply)
          ~ http_tokens                 = "optional" -> (known after apply)
        }

      + network_interface {
          + delete_on_termination = (known after apply)
          + device_index          = (known after apply)
          + network_interface_id  = (known after apply)
        }

      ~ root_block_device {
          ~ device_name           = "/dev/xvda" -> (known after apply)
          ~ encrypted             = false -> (known after apply)
          ~ iops                  = 100 -> (known after apply)
          + kms_key_id            = (known after apply)
          ~ volume_id             = "vol-0957d4e0c891f76bb" -> (known after apply)
            # (3 unchanged attributes hidden)
        }
    }

Plan: 1 to add, 1 to change, 1 to destroy.


Warning: Version constraints inside provider configuration blocks are deprecated

  on provider.tf line 2, in provider "aws":
   2:   version    = "~> 2.0"

Terraform 0.13 and earlier allowed provider version constraints inside the
provider configuration block, but that is now deprecated and will be removed
in a future version of Terraform. To silence this warning, move the provider
version constraint into the required_providers block.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:
```
内容としてはなぜかec2インスタンスを削除(1 to destroy)再作成(1 to add)してます。それに伴ってElasticIPに設定するインスタンスIDも変更(1 to change)になってます。

実際にはtfstateファイルに記載された構成と変わりはないのですが、何故かこうなります。
これをそのまま安易に実行してしまえば当然ec2インスタンスは作り直しとなり、もしアプリを動かしていたならばそれは消えてしまいます。

そのためterraformコードはリソースの作成を行った後に、そのリソースが不用意に再作成されないように対策をする必要があります。

### 不要な削除(destroy)を抑止する
方法の一つとしてリソースに`lifecycle`の設定を行うというものがあります。

参考
https://www.terraform.io/docs/language/meta-arguments/lifecycle.html#ignore_changes

詳細は割愛しますが、一番簡単な方法だと変更を抑止したいリソースに下記を追加します。

```
  lifecycle {
    ignore_changes = all
  }
```
lifecycleを使う場合はignore_changesで変更があっても無視する属性を指定します。上述のallは手っ取り早く全指定して一切の変化を無視します。

これを追加したec2.tfが下記になります。

```
resource "aws_instance" "terraform-ec2-01" {
  ami                         = "ami-02892a4ea9bfa2192"
  availability_zone           = "ap-northeast-1a"
  instance_type               = "t2.micro"
  key_name                    = "study_ec2"
  subnet_id                   = aws_subnet.test-subnet-a.id
  vpc_security_group_ids      = [aws_security_group.test-sg.id]
  associate_public_ip_address = false
  private_ip                  = "10.40.0.10"
  root_block_device {
    volume_type           = "gp2"
    volume_size           = 8
    delete_on_termination = true
  }
  tags = {
    "Name" = "terraform-ec2-01"
  }
  lifecycle {
    ignore_changes = all
  }
}

resource "aws_eip" "ec2-eip" {
  instance = aws_instance.terraform-ec2-01.id
  vpc      = true
}
```

実際に`lifecycle`をec2インスタンスに設定した場合のapplyを実行結果を見てみましょう。
下記の実行結果は一度`terraform apply`を実行した直後に再度実行した結果です。

```
yamanashi@DESKTOP-9KCV2CQ:~/studymyself/terraform/aws/make_ec2_01$ terraform apply
aws_vpc.test-vpc: Refreshing state... [id=vpc-02c30b828453c8908]
aws_internet_gateway.test-igw: Refreshing state... [id=igw-07016852b089d2d1a]
aws_route_table.test-routetable: Refreshing state... [id=rtb-0c9ce935c7c847ec4]
aws_security_group.test-sg: Refreshing state... [id=sg-0fb49bf714c72a67b]
aws_subnet.test-subnet-a: Refreshing state... [id=subnet-03f06d9427892f253]
aws_route.route-to-igw: Refreshing state... [id=r-rtb-0c9ce935c7c847ec41080289494]
aws_route_table_association.rt_association: Refreshing state... [id=rtbassoc-0754805fc19a3f01a]
aws_security_group_rule.outbound_all: Refreshing state... [id=sgrule-3759350995]
aws_security_group_rule.inbound_ssh: Refreshing state... [id=sgrule-1405075988]
aws_security_group_rule.inbound_icmp: Refreshing state... [id=sgrule-1057746502]
aws_instance.terraform-ec2-01: Refreshing state... [id=i-0804f19757d0f5bdd]
aws_eip.ec2-eip: Refreshing state... [id=eipalloc-039cc86c08d71bcc1]

Warning: Version constraints inside provider configuration blocks are deprecated

  on provider.tf line 2, in provider "aws":
   2:   version    = "~> 2.0"

Terraform 0.13 and earlier allowed provider version constraints inside the
provider configuration block, but that is now deprecated and will be removed
in a future version of Terraform. To silence this warning, move the provider
version constraint into the required_providers block.


Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
```
これでec2インスタンスが不用意に削除再作成されることは無くなりました。

## 他の方法について
### planやapplyを実行する際に対象となるリソースを指定する
terraformコマンドのオプションとして-targetがあります。-targetの後にリソースを指定してやると指定したリソースのみplanやapplyの対象にすることができます。

例：`terraform apply -target aws_instance.terraform-ec2-01`

.tfファイルに追加したリソースだけを指定することで無関係のリソースへの影響を抑えることができます。

### 手動で作って管理だけterraform
手動でリソースを作成し、terraformがそれを参照して管理するという方法があります。
この場合terraformでこのリソースの設定を変更することはできません。