├── 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 |
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 |
--------------------------------------------------------------------------------