ぴろログ

Output Driven

「HashiCorp Learn Platform」でTerraformのお勉強。

Terraformをさわってみた

IaC(Infrastructure as Code)ができないインフラ担当は人権が無くなるそうなので、人権維持のためにTerraformを勉強しました。

AWSのCloudFormationの使用経験はあるのですが、どこかで「Terraformは文学」というフレーズを見かけてTeraformっていったい何者?と気になっていたのも勉強理由の一つです。

HashiCorp Learn Platform

kakakakakku.hatenablog.com

HashiCorpの学習コンテンツがあることを知り、Learn about provisioning infrastructure with HashiCorp TerraformGetting Stared -AWS でTerraformの概要を勉強しました。
※想定する学習時間が76minsと書いてますが、初心者で色々調べながら進めたので、倍ぐらいの時間がかかりました。

learn.hashicorp.com

流れ

  • Installing Terraform
  • Build Infrastructure
  • Change Infrastructure
  • Destroy Infrastructure
  • Resource Dependencies
  • Provision
  • Input Variables
  • Output Variables
  • Modules
  • Terraform Remote
  • Next Steps

Installing Terraform

Terraformのインストール。実行環境がMacOSだったので、ガイドを無視してHomebrewで導入しちゃいました。

$ brew install terraform
・・・
==> Downloading https://homebrew.bintray.com/bottles/terraform-0.11.13.mojave.bottle.tar.gz
######################################################################## 100.0%
==> Pouring terraform-0.11.13.mojave.bottle.tar.gz
🍺  /usr/local/Cellar/terraform/0.11.13: 6 files, 120.6MB

Build Infrastructure

構成ファイルを作成して環境を構築します。拡張子は.tfで作成します。

  • sample.tf (今回作成したファイルの名前)
    • HCL (HashiCorp Configuration Language) という書式で記述。yamlに似てるので習得難易度は高くなさそう。
    • providerブロック
      • 構築先のプロバイダを指定。
      • 対応プロバイダはProvidersで確認できる。(AlicloudってのはAlibaba Cloudのことみたい。)
    • resourceブロック
# sample.tf

provider "aws" {
  access_key = "ACCESS_KEY_HERE"
  secret_key = "SECRET_KEY_HERE"
  region     = "us-east-1"
}

resource "aws_instance" "example" {
  ami           = "ami-2757f631"
  instance_type = "t2.micro"
}

Initialization

terraform initコマンドで初期化します。実行後にpluginフォルダが作成され、AWS用のプラグインSDK?)が自動ダウンロードされました。sample.tfproviderブロックを参照しているようです。

$ terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (2.4.0)...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version = "~> 2.4"

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.

$ ls -Ralt .terraform/
・・・
drwxr-xr-x  11 pirox  staff  352  4  1 02:09 ..
drwxr-xr-x   5 pirox  staff  160  3 31 22:54 .
drwxr-xr-x   3 pirox  staff   96  3 31 22:48 plugins

.terraform//plugins:
total 0
drwxr-xr-x  5 pirox  staff  160  3 31 22:54 ..
drwxr-xr-x  3 pirox  staff   96  3 31 22:48 .
drwxr-xr-x  4 pirox  staff  128  3 31 22:48 darwin_amd64

.terraform//plugins/darwin_amd64:
total 279504
-rwxr-xr-x  1 pirox  staff         79  3 31 22:54 lock.json
drwxr-xr-x  4 pirox  staff        128  3 31 22:48 .
drwxr-xr-x  3 pirox  staff         96  3 31 22:48 ..
-rwxr-xr-x  1 pirox  staff  143098056  3 31 17:38 terraform-provider-aws_v2.4.0_x4

Apply Changes

terraform apply コマンドで環境を作成します。
① 実行した場合の結果が表示され(dry run)、
② 「これで良い?(Do you want to perform these actions?)」に対してyesを返すと、
③ 実際にリソースがのビルドが始まります。

①の情報は terraform planコマンドでも表示できます。

$ terraform apply

An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
  + create

Terraform will perform the following actions:

  + aws_instance.example
      id:                           <computed>
      ami:                          "ami-2757f631"
      arn:                          <computed>
      associate_public_ip_address:  <computed>
      availability_zone:            <computed>
      cpu_core_count:               <computed>
      cpu_threads_per_core:         <computed>
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      get_password_data:            "false"
      host_id:                      <computed>
      instance_state:               <computed>
      instance_type:                "t2.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     <computed>
      network_interface.#:          <computed>
      network_interface_id:         <computed>
      password_data:                <computed>
      placement_group:              <computed>
      primary_network_interface_id: <computed>
      private_dns:                  <computed>
      private_ip:                   <computed>
      public_dns:                   <computed>
      public_ip:                    <computed>
      root_block_device.#:          <computed>
      security_groups.#:            <computed>
      source_dest_check:            "true"
      subnet_id:                    <computed>
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>


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  # ←対話形式で「yes」を入力した

