HashiCorp TerraformでAWSリソースを作成するには


Terraformを使う機会が会ったので、概要をまとめてみました。

TerraformはCloudFormationよりも定義ファイルが簡潔で、驚くほど習得しやすいツールです。
また、Chefやシェルによるプロビジョニングの仕組みも用意されているためミドルウェアまでささっと構築するようなユースケースにも使えそうです。

CloudFormationと比較した特徴については、「HashiCorpの新オーケストレーションツールTerraformを試してみた」の「CloudFormationの弱いところはカバー済み」が参考になります。

以下は、ほぼGETTING STARTEDを確認した内容になります

また、各リソースはデフォルトVPCに構築しています。

セットアップ

INSTALL TERRAFORMの手順に従ってインストール

バイナリパッケージがあるので、DOWNLOAD TERRAFORMよりダウンロードして適当なディレクトリに解凍して、パスを通すだけ

Mac OS Xの場合は、.bash_profileあたりに設定しておく
以下 /opt/terraform_0.6.6に解凍した場合

# .bash_profile
export PATH=$PATH:/opt/terraform_0.6.6

ターミナルでterraformコマンドを実行できればセットアップ完了

$ terraform
usage: terraform [--version] [--help] <command> [<args>]

Available commands are:
    apply      Builds or changes infrastructure
    destroy    Destroy Terraform-managed infrastructure
    get        Download and install modules for the configuration
    graph      Create a visual graph of Terraform resources
    init       Initializes Terraform configuration from a module
    output     Read an output from a state file
    plan       Generate and show an execution plan
    push       Upload this Terraform module to Atlas to run
    refresh    Update local state file against real resources
    remote     Configure remote state storage
    show       Inspect Terraform state or plan
    taint      Manually mark a resource for recreation
    version    Prints the Terraform version

インフラを構築してみる

BUILD INFRASTRUCTUREの手順にそって、AWSにEC2を起動してみます

# example.tf
provider "aws" {
    access_key = "ACCESS_KEY_HERE"
    secret_key = "SECRET_KEY_HERE"
    region = "ap-northeast-1"
}

resource "aws_instance" "example" {
    ami = "ami-383c1956"
    instance_type = "t2.micro"
    subnet_id = "subnet-7d83150a"
}

amiは東京リージョンのAmazon Linuxを指定
instance_typeはt2.microを指定しているので、subnet_idが必須になります。
ここでは、デフォルトVPCのサブネットの1つを指定しています

terraform planコマンドで実行計画を確認できる

$ terraform plan
Refreshing Terraform state prior to plan...


The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

+ aws_instance.example
    ami:                      "" => "ami-383c1956"
    availability_zone:        "" => "<computed>"
    ebs_block_device.#:       "" => "<computed>"
    ephemeral_block_device.#: "" => "<computed>"
    instance_type:            "" => "t2.micro"
    key_name:                 "" => "<computed>"
    placement_group:          "" => "<computed>"
    private_dns:              "" => "<computed>"
    private_ip:               "" => "<computed>"
    public_dns:               "" => "<computed>"
    public_ip:                "" => "<computed>"
    root_block_device.#:      "" => "<computed>"
    security_groups.#:        "" => "<computed>"
    source_dest_check:        "" => "1"
    subnet_id:                "" => "subnet-7d83150a"
    tenancy:                  "" => "<computed>"
    vpc_security_group_ids.#: "" => "<computed>"


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

<computed>はリソースが作成されてみないとわからない項目です

applyコマンドで適用

$ terraform apply
aws_instance.example: Creating...
  ami:                      "" => "ami-383c1956"
  availability_zone:        "" => "<computed>"
  ebs_block_device.#:       "" => "<computed>"
  ephemeral_block_device.#: "" => "<computed>"
  instance_type:            "" => "t2.micro"
  key_name:                 "" => "<computed>"
  placement_group:          "" => "<computed>"
  private_dns:              "" => "<computed>"
  private_ip:               "" => "<computed>"
  public_dns:               "" => "<computed>"
  public_ip:                "" => "<computed>"
  root_block_device.#:      "" => "<computed>"
  security_groups.#:        "" => "<computed>"
  source_dest_check:        "" => "1"
  subnet_id:                "" => "subnet-7d83150a"
  tenancy:                  "" => "<computed>"
  vpc_security_group_ids.#: "" => "<computed>"
