【技術編】インフラ構成のコード化。terraformを使ってAWSインスタンスをデプロイしてみる。
目次
インフラエンジニアの寺岡です。
今回は前回の知識編に続いて技術編です。
infrastructure as codeを実際に体験してみましょう。
前回のただ長いだけ知識編のブログは以下からご覧ください。
【知識編】インフラ構成のコード化。構成管理をスマートにするための「infrastructure as code」という考え方を学ぼう。
この記事ではterraformというツールを使って
AWSにEC2インスタンスを1台構築してみたいと思います。
■少し前置き、terraformって?
HashiCorpによって制作されている構成管理ツールで
インフラ構築や設定をコードによって自動化するためのツールです。
この会社自体はVagrantの開発などで有名ですね(いつもお世話になっております)
開発自体がすごく活発で頻繁にアップデートが行われているので個人的にすごく注目しているツールです。
・・・・・はい、さっそく手を動かしていきましょう。
■terraform実行環境の用意
まずは実行環境の用意。これをしないと始まらないですね(
zipファイルが配布されているのでwgetで取りに行くだけ。簡単。便利。
解凍してパスを通しておきます。
$ wget https://releases.hashicorp.com/terraform/0.9.0/terraform_0.9.0_linux_amd64.zip $ unzip terraform_0.9.0_linux_amd64.zip $ mv terraform /usr/bin/ $ terraform -v Terraform v0.9.0
・・・・・出来ました!
実行環境の用意はたったこれだけです(
■テンプレートファイルの作成
ざっくり説明すると・・・
「こんな構成のインフラを作成します」という手順書を
terraformが解釈できるようにコードで記述したものをテンプレートファイルといいます。
このファイルを元にterraformがインフラを構築してくれます。
まずは作業用のディレクトリを用意してその中にテンプレートファイルを作成します。
$ mkdir /var/tmp/terraform $ cd /var/tmp/terraform $ touch main.tf
テンプレートファイルの拡張子は「*.tf」にしましょう。
この拡張子のファイルをterraformがテンプレートとして認識してくれます。
ではさっそく、EC2インスタンス一台を構築するために作成したテンプレートファイルはこちら↓
(予め作っておきました。料理番組でも使われるチートです)
variable "aws_access_key" {} variable "aws_secret_key" {} variable "region" { default = "ap-northeast-1" } variable "images" { default = { us-east-1 = "ami-1ecae776" us-west-2 = "ami-e7527ed7" us-west-1 = "ami-d114f295" eu-west-1 = "ami-a10897d6" eu-central-1 = "ami-a8221fb5" ap-southeast-1 = "ami-68d8e93a" ap-southeast-2 = "ami-fd9cecc7" ap-northeast-1 = "ami-cbf90ecb" sa-east-1 = "ami-b52890a8" } } provider "aws" { access_key = "${var.aws_access_key}" secret_key = "${var.aws_secret_key}" region = "${var.region}" } resource "aws_vpc" "default" { cidr_block = "172.30.0.0/16" instance_tenancy = "default" enable_dns_support = "true" enable_dns_hostnames = "false" tags { Name = "default" } } resource "aws_internet_gateway" "gw" { vpc_id = "${aws_vpc.default.id}" } resource "aws_subnet" "public_subnet_a" { vpc_id = "${aws_vpc.default.id}" availability_zone = "ap-northeast-1a" cidr_block = "${cidrsubnet(aws_vpc.default.cidr_block, 4, 1)}" } resource "aws_route_table" "public_route" { vpc_id = "${aws_vpc.default.id}" route { cidr_block = "0.0.0.0/0" gateway_id = "${aws_internet_gateway.gw.id}" } } resource "aws_route_table_association" "public_route_a" { subnet_id = "${aws_subnet.public_subnet_a.id}" route_table_id = "${aws_route_table.public_route.id}" } resource "aws_security_group" "ec2_terraform_test" { name = "ec2_terraform_test" vpc_id = "${aws_vpc.default.id}" ingress { from_port = 22 to_port = 22 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" "terraform-test" { ami = "${var.images["ap-northeast-1"]}" instance_type = "t2.micro" key_name = "XXXXXXXX" vpc_security_group_ids = [ "${aws_security_group.ec2_terraform_test.id}" ] subnet_id = "${aws_subnet.public_subnet_a.id}" associate_public_ip_address = "true" root_block_device { volume_type = "gp2" volume_size = "8" } tags { Name = "terraform-test" } } output "public ip" { value = "${aws_instance.terraform-test.public_ip}" }
・・・・・・なるほどですね(
コードの各部分について一つずつ以下に書いていきます↓
プロバイダの定義
provider "aws" { access_key = "${var.aws_access_key}" secret_key = "${var.aws_secret_key}" region = "${var.region}" }
まずはこの部分。Terraformではまず初めにプロバイダの設定を行う必要があります。
実はTerraform、AzureやGoogle Cloudなどのクラウドサービスにも対応しているので
「どのプロバイダの構成を記述するのか」を先に教えてあげる必要があります。
今回は、AWSでEC2インスタンスを構築したいのでAWSを指定しています。
変数の定義
variable "aws_access_key" {} variable "aws_secret_key" {} variable "region" { default = "ap-northeast-1" }
variableブロックを利用することで変数を定義することができます。
この変数の中に格納した値は他のブロックから呼び出して利用することができます。
プロバイダを定義する部分で変数の中の値を呼び出しています。
「var.aws_access_key」などの部分のことですね。
ちなみにコードを見る限りでは「どこで変数に値を格納しているんだ?」ってなりますが
terraformでは、別ファイルに変数に格納したい値を記述しておくことで
そのファイルを参照して自動で変数に格納してくれます。
このファイルにも拡張子の指定があって、「*.tfvars」にする必要があります。
aws_access_key = "AKIAXXXXXXXXXXXXXXXXXX" aws_secret_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
中身はこんな感じ。「変数名="値"」です。
リソースの定義
resource "aws_vpc" "default" { cidr_block = "172.30.0.0/16" instance_tenancy = "default" enable_dns_support = "true" enable_dns_hostnames = "false" tags { Name = "default" } }
この部分。普段マネジメントコンソールから作成しているリソースの構成を記述していきます。
terraformでは、「VPC」や「EC2」などは全てリソースとして扱われます。
VPCを定義する場合は、「cidr_block」を指定したりします。
セキュリティグループを定義するなら、「ingress」「egress」等を指定しますし
EC2インスタンスなら、「ami」や「instance_type」などを指定します。
ちなみに各記述項目ですが、コードを記述する人が自由に決められるものではありません。
terraformならではのお作法があって、書き方を間違えると構文エラーで怒られます(
詳しくは以下の公式ドキュメントをご覧ください。(全部英語ですが)
AWS PROVIDER:Terraform by HashiCorp
アウトプット
output "public ip" { value = "${aws_instance.terraform-test.public_ip}" }
コードをデプロイした結果が返ってこないと、何がどうなったのかわからないですよね。
outputブロックを記述するとデプロイを行った結果を受け取ることができます。
上のコードでは作成されたEC2インスタンスのパブリックIPを受け取るようにしています。
■実際にデプロイしてみる。でもその前に
当然ですが、デプロイを行うと即時に結果が反映されます。
記述したコードにミスがあったとしても・・・・です。(これは精神衛生上よろしくない)
ということで、terraformにはドライランの機能があるのでつかってみましょう。
予めコードにミスがないかどうかを検証して実行計画を確認できる便利機能です。
ここで構文ミスがあったりするとエラーで怒られるので気づけます(
「terraform plan」と入力してみましょう。
$ 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. 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. Cyan entries are data sources to be read. 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.terraform-test ami: "ami-cbf90ecb" associate_public_ip_address: "true" availability_zone: "<computed>" ebs_block_device.#: "<computed>" ephemeral_block_device.#: "<computed>" instance_state: "<computed>" instance_type: "t2.micro" ipv6_addresses.#: "<computed>" key_name: "XXXXXXXXXX" network_interface_id: "<computed>" placement_group: "<computed>" private_dns: "<computed>" private_ip: "<computed>" public_dns: "<computed>" public_ip: "<computed>" root_block_device.#: "1" root_block_device.0.delete_on_termination: "true" root_block_device.0.iops: "<computed>" root_block_device.0.volume_size: "8" root_block_device.0.volume_type: "gp2" security_groups.#: "<computed>" source_dest_check: "true" subnet_id: "${aws_subnet.public_subnet_a.id}" tags.%: "1" tags.Name: "terraform-test" tenancy: "<computed>" vpc_security_group_ids.#: "<computed>" + aws_internet_gateway.gw vpc_id: "${aws_vpc.default.id}" + aws_route_table.public_route route.#: "1" route.~2599208424.cidr_block: "0.0.0.0/0" route.~2599208424.egress_only_gateway_id: "" route.~2599208424.gateway_id: "${aws_internet_gateway.gw.id}" route.~2599208424.instance_id: "" route.~2599208424.ipv6_cidr_block: "" route.~2599208424.nat_gateway_id: "" route.~2599208424.network_interface_id: "" route.~2599208424.vpc_peering_connection_id: "" vpc_id: "${aws_vpc.default.id}" + aws_route_table_association.public_route_a route_table_id: "${aws_route_table.public_route.id}" subnet_id: "${aws_subnet.public_subnet_a.id}" + aws_security_group.ec2_terraform_test description: "Managed by Terraform" egress.#: "1" egress.482069346.cidr_blocks.#: "1" egress.482069346.cidr_blocks.0: "0.0.0.0/0" egress.482069346.from_port: "0" egress.482069346.ipv6_cidr_blocks.#: "0" egress.482069346.prefix_list_ids.#: "0" egress.482069346.protocol: "-1" egress.482069346.security_groups.#: "0" egress.482069346.self: "false" egress.482069346.to_port: "0" ingress.#: "1" ingress.2541437006.cidr_blocks.#: "1" ingress.2541437006.cidr_blocks.0: "0.0.0.0/0" ingress.2541437006.from_port: "22" ingress.2541437006.ipv6_cidr_blocks.#: "0" ingress.2541437006.protocol: "tcp" ingress.2541437006.security_groups.#: "0" ingress.2541437006.self: "false" ingress.2541437006.to_port: "22" name: "ec2_terraform_test" owner_id: "<computed>" vpc_id: "${aws_vpc.default.id}" + aws_subnet.public_subnet_a assign_ipv6_address_on_creation: "false" availability_zone: "ap-northeast-1a" cidr_block: "172.30.16.0/20" ipv6_cidr_block_association_id: "<computed>" map_public_ip_on_launch: "false" vpc_id: "${aws_vpc.default.id}" + aws_vpc.default assign_generated_ipv6_cidr_block: "false" cidr_block: "172.30.0.0/16" default_network_acl_id: "<computed>" default_route_table_id: "<computed>" default_security_group_id: "<computed>" dhcp_options_id: "<computed>" enable_classiclink: "<computed>" enable_dns_hostnames: "false" enable_dns_support: "true" instance_tenancy: "default" ipv6_association_id: "<computed>" ipv6_cidr_block: "<computed>" main_route_table_id: "<computed>" tags.%: "1" tags.Name: "default" Plan: 7 to add, 0 to change, 0 to destroy.
エラーがなく正常に実行されると
実際にデプロイを行ったときにどのリソースが追加されるのかが結果として返ってきます。
今回は「Plan: 7 to add」なので合計7つのリソースが追加されることになりますね。
下記のような感じの結果が返ってきた場合は残念。コードのバグを直しましょう(
2 error(s) occurred: * aws_security_group.ec2_terraform_test: egress.0: invalid or unknown key: cidr_block * aws_security_group.ec2_terraform_test: ingress.0: invalid or unknown key: cidr_block
■実際にデプロイしてみる
ドライランでエラーが出ないことが確認できたので実際にデプロイしてみましょう。
デプロイを行うのもコマンドで一発、「terraform apply」と入力してみましょう。
$ terraform apply aws_vpc.default: Creating... assign_generated_ipv6_cidr_block: "" => "false" cidr_block: "" => "172.30.0.0/16" default_network_acl_id: "" => "<computed>" default_route_table_id: "" => "<computed>" default_security_group_id: "" => "<computed>" dhcp_options_id: "" => "<computed>" enable_classiclink: "" => "<computed>" enable_dns_hostnames: "" => "false" enable_dns_support: "" => "true" instance_tenancy: "" => "default" ipv6_association_id: "" => "<computed>" ipv6_cidr_block: "" => "<computed>" main_route_table_id: "" => "<computed>" tags.%: "" => "1" tags.Name: "" => "default" aws_vpc.default: Creation complete (ID: vpc-XXXXXXXX) aws_internet_gateway.gw: Creating... vpc_id: "" => "vpc-XXXXXXXX" aws_security_group.ec2_terraform_test: Creating... description: "" => "Managed by Terraform" egress.#: "" => "1" egress.482069346.cidr_blocks.#: "" => "1" egress.482069346.cidr_blocks.0: "" => "0.0.0.0/0" egress.482069346.from_port: "" => "0" egress.482069346.ipv6_cidr_blocks.#: "" => "0" egress.482069346.prefix_list_ids.#: "" => "0" egress.482069346.protocol: "" => "-1" egress.482069346.security_groups.#: "" => "0" egress.482069346.self: "" => "false" egress.482069346.to_port: "" => "0" ingress.#: "" => "1" ingress.2541437006.cidr_blocks.#: "" => "1" ingress.2541437006.cidr_blocks.0: "" => "0.0.0.0/0" ingress.2541437006.from_port: "" => "22" ingress.2541437006.ipv6_cidr_blocks.#: "" => "0" ingress.2541437006.protocol: "" => "tcp" ingress.2541437006.security_groups.#: "" => "0" ingress.2541437006.self: "" => "false" ingress.2541437006.to_port: "" => "22" name: "" => "ec2_terraform_test" owner_id: "" => "<computed>" vpc_id: "" => "vpc-XXXXXXXX" aws_subnet.public_subnet_a: Creating... assign_ipv6_address_on_creation: "" => "false" availability_zone: "" => "ap-northeast-1a" cidr_block: "" => "172.30.16.0/20" ipv6_cidr_block_association_id: "" => "<computed>" map_public_ip_on_launch: "" => "false" vpc_id: "" => "vpc-XXXXXXXX" aws_internet_gateway.gw: Creation complete (ID: igw-XXXXXXXX) aws_route_table.public_route: Creating... route.#: "" => "1" route.3460203481.cidr_block: "" => "0.0.0.0/0" route.3460203481.egress_only_gateway_id: "" => "" route.3460203481.gateway_id: "" => "igw-XXXXXXXX" route.3460203481.instance_id: "" => "" route.3460203481.ipv6_cidr_block: "" => "" route.3460203481.nat_gateway_id: "" => "" route.3460203481.network_interface_id: "" => "" route.3460203481.vpc_peering_connection_id: "" => "" vpc_id: "" => "vpc-XXXXXXXX" aws_subnet.public_subnet_a: Creation complete (ID: subnet-XXXXXXXX) aws_route_table.public_route: Creation complete (ID: rtb-XXXXXXXX) aws_route_table_association.public_route_a: Creating... route_table_id: "" => "rtb-XXXXXXXX" subnet_id: "" => "subnet-XXXXXXXX" aws_route_table_association.public_route_a: Creation complete (ID: rtbassoc-XXXXXXXX) aws_security_group.ec2_terraform_test: Creation complete (ID: sg-XXXXXXXX) aws_instance.terraform-test: Creating... ami: "" => "ami-cbf90ecb" associate_public_ip_address: "" => "true" availability_zone: "" => "<computed>" ebs_block_device.#: "" => "<computed>" ephemeral_block_device.#: "" => "<computed>" instance_state: "" => "<computed>" instance_type: "" => "t2.micro" ipv6_addresses.#: "" => "<computed>" key_name: "" => "XXXXXXXX" network_interface_id: "" => "<computed>" placement_group: "" => "<computed>" private_dns: "" => "<computed>" private_ip: "" => "<computed>" public_dns: "" => "<computed>" public_ip: "" => "<computed>" root_block_device.#: "" => "1" root_block_device.0.delete_on_termination: "" => "true" root_block_device.0.iops: "" => "<computed>" root_block_device.0.volume_size: "" => "8" root_block_device.0.volume_type: "" => "gp2" security_groups.#: "" => "<computed>" source_dest_check: "" => "true" subnet_id: "" => "subnet-XXXXXXXX" tags.%: "" => "1" tags.Name: "" => "terraform-test" tenancy: "" => "<computed>" vpc_security_group_ids.#: "" => "1" vpc_security_group_ids.766820655: "" => "sg-XXXXXXXX" aws_instance.terraform-test: Still creating... (10s elapsed) aws_instance.terraform-test: Still creating... (20s elapsed) aws_instance.terraform-test: Creation complete (ID: i-XXXXXXXXXXXXXXXXX) Apply complete! Resources: 7 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: Outputs: public ip = XX.XX.XXX.XX
はい、Outputsの項目で作成されたインスタンスのパブリックIPが返ってきました。
実際にマネジメントコンソールからインスタンスが作成されているか確認してみましょう。
・・・・・出来てた(すごい)
同時に「terraform.tfstate」というファイルがひっそりと出来上がっています。
これは「今のインフラの状態」をjson形式で保持しておくためのファイルで
terraformでは、このファイルと定義ファイル(.tfファイル)や実際のリソースの状態を比較することで
対象リソースに対する作成・変更・削除などの、どの操作をすべきかを判定しています。
つまり、このファイルに不整合が生じると割と普通にデプロイできなくなるので
扱いには注意を払わないといけないです(
■まとめ
「インフラ構成をコード化」ということでterraformを使ってみましたが
記述するコードの可読性が高いのと、基本扱うコマンド等も簡単なので扱いやすい印象を受けました。
devopsという言葉と一緒にこの手のツールは語られることが多いですが
開発担当者と運用担当者が連携していくのであれば、ブラックボックス化を防ぐために
是非ともツールを有効活用していきたいですね!
知識編よりも記事が長くなってしまったことは内緒です。
以上です、読んでいただきありがとうございました!