Presentations are a work in progress. The content is here, but "flattened", so it lacks progressive disclosure.

Outline

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

What is Terraform?

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

Speaker notes:

  • Hashicorp working on cloud provisioning / orchestration
    • Consul, vault, packer, serf, nomad, atlas
  • No more artisanal infrastructure
  • Dependencies between resources ensures everything is brought up in correct order
  • Modules encourage reusability
  • Generated state files can be used as inputs to other stacks
2 / 16

Related technology

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

Speaker notes:

  • AWS only => Terraform is platform agnostic (Google, Docker, Dynect, Heroku, etc)
  • Proprietary => cannot extend the language
  • Config management => Terraform is infrastructure
  • Plugins for infrastructure =>
    • Works, but not what these things are built for (cohesiveness)
    • What happens when I modify the infrastructure resource?
3 / 16

Creating your first stack

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

Speaker notes:

  • Describe what a VPC is
  • Keeping things simple with public subnets
  • Application deployment is not really what Terraform should be doing
4 / 16

Creating your first stack

Design

5 / 16

Creating your first stack – Provider

1provider "aws" {
2  region = "us-east-1"
3}
  • 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

Speaker notes:

  • This is HCL (Hashicorp Config Language). Can also write in pure JSON
  • Provider blocks often optional because they either have no config (e.g., template files) or read from env (e.g., aws)
6 / 16

Creating your first stack – VPC

1resource "aws_vpc" "main" {
2  cidr_block = "10.0.0.0/24"
3}
4
5resource "aws_internet_gateway" "gw" {
6  vpc_id = "${aws_vpc.main.id}"
7}
  • ${…} is an interpolation
  • Other resources referenced in an interpolation via ${type.name.attribute}

Speaker notes:

  • Explain the CIDR notation (last 8 bits vary)
  • Internet gateways connect our VPCs to the public internet
7 / 16

Creating your first stack – Subnets

1variable "azs" {
2  default = "b,c,d,e"
3  description = "Availability zones to use for subnets"
4}
5
6resource "aws_subnet" "public" {
7  count = 4
8  vpc_id = "${aws_vpc.main.id}"
9  map_public_ip_on_launch = true
10
11  lifecycle {
12    prevent_destroy = true
13  }
14
15  cidr_block = "${cidrsubnet(aws_vpc.main.cidr_block, 2, count.index)}"
16
17  availability_zone = "us-east-1${element(split(",", var.azs), count.index)}"
18
19  tags {
20    Name = "Public Subnet"
21  }
22}
  • 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

Speaker notes:

  • No such thing as a constant, so we make use of variables
  • Many more attributes on resources, this is just a sample
  • prevent_destroy needs to be removed or set to false before resource can be destroyed
8 / 16

Creating your first stack – Route table

1resource "aws_route_table" "public" {
2  vpc_id = "${aws_vpc.main.id}"
3}
4
5resource "aws_route" "internet" {
6  route_table_id = "${aws_route_table.public.id}"
7  destination_cidr_block = "0.0.0.0/0"
8  gateway_id = "${aws_internet_gateway.gw.id}"
9}
10
11resource "aws_route_table_association" "public" {
12  count = 4
13  subnet_id = "${element(aws_subnet.public.*.id, count.index)}"
14  route_table_id = "${aws_route_table.public.id}"
15}
  • 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

Speaker notes:

  • 0.0.0.0/0 is your default route
  • Terraform doesn't manage inline routes / security group rules well, so we prefer using separate resources instead
  • Can reference attributes of individual resource; use count, lifecycle, etc
9 / 16

Creating your first stack – Instances

1resource "aws_security_group_rule" "allow_all_ssh" {
2  security_group_id = "${aws_vpc.main.default_security_group_id}"
3  type = "ingress"
4  from_port = 22
5  to_port = 22
6  protocol = "tcp"
7  cidr_blocks = ["0.0.0.0/0"]
8}
9
10resource "aws_instance" "www" {
11  count = "${var.num_instances}"
12  ami = "${var.ami}"
13  instance_type = "${var.instance_type}"
14  subnet_id = "${element(aws_subnet.public.*.id, count.index)}"
15  key_name = "${aws_key_pair.deploy.key_name}"
16  user_data = "${file(concat(path.module, "/user_data.sh"))}"
17
18  tags {
19    Name = "web-server-${count.index}"
20  }
21}
  • 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

Speaker notes:

  • Wrapping is a good default, if we have more instances than subnets, for example
  • VPN is an alternative to allowing all SSH
  • from/to port values can differ to cover all traffic (see documentation)
10 / 16

Creating your first stack – ELB

1resource "aws_elb" "www" {
2  name = "www"
3  instances = ["${aws_instance.www.*.id}"]
4  subnets = ["${aws_subnet.public.*.id}"]
5  cross_zone_load_balancing = true
6  security_groups = [
7    "${aws_vpc.main.default_security_group_id}",
8    "${aws_security_group.allow_elb_http.id}",
9  ]
10
11  listener {
12    instance_port = 80
13    instance_protocol = "http"
14    lb_port = 80
15    lb_protocol = "http"
16  }
17
18  # health checks
19
20  tags { Name = "www" }
21}
  • 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

Speaker notes:

  • See documentation for health check syntax
  • allow_http security group allows ingress from all traffic on ports 80/443 and all egress
  • Note that instances will need allow traffic on port 80 for ELBs, but the default security group allows all traffic within a VPC. This traffic does not have to be open to the world, just the ELB
11 / 16

Creating your first stack – Deploy

1resource "null_resource" "deploy" {
2  count = "${var.num_instances}"
3
4  connection {
5    user = "ec2-user"
6    host = "${element(aws_instance.www.*.public_ip, count.index)}"
7    agent = true
8  }
9
10  provisioner "remote-exec" {
11    inline = [ "rm -rf ~/www && mkdir ~/www" ]
12  }
13
14  provisioner "file" {
15    source = "${template_file.local_www_path.rendered}/"
16    destination = "/home/ec2-user/www"
17  }
18
19  provisioner "remote-exec" {
20    inline = [
21      "sudo mv /home/ec2-user/html/* /usr/share/nginx/html",
22      "sudo service nginx reload"
23    ]
24  }
25}
  • 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

Speaker notes:

  • Remember that user can vary based on AMI
  • Provisioners executed in order
  • Connections support private key auth, bastion hosts, and windows remote management
12 / 16

Demo

Speaker notes:

$ vim user_data.sh
$ vim terraform.tfvars
$ terraform plan --out=foo.plan
$ terraform show foo.plan
$ terraform apply foo.plan
$ open `terraform output endpoint`

If enough time, modify www/index.html, taint the deploy resources and apply again

13 / 16

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)

Speaker notes:

  • No easy way to pass mappings to modules, but can pass keys + values list and get a 1-level map
  • count = 0 will not make that resource
  • If you use var files, the plan you see from terraform plan may be different from what gets applied with terraform apply (if you happen to forget to use the same var files)
14 / 16

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

Speaker notes:

  • DNS plugin is quite simple and a great example of extending the language
  • DNS plugin isn't really doing CRUD resources, but just RU, because there are no other options right now. Hashicorp is working on this (data resources)
  • Chef provider was recently added, so our plugin is no longer necessary
  • Check out Shopify on GitHub to see these plugins
15 / 16

More information

16 / 16