Terraformをさわってみた
IaC(Infrastructure as Code)ができないインフラ担当は人権が無くなるそうなので、人権維持のためにTerraformを勉強しました。
AWSのCloudFormationの使用経験はあるのですが、どこかで「Terraformは文学」というフレーズを見かけてTeraformっていったい何者?と気になっていたのも勉強理由の一つです。
HashiCorp Learn Platform
HashiCorpの学習コンテンツがあることを知り、Learn about provisioning infrastructure with HashiCorp Terraformの Getting Stared -AWS でTerraformの概要を勉強しました。
※想定する学習時間が76minsと書いてますが、初心者で色々調べながら進めたので、倍ぐらいの時間がかかりました。
流れ
- 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
(今回作成したファイルの名前)
# 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.tf
の provider
ブロックを参照しているようです。
$ 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
変数の値を指定する方法は複数あって、下記の順番通りに適用されていく。
- Command-line flags
- コマンド実行時にフラグをつけて指定。
- 例:
$ terraform apply -var 'access_key=xxx' -var 'secret_key=xxx'
- From a file
- コマンド実行時に
〜.tfvars
から値を読み込む。 - 例:
$ terraform apply -var-file=xxx.tfvars
- 既定のファイル名で作成しておくと、
-var-file=
の指定無しでも自動で読み込まれる。terraform.tfvars
*.auto.tfvars
- コマンド実行時に
- From environment variables
〜.tf
内でTF_VAR_xxx
という形で指定しておけば、OSの環境変数:xxx
が適用される。
- UI Input
- 1〜4に該当しない場合、
terraform apply
コマンド実行時に対話的に値の入力が求められる。
- 1〜4に該当しない場合、
- 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
ブロックを定義することで、プロビジョニングされた環境の属性値(例:EC2のパブリックIPアドレスの値)を出力できる。output
# sample.tf
…
output "ip" {
value = "${aws_eip.ip.public_ip}"
}
$ terraform apply
…
Outputs:
ip = 54.159.208.102
Modules
再利用性の高いリソース群をパッケージ化したテンプレートで、呼び出し時に変数でパラメタを設定してあげるイメージ。
HashiCorp公式のTerraform Module RegistryやGitHub上で多くの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
各種ドキュメントの紹介
- Terraform Documentation:詳細なリファレンス
- Examples Configurations:設定ファイルのサンプル(GitHubでも公開)
- Import:既存インフラをTerraformに取り込む方法
まとめ
- HashiCorp Learn Platform で Terraformの基本を勉強した。
- 基本のファイルを覚えた。
.tf
:構成を記述するファイル(の拡張子)。variables.tf
:変数を宣言。terraform.tfvars
/*.auto.tfvars
:変数の値を記述。
- 基本コマンドを覚えた
terraform init
terraform plan
terraform apply
terraform show
terraform destroy
これだけではTerraformの文学性が理解できなかったので、もうちょっと勉強を続けたいと思います。