AWS Advent 2014 – Using Terraform to build infrastructure on AWS
Today’s post on using Terraform to build infrastructure on AWS comes from Justin Downing.
Introduction
Building interconnected resources on AWS can be challenging. A simple web application can have a load balancer, application servers, DNS records, and a security group. While a sysadmin can launch and manage these resources from the AWS web console or a CLI tool like fog, doing so can be time consuming and tedious considering all the metadata you have to shuffle amongst the other resources.
An elegant solution to this problem has been solved by the fine folks at Hashicorp: Terraform. This tool aims to take the concept of “infrastructure as code” and add the missing pieces that other provisioning tools like fog miss, namely the glue to interconnect your resources. For anyone with a background in software configuration management (Chef, Puppet), then using Terraform should be a natural fit for describing and configuring infrastructure resources.
Terraform can be used with several different providers including AWS, GCE, and Digital Ocean. We will be discussing provisioning resources on AWS. You can read more about the built-in AWS provider here.
Installation
Terraform is written in go and distributed as a package of binaries. You can download the appropriate package from the website. If you are using OSX and homebrew, you can simply brew install terraform
to get everything installed and setup.
Configuration
Now, that you have Terraform installed, let’s build some infrastructure! Terraform configuration files are text files that resemble JSON, but are more readable and can include comments. These files should end in .tf
(more details on configuration is available here). Rather than invent an example to use Terraform with AWS, I’m going to step through the example published by Hashicorp.
NOTE: I am assuming here that you have AWS keys capable of creating/terminating resources. Also, it would help if had the AWS CLI is installed and configured as Terraform will use those credentials to interract with AWS. The example below is using AWS region us-west-2
.
Let’s use the AWS Two-Tier example to build an ELB and EC2 instance:
1 2 3 4 5 |
$ mkdir /tmp/aws-tf $ cd /tmp/aws-tf $ terraform init github.com/hashicorp/terraform/examples/aws-two-tier $ aws ec2 --region us-west-2 create-key-pair --key-name terraform | jq -r ".KeyMaterial" > terraform.pem |
Here, we initialized a new directory with the example. Then, we created a new keypair and saved the private key to our directory. Here, you will note the files with the tf extension. These are the configuration files used to describe the resources we want to build. As the name indicates, one is the main configuration, one contains the variables used, and one describes the desired output. When you build this configuration, Terrraform will combine all .tf
files in the current directory to greate theresource graph.
Make a Plan
I encourage you to review the configuration details in main.tf
, variables.tf
, and outputs.tf
. With the help of comments and descriptions, it’s very easy to learn how different resources are intended to work together. You can also run plan to see how Terraform intends to build the resources you declared.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
$ terraform plan -var 'key_name=awsadvent' -var 'key_path=/tmp/aws-tf/awsadvent.pem' 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_elb.web ... + aws_instance.web ami: "" => "ami-21f78e11" ... + aws_security_group.default description: "" => "Used in the terraform" ... |
This also doubles as a linter by checking the validity of your configuration files. For example, if I comment out the instance_type in main.tf
, we receive an error:
1 2 3 4 5 6 7 8 |
$ terraform plan -var 'key_name=awsadvent' -var 'key_path=/tmp/aws-tf/awsadvent.pem' There are warnings and/or errors related to your configuration. Please fix these before continuing. Errors: * 'aws_instance.web' error: instance_type: required field is not set |
Variables
You will note that some pieces of the configuration are parameterized. This is very useful when sharing your Terraform plans, committing them to source control, or protecting sensitive data like access keys. By using variables and setting defaults for some, you allow for better portability when you share your Terraform plan with other members of your team. If you define a variable that does not have a default value, Terraform will require that you provide a value before proceeding. You can either (a) provide the values on the command line or (b) write them to a terraform.tfvars
file. This file acts like a “secrets” file with a key/value pair on each line. For example:
1 2 3 4 5 |
access_key = "ABC123" secret_key = "789xyz" key_name = "terraform" key_path = "terraform.pem" |
Due to the sensitive information included in this file, it is recommended that you includeterraform.tfvars
in your source control ignore list (eg: echo terraform.tfvars >> .gitignore
) if you want to share your plan.
Build Your Infrastructure
Now, we can build the resources using apply:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
$ terraform apply -var 'key_name=terraform' -var 'key_path=/tmp/aws-tf/terraform.pem' aws_security_group.default: Refreshing state... (ID: sg-74077d11) aws_security_group.default: Creating... description: "" => "Used in the terraform" ingress.#: "" => "2" ingress.0.cidr_blocks.#: "" => "1" ingress.0.cidr_blocks.0: "" => "0.0.0.0/0" ingress.0.from_port: "" => "22" ingress.0.protocol: "" => "tcp" ingress.0.security_groups.#: "" => "0" ingress.0.self: "" => "0" ingress.0.to_port: "" => "22" ingress.1.cidr_blocks.#: "" => "1" ingress.1.cidr_blocks.0: "" => "0.0.0.0/0" ingress.1.from_port: "" => "80" ingress.1.protocol: "" => "tcp" ingress.1.security_groups.#: "" => "0" ingress.1.self: "" => "0" ingress.1.to_port: "" => "80" name: "" => "terraform_example" owner_id: "" => "<computed>" vpc_id: "" => "<computed>" aws_security_group.default: Creation complete aws_instance.web: Creating... ami: "" => "ami-21f78e11" availability_zone: "" => "<computed>" instance_type: "" => "m1.small" key_name: "" => "terraform" private_dns: "" => "<computed>" private_ip: "" => "<computed>" public_dns: "" => "<computed>" public_ip: "" => "<computed>" security_groups.#: "" => "1" security_groups.0: "" => "terraform_example" subnet_id: "" => "<computed>" tenancy: "" => "<computed>" aws_instance.web: Provisioning with 'remote-exec'... aws_instance.web (remote-exec): Connecting to remote host via SSH... aws_instance.web (remote-exec): Host: 54.148.154.146 aws_instance.web (remote-exec): User: ubuntu aws_instance.web (remote-exec): Password: false aws_instance.web (remote-exec): Private key: true aws_instance.web (remote-exec): Connected! Executing scripts... ........................ ... output truncated ... ........................ aws_instance.web (remote-exec): Starting nginx: nginx. aws_instance.web: Creation complete aws_elb.web: Creating... availability_zones.#: "" => "1" availability_zones.0: "" => "us-west-2a" dns_name: "" => "<computed>" health_check.#: "" => "<computed>" instances.#: "" => "1" instances.0: "" => "i-3cddb836" internal: "" => "<computed>" listener.#: "" => "1" listener.0.instance_port: "" => "80" listener.0.instance_protocol: "" => "http" listener.0.lb_port: "" => "80" listener.0.lb_protocol: "" => "http" listener.0.ssl_certificate_id: "" => "" name: "" => "terraform-example-elb" security_groups.#: "" => "<computed>" subnets.#: "" => "<computed>" aws_elb.web: Creation complete Apply complete! Resources: 3 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 Outputs: address = terraform-example-elb-419196096.us-west-2.elb.amazonaws.com |
The output above is truncated, but Terraform did a few things for us here:
- Created a ‘terraform-example’ security group allowing SSH and HTTP access
- Created an EC2 instances from the Ubuntu 12.04 AMI
- Created an ELB instance and used the EC2 instance as its backend
- Printed the ELB public DNS address in the Outputs section
- Saved the state of your infrastructure in a
terraform.tfstate
file
You should be able to open the ELB public address in a web browser and see “Welcome to Nginx!” (note: this may take a minute or two after initialization in order for the ELB health check to pass).
The terraform.tfstate
file is very important as it tracks the status of your resources. As such, if you are sharing your configurations, it is recommended that you include this file in source control. This way, after initializing some resources, another member of your team will not try and re-initialize those same resources. In fact, she can see the status of the resources with terraform show
. In the event the state has not been kept up-to-date, you can use terraform refresh
to update the state file.
And…that’s it! With a few descriptive text files, Terraform is able to build cooperative resources on AWS in a matter of minutes. You no longer need complicated wrappers around existing AWS libraries/tools to orchestrate the creation or destruction of resources. When you are finished, you can simply run terraform destroy
to remove all the resources described in your .tf
configuration files.
Conclusion
With Terraform, building infrastructure resources is as simple as describing them in text. Of course, there is a lot more you can do with this tool including managing DNS records and configure Mailgun. You can even mix these providers together in a single plan (eg: EC2 instances, DNSimple records, Atlas metadata) and Terraform will manage it all! Check out the documentation and examples for the details.
Terraform Docs: https://terraform.io/docs/index.html
Terraform Examples: https://github.com/hashicorp/terraform/tree/master/examples