aws_instance.example: Creation complete

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

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate

リソース名の”example”は、terraformの中で管理されるもので、Tag Nameにはなりません。

terraform-build-infrastructure

インフラの変更

CHANGE INFRASTRUCTURE

$ terraform plan
Refreshing Terraform state prior to plan...

aws_instance.example: Refreshing state... (ID: i-4ba71cee)

The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

~ aws_instance.example
    tags.Name: "" => "Terraform example"


Plan: 0 to add, 1 to change, 0 to destroy.
$ terraform apply
aws_instance.example: Refreshing state... (ID: i-4ba71cee)
aws_instance.example: Modifying...
  tags.Name: "" => "Terraform example"
aws_instance.example: Modifications complete

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

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate

tags.Nameに指定した値が適用されました。

terraform-build-infrastructure-with-tagname

インフラの削除

DESTROY INFRASTRUCTURE

$ terraform plan -destroy
Refreshing Terraform state prior to plan...

aws_instance.example: Refreshing state... (ID: i-4ba71cee)

The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

- aws_instance.example


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

Enter a value: でyesと入力すると実行される

$ terraform destroy
Do you really want to destroy?
  Terraform will delete all your managed infrastructure.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

aws_instance.example: Refreshing state... (ID: i-4ba71cee)
aws_instance.example: Destroying...
aws_instance.example: Destruction complete

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

リソースの依存について

RESOURCE DEPENDENCIES
ここまで、ドキュメントにしたがって、1つのaws_instanceの作成について確認してきましたが、複数のリソースを指定した場合は、適用順序を制御する必要があります。

EC2インスタンスにElastic IPリソースを追加するようにexample.tfaws_eipリソースを追加してみます。

# example.tf" highlight="7-10"]
provider "aws" {
    access_key = "ACCESS_KEY_HERE"
    secret_key = "SECRET_KEY_HERE"
    region = "ap-northeast-1"
}

resource "aws_eip" "ip" {
    instance = "${aws_instance.example.id}"
    vpc = true
}

resource "aws_instance" "example" {
    ami = "ami-383c1956"
    instance_type = "t2.micro"
    subnet_id = "subnet-7d83150a"
    tags {
        Name = "Terraform example"
    }
}

aws_eipリソースの${aws_instance.example.id}は、後のaws_instanceリソースaws_instance.exampleを参照します。

実行計画を確認

$ terraform plan
Refreshing Terraform state prior to plan...


The Terraform execution plan has been generated and is shown below.
Resources are shown in alphabetical order for quick scanning. Green resources
will be created (or destroyed and then created if an existing resource
exists), yellow resources are being changed in-place, and red resources
will be destroyed.

Note: You didn't specify an "-out" parameter to save this plan, so when
"apply" is called, Terraform can't guarantee this is what will execute.

+ aws_eip.ip
    allocation_id:     "" => "<computed>"
    association_id:    "" => "<computed>"
    domain:            "" => "<computed>"
    instance:          "" => "${aws_instance.example.id}"
    network_interface: "" => "<computed>"
    private_ip:        "" => "<computed>"
    public_ip:         "" => "<computed>"

