Skip to content

Spinning up an EC2 with Terraform and Vault.

Today we will look at how to setup EC2 instance with Terraform.

  1. Set up Terraform
  2. Spin up EC2
  3. Externalise secrets and other resources with terraform variables.
  4. Set up Vault as secret repo

1. Set up Terraform

So first thing first, quick installation guide, visit https://www.terraform.io/downloads.html , pick up right version and download:

➜  apps wget https://releases.hashicorp.com/terraform/0.11.1/terraform_0.11.1_darwin_amd64.zip\?_ga\=2.1738614.654909398.1512400028-228831855.1511115744
--2017-12-04 15:16:06--  https://releases.hashicorp.com/terraform/0.11.1/terraform_0.11.1_darwin_amd64.zip?_ga=2.1738614.654909398.1512400028-228831855.1511115744
Resolving releases.hashicorp.com... 151.101.17.183, 2a04:4e42:4::439
Connecting to releases.hashicorp.com|151.101.17.183|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 15750266 (15M) [application/zip]
Saving to: ‘terraform_0.11.1_darwin_amd64.zip?_ga=2.1738614.654909398.1512400028-228831855.1511115744’

terraform_0.11.1_darwin_amd64.zip?_ga=2.17386 100%[=================================================================================================>]  15.02M   499KB/s    in 30s

2017-12-04 15:16:36 (517 KB/s) - ‘terraform_0.11.1_darwin_amd64.zip?_ga=2.1738614.654909398.1512400028-228831855.1511115744’ saved [15750266/15750266]

Then unzip:

➜  apps unzip terraform_0.11.1_darwin_amd64.zip\?_ga=2.1738614.654909398.1512400028-228831855.1511115744
Archive:  terraform_0.11.1_darwin_amd64.zip?_ga=2.1738614.654909398.1512400028-228831855.1511115744
  inflating: terraform

Finally make sure location added to PATH:

➜  ~ export PATH=~/apps:$PATH

Check installation works:

➜  ~ terraform -v
Terraform v0.11.1

2. Spin up EC2

The plan is to spin up latest Ubuntu.

Before terraform can do anything to AWS we need to create an access for terraform user in AWS IAM,
here is how:

Pick up EC2 AMI from https://cloud-images.ubuntu.com/locator/ec2/, you need to specify your region and then ubuntu release name, so I used ‘eu-west-2 zesty’.

So once AMI id is found and user is created we are ready to perform our first test.

Create a file instance.tf as below(use your own access key and secret of course):

➜  terraform_demo cat instance.tf
provider "aws" {
  access_key = "AKIAJ3XZ5SA3OKEWGGIQ"
  secret_key = "vU9d6MmQFX+RODfBaK5A1YrDVpOSIt2KZzE9FntC"
  region     = "eu-west-2"
}

resource "aws_instance" "ubuntu_zesty" {
  ami           = "ami-6b7f610f"
  instance_type = "t2.micro"
}
➜  terraform_demo

We are ready to provision it now, let’s first create plan:

➜  terraform_demo terraform plan
Plugin reinitialization required. Please run "terraform init".
Reason: Could not satisfy plugin requirements.

Plugins are external binaries that Terraform uses to access and manipulate
resources. The configuration provided requires plugins which can't be located,
don't satisfy the version constraints, or are otherwise incompatible.

1 error(s) occurred:

* provider.aws: no suitable version installed
  version requirements: "(any version)"
  versions installed: none

Terraform automatically discovers provider requirements from your
configuration, including providers used in child modules. To see the
requirements and constraints from each module, run "terraform providers".


Error: error satisfying plugin requirements

Oops, we need to initialise a Terraform working directory, let’s fix it:


➜  terraform_demo terraform init

Initializing provider plugins...
- Checking for available provider plugins on https://releases.hashicorp.com...
- Downloading plugin for provider "aws" (1.5.0)...

The following providers do not have any version constraints in configuration,
so the latest version was installed.