aws_instance.example: Creating...
  ami:                          "" => "ami-2757f631"
  arn:                          "" => "<computed>"
  associate_public_ip_address:  "" => "<computed>"
  availability_zone:            "" => "<computed>"
  cpu_core_count:               "" => "<computed>"
  cpu_threads_per_core:         "" => "<computed>"
  ebs_block_device.#:           "" => "<computed>"
  ephemeral_block_device.#:     "" => "<computed>"
  get_password_data:            "" => "false"
  host_id:                      "" => "<computed>"
  instance_state:               "" => "<computed>"
  instance_type:                "" => "t2.micro"
  ipv6_address_count:           "" => "<computed>"
  ipv6_addresses.#:             "" => "<computed>"
  key_name:                     "" => "<computed>"
  network_interface.#:          "" => "<computed>"
  network_interface_id:         "" => "<computed>"
  password_data:                "" => "<computed>"
  placement_group:              "" => "<computed>"
  primary_network_interface_id: "" => "<computed>"
  private_dns:                  "" => "<computed>"
  private_ip:                   "" => "<computed>"
  public_dns:                   "" => "<computed>"
  public_ip:                    "" => "<computed>"
  root_block_device.#:          "" => "<computed>"
  security_groups.#:            "" => "<computed>"
  source_dest_check:            "" => "true"
  subnet_id:                    "" => "<computed>"
  tenancy:                      "" => "<computed>"
  volume_tags.%:                "" => "<computed>"
  vpc_security_group_ids.#:     "" => "<computed>"
aws_instance.example: Still creating... (10s elapsed)
aws_instance.example: Still creating... (20s elapsed)
aws_instance.example: Still creating... (30s elapsed)
aws_instance.example: Creation complete after 33s (ID: i-06662143b7150d42c)

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

ビルド後にterraform.tfstate というファイルが作成されました。このファイルで状態を管理しているようです。各属性の値がJSONで記述されています。

$ cat terraform.tfstate 
{
    "version": 3,
    "terraform_version": "0.11.13",
    "serial": 1,
    "lineage": "0902678a-1bc1-b73b-7ea1-ddf523fe4130",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "aws_instance.example": {
                    "type": "aws_instance",
                    "depends_on": [],
                    "primary": {
                        "id": "i-06662143b7150d42c",
                        "attributes": {
                            "ami": "ami-2757f631",
                            "arn": "arn:aws:ec2:us-east-1:898422076535:instance/i-06662143b7150d42c",
                            "associate_public_ip_address": "true",
                            "availability_zone": "us-east-1c",
                            "cpu_core_count": "1",
                            "cpu_threads_per_core": "1",
                            "credit_specification.#": "1",
                            "credit_specification.0.cpu_credits": "standard",
                            "disable_api_termination": "false",
                            "ebs_block_device.#": "0",
                            "ebs_optimized": "false",
                            "ephemeral_block_device.#": "0",
                            "get_password_data": "false",
                            "iam_instance_profile": "",
                            "id": "i-06662143b7150d42c",
                            "instance_state": "running",
                            "instance_type": "t2.micro",
                            "ipv6_addresses.#": "0",
                            "key_name": "",
                            "monitoring": "false",
                            "network_interface.#": "0",
                            "password_data": "",
                            "placement_group": "",
                            "primary_network_interface_id": "eni-0966dcb8770c1201e",
                            "private_dns": "ip-172-31-91-36.ec2.internal",
                            "private_ip": "172.31.91.36",
                            "public_dns": "ec2-54-144-225-187.compute-1.amazonaws.com",
                            "public_ip": "54.144.225.187",
                            "root_block_device.#": "1",
                            "root_block_device.0.delete_on_termination": "true",
                            "root_block_device.0.iops": "100",
                            "root_block_device.0.volume_id": "vol-0166b721d83b7af5e",
                            "root_block_device.0.volume_size": "8",
                            "root_block_device.0.volume_type": "gp2",
                            "security_groups.#": "1",
                            "security_groups.3814588639": "default",
                            "source_dest_check": "true",
                            "subnet_id": "subnet-67d8a049",
                            "tags.%": "0",
                            "tenancy": "default",
                            "volume_tags.%": "0",
                            "vpc_security_group_ids.#": "1",
                            "vpc_security_group_ids.1775056293": "sg-f3a29ab4"
                        },
                        "meta": {
                            "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0": {
                                "create": 600000000000,
                                "delete": 1200000000000,
                                "update": 600000000000
                            },
                            "schema_version": "1"
                        },
                        "tainted": false
                    },
                    "deposed": [],
                    "provider": "provider.aws"
                }
            },
            "depends_on": []
        }
    ]
}

