Terraform 初心者の超簡単まとめ

Nov 14, 2018 00:00 · 3962 words · 8 minutes read #terraform

モチベーション

AWS マネジメントコンソールから各種サービスを管理するのに疲弊したのと、遊びでミドルウェアを触るときにローカルだとスペックが足りないケースが多く、サクッとインスタンスを立てれたら楽だなと思い、調べてまとめてみることにしました。ゆくゆくは Ansible などの構成管理ツールと組み合わせて、自分の好きなタイミングで自由に遊び環境を立てたいと考えています。

Terraform とは?

とりあえず公式ドキュメントを読んでみる。
https://www.terraform.io/

重要そうな箇所をピックアップすると、こんな特徴を持つツールのようですごい便利な感じがあります。

  • みんな大好き HashiCorp社 の OSS
  • Infrastructure as Code を実現できる(コードベースで安全で効率的にインフラを管理できる)
  • インスタンスやネットワークなどの低レイヤーから、DNS や SaaS など高レイヤーまで幅広く管理可能
  • 豊富な Provider が提供されていて物理マシンからパブリッククラウドまで手厚くサポートしている
  • コードの差分を判斷した上で最適な実行計画を練ってインフラの更新をしてくれる
  • 構成ファイルからリソースグラフが生成可能

なお GitHub で公開されているリポジトリはこちらとなります。
commit log を眺めた感じだと、ガッツリ開発は続けられている様子でとても安心。
https://github.com/hashicorp/terraform

チュートリアルをやってみて理解したことまとめていく

Getting Started を眺めて、AWS 上にリソースを作成していく過程で Terraform の挙動を理解していきます。
https://www.terraform.io/intro/getting-started/install.html

挙動の確認に利用する Terraform のバージョン

今回は brew でインストールした v0.11.9 を利用して挙動をみていきます

$ brew install terraform

$ terraform --version
Terraform v0.11.9

自分は MacOS を使っているので brew でサクッとインストールしましたが、Windows を利用されている方は、以下の資料に沿ってインストールが可能のようです。参考までに。
https://qiita.com/miwato/items/b7e66cb087666c3f9583

Terraform で扱うファイルに関して

  • Terraform で扱う設定ファイルは Terraform configurations と呼ばれ Terraform format か JSON で記述可能
  • 拡張子は Terraform format なら .tf で JSON なら .tf.json を使用
  • 人間が読みやすくコメントも書ける Terraform format が推奨
  • Terraform format は HashiCorp Configuration Language( HCL )の文法に従う

以下が AWS 上に EC2 インスタンスを作成する .tf ファイルの例となります。確かに Terraform format であれば、記述内容を元に一体どのようなリソースが作成されるか容易に想像できます。ちなみに以下は us-west-2 リージョンに t2.micro の EC2 インスタンスを作成する設定となっております。

$ cat example.tf
provider "aws" {
  access_key = "xxx"
  secret_key = "xxx"
  region     = "us-west-2"
}

resource "aws_instance" "example" {
  ami           = "ami-3ecc8f46"
  instance_type = "t2.micro"
}

基本的なコマンド

terraform init

ワークスペースを初期化するコマンド。
Terraform を実行するためには、1番初めに terraform init でワークスペースを初期化することが必須となっています。terraform init を実行すると、.tf ファイルで利用している plugin(先述の例でいうと aws provider など)のダウンロード処理などが走ります。

$ terraform init

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

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 直下に配置されます。

$ ls -lR .terraform
total 0
drwxr-xr-x  3 yoshida  staff   102B 11 14 00:10 plugins

.terraform/plugins:
total 0
drwxr-xr-x  4 yoshida  staff   136B 11 14 00:10 darwin_amd64

.terraform/plugins/darwin_amd64:
total 219904
-rwxr-xr-x  1 yoshida  staff    79B 11 14 00:10 lock.json
-rwxr-xr-x  1 yoshida  staff   107M 11 14 00:10 terraform-provider-aws_v1.41.0_x4