To prevent automatic upgrades to new major versions that may contain breaking
changes, it is recommended to add version = "..." constraints to the
corresponding provider blocks in configuration, with the constraint strings
suggested below.

* provider.aws: version = "~> 1.5"

Terraform has been successfully initialized!

You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.

If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

Now we are ready to generate and show an execution plan:

➜  terraform_demo 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.


------------------------------------------------------------------------

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.ubuntu_zesty
      id:                           <computed>
      ami:                          "ami-6b7f610f"
      associate_public_ip_address:  <computed>
      availability_zone:            <computed>
      ebs_block_device.#:           <computed>
      ephemeral_block_device.#:     <computed>
      instance_state:               <computed>
      instance_type:                "t2.micro"
      ipv6_address_count:           <computed>
      ipv6_addresses.#:             <computed>
      key_name:                     <computed>
      network_interface.#:          <computed>
      network_interface_id:         <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:                    <computed>
      tenancy:                      <computed>
      volume_tags.%:                <computed>
      vpc_security_group_ids.#:     <computed>


Plan: 1 to add, 0 to change, 0 to destroy.

------------------------------------------------------------------------

Note: You didn't specify an "-out" parameter to save this plan, so Terraform
can't guarantee that exactly these actions will be performed if
"terraform apply" is subsequently run.

If we are happy, we can proceed, I am using ‘auto-approve’ flag so command doesn’t stop and asks for annoying confirmation:


➜  terraform_demo terraform apply -auto-approve
aws_instance.ubuntu_zesty: Creating...
  ami:                          "" => "ami-6b7f610f"
  associate_public_ip_address:  "" => "<computed>"
  availability_zone:            "" => "<computed>"
  ebs_block_device.#:           "" => "<computed>"
  ephemeral_block_device.#:     "" => "<computed>"
  instance_state:               "" => "<computed>"
  instance_type:                "" => "t2.micro"
  ipv6_address_count:           "" => "<computed>"
  ipv6_addresses.#:             "" => "<computed>"
  key_name:                     "" => "<computed>"
  network_interface.#:          "" => "<computed>"
  network_interface_id:         "" => "<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:                    "" => "<computed>"
  tenancy:                      "" => "<computed>"
  volume_tags.%:                "" => "<computed>"
  vpc_security_group_ids.#:     "" => "<computed>"
aws_instance.ubuntu_zesty: Still creating... (10s elapsed)
aws_instance.ubuntu_zesty: Still creating... (20s elapsed)
aws_instance.ubuntu_zesty: Creation complete after 23s (ID: i-0aa512bde10cb1358)

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

We can check the state from file:

➜  terraform_demo cat terraform.tfstate
{
    "version": 3,
    "terraform_version": "0.11.1",
    "serial": 1,
    "lineage": "1fa856fe-712d-4467-85ab-309a05254fdb",
    "modules": [
        {
            "path": [
                "root"
            ],
            "outputs": {},
            "resources": {
                "aws_instance.ubuntu_zesty": {
                    "type": "aws_instance",
                    "depends_on": [],
                    "primary": {
                        "id": "i-000014b0382871d78",
                        "attributes": {
                            "ami": "ami-6b7f610f",
                            "associate_public_ip_address": "true",
                            "availability_zone": "eu-west-2a",
                            "disable_api_termination": "false",
                            "ebs_block_device.#": "0",
                            "ebs_optimized": "false",
                            "ephemeral_block_device.#": "0",
                            "iam_instance_profile": "",
                            "id": "i-000014b0382871d78",
                            "instance_state": "running",
                            "instance_type": "t2.micro",
                            "ipv6_addresses.#": "0",
                            "key_name": "",
                            "monitoring": "false",
                            "network_interface.#": "0",
                            "network_interface_id": "eni-e103a6b4",
                            "placement_group": "",
                            "primary_network_interface_id": "eni-e103a6b4",
                            "private_dns": "ip-172-31-0-208.eu-west-2.compute.internal",
                            "private_ip": "172.31.0.208",
                            "public_dns": "ec2-52-56-204-79.eu-west-2.compute.amazonaws.com",
                            "public_ip": "52.56.204.79",
                            "root_block_device.#": "1",
                            "root_block_device.0.delete_on_termination": "true",
                            "root_block_device.0.iops": "100",
                            "root_block_device.0.volume_size": "8",
                            "root_block_device.0.volume_type": "gp2",
                            "security_groups.#": "1",
                            "security_groups.3814588639": "default",
                            "source_dest_check": "true",
                            "subnet_id": "subnet-42f80a39",
                            "tags.%": "0",
                            "tenancy": "default",
                            "volume_tags.%": "0",
                            "vpc_security_group_ids.#": "0"
                        },
                        "meta": {
                            "e2bfb730-ecaa-11e6-8f88-34363bc7c4c0": {
                                "create": 600000000000,
                                "delete": 600000000000,
                                "update": 600000000000
                            },
                            "schema_version": "1"
                        },
                        "tainted": false
                    },
                    "deposed": [],
                    "provider": "provider.aws"
                }
            },
            "depends_on": []
        }
    ]
}