terraform showコマンドでも状態の確認が可能です。terraform.tfstate と同じ内容ですが、HCL形式で記述されているので、こっちの方が可読性が高いです。

$ terraform show
aws_instance.example:
  id = i-06662143b7150d42c
  ami = ami-2757f631
  arn = arn:aws:ec2:us-east-1:898422076535:instance/i-06662143b7150d42c
  associate_public_ip_address = true
  availability_zone = us-east-1c
  cpu_core_count = 1
  cpu_threads_per_core = 1
  credit_specification.# = 1
  credit_specification.0.cpu_credits = standard
  disable_api_termination = false
  ebs_block_device.# = 0
  ebs_optimized = false
  ephemeral_block_device.# = 0
  get_password_data = false
  iam_instance_profile = 
  instance_state = running
  instance_type = t2.micro
  ipv6_addresses.# = 0
  key_name = 
  monitoring = false
  network_interface.# = 0
  password_data = 
  placement_group = 
  primary_network_interface_id = eni-0966dcb8770c1201e
  private_dns = ip-172-31-91-36.ec2.internal
  private_ip = 172.31.91.36
  public_dns = ec2-54-144-225-187.compute-1.amazonaws.com
  public_ip = 54.144.225.187
  root_block_device.# = 1
  root_block_device.0.delete_on_termination = true
  root_block_device.0.iops = 100
  root_block_device.0.volume_id = vol-0166b721d83b7af5e
  root_block_device.0.volume_size = 8
  root_block_device.0.volume_type = gp2
  security_groups.# = 1
  security_groups.3814588639 = default
  source_dest_check = true
  subnet_id = subnet-67d8a049
  tags.% = 0
  tenancy = default
  volume_tags.% = 0
  vpc_security_group_ids.# = 1
  vpc_security_group_ids.1775056293 = sg-f3a29ab4

Change Infrastrucure

amiの指定を変更して terraform applyコマンドを実行します。
-/+」マークがつき、変更点が「=>」で表示されます。コマンド実行中にAWSコンソールを見ていると、稼働中のEC2インスタンスが削除されて新たなインスタンスが起動したことが確認できました。

$ terraform apply
aws_instance.example: Refreshing state... (ID: i-06662143b7150d42c)

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

Terraform will perform the following actions:

-/+ aws_instance.example (new resource required)
      id:                           "i-06662143b7150d42c" => <computed> (forces new resource)
      ami:                          "ami-2757f631" => "ami-b374d5a5" (forces new resource)
      arn:                          "arn:aws:ec2:us-east-1:898422076535:instance/i-06662143b7150d42c" => <computed>
      associate_public_ip_address:  "true" => <computed>
      availability_zone:            "us-east-1c" => <computed>
      cpu_core_count:               "1" => <computed>
      cpu_threads_per_core:         "1" => <computed>
      ebs_block_device.#:           "0" => <computed>
      ephemeral_block_device.#:     "0" => <computed>
      get_password_data:            "false" => "false"
      host_id:                      "" => <computed>
      instance_state:               "running" => <computed>
      instance_type:                "t2.micro" => "t2.micro"
      ipv6_address_count:           "" => <computed>
      ipv6_addresses.#:             "0" => <computed>
      key_name:                     "" => <computed>
      network_interface.#:          "0" => <computed>
      network_interface_id:         "" => <computed>
      password_data:                "" => <computed>
      placement_group:              "" => <computed>
      primary_network_interface_id: "eni-0966dcb8770c1201e" => <computed>
      private_dns:                  "ip-172-31-91-36.ec2.internal" => <computed>
      private_ip:                   "172.31.91.36" => <computed>
      public_dns:                   "ec2-54-144-225-187.compute-1.amazonaws.com" => <computed>
      public_ip:                    "54.144.225.187" => <computed>
      root_block_device.#:          "1" => <computed>
      security_groups.#:            "1" => <computed>
      source_dest_check:            "true" => "true"
      subnet_id:                    "subnet-67d8a049" => <computed>
      tenancy:                      "default" => <computed>
      volume_tags.%:                "0" => <computed>
      vpc_security_group_ids.#:     "1" => <computed>