terraform plan

Terraform による実行計画を参照するコマンド。
.tf ファイルに記載された情報を元に、どのようなリソースが 作成/修正/削除 されるかを参照することが可能になります。今回のケースだとリソースの作成となるので “+create” と出力されました。

$ terraform plan
Refreshing Terraform state in-memory prior to plan...
The refreshed state will be used to calculate this plan, but will not be
persisted to local or remote state storage.


------------------------------------------------------------------------

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-3ecc8f46"
      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"
      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.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

terraform apply

.tf ファイルに記載された情報を元にリソースを作成するコマンド。
リソースが作成されると terraform.state というファイルに、作成されたリソースに関連する情報が保存されます。また、2度目以降の実行後には、1世代前のものが terraform.tfstate.backup に保存される形となります。Terraform において、この状態を管理する terraform.state ファイルが非常に重要になってくるようです。

$ 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-3ecc8f46"
      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"
      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

aws_instance.example: Creating...
  ami:                          "" => "ami-3ecc8f46"
  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"
  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: Creation complete after 28s (ID: i-03446cd105dce674c)

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

AWS Console から EC2 ダッシュボードを確認すると、想定通り EC2 インスタンスが作成されました。

ec2-state-when-terraform-apply.png

terraform apply 実行後に terraform.state ファイルが作成されていることも確認できました。

$ cat terraform.tfstate.backup
{
    "version": 3,
    "terraform_version": "0.11.9",
    "serial": 14,
# 以下割愛

terraform show

terraform.state ファイルを元に現在のリソースの状態を参照するコマンド。

$ terraform show
aws_instance.example:
  id = i-03446cd105dce674c
  ami = ami-3ecc8f46
# 以下割愛

terraform destroy

.tf ファイルに記載された情報を元にリソースを削除するコマンド。
なお、実行すると terraform.tfstate のリソース情報がスカスカになり、削除直前のものは terraform.tfstate.backup に保存される形となります。

$ terraform destroy
aws_instance.example: Refreshing state... (ID: i-03446cd105dce674c)

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

Terraform will perform the following actions:

  - aws_instance.example


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.example: Destroying... (ID: i-03446cd105dce674c)
aws_instance.example: Still destroying... (ID: i-03446cd105dce674c, 10s elapsed)
aws_instance.example: Still destroying... (ID: i-03446cd105dce674c, 20s elapsed)
aws_instance.example: Still destroying... (ID: i-03446cd105dce674c, 30s elapsed)
aws_instance.example: Still destroying... (ID: i-03446cd105dce674c, 40s elapsed)
aws_instance.example: Still destroying... (ID: i-03446cd105dce674c, 50s elapsed)
aws_instance.example: Still destroying... (ID: i-03446cd105dce674c, 1m0s elapsed)
aws_instance.example: Destruction complete after 1m4s

Destroy complete! Resources: 1 destroyed.

AWS Console から EC2 ダッシュボードを確認すると、想定通り EC2 インスタンスが削除されました。

ec2-state-when-terraform-destroy.png

terraform.tfstate のリソース情報はスカスカとなり terraform.tfstate.backup に削除前の状態が保存されているので、terraform show を実行しても何も出力されないことがわかります。

$ cat terraform.tfstate
{
    "version": 3,
    "terraform_version": "0.11.9",
    "serial": 14,
    "lineage": "b0b05783-eba7-3f2f-70f0-019a388e9b39",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {},
            "depends_on": []
        }
    ]
}

