[Technical Edition] Coding infrastructure configuration. Deploying AWS instances using terraform

table of contents
My name is Teraoka and I am an infrastructure engineer
This time, we will be looking at technical aspects, following on from the knowledge section from last time
infrastructure as codeLet's try it out for ourselves
Last timeIt's just longCheck out the Knowledge Blog below:
In this article,
I will build an EC2 instance on AWS using a tool called terraform
■A little introduction: what is terraform?
a configuration management tool created by HashiCorp that
automates infrastructure construction and configuration through code.
The company itself Vagrant (which I am always grateful for).
Development is very active and updates are made frequently, so it is a tool that I am personally paying close attention to.
...Okay, let's get started right away
■Preparing the terraform execution environment
First, prepare the execution environment. You can't get started without this (
the zip file is distributed, so just download it with wget. Easy and convenient.
Unzip it and set the path.
$ 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
....It's done!
That's all you need to do to prepare the execution environment (
■ Creating a template file
To put it simply,
a template file is a code that Terraform can interpret to describe
the steps to create the infrastructure with this configuration Terraform will then build the infrastructure based on this file.
First, prepare a working directory and create a template file in it
$ mkdir /var/tmp/terraform $ cd /var/tmp/terraform $ touch main.tf
The template file extension should be "*.tf".
Terraform will recognize files with this extension as templates.
So, here is the template file I created to build one EC2 instance:
(I created this in advance. It's a cheat that's also used on cooking shows.)
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 = } } 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}" }
...I see (
I'll explain each part of the code one by one below.)
Defining a Provider
provider "aws" { access_key = "${var.aws_access_key}" secret_key = "${var.aws_secret_key}" region = "${var.region}" }
Let's start with this part. In Terraform, you first need to configure the provider.
In fact, Terraform also supports cloud services such as Azure and Google Cloud, so
you need to first tell it which provider's configuration you want to write.
In this case, we want to build an EC2 instance on AWS, so we'll specify AWS.
Defining variables
variable "aws_access_key" {} variable "aws_secret_key" {} variable "region" { default = "ap-northeast-1" }
You can define variables by using the variable block.
The value stored in this variable can be called and used from other blocks.
The value in the variable is called in the part that defines the provider.
This is the part such as "var.aws_access_key".
By the way, just looking at the code, you might be wondering "where are the values stored in the variables?", but
with terraform, you can write the values you want to store in variables in a separate file, and
the file will be referenced and automatically stored in the variables.
This file also needs to have an extension specified, which must be "*.tfvars".
aws_access_key = "AKIAXXXXXXXXXXXXXXXX" aws_secret_key = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
The contents look like this: "Variable name="value""
Resource definition
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" } }
This is the section where you write the configuration of resources that you normally create from the management console.
In Terraform, "VPC" and "EC2" are all treated as resources.
When defining a VPC, you specify "cidr_block".
When defining a security group, you specify "ingress" and "egress", and
for an EC2 instance, you specify "ami" and "instance_type".
Incidentally, each description item is not something that can be freely decided by the person writing the code.
Terraform has its own unique conventions, and if you make a mistake in writing it, you will be warned with a syntax error (
for more information, please see the official documentation below (it's all in English).)
AWS PROVIDER: Terraform by HashiCorp
Output
output "public ip" { value = "${aws_instance.terraform-test.public_ip}" }
If the results of deploying your code aren't returned, you won't know what happened.
By writing an output block, you can receive the results of the deployment.
The code above receives the public IP of the created EC2 instance.
■ Let's actually deploy it. But before that
Naturally, when you deploy, the results are reflected immediately.
Even if there are mistakes in the code you wrote... (This is not good for your mental health.)
So, let's use the dry run function in Terraform.
This is a convenient function that allows you to check the execution plan in advance to see if there are any mistakes in the code.
If there are any syntax mistakes here, you will be warned with an error, so you will be able to notice them (
try typing "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 + 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.
If the execution is successful without any errors,
the result will tell you which resources will be added when you actually deploy.
In this case, it's "Plan: 7 to add", so a total of 7 resources will be added.
If the result looks like the one below, that's unfortunate. Let's fix the bug in the code (
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
■ Try actually deploying
Now that we have confirmed that there are no errors in the dry run, let's actually deploy it.
To deploy, just type in the command "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-XXXXXXXXXXXXXXXX) 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
Yes, the public IP address of the instance created in the Outputs section was returned.
Let's check whether the instance was actually created in the management console.

....It worked (amazing)
At the same time, a file called "terraform.tfstate" is quietly created.
This is a file that stores the "current infrastructure state" in JSON format
, and terraform compares this file with the definition file (.tf file) and the actual state of the resources to
determine which operations to perform on the target resources, such as creating, modifying, or deleting.
In other words, if there is an inconsistency in this file, it will not be possible to deploy normally, so
you need to be careful when handling it (
Summary
I tried using terraform to "code infrastructure configuration"
The code written is highly readable, and the basic commands are simple to use, so I got the impression that it is easy to use
These tools are often mentioned together with the word devops
If developers and operators are to work together, to prevent the system from becoming a black box,
I'd love to make good use of this tool!
I won't tell you that this article turned out to be longer than the knowledge section
That's all, thank you for reading!
0