Plan: 1 to add, 0 to change, 1 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.example: Destroying... (ID: i-06662143b7150d42c)
aws_instance.example: Still destroying... (ID: i-06662143b7150d42c, 10s elapsed)
aws_instance.example: Still destroying... (ID: i-06662143b7150d42c, 20s elapsed)
aws_instance.example: Destruction complete after 24s
aws_instance.example: Creating...
  ami:                          "" => "ami-b374d5a5"
  arn:                          "" => "<computed>"
  associate_public_ip_address:  "" => "<computed>"
  availability_zone:            "" => "<computed>"
  cpu_core_count:               "" => "<computed>"
  cpu_threads_per_core:         "" => "<computed>"
  ebs_block_device.#:           "" => "<computed>"
  ephemeral_block_device.#:     "" => "<computed>"
  get_password_data:            "" => "false"
  host_id:                      "" => "<computed>"
  instance_state:               "" => "<computed>"
  instance_type:                "" => "t2.micro"
  ipv6_address_count:           "" => "<computed>"
  ipv6_addresses.#:             "" => "<computed>"
  key_name:                     "" => "<computed>"
  network_interface.#:          "" => "<computed>"
  network_interface_id:         "" => "<computed>"
  password_data:                "" => "<computed>"
  placement_group:              "" => "<computed>"
  primary_network_interface_id: "" => "<computed>"
  private_dns:                  "" => "<computed>"
  private_ip:                   "" => "<computed>"
  public_dns:                   "" => "<computed>"
  public_ip:                    "" => "<computed>"
  root_block_device.#:          "" => "<computed>"
  security_groups.#:            "" => "<computed>"
  source_dest_check:            "" => "true"
  subnet_id:                    "" => "<computed>"
  tenancy:                      "" => "<computed>"
  volume_tags.%:                "" => "<computed>"
  vpc_security_group_ids.#:     "" => "<computed>"
aws_instance.example: Still creating... (10s elapsed)
aws_instance.example: Still creating... (20s elapsed)
aws_instance.example: Still creating... (30s elapsed)
aws_instance.example: Still creating... (40s elapsed)
aws_instance.example: Creation complete after 44s (ID: i-056b53fb875df899a)

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

Destroy Provisioners

terraform destoryコマンドで全てのリソースを一括削除できます。スッキリ。

Resource Dependencies

各リソースの依存関係を定義できます。

implicit dependency(暗黙の依存関係)

.tfファイル内で${aws_instance.example.id}のように記述しておけば、Terraformが自動で依存関係(リソースの作成順序)を読み取ってくれます。

# sample.tf
・・・
resource "aws_instance" "example" {
  ami           = "${lookup(var.amis, var.region)}"
  instance_type = "t2.micro"
}
resource "aws_eip" "ip" {
  instance = "${aws_instance.example.id}"
}
・・・

explicit dependency(明示的な依存関係)

resoureceブロック内に depends_onを記述することで、依存関係を明示的に指定できます。以下の例だと、S3バケットが作成された後にEC2インスタンスが作成せれます。(AWSコンソール画面でも確認できた)

# sample.tf
・・・
resource "aws_instance" "example" {
  ami           = "ami-2757f631"
  instance_type = "t2.micro"

  # Tells Terraform that this EC2 instance must be created only after the
  # S3 bucket has been created.
  depends_on = ["aws_s3_bucket.example"]
}

resource "aws_s3_bucket" "example" {
  bucket = "pirox-terraform-getting-started-guide" #バケット名が一意になるように指定
  acl    = "private"
}

・・・

このとき使っていたIAMユーザにS3を触れるポリシーをアタッチしていかなったのでterraform apply実行でS3バケットの作成に失敗したのですが、依存関係にあったEC2インスタンスは作成されました。その時のメッセージ。

・・・
Terraform does not automatically rollback in the face of errors.
Instead, your Terraform state file has been partially updated with any resources that successfully completed. Please address the error above and apply again to incrementally change your infrastructure.

作成に失敗したリソースがあっても、CloudFormationみたいにロールバックされないんですね。これ、割と危険なんじゃ。。

Provision

マシン作成後に、任意のコマンドを実行することができます。
provisioner で指定。

マシンの起動時に実行されるもので(ブートストラップ)、構成管理には別ツールを使用する必要がある。Provisionerで構成管理ツールを起動してあげる使い方のイメージ。
詳細はこちら

Input Variables

変数の利用方法です。AWSのクレデンシャル情報やAMI IDなどの、機密情報や可変にしておきたい部分を変数で指定します。

