Outline

  • What is Terraform?
  • Related technology
  • Creating your first stack
    • VPC
    • Subnets
    • Instances
    • ELB
    • Demo deploy
  • Tips and tricks
  • Terraform at Shopify

What is Terraform?

  • Infrastructure as code
  • Understands dependencies between resources
  • Group collections of resources together as a module
  • Expose information about stacks via outputs

Related technology

  • CloudFormation
    • AWS only
    • Proprietary
  • Chef, Puppet, Ansible
    • Mostly focused on config management and application orchestration
    • Plugins for infrastructure

Creating your first stack

  1. Setting up a VPC
  2. Adding subnets
  3. Adding instances
  4. Adding an ELB
  5. Deploy!

Creating your first stack

Design

Creating your first stack – Provider

provider "aws" {
  region = "us-east-1"
}
  • provider blocks configure any of the supported resource providers
  • chef, google (cloud), and many other providers come by default
  • Some read in environment vars for config. For example, AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY

Creating your first stack – VPC

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/24"
}

resource "aws_internet_gateway" "gw" {
  vpc_id = "${aws_vpc.main.id}"
}
  • ${…} is an interpolation
  • Other resources referenced in an interpolation via ${type.name.attribute}

Creating your first stack – Subnets

variable "azs" {
  default = "b,c,d,e"
  description = "Availability zones to use for subnets"
}

resource "aws_subnet" "public" {
  count = 4
  vpc_id = "${aws_vpc.main.id}"
  map_public_ip_on_launch = true

  lifecycle {
    prevent_destroy = true
  }

  cidr_block = "${cidrsubnet(aws_vpc.main.cidr_block, 2, count.index)}"

  availability_zone = "us-east-1${element(split(",", var.azs), count.index)}"

  tags {
    Name = "Public Subnet"
  }
}
  • Variables are inputs to your terraform stacks and modules
  • count is a special attribute to support multiplicity of resources
  • lifecycle is also a special attribute, here preventing the resource from being destroyed (terraform destroy, from changing a ForceNewResource attribute)
  • Interpolations can use functions, like cidrsubnet

Creating your first stack – Route table

resource "aws_route_table" "public" {
  vpc_id = "${aws_vpc.main.id}"
}

resource "aws_route" "internet" {
  route_table_id = "${aws_route_table.public.id}"
  destination_cidr_block = "0.0.0.0/0"
  gateway_id = "${aws_internet_gateway.gw.id}"
}

resource "aws_route_table_association" "public" {
  count = 4
  subnet_id = "${element(aws_subnet.public.*.id, count.index)}"
  route_table_id = "${aws_route_table.public.id}"
}
  • resource.name.*.attr is a splat, used when count > 1
  • Routes can be specified in the route table itself, but using the aws_route resource simplifies future additions / removals

Creating your first stack – Instances

resource "aws_security_group_rule" "allow_all_ssh" {
  security_group_id = "${aws_vpc.main.default_security_group_id}"
  type = "ingress"
  from_port = 22
  to_port = 22
  protocol = "tcp"
  cidr_blocks = ["0.0.0.0/0"]
}

resource "aws_instance" "www" {
  count = "${var.num_instances}"
  ami = "${var.ami}"
  instance_type = "${var.instance_type}"
  subnet_id = "${element(aws_subnet.public.*.id, count.index)}"
  key_name = "${aws_key_pair.deploy.key_name}"
  user_data = "${file(concat(path.module, "/user_data.sh"))}"

  tags {
    Name = "web-server-${count.index}"
  }
}
  • element wraps around the input list
  • Allowing all SSH will simplify our deployment resource, which will connect directly to the instance via SSH
  • We read in the userdata script with the file function, a script that will set up nginx

Creating your first stack – ELB

resource "aws_elb" "www" {
  name = "www"
  instances = ["${aws_instance.www.*.id}"]
  subnets = ["${aws_subnet.public.*.id}"]
  cross_zone_load_balancing = true
  security_groups = [
    "${aws_vpc.main.default_security_group_id}",
    "${aws_security_group.allow_elb_http.id}",
  ]

  listener {
    instance_port = 80
    instance_protocol = "http"
    lb_port = 80
    lb_protocol = "http"
  }

  # health checks

  tags { Name = "www" }
}
  • Some resources have "nested blocks" for configuration
  • For aws_elb, we can have multiple listener and health_check blocks
  • We still need square brackets around interpolations that produce lists so that we pass schema validation

Creating your first stack – Deploy

resource "null_resource" "deploy" {
  count = "${var.num_instances}"

  connection {
    user = "ec2-user"
    host = "${element(aws_instance.www.*.public_ip, count.index)}"
    agent = true
  }

  provisioner "remote-exec" {
    inline = [ "rm -rf ~/www && mkdir ~/www" ]
  }
  provisioner "file" {
    source = "${template_file.local_www_path.rendered}/"
    destination = "/home/ec2-user/www"
  }

  provisioner "remote-exec" {
    inline = [
      "sudo mv /home/ec2-user/html/* /usr/share/nginx/html",
      "sudo service nginx reload"
    ]
  }
}
  • local_www_path exists solely to strip an optional trailing slash
  • connection blocks specify how provisioners will connect to a resource
  • Normally configured by the provider, but sometimes the defaults aren't sufficient
  • Can be placed inside a provisioner for local configuration

Demo

Tips and tricks

  • Variables are only strings (for now), so to support lists you can join on a delimiter when passing a value into a module and split within the module
  • No support for conditionals, but you can use interpolations in certain ways to simulate them (e.g., ternary operations)
  • Never do a raw terraform apply, but rather output a plan file from terraform plan to use
  • State can be stored in many ways, but git is perhaps the simplest
  • Use vars files to switch between different environments (e.g., production, staging)

Terraform at Shopify

  • Manage a ~100 instance cluster of nodes (over-provisioned for flash sales)
  • Went from time to scale time taking ~1 hour to just a few minutes
  • DNS plugin for reading TXT records
  • Chef plugin for creating/delete chef nodes and clients
  • Wrapped Terraform binary to ensure best practices