Terraform

From Christoph's Personal Wiki
Revision as of 19:03, 26 April 2018 by Christoph (Talk | contribs) (External links)

Jump to: navigation, search

Terraform is a tool for building, changing, and versioning infrastructure safely and efficiently. Terraform can manage existing and popular service providers as well as custom in-house solutions. It is a popular tool in DevOps.

Introduction

  • Infrastructure as Code
  • Used for the automation of your infrastructure
  • It keeps your infrastructure in a certain state (compliant)
    • E.g., 2 web instances and 2 volumes and 1 load balancer
  • It makes your infrastructure auditable
    • That is, you can keep your infrastructure change history in a version control system (e.g., git)

A high-level difference and/or reason to use Terraform over CAPS (Chef, Ansible, Puppet, Salt) is that these others have a focus on automating the installation and configuration of software (i.e., keeping the machines in compliance and in a certain state). Terraform, however, can automate provisioning of the infrastructure itself (e.g., in AWS or Google). One can, of course, do the same with, say, Ansible. However, Terraform really shines in infrastructure management and automation.

Examples

Basic example #1

The following is a super simple example of how to use Terraform to spin up a single AWS EC2 instance.

  • Create a working directory for your Terraform project:
$ mkdir ~/dev/terraform
  • Create a Terraform file describing the AWS EC2 instance to create:
$ cat << EOF >> instance.tf
provider "aws" {
  access_key = "<REDACTED>"
  secret_key = "<REDACTED>"
  region     = "us-west-2"
}

resource "aws_instance" "xtof-terraform" {
  ami           = "ami-a042f4d8"  # CentOS 7.4
  instance_type = "t2.micro"
}
EOF
  • Initialize your Terraform working directory:
$ terraform init
  • Create your EC2 instance:
$ terraform plan
$ terraform apply

Note: A better method to use is:

$ terraform plan -out myinstance.terraform
$ terraform apply myinstance.terraform

By using the two separate above commands, Terraform will first show you what changes it will make without doing the actual changes. The second command will ensure that only the changes you saw on screen are applied. If you would just use terraform apply, more changes could have been added, because the remote infrastructure can change or files could have been edited (e.g., by someone else on your team). In short, always use the plan/apply file method.

  • Destroy the above instance:
$ terraform destroy

Basic example #2

The following expounds upon what we did in "Basic example #1", except we are building a more "Best Practices" approach. We will continue to build these examples.

  • Create a working directory (aws.create_ec2_instance) with the following files:
aws.create_ec2_instance/
├── .gitignore
├── instance.tf
├── provider.tf
├── terraform.tfvars
└── vars.tf
$ cat << EOF > .gitignore
*/terraform.tfvars
*/terraform.tfstate
*/terraform.tfstate.backup
*/.terraform

The contents of each of the above files should look like the following:

$ cat << EOF >> instance.tf 
resource "aws_instance" "example" {
  ami           = "${lookup(var.AMIS, var.AWS_REGION)}"
  instance_type = "t2.micro"
}
EOF

$ cat << EOF >> provider.tf 
provider "aws" {
  access_key = "${var.AWS_ACCESS_KEY}"
  secret_key = "${var.AWS_SECRET_KEY}"
  region = "${var.AWS_REGION}"
}
EOF

$ cat << EOF >> terraform.tfvars 
AWS_ACCESS_KEY = "<REDACTED>"
AWS_SECRET_KEY = "<REDACTED>"
EOF

$ cat << EOF >> vars.tf 
variable "AWS_ACCESS_KEY" {}
variable "AWS_SECRET_KEY" {}
variable "AWS_REGION" {
  default = "us-west-2"
}
variable "AMIS" {
  type = "map"
  default = {
    us-west-2 = "ami-b2d463d2"
    us-east-1 = "ami-13be557e"
    eu-west-1 = "ami-0d729a60"
  }
}
EOF
  • Initialize the Terraform working directory:
$ terraform init
  • Now, "plan" your execution with:
$ terraform plan -out myinstance.terraform
...
+ aws_instance.example
    ami:                         "ami-b2d463d2"
    associate_public_ip_address: "<computed>"
    availability_zone:           "<computed>"
    ebs_block_device.#:          "<computed>"
    ephemeral_block_device.#:    "<computed>"
    instance_state:              "<computed>"
    instance_type:               "t2.micro"
    key_name:                    "<computed>"
    network_interface_id:        "<computed>"
    placement_group:             "<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>"
    vpc_security_group_ids.#:    "<computed>"

Plan: 1 to add, 0 to change, 0 to destroy.
  • Now, "apply" (or actually create the EC2 instance):
$ terraform apply myinstance.terraform

Concepts

Provisioners

File uploads
resource "aws_instance" "example" {
  ami           = "${lookup(var.AMIS, var.AWS_REGION)}"
  instance_type = "t2.micro"

  provisioner "file" {
    source      = "app.conf"
    destination = "/etc/myapp.conf"
  }
}
Connection
# Copies the file as the instance_username user using SSH
provisioner "file" {
  source      = "conf/myapp.conf"
  destination = "/etc/myapp.conf"

  connection {
    type     = "ssh"
    user     = "${var.instance_username}"
    password = "${var.instance_password}"
  }
}
  • Copy a script to the instance and execute it:
resource "aws_key_pair" "mykey" {
  key_name   = "christoph-aws-key"
  #public_key = "ssh-rsa my-public-key"
  public_key = "${file("${var.PATH_TO_PUBLIC_KEY}")}"
}

resource "aws_instance" "example" {
  ami           = "${lookup(var.AMIS, var.AWS_REGION)}"
  instance_type = "t2.micro"
  key_name      = "${aws_key_pair.mykey.key_name}"

  provisioner "file" {
    source      = "src/script.sh"
    destination = "/tmp/script.sh"
  }
  provisioner "remote-exec" {
    inline = [
      "chmod +x /tmp/script.sh",
      "sudo /tmp/script.sh"
    ]
  }

  connection {
    type        = "ssh"
    user        = "${var.instance_username}"
    private_key = "${file("${var.PATH_TO_PRIVATE_KEY}")}"
  }
}

Bash completion

 $ cat << EOF >> /etc/bash_completion.d/terraform
_terraform()
{
   local cmds cur colonprefixes
   cmds="apply destroy fmt get graph import init \
      output plan push refresh remote show taint \
      untaint validate version state"

   COMPREPLY=()
   cur=${COMP_WORDS[COMP_CWORD]}
   # Work-around bash_completion issue where bash interprets a colon
   # as a separator.
   # Work-around borrowed from the darcs work-around for the same
   # issue.
   colonprefixes=${cur%"${cur##*:}"}
   COMPREPLY=( $(compgen -W '$cmds'  -- $cur))
   local i=${#COMPREPLY[*]}
   while [ $((--i)) -ge 0 ]; do
      COMPREPLY[$i]=${COMPREPLY[$i]#"$colonprefixes"}
   done

        return 0
} &&
complete -F _terraform terraform
EOF

External links