+ aws_instance.example
    ami:                      "" => "ami-383c1956"
    availability_zone:        "" => "<computed>"
    ebs_block_device.#:       "" => "<computed>"
    ephemeral_block_device.#: "" => "<computed>"
    instance_type:            "" => "t2.micro"
    key_name:                 "" => "<computed>"
    placement_group:          "" => "<computed>"
    private_dns:              "" => "<computed>"
    private_ip:               "" => "<computed>"
    public_dns:               "" => "<computed>"
    public_ip:                "" => "<computed>"
    root_block_device.#:      "" => "<computed>"
    security_groups.#:        "" => "<computed>"
    source_dest_check:        "" => "1"
    subnet_id:                "" => "subnet-7d83150a"
    tags.#:                   "" => "1"
    tags.Name:                "" => "Terraform example"
    tenancy:                  "" => "<computed>"
    vpc_security_group_ids.#: "" => "<computed>"


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

適用時には、Elastic IPリソースに指定されているEC2インスタンスIDへの参照を検出して、暗黙的にEC2インスタンスを先に作成します。

$ terraform apply
aws_instance.example: Creating...
  ami:                      "" => "ami-383c1956"
  availability_zone:        "" => "<computed>"
  ebs_block_device.#:       "" => "<computed>"
  ephemeral_block_device.#: "" => "<computed>"
  instance_type:            "" => "t2.micro"
  key_name:                 "" => "<computed>"
  placement_group:          "" => "<computed>"
  private_dns:              "" => "<computed>"
  private_ip:               "" => "<computed>"
  public_dns:               "" => "<computed>"
  public_ip:                "" => "<computed>"
  root_block_device.#:      "" => "<computed>"
  security_groups.#:        "" => "<computed>"
  source_dest_check:        "" => "1"
  subnet_id:                "" => "subnet-7d83150a"
  tags.#:                   "" => "1"
  tags.Name:                "" => "Terraform example"
  tenancy:                  "" => "<computed>"
  vpc_security_group_ids.#: "" => "<computed>"
aws_instance.example: Creation complete
aws_eip.ip: Creating...
  allocation_id:     "" => "<computed>"
  association_id:    "" => "<computed>"
  domain:            "" => "<computed>"
  instance:          "" => "i-2445c181"
  network_interface: "" => "<computed>"
  private_ip:        "" => "<computed>"
  public_ip:         "" => "<computed>"
  vpc:               "" => "1"
aws_eip.ip: Creation complete

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

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate

depends_onパラメータを利用して明示的に依存するリソースを指定することもできます。

プロビジョニング

PROVISION

remote-execプロビジョナーを使って、Amazon Linuxに、Nginxをインストールして起動してみます。
PCでTerraformを実行しているので、インスタンス作成後にsshログインして、コマンドを実行できるようにconnectを設定します。
また、sshとWebアクセス用にインバウンド22, 80ポート、yum実行用にアウトバウンドポート全てを空けたセキュリティグループを作成しています。

# example.tf" highlight="7-29,35,40-51"]
provider "aws" {
    access_key = "ACCESS_KEY_HERE"
    secret_key = "SECRET_KEY_HERE"
    region = "ap-northeast-1"
}

