diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
new file mode 100644
index 0000000..f67eda9
--- /dev/null
+++ b/.github/CODEOWNERS
@@ -0,0 +1 @@
+* @rhythmictech/engineering
diff --git a/.github/terraform-backend.drawio.png b/.github/terraform-backend.drawio.png
new file mode 100644
index 0000000..dcad379
Binary files /dev/null and b/.github/terraform-backend.drawio.png differ
diff --git a/.github/workflows/misspell.yaml b/.github/workflows/misspell.yaml
new file mode 100644
index 0000000..6574e3d
--- /dev/null
+++ b/.github/workflows/misspell.yaml
@@ -0,0 +1,23 @@
+---
+name: misspell
+on:
+ push:
+ branches:
+ - main
+ - master
+ - prod
+ - develop
+
+jobs:
+ misspell:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: misspell
+ uses: reviewdog/action-misspell@v1
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ locale: "US"
+ reporter: github-check
+ filter_mode: nofilter
+ level: error
diff --git a/.github/workflows/pre-commit.yaml b/.github/workflows/pre-commit.yaml
new file mode 100644
index 0000000..398767c
--- /dev/null
+++ b/.github/workflows/pre-commit.yaml
@@ -0,0 +1,30 @@
+---
+name: pre-commit-check
+on:
+ push:
+ branches:
+ - master
+ - prod
+ - develop
+
+jobs:
+ pre-commit-check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ - name: Install prerequisites
+ run: ./bin/install-ubuntu.sh
+ - name: initialize Terraform
+ run: terraform init --backend=false
+ - name: pre-commit
+ uses: pre-commit/action@v2.0.3
+ env:
+ AWS_DEFAULT_REGION: us-east-1
+ # many of these are covered by better reviewdog linters below
+ SKIP: >-
+ terraform_tflint_deep,
+ no-commit-to-branch,
+ terraform_tflint_nocreds,
+ terraform_tfsec
diff --git a/.github/workflows/pullRequest.yaml b/.github/workflows/pullRequest.yaml
new file mode 100644
index 0000000..8f1eba5
--- /dev/null
+++ b/.github/workflows/pullRequest.yaml
@@ -0,0 +1,81 @@
+---
+name: pull request
+on:
+ pull_request:
+
+jobs:
+ # TODO: #22 add job using https://github.com/reviewdog/action-alex
+ pre-commit:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python
+ uses: actions/setup-python@v2
+ - name: Install prerequisites
+ run: ./bin/install-ubuntu.sh
+ - name: initialize Terraform
+ run: terraform init --backend=false
+ - name: pre-commit
+ uses: pre-commit/action@v2.0.3
+ env:
+ AWS_DEFAULT_REGION: us-east-1
+ # many of these are covered by better reviewdog linters below
+ SKIP: >-
+ terraform_tflint_deep,
+ no-commit-to-branch,
+ terraform_tflint_nocreds,
+ terraform_tfsec
+ tflint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install prerequisites
+ run: ./bin/install-ubuntu.sh
+ - name: Terraform init
+ run: terraform init --backend=false
+ - name: tflint
+ uses: reviewdog/action-tflint@master
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ reporter: github-pr-check
+ filter_mode: added
+ flags: --module
+ level: error
+ tfsec:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install prerequisites
+ run: ./bin/install-ubuntu.sh
+ - name: Terraform init
+ run: terraform init --backend=false
+ - name: tfsec
+ uses: reviewdog/action-tfsec@master
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ reporter: github-pr-check
+ filter_mode: added
+ level: warning
+ misspell:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: misspell
+ uses: reviewdog/action-misspell@v1
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ locale: "US"
+ reporter: github-pr-check
+ filter_mode: added
+ level: error
+ yamllint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: yamllint
+ uses: reviewdog/action-yamllint@v1
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ reporter: github-pr-check
+ filter_mode: added
+ level: error
diff --git a/.github/workflows/tflint.yaml b/.github/workflows/tflint.yaml
new file mode 100644
index 0000000..10457b5
--- /dev/null
+++ b/.github/workflows/tflint.yaml
@@ -0,0 +1,27 @@
+---
+name: tflint
+on:
+ push:
+ branches:
+ - main
+ - master
+ - prod
+ - develop
+
+jobs:
+ tflint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install prerequisites
+ run: ./bin/install-ubuntu.sh
+ - name: Terraform init
+ run: terraform init --backend=false
+ - name: tflint
+ uses: reviewdog/action-tflint@master
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ reporter: github-check
+ filter_mode: nofilter
+ flags: --module
+ level: error
diff --git a/.github/workflows/tfsec.yaml b/.github/workflows/tfsec.yaml
new file mode 100644
index 0000000..2f75a3e
--- /dev/null
+++ b/.github/workflows/tfsec.yaml
@@ -0,0 +1,26 @@
+---
+name: tfsec
+on:
+ push:
+ branches:
+ - main
+ - master
+ - prod
+ - develop
+
+jobs:
+ tfsec:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: Install prerequisites
+ run: ./bin/install-ubuntu.sh
+ - name: Terraform init
+ run: terraform init --backend=false
+ - name: tfsec
+ uses: reviewdog/action-tfsec@master
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ reporter: github-check
+ filter_mode: nofilter
+ level: error
diff --git a/.github/workflows/yamllint.yaml b/.github/workflows/yamllint.yaml
new file mode 100644
index 0000000..0b6f93a
--- /dev/null
+++ b/.github/workflows/yamllint.yaml
@@ -0,0 +1,22 @@
+---
+name: yamllint
+on:
+ push:
+ branches:
+ - main
+ - master
+ - prod
+ - develop
+
+jobs:
+ yamllint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - name: yamllint
+ uses: reviewdog/action-yamllint@v1
+ with:
+ github_token: ${{ secrets.GITHUB_TOKEN }}
+ reporter: github-check
+ filter_mode: nofilter
+ level: error
diff --git a/.gitignore b/.gitignore
index 428217a..b948591 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,8 @@
# .tfstate files
*.tfstate
*.tfstate.*
+
+assumerole
+assumerole/*
+
+.DS_Store
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..5ba9f36
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,89 @@
+exclude: ".terraform"
+repos:
+ - repo: https://github.com/antonbabenko/pre-commit-terraform
+ rev: v1.74.1
+ hooks:
+ - id: terraform_docs
+ always_run: true
+ - id: terraform_fmt
+ - id: terraform_tflint
+ alias: terraform_tflint_nocreds
+ name: terraform_tflint_nocreds
+ - id: terraform_tfsec
+ - repo: local
+ hooks:
+ - id: terraform_validate
+ name: terraform_validate
+ entry: |
+ bash -c '
+ AWS_DEFAULT_REGION=us-east-1
+ declare -a DIRS
+ for FILE in "$@"
+ do
+ DIRS+=($(dirname "$FILE"))
+ done
+ for DIR in $(printf "%s\n" "${DIRS[@]}" | sort -u)
+ do
+ cd $(dirname "$FILE")
+ terraform init --backend=false
+ terraform validate .
+ cd ..
+ done
+ '
+ language: system
+ verbose: true
+ files: \.tf(vars)?$
+ exclude: examples
+ - id: tflock
+ name: provider_locks
+ entry: |
+ bash -c '
+ AWS_DEFAULT_REGION=us-east-1
+ declare -a DIRS
+ for FILE in "$@"
+ do
+ DIRS+=($(dirname "$FILE"))
+ done
+ for DIR in $(printf "%s\n" "${DIRS[@]}" | sort -u)
+ do
+ cd $(dirname "$FILE")
+ terraform providers lock -platform=linux_amd64
+ cd ..
+ done
+ '
+ language: system
+ verbose: true
+ files: \.tf(vars)?$
+ exclude: examples
+ - repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.2.0
+ hooks:
+ - id: check-case-conflict
+ - id: check-json
+ - id: check-merge-conflict
+ - id: check-symlinks
+ - id: check-yaml
+ args:
+ - --unsafe
+ - id: end-of-file-fixer
+ - id: mixed-line-ending
+ args:
+ - --fix=lf
+ - id: no-commit-to-branch
+ args:
+ - --branch
+ - main
+ - --branch
+ - master
+ - --branch
+ - prod
+ - id: pretty-format-json
+ args:
+ - --autofix
+ - --top-keys=name,Name
+ - id: trailing-whitespace
+ args:
+ - --markdown-linebreak-ext=md
+ exclude: README.md
+ci:
+ skip: [terraform_docs, terraform_fmt, terraform_tflint, terraform_tfsec, tflock]
diff --git a/.terraform-version b/.terraform-version
new file mode 100644
index 0000000..4f20ea7
--- /dev/null
+++ b/.terraform-version
@@ -0,0 +1 @@
+latest:^1.1
diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl
new file mode 100644
index 0000000..3d75310
--- /dev/null
+++ b/.terraform.lock.hcl
@@ -0,0 +1,71 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/aws" {
+ version = "4.28.0"
+ constraints = ">= 4.0.0"
+ hashes = [
+ "h1:OsPtt0JGl70xbY2+V/ai0d+Jk2p7pvkW8h9IKhIr288=",
+ "h1:TXCUuuaf2q54C43bxSNiF9g+cxTr8zqEZem0pW15cjE=",
+ "h1:Z7sun0kcsON43rhvjmlv0nzkL2XMoBUTGLdCIFTmJ4I=",
+ "h1:tiM/l5kEY7c2m4W5d98gPB9zZauX1mMB45olCglVnBk=",
+ "zh:1d4806e50971d2cd565273cedf3206e38931677a6f546cf2b9fb140b52b80604",
+ "zh:3f076791002b8afa5ba2d2038f1e1db5956022327eb5242152723ed410ae4571",
+ "zh:40e5944a9df0d083dbd316bcc6ac9ceada5c00dab70c21897e62b68c4c936bc9",
+ "zh:68b78d0c1866aa0bcbbadb1cf51349c9af697f8789f5778b7e7e2912a9c4845d",
+ "zh:72d6e66136841c0e5ae264e03555cf59751ddae1b9784eafcb877c624332c70a",
+ "zh:902c8f89dc10d321b87c09270c27a31a42d4e74e4da1608e55b7f241cd010a62",
+ "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
+ "zh:bf54c9f55d420b4e1fe68db81a759c40b9f9747159dea3061212a1c9768dcdfd",
+ "zh:bfbe7e745c420a4ebd27ca35dfe5c2acc7cdd05092e1daf60f5ae29a1130d752",
+ "zh:d271a30b16f0861f020e423d120d1458cf1757e740e016ace22084c39dc13550",
+ "zh:f1e4672d1625fd1f1268d4b807cb90e28150d46fb2d0dd0836de65db29c8d5e6",
+ "zh:f5cee910b4db2da3c2a28dae9055cbca4273eb774c362bb7bb5bde04deff4557",
+ ]
+}
+
+provider "registry.terraform.io/hashicorp/local" {
+ version = "2.2.3"
+ constraints = ">= 2.0.0"
+ hashes = [
+ "h1:3bH88Z7tlWvcoubm6hQUBk3s9bSIJC8bVHQz749B87E=",
+ "h1:FvRIEgCmAezgZUqb2F+PZ9WnSSnR5zbEM2ZI+GLmbMk=",
+ "h1:KmHz81iYgw9Xn2L3Carc2uAzvFZ1XsE7Js3qlVeC77k=",
+ "h1:aWp5iSUxBGgPv1UnV5yag9Pb0N+U1I0sZb38AXBFO8A=",
+ "zh:04f0978bb3e052707b8e82e46780c371ac1c66b689b4a23bbc2f58865ab7d5c0",
+ "zh:6484f1b3e9e3771eb7cc8e8bab8b35f939a55d550b3f4fb2ab141a24269ee6aa",
+ "zh:78a56d59a013cb0f7eb1c92815d6eb5cf07f8b5f0ae20b96d049e73db915b238",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:8aa9950f4c4db37239bcb62e19910c49e47043f6c8587e5b0396619923657797",
+ "zh:996beea85f9084a725ff0e6473a4594deb5266727c5f56e9c1c7c62ded6addbb",
+ "zh:9a7ef7a21f48fabfd145b2e2a4240ca57517ad155017e86a30860d7c0c109de3",
+ "zh:a63e70ac052aa25120113bcddd50c1f3cfe61f681a93a50cea5595a4b2cc3e1c",
+ "zh:a6e8d46f94108e049ad85dbed60354236dc0b9b5ec8eabe01c4580280a43d3b8",
+ "zh:bb112ce7efbfcfa0e65ed97fa245ef348e0fd5bfa5a7e4ab2091a9bd469f0a9e",
+ "zh:d7bec0da5c094c6955efed100f3fe22fca8866859f87c025be1760feb174d6d9",
+ "zh:fb9f271b72094d07cef8154cd3d50e9aa818a0ea39130bc193132ad7b23076fd",
+ ]
+}
+
+provider "registry.terraform.io/hashicorp/random" {
+ version = "3.4.1"
+ constraints = ">= 3.0.0"
+ hashes = [
+ "h1:NfxR2vBvEEfI0nA7FiL+QMuWFnC6rl2avzQ9+ykoSo4=",
+ "h1:Q+1ZwDtRYtFZLrPfpe9wnz7ulNvotLfM6WmwXFCGnSA=",
+ "h1:gveAzKTj7/ji9kYyW9BoxmJ8TUHulmwhHaodm5dLfr4=",
+ "h1:tHwxkMmT5lV0jFsUaEDJav+S1KJXUF20UL3ympHE5UY=",
+ "zh:081d91c6f2602f76acef4a8f18d6bf392e104fe02a7885d167b06fe60adf7277",
+ "zh:0cbde9a961a3d4581edbf3af8137eac11e52b9b8b6117a6bda916150b68f7281",
+ "zh:1ad33b85a6e7400c438d33acd7c8a43c74d79711f11c7b8fd715bf94379a30ac",
+ "zh:5bfa3c71d28c9f961d1c46cdfa583b3a82a59d7298f4afe2c89081ebdf8863b8",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:a34a4286aff007a3834e13e70d28e7ce9b8ae162c5bef9412cae89df6226fe2d",
+ "zh:a6adefa0199b60cdc1accc617b24851c1f7da501891bc97d039d819749ead537",
+ "zh:c0ad3b665de7b7124d3159eefabcaae29f0bd8758847bfef6204afbd7e083bba",
+ "zh:d8a086da281e36949a70e8fce7aef449db34d65e12195e5856ddfb4e1b5747f2",
+ "zh:e1fcc67afc6b808a616bf53da6da18420fad7594cadca1b3a6d2a447f52dc8c5",
+ "zh:ed1877c6850c2d7101044fb3cf352ce81e5c3743aa78b1087bcf557b5163e887",
+ "zh:f7992c6fa4a639b1464dfdd7648a12e1ae6f05c6b8958c90c9705f09fd0b5bb5",
+ ]
+}
diff --git a/.tflint.hcl b/.tflint.hcl
new file mode 100644
index 0000000..854fb92
--- /dev/null
+++ b/.tflint.hcl
@@ -0,0 +1,44 @@
+config {
+ module = true
+}
+
+rule "terraform_deprecated_interpolation" {
+ enabled = true
+}
+
+rule "terraform_unused_declarations" {
+ enabled = true
+}
+
+rule "terraform_comment_syntax" {
+ enabled = true
+}
+
+rule "terraform_documented_outputs" {
+ enabled = true
+}
+
+rule "terraform_documented_variables" {
+ enabled = true
+}
+
+rule "terraform_typed_variables" {
+ enabled = true
+}
+
+rule "terraform_module_pinned_source" {
+ enabled = true
+}
+
+rule "terraform_naming_convention" {
+ enabled = true
+ format = "snake_case"
+}
+
+rule "terraform_required_version" {
+ enabled = false
+}
+
+rule "terraform_required_providers" {
+ enabled = true
+}
diff --git a/.yamllint.yml b/.yamllint.yml
new file mode 100644
index 0000000..e1a518a
--- /dev/null
+++ b/.yamllint.yml
@@ -0,0 +1,2 @@
+truthy:
+ check-keys: false
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..5fe0374
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2022 Rhythmic Technologies, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 7c1a321..9117714 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,161 @@
# terraform-aws-backend
-Creates a backend S3 bucket and DynamoDB table for managing Terraform state
-
-## Required Variables
-* `region`
-* `namespace`
-* `owner`
-* `bucket`
-* `table`
-
-## Optional Variables
-* `extra_tags` = `{}`
-* `env` = `global`
+
+[](https://github.com/rhythmictech/terraform-aws-backend/actions?query=workflow%3Atflint+event%3Apush+branch%3Amaster)
+[](https://github.com/rhythmictech/terraform-aws-backend/actions?query=workflow%3Atfsec+event%3Apush+branch%3Amaster)
+[](https://github.com/rhythmictech/terraform-aws-backend/actions?query=workflow%3Ayamllint+event%3Apush+branch%3Amaster)
+[](https://github.com/rhythmictech/terraform-aws-backend/actions?query=workflow%3Amisspell+event%3Apush+branch%3Amaster)
+[](https://github.com/rhythmictech/terraform-aws-backend/actions?query=workflow%3Apre-commit-check+event%3Apush+branch%3Amaster)
+
+
+Creates an S3 bucket and DynamoDB table for managing Terraform state. Note that when bootstrapping a new environment, it is typically easier to use a separate method for creating the bucket and lock table, like [a CloudFormation Stack](https://github.com/rhythmictech/AWS-CFN-Terraform-Bootstrap). This module is intended to create a backend in an AWS account that is already Terraform-managed. This is useful to store the state for other accounts externally.
+
+This module will create a CloudFormation stack and an optional wrapper script to deploy it. This stack is suitable to run in any account that will store its Terraform state in the bucket created by this module. It creates an IAM role with the AdministratorAccess policy attached and with an External ID which can then be assumed by terraform to create resources in the child account(s).
+
+
+
+*Breaking Changes*
+
+Previous versions of this module had support for cross-account management in a way that proved awkward for many uses cases and made it more difficult than it should've to fully secure the tfstate between accounts. Version 4.x and later eliminates support for this and refocuses the module on using centralized tfstate buckets with cross-account role assumption for execution of terraform. As a result, many variable names have changed and functionality has been dropped. Upgrade to this version at your own peril.
+
+## Multi-Account Usage
+These instructions assume two AWS accounts; a "Parent" account which holds the terraform state and IAM users, and a "Child" account.
+
+1) In the parent account create this module. The below code is a serving suggestion.
+```
+module "backend" {
+ source = "rhythmictech/backend/aws"
+ version = "4.1.0"
+
+ bucket_name = "${local.account_id}-${var.region}-terraform-state"
+ create_assumerole_template = true
+ logging_target_bucket = module.s3logging-bucket.s3_bucket_name
+ logging_target_prefix = "${local.account_id}-${var.region}-tf-state"
+ tags = module.tags.tags_no_name
+}
+```
+
+It will create a folder with a shell script and a CloudFormation stack in it.
+
+2) Log into the child account and run the shell script, `assumerole/addrole.sh`. This will create a CloudFormation stack in that child account.
+
+3) In the terraform code for the child account create the provider and backend sections like below, substituting `PARENT_ACCT_ID` and `PARENT_REGION`, `CHILD_ACCT_ID`, AND `EXTERNAL_ID`.
+
+terraform backend config:
+```
+bucket = "PARENT_ACCT_ID-PARENT_REGION-terraform-state"
+dynamodb_table = "tf-locktable"
+key = "account.tfstate"
+region = "PARENT_REGION"
+```
+
+provider config:
+```
+provider "aws" {
+ assume_role {
+ role_arn = "arn:aws:iam::CHILD_ACCT_ID:role/Terraform"
+ session_name = "terraform-network"
+ external_id = "EXTERNAL_ID"
+ }
+}
+```
+
+4) Log in to the master account and run terraform using this backend and provider config. The state will be stored in the parent account but terraform will assume the child account role.
+
+## Cross Account State Management
+
+See [Use AssumeRole to Provision AWS Resources Across Accounts](https://learn.hashicorp.com/tutorials/terraform/aws-assumerole) for more information on this pattern.
+
+This module is not intended to hold the state for the account in which it is created. If the account itself is also Terraform managed, it is recommended to create a separate bucket for its own state manually or via a different IaC method (e.g., CloudFormation) to avoid the chicken-and-egg problem. See [this CloudFormation template](https://github.com/rhythmictech/AWS-CFN-Terraform-Bootstrap) to create terraform backend for this or any other single account.
+
+You can test the ability to assume a role in the child account by logging in with the parent account and running this
+```
+
+export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s" \
+$(aws sts assume-role \
+--external-id EXTERNAL_ID \
+--role-arn arn:aws:iam::CHILD_ACCT_ID:role/Terraform \
+--role-session-name testme \
+--query "Credentials.[AccessKeyId,SecretAccessKey,SessionToken]" \
+--output text))%
+
+AWS_SECURITY_TOKEN=
+```
+Then `aws sts get-caller-identity` should reveal you to be in the child account.
+
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 0.14 |
+| [aws](#requirement\_aws) | >= 4.0 |
+| [local](#requirement\_local) | >= 2.0 |
+| [random](#requirement\_random) | >= 3.0 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [aws](#provider\_aws) | 4.28.0 |
+| [local](#provider\_local) | 2.2.3 |
+| [random](#provider\_random) | 3.4.1 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [aws_dynamodb_table.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource |
+| [aws_kms_alias.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_alias) | resource |
+| [aws_kms_key.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/kms_key) | resource |
+| [aws_s3_bucket.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource |
+| [aws_s3_bucket_lifecycle_configuration.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_lifecycle_configuration) | resource |
+| [aws_s3_bucket_logging.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_logging) | resource |
+| [aws_s3_bucket_ownership_controls.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_ownership_controls) | resource |
+| [aws_s3_bucket_public_access_block.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource |
+| [aws_s3_bucket_server_side_encryption_configuration.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_server_side_encryption_configuration) | resource |
+| [aws_s3_bucket_versioning.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource |
+| [local_file.assumerole_addrole](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/file) | resource |
+| [local_sensitive_file.assumerole_tfassumerole](https://registry.terraform.io/providers/hashicorp/local/latest/docs/resources/sensitive_file) | resource |
+| [random_password.external_id](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource |
+| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
+| [aws_iam_policy_document.key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
+| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source |
+| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [assumerole\_role\_attach\_policies](#input\_assumerole\_role\_attach\_policies) | Policy ARNs to attach to role (can be managed or custom but must exist) | `list(string)` |
[| no | +| [assumerole\_role\_external\_id](#input\_assumerole\_role\_external\_id) | External ID to attach to role (this is required, a random ID will be generated if not specified here) | `string` | `null` | no | +| [assumerole\_role\_name](#input\_assumerole\_role\_name) | Name of role to create in assumerole template | `string` | `"Terraform"` | no | +| [assumerole\_stack\_name](#input\_assumerole\_stack\_name) | Name of CloudFormation stack | `string` | `"tf-assumerole"` | no | +| [assumerole\_template\_name](#input\_assumerole\_template\_name) | File name of assumerole cloudformation template | `string` | `"assumerole/tfassumerole.cfn.yml"` | no | +| [bucket\_name](#input\_bucket\_name) | Name of bucket to hold tf state | `string` | n/a | yes | +| [create\_assumerole\_template](#input\_create\_assumerole\_template) | If true, create a CloudFormation template that can be run against accounts to create an assumable role | `bool` | `false` | no | +| [dynamo\_locktable\_name](#input\_dynamo\_locktable\_name) | Name of lock table for terraform | `string` | `"tf-locktable"` | no | +| [dynamodb\_kms\_key\_arn](#input\_dynamodb\_kms\_key\_arn) | KMS key arn to enable encryption on dynamodb table. Defaults to `alias/aws/dynamodb` | `string` | `null` | no | +| [dynamodb\_point\_in\_time\_recovery](#input\_dynamodb\_point\_in\_time\_recovery) | DynamoDB point-in-time recovery. | `bool` | `true` | no | +| [dynamodb\_server\_side\_encryption](#input\_dynamodb\_server\_side\_encryption) | Bool to enable encryption on dynamodb table | `bool` | `true` | no | +| [kms\_alias\_name](#input\_kms\_alias\_name) | Name of KMS Alias | `string` | `null` | no | +| [kms\_key\_id](#input\_kms\_key\_id) | ARN for KMS key for all encryption operations (a key will be created if this is not provided) | `string` | `null` | no | +| [lifecycle\_rules](#input\_lifecycle\_rules) | lifecycle rules to apply to the bucket (set to null to skip lifecycle rules) |
"arn:aws:iam::aws:policy/AdministratorAccess"
]
list(object(| `null` | no | +| [logging\_target\_bucket](#input\_logging\_target\_bucket) | The name of the bucket that will receive the log objects (logging will be disabled if null) | `string` | `null` | no | +| [logging\_target\_prefix](#input\_logging\_target\_prefix) | A key prefix for log objects | `string` | `null` | no | +| [tags](#input\_tags) | Mapping of any extra tags you want added to resources | `map(string)` | `{}` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [backend\_config\_stub](#output\_backend\_config\_stub) | Backend config stub to be used in child account(s) | +| [external\_id](#output\_external\_id) | External ID attached to IAM role in managed accounts | +| [kms\_key\_arn](#output\_kms\_key\_arn) | ARN of KMS Key for S3 bucket | +| [provider\_config\_stub](#output\_provider\_config\_stub) | Provider config stub to be used in child account(s) | +| [s3\_bucket\_backend](#output\_s3\_bucket\_backend) | S3 bucket used to store TF state | + diff --git a/assumerole.tf b/assumerole.tf new file mode 100644 index 0000000..9fcede2 --- /dev/null +++ b/assumerole.tf @@ -0,0 +1,37 @@ +locals { + external_id = coalesce(var.assumerole_role_external_id, random_password.external_id.result) +} + +resource "local_file" "assumerole_addrole" { + count = var.create_assumerole_template ? 1 : 0 + + filename = "assumerole/addrole.sh" + + content = templatefile("${path.module}/template/addrole.sh.tftpl", { + stack_name = var.assumerole_stack_name + assumerole_template_name = var.assumerole_template_name + }) + +} + +resource "local_sensitive_file" "assumerole_tfassumerole" { + count = var.create_assumerole_template ? 1 : 0 + + filename = var.assumerole_template_name + + content = templatefile("${path.module}/template/tfassumerole.cfn.yml.tftpl", { + external_id = local.external_id + parent_account_id = local.account_id + partition = local.partition + policy_arns = var.assumerole_role_attach_policies + role_name = var.assumerole_role_name + }) + +} + +# not used if an external id is specified +resource "random_password" "external_id" { + length = 16 + special = true + override_special = "-_=+" +} diff --git a/bin/install-macos.sh b/bin/install-macos.sh new file mode 100755 index 0000000..3800165 --- /dev/null +++ b/bin/install-macos.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +echo 'installing brew packages' +brew update +brew tap liamg/tfsec +brew install tfenv tflint terraform-docs pre-commit liamg/tfsec/tfsec coreutils +brew upgrade tfenv tflint terraform-docs pre-commit liamg/tfsec/tfsec coreutils + +echo 'installing pre-commit hooks' +pre-commit install + +echo 'setting pre-commit hooks to auto-install on clone in the future' +git config --global init.templateDir ~/.git-template +pre-commit init-templatedir ~/.git-template + +echo 'installing terraform with tfenv' +tfenv install diff --git a/bin/install-ubuntu.sh b/bin/install-ubuntu.sh new file mode 100755 index 0000000..3390308 --- /dev/null +++ b/bin/install-ubuntu.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +echo 'installing dependencies' +sudo apt install python3-pip gawk &&\ +pip3 install pre-commit + +# terraform docs +mkdir tmp +cd tmp +curl -Lo ./terraform-docs.tar.gz https://github.com/terraform-docs/terraform-docs/releases/download/v0.16.0/terraform-docs-v0.16.0-$(uname)-amd64.tar.gz +tar -xzf terraform-docs.tar.gz +chmod +x terraform-docs +sudo mv terraform-docs /usr/bin/ +cd .. +rm -rf tmp + +curl -L "$(curl -sL https://api.github.com/repos/terraform-linters/tflint/releases/latest | grep -o -E "https://.+?_linux_amd64.zip")" > tflint.zip && unzip tflint.zip && rm tflint.zip && sudo mv tflint /usr/bin/ +env GO111MODULE=on go get -u github.com/liamg/tfsec/cmd/tfsec +git clone https://github.com/tfutils/tfenv.git ~/.tfenv || true +mkdir -p ~/.local/bin/ +. ~/.profile +ln -s ~/.tfenv/bin/* ~/.local/bin + +echo 'installing pre-commit hooks' +pre-commit install + +echo 'setting pre-commit hooks to auto-install on clone in the future' +git config --global init.templateDir ~/.git-template +pre-commit init-templatedir ~/.git-template + +echo 'installing terraform with tfenv' +tfenv install diff --git a/examples/basic/README.md b/examples/basic/README.md new file mode 100644 index 0000000..22e2bf0 --- /dev/null +++ b/examples/basic/README.md @@ -0,0 +1,42 @@ +# Basic Backend Example +Creates resources for a secure backend in AWS to support separate AWS accounts. To use this backend, use the following provider definition: + +``` +provider "aws" { + region = var.region + + assume_role { + role_arn = "arn:aws-us-gov:iam::012345678901:role/Terraform" + external_id = "YourExternalID" + } +} + +You will need to run Terraform with IAM credentials of the account that holds the state rather than the accounts that you are working on. + + +## Requirements + +No requirements. + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [tfstate](#module\_tfstate) | ../.. | n/a | + +## Resources + +No resources. + +## Inputs + +No inputs. + +## Outputs + +No outputs. + diff --git a/examples/basic/main.tf b/examples/basic/main.tf new file mode 100644 index 0000000..154befc --- /dev/null +++ b/examples/basic/main.tf @@ -0,0 +1,7 @@ +module "tfstate" { + source = "../.." + + bucket_name = "tf-state-remote" + create_assumerole_template = true + dynamo_locktable_name = "tf-locktable-remote" +} diff --git a/examples/workspaces/README.md b/examples/workspaces/README.md new file mode 100644 index 0000000..103bf7c --- /dev/null +++ b/examples/workspaces/README.md @@ -0,0 +1,68 @@ +# Workspaces Backend Example +Creates resources for a secure backend in AWS to support multiple AWS accounts. To use this backend with accounts managed by Terraform workspaces, use the following provider definition and variable: + +``` +provider "aws" { + region = var.region + + assume_role { + role_arn = var.workspace_iam_roles[terraform.workspace].role_arn + external_id = var.workspace_iam_roles[terraform.workspace].external_id + } +} + +variable "workspace_iam_roles" { + description = "IAM roles to assume" + type = any +} + +``` + +Then define variable entries for each account: + +``` +workspace_iam_roles = { + dev = { + role_arn = "arn:aws-us-gov:iam::012345678901:role/Terraform" + external_id = "YourExternalID" + } + test = { + role_arn = "arn:aws:iam::012345678902:role/Terraform" + external_id = "YourExternalID" + } + prod = { + role_arn = "arn:aws-us-gov:iam::012345678903:role/Terraform" + external_id = "YourExternalID" + } +} +``` + +You will need to run Terraform with IAM credentials of the account that holds the state rather than the accounts that you are working on. + + +## Requirements + +No requirements. + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [tfstate](#module\_tfstate) | ../.. | n/a | + +## Resources + +No resources. + +## Inputs + +No inputs. + +## Outputs + +No outputs. + diff --git a/examples/workspaces/main.tf b/examples/workspaces/main.tf new file mode 100644 index 0000000..154befc --- /dev/null +++ b/examples/workspaces/main.tf @@ -0,0 +1,7 @@ +module "tfstate" { + source = "../.." + + bucket_name = "tf-state-remote" + create_assumerole_template = true + dynamo_locktable_name = "tf-locktable-remote" +} diff --git a/kms.tf b/kms.tf new file mode 100644 index 0000000..f41ff24 --- /dev/null +++ b/kms.tf @@ -0,0 +1,32 @@ +data "aws_iam_policy_document" "key" { + statement { + actions = ["kms:*"] + effect = "Allow" + resources = ["*"] + + principals { + type = "AWS" + identifiers = ["arn:aws:iam::${local.account_id}:root"] + } + } +} + +resource "aws_kms_key" "this" { + count = var.kms_key_id == "" ? 1 : 0 + + deletion_window_in_days = 7 + description = "Terraform State KMS key" + enable_key_rotation = true + policy = data.aws_iam_policy_document.key.json + + tags = merge(var.tags, { + "Name" = var.kms_alias_name != null ? var.kms_alias_name : "tf_backend_key" + }) +} + +resource "aws_kms_alias" "this" { + count = var.kms_key_id == "" ? 1 : 0 + + name = "alias/${var.kms_alias_name != "" ? var.kms_alias_name : "tf_backend_key"}" + target_key_id = aws_kms_key.this[0].id +} diff --git a/main.tf b/main.tf index 9315a53..c86f8d0 100644 --- a/main.tf +++ b/main.tf @@ -1,66 +1,122 @@ -resource "aws_s3_bucket" "config_bucket" { - bucket = var.bucket - acl = "log-delivery-write" - - tags = merge( - local.common_tags, - var.extra_tags, - { - "Name" = "tf-state" - }, - ) - - versioning { - enabled = true +data "aws_caller_identity" "current" {} + +data "aws_partition" "current" {} + +data "aws_region" "current" {} + +locals { + account_id = data.aws_caller_identity.current.account_id + partition = data.aws_partition.current.partition + region = data.aws_region.current.name + + # Resolve resource names + bucket_name = try(var.bucket_name, "${local.account_id}-${local.region}-tfstate") + kms_key_id = try(aws_kms_key.this[0].arn, var.kms_key_id) +} + +# tfsec is not yet smart enough to know new tf syntax for crypto/logging +#tfsec:ignore:AWS017 #tfsec:ignore:AWS002 +resource "aws_s3_bucket" "this" { + bucket = local.bucket_name + + tags = merge(var.tags, { + "Name" = local.bucket_name + }) +} + +resource "aws_s3_bucket_ownership_controls" "this" { + bucket = aws_s3_bucket.this.id + + rule { + object_ownership = "BucketOwnerEnforced" } +} + +resource "aws_s3_bucket_lifecycle_configuration" "this" { + count = var.lifecycle_rules == null ? 0 : 1 + + bucket = aws_s3_bucket.this.id - lifecycle_rule { - id = "expire" - enabled = true + dynamic "rule" { + iterator = rule + for_each = var.lifecycle_rules - noncurrent_version_expiration { - days = 90 + content { + id = rule.value.id + status = rule.value.enabled ? "Enabled" : "Disabled" + + filter { + prefix = lookup(rule.value, "prefix", null) + } + + expiration { + days = lookup(rule.value, "expiration", 2147483647) + } + + noncurrent_version_expiration { + noncurrent_days = lookup(rule.value, "noncurrent_version_expiration", 2147483647) + } } } - server_side_encryption_configuration { - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "aws:kms" - } + depends_on = [aws_s3_bucket_versioning.this] +} + +resource "aws_s3_bucket_logging" "this" { + count = var.logging_target_bucket == null ? 0 : 1 + + bucket = aws_s3_bucket.this.id + target_bucket = var.logging_target_bucket + target_prefix = var.logging_target_prefix +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "this" { + bucket = aws_s3_bucket.this.bucket + + rule { + apply_server_side_encryption_by_default { + kms_master_key_id = local.kms_key_id + sse_algorithm = "aws:kms" } } +} - logging { - target_bucket = var.bucket - target_prefix = "TFStateLogs/" +resource "aws_s3_bucket_versioning" "this" { + bucket = aws_s3_bucket.this.id + + versioning_configuration { + status = "Enabled" } } -resource "aws_s3_bucket_public_access_block" "block_public_access" { - bucket = aws_s3_bucket.config_bucket.id +resource "aws_s3_bucket_public_access_block" "this" { + bucket = aws_s3_bucket.this.id block_public_acls = true block_public_policy = true ignore_public_acls = true restrict_public_buckets = true } -resource "aws_dynamodb_table" "terraform_statelock" { - name = var.table +resource "aws_dynamodb_table" "this" { + name = var.dynamo_locktable_name billing_mode = "PAY_PER_REQUEST" hash_key = "LockID" + tags = merge(var.tags, { + "Name" = var.dynamo_locktable_name + }) + attribute { name = "LockID" type = "S" } - tags = merge( - local.common_tags, - var.extra_tags, - { - "Name" = "tf-locktable" - }, - ) -} + point_in_time_recovery { + enabled = var.dynamodb_point_in_time_recovery + } + server_side_encryption { + enabled = var.dynamodb_server_side_encryption + kms_key_arn = var.dynamodb_kms_key_arn + } +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..c7abb70 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,41 @@ + +output "external_id" { + description = "External ID attached to IAM role in managed accounts" + value = local.external_id +} + +output "kms_key_arn" { + description = "ARN of KMS Key for S3 bucket" + value = try(aws_kms_key.this[0].arn, var.kms_key_id) +} + +output "s3_bucket_backend" { + description = "S3 bucket used to store TF state" + value = aws_s3_bucket.this.bucket +} + +########################################## +# stubs +########################################## + +output "backend_config_stub" { + description = "Backend config stub to be used in child account(s)" + value = <
{
id = string
enabled = bool
prefix = string
expiration = number
noncurrent_version_expiration = number
}))