Let’s see how we can destroy it now, you can also view it in EC2 console if you like, before destroying.
Again, I am running it with ‘force’ flag, so it doesn’t ask for confirmation.
I have to admit there is no consistency between apply and destroy, as each uses different argument for same semantic:

➜  terraform_demo terraform destroy -force
aws_instance.ubuntu_zesty: Refreshing state... (ID: i-000014b0382871d78)
aws_instance.ubuntu_zesty: Destroying... (ID: i-000014b0382871d78)
aws_instance.ubuntu_zesty: Still destroying... (ID: i-000014b0382871d78, 10s elapsed)
aws_instance.ubuntu_zesty: Still destroying... (ID: i-000014b0382871d78, 20s elapsed)
aws_instance.ubuntu_zesty: Still destroying... (ID: i-000014b0382871d78, 30s elapsed)
aws_instance.ubuntu_zesty: Still destroying... (ID: i-000014b0382871d78, 40s elapsed)
aws_instance.ubuntu_zesty: Still destroying... (ID: i-000014b0382871d78, 50s elapsed)
aws_instance.ubuntu_zesty: Destruction complete after 51s

Destroy complete! Resources: 1 destroyed.
➜  terraform_demo

Please note that terraform created a backup for the state:

➜  terraform_demo ls
instance.tf              terraform.tfstate        terraform.tfstate.backup
➜  terraform_demo

3. Externalise secrets and other resources with terraform variables.

Let’s look at Terraform Variables now.

Obviously you don’t want to save the AWS secret in the terraform sources. For that reason
you would normally use variables. Let’s split our config into two parts:

➜  terraform_demo tail -n +1 *.tf
==> instance.tf <==
provider "aws" {
  access_key = "AKIAIUWY24KNLBE3OQKA"
  secret_key = "${var.secret_key}"
  region     = "eu-west-2"
}

resource "aws_instance" "ubuntu_zesty" {
  ami           = "ami-6b7f610f"
  instance_type = "t2.micro"
}

==> variabels.tf <==
variable "secret_key" {}
➜  terraform_demo

As you see instance.tf refers to secret_key variable and variabels.tf declares it, but as the value is not assigned, terraform will ask it upon running:

  terraform_demo terraform plan
var.secret_key
  Enter a value:

Now if you want to automate this and not enter manually, you would need to read the value with some tool, I used vault for instance.

4. Set up Vault to as secret repo

If you don’t want have vault installed and don’t want to install it either (in case you are very lazy guy) you can simply test this with echo:

➜  terraform_demo echo NVMqHQYd3/hw6uXX+EQk57MNfKEP6MImUl0l8Lfd | terraform plan
var.secret_key
  Enter a value:
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.
..
....