resource "aws_security_group" "web-server" {
    name = "web-server"
    description = "Allow HTTP and SSH inbound traffic"
    vpc_id = "vpc-bdd851d8"
    ingress {
        from_port = 22
        to_port = 22
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
    ingress {
        from_port = 80
        to_port = 80
        protocol = "tcp"
        cidr_blocks = ["0.0.0.0/0"]
    }
    egress {
      from_port = 0
      to_port = 0
      protocol = "-1"
      cidr_blocks = ["0.0.0.0/0"]
    }
}

resource "aws_instance" "example" {
    ami = "ami-383c1956"
    instance_type = "t2.micro"
    subnet_id = "subnet-cda6c5ba"
    vpc_security_group_ids = ["${aws_security_group.web-server.id}"]
    key_name = "co-meeting"
    tags {
        Name = "Terraform example"
    }
    provisioner "remote-exec" {
      connection {
        type = "ssh"
        user = "ec2-user"
        key_file = "~/.ssh/my-key.pem"
      }
      inline = [
        "sudo yum -y install nginx",
        "sudo service nginx start",
        "sudo chkconfig nginx on"
      ]
    }
}

適用します

$ terraform apply
aws_security_group.web-server: Refreshing state... (ID: sg-448f7044)
aws_security_group.web-server: Destroying...
aws_security_group.web-server: Destruction complete
aws_security_group.web-server: Creating...
  description:                          "" => "Allow HTTP and SSH inbound traffic"
  egress.#:                             "" => "1"
  egress.482069346.cidr_blocks.#:       "" => "1"
  egress.482069346.cidr_blocks.0:       "" => "0.0.0.0/0"
  egress.482069346.from_port:           "" => "0"
  egress.482069346.protocol:            "" => "-1"
  egress.482069346.security_groups.#:   "" => "0"
  egress.482069346.self:                "" => "0"
  egress.482069346.to_port:             "" => "0"
  ingress.#:                            "" => "2"
  ingress.2214680975.cidr_blocks.#:     "" => "1"
  ingress.2214680975.cidr_blocks.0:     "" => "0.0.0.0/0"
  ingress.2214680975.from_port:         "" => "80"
  ingress.2214680975.protocol:          "" => "tcp"
  ingress.2214680975.security_groups.#: "" => "0"
  ingress.2214680975.self:              "" => "0"
  ingress.2214680975.to_port:           "" => "80"
  ingress.2541437006.cidr_blocks.#:     "" => "1"
  ingress.2541437006.cidr_blocks.0:     "" => "0.0.0.0/0"
  ingress.2541437006.from_port:         "" => "22"
  ingress.2541437006.protocol:          "" => "tcp"
  ingress.2541437006.security_groups.#: "" => "0"
  ingress.2541437006.self:              "" => "0"
  ingress.2541437006.to_port:           "" => "22"
  name:                                 "" => "web-server"
  owner_id:                             "" => "<computed>"
  vpc_id:                               "" => "vpc-bdd851d8"
aws_security_group.web-server: Creation complete
aws_instance.example: Creating...
  ami:                              "" => "ami-383c1956"
  availability_zone:                "" => "<computed>"
  ebs_block_device.#:               "" => "<computed>"
  ephemeral_block_device.#:         "" => "<computed>"
  instance_type:                    "" => "t2.micro"
  key_name:                         "" => "co-meeting"
  placement_group:                  "" => "<computed>"
  private_dns:                      "" => "<computed>"
  private_ip:                       "" => "<computed>"
  public_dns:                       "" => "<computed>"
  public_ip:                        "" => "<computed>"
  root_block_device.#:              "" => "<computed>"
  security_groups.#:                "" => "<computed>"
  source_dest_check:                "" => "1"
  subnet_id:                        "" => "subnet-cda6c5ba"
  tags.#:                           "" => "1"
  tags.Name:                        "" => "Terraform example"
  tenancy:                          "" => "<computed>"
  vpc_security_group_ids.#:         "" => "1"
  vpc_security_group_ids.618137044: "" => "sg-98c2eafd"
aws_instance.example: Provisioning with 'remote-exec'...
aws_instance.example (remote-exec): Connecting to remote host via SSH...
aws_instance.example (remote-exec):   Host: 52.192.72.204
aws_instance.example (remote-exec):   User: ec2-user
aws_instance.example (remote-exec):   Password: false
aws_instance.example (remote-exec):   Private key: true
aws_instance.example (remote-exec):   SSH Agent: true

...

aws_instance.example (remote-exec): Connecting to remote host via SSH...
aws_instance.example (remote-exec):   Host: 52.192.72.204
aws_instance.example (remote-exec):   User: ec2-user
aws_instance.example (remote-exec):   Password: false
aws_instance.example (remote-exec):   Private key: true
aws_instance.example (remote-exec):   SSH Agent: true
aws_instance.example (remote-exec): Connected!
aws_instance.example (remote-exec): Loaded plugins: priorities, update-motd,
aws_instance.example (remote-exec):               : upgrade-helper
aws_instance.example (remote-exec): Resolving Dependencies

...

aws_instance.example (remote-exec): Dependencies Resolved

aws_instance.example (remote-exec): ========================================
aws_instance.example (remote-exec):  Package   Arch   Version
aws_instance.example (remote-exec):                      Repository    Size
aws_instance.example (remote-exec): ========================================
aws_instance.example (remote-exec): Installing:
aws_instance.example (remote-exec):  nginx     x86_64 1:1.8.0-10.25.amzn1
aws_instance.example (remote-exec):                      amzn-main    555 k
aws_instance.example (remote-exec): Installing for dependencies:
aws_instance.example (remote-exec):  GeoIP     x86_64 1.4.8-1.5.amzn1
aws_instance.example (remote-exec):                      amzn-main    783 k
aws_instance.example (remote-exec):  gd        x86_64 2.0.35-11.10.amzn1
aws_instance.example (remote-exec):                      amzn-main    155 k
aws_instance.example (remote-exec):  gperftools-libs
aws_instance.example (remote-exec):            x86_64 2.0-11.5.amzn1
aws_instance.example (remote-exec):                      amzn-main    570 k
aws_instance.example (remote-exec):  libXpm    x86_64 3.5.10-2.9.amzn1
aws_instance.example (remote-exec):                      amzn-main     54 k
aws_instance.example (remote-exec):  libunwind x86_64 1.1-10.8.amzn1
aws_instance.example (remote-exec):                      amzn-updates  72 k

aws_instance.example (remote-exec): Transaction Summary
aws_instance.example (remote-exec): ========================================
aws_instance.example (remote-exec): Install  1 Package (+5 Dependent packages)

...

aws_instance.example (remote-exec): Complete!
aws_instance.example (remote-exec): Starting nginx:
aws_instance.example (remote-exec):                        [  OK  ]
aws_instance.example: Creation complete

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

The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.

State path: terraform.tfstate

割り当てられたPublic IPにブラウザからアクセスするとNginxのデフォルトページが表示されます。

terraform-install-nginx-on-amazon-linux

プロビジョニング参考
【Terraform】remote-execを使ったリモートサーバーのプロビジョニング
How To Use Terraform with DigitalOcean
TerraformだけでAWS環境にWordPressを構築する

変数の入力について

INPUT VARIABLES
上記までの例では、AWSのアクセスキーとシークレットキーなどをexample.tfに直接設定していましたが、このような環境設定については、別ファイルやコマンド実行時の変数、環境変数から渡すことができます。

新たにvariables.tfを作成し、variableで変数を定義します

# variables.tf"]
variable "access_key" {}
variable "secret_key" {}
variable "region" {
    default = "ap-northeast-1"
}

regionはデフォルト値を設定しているので、入力がなければap-northeast-1が使われます
変数に対応したAWS providerの定義は以下のようになります

# example.tf"]
variable "access_key" {}
variable "secret_key" {}
variable "region" {
    default = "ap-northeast-1"
}

変数の渡し方はいくつかあります

コマンドラインオプション

$ terraform apply -var 'access_key=ACCESS_KEY_HERE' -var 'secret_key=SECRET_KEY_HERE

変数ファイル terraform.tfvars
terraform.tfvarsという名前のファイルで変数をセットすることもできます。

# terraform.tfvars
access_key = "ACCESS_KEY_HERE"
secret_key = "SECRET_KEY_HERE"

terraformコマンド実行時に、カレントディレクトリのterraform.tfvarsファイルは、自動で読み込まれるので特にオプションは必要ありません。
他の名前を使いたい場合は、-var-fileオプションで指定できます。

環境変数
TF_VAR_<変数名>の環境変数は、変数として参照してくれます。
access_keyの場合は、TF_VAR_access_keyと指定します。

また、AWS ProviderAWS_ACCESS_KEY_IDのようにプロバイダーによっては、特定の環境変数を読み込むものもあります。

,