🗂

AWS入門 - Terraform 使ってみる

2021/11/15に公開

AWSを使う必要が出てきたので、色々と試してみる。
ここでは、Terraform を試す。

Terraform

https://www.terraform.io/

学生時代にVagrantでお世話になったHashiCorpが作っているIaCツール。
CloudFormationと同じように、指定の書式でリソース構成を定義し、それを元にリソース構築できる。
AWSだけでなく、GoogleCloudやAzureでも使える。

大まかな仕組み

CTOの解説動画がとても分かりやすい。

https://www.youtube.com/watch?v=h970ZBgKINg

Terraformでは TFConfig という名の設定ファイルでインフラを定義する。
その TFConfig を現実世界に反映する時、大きく3つのステップがある。

  • Refresh
    • 現実世界をインフラ定義に反映する(git pullする的な感じ)
  • Plan
    • 現実世界の内容を元に、どのような変更を加えるか計画する(PR出す的な感じ)
  • Apply
    • 計画した内容を元に、現実世界へと計画を反映する(mergeする的な感じ)

この3つのステップを繰り返すことで、インフラを更新し続けることができる。
ただし、インフラを削除する場合だけ Apply とは若干異なる感じになるらしい。

で、CloudFormationとは違ったTerraformの特徴の1つが、様々なインフラ・サービスを定義できる点である。
この様々なインフラ・サービスに応じた処理を担っているのが Provider と呼ばれるもの。
現実世界の情報である State と TFConfig を Terraform のコア機能へと渡し、各Provider を通じで現実世界を更新する。

さらに、Terraform Cloud というサービスが提供されている。
これを使うことで、サービス上で Refresh・Plan・Apply のサイクルを実行することができる。
CIツール的なやつ。

最後に Module という仕組みがある。
パターン化した TFConfig を Module としてまとめることができ、指定した入力に応じでTFConfig を出力する。
複雑な設定を共通化したり、ブラックボックス化したり、人が増えたりした時に役立つやつ。
Node.js で言うところの NPM 的なものであり、NPM Registory に相当するのが Terraform Tegistory らしい。

https://registry.terraform.io/

つまり、
TFConfig と State を保持して、
Refresh・Plan・Apply を繰り返す
のである。

Terraformを使ってみる

まずは、簡単な環境構築を試してみる。
適当なディレクトリを作成し、その中に main.tf ファイルを作成する。

main.tf
terraform {
  required_providers {
    docker = {
      source = "kreuzwerker/docker"
      version = "~> 2.15.0"
    }
  }
}

provider "docker" {}

resource "docker_image" "nginx" {
  name = "nginx:latest"
  keep_locally = false
}

resource "docker_container" "nginx" {
  image = docker_image.nginx.latest
  name = "tutorial"
  ports {
    internal = 80
    external = 8080
  }
}

まずは、terraform init で初期化。
lockファイルが生成される。

$ terraform init

Initializing the backend...

Initializing provider plugins...
- Finding kreuzwerker/docker versions matching "~> 2.15.0"...
- Installing kreuzwerker/docker v2.15.0...
- Installed kreuzwerker/docker v2.15.0 (self-signed, key ID BD080C4571C6104C)

Partner and community providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.terraform.io/docs/cli/plugins/signing.html

Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

terraform apply で実際にリソースを構築する。

$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # docker_container.nginx will be created
  + resource "docker_container" "nginx" {
      + attach           = false
      + bridge           = (known after apply)
      + command          = (known after apply)
      + container_logs   = (known after apply)
      + entrypoint       = (known after apply)
      + env              = (known after apply)
      + exit_code        = (known after apply)
      + gateway          = (known after apply)
      + hostname         = (known after apply)
      + id               = (known after apply)
      + image            = (known after apply)
      + init             = (known after apply)
      + ip_address       = (known after apply)
      + ip_prefix_length = (known after apply)
      + ipc_mode         = (known after apply)
      + log_driver       = "json-file"
      + logs             = false
      + must_run         = true
      + name             = "tutorial"
      + network_data     = (known after apply)
      + read_only        = false
      + remove_volumes   = true
      + restart          = "no"
      + rm               = false
      + security_opts    = (known after apply)
      + shm_size         = (known after apply)
      + start            = true
      + stdin_open       = false
      + tty              = false

      + healthcheck {
          + interval     = (known after apply)
          + retries      = (known after apply)
          + start_period = (known after apply)
          + test         = (known after apply)
          + timeout      = (known after apply)
        }

      + labels {
          + label = (known after apply)
          + value = (known after apply)
        }

      + ports {
          + external = 8080
          + internal = 80
          + ip       = "0.0.0.0"
          + protocol = "tcp"
        }
    }

  # docker_image.nginx will be created
  + resource "docker_image" "nginx" {
      + id           = (known after apply)
      + keep_locally = false
      + latest       = (known after apply)
      + name         = "nginx:latest"
      + output       = (known after apply)
      + repo_digest  = (known after apply)
    }

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

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

  Enter a value: yes

