This post will provide a walkthrough on AWS infrastructure provisioning using Terraform.
Prerequisites
-
Download Terraform and include it in your PATH.
-
In the AWS console, create a new IAM user and take note of the Access Key ID and Secret Access Key.
Let's get started.
Generate SSH keys for the EC2 instance.
$ mkdir infra && \
cd infra && \
ssh-keygen -N "" -f infra_key
Create the Terraform config files
To be able to communicate with the AWS API, we need to define the credentials.
Create a file named terraform.tfvars
with the following contens:
AWS_ACCESS_KEY = "<your_aws_access_key>"
AWS_SECRET_KEY = "<your_aws_secret_key>"
AWS_REGION = "<your_aws_region>"
Next, let's define the infrastructure using the following templates.
vars.tf
variable "AWS_ACCESS_KEY" {}
variable "AWS_SECRET_KEY" {}
variable "AWS_REGION" {
default = "ap-southeast-1"
}
variable "AMIS" {
type = "map"
default = {
ap-southeast-1 = "ami-10bb2373"
}
}
variable "PATH_TO_PRIVATE_KEY" {
default = "infra_key"
}
variable "PATH_TO_PUBLIC_KEY" {
default = "infra_key.pub"
}
variable "INSTANCE_USERNAME" {
default = "ec2-user"
}
provider.tf
provider "aws" {
access_key = "${var.AWS_ACCESS_KEY}"
secret_key = "${var.AWS_SECRET_KEY}"
region = "${var.AWS_REGION}"
}
key.tf
resource "aws_key_pair" "infra_key" {
key_name = "infra_key"
public_key = "${file("${var.PATH_TO_PUBLIC_KEY}")}"
}
vpc.tf
# Internet VPC
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
instance_tenancy = "default"
enable_dns_support = "true"
enable_dns_hostnames = "true"
enable_classiclink = "false"
tags {
Name = "main"
}
}
# Subnets
resource "aws_subnet" "main-public-1" {
vpc_id = "${aws_vpc.main.id}"
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = "true"
availability_zone = "ap-southeast-1a"
tags {
Name = "main-public-1"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main-gw" {
vpc_id = "${aws_vpc.main.id}"
tags {
Name = "main"
}
}
# Set default route to Internet Gateway
resource "aws_route_table" "main-public" {
vpc_id = "${aws_vpc.main.id}"
route {
cidr_block = "0.0.0.0/0"
gateway_id = "${aws_internet_gateway.main-gw.id}"
}
tags {
Name = "main-public-1"
}
}
# Associate routing table to public subnet
resource "aws_route_table_association" "main-public-1-a" {
subnet_id = "${aws_subnet.main-public-1.id}"
route_table_id = "${aws_route_table.main-public.id}"
}
instance.tf
resource "aws_instance" "apsglxapi01" {
ami = "${lookup(var.AMIS, var.AWS_REGION)}"
instance_type = "t2.micro"
key_name = "${aws_key_pair.infra_key.key_name}"
provisioner "local-exec" {
command = "echo ${aws_instance.apsglxapi01.private_ip}"
}
connection {
user = "${var.INSTANCE_USERNAME}"
private_key = "${file("${var.PATH_TO_PRIVATE_KEY}")}"
}
# the VPC subnet
subnet_id = "${aws_subnet.main-public-1.id}"
# the security group
vpc_security_group_ids = ["${aws_security_group.allow-ssh.id}","${aws_security_group.allow-http.id}","${aws_security_group.allow-https.id}"]
}
output "apsghost_ip" {
value = "${aws_instance.apsglxapi01.public_ip}"
}
securitygroup.tf
resource "aws_security_group" "allow-ssh" {
vpc_id = "${aws_vpc.main.id}"
name = "allow-ssh"
description = "security group that allows ssh and all egress traffic"
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags {
Name = "allow-ssh"
}
}
resource "aws_security_group" "allow-http" {
vpc_id = "${aws_vpc.main.id}"
name = "allow-http"
description = "security group that allows http and all egress traffic"
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags {
Name = "allow-http"
}
}
resource "aws_security_group" "allow-https" {
vpc_id = "${aws_vpc.main.id}"
name = "allow-https"
description = "security group that allows https and all egress traffic"
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
tags {
Name = "allow-https"
}
}
Create the plan
$ terraform init
$ terraform plan -out=infra_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.apsglxapi01
id: <computed>
ami: "ami-10bb2373"
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: "infra_key"
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: "${aws_subnet.main-public-1.id}"
tenancy: <computed>
volume_tags.%: <computed>
vpc_security_group_ids.#: <computed>
[...]
[...]
[...]
[...]
[...]
Plan: 10 to add, 0 to change, 0 to destroy.
------------------------------------------------------------------------
This plan was saved to: infra_plan
To perform exactly these actions, run the following command to apply:
terraform apply "infra_plan"
Create the resources
$ terraform apply "infra_plan"
aws_key_pair.infra_key: Creating...
fingerprint: "" => "<computed>"
key_name: "" => "infra_key"
public_key: "" => "ssh-rsa xxxxxx ron@Ronald-Riveras-MacBook-Pro.local"
aws_vpc.main: Creating...
arn: "" => "<computed>"
assign_generated_ipv6_cidr_block: "" => "false"
cidr_block: "" => "10.0.0.0/16"
default_network_acl_id: "" => "<computed>"
default_route_table_id: "" => "<computed>"
default_security_group_id: "" => "<computed>"
dhcp_options_id: "" => "<computed>"
enable_classiclink: "" => "false"
enable_classiclink_dns_support: "" => "<computed>"
enable_dns_hostnames: "" => "true"
enable_dns_support: "" => "true"
instance_tenancy: "" => "default"
ipv6_association_id: "" => "<computed>"
ipv6_cidr_block: "" => "<computed>"
main_route_table_id: "" => "<computed>"
owner_id: "" => "<computed>"
tags.%: "" => "1"
tags.Name: "" => "main"
[...]
[...]
[...]
[...]
[...]
aws_route_table.main-public: Creation complete after 6s (ID: rtb-0ae973703922bb0f4)
aws_route_table_association.main-public-1-a: Creating...
route_table_id: "" => "rtb-0ae973703922bb0f4"
subnet_id: "" => "subnet-0d8909e6f4d05a627"
aws_route_table_association.main-public-1-a: Creation complete after 1s (ID: rtbassoc-051622b0eb13c5537)
aws_instance.apsglxapi01: Still creating... (10s elapsed)
aws_instance.apsglxapi01: Still creating... (20s elapsed)
aws_instance.apsglxapi01: Still creating... (30s elapsed)
aws_instance.apsglxapi01: Still creating... (40s elapsed)
aws_instance.apsglxapi01: Provisioning with 'local-exec'...
aws_instance.apsglxapi01 (local-exec): Executing: ["/bin/sh" "-c" "echo 10.0.1.241"]
aws_instance.apsglxapi01 (local-exec): 10.0.1.241
aws_instance.apsglxapi01: Creation complete after 44s (ID: i-0e557b8b5039a5f75)
Apply complete! Resources: 10 added, 0 changed, 0 destroyed.
Outputs:
apsghost_ip = 54.255.183.42
Take note of the IP address assigned as it will be used in the subsequent steps below.
Verify we can login to the EC2 instance
$ ssh -i infra_key -l ec2-user 54.255.183.42
Last login: Thu Jan 17 14:20:12 2019 from x.x.x.x
[ec2-user@ip-10-0-1-241 ~]$ hostname
ip-10-0-1-241.ap-southeast-1.compute.internal
To ensure we don't exceed our free tier usage, let's destroy the infra by running terraform destroy
.
$ terraform destroy
aws_vpc.main: Refreshing state... (ID: vpc-070672d0924e82785)
aws_key_pair.infra_key: Refreshing state... (ID: infra_key)
aws_security_group.allow-https: Refreshing state... (ID: sg-0caf2d0e03a0b27bc)
aws_security_group.allow-ssh: Refreshing state... (ID: sg-0846f720b79bd8612)
aws_subnet.main-public-1: Refreshing state... (ID: subnet-0d8909e6f4d05a627)
aws_security_group.allow-http: Refreshing state... (ID: sg-02ccd13d8c8defe52)
aws_internet_gateway.main-gw: Refreshing state... (ID: igw-0c267b6b6f12449ce)
aws_route_table.main-public: Refreshing state... (ID: rtb-0ae973703922bb0f4)
aws_instance.apsglxapi01: Refreshing state... (ID: i-0e557b8b5039a5f75)
aws_route_table_association.main-public-1-a: Refreshing state... (ID: rtbassoc-051622b0eb13c5537)
An execution plan has been generated and is shown below.
Resource actions are indicated with the following symbols:
- destroy
[...]
[...]
[...]
[...]
[...]
aws_subnet.main-public-1: Destruction complete after 3s
aws_security_group.allow-http: Destruction complete after 3s
aws_security_group.allow-https: Destruction complete after 3s
aws_security_group.allow-ssh: Destruction complete after 3s
aws_vpc.main: Destroying... (ID: vpc-070672d0924e82785)
aws_vpc.main: Destruction complete after 1s
Destroy complete! Resources: 10 destroyed.
Conclusion
Terraform is a tool for building, changing, and versioning infrastructure safely and efficiently. The tool provides a capability to first create plan to validate and then apply for the actual infrastructure provisioning. Once provisioned according to plan, a state file is created/updated to incorporate changes. When the infrastructure is not needed anymore, it can be destroyed easily using a single command.