├── modules ├── components │ └── .gitkeep └── stacks │ └── app │ └── test-stack │ ├── main.tf │ ├── providers.tf │ ├── variables.tf │ └── outputs.tf ├── .terraform-version ├── .terragrunt-version ├── deployments ├── app │ ├── dev │ │ ├── config.yml │ │ └── test-stack │ │ │ ├── config.yml │ │ │ └── terragrunt.hcl │ ├── prod │ │ ├── config.yml │ │ └── test-stack │ │ │ ├── config.yml │ │ │ └── terragrunt.hcl │ ├── stage │ │ ├── config.yml │ │ └── test-stack │ │ │ ├── config.yml │ │ │ └── terragrunt.hcl │ └── config.yml ├── config.yml └── root.hcl ├── .pre-commit-config.yaml ├── LICENSE.md ├── .gitignore ├── CHANGELOG.md ├── README.md ├── Makefile └── init └── admin └── init-admin-account.cf.yml /modules/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.terraform-version: -------------------------------------------------------------------------------- 1 | 0.13.5 2 | -------------------------------------------------------------------------------- /.terragrunt-version: -------------------------------------------------------------------------------- 1 | 0.26.4 2 | -------------------------------------------------------------------------------- /modules/stacks/app/test-stack/main.tf: -------------------------------------------------------------------------------- 1 | resource "random_pet" "pet" { 2 | } 3 | -------------------------------------------------------------------------------- /deployments/app/dev/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env_var: "set in deployments/app/dev" 3 | -------------------------------------------------------------------------------- /deployments/app/prod/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env_var: "set in deployments/app/prod" 3 | -------------------------------------------------------------------------------- /deployments/app/stage/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | env_var: "set in deployments/app/stage" 3 | -------------------------------------------------------------------------------- /deployments/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | global_var: "set-in-deployments/" 3 | unused: true 4 | -------------------------------------------------------------------------------- /deployments/app/dev/test-stack/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | stack_var: "set in deployments/app/dev/test-stack/" 3 | -------------------------------------------------------------------------------- /deployments/app/prod/test-stack/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | stack_var: "set in deployments/app/prod/test-stack/" 3 | -------------------------------------------------------------------------------- /deployments/app/dev/test-stack/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | include { 2 | path = find_in_parent_folders("root.hcl") 3 | } 4 | -------------------------------------------------------------------------------- /deployments/app/stage/test-stack/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | stack_var: "set in deployments/app/stage/test-stack/" 3 | -------------------------------------------------------------------------------- /deployments/app/prod/test-stack/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | include { 2 | path = find_in_parent_folders("root.hcl") 3 | } 4 | -------------------------------------------------------------------------------- /deployments/app/stage/test-stack/terragrunt.hcl: -------------------------------------------------------------------------------- 1 | include { 2 | path = find_in_parent_folders("root.hcl") 3 | } 4 | -------------------------------------------------------------------------------- /deployments/app/config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | global_var: "overridden in deployments/app" 3 | tier_var: "set in deployments/app/" 4 | -------------------------------------------------------------------------------- /modules/stacks/app/test-stack/providers.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_providers { 3 | random = { 4 | source = "hashicorp/random" 5 | version = "~> 3.0.0" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /modules/stacks/app/test-stack/variables.tf: -------------------------------------------------------------------------------- 1 | variable "global_var" { 2 | default = "unset" 3 | } 4 | 5 | variable "tier_var" { 6 | default = "unset" 7 | } 8 | 9 | variable "env_var" { 10 | default = "unset" 11 | } 12 | 13 | variable "layer_var" { 14 | default = "unset" 15 | } 16 | 17 | variable "stack_var" { 18 | default = "unset" 19 | } 20 | -------------------------------------------------------------------------------- /modules/stacks/app/test-stack/outputs.tf: -------------------------------------------------------------------------------- 1 | output "pet" { 2 | value = random_pet.pet.id 3 | } 4 | 5 | output "global_var" { 6 | value = var.global_var 7 | } 8 | 9 | output "tier_var" { 10 | value = var.tier_var 11 | } 12 | 13 | output "env_var" { 14 | value = var.env_var 15 | } 16 | 17 | output "layer_var" { 18 | value = var.layer_var 19 | } 20 | 21 | output "stack_var" { 22 | value = var.stack_var 23 | } 24 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: git@github.com:pre-commit/pre-commit-hooks 3 | rev: v3.3.0 4 | hooks: 5 | - id: mixed-line-ending 6 | args: [ --fix=lf ] 7 | - id: trailing-whitespace 8 | args: [ --markdown-linebreak-ext=* ] 9 | - id: check-yaml 10 | args: [ --allow-multiple-documents ] 11 | exclude: .*\.cf\.(yml|yaml)$ 12 | - id: check-json 13 | - id: check-merge-conflict 14 | - id: detect-aws-credentials 15 | - id: double-quote-string-fixer 16 | - id: end-of-file-fixer 17 | 18 | - repo: git@github.com:antonbabenko/pre-commit-terraform 19 | rev: v1.45.0 20 | hooks: 21 | - id: terraform_fmt 22 | - id: terraform_validate 23 | 24 | - repo: git@github.com:gruntwork-io/pre-commit 25 | rev: v0.1.10 26 | hooks: 27 | - id: terragrunt-hclfmt 28 | 29 | - repo: https://github.com/aws-cloudformation/cfn-python-lint 30 | rev: v0.44.5 31 | hooks: 32 | - id: cfn-python-lint 33 | files: init/.*\.cf\.(yml|yaml)$ 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Chris Kent 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/terraform,terragrunt 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=terraform,terragrunt 4 | 5 | ### Terraform ### 6 | # Local .terraform directories 7 | **/.terraform/* 8 | 9 | # .tfstate files 10 | *.tfstate 11 | *.tfstate.* 12 | 13 | # Crash log files 14 | crash.log 15 | 16 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 17 | # .tfvars files are managed as part of configuration and so should be included in 18 | # version control. 19 | # 20 | # example.tfvars 21 | 22 | # Ignore override files as they are usually used to override resources locally and so 23 | # are not checked in 24 | override.tf 25 | override.tf.json 26 | *_override.tf 27 | *_override.tf.json 28 | 29 | # Include override files you do wish to add to version control using negated pattern 30 | # !example_override.tf 31 | 32 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 33 | # example: *tfplan* 34 | 35 | ### Terragrunt ### 36 | # terragrunt cache directories 37 | **/.terragrunt-cache/* 38 | 39 | # End of https://www.toptal.com/developers/gitignore/api/terraform,terragrunt 40 | -------------------------------------------------------------------------------- /deployments/root.hcl: -------------------------------------------------------------------------------- 1 | locals { 2 | root_deployments_dir = get_parent_terragrunt_dir() 3 | relative_deployment_path = path_relative_to_include() 4 | deployment_path_components = compact(split("/", local.relative_deployment_path)) 5 | 6 | tier = local.deployment_path_components[0] 7 | stack = reverse(local.deployment_path_components)[0] 8 | 9 | # Get a list of every path between root_deployments_directory and the path of 10 | # the deployment 11 | possible_config_dirs = [ 12 | for i in range(0, length(local.deployment_path_components) + 1) : 13 | join("/", concat( 14 | [local.root_deployments_dir], 15 | slice(local.deployment_path_components, 0, i) 16 | )) 17 | ] 18 | 19 | # Generate a list of possible config files at every possible_config_dir 20 | # (support both .yml and .yaml) 21 | possible_config_paths = flatten([ 22 | for dir in local.possible_config_dirs : [ 23 | "${dir}/config.yml", 24 | "${dir}/config.yaml" 25 | ] 26 | ]) 27 | 28 | # Load every YAML config file that exists into an HCL map 29 | file_configs = [ 30 | for path in local.possible_config_paths : 31 | yamldecode(file(path)) if fileexists(path) 32 | ] 33 | 34 | # Merge the maps together, with deeper configs overriding higher configs 35 | merged_config = merge(local.file_configs...) 36 | } 37 | 38 | # Pass the merged config to terraform as variable values using TF_VAR_ 39 | # environment variables 40 | inputs = local.merged_config 41 | 42 | remote_state { 43 | backend = "s3" 44 | generate = { 45 | path = "backend.tf" 46 | if_exists = "overwrite" 47 | } 48 | config = { 49 | bucket = "terraform-skeleton-state" 50 | region = "us-east-1" 51 | encrypt = true 52 | role_arn = "arn:aws:iam::${get_aws_account_id()}:role/terraform/TerraformBackend" 53 | 54 | key = "${dirname(local.relative_deployment_path)}/${local.stack}.tfstate" 55 | 56 | dynamodb_table = "terraform-skeleton-state-locks" 57 | accesslogging_bucket_name = "terraform-skeleton-state-logs" 58 | } 59 | } 60 | 61 | # Default the stack each deployment deploys based on its directory structure 62 | # Can be overridden by redefining this block in a child terragrunt.hcl 63 | terraform { 64 | source = "${local.root_deployments_dir}/../modules/stacks/${local.tier}/${local.stack}" 65 | } 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [v1.5.0] - 2021-02-25 10 | 11 | Companion blog post: 12 | 13 | https://thirstydeveloper.io/tf-skeleton/2021/02/25/part-6-protecting-state.html 14 | 15 | ### Added 16 | 17 | * Bucket policy restricting access to state bucket 18 | * Encrypted log bucket 19 | * Explicitly block public access to log bucket 20 | 21 | ### Removed 22 | 23 | * Backend role's ability to create state bucket, log bucket, and lock table since 24 | those are now managed by CloudFormation 25 | 26 | ## [v1.4.0] - 2021-02-17 27 | 28 | Companion blog post: 29 | 30 | https://thirstydeveloper.io/tf-skeleton/2021/02/17/part-5-cfn-terraform-state.html 31 | 32 | ### Added 33 | 34 | * Terraform operational infrastructure to CloudFormation (state bucket, log bucket, and lock table) 35 | * Makefile targets for importing terragrunt-created operational infrastructure to CloudFormation 36 | 37 | ## [v1.3.0] - 2021-02-10 38 | 39 | Companion blog post: 40 | 41 | https://thirstydeveloper.io/tf-skeleton/2021/02/10/part-4-backend-role.html 42 | 43 | ### Added 44 | 45 | * TerraformBackend IAM role, created by CloudFormation stack. Access restricted by IAM 46 | principal tag. 47 | * Use of TerraformBackend IAM role by S3 remote state backend 48 | * CloudFormation pre-commit hook (cfn-python-lint v0.44.5) 49 | * Makefile for deploying CloudFormation stack and initializing all deployments 50 | 51 | ### Changed 52 | 53 | * Excluded .cf.yml / .cf.yaml files from YAML pre-commit hook since it can't handle 54 | CloudFormation template interpolation 55 | 56 | ## [v1.2.0] - 2021-01-28 57 | 58 | Companion blog post: 59 | 60 | https://thirstydeveloper.io/tf-skeleton/2021/01/28/part-3-aws-backend.html 61 | 62 | ### Changed 63 | 64 | * Changed state storage backend from local to S3 65 | 66 | ## [v1.1.0] - 2020-12-06 67 | 68 | Companion blog post: 69 | 70 | https://thirstydeveloper.io/tf-skeleton/2021/01/23/part-2-variables.html 71 | 72 | ### Added 73 | 74 | * Loading variable values from config.yml files 75 | 76 | ## [v1.0.0] - 2020-11-24 77 | 78 | Companion blog post: 79 | 80 | https://thirstydeveloper.io/2021/01/17/part-1-organizing-terragrunt.html 81 | 82 | ### Added 83 | 84 | * .gitignore from [gitignore.io](https://www.toptal.com/developers/gitignore/api/terraform,terragrunt) 85 | * Set [tfenv](https://github.com/tfutils/tfenv) .terraform-version to 0.13.5 86 | * Set [tgenv](https://github.com/cunymatthieu/tgenv) .terragrunt-version to 0.26.4 87 | * Add initial [pre-commit](https://pre-commit.com/) hooks 88 | * Initial skeleton deployments 89 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # terraform-terragrunt-skeleton [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit&logoColor=white)](https://github.com/pre-commit/pre-commit) 2 | 3 | This repository implements a skeleton repository for teams to use when first 4 | getting started with [terraform](https://www.terraform.io/). It uses 5 | [terragrunt](https://terragrunt.gruntwork.io/) as a workflow tool. 6 | 7 | For a step-by-step guide for how this repo was built, the why behind it, and 8 | how to use it, see this blog series: 9 | 10 | https://thirstydeveloper.io/series/tf-skeleton 11 | 12 | ## Prerequisites 13 | 14 | You will need: 15 | 16 | 1. An AWS account for storing remote state in S3 17 | 1. An IAM user in that account with 18 | 1. Administrative access 19 | 1. An [IAM user tag](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_tags_users.html) 20 | of `Terraformer` set to `Admin` 21 | 1. Credentials for the above IAM user configured in the terminal used for running 22 | `terraform` and `terragrunt` commands 23 | 24 | If you prefer to work from a very basic version of this skeleton that instead 25 | uses the local filesystem backend, use branch 26 | [release/1.1](https://github.com/thirstydeveloper/terraform-terragrunt-skeleton/tree/release/1.1). 27 | 28 | This project uses: 29 | 30 | * [tfenv](https://github.com/tfutils/tfenv) for managing terraform versions 31 | * [tgenv](https://github.com/cunymatthieu/tgenv) for managing terragrunt versions 32 | * [pre-commit](https://pre-commit.com/) for running syntax, semantic, and style checks on `git commit` 33 | 34 | After installing those tools run `tfenv install` and `tgenv install` from the 35 | clone of this repository to install the configured versions of terraform and 36 | terragrunt. Then, run `pre-commit install` to install the pre-commit hooks. 37 | 38 | ## Initialization 39 | 40 | 1. Create an [AWS credentials profile](https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_credentials_profiles.html) 41 | named `tf-admin-account` 42 | 2. Run `make init-admin` to deploy a CloudFormation stack to that account containing 43 | [the infrastructure terraform needs to run](https://thirstydeveloper.io/tf-skeleton/2021/02/17/part-5-cfn-terraform-state.html) 44 | 45 | ## Usage 46 | 47 | Run `terragrunt` commands from directories under `deployments/` containing 48 | `terragrunt.hcl` files. 49 | 50 | terragrunt `*-all` commands can be run from the repository root, or the 51 | `deployments/` and any directory underneath it. For instance: 52 | 53 | * Run `terragrunt plan-all` from the repository root to generate terraform 54 | plans for all deployments. 55 | * Run `terragrunt plan-all` from `deployments/app/dev` to generate plans for 56 | all `app/dev` deployments. 57 | 58 | For additional guidance, see the companion blog series: 59 | 60 | https://thirstydeveloper.io/series/tf-skeleton 61 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | ADMIN_INIT_STACK_NAME := tf-admin-init 2 | STATE_BUCKET_NAME := terraform-skeleton-state 3 | STATE_LOG_BUCKET_NAME := terraform-skeleton-state-logs 4 | LOCK_TABLE_NAME := terraform-skeleton-state-locks 5 | 6 | # Use a known profile to ensure account ID is correct 7 | ADMIN_ACCOUNT_ID := $(shell \ 8 | aws --profile tf-admin-account sts get-caller-identity | jq -r .Account \ 9 | ) 10 | 11 | BACKEND_ROLE_PATH := terraform/TerraformBackend 12 | BACKEND_ROLE_ARN := arn:aws:iam::${ADMIN_ACCOUNT_ID}:role/${BACKEND_ROLE_PATH} 13 | 14 | DEPLOYMENT_DIRS := $(shell find deployments -name terragrunt.hcl \ 15 | -not -path */.terragrunt-cache/* -exec dirname {} \; \ 16 | ) 17 | 18 | CFN := aws cloudformation 19 | CFN_START_DRIFT_DETECTION := $(CFN) detect-stack-drift --stack-name 20 | CFN_STATUS_DRIFT_DETECTION := $(CFN) describe-stack-drift-detection-status \ 21 | --stack-drift-detection-id 22 | 23 | define wait_cfn_drift_detect_job 24 | @while [[ \ 25 | "$$($(CFN_STATUS_DRIFT_DETECTION) $(1) | jq -r .DetectionStatus)" == \ 26 | "DETECTION_IN_PROGRESS" \ 27 | ]]; do \ 28 | echo "Detection in progress. Waiting 3 seconds..."; \ 29 | sleep 3; \ 30 | done 31 | endef 32 | 33 | define show_cfn_drift 34 | $(eval DRIFT_ID=$(shell $(CFN_START_DRIFT_DETECTION) $(1) \ 35 | | jq -r .StackDriftDetectionId)) 36 | $(call wait_cfn_drift_detect_job,${DRIFT_ID}) 37 | @$(CFN_STATUS_DRIFT_DETECTION) $(DRIFT_ID) | jq '{ \ 38 | DetectionStatus, \ 39 | StackDriftStatus, \ 40 | DriftedStackResourceCount \ 41 | }' 42 | endef 43 | 44 | .PHONY: init-admin 45 | init-admin: 46 | aws cloudformation deploy \ 47 | --template-file init/admin/init-admin-account.cf.yml \ 48 | --stack-name ${ADMIN_INIT_STACK_NAME} \ 49 | --capabilities CAPABILITY_NAMED_IAM \ 50 | --parameter-overrides \ 51 | AdminAccountId=${ADMIN_ACCOUNT_ID} \ 52 | StateBucketName=${STATE_BUCKET_NAME} \ 53 | StateLogBucketName=${STATE_LOG_BUCKET_NAME} \ 54 | LockTableName=${LOCK_TABLE_NAME} 55 | aws cloudformation update-termination-protection \ 56 | --stack-name ${ADMIN_INIT_STACK_NAME} \ 57 | --enable-termination-protection 58 | 59 | .PHONY: check-init-admin-drift 60 | check-init-admin-drift: 61 | $(call show_cfn_drift,${ADMIN_INIT_STACK_NAME}) 62 | 63 | import-terragrunt-changeset.json: 64 | @aws cloudformation create-change-set \ 65 | --stack-name ${ADMIN_INIT_STACK_NAME} \ 66 | --change-set-name ${ADMIN_INIT_STACK_NAME}-import-terragrunt \ 67 | --change-set-type IMPORT \ 68 | --template-body file://init/admin/init-admin-account.cf.yml \ 69 | --capabilities CAPABILITY_NAMED_IAM \ 70 | --parameters \ 71 | ParameterKey=AdminAccountId,UsePreviousValue=True \ 72 | ParameterKey=StateBucketName,UsePreviousValue=True \ 73 | ParameterKey=StateLogBucketName,UsePreviousValue=True \ 74 | ParameterKey=LockTableName,UsePreviousValue=True \ 75 | --resources-to-import "[ \ 76 | { \ 77 | \"ResourceType\":\"AWS::S3::Bucket\", \ 78 | \"LogicalResourceId\":\"TerraformStateBucket\", \ 79 | \"ResourceIdentifier\": { \ 80 | \"BucketName\": \"${STATE_BUCKET_NAME}\" \ 81 | } \ 82 | }, \ 83 | { \ 84 | \"ResourceType\":\"AWS::S3::Bucket\", \ 85 | \"LogicalResourceId\":\"TerraformStateLogBucket\", \ 86 | \"ResourceIdentifier\": { \ 87 | \"BucketName\": \"${STATE_LOG_BUCKET_NAME}\" \ 88 | } \ 89 | }, \ 90 | { \ 91 | \"ResourceType\":\"AWS::DynamoDB::Table\", \ 92 | \"LogicalResourceId\":\"TerraformStateLockTable\", \ 93 | \"ResourceIdentifier\": { \ 94 | \"TableName\": \"${LOCK_TABLE_NAME}\" \ 95 | } \ 96 | } \ 97 | ]" | tee import-terragrunt-changeset.json 98 | 99 | .PHONY: prepare-cfn-import-terragrunt 100 | prepare-cfn-import-terragrunt: import-terragrunt-changeset.json 101 | $(eval CHANGE_SET_ID=$(shell jq -r .Id import-terragrunt-changeset.json)) 102 | aws cloudformation wait change-set-create-complete \ 103 | --change-set-name ${CHANGE_SET_ID} \ 104 | --stack-name ${ADMIN_INIT_STACK_NAME} 105 | @aws cloudformation describe-change-set \ 106 | --change-set-name ${CHANGE_SET_ID} \ 107 | --stack-name ${ADMIN_INIT_STACK_NAME} \ 108 | | jq '{ Changes, Status, StatusReason }' 109 | 110 | .PHONY: discard-cfn-import-terragrunt 111 | discard-cfn-import-terragrunt: import-terragrunt-changeset.json 112 | $(eval CHANGE_SET_ID=$(shell jq -r .Id import-terragrunt-changeset.json)) 113 | aws cloudformation delete-change-set \ 114 | --change-set-name ${CHANGE_SET_ID} \ 115 | --stack-name ${ADMIN_INIT_STACK_NAME} 116 | @rm import-terragrunt-changeset.json 117 | 118 | .PHONY: cfn-import-terragrunt 119 | cfn-import-terragrunt: import-terragrunt-changeset.json 120 | $(eval CHANGE_SET_ID=$(shell jq -r .Id import-terragrunt-changeset.json)) 121 | aws cloudformation wait change-set-create-complete \ 122 | --change-set-name ${CHANGE_SET_ID} \ 123 | --stack-name ${ADMIN_INIT_STACK_NAME} 124 | aws cloudformation execute-change-set \ 125 | --change-set-name ${CHANGE_SET_ID} \ 126 | --stack-name ${ADMIN_INIT_STACK_NAME} 127 | @rm import-terragrunt-changeset.json 128 | aws cloudformation wait stack-import-complete \ 129 | --stack-name ${ADMIN_INIT_STACK_NAME} 130 | $(call show_cfn_drift,${ADMIN_INIT_STACK_NAME}) 131 | 132 | .PHONY: test-backend-assume 133 | test-backend-assume: 134 | aws sts assume-role \ 135 | --role-arn ${BACKEND_ROLE_ARN} \ 136 | --role-session-name $(shell whoami) 137 | 138 | .PHONY: init-all 139 | init-all: 140 | for d in ${DEPLOYMENT_DIRS}; do \ 141 | pushd $$d; \ 142 | terragrunt init; \ 143 | popd; \ 144 | done 145 | 146 | .PHONY: clean 147 | clean: 148 | rm import-terragrunt-changeset.json 149 | -------------------------------------------------------------------------------- /init/admin/init-admin-account.cf.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # https://www.terraform.io/docs/backends/types/s3.html 3 | AWSTemplateFormatVersion: '2010-09-09' 4 | Description: Initialize terraform admin account 5 | Metadata: 6 | AWS::CloudFormation::Interface: 7 | ParameterGroups: 8 | - Label: 9 | default: Admin Account Config 10 | Parameters: 11 | - AdminAccountId 12 | - Label: 13 | default: Terraform State Resources 14 | Parameters: 15 | - StateBucketName 16 | - StateLogBucketName 17 | - LockTableName 18 | Parameters: 19 | AdminAccountId: 20 | Type: String 21 | Description: Account ID of the admin account to contain the state 22 | StateBucketName: 23 | Type: String 24 | Description: Name of the S3 bucket for terraform state 25 | StateLogBucketName: 26 | Type: String 27 | Description: Name of the S3 bucket for terraform state logs 28 | LockTableName: 29 | Type: String 30 | Description: Name of the terraform DynamoDB lock table 31 | 32 | Rules: 33 | EnsureDeployingToCorrectAccount: 34 | Assertions: 35 | - Assert: !Equals 36 | - !Ref AWS::AccountId 37 | - !Ref AdminAccountId 38 | AssertDescription: 'Stack can only be deployed into the specified AdminAccountId' 39 | 40 | Resources: 41 | TerraformStateLogBucket: 42 | Type: 'AWS::S3::Bucket' 43 | DeletionPolicy: Retain 44 | UpdateReplacePolicy: Retain 45 | Properties: 46 | BucketName: !Ref StateLogBucketName 47 | AccessControl: LogDeliveryWrite 48 | BucketEncryption: 49 | ServerSideEncryptionConfiguration: 50 | - ServerSideEncryptionByDefault: 51 | SSEAlgorithm: aws:kms 52 | PublicAccessBlockConfiguration: 53 | BlockPublicAcls: True 54 | BlockPublicPolicy: True 55 | IgnorePublicAcls: True 56 | RestrictPublicBuckets: True 57 | 58 | TerraformStateBucket: 59 | Type: 'AWS::S3::Bucket' 60 | DeletionPolicy: Retain 61 | UpdateReplacePolicy: Retain 62 | Properties: 63 | BucketName: !Ref StateBucketName 64 | BucketEncryption: 65 | ServerSideEncryptionConfiguration: 66 | - ServerSideEncryptionByDefault: 67 | SSEAlgorithm: aws:kms 68 | LoggingConfiguration: 69 | DestinationBucketName: !Ref StateLogBucketName 70 | LogFilePrefix: TFStateLogs/ 71 | PublicAccessBlockConfiguration: 72 | BlockPublicAcls: True 73 | BlockPublicPolicy: True 74 | IgnorePublicAcls: True 75 | RestrictPublicBuckets: True 76 | VersioningConfiguration: 77 | Status: Enabled 78 | 79 | TerraformStateLockTable: 80 | Type: 'AWS::DynamoDB::Table' 81 | DeletionPolicy: Retain 82 | UpdateReplacePolicy: Retain 83 | Properties: 84 | TableName: !Ref LockTableName 85 | AttributeDefinitions: 86 | - AttributeName: LockID 87 | AttributeType: S 88 | KeySchema: 89 | - AttributeName: LockID 90 | KeyType: HASH 91 | BillingMode: PAY_PER_REQUEST 92 | 93 | TerraformStateReadWritePolicy: 94 | Type: 'AWS::IAM::ManagedPolicy' 95 | Properties: 96 | ManagedPolicyName: TerraformStateReadWrite 97 | Path: /terraform/ 98 | Description: Read/write access to terraform state 99 | PolicyDocument: 100 | Version: 2012-10-17 101 | # Permissions are based on: 102 | # https://www.terraform.io/docs/backends/types/s3.html#example-configuration 103 | # https://github.com/gruntwork-io/terragrunt/issues/919 104 | Statement: 105 | - Sid: AllowStateBucketList 106 | Effect: Allow 107 | Action: 108 | - 's3:ListBucket' 109 | - 's3:GetBucketVersioning' 110 | Resource: !Sub "arn:aws:s3:::${StateBucketName}" 111 | - Sid: AllowStateReadWrite 112 | Effect: Allow 113 | Action: 114 | - 's3:GetObject' 115 | - 's3:PutObject' 116 | Resource: !Sub "arn:aws:s3:::${StateBucketName}/*" 117 | - Sid: AllowStateLockReadWrite 118 | Effect: Allow 119 | Action: 120 | - 'dynamodb:DescribeTable' 121 | - 'dynamodb:GetItem' 122 | - 'dynamodb:PutItem' 123 | - 'dynamodb:DeleteItem' 124 | Resource: !Sub "arn:aws:dynamodb:${AWS::Region}:${AWS::AccountId}:table/${LockTableName}" 125 | 126 | TerraformBackendRole: 127 | Type: 'AWS::IAM::Role' 128 | Properties: 129 | AssumeRolePolicyDocument: 130 | Version: 2012-10-17 131 | Statement: 132 | - Effect: Allow 133 | Principal: 134 | AWS: !Ref AWS::AccountId 135 | Action: 136 | - 'sts:AssumeRole' 137 | Condition: 138 | StringEquals: 139 | aws:PrincipalType: User 140 | StringLike: 141 | 'aws:PrincipalTag/Terraformer': '*' 142 | RoleName: TerraformBackend 143 | Path: /terraform/ 144 | ManagedPolicyArns: 145 | - !Ref TerraformStateReadWritePolicy 146 | 147 | TerraformStateBucketPolicy: 148 | Type: 'AWS::S3::BucketPolicy' 149 | DeletionPolicy: Retain 150 | UpdateReplacePolicy: Retain 151 | Properties: 152 | Bucket: !Ref TerraformStateBucket 153 | PolicyDocument: 154 | Version: '2012-10-17' 155 | Statement: 156 | ################################################ 157 | # Require TLS always 158 | ################################################ 159 | 160 | - Sid: 'AllowTLSRequestsOnly' 161 | Principal: '*' 162 | Condition: 163 | Bool: 164 | 'aws:SecureTransport': false 165 | Effect: Deny 166 | Action: '*' 167 | Resource: 168 | - !GetAtt "TerraformStateBucket.Arn" 169 | - !Sub 170 | - "${Bucket}/*" 171 | - Bucket: !GetAtt "TerraformStateBucket.Arn" 172 | 173 | ################################################ 174 | # User permissions 175 | ################################################ 176 | 177 | # If principal is an IAM user and that user does 178 | # not have the Terraformer tag, deny everything 179 | - Sid: DenyNonTerraformerUsers 180 | Principal: "*" 181 | Condition: 182 | StringEquals: 183 | aws:PrincipalType: User 184 | StringNotLike: 185 | 'aws:PrincipalTag/Terraformer': '*' 186 | Effect: Deny 187 | Action: '*' 188 | Resource: 189 | - !GetAtt "TerraformStateBucket.Arn" 190 | - !Sub 191 | - "${Bucket}/*" 192 | - Bucket: !GetAtt "TerraformStateBucket.Arn" 193 | 194 | # If principal is an IAM user and that user has the Terraformer tag, 195 | # but the tag value is not set to Admin, limit to read/write access. 196 | # By extension, if the user has the Terraformer tag set to Admin, this 197 | # policy places no restriction, granting that user whatever access is 198 | # specified in their IAM user policy. 199 | - Sid: RestrictTerraformNonAdmins 200 | Principal: "*" 201 | Condition: 202 | StringEquals: 203 | aws:PrincipalType: User 204 | StringLike: 205 | 'aws:PrincipalTag/Terraformer': '*' 206 | StringNotEquals: 207 | 'aws:PrincipalTag/Terraformer': 'Admin' 208 | Effect: Deny 209 | NotAction: 210 | - 's3:List*' 211 | - 's3:Get*' 212 | - 's3:Describe*' 213 | - 's3:PutObject' 214 | # Granting DeleteObject is safe because we have versioning enabled 215 | # on the bucket. DeleteObjectVersion is what we have to restrict 216 | # so that someone can't delete the delete marker and thereby 217 | # permanently delete state. Granting DeleteObject is helpful 218 | # because it allows NonAdmins to migrate state (i.e., rename state 219 | # files) 220 | - 's3:DeleteObject' 221 | Resource: 222 | - !GetAtt "TerraformStateBucket.Arn" 223 | - !Sub 224 | - "${Bucket}/*" 225 | - Bucket: !GetAtt "TerraformStateBucket.Arn" 226 | 227 | ################################################ 228 | # Role permissions 229 | ################################################ 230 | 231 | # https://aws.amazon.com/blogs/security/how-to-restrict-amazon-s3-bucket-access-to-a-specific-iam-role/ 232 | 233 | # If the principal is an assumed IAM role and that role is not the 234 | # backend role, deny access. 235 | - Sid: DenyNonBackendRoles 236 | Principal: "*" 237 | Condition: 238 | StringEquals: 239 | aws:PrincipalType: AssumedRole 240 | StringNotLike: 241 | aws:userId: 242 | - !Sub 243 | - "${TerraformBackendRoleId}:*" 244 | - TerraformBackendRoleId: !GetAtt "TerraformBackendRole.RoleId" 245 | Effect: Deny 246 | Action: '*' 247 | Resource: 248 | - !GetAtt "TerraformStateBucket.Arn" 249 | - !Sub 250 | - "${Bucket}/*" 251 | - Bucket: !GetAtt "TerraformStateBucket.Arn" 252 | 253 | # If the principal is an assumed IAM role and that role is the 254 | # backend role, limit to read/write access. 255 | - Sid: ResrictBackendRoleToReadWrite 256 | Principal: "*" 257 | Condition: 258 | StringEquals: 259 | aws:PrincipalType: AssumedRole 260 | StringLike: 261 | aws:userId: 262 | - !Sub 263 | - "${TerraformBackendRoleId}:*" 264 | - TerraformBackendRoleId: !GetAtt "TerraformBackendRole.RoleId" 265 | Effect: Deny 266 | NotAction: 267 | - 's3:ListBucket' 268 | - 's3:GetBucketVersioning' 269 | - 's3:GetObject' 270 | - 's3:PutObject' 271 | Resource: 272 | - !GetAtt "TerraformStateBucket.Arn" 273 | - !Sub 274 | - "${Bucket}/*" 275 | - Bucket: !GetAtt "TerraformStateBucket.Arn" 276 | 277 | ################################################ 278 | # Default deny 279 | ################################################ 280 | 281 | # https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_variables.html#principaltable 282 | 283 | - Sid: DenyAllOtherPrincipals 284 | Principal: "*" 285 | Condition: 286 | StringNotEquals: 287 | aws:PrincipalType: 288 | - AssumedRole 289 | - Account 290 | - User 291 | Effect: Deny 292 | Action: '*' 293 | Resource: 294 | - !GetAtt "TerraformStateBucket.Arn" 295 | - !Sub 296 | - "${Bucket}/*" 297 | - Bucket: !GetAtt "TerraformStateBucket.Arn" 298 | --------------------------------------------------------------------------------