docker_image.nginx: Creating...
docker_image.nginx: Still creating... [10s elapsed]
docker_image.nginx: Creation complete after 17s [id=sha256:04661cdce5812210bac48a8af672915d0719e745414b4c322719ff48c7da5b83nginx:latest]
docker_container.nginx: Creating...
docker_container.nginx: Creation complete after 0s [id=f7f50f8c3a344bb24aa7aa9c550ad24ef8fc1db04d9d501c5bd556bd754d8a42]

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

nginxのイメージが起動していることが確認できた。
terraform.tfstateも生成されていて、起動しているnginxイメージに関する情報が記載されている。

$ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                  NAMES
f7f50f8c3a34   04661cdce581   "/docker-entrypoint.…"   3 minutes ago   Up 3 minutes   0.0.0.0:8080->80/tcp   tutorial

$ curl http://localhost:8080
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

terraform destory で構築したリソースを削除できる。

今の所、CloudFormationと違って、使い方がわかりやすくて良い。

TerraformでAWSリソースを構築してみる

リソースを作成する

先程と同じく適当なディレクトリに main.tf を作成する。
そして、terraform initで初期化する。

main.tf
terraform {
  required_providers {
    aws = {
      source = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}

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

resource "aws_instance" "app_server" {
  ami = "ami-0e60b6d05dc38ff11"
  instance_type = "t2.micro"
  tags = {
    "Name" = "ec2-1115"
  }
}

terraform validate でtfファイルの内容を検証することができる。
例えば、aws_instance.app_serveramiをコメントアウトすると、必須パラメータが未指定であるエラーが出た。
CloudFormationではスタックを作成するまで内容を検証することがほぼ無理だったので、これはとても良い。

$ terraform validate
╷
│ Error: Missing required argument
│
│   with aws_instance.app_server,
│   on main.tf line 15, in resource "aws_instance" "app_server":15: resource "aws_instance" "app_server" {
│
│ "ami": one of `ami,launch_template` must be specified
╵

terraform apply でリソースを構築する。

$ terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  # aws_instance.app_server will be created
  + resource "aws_instance" "app_server" {
      + ami                                  = "ami-0e60b6d05dc38ff11"
      + arn                                  = (known after apply)
      + associate_public_ip_address          = (known after apply)
      + availability_zone                    = (known after apply)
      + cpu_core_count                       = (known after apply)
      + cpu_threads_per_core                 = (known after apply)
      + disable_api_termination              = (known after apply)
      + ebs_optimized                        = (known after apply)
      + get_password_data                    = false
      + host_id                              = (known after apply)
      + id                                   = (known after apply)
      + instance_initiated_shutdown_behavior = (known after apply)
      + instance_state                       = (known after apply)
      + instance_type                        = "t2.micro"
      + ipv6_address_count                   = (known after apply)
      + ipv6_addresses                       = (known after apply)
      + key_name                             = (known after apply)
      + monitoring                           = (known after apply)
      + outpost_arn                          = (known after apply)
      + password_data                        = (known after apply)
      + placement_group                      = (known after apply)
      + placement_partition_number           = (known after apply)
      + primary_network_interface_id         = (known after apply)
      + private_dns                          = (known after apply)
      + private_ip                           = (known after apply)
      + public_dns                           = (known after apply)
      + public_ip                            = (known after apply)
      + secondary_private_ips                = (known after apply)
      + security_groups                      = (known after apply)
      + source_dest_check                    = true
      + subnet_id                            = (known after apply)
      + tags                                 = {
          + "Name" = "ec2-1115"
        }
      + tags_all                             = {
          + "Name" = "ec2-1115"
        }
      + tenancy                              = (known after apply)
      + user_data                            = (known after apply)
      + user_data_base64                     = (known after apply)
      + vpc_security_group_ids               = (known after apply)

      + capacity_reservation_specification {
          + capacity_reservation_preference = (known after apply)

          + capacity_reservation_target {
              + capacity_reservation_id = (known after apply)
            }
        }

      + 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)
          + tags                  = (known after apply)
          + throughput            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }

      + enclave_options {
          + enabled = (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               = (known after apply)
          + http_put_response_hop_limit = (known after apply)
          + http_tokens                 = (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 {
          + 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)
          + tags                  = (known after apply)
          + throughput            = (known after apply)
          + volume_id             = (known after apply)
          + volume_size           = (known after apply)
          + volume_type           = (known after apply)
        }
    }

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

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

  Enter a value: yes

aws_instance.app_server: Creating...
aws_instance.app_server: Still creating... [10s elapsed]
aws_instance.app_server: Still creating... [20s elapsed]
aws_instance.app_server: Still creating... [30s elapsed]
aws_instance.app_server: Still creating... [40s elapsed]
aws_instance.app_server: Creation complete after 46s [id=i-0569728a49e34a8a1]

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

EC2インスタンスが作成された。
VPCは指定していなかったので、デフォルトVPCに紐付いていた。

terraform show で tfstate に保存された内容を確認できたりする。

$ terraform show
# aws_instance.app_server:
resource "aws_instance" "app_server" {
    ami                                  = "ami-0e60b6d05dc38ff11"
    ...
    instance_type                        = "t2.micro"
    ...
}

リソースを更新する

適当にリソースを更新してみる。
AMIをAmazonLinuxからUbuntuへと変更してみる。

main.tf
...

resource "aws_instance" "app_server" {
  ami = "ami-036d0684fc96830ca"
  ...
}

terraform apply で更新内容をリソースに反映する。
AMIの変更なのでEC2インスタンスが作り直されることが分かる。

$ terraform apply
aws_instance.app_server: Refreshing state... [id=i-0569728a49e34a8a1]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
-/+ destroy and then create replacement

Terraform will perform the following actions:

  # aws_instance.app_server must be replaced
-/+ resource "aws_instance" "app_server" {
      ~ ami                                  = "ami-0e60b6d05dc38ff11" -> "ami-036d0684fc96830ca" # forces replacement
...
Apply complete! Resources: 1 added, 0 changed, 1 destroyed.

指定したとおり、UbuntuのAMIでインスタンスが作り直された。
ただし、Subnetは指定していないので、先程とは異なるものが設定されていた。

リソースを削除する

最後に、作成したリソースを削除してみる。
terraform destroy で tfstate にあるリソースを全て削除できる。

$ terraform destroy
aws_instance.app_server: Refreshing state... [id=i-081b6629df58edbd5]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols:
  - destroy

Terraform will perform the following actions:

  # aws_instance.app_server will be destroyed
  - resource "aws_instance" "app_server" {
...
    }

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

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

aws_instance.app_server: Destroying... [id=i-081b6629df58edbd5]
aws_instance.app_server: Still destroying... [id=i-081b6629df58edbd5, 10s elapsed]
aws_instance.app_server: Still destroying... [id=i-081b6629df58edbd5, 20s elapsed]
aws_instance.app_server: Still destroying... [id=i-081b6629df58edbd5, 30s elapsed]
aws_instance.app_server: Destruction complete after 37s

Destroy complete! Resources: 1 destroyed.

無事にEC2インスタンスが削除された。

削除後の tfstate は↓のようになっていた。
元の tfstate もバックアップファイルとして同時に作成されていた。

{
  "version": 4,
  "terraform_version": "1.0.11",
  "serial": 6,
  "lineage": "69ed1452-d097-abbb-12d0-63caca60009a",
  "outputs": {},
  "resources": []
}

まとめ

手軽に使えて良い感じ。
覚えるべきことは殆んど無く、各Providerの使い方もRegistryにあるドキュメントを都度確認すれば良さそうである。

ただし、手軽に使えるということは、AWSの知識がなくても何となく動いてしまう、という事でもありそうな感じではある。
まぁ、勝手に本番環境に向かって terraform apply して yes とか入力しなければ、よいだけだとは思うが。

CloudFormationはスタックをAWS側で管理してくれたり、ロールバックしてくれたりする点は良い。
だが、あのテンプレートを書き続けるのは、常人には厳しいと思われる。。。
なので、Terraformを使う方向で、色々と試してみようと思う。

Discussion