Terraform at Scale
◉ Introduction
◉ Terraform at Scale?
◉ Gotta Keep ‘em Separated
◉ Reducing Complexity With Style
◉ A Tale of Two Modules
Introduction: Agenda and Takeaways
80%
Percentage of Outages Caused by Changes*
* Source: Behr, K., Kim, G., & Spafford, G. (2013). The Visible Ops Handbook
Post-fordism, cognitive-cultural economy derived from spontaneous order.
Simple and Powerful
Source: terraform.io
Terraform Providers
Terraform Providers
Terraform at Scale?
Terraform at Scale?
Terraform at Scale?
Terraform at Scale?
Terraform at Scale?
Terraform at Scale?
Terraform at Scale?
Terraform at Scale?
Terraform at Scale?
Terraform at Scale?
Terraform at Scale?
Terraform at Scale?
Terraform at Scale?
This is fine.
Gotta Keep ‘em Separated
Dive In: Code Samples
AWS Provider
provider.tf
provider "aws" {
region = "us-east-1"
}
AWS Route53 Provisioners
route53.tf
resource "aws_route53_zone" "route53_zone" {
name = "devops-demo.xyz"
}
resource "aws_route53_record" "api_route53_record" {
zone_id = "${aws_route53_zone.route53_zone.zone_id}"
name = "dev-api.devops-demo.xyz"
type = "A"
ttl = "300"
records = ["86.75.30.9"]
}
AWS Route53 Provisioners
route53.tf
resource "aws_route53_zone" "route53_zone" {
name = "devops-demo.xyz"
}
resource "aws_route53_record" "api_route53_record" {
zone_id = "${aws_route53_zone.route53_zone.zone_id}"
name = "dev-api.devops-demo.xyz"
type = "A"
ttl = "300"
records = ["86.75.30.9"]
}
route53.tf
resource "aws_route53_zone" "route53_zone" {
name = "devops-demo.xyz"
}
resource "aws_route53_record" "api_route53_record" {
zone_id = "${aws_route53_zone.route53_zone.zone_id}"
name = "dev-api.devops-demo.xyz"
type = "A"
ttl = "300"
records = ["86.75.30.9"]
}
AWS Route53 Provisioners
route53.tf
resource "aws_route53_zone" "route53_zone" {
name = "devops-demo.xyz"
}
resource "aws_route53_record" "api_route53_record" {
zone_id = "${aws_route53_zone.route53_zone.zone_id}"
name = "dev-api.devops-demo.xyz"
type = "A"
ttl = "300"
records = ["86.75.30.9"]
}
AWS Route53 Provisioners
route53.tf
resource "aws_route53_zone" "route53_zone" {
name = "devops-demo.xyz"
}
resource "aws_route53_record" "api_route53_record" {
zone_id = "${aws_route53_zone.route53_zone.zone_id}"
name = "dev-api.devops-demo.xyz"
type = "A"
ttl = "300"
records = ["86.75.30.9"]
}
AWS Route53 Provisioners
terraform plan (console output)
+ aws_route53_record.api_route53_record
fqdn: "<computed>"
name: "dev-api.devops-demo.xyz"
records.#: "1"
records.4228697306: "86.75.30.9"
ttl: "300"
type: "A"
zone_id: "${aws_route53_zone.route53_zone.zone_id}"
+ aws_route53_zone.route53_zone
comment: "Managed by Terraform"
force_destroy: "false"
name: "devops-demo.xyz"
name_servers.#: "<computed>"
vpc_region: "<computed>"
zone_id: "<computed>"
Plan: 2 to add, 0 to change, 0 to destroy.
Root Module Example
providers.tf
route53.tf
Our First Module v1
outputs.tf
providers.tf
route53.tf
variables.tf
Congratulations On Your WET Module!
Image Source: “Campaign Shake-Up” Parks and Recreation, season 4, episode 17, NBC, 01 Mar. 2012
Parameterize To Stay DRY
provider.tf
provider "aws" {
region = "us-east-1"
}
route53.tf
resource "aws_route53_zone" "route53_zone" {
name = "devops-demo.xyz"
}
resource "aws_route53_record" "api_route53_record" {
zone_id = "${aws_route53_zone.route53_zone.zone_id}"
name = "dev-api.devops-demo.xyz"
type = "A"
ttl = "300"
records = ["86.75.30.9"]
}
Input Variables
provider.tf
provider "aws" {
region = "${var.aws_region}"
}
route53.tf
resource "aws_route53_zone" "route53_zone" {
name = "${var.domain_name}"
}
resource "aws_route53_record" "api_route53_record" {
zone_id = "${aws_route53_zone.route53_zone.zone_id}"
name = "${var.environment}-api.${var.domain_name}"
type = "A"
ttl = "300"
records = ["${var.ip_address}”]
}
variables.tf
variable "aws_account" {
default = "sysadvent-production"
}
variable "aws_region" {
default = "us-east-1"
}
variable "domain_name" {
default = "devops-demo.xyz"
}
variable "environment" {
default = "dev"
}
variable "ip_address" {
default = "86.75.30.9"
}
terraform apply (console output)
aws_route53_zone.route53_zone: Creating...
comment: "" => "Managed by Terraform"
force_destroy: "" => "false"
name: "" => "devops-demo.xyz"
name_servers.#: "" => "<computed>"
vpc_region: "" => "<computed>"
zone_id: "" => "<computed>"
aws_route53_zone.route53_zone: Creation complete (ID: Z3CIMZCT6RGERD)
aws_route53_record.api_route53_record: Creating...
fqdn: "" => "<computed>"
name: "" => "dev-api.devops-demo.xyz"
records.#: "" => "1"
records.4228697306: "" => "86.75.30.9"
ttl: "" => "300"
type: "" => "A"
zone_id: "" => "Z3QZEABS9HJLIN"
aws_route53_record.api_route53_record: Creation complete (ID: Z3CIMZCT6RGERD_api.devops-demo.xyz_A)
...
terraform apply (console output cont.)
...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.
State path:
Outputs:
api_fqdn = dev-api.devops-demo.xyz
domain_name = devops-demo.xyz
zone_id = Z3QZEABS9HJLIN
terraform apply (console output cont.)
...
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
The state of your infrastructure has been saved to the path
below. This state is required to modify and destroy your
infrastructure, so keep it safe. To inspect the complete state
use the `terraform show` command.
State path:
Outputs:
api_fqdn = dev-api.devops-demo.xyz
domain_name = devops-demo.xyz
zone_id = Z3QZEABS9HJLIN
outputs.tf
output "api_fqdn" {
value = "${aws_route53_record.api_route53_record.fqdn}"
}
output "domain_name" {
value = "${var.domain_name}"
}
output "zone_id" {
value = "${aws_route53_zone.route53_zone.id}"
}
Output Variables
outputs.tf
output "api_fqdn" {
value = "${aws_route53_record.api_route53_record.fqdn}"
}
output "domain_name" {
value = "${var.domain_name}"
}
output "zone_id" {
value = "${aws_route53_zone.route53_zone.id}"
}
Output Variables
outputs.tf
output "api_fqdn" {
value = "${aws_route53_record.api_route53_record.fqdn}"
}
output "domain_name" {
value = "${var.domain_name}"
}
output "zone_id" {
value = "${aws_route53_zone.route53_zone.id}"
}
Output Variables
Root Module Example
outputs.tf
providers.tf
route53.tf
variables.tf
Our First Module v2
Reducing Complexity
With Style
eip.tf
internet_gateway.tf
nat_gateway.tf
outputs.tf
providers.tf
route.tf
subnets.tf
variables.tf
vpc.tf
Naming Conventions: File Names
Naming Conventions: Resource Names
◉ RESOURCE NAME = RESOURCE TYPE - PROVIDER NAME
resource "aws_security_group" "security_group" {
name = "${var.resource_name}-security-group"
...
Naming Conventions: Resource Names (cont.)
resource "aws_s3_bucket" "data_s3_bucket" {
bucket = "${var.env}-data-${var.aws_region}"
}
resource "aws_s3_bucket" "images_s3_bucket" {
bucket = "${var.env}-images-${var.aws_region}"
}
◉ Multiple resources of the same TYPE should have a minimalistic identifier to
differentiate between the two resources.
When to Use an Underscore
resource "aws_route53_record" "api_route53_record" {
zone_id = "${aws_route53_zone.route53_zone.zone_id}"
name = "${var.environment}-api.${var.domain_name}"
type = "A"
ttl = "300"
records = ["${var.ip_address}"]
}
◉ Variable Names
◉ Resource Names
◉ Anything Interpolated
When to Use a Hyphen
resource "aws_route53_record" "api_route53_record" {
zone_id = "${aws_route53_zone.route53_zone.zone_id}"
name = "${var.environment}-api.${var.domain_name}"
type = "A"
ttl = "300"
records = ["${var.ip_address}"]
}
◉ Resources Being Created
Source: terraform.io
Source: terraform.io
A Tale of Two Modules
Service Module
● Reusable library
● Creates all required resources a service needs to be operational i.e. EC2
instances, S3 bucket, DNS Entries
A Tale of Two Modules
Infrastructure Module
● Single repository comprised of multiple root modules
● This is where Service Modules are instantiated/Terraform is run.
Why Infrastructure Modules?
◉ Conditional Statements
○ Don’t exist Are painful (1604)
◉ Segment State
○ Reduce risk of changes
○ Flexible state reference
◉ DRY Terraform Configurations
○ Instantiate reusable modules
◉ Environments Aren’t Recommended (0.9)
◉ Workspaces are a thing (renamed statefile)
The Candle Problem
Source: http://whatismotivation.weebly.com/uploads/2/9/9/1/29913749/1713129_orig.png
Overcome Functional Fixedness
Source: http://whatismotivation.weebly.com/uploads/2/9/9/1/29913749/1713129_orig.png
https://kvaes.wordpress.com/2013/06/05/lingo-explained-greenfield-vs-brownfield/
Terraform: Configuration by Convention
Configuration by Convention
Infrastructure Repository Folder Structure
AWS (provider)
|__ production-account (aws-account root module)
|__ us-east-1 (aws-region root module)
|__ production-us-east-1-vpc (vpc root module)
|__ production (environment root module)
|__ inf-bastion (service root module)
Account Root Module
Infrastructure Repository Folder Structure
AWS (provider)
|__ production-account (aws-account root module) <~~~~~ YOU ARE HERE
|__ us-east-1 (aws-region root module)
|__ production-us-east-1-vpc (vpc root module)
|__ production (environment root module)
|__ inf-bastion (service root module)
terraform.sh (bash wrapper)
Usage: ./templates/account-terraform.sh [apply|destroy|plan|refresh|show]
The following arguments are supported:
apply Refresh the Terraform remote state, "terraform get -update", and "terraform apply"
destroy Refresh the Terraform remote state and destroy the Terraform stack
plan Refresh the Terraform remote state, "terraform get -update", and "terraform plan"
refresh Refresh the Terraform remote state
show Refresh and show the Terraform remote state
Refresh Function
...
refresh() {
root=$(pwd | awk -F "/" '{print $(NF-1)}')
aws_account=$(pwd | awk -F "/" '{print $NF}')
export TF_VAR_root="$root"
export TF_VAR_aws_account="$aws_account"
export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache"
echo -e "nn***** Refreshing State and Upgrading Modules *****"
echo "no" | terraform init -get=true 
-upgrade 
-input=false 
-backend=true 
-backend-config "bucket=${aws_account}-terraform-state" 
-backend-config "key=${root}/terraform.tfstate" 
-backend-config "profile=${aws_account}" 
-backend-config "region=us-east-1"
}
...
Refresh Function
...
refresh() {
root=$(pwd | awk -F "/" '{print $(NF-1)}')
aws_account=$(pwd | awk -F "/" '{print $NF}')
export TF_VAR_root="$root"
export TF_VAR_aws_account="$aws_account"
export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache"
echo -e "nn***** Refreshing State and Upgrading Modules *****"
echo "no" | terraform init -get=true 
-upgrade 
-input=false 
-backend=true 
-backend-config "bucket=${aws_account}-terraform-state" 
-backend-config "key=${root}/terraform.tfstate" 
-backend-config "profile=${aws_account}" 
-backend-config "region=us-east-1"
}
...
Refresh Function
...
refresh() {
root=$(pwd | awk -F "/" '{print $(NF-1)}')
aws_account=$(pwd | awk -F "/" '{print $NF}')
export TF_VAR_root="$root"
export TF_VAR_aws_account="$aws_account"
export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache"
echo -e "nn***** Refreshing State and Upgrading Modules *****"
echo "no" | terraform init -get=true 
-upgrade 
-input=false 
-backend=true 
-backend-config "bucket=${aws_account}-terraform-state" 
-backend-config "key=${root}/terraform.tfstate" 
-backend-config "profile=${aws_account}" 
-backend-config "region=us-east-1"
}
...
...
createBackendConfig() {
/bin/cat > backend.tf <<EOL
terraform {
backend "s3" {}
}
EOL
}
...
Create Backend Config
backend.tf
terraform {
backend "s3" {}
}
Account Root Module
backend.tf
outputs.tf
providers.tf
terraform.sh
route53.tf
variables.tf
Our First Module v3
AWS-Region Root Module
Infrastructure Repository Folder Structure
AWS (provider)
|__ production-account (aws-account root module)
|__ us-east-1 (aws-region root module) <~~~~~ PLACEHOLDER
|__ production-us-east-1-vpc (vpc root module)
|__ production (environment root module)
|__ inf-bastion (service root module)
VPC Root Module
Infrastructure Repository Folder Structure
AWS (provider)
|__ production-account (aws-account root module)
|__ us-east-1 (aws-region root module)
|__ production-us-east-1-vpc (vpc root module) <~~~~~ YOU ARE HERE
|__ production (environment root module)
|__ inf-bastion (service root module)
VPC Root Module
backend.tf
main.tf
outputs.tf
terraform.sh
variables.tf
VPC Root Module (cont.)
main.tf
module "vpc" {
source = "git@github.com:TerraformDesignPattern/vpc.git?ref=1.2.3"
availability_zones = "${var.availability_zones}"
aws_region = "${var.aws_region}"
private_subnets = "${var.private_subnets}"
public_subnets = "${var.public_subnets}"
vpc_cidr = "${var.vpc_cidr}"
vpc_name = "${var.vpc_name}"
}
VPC Root Module (cont.)
main.tf
module "vpc" {
source = "git@github.com:TerraformDesignPattern/vpc.git?ref=1.2.3"
availability_zones = "${var.availability_zones}"
aws_region = "${var.aws_region}"
private_subnets = "${var.private_subnets}"
public_subnets = "${var.public_subnets}"
vpc_cidr = "${var.vpc_cidr}"
vpc_name = "${var.vpc_name}"
}
VPC Root Module (cont.)
main.tf
module "vpc" {
source = "git@github.com:TerraformDesignPattern/vpc.git?ref=${var.branch}"
availability_zones = "${var.availability_zones}"
aws_region = "${var.aws_region}"
private_subnets = "${var.private_subnets}"
public_subnets = "${var.public_subnets}"
vpc_cidr = "${var.vpc_cidr}"
vpc_name = "${var.vpc_name}"
}
VPC Root Module (cont.)
NOPE.
main.tf
module "vpc" {
source = "git@github.com:TerraformDesignPattern/vpc.git?ref=${var.branch}"
availability_zones = "${var.availability_zones}"
aws_region = "${var.aws_region}"
private_subnets = "${var.private_subnets}"
public_subnets = "${var.public_subnets}"
vpc_cidr = "${var.vpc_cidr}"
vpc_name = "${var.vpc_name}"
}
VPC Root Module (cont.)
No interpolation allowed!
Issue #1439
main.tf
module "vpc" {
source = "git@github.com:TerraformDesignPattern/vpc.git?ref=${var.branch}"
availability_zones = "${var.availability_zones}"
aws_region = "${var.aws_region}"
private_subnets = "${var.private_subnets}"
public_subnets = "${var.public_subnets}"
vpc_cidr = "${var.vpc_cidr}"
vpc_name = "${var.vpc_name}"
}
VPC Root Module (cont.)
No interpolation allowed!
Issue #1439
NOPE.
Soon.
Maybe?
main.tf
module "vpc" {
source = "git@github.com:TerraformDesignPattern/vpc.git?ref=1.2.3"
availability_zones = "${var.availability_zones}"
aws_region = "${var.aws_region}"
private_subnets = "${var.private_subnets}"
public_subnets = "${var.public_subnets}"
vpc_cidr = "${var.vpc_cidr}"
vpc_name = "${var.vpc_name}"
}
VPC Root Module (cont.)
VPC Root Module
backend.tf
main.tf
outputs.tf
terraform.sh
variables.tf
VPC Root Module (cont.)
variables.tf
...
variable "public_subnets" {
default = [
"172.19.101.0/24",
"172.19.102.0/24",
"172.19.103.0/24",
]
}
...
VPC Root Module (cont.)
VPC Module Repository
eip.tf
internet_gateway.tf
nat_gateway.tf
outputs.tf
providers.tf
route.tf
subnets.tf
variables.tf
vpc.tf
VPC Service Module
outputs.tf
...
output "public_subnet_ids" {
value = ["${aws_subnet.public_subnet.*.id}"]
}
...
VPC Service Module Outputs
outputs.tf
...
output "public_subnet_ids" {
value = ["${module.vpc.public_subnet_ids}"]
}
...
VPC Root Module Outputs
To Begin Again... From The Beginning
Quote: Waking Life. Dir. Richard Linklater. Fox Searchlight Pictures, 2001. FIlm.
Image: Fight Club. Dir. David Fincher. 20th Century Fox, 1999. Film.
s3.tf
resource "aws_s3_bucket_object" "outputs_object" {
bucket = "${var.aws_account}-terraform-state"
key =
"aws/${var.aws_region}/${var.vpc_name}/dummy_object_outputs"
source = "outputs.tf"
etag = "${md5(file("outputs.tf"))}"
}
resource "aws_s3_bucket_object" "variables_object" {
bucket = "${var.aws_account}-terraform-state"
key =
"aws/${var.aws_region}/${var.vpc_name}/dummy_object_variables"
source = "variables.tf"
etag = "${md5(file("variables.tf"))}"
}
Root Module State Seeding
outputs.tf
output "public_subnet_ids" {
value = ["${var.public_subnets}"]
}
...
variables.tf
variable "public_subnets" {
default = [
"172.19.101.0/24",
"172.19.102.0/24",
"172.19.103.0/24",
]
}
...
Environment Root Module
Infrastructure Repository Folder Structure
AWS (provider)
|__ production-account (aws-account root module)
|__ us-east-1 (aws-region root module)
|__ production-us-east-1-vpc (vpc root module)
|__ production (environment root module) <~~~~~ PLACEHOLDER
|__ inf-bastion (service root module)
http://kristinvogel.edublogs.org/files/2014/04/home-stretch-ouwf9k.jpg
Service Root Module
Infrastructure Repository Folder Structure
AWS (provider)
|__ production-account (aws-account root module)
|__ us-east-1 (aws-region root module)
|__ production-us-east-1-vpc (vpc root module)
|__ production (environment root module)
|__ inf-bastion (service root module) <~~~~~ YOU ARE HERE
SSH Bastion Root Module
backend.tf
main.tf
outputs.tf
terraform.sh
variables.tf
SSH Bastion Root Module
variables.tf
variable "aws_account" {}
variable "aws_region" {}
variable "aws_environment_name" {}
variable "service_name" {}
variable "vpc_name" {}
SSH Bastion Root Module (cont.)
refresh() {
root=$(pwd | awk -F "/" '{print $(NF-5)}')
aws_account=$(pwd | awk -F "/" '{print $(NF-4)}')
aws_region=$(pwd | awk -F "/" '{print $(NF-3)}')
vpc_name=$(pwd | awk -F "/" '{print $(NF-2)}')
environment_name=$(pwd | awk -F "/" '{print $(NF-1)}')
service_name=$(pwd | awk -F "/" '{print $NF}')
bucket_key="${root}/${aws_region}/${vpc_name}/${environment_name}/${service_name}"
export TF_VAR_root="$root"
export TF_VAR_aws_account="$aws_account"
export TF_VAR_aws_region="$aws_region"
export TF_VAR_vpc_name="$vpc_name"
export TF_VAR_environment_name="$environment_name"
export TF_VAR_service_name="$service_name"
export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache"
echo -e "nn***** Refreshing State and Upgrading Modules *****"
echo "no" | terraform init -get=true 
-upgrade 
-input=false 
-backend=true 
-backend-config "bucket=${aws_account}-terraform-state" 
-backend-config "key=${bucket_key}/terraform.tfstate" 
-backend-config "profile=${aws_account}" 
-backend-config "region=us-east-1"
}
Refresh Function
refresh() {
root=$(pwd | awk -F "/" '{print $(NF-5)}')
aws_account=$(pwd | awk -F "/" '{print $(NF-4)}')
aws_region=$(pwd | awk -F "/" '{print $(NF-3)}')
vpc_name=$(pwd | awk -F "/" '{print $(NF-2)}')
environment_name=$(pwd | awk -F "/" '{print $(NF-1)}')
service_name=$(pwd | awk -F "/" '{print $NF}')
bucket_key="${root}/${aws_region}/${vpc_name}/${environment_name}/${service_name}"
export TF_VAR_root="$root"
export TF_VAR_aws_account="$aws_account"
export TF_VAR_aws_region="$aws_region"
export TF_VAR_vpc_name="$vpc_name"
export TF_VAR_environment_name="$environment_name"
export TF_VAR_service_name="$service_name"
export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache"
echo -e "nn***** Refreshing State and Upgrading Modules *****"
echo "no" | terraform init -get=true 
-upgrade 
-input=false 
-backend=true 
-backend-config "bucket=${aws_account}-terraform-state" 
-backend-config "key=${bucket_key}/terraform.tfstate" 
-backend-config "profile=${aws_account}" 
-backend-config "region=us-east-1"
}
Refresh Function
refresh() {
root=$(pwd | awk -F "/" '{print $(NF-5)}')
aws_account=$(pwd | awk -F "/" '{print $(NF-4)}')
aws_region=$(pwd | awk -F "/" '{print $(NF-3)}')
vpc_name=$(pwd | awk -F "/" '{print $(NF-2)}')
environment_name=$(pwd | awk -F "/" '{print $(NF-1)}')
service_name=$(pwd | awk -F "/" '{print $NF}')
bucket_key="${root}/${aws_region}/${vpc_name}/${environment_name}/${service_name}"
export TF_VAR_root="$root"
export TF_VAR_aws_account="$aws_account"
export TF_VAR_aws_region="$aws_region"
export TF_VAR_vpc_name="$vpc_name"
export TF_VAR_environment_name="$environment_name"
export TF_VAR_service_name="$service_name"
export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache"
echo -e "nn***** Refreshing State and Upgrading Modules *****"
echo "no" | terraform init -get=true 
-upgrade 
-input=false 
-backend=true 
-backend-config "bucket=${aws_account}-terraform-state" 
-backend-config "key=${bucket_key}/terraform.tfstate" 
-backend-config "profile=${aws_account}" 
-backend-config "region=us-east-1"
}
Refresh Function
refresh() {
root=$(pwd | awk -F "/" '{print $(NF-5)}')
aws_account=$(pwd | awk -F "/" '{print $(NF-4)}')
aws_region=$(pwd | awk -F "/" '{print $(NF-3)}')
vpc_name=$(pwd | awk -F "/" '{print $(NF-2)}')
environment_name=$(pwd | awk -F "/" '{print $(NF-1)}')
service_name=$(pwd | awk -F "/" '{print $NF}')
bucket_key="${root}/${aws_region}/${vpc_name}/${environment_name}/${service_name}"
export TF_VAR_root="$root"
export TF_VAR_aws_account="$aws_account"
export TF_VAR_aws_region="$aws_region"
export TF_VAR_vpc_name="$vpc_name"
export TF_VAR_environment_name="$environment_name"
export TF_VAR_service_name="$service_name"
export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache"
echo -e "nn***** Refreshing State and Upgrading Modules *****"
echo "no" | terraform init -get=true 
-upgrade 
-input=false 
-backend=true 
-backend-config "bucket=${aws_account}-terraform-state" 
-backend-config "key=${bucket_key}/terraform.tfstate" 
-backend-config "profile=${aws_account}" 
-backend-config "region=us-east-1"
}
Refresh Function
refresh() {
root=$(pwd | awk -F "/" '{print $(NF-5)}')
aws_account=$(pwd | awk -F "/" '{print $(NF-4)}')
aws_region=$(pwd | awk -F "/" '{print $(NF-3)}')
vpc_name=$(pwd | awk -F "/" '{print $(NF-2)}')
environment_name=$(pwd | awk -F "/" '{print $(NF-1)}')
service_name=$(pwd | awk -F "/" '{print $NF}')
bucket_key="${root}/${aws_region}/${vpc_name}/${environment_name}/${service_name}"
export TF_VAR_root="$root"
export TF_VAR_aws_account="$aws_account"
export TF_VAR_aws_region="$aws_region"
export TF_VAR_vpc_name="$vpc_name"
export TF_VAR_environment_name="$environment_name"
export TF_VAR_service_name="$service_name"
export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache"
echo -e "nn***** Refreshing State and Upgrading Modules *****"
echo "no" | terraform init -get=true 
-upgrade 
-input=false 
-backend=true 
-backend-config "bucket=${aws_account}-terraform-state" 
-backend-config "key=${bucket_key}/terraform.tfstate" 
-backend-config "profile=${aws_account}" 
-backend-config "region=us-east-1"
}
Refresh Function
refresh() {
root=$(pwd | awk -F "/" '{print $(NF-5)}')
aws_account=$(pwd | awk -F "/" '{print $(NF-4)}')
aws_region=$(pwd | awk -F "/" '{print $(NF-3)}')
vpc_name=$(pwd | awk -F "/" '{print $(NF-2)}')
environment_name=$(pwd | awk -F "/" '{print $(NF-1)}')
service_name=$(pwd | awk -F "/" '{print $NF}')
bucket_key="${root}/${aws_region}/${vpc_name}/${environment_name}/${service_name}"
export TF_VAR_root="$root"
export TF_VAR_aws_account="$aws_account"
export TF_VAR_aws_region="$aws_region"
export TF_VAR_vpc_name="$vpc_name"
export TF_VAR_environment_name="$environment_name"
export TF_VAR_service_name="$service_name"
export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache"
echo -e "nn***** Refreshing State and Upgrading Modules *****"
echo "no" | terraform init -get=true 
-upgrade 
-input=false 
-backend=true 
-backend-config "bucket=${aws_account}-terraform-state" 
-backend-config "key=${bucket_key}/terraform.tfstate" 
-backend-config "profile=${aws_account}" 
-backend-config "region=us-east-1"
}
Refresh Function
module “bastion" {
source = "git@github.com:TerraformDesignPattern/bastionhost.git?ref=0.1.0"
aws_account = "${var.aws_account}"
aws_region = "${var.aws_region}"
environment_name = "${var.environment_name}"
vpc_name = "${var.vpc_name}"
service_name = "${element(split("-", var.service_name), 0)}"
tier_name = "${element(split("-", var.service_name), 1)}"
}
SSH Bastion Root Module (cont.)
SSH Bastion Module Repository
data.tf
ec2.tf
outputs.tf
providers.tf
route53.tf
security_groups.tf
variables.tf
SSH Bastion Service Module
data.tf
data "terraform_remote_state" "account" {
backend = "s3"
config {
bucket = "${var.aws_account}-terraform-state"
key = "aws/terraform.tfstate"
region = "us-east-1"
}
}
data "terraform_remote_state" "vpc" {
backend = "s3"
config {
bucket = "${var.aws_account}-terraform-state"
key = "aws/${var.aws_region}/${var.vpc_name}/terraform.tfstate"
region = "us-east-1"
}
}
route53.tf
resource "aws_route53_record" "route53_record" {
zone_id = "${data.terraform_remote_state.account.zone_id}"
name = "${local.hostname}.${data.terraform_remote_state.account.domain_name}"
type = "A"
ttl = "300"
records = ["${aws_instance.instance.public_ip}"]
}
...
security_groups.tf
resource "aws_security_group" "elb_security_group" {
name = "${local.resource_name}-elb-sg-${data.terraform_remote_state.vpc.aws_region_shortname}"
vpc_id = "${data.terraform_remote_state.vpc.vpc_id}"
...
route53.tf
resource "aws_route53_record" "route53_record" {
zone_id = "${data.terraform_remote_state.account.zone_id}"
name = "${local.hostname}.${data.terraform_remote_state.account.domain_name}"
type = "A"
ttl = "300"
records = ["${aws_instance.instance.public_ip}"]
}
...
security_groups.tf
resource "aws_security_group" "elb_security_group" {
name = "${local.resource_name}-elb-sg-${data.terraform_remote_state.vpc.aws_region_shortname}"
vpc_id = "${data.terraform_remote_state.vpc.vpc_id}"
...
variables.tf
locals {
hostname = "${var.environment_name}-${var.service_name}-${var.tier}-${data.terraform_remote_state.vpc.aws_region_shortname}"
resource_name = "${var.environment_name}-${var.service_name}-${var.tier}"
}
...
Module “Local” Variables
Manage State & Variables Dynamically
Infrastructure Repository Folder Structure
AWS (provider)
|__ production-account (root aws-account module)
|__ us-east-1 (root aws-region module)
|__ production-us-east-1-vpc (root vpc module)
|__ production (root environment module)
|__ bastion (root service module)
◉ Introduction
◉ Terraform at Scale?
◉ Gotta Keep ‘em Separated
◉ Reducing Complexity With Style
◉ A Tale of Two Modules
Closing: Agenda and Takeaways
Contact:
◉ Twitter: @jonbrouse
◉ Github: github.com/jonbrouse
◉ Site: jonbrouse.com
◉ github.com/TerraformDesignPattern
Thank you!
Jon Brouse

Terraform at Scale - All Day DevOps 2017

  • 1.
  • 2.
    ◉ Introduction ◉ Terraformat Scale? ◉ Gotta Keep ‘em Separated ◉ Reducing Complexity With Style ◉ A Tale of Two Modules Introduction: Agenda and Takeaways
  • 3.
    80% Percentage of OutagesCaused by Changes* * Source: Behr, K., Kim, G., & Spafford, G. (2013). The Visible Ops Handbook
  • 4.
    Post-fordism, cognitive-cultural economyderived from spontaneous order.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
    AWS Route53 Provisioners route53.tf resource"aws_route53_zone" "route53_zone" { name = "devops-demo.xyz" } resource "aws_route53_record" "api_route53_record" { zone_id = "${aws_route53_zone.route53_zone.zone_id}" name = "dev-api.devops-demo.xyz" type = "A" ttl = "300" records = ["86.75.30.9"] }
  • 27.
    AWS Route53 Provisioners route53.tf resource"aws_route53_zone" "route53_zone" { name = "devops-demo.xyz" } resource "aws_route53_record" "api_route53_record" { zone_id = "${aws_route53_zone.route53_zone.zone_id}" name = "dev-api.devops-demo.xyz" type = "A" ttl = "300" records = ["86.75.30.9"] }
  • 28.
    route53.tf resource "aws_route53_zone" "route53_zone"{ name = "devops-demo.xyz" } resource "aws_route53_record" "api_route53_record" { zone_id = "${aws_route53_zone.route53_zone.zone_id}" name = "dev-api.devops-demo.xyz" type = "A" ttl = "300" records = ["86.75.30.9"] } AWS Route53 Provisioners
  • 29.
    route53.tf resource "aws_route53_zone" "route53_zone"{ name = "devops-demo.xyz" } resource "aws_route53_record" "api_route53_record" { zone_id = "${aws_route53_zone.route53_zone.zone_id}" name = "dev-api.devops-demo.xyz" type = "A" ttl = "300" records = ["86.75.30.9"] } AWS Route53 Provisioners
  • 30.
    route53.tf resource "aws_route53_zone" "route53_zone"{ name = "devops-demo.xyz" } resource "aws_route53_record" "api_route53_record" { zone_id = "${aws_route53_zone.route53_zone.zone_id}" name = "dev-api.devops-demo.xyz" type = "A" ttl = "300" records = ["86.75.30.9"] } AWS Route53 Provisioners
  • 31.
    terraform plan (consoleoutput) + aws_route53_record.api_route53_record fqdn: "<computed>" name: "dev-api.devops-demo.xyz" records.#: "1" records.4228697306: "86.75.30.9" ttl: "300" type: "A" zone_id: "${aws_route53_zone.route53_zone.zone_id}" + aws_route53_zone.route53_zone comment: "Managed by Terraform" force_destroy: "false" name: "devops-demo.xyz" name_servers.#: "<computed>" vpc_region: "<computed>" zone_id: "<computed>" Plan: 2 to add, 0 to change, 0 to destroy.
  • 32.
  • 33.
    outputs.tf providers.tf route53.tf variables.tf Congratulations On YourWET Module! Image Source: “Campaign Shake-Up” Parks and Recreation, season 4, episode 17, NBC, 01 Mar. 2012
  • 34.
    Parameterize To StayDRY provider.tf provider "aws" { region = "us-east-1" } route53.tf resource "aws_route53_zone" "route53_zone" { name = "devops-demo.xyz" } resource "aws_route53_record" "api_route53_record" { zone_id = "${aws_route53_zone.route53_zone.zone_id}" name = "dev-api.devops-demo.xyz" type = "A" ttl = "300" records = ["86.75.30.9"] }
  • 35.
    Input Variables provider.tf provider "aws"{ region = "${var.aws_region}" } route53.tf resource "aws_route53_zone" "route53_zone" { name = "${var.domain_name}" } resource "aws_route53_record" "api_route53_record" { zone_id = "${aws_route53_zone.route53_zone.zone_id}" name = "${var.environment}-api.${var.domain_name}" type = "A" ttl = "300" records = ["${var.ip_address}”] }
  • 36.
    variables.tf variable "aws_account" { default= "sysadvent-production" } variable "aws_region" { default = "us-east-1" } variable "domain_name" { default = "devops-demo.xyz" } variable "environment" { default = "dev" } variable "ip_address" { default = "86.75.30.9" }
  • 37.
    terraform apply (consoleoutput) aws_route53_zone.route53_zone: Creating... comment: "" => "Managed by Terraform" force_destroy: "" => "false" name: "" => "devops-demo.xyz" name_servers.#: "" => "<computed>" vpc_region: "" => "<computed>" zone_id: "" => "<computed>" aws_route53_zone.route53_zone: Creation complete (ID: Z3CIMZCT6RGERD) aws_route53_record.api_route53_record: Creating... fqdn: "" => "<computed>" name: "" => "dev-api.devops-demo.xyz" records.#: "" => "1" records.4228697306: "" => "86.75.30.9" ttl: "" => "300" type: "" => "A" zone_id: "" => "Z3QZEABS9HJLIN" aws_route53_record.api_route53_record: Creation complete (ID: Z3CIMZCT6RGERD_api.devops-demo.xyz_A) ...
  • 38.
    terraform apply (consoleoutput cont.) ... Apply complete! Resources: 2 added, 0 changed, 0 destroyed. The state of your infrastructure has been saved to the path below. This state is required to modify and destroy your infrastructure, so keep it safe. To inspect the complete state use the `terraform show` command. State path: Outputs: api_fqdn = dev-api.devops-demo.xyz domain_name = devops-demo.xyz zone_id = Z3QZEABS9HJLIN
  • 40.
    terraform apply (consoleoutput cont.) ... Apply complete! Resources: 2 added, 0 changed, 0 destroyed. The state of your infrastructure has been saved to the path below. This state is required to modify and destroy your infrastructure, so keep it safe. To inspect the complete state use the `terraform show` command. State path: Outputs: api_fqdn = dev-api.devops-demo.xyz domain_name = devops-demo.xyz zone_id = Z3QZEABS9HJLIN
  • 41.
    outputs.tf output "api_fqdn" { value= "${aws_route53_record.api_route53_record.fqdn}" } output "domain_name" { value = "${var.domain_name}" } output "zone_id" { value = "${aws_route53_zone.route53_zone.id}" } Output Variables
  • 42.
    outputs.tf output "api_fqdn" { value= "${aws_route53_record.api_route53_record.fqdn}" } output "domain_name" { value = "${var.domain_name}" } output "zone_id" { value = "${aws_route53_zone.route53_zone.id}" } Output Variables
  • 43.
    outputs.tf output "api_fqdn" { value= "${aws_route53_record.api_route53_record.fqdn}" } output "domain_name" { value = "${var.domain_name}" } output "zone_id" { value = "${aws_route53_zone.route53_zone.id}" } Output Variables
  • 44.
  • 45.
  • 46.
  • 47.
    Naming Conventions: ResourceNames ◉ RESOURCE NAME = RESOURCE TYPE - PROVIDER NAME resource "aws_security_group" "security_group" { name = "${var.resource_name}-security-group" ...
  • 48.
    Naming Conventions: ResourceNames (cont.) resource "aws_s3_bucket" "data_s3_bucket" { bucket = "${var.env}-data-${var.aws_region}" } resource "aws_s3_bucket" "images_s3_bucket" { bucket = "${var.env}-images-${var.aws_region}" } ◉ Multiple resources of the same TYPE should have a minimalistic identifier to differentiate between the two resources.
  • 49.
    When to Usean Underscore resource "aws_route53_record" "api_route53_record" { zone_id = "${aws_route53_zone.route53_zone.zone_id}" name = "${var.environment}-api.${var.domain_name}" type = "A" ttl = "300" records = ["${var.ip_address}"] } ◉ Variable Names ◉ Resource Names ◉ Anything Interpolated
  • 50.
    When to Usea Hyphen resource "aws_route53_record" "api_route53_record" { zone_id = "${aws_route53_zone.route53_zone.zone_id}" name = "${var.environment}-api.${var.domain_name}" type = "A" ttl = "300" records = ["${var.ip_address}"] } ◉ Resources Being Created
  • 51.
  • 52.
  • 54.
    A Tale ofTwo Modules
  • 55.
    Service Module ● Reusablelibrary ● Creates all required resources a service needs to be operational i.e. EC2 instances, S3 bucket, DNS Entries A Tale of Two Modules Infrastructure Module ● Single repository comprised of multiple root modules ● This is where Service Modules are instantiated/Terraform is run.
  • 56.
    Why Infrastructure Modules? ◉Conditional Statements ○ Don’t exist Are painful (1604) ◉ Segment State ○ Reduce risk of changes ○ Flexible state reference ◉ DRY Terraform Configurations ○ Instantiate reusable modules ◉ Environments Aren’t Recommended (0.9) ◉ Workspaces are a thing (renamed statefile)
  • 57.
    The Candle Problem Source:http://whatismotivation.weebly.com/uploads/2/9/9/1/29913749/1713129_orig.png
  • 58.
    Overcome Functional Fixedness Source:http://whatismotivation.weebly.com/uploads/2/9/9/1/29913749/1713129_orig.png
  • 59.
  • 60.
    Configuration by Convention InfrastructureRepository Folder Structure AWS (provider) |__ production-account (aws-account root module) |__ us-east-1 (aws-region root module) |__ production-us-east-1-vpc (vpc root module) |__ production (environment root module) |__ inf-bastion (service root module)
  • 61.
    Account Root Module InfrastructureRepository Folder Structure AWS (provider) |__ production-account (aws-account root module) <~~~~~ YOU ARE HERE |__ us-east-1 (aws-region root module) |__ production-us-east-1-vpc (vpc root module) |__ production (environment root module) |__ inf-bastion (service root module)
  • 62.
    terraform.sh (bash wrapper) Usage:./templates/account-terraform.sh [apply|destroy|plan|refresh|show] The following arguments are supported: apply Refresh the Terraform remote state, "terraform get -update", and "terraform apply" destroy Refresh the Terraform remote state and destroy the Terraform stack plan Refresh the Terraform remote state, "terraform get -update", and "terraform plan" refresh Refresh the Terraform remote state show Refresh and show the Terraform remote state
  • 63.
    Refresh Function ... refresh() { root=$(pwd| awk -F "/" '{print $(NF-1)}') aws_account=$(pwd | awk -F "/" '{print $NF}') export TF_VAR_root="$root" export TF_VAR_aws_account="$aws_account" export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache" echo -e "nn***** Refreshing State and Upgrading Modules *****" echo "no" | terraform init -get=true -upgrade -input=false -backend=true -backend-config "bucket=${aws_account}-terraform-state" -backend-config "key=${root}/terraform.tfstate" -backend-config "profile=${aws_account}" -backend-config "region=us-east-1" } ...
  • 64.
    Refresh Function ... refresh() { root=$(pwd| awk -F "/" '{print $(NF-1)}') aws_account=$(pwd | awk -F "/" '{print $NF}') export TF_VAR_root="$root" export TF_VAR_aws_account="$aws_account" export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache" echo -e "nn***** Refreshing State and Upgrading Modules *****" echo "no" | terraform init -get=true -upgrade -input=false -backend=true -backend-config "bucket=${aws_account}-terraform-state" -backend-config "key=${root}/terraform.tfstate" -backend-config "profile=${aws_account}" -backend-config "region=us-east-1" } ...
  • 65.
    Refresh Function ... refresh() { root=$(pwd| awk -F "/" '{print $(NF-1)}') aws_account=$(pwd | awk -F "/" '{print $NF}') export TF_VAR_root="$root" export TF_VAR_aws_account="$aws_account" export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache" echo -e "nn***** Refreshing State and Upgrading Modules *****" echo "no" | terraform init -get=true -upgrade -input=false -backend=true -backend-config "bucket=${aws_account}-terraform-state" -backend-config "key=${root}/terraform.tfstate" -backend-config "profile=${aws_account}" -backend-config "region=us-east-1" } ...
  • 66.
    ... createBackendConfig() { /bin/cat >backend.tf <<EOL terraform { backend "s3" {} } EOL } ... Create Backend Config backend.tf terraform { backend "s3" {} }
  • 67.
  • 68.
    AWS-Region Root Module InfrastructureRepository Folder Structure AWS (provider) |__ production-account (aws-account root module) |__ us-east-1 (aws-region root module) <~~~~~ PLACEHOLDER |__ production-us-east-1-vpc (vpc root module) |__ production (environment root module) |__ inf-bastion (service root module)
  • 69.
    VPC Root Module InfrastructureRepository Folder Structure AWS (provider) |__ production-account (aws-account root module) |__ us-east-1 (aws-region root module) |__ production-us-east-1-vpc (vpc root module) <~~~~~ YOU ARE HERE |__ production (environment root module) |__ inf-bastion (service root module)
  • 70.
  • 71.
    main.tf module "vpc" { source= "git@github.com:TerraformDesignPattern/vpc.git?ref=1.2.3" availability_zones = "${var.availability_zones}" aws_region = "${var.aws_region}" private_subnets = "${var.private_subnets}" public_subnets = "${var.public_subnets}" vpc_cidr = "${var.vpc_cidr}" vpc_name = "${var.vpc_name}" } VPC Root Module (cont.)
  • 72.
    main.tf module "vpc" { source= "git@github.com:TerraformDesignPattern/vpc.git?ref=1.2.3" availability_zones = "${var.availability_zones}" aws_region = "${var.aws_region}" private_subnets = "${var.private_subnets}" public_subnets = "${var.public_subnets}" vpc_cidr = "${var.vpc_cidr}" vpc_name = "${var.vpc_name}" } VPC Root Module (cont.)
  • 73.
    main.tf module "vpc" { source= "git@github.com:TerraformDesignPattern/vpc.git?ref=${var.branch}" availability_zones = "${var.availability_zones}" aws_region = "${var.aws_region}" private_subnets = "${var.private_subnets}" public_subnets = "${var.public_subnets}" vpc_cidr = "${var.vpc_cidr}" vpc_name = "${var.vpc_name}" } VPC Root Module (cont.)
  • 74.
  • 75.
    main.tf module "vpc" { source= "git@github.com:TerraformDesignPattern/vpc.git?ref=${var.branch}" availability_zones = "${var.availability_zones}" aws_region = "${var.aws_region}" private_subnets = "${var.private_subnets}" public_subnets = "${var.public_subnets}" vpc_cidr = "${var.vpc_cidr}" vpc_name = "${var.vpc_name}" } VPC Root Module (cont.) No interpolation allowed! Issue #1439
  • 77.
    main.tf module "vpc" { source= "git@github.com:TerraformDesignPattern/vpc.git?ref=${var.branch}" availability_zones = "${var.availability_zones}" aws_region = "${var.aws_region}" private_subnets = "${var.private_subnets}" public_subnets = "${var.public_subnets}" vpc_cidr = "${var.vpc_cidr}" vpc_name = "${var.vpc_name}" } VPC Root Module (cont.) No interpolation allowed! Issue #1439
  • 78.
  • 80.
  • 81.
  • 82.
    main.tf module "vpc" { source= "git@github.com:TerraformDesignPattern/vpc.git?ref=1.2.3" availability_zones = "${var.availability_zones}" aws_region = "${var.aws_region}" private_subnets = "${var.private_subnets}" public_subnets = "${var.public_subnets}" vpc_cidr = "${var.vpc_cidr}" vpc_name = "${var.vpc_name}" } VPC Root Module (cont.)
  • 83.
  • 84.
    variables.tf ... variable "public_subnets" { default= [ "172.19.101.0/24", "172.19.102.0/24", "172.19.103.0/24", ] } ... VPC Root Module (cont.)
  • 85.
  • 86.
    outputs.tf ... output "public_subnet_ids" { value= ["${aws_subnet.public_subnet.*.id}"] } ... VPC Service Module Outputs
  • 87.
    outputs.tf ... output "public_subnet_ids" { value= ["${module.vpc.public_subnet_ids}"] } ... VPC Root Module Outputs
  • 89.
    To Begin Again...From The Beginning Quote: Waking Life. Dir. Richard Linklater. Fox Searchlight Pictures, 2001. FIlm. Image: Fight Club. Dir. David Fincher. 20th Century Fox, 1999. Film.
  • 90.
    s3.tf resource "aws_s3_bucket_object" "outputs_object"{ bucket = "${var.aws_account}-terraform-state" key = "aws/${var.aws_region}/${var.vpc_name}/dummy_object_outputs" source = "outputs.tf" etag = "${md5(file("outputs.tf"))}" } resource "aws_s3_bucket_object" "variables_object" { bucket = "${var.aws_account}-terraform-state" key = "aws/${var.aws_region}/${var.vpc_name}/dummy_object_variables" source = "variables.tf" etag = "${md5(file("variables.tf"))}" } Root Module State Seeding outputs.tf output "public_subnet_ids" { value = ["${var.public_subnets}"] } ... variables.tf variable "public_subnets" { default = [ "172.19.101.0/24", "172.19.102.0/24", "172.19.103.0/24", ] } ...
  • 91.
    Environment Root Module InfrastructureRepository Folder Structure AWS (provider) |__ production-account (aws-account root module) |__ us-east-1 (aws-region root module) |__ production-us-east-1-vpc (vpc root module) |__ production (environment root module) <~~~~~ PLACEHOLDER |__ inf-bastion (service root module)
  • 92.
  • 93.
    Service Root Module InfrastructureRepository Folder Structure AWS (provider) |__ production-account (aws-account root module) |__ us-east-1 (aws-region root module) |__ production-us-east-1-vpc (vpc root module) |__ production (environment root module) |__ inf-bastion (service root module) <~~~~~ YOU ARE HERE
  • 94.
    SSH Bastion RootModule backend.tf main.tf outputs.tf terraform.sh variables.tf SSH Bastion Root Module
  • 95.
    variables.tf variable "aws_account" {} variable"aws_region" {} variable "aws_environment_name" {} variable "service_name" {} variable "vpc_name" {} SSH Bastion Root Module (cont.)
  • 96.
    refresh() { root=$(pwd |awk -F "/" '{print $(NF-5)}') aws_account=$(pwd | awk -F "/" '{print $(NF-4)}') aws_region=$(pwd | awk -F "/" '{print $(NF-3)}') vpc_name=$(pwd | awk -F "/" '{print $(NF-2)}') environment_name=$(pwd | awk -F "/" '{print $(NF-1)}') service_name=$(pwd | awk -F "/" '{print $NF}') bucket_key="${root}/${aws_region}/${vpc_name}/${environment_name}/${service_name}" export TF_VAR_root="$root" export TF_VAR_aws_account="$aws_account" export TF_VAR_aws_region="$aws_region" export TF_VAR_vpc_name="$vpc_name" export TF_VAR_environment_name="$environment_name" export TF_VAR_service_name="$service_name" export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache" echo -e "nn***** Refreshing State and Upgrading Modules *****" echo "no" | terraform init -get=true -upgrade -input=false -backend=true -backend-config "bucket=${aws_account}-terraform-state" -backend-config "key=${bucket_key}/terraform.tfstate" -backend-config "profile=${aws_account}" -backend-config "region=us-east-1" } Refresh Function
  • 97.
    refresh() { root=$(pwd |awk -F "/" '{print $(NF-5)}') aws_account=$(pwd | awk -F "/" '{print $(NF-4)}') aws_region=$(pwd | awk -F "/" '{print $(NF-3)}') vpc_name=$(pwd | awk -F "/" '{print $(NF-2)}') environment_name=$(pwd | awk -F "/" '{print $(NF-1)}') service_name=$(pwd | awk -F "/" '{print $NF}') bucket_key="${root}/${aws_region}/${vpc_name}/${environment_name}/${service_name}" export TF_VAR_root="$root" export TF_VAR_aws_account="$aws_account" export TF_VAR_aws_region="$aws_region" export TF_VAR_vpc_name="$vpc_name" export TF_VAR_environment_name="$environment_name" export TF_VAR_service_name="$service_name" export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache" echo -e "nn***** Refreshing State and Upgrading Modules *****" echo "no" | terraform init -get=true -upgrade -input=false -backend=true -backend-config "bucket=${aws_account}-terraform-state" -backend-config "key=${bucket_key}/terraform.tfstate" -backend-config "profile=${aws_account}" -backend-config "region=us-east-1" } Refresh Function
  • 98.
    refresh() { root=$(pwd |awk -F "/" '{print $(NF-5)}') aws_account=$(pwd | awk -F "/" '{print $(NF-4)}') aws_region=$(pwd | awk -F "/" '{print $(NF-3)}') vpc_name=$(pwd | awk -F "/" '{print $(NF-2)}') environment_name=$(pwd | awk -F "/" '{print $(NF-1)}') service_name=$(pwd | awk -F "/" '{print $NF}') bucket_key="${root}/${aws_region}/${vpc_name}/${environment_name}/${service_name}" export TF_VAR_root="$root" export TF_VAR_aws_account="$aws_account" export TF_VAR_aws_region="$aws_region" export TF_VAR_vpc_name="$vpc_name" export TF_VAR_environment_name="$environment_name" export TF_VAR_service_name="$service_name" export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache" echo -e "nn***** Refreshing State and Upgrading Modules *****" echo "no" | terraform init -get=true -upgrade -input=false -backend=true -backend-config "bucket=${aws_account}-terraform-state" -backend-config "key=${bucket_key}/terraform.tfstate" -backend-config "profile=${aws_account}" -backend-config "region=us-east-1" } Refresh Function
  • 99.
    refresh() { root=$(pwd |awk -F "/" '{print $(NF-5)}') aws_account=$(pwd | awk -F "/" '{print $(NF-4)}') aws_region=$(pwd | awk -F "/" '{print $(NF-3)}') vpc_name=$(pwd | awk -F "/" '{print $(NF-2)}') environment_name=$(pwd | awk -F "/" '{print $(NF-1)}') service_name=$(pwd | awk -F "/" '{print $NF}') bucket_key="${root}/${aws_region}/${vpc_name}/${environment_name}/${service_name}" export TF_VAR_root="$root" export TF_VAR_aws_account="$aws_account" export TF_VAR_aws_region="$aws_region" export TF_VAR_vpc_name="$vpc_name" export TF_VAR_environment_name="$environment_name" export TF_VAR_service_name="$service_name" export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache" echo -e "nn***** Refreshing State and Upgrading Modules *****" echo "no" | terraform init -get=true -upgrade -input=false -backend=true -backend-config "bucket=${aws_account}-terraform-state" -backend-config "key=${bucket_key}/terraform.tfstate" -backend-config "profile=${aws_account}" -backend-config "region=us-east-1" } Refresh Function
  • 100.
    refresh() { root=$(pwd |awk -F "/" '{print $(NF-5)}') aws_account=$(pwd | awk -F "/" '{print $(NF-4)}') aws_region=$(pwd | awk -F "/" '{print $(NF-3)}') vpc_name=$(pwd | awk -F "/" '{print $(NF-2)}') environment_name=$(pwd | awk -F "/" '{print $(NF-1)}') service_name=$(pwd | awk -F "/" '{print $NF}') bucket_key="${root}/${aws_region}/${vpc_name}/${environment_name}/${service_name}" export TF_VAR_root="$root" export TF_VAR_aws_account="$aws_account" export TF_VAR_aws_region="$aws_region" export TF_VAR_vpc_name="$vpc_name" export TF_VAR_environment_name="$environment_name" export TF_VAR_service_name="$service_name" export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache" echo -e "nn***** Refreshing State and Upgrading Modules *****" echo "no" | terraform init -get=true -upgrade -input=false -backend=true -backend-config "bucket=${aws_account}-terraform-state" -backend-config "key=${bucket_key}/terraform.tfstate" -backend-config "profile=${aws_account}" -backend-config "region=us-east-1" } Refresh Function
  • 101.
    refresh() { root=$(pwd |awk -F "/" '{print $(NF-5)}') aws_account=$(pwd | awk -F "/" '{print $(NF-4)}') aws_region=$(pwd | awk -F "/" '{print $(NF-3)}') vpc_name=$(pwd | awk -F "/" '{print $(NF-2)}') environment_name=$(pwd | awk -F "/" '{print $(NF-1)}') service_name=$(pwd | awk -F "/" '{print $NF}') bucket_key="${root}/${aws_region}/${vpc_name}/${environment_name}/${service_name}" export TF_VAR_root="$root" export TF_VAR_aws_account="$aws_account" export TF_VAR_aws_region="$aws_region" export TF_VAR_vpc_name="$vpc_name" export TF_VAR_environment_name="$environment_name" export TF_VAR_service_name="$service_name" export TF_PLUGIN_CACHE_DIR="~/.terraform.d/plugin-cache" echo -e "nn***** Refreshing State and Upgrading Modules *****" echo "no" | terraform init -get=true -upgrade -input=false -backend=true -backend-config "bucket=${aws_account}-terraform-state" -backend-config "key=${bucket_key}/terraform.tfstate" -backend-config "profile=${aws_account}" -backend-config "region=us-east-1" } Refresh Function
  • 102.
    module “bastion" { source= "git@github.com:TerraformDesignPattern/bastionhost.git?ref=0.1.0" aws_account = "${var.aws_account}" aws_region = "${var.aws_region}" environment_name = "${var.environment_name}" vpc_name = "${var.vpc_name}" service_name = "${element(split("-", var.service_name), 0)}" tier_name = "${element(split("-", var.service_name), 1)}" } SSH Bastion Root Module (cont.)
  • 103.
    SSH Bastion ModuleRepository data.tf ec2.tf outputs.tf providers.tf route53.tf security_groups.tf variables.tf SSH Bastion Service Module
  • 104.
    data.tf data "terraform_remote_state" "account"{ backend = "s3" config { bucket = "${var.aws_account}-terraform-state" key = "aws/terraform.tfstate" region = "us-east-1" } } data "terraform_remote_state" "vpc" { backend = "s3" config { bucket = "${var.aws_account}-terraform-state" key = "aws/${var.aws_region}/${var.vpc_name}/terraform.tfstate" region = "us-east-1" } }
  • 105.
    route53.tf resource "aws_route53_record" "route53_record"{ zone_id = "${data.terraform_remote_state.account.zone_id}" name = "${local.hostname}.${data.terraform_remote_state.account.domain_name}" type = "A" ttl = "300" records = ["${aws_instance.instance.public_ip}"] } ... security_groups.tf resource "aws_security_group" "elb_security_group" { name = "${local.resource_name}-elb-sg-${data.terraform_remote_state.vpc.aws_region_shortname}" vpc_id = "${data.terraform_remote_state.vpc.vpc_id}" ...
  • 106.
    route53.tf resource "aws_route53_record" "route53_record"{ zone_id = "${data.terraform_remote_state.account.zone_id}" name = "${local.hostname}.${data.terraform_remote_state.account.domain_name}" type = "A" ttl = "300" records = ["${aws_instance.instance.public_ip}"] } ... security_groups.tf resource "aws_security_group" "elb_security_group" { name = "${local.resource_name}-elb-sg-${data.terraform_remote_state.vpc.aws_region_shortname}" vpc_id = "${data.terraform_remote_state.vpc.vpc_id}" ...
  • 107.
    variables.tf locals { hostname ="${var.environment_name}-${var.service_name}-${var.tier}-${data.terraform_remote_state.vpc.aws_region_shortname}" resource_name = "${var.environment_name}-${var.service_name}-${var.tier}" } ... Module “Local” Variables
  • 108.
    Manage State &Variables Dynamically Infrastructure Repository Folder Structure AWS (provider) |__ production-account (root aws-account module) |__ us-east-1 (root aws-region module) |__ production-us-east-1-vpc (root vpc module) |__ production (root environment module) |__ bastion (root service module)
  • 109.
    ◉ Introduction ◉ Terraformat Scale? ◉ Gotta Keep ‘em Separated ◉ Reducing Complexity With Style ◉ A Tale of Two Modules Closing: Agenda and Takeaways
  • 111.
    Contact: ◉ Twitter: @jonbrouse ◉Github: github.com/jonbrouse ◉ Site: jonbrouse.com ◉ github.com/TerraformDesignPattern Thank you! Jon Brouse