Here is a quick way to test it with vault. Go to https://www.vaultproject.io/downloads.html and download version for your machine, then unzip it:

➜  apps wget https://releases.hashicorp.com/vault/0.9.0/vault_0.9.0_darwin_amd64.zip\?_ga\=2.252075081.1958392905.1512420796-1969448237.1511121920
--2017-12-05 17:25:10--  https://releases.hashicorp.com/vault/0.9.0/vault_0.9.0_darwin_amd64.zip?_ga=2.252075081.1958392905.1512420796-1969448237.1511121920
Resolving releases.hashicorp.com... 151.101.17.183, 2a04:4e42:4::439
Connecting to releases.hashicorp.com|151.101.17.183|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 17470290 (17M) [application/zip]
Saving to: ‘vault_0.9.0_darwin_amd64.zip?_ga=2.252075081.1958392905.1512420796-1969448237.1511121920’

vault_0.9.0_darwin_amd64.zip?_ga=2.252075081. 100%[=================================================================================================>]  16.66M   351KB/s    in 59s

2017-12-05 17:26:09 (289 KB/s) - ‘vault_0.9.0_darwin_amd64.zip?_ga=2.252075081.1958392905.1512420796-1969448237.1511121920’ saved [17470290/17470290]


➜  apps unzip vault_0.9.0_darwin_amd64.zip\?_ga=2.252075081.1958392905.1512420796-1969448237.1511121920
Archive:  vault_0.9.0_darwin_amd64.zip?_ga=2.252075081.1958392905.1512420796-1969448237.1511121920
  inflating: vault
  
  
➜  apps vault -v
Vault v0.9.0 ('bdac1854478538052ba5b7ec9a9ec688d35a3335')

Once you done these steps, add vault installation to your PATH, also add this line to your PATH:

export VAULT_ADDR='http://127.0.0.1:8200'

Otherwise you will get error asby default it tries to connect to https:

➜  ~ vault write secret/aws value=NVMqHQYd3/hw6uXX+EQk57MNfKEP6MImUl0l8Lfd
Error writing data to secret/aws: Put https://127.0.0.1:8200/v1/secret/aws: http: server gave HTTP response to HTTPS client

Now start the vault:

vault server -dev

You are now ready to persist new secret and then read it:

➜  ~ vault read  secret/aws
No value found at secret/aws

➜  ~ vault write secret/aws value=NVMqHQYd3/hw6uXX+EQk57MNfKEP6MImUl0l8Lfd
Success! Data written to: secret/aws

➜  ~ vault read  secret/aws
Key             	Value
---             	-----
refresh_interval	768h0m0s
value           	NVMqHQYd3/hw6uXX+EQk57MNfKEP6MImUl0l8Lfd

We can now ready to feed the secret to terraform, note I used ‘field’ argument so there is not need to do any hack with grep/sed,
it alwasy worth reading it’s docs, ‘vault read -help’ in this case:


➜  terraform_demo vault read -field=value secret/aws  | terraform plan
var.secret_key
  Enter a value:
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.

..
....
......

Please note that terraform plan is not a dry-run sort of a thing, it actually goes to AWS
and makes necessary arrangements to make sure ‘apply’ is going to work, as an example, if I
delete the user from IAM and try again, I will get an error:

➜  terraform_demo echo NVMqHQYd3/hw6uXX+EQk57MNfKEP6MImUl0l8Lfd | terraform plan
var.secret_key
  Enter a value:
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.


------------------------------------------------------------------------

Error: Error running plan: 1 error(s) occurred:

* provider.aws: InvalidClientTokenId: The security token included in the request is invalid.
	status code: 403, request id: f67be4a2-d938-11e7-bb3b-6f82b8195005


➜  terraform_demo

That is it, we now familiar with these two nice devops tools made by hashicorp. Consul is also made
by them by the way, and I have a nice article about how you can use it for service discovery.