├── web └── index.html ├── .gitignore ├── terraform ├── example2 │ ├── terraform.tfvars │ └── main.tf ├── example1 │ └── main.tf └── example3 │ └── main.tf ├── terraform_circleci_pipeline.png ├── .pre-commit-config.yaml ├── .editorconfig ├── .github └── main.workflow ├── packer ├── app.json └── app-amazon-chroot.json ├── README.md └── .circleci └── config.yml /web/index.html: -------------------------------------------------------------------------------- 1 | Hello FullStackFest!

2 | 3 | ${BUILD_DETAILS} 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Terraform files 2 | .terraform 3 | terraform.tfstate 4 | *.tfstate* 5 | 6 | vendor -------------------------------------------------------------------------------- /terraform/example2/terraform.tfvars: -------------------------------------------------------------------------------- 1 | vpc_id = "vpc-9651acf1" 2 | subnet_id = "subnet-6fe3d837" 3 | instance_type = "t2.nano" 4 | -------------------------------------------------------------------------------- /terraform_circleci_pipeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonbabenko/terraform-deployment-pipeline-talk/HEAD/terraform_circleci_pipeline.png -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: git://github.com/antonbabenko/pre-commit-terraform 3 | rev: v1.7.3 4 | hooks: 5 | - id: terraform_fmt 6 | - repo: git://github.com/pre-commit/pre-commit-hooks 7 | rev: v1.3.0 8 | hooks: 9 | - id: check-merge-conflict 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | # Uses editorconfig to maintain consistent coding styles 3 | 4 | # top-most EditorConfig file 5 | root = true 6 | 7 | # Unix-style newlines with a newline ending every file 8 | [*] 9 | charset = utf-8 10 | end_of_line = lf 11 | indent_size = 2 12 | indent_style = space 13 | insert_final_newline = true 14 | max_line_length = 80 15 | trim_trailing_whitespace = true 16 | 17 | [*.{tf,tfvars}] 18 | indent_size = 2 19 | indent_style = space 20 | 21 | [*.md] 22 | max_line_length = 0 23 | trim_trailing_whitespace = false 24 | 25 | [Makefile] 26 | tab_width = 2 27 | indent_style = tab 28 | 29 | [COMMIT_EDITMSG] 30 | max_line_length = 0 -------------------------------------------------------------------------------- /.github/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "Terraform 2" { 2 | resolves = "terraform-plan" 3 | on = "push" 4 | } 5 | 6 | action "terraform-fmt" { 7 | uses = "hashicorp/terraform-github-actions/fmt@v0.1.1" 8 | secrets = ["GITHUB_TOKEN"] 9 | env = { 10 | TF_ACTION_WORKING_DIR = "./terraform/example1" 11 | } 12 | } 13 | 14 | action "terraform-init" { 15 | uses = "hashicorp/terraform-github-actions/init@v0.1.1" 16 | needs = "terraform-fmt" 17 | secrets = ["GITHUB_TOKEN"] 18 | env = { 19 | TF_ACTION_WORKING_DIR = "./terraform/example1" 20 | } 21 | } 22 | 23 | action "terraform-validate" { 24 | uses = "hashicorp/terraform-github-actions/validate@v0.1.1" 25 | needs = "terraform-init" 26 | secrets = ["GITHUB_TOKEN"] 27 | env = { 28 | TF_ACTION_WORKING_DIR = "./terraform/example1" 29 | } 30 | } 31 | 32 | action "terraform-plan" { 33 | uses = "hashicorp/terraform-github-actions/plan@v0.1.1" 34 | needs = "terraform-validate" 35 | secrets = ["GITHUB_TOKEN"] 36 | env = { 37 | TF_ACTION_WORKING_DIR = "./terraform/example1" 38 | 39 | # If you're using Terraform workspaces, set this to the workspace name. 40 | # TF_ACTION_WORKSPACE = "default" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /terraform/example1/main.tf: -------------------------------------------------------------------------------- 1 | /* 2 | terraform { 3 | backend "s3" { 4 | bucket = "my-tf-states-anton-demo" 5 | key = "terraform-delivery-pipeline-example1" 6 | region = "eu-west-1" 7 | encrypt = true 8 | } 9 | } 10 | */ 11 | 12 | provider "aws" { 13 | region = "eu-west-1" 14 | } 15 | 16 | resource "random_pet" "bucket" {} 17 | 18 | resource "aws_s3_bucket" "app" { 19 | bucket = "fullstackfest-${random_pet.bucket.id}" 20 | acl = "public-read" 21 | 22 | website { 23 | index_document = "index.html" 24 | } 25 | } 26 | 27 | data "template_file" "index" { 28 | template = "${file("../../web/index.html")}" 29 | 30 | vars { 31 | BUILD_DETAILS = "I was deployed from example1 to ${aws_s3_bucket.app.website_endpoint}" 32 | } 33 | } 34 | 35 | resource "aws_s3_bucket_object" "object" { 36 | bucket = "${aws_s3_bucket.app.id}" 37 | key = "index.html" 38 | content = "${data.template_file.index.rendered}" 39 | etag = "${md5(data.template_file.index.rendered)}" 40 | content_type = "text/html" 41 | acl = "public-read" 42 | } 43 | 44 | output "app_website_endpoint" { 45 | value = "${aws_s3_bucket.app.website_endpoint}" 46 | } 47 | -------------------------------------------------------------------------------- /packer/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "variables": { 3 | "aws_region": "eu-west-1" 4 | }, 5 | "builders": [ 6 | { 7 | "ami_name": "fullstackfest-demo-{{uuid | clean_ami_name}}", 8 | "ami_description": "FullStackFest demo AMI based on Amazon Linux", 9 | "instance_type": "t2.large", 10 | "region": "{{user `aws_region`}}", 11 | "type": "amazon-ebs", 12 | "ssh_username": "ec2-user", 13 | "source_ami_filter": { 14 | "filters": { 15 | "virtualization-type": "hvm", 16 | "name": "amzn-ami-hvm-*-x86_64-gp2", 17 | "root-device-type": "ebs" 18 | }, 19 | "owners": [ 20 | "137112412989" 21 | ], 22 | "most_recent": true 23 | } 24 | } 25 | ], 26 | "provisioners": [ 27 | { 28 | "type": "shell", 29 | "inline": [ 30 | "sudo yum update -y && sudo yum install -y nginx" 31 | ] 32 | }, 33 | { 34 | "type": "file", 35 | "source": "../web", 36 | "destination": "/tmp" 37 | }, 38 | { 39 | "type": "shell", 40 | "inline": [ 41 | "sudo /bin/cp -Rf /tmp/web/* /usr/share/nginx/html", 42 | "sudo chkconfig nginx on" 43 | ] 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-delivery-pipeline-talk 2 | 3 | This repository contains code for the "Terraform in delivery pipeline" talk by [Anton Babenko](https://github.com/antonbabenko). Usually slides from my tech talks are hosted [here](https://www.slideshare.net/AntonBabenko). 4 | 5 | There are several independent Terraform configurations in `terraform` directory. 6 | 7 | ## terraform/example1 8 | 9 | Create S3 bucket with single object placed there. There is no automation, no support for multiple environments. This code can be executed using `terraform init && terraform plan && terraform apply`. 10 | 11 | ## terraform/example2 12 | 13 | [Terraform community module](https://github.com/terraform-community-modules) is used to create security group, AWS AMI data source is used to find AMI produced 14 | by configuration at `packer/app.json`. 15 | 16 | ## terraform/example3 17 | 18 | [Terraform AWS modules](https://github.com/terraform-aws-modules) are used to create security group and launch an EC2 instance. CircleCI workflow configuration is at `.circleci/config.yml`. 19 | 20 | ## Complete CircleCI workflow 21 | 22 | Terraform in your delivery pipeline - CircleCI workflow 23 | -------------------------------------------------------------------------------- /packer/app-amazon-chroot.json: -------------------------------------------------------------------------------- 1 | { 2 | "builders": [ 3 | { 4 | "type": "amazon-chroot", 5 | "region": "eu-west-1", 6 | "source_ami_filter": { 7 | "filters": { 8 | "virtualization-type": "hvm", 9 | "name": "amzn-ami-hvm-*-x86_64-gp2", 10 | "root-device-type": "ebs" 11 | }, 12 | "owners": [ 13 | "137112412989" 14 | ], 15 | "most_recent": true 16 | }, 17 | "ami_name": "app-amazon-chroot-{{isotime | clean_ami_name}}" 18 | } 19 | ], 20 | "provisioners": [ 21 | { 22 | "type": "shell", 23 | "inline": [ 24 | "echo '#!/bin/sh' > /usr/sbin/policy-rc.d", 25 | "echo 'exit 101' >> /usr/sbin/policy-rc.d", 26 | "chmod a+x /usr/sbin/policy-rc.d" 27 | ] 28 | }, 29 | { 30 | "type": "shell", 31 | "inline": [ 32 | "mkdir /run/shm", 33 | "echo 'tmpfs /dev/shm tmpfs defaults 0 0' >> /etc/fstab", 34 | "mount -a" 35 | ] 36 | }, 37 | { 38 | "type": "shell", 39 | "inline": [ 40 | "sudo yum install -y nginx" 41 | ] 42 | }, 43 | { 44 | "type": "file", 45 | "source": "../web", 46 | "destination": "/tmp" 47 | }, 48 | { 49 | "type": "shell", 50 | "inline": [ 51 | "sudo /bin/cp -Rf /tmp/web/* /usr/share/nginx/html", 52 | "sudo chkconfig nginx on" 53 | ] 54 | }, 55 | { 56 | "type": "shell", 57 | "inline": [ 58 | "umount /dev/shm", 59 | "rm -f /usr/sbin/policy-rc.d" 60 | ] 61 | } 62 | ] 63 | } 64 | -------------------------------------------------------------------------------- /terraform/example2/main.tf: -------------------------------------------------------------------------------- 1 | ######################### 2 | # Terraform configuration 3 | ######################### 4 | terraform { 5 | backend "s3" { 6 | bucket = "my-tf-states-anton-demo" 7 | key = "terraform-delivery-pipeline-example2" 8 | region = "eu-west-1" 9 | encrypt = true 10 | } 11 | } 12 | 13 | ########### 14 | # Providers 15 | ########### 16 | provider "aws" { 17 | region = "eu-west-1" 18 | } 19 | 20 | ########### 21 | # Variables 22 | ########### 23 | variable "vpc_id" { 24 | description = "ID of VPC where resources will be created" 25 | } 26 | 27 | variable "subnet_id" { 28 | description = "ID of subnet where resources will be created" 29 | } 30 | 31 | variable "instance_type" { 32 | description = "Type of EC2 instance to launch" 33 | } 34 | 35 | ############## 36 | # Data sources 37 | ############## 38 | data "aws_ami" "amazon_linux" { 39 | most_recent = true 40 | 41 | filter { 42 | name = "name" 43 | 44 | values = [ 45 | "amzn-ami-hvm-*-x86_64-gp2", 46 | ] 47 | } 48 | 49 | filter { 50 | name = "owner-alias" 51 | 52 | values = [ 53 | "amazon", 54 | ] 55 | } 56 | } 57 | 58 | ######### 59 | # Modules 60 | ######### 61 | module "sg_web" { 62 | source = "terraform-aws-modules/security-group/aws" 63 | 64 | name = "my-app" 65 | vpc_id = "${var.vpc_id}" 66 | } 67 | 68 | ########### 69 | # Resources 70 | ########### 71 | resource "aws_instance" "app" { 72 | ami = "${data.aws_ami.amazon_linux.id}" 73 | instance_type = "${var.instance_type}" 74 | subnet_id = "${var.subnet_id}" 75 | vpc_security_group_ids = ["${module.sg_web.this_security_group_id}"] 76 | } 77 | 78 | ######### 79 | # Outputs 80 | ######### 81 | output "app_public_ip" { 82 | description = "Public IP of EC2 instance running an application" 83 | value = "${aws_instance.app.public_ip}" 84 | } 85 | -------------------------------------------------------------------------------- /terraform/example3/main.tf: -------------------------------------------------------------------------------- 1 | # "admin" role requires MFA, so it can be assumed like this: 2 | # aws sts assume-role --role-arn arn:aws:iam::835367859851:role/demo-terraform-admin --role-session-name "assumed-role-admin" --serial-number arn:aws:iam::835367859851:mfa/anton --token-code 123456 3 | # Retrieved credentials can be placed should be stored as AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN 4 | 5 | ######################### 6 | # Terraform configuration 7 | ######################### 8 | terraform { 9 | required_version = ">= 0.11" 10 | 11 | backend "s3" { 12 | bucket = "my-tf-states-anton-demo" 13 | key = "terraform-delivery-pipeline-example3" 14 | region = "eu-west-1" 15 | encrypt = true 16 | } 17 | } 18 | 19 | ########### 20 | # Providers 21 | ########### 22 | provider "aws" { 23 | version = ">= 1.12.0" 24 | region = "eu-west-1" 25 | } 26 | 27 | ########### 28 | # Variables 29 | ########### 30 | variable "key_name" { 31 | description = "Name of EC2 keypair to use" 32 | default = "" 33 | } 34 | 35 | ############## 36 | # Data sources 37 | ############## 38 | data "aws_vpc" "default" { 39 | default = true 40 | } 41 | 42 | data "aws_subnet_ids" "all" { 43 | vpc_id = "${data.aws_vpc.default.id}" 44 | } 45 | 46 | data "aws_ami" "amazon_linux" { 47 | most_recent = true 48 | 49 | filter { 50 | name = "name" 51 | 52 | values = [ 53 | "amzn-ami-hvm-*-x86_64-gp2", 54 | ] 55 | } 56 | 57 | filter { 58 | name = "owner-alias" 59 | 60 | values = [ 61 | "amazon", 62 | ] 63 | } 64 | } 65 | 66 | data "template_file" "ec2_userdata" { 67 | template = <> ~/.ssh/known_hosts 28 | - run: 29 | name: Set terraform plugins directory 30 | command: echo -e "plugin_cache_dir = \"$HOME/.terraform.d/plugin-cache\"\ndisable_checkpoint = true" > ~/.terraformrc 31 | - run: 32 | name: terraform init 33 | command: terraform init -input=false 34 | - run: 35 | name: Validate Terraform configurations 36 | command: find * -type f -name "*.tf" ! -path "*/.terraform" ! -path "*/.terraform/*" ! -path "modules" ! -path "modules/*" -exec dirname {} \;|sort -u | while read m; do (terraform validate -check-variables=false "$m" && echo "√ $m") || exit 1 ; done 37 | - run: 38 | name: Check if Terraform configurations are properly formatted 39 | command: if [[ -n "$(terraform fmt -write=false)" ]]; then echo "Some terraform files need be formatted, run 'terraform fmt' to fix"; exit 1; fi 40 | 41 | plan_infrastructure: 42 | <<: *terraform 43 | steps: 44 | - attach_workspace: 45 | at: /tmp/workspace 46 | - run: 47 | name: Add github.com to ~/.ssh/known_hosts 48 | command: mkdir ~/.ssh && ssh-keyscan -t rsa github.com >> ~/.ssh/known_hosts 49 | - run: 50 | name: terraform init 51 | command: terraform init -input=false 52 | - run: 53 | name: terraform plan 54 | command: terraform plan -input=false -out=tfplan 55 | - persist_to_workspace: 56 | root: . 57 | paths: 58 | - .terraform # persist this to be able to apply execution plan without running init once more 59 | - tfplan 60 | 61 | apply_infrastructure: 62 | <<: *terraform 63 | working_directory: /tmp/workspace 64 | steps: 65 | - attach_workspace: 66 | at: /tmp/workspace 67 | - run: 68 | name: terraform apply 69 | command: terraform apply -input=false tfplan 70 | 71 | master_workflow_filters: &master_workflow_filters 72 | filters: 73 | branches: 74 | only: 75 | - master 76 | 77 | workflows: 78 | version: 2 79 | test-build-plan-apply: 80 | jobs: 81 | - checkout 82 | - validate_infrastructure: 83 | requires: 84 | - checkout 85 | - plan_infrastructure: 86 | requires: 87 | - validate_infrastructure 88 | - approve_infrastructure: 89 | <<: *master_workflow_filters 90 | type: approval 91 | requires: 92 | - plan_infrastructure 93 | - apply_infrastructure: 94 | <<: *master_workflow_filters 95 | requires: 96 | - approve_infrastructure 97 | --------------------------------------------------------------------------------