Skip to main content

AWS Provisioning with Terraform

Written on December 14, 2017 by Ron Rivera.

6 min read
––– views

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.

Tweet this article

Join my newsletter

Get notified whenever I post, 100% no spam.

Subscribe Now