Defining Variables

Using Variables in Configuration

.tf ファイル内での変数の使い方。

# sample.tf
provider "aws" {
  access_key = "${var.access_key}"
  secret_key = "${var.secret_key}"
  region     = "${var.region}"
}

Assigning Variables

変数の値を指定する方法は複数あって、下記の順番通りに適用されていく。

  1. Command-line flags
    • コマンド実行時にフラグをつけて指定。
    • 例: $ terraform apply -var 'access_key=xxx' -var 'secret_key=xxx'
  2. From a file
    • コマンド実行時に〜.tfvarsから値を読み込む。
    • 例: $ terraform apply -var-file=xxx.tfvars
    • 既定のファイル名で作成しておくと、 -var-file=の指定無しでも自動で読み込まれる。
      • terraform.tfvars
      • *.auto.tfvars
  3. From environment variables
    • 〜.tf内でTF_VAR_xxxという形で指定しておけば、OSの環境変数:xxxが適用される。
  4. UI Input
    • 1〜4に該当しない場合、 terraform applyコマンド実行時に対話的に値の入力が求められる。
  5. Variable Defaults
    • 変数の宣言時にデフォルト値を設定している場合、その値が適用される。

この章はガイドの情報を読み取りきれなかったのか、あれこれ試行錯誤してようやく成功しました。。以下の3つのファイルを同一ディレクトリに作成して terraform applyコマンドを実行。

# sample.tf

provider "aws" {
  access_key = "${var.access_key}"
  secret_key = "${var.secret_key}"
  region     = "${var.region}"
}
resource "aws_instance" "example" {
  ami           = "${lookup(var.amis, var.region)}"
  instance_type = "t2.micro"
  provisioner "local-exec" {
    command = "echo ${aws_instance.example.public_ip} > ip_address.txt"
  }
}
resource "aws_eip" "ip" {
  instance = "${aws_instance.example.id}"
}
# variables.tf

variable "access_key" {}
variable "secret_key" {}
variable "region" {
  default = "us-east-1"
}
variable "amis" {
    type = "map"
}
variable "cidrs" {
    type = "list"
}
# terraform.tfvars

access_key = "xxxxx(マスキングしてます)"
secret_key = "xxxxx(マスキングしてます)"
cidrs = [ "10.0.0.0/16", "10.1.0.0/16" ]
amis = {
  "us-east-1" = "ami-b374d5a5"
  "us-west-2" = "ami-4b32be2b"
}

クレデンシャルの値の指定には環境変数を使った方がいいですかね。

Output Variables

outputブロックを定義することで、プロビジョニングされた環境の属性値(例:EC2のパブリックIPアドレスの値)を出力できる。

# sample.tf
 …
output "ip" {
  value = "${aws_eip.ip.public_ip}"
}
$ terraform apply
 …

Outputs:

ip = 54.159.208.102

Modules

再利用性の高いリソース群をパッケージ化したテンプレートで、呼び出し時に変数でパラメタを設定してあげるイメージ。

www.terraform.io

HashiCorp公式のTerraform Module RegistryGitHub上で多くのmoduleが公開されており、実際にTerraformを使うときにはここからテンプレートを入手するのが良さそう。

registry.terraform.io github.com

Terraform Remote

環境の状態を terraform.fstate ファイルで管理しており、 ~.tf と同一ディレクトリに作成されます。Terraformをチームで管理する場合、特定の誰かのローカルPCに保存されるとNGなので、共有できるリモート環境で管理する必要があります。ここではconsulを使って管理する方法が紹介されていました。

# sample.tfの末尾に追記

・・・
terraform {
  backend "consul" {
    address = "demo.consul.io"
    path    = "getting-started-RANDOMSTRING" #RANDOMSTRINGは任意の文字列で置き換え
    lock    = false
  }
}

terraform apply コマンドを実行後、 terraform.fstateには何も記載されず空ファイルとなっていることを確認。

Next Steps

各種ドキュメントの紹介

まとめ

  • HashiCorp Learn Platform で Terraformの基本を勉強した。
  • 基本のファイルを覚えた。
    • .tf:構成を記述するファイル(の拡張子)。
    • variables.tf:変数を宣言。
    • terraform.tfvars/ *.auto.tfvars:変数の値を記述。
  • 基本コマンドを覚えた
    • terraform init
    • terraform plan
    • terraform apply
    • terraform show
    • terraform destroy

これだけではTerraformの文学性が理解できなかったので、もうちょっと勉強を続けたいと思います。