$ cat terraform.tfstate.backup
{
    "version": 3,
    "terraform_version": "0.11.9",
    "serial": 14,
# 以下割愛

$ terraform show

リソース間の依存関係の定義に関して

複数のリソースを用いるユースケースに対応するために、Terraform では、各種リソースの依存性を定義することが可能になっています。リソースの依存性に関しては .tf ファイルから Terraform に暗黙的に依存関係を判定させる方法と、.tf ファイルの中で明示的に依存関係を定義する方法の2つがあります。今回は詳細は割愛しますので、具体的な例は公式ドキュメントを参照ください。
https://www.terraform.io/intro/getting-started/dependencies.html

プロビジョニングに関して

.tf ファイル内で provisioner を用いてコマンドを列挙することで、プロビジョニングも可能となっています。基本的に provisioner は、リソースが作成されたタイミングだけ走る(既に起動中のリソースには当てられない)仕様となっているようで、設定によっては削除のタイミングでコマンドを実行させることも可能とのことです。

ドキュメントの中で注意点として挙げられていたのは、Terraform のプロビジョニング機能は、構成管理の代替ではなく、単にマシンのブートストラップに過ぎないということです 。構成管理をしたければ、Ansible のような構成管理ツールをセットアップするために、Terraform のプロビジョニング機能を使うようなアドバイスがされていました。まさに自分がやりたかったことなので想定通りで良かったです。

また、同じく HashiCorp社 が公開している Packer を用いて、Amazon Machine Image( AMI )などをカスタムするアプローチでの、プロビジョニングも方法の1つとしてあるとのことでした。追々 Packer も触ることになりそうです。

変数に関して

.tf ファイル内に変数をデフォルト値として、ハードコーディング的に定義することも可能ですが、別のアプローチとして以下3つの方法があるようです。以下のファイルの access_key と secret_key に値を渡すことを目標に例として進めていきます。

$ cat example.tf
provider "aws" {
  access_key = "${var.access_key}"
  secret_key = "${var.secret_key}"
  region     = "${var.region}"
}

# 変数値が未定義だと terraform コマンド実行時に対話的に入力を求められる
$ cat variables.tf
variable "access_key" {}
variable "secret_key" {}
variable "region" {
  default = "us-west-2"
}

1. terraform コマンド実行時にフラグに値を乗せる

$ terraform apply -var 'access_key=xxx' -var 'secret_key=xxx'

2. terraform コマンド実行時に別ファイルを指定して読み込ませる

$ cat secret.tfvars
access_key = "xxx"
secret_key = "xxx"

$ terraform apply -var-file=secret.tfvars

なお、別ファイルを terraform.tfvars または *.auto.tfvars という名前のファイルにしておくと、自動で読み込まれるとのこと。

$ cat terraform.tfvars
access_key = "xxx"
secret_key = "xxx"

# ファイルを指定しなくても terraform.tfvars または *.auto.tfvars が自動で読み込まれる
$ terraform apply

3. TF_VAR_[変数名] な形で環境変数から読み込ませることも可能

TF_VAR_[変数名] を環境変数に定義しておけば .tf ファイルから ${var.[変数名]} で参照可能となっている。

$ env | grep -i tf
TF_VAR_access_key=xxx
TF_VAR_secret_key=xxx

$ terraform apply

Module に関して

特定のミドルウェアなどを構築するための .tf ファイルを含むスクリプト群がパッケージングされたものが、Terraform Module Resitory で公開されていて、そこで公開されているものを呼び出して、ミドルウェアなどを構築することも可能となっているらしい。Docker でいうところの Docker Hub、Ansible でいうところの Ansible Galaxy 的なものが Terraform Module Resitory からも提供されているようです。
https://registry.terraform.io/

Remote Backend に関して

tfstate ファイルなどの管理のベストプラクティスの文脈で紹介されていて、Consul や S3 を使うのが良いでしょうと書かれていました。今回はこの程度で詳細は割愛します。気になる方は公式ドキュメントを参照ください。
https://www.terraform.io/intro/getting-started/remote.html

さいごに

まだまだ浅はかですが、とりあえず terraform の概要を理解することができたので、今度時間があるときにでも、Modules と Remote Backend を深掘りしたり、.tfstate ファイルや .tfvars ファイルの管理のベストプラクティスなどを、より詳細に調べていこうと思います!

さあ、Terraform やっていくぞ ₍₍ (ง ˘ω˘ )ว ⁾⁾