From 4828a3c0d07e13ddf5157f8c760b728f2c87002d Mon Sep 17 00:00:00 2001 From: Steven Black Date: Mon, 7 Dec 2020 16:44:44 -0500 Subject: [PATCH 01/13] fix workflow install script --- bin/install-macos.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/install-macos.sh b/bin/install-macos.sh index 4bc710b..3800165 100755 --- a/bin/install-macos.sh +++ b/bin/install-macos.sh @@ -14,5 +14,4 @@ git config --global init.templateDir ~/.git-template pre-commit init-templatedir ~/.git-template echo 'installing terraform with tfenv' -tfenv install min-required -tfenv use min-required +tfenv install From 36bf92e1c0662f3db16a020f81277359aaa588bf Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 10 May 2021 19:30:08 +0000 Subject: [PATCH 02/13] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/antonbabenko/pre-commit-terraform: v1.31.0 → v1.50.0](https://github.com/antonbabenko/pre-commit-terraform/compare/v1.31.0...v1.50.0) - [github.com/pre-commit/pre-commit-hooks: v3.0.0 → v3.4.0](https://github.com/pre-commit/pre-commit-hooks/compare/v3.0.0...v3.4.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index eafd12d..448955f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.31.0 + rev: v1.50.0 hooks: - id: terraform_docs always_run: true @@ -40,7 +40,7 @@ repos: files: \.tf(vars)?$ exclude: examples - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.0.0 + rev: v3.4.0 hooks: - id: check-case-conflict - id: check-json From 147fabc760fe74b971802aaf9ef7399ecbb03e77 Mon Sep 17 00:00:00 2001 From: Linter Bot Date: Mon, 10 May 2021 19:33:46 +0000 Subject: [PATCH 03/13] Apply automatic changes --- README.md | 47 ++++++++++++++++++++--------- examples/basic/README.md | 21 ++++++++++--- examples/external-logging/README.md | 19 +++++++++--- 3 files changed, 64 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 372cd7c..b29916e 100644 --- a/README.md +++ b/README.md @@ -58,34 +58,51 @@ region = "us-east-1" | Name | Version | |------|---------| -| terraform | >= 0.13 | -| aws | ~> 3.15.0 | +| [terraform](#requirement\_terraform) | >= 0.13 | +| [aws](#requirement\_aws) | ~> 3.15.0 | ## Providers | Name | Version | |------|---------| -| aws | ~> 3.15.0 | +| [aws](#provider\_aws) | ~> 3.15.0 | + +## 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_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | 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_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_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| allowed\_account\_ids | Account IDs that are allowed to access the bucket/KMS key | `list(string)` | `[]` | no | -| bucket | Name of bucket to create (do not provide if using `remote_bucket`) | `string` | `""` | no | -| kms\_alias\_name | Name of KMS Alias | `string` | `""` | no | -| kms\_key\_id | ARN for KMS key for all encryption operations. | `string` | `""` | no | -| logging\_target\_bucket | The name of the bucket that will receive the log objects | `string` | `null` | no | -| logging\_target\_prefix | A key prefix for log objects | `string` | `"TFStateLogs/"` | no | -| remote\_bucket | If specified, the remote bucket will be used for the backend. A new bucket will not be created | `string` | `""` | no | -| table | Name of Dynamo Table to create | `string` | `"tf-locktable"` | no | -| tags | Mapping of any extra tags you want added to resources | `map(string)` | `{}` | no | +| [allowed\_account\_ids](#input\_allowed\_account\_ids) | Account IDs that are allowed to access the bucket/KMS key | `list(string)` | `[]` | no | +| [bucket](#input\_bucket) | Name of bucket to create (do not provide if using `remote_bucket`) | `string` | `""` | no | +| [kms\_alias\_name](#input\_kms\_alias\_name) | Name of KMS Alias | `string` | `""` | no | +| [kms\_key\_id](#input\_kms\_key\_id) | ARN for KMS key for all encryption operations. | `string` | `""` | no | +| [logging\_target\_bucket](#input\_logging\_target\_bucket) | The name of the bucket that will receive the log objects | `string` | `null` | no | +| [logging\_target\_prefix](#input\_logging\_target\_prefix) | A key prefix for log objects | `string` | `"TFStateLogs/"` | no | +| [remote\_bucket](#input\_remote\_bucket) | If specified, the remote bucket will be used for the backend. A new bucket will not be created | `string` | `""` | no | +| [table](#input\_table) | Name of Dynamo Table to create | `string` | `"tf-locktable"` | no | +| [tags](#input\_tags) | Mapping of any extra tags you want added to resources | `map(string)` | `{}` | no | ## Outputs | Name | Description | |------|-------------| -| kms\_key\_arn | ARN of KMS Key for S3 bucket | -| s3\_bucket\_backend | S3 bucket | - +| [kms\_key\_arn](#output\_kms\_key\_arn) | ARN of KMS Key for S3 bucket | +| [s3\_bucket\_backend](#output\_s3\_bucket\_backend) | S3 bucket | diff --git a/examples/basic/README.md b/examples/basic/README.md index 04d1618..53ed62e 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -10,14 +10,27 @@ No requirements. | Name | Version | |------|---------| -| aws | n/a | +| [aws](#provider\_aws) | n/a | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [backend](#module\_backend) | ../.. | | +| [tags](#module\_tags) | rhythmictech/tags/terraform | 1.0.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | ## Inputs -No input. +No inputs. ## Outputs -No output. - +No outputs. diff --git a/examples/external-logging/README.md b/examples/external-logging/README.md index 7f087d1..ff91b9c 100644 --- a/examples/external-logging/README.md +++ b/examples/external-logging/README.md @@ -9,14 +9,25 @@ No requirements. ## Providers -No provider. +No providers. + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [backend](#module\_backend) | ../.. | | +| [bucket](#module\_bucket) | rhythmictech/s3logging-bucket/aws | 2.0.0 | +| [tags](#module\_tags) | rhythmictech/tags/terraform | 1.0.0 | + +## Resources + +No resources. ## Inputs -No input. +No inputs. ## Outputs -No output. - +No outputs. From d7772fa8f82e0f748896e368df23bb3f7d0590ea Mon Sep 17 00:00:00 2001 From: Cris Daniluk Date: Thu, 1 Sep 2022 14:07:43 -0400 Subject: [PATCH 04/13] update precommit checks --- .github/workflows/misspell.yaml | 23 +++++++ .github/workflows/pre-commit-check.yaml | 41 ------------- .github/workflows/pre-commit.yaml | 30 +++++++++ .github/workflows/pullRequest.yaml | 81 +++++++++++++++++++++++++ .github/workflows/tflint.yaml | 27 +++++++++ .github/workflows/tfsec.yaml | 26 ++++++++ .github/workflows/yamllint.yaml | 22 +++++++ .pre-commit-config.yaml | 44 +++++++++++--- .terraform-version | 2 +- .tflint.hcl | 3 +- .yamllint.yml | 2 + LICENSE | 2 +- bin/install-ubuntu.sh | 32 ++++++++++ 13 files changed, 280 insertions(+), 55 deletions(-) create mode 100644 .github/workflows/misspell.yaml delete mode 100644 .github/workflows/pre-commit-check.yaml create mode 100644 .github/workflows/pre-commit.yaml create mode 100644 .github/workflows/pullRequest.yaml create mode 100644 .github/workflows/tflint.yaml create mode 100644 .github/workflows/tfsec.yaml create mode 100644 .github/workflows/yamllint.yaml create mode 100644 .yamllint.yml create mode 100755 bin/install-ubuntu.sh 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-check.yaml b/.github/workflows/pre-commit-check.yaml deleted file mode 100644 index a0459a5..0000000 --- a/.github/workflows/pre-commit-check.yaml +++ /dev/null @@ -1,41 +0,0 @@ ---- -name: pre-commit-check -on: - push: - branches: - - master - - develop - pull_request: - -jobs: - build: - runs-on: macOS-latest - steps: - - uses: actions/checkout@v2 - with: - ref: ${{ github.head_ref }} - - name: Install prerequisites - run: ./bin/install-macos.sh - - name: initialize Terraform - run: terraform init --backend=false - - uses: actions/cache@v1 - with: - path: ~/.cache/pre-commit - key: pre-commit|${{ hashFiles('.pre-commit-config.yaml') }} - restore-keys: | - pre-commit - - name: pre-commit run all - run: | - pre-commit run -a - env: - AWS_DEFAULT_REGION: us-east-1 - SKIP: terraform_tflint_deep,no-commit-to-branch - - uses: stefanzweifel/git-auto-commit-action@v4 - if: ${{ failure() }} - with: - commit_message: Apply automatic changes - commit_options: "--no-verify" - # Optional commit user and author settings - commit_user_name: Linter Bot - commit_user_email: noreply@rhythmictech.com - commit_author: Linter Bot 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/.pre-commit-config.yaml b/.pre-commit-config.yaml index 448955f..88d34c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,17 +1,11 @@ +exclude: ".terraform" repos: - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.50.0 + rev: v1.72.1 hooks: - id: terraform_docs always_run: true - args: - - --args=--sort-by-required - id: terraform_fmt - - id: terraform_tflint - alias: terraform_tflint_deep - name: terraform_tflint_deep - args: - - --args=--deep - id: terraform_tflint alias: terraform_tflint_nocreds name: terraform_tflint_nocreds @@ -33,6 +27,28 @@ repos: 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=windows_amd64 -platform=darwin_amd64 -platform=linux_amd64 + cd .. done ' language: system @@ -40,7 +56,7 @@ repos: files: \.tf(vars)?$ exclude: examples - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.2.0 hooks: - id: check-case-conflict - id: check-json @@ -50,11 +66,17 @@ repos: args: - --unsafe - id: end-of-file-fixer - - id: trailing-whitespace - 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 @@ -63,3 +85,5 @@ repos: 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 index c37136a..4f20ea7 100644 --- a/.terraform-version +++ b/.terraform-version @@ -1 +1 @@ -0.13.5 +latest:^1.1 diff --git a/.tflint.hcl b/.tflint.hcl index 5cba22a..854fb92 100644 --- a/.tflint.hcl +++ b/.tflint.hcl @@ -1,6 +1,5 @@ config { module = true - deep_check = false } rule "terraform_deprecated_interpolation" { @@ -37,7 +36,7 @@ rule "terraform_naming_convention" { } rule "terraform_required_version" { - enabled = true + enabled = false } rule "terraform_required_providers" { 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 index 3fe5c34..5fe0374 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2020 Rhythmic Technologies, Inc. +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 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 From 4bba286992286db19fad8b9d799934e1d3eff417 Mon Sep 17 00:00:00 2001 From: Cris Daniluk Date: Thu, 1 Sep 2022 14:29:57 -0400 Subject: [PATCH 05/13] new approach to this module to focus on external acct mgmt --- .pre-commit-config.yaml | 2 +- .terraform.lock.hcl | 25 ++++++ README.md | 82 ++++++++------------ kms.tf | 37 +++------ main.tf | 164 ++++++++++++++++++---------------------- outputs.tf | 2 +- variables.tf | 49 +++++++----- versions.tf | 4 +- 8 files changed, 173 insertions(+), 192 deletions(-) create mode 100644 .terraform.lock.hcl diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 88d34c1..98dd84f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,7 +1,7 @@ exclude: ".terraform" repos: - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.72.1 + rev: v1.74.1 hooks: - id: terraform_docs always_run: true diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 0000000..23c96e6 --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# 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", + ] +} diff --git a/README.md b/README.md index b29916e..ac7bbd6 100644 --- a/README.md +++ b/README.md @@ -1,71 +1,47 @@ # terraform-aws-backend -[![](https://github.com/rhythmictech/terraform-aws-backend/workflows/pre-commit-check/badge.svg)](https://github.com/rhythmictech/terraform-aws-backend/actions) follow on Twitter -Creates a backend S3 bucket and DynamoDB table for managing Terraform state. Useful for bootstrapping a new -environment. This module supports cross-account state management, using a centralized account that holds the S3 bucket and KMS key. +[![tflint](https://github.com/rhythmictech/terraform-aws-backend/workflows/tflint/badge.svg?branch=master&event=push)](https://github.com/rhythmictech/terraform-aws-backend/actions?query=workflow%3Atflint+event%3Apush+branch%3Amaster) +[![tfsec](https://github.com/rhythmictech/terraform-aws-backend/workflows/tfsec/badge.svg?branch=master&event=push)](https://github.com/rhythmictech/terraform-aws-backend/actions?query=workflow%3Atfsec+event%3Apush+branch%3Amaster) +[![yamllint](https://github.com/rhythmictech/terraform-aws-backend/workflows/yamllint/badge.svg?branch=master&event=push)](https://github.com/rhythmictech/terraform-aws-backend/actions?query=workflow%3Ayamllint+event%3Apush+branch%3Amaster) +[![misspell](https://github.com/rhythmictech/terraform-aws-backend/workflows/misspell/badge.svg?branch=master&event=push)](https://github.com/rhythmictech/terraform-aws-backend/actions?query=workflow%3Amisspell+event%3Apush+branch%3Amaster) +[![pre-commit-check](https://github.com/rhythmictech/terraform-aws-backend/workflows/pre-commit-check/badge.svg?branch=master&event=push)](https://github.com/rhythmictech/terraform-aws-backend/actions?query=workflow%3Apre-commit-check+event%3Apush+branch%3Amaster) +follow on Twitter -_Note: A centralized DynamoDB locking table is not supported because terraform cannot assume more than one IAM role per execution._ +Creates a backend 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. 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, which is always preferred. + +*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. ## Usage ``` module "backend" { - source = "git::ssh://git@github.com/rhythmictech/terraform-aws-backend" + source = "rhythmictech/backend/aws" + bucket = "project-tfstate" region = "us-east-1" table = "tf-locktable" } - ``` ## Cross Account State Management -Managing state across accounts requires additional configuration to ensure that the S3 bucket is appropriately accessible and the KMS key is usable. - -The following module declaration will create an S3 bucket and KMS key that are accessible to the root account (and users with the AdministratorAccess managed role) in the target account: - -```yaml -module "backend" { - source = "git::ssh://git@github.com/rhythmictech/terraform-aws-backend" - allowed_account_ids = ["123456789012"] - bucket = "012345678901-us-east-1-tfstate" - region = "us-east-1" -} -``` - -In the target account, use this declaration to import the module: +To use this bucket to manage the state for other AWS accounts, you must create IAM roles in those accounts and allow the users who run Terraform to assume them. -```yaml -module "backend" { - source = "git::ssh://git@github.com/rhythmictech/terraform-aws-backend" - kms_key_id = "arn:aws:kms:us-east-1:012345678901:key/59381274-af42-8521-04af-ab0acfe3d521" - region = "us-east-1" - remote_bucket = "012345678901-us-east-1-tfstate" -} -``` - -The module will automatically write to the source account S3 bucket using the KMS key with cross-account access. - -Access to the source S3 bucket is done based on a prefix that matches the AWS Account ID. Therefore, target accounts must use a `workspace_key_prefix` that matches the account ID, such as in the following sample backend-config values: - -``` -bucket = "012345678901-us-east-1-tf-state" -key = "project.tfstate" -workspace_key_prefix = "123456789012" -region = "us-east-1" -``` +See [Use AssumeRole to Provision AWS Resources Across Accounts](https://learn.hashicorp.com/tutorials/terraform/aws-assumerole) for more information on this pattern. ## Requirements | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 0.13 | -| [aws](#requirement\_aws) | ~> 3.15.0 | +| [terraform](#requirement\_terraform) | >= 0.14 | +| [aws](#requirement\_aws) | >= 4.0 | ## Providers | Name | Version | |------|---------| -| [aws](#provider\_aws) | ~> 3.15.0 | +| [aws](#provider\_aws) | 4.28.0 | ## Modules @@ -79,23 +55,27 @@ No modules. | [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_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | +| [aws_s3_bucket_acl.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_acl) | 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_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.versioning_example](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | resource | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_canonical_user_id.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/canonical_user_id) | data source | | [aws_iam_policy_document.key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | -| [aws_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | 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 | |------|-------------|------|---------|:--------:| -| [allowed\_account\_ids](#input\_allowed\_account\_ids) | Account IDs that are allowed to access the bucket/KMS key | `list(string)` | `[]` | no | -| [bucket](#input\_bucket) | Name of bucket to create (do not provide if using `remote_bucket`) | `string` | `""` | no | -| [kms\_alias\_name](#input\_kms\_alias\_name) | Name of KMS Alias | `string` | `""` | no | -| [kms\_key\_id](#input\_kms\_key\_id) | ARN for KMS key for all encryption operations. | `string` | `""` | no | -| [logging\_target\_bucket](#input\_logging\_target\_bucket) | The name of the bucket that will receive the log objects | `string` | `null` | no | -| [logging\_target\_prefix](#input\_logging\_target\_prefix) | A key prefix for log objects | `string` | `"TFStateLogs/"` | no | -| [remote\_bucket](#input\_remote\_bucket) | If specified, the remote bucket will be used for the backend. A new bucket will not be created | `string` | `""` | no | +| [bucket\_name](#input\_bucket\_name) | Name of bucket to create | `string` | n/a | yes | +| [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) |
list(object(
{
id = string
enabled = bool
prefix = string
expiration = number
noncurrent_version_expiration = number
}))
|
[
{
"enabled": true,
"expiration": 90,
"id": "tfstate-expire",
"noncurrent_version_expiration": 90,
"prefix": 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 | | [table](#input\_table) | Name of Dynamo Table to create | `string` | `"tf-locktable"` | no | | [tags](#input\_tags) | Mapping of any extra tags you want added to resources | `map(string)` | `{}` | no | diff --git a/kms.tf b/kms.tf index 2664e51..f41ff24 100644 --- a/kms.tf +++ b/kms.tf @@ -3,49 +3,30 @@ data "aws_iam_policy_document" "key" { actions = ["kms:*"] effect = "Allow" resources = ["*"] + principals { type = "AWS" identifiers = ["arn:aws:iam::${local.account_id}:root"] } } - - dynamic "statement" { - for_each = var.allowed_account_ids - - content { - effect = "Allow" - resources = ["*"] - actions = [ - "kms:Encrypt*", - "kms:Decrypt*", - "kms:ReEncrypt*", - "kms:GenerateDataKey*", - "kms:DescribeKey", - ] - principals { - type = "AWS" - identifiers = ["arn:aws:iam::${statement.value}:root"] - } - } - } } resource "aws_kms_key" "this" { - count = var.kms_key_id == "" ? 1 : 0 + 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( - { - "Name" = var.kms_alias_name != "" ? var.kms_alias_name : "tf_backend_key" - }, - var.tags - ) + + 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 + 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 e226817..25522b0 100644 --- a/main.tf +++ b/main.tf @@ -1,62 +1,95 @@ data "aws_caller_identity" "current" {} - +data "aws_region" "current" {} locals { account_id = data.aws_caller_identity.current.account_id - - # Account IDs that will have access to stream CloudTrail logs - account_ids = concat([local.account_id], var.allowed_account_ids) - - # Format account IDs into necessary resource lists. - iam_account_principals = formatlist("arn:aws:iam::%s:root", local.account_ids) + region = data.aws_region.current.name # Resolve resource names - bucket = try(aws_s3_bucket.this[0].id, var.remote_bucket) - kms_key_id = try(aws_kms_key.this[0].arn, var.kms_key_id) + 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) } +data "aws_canonical_user_id" "current" {} + +# 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" { - count = var.remote_bucket == "" ? 1 : 0 - bucket = var.bucket - acl = "log-delivery-write" - tags = merge( - var.tags, - { - "Name" = var.bucket - }, - ) - - versioning { - enabled = true - } + bucket = local.bucket_name + + tags = merge(var.tags, { + "Name" = local.bucket_name + }) +} - lifecycle_rule { - id = "expire" - enabled = true +resource "aws_s3_bucket_acl" "this" { + bucket = aws_s3_bucket.this.id - noncurrent_version_expiration { - days = 90 + access_control_policy { + owner { + id = data.aws_canonical_user_id.current.id } } +} + +resource "aws_s3_bucket_lifecycle_configuration" "this" { + count = var.lifecycle_rules == null ? 0 : 1 - server_side_encryption_configuration { - rule { - apply_server_side_encryption_by_default { - sse_algorithm = "aws:kms" - kms_master_key_id = local.kms_key_id + bucket = aws_s3_bucket.this.id + + dynamic "rule" { + iterator = rule + for_each = var.lifecycle_rules + + 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) } } } - logging { - target_bucket = coalesce(var.logging_target_bucket, var.bucket) - target_prefix = var.logging_target_prefix + 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" + } + } +} + +resource "aws_s3_bucket_versioning" "this" { + bucket = aws_s3_bucket.this.id + + versioning_configuration { + status = "Enabled" } } resource "aws_s3_bucket_public_access_block" "this" { - depends_on = [aws_s3_bucket_policy.this] - count = var.remote_bucket == "" ? 1 : 0 - bucket = aws_s3_bucket.this[0].id + bucket = aws_s3_bucket.this.id block_public_acls = true block_public_policy = true ignore_public_acls = true @@ -68,59 +101,12 @@ resource "aws_dynamodb_table" "this" { billing_mode = "PAY_PER_REQUEST" hash_key = "LockID" + tags = merge(var.tags, { + "Name" = var.table + }) + attribute { name = "LockID" type = "S" } - - tags = merge( - var.tags, - { - "Name" = "tf-locktable" - }, - ) -} - -data "aws_iam_policy_document" "this" { - statement { - effect = "Allow" - - actions = [ - "s3:ListBucket" - ] - - resources = [ - "arn:aws:s3:::${local.bucket}" - ] - - principals { - type = "AWS" - identifiers = local.iam_account_principals - } - } - - dynamic "statement" { - for_each = var.allowed_account_ids - - content { - effect = "Allow" - resources = ["arn:aws:s3:::${local.bucket}/${statement.value}/*"] - actions = [ - "s3:GetObject", - "s3:PutObject", - "s3:DeleteObject" - ] - - principals { - type = "AWS" - identifiers = ["arn:aws:iam::${statement.value}:root"] - } - } - } -} - -resource "aws_s3_bucket_policy" "this" { - count = var.remote_bucket == "" ? 1 : 0 - bucket = aws_s3_bucket.this[0].id - policy = data.aws_iam_policy_document.this.json } diff --git a/outputs.tf b/outputs.tf index e3dde8a..e826e19 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,6 +1,6 @@ output "s3_bucket_backend" { description = "S3 bucket" - value = try(aws_s3_bucket.this[0].bucket, var.remote_bucket) + value = aws_s3_bucket.this.bucket } output "kms_key_arn" { diff --git a/variables.tf b/variables.tf index b7ecb96..ee8f718 100644 --- a/variables.tf +++ b/variables.tf @@ -1,44 +1,53 @@ -variable "allowed_account_ids" { - default = [] - description = "Account IDs that are allowed to access the bucket/KMS key" - type = list(string) -} - -variable "bucket" { - default = "" - description = "Name of bucket to create (do not provide if using `remote_bucket`)" +variable "bucket_name" { + description = "Name of bucket to create" type = string } + variable "kms_alias_name" { - default = "" + default = null description = "Name of KMS Alias" type = string } variable "kms_key_id" { - default = "" - description = "ARN for KMS key for all encryption operations." + default = null + description = "ARN for KMS key for all encryption operations (a key will be created if this is not provided)" type = string } +variable "lifecycle_rules" { + description = "lifecycle rules to apply to the bucket (set to null to skip lifecycle rules)" + + default = [{ + id = "tfstate-expire" + enabled = true + expiration = 90 + noncurrent_version_expiration = 90 + prefix = null + }] + + type = list(object( + { + id = string + enabled = bool + prefix = string + expiration = number + noncurrent_version_expiration = number + })) +} + variable "logging_target_bucket" { default = null - description = "The name of the bucket that will receive the log objects" + description = "The name of the bucket that will receive the log objects (logging will be disabled if null)" type = string } variable "logging_target_prefix" { - default = "TFStateLogs/" + default = null description = "A key prefix for log objects" type = string } -variable "remote_bucket" { - default = "" - description = "If specified, the remote bucket will be used for the backend. A new bucket will not be created" - type = string -} - variable "table" { default = "tf-locktable" description = "Name of Dynamo Table to create" diff --git a/versions.tf b/versions.tf index 0c24762..1740c7d 100644 --- a/versions.tf +++ b/versions.tf @@ -1,11 +1,11 @@ terraform { - required_version = ">= 0.13" + required_version = ">= 0.14" required_providers { aws = { source = "hashicorp/aws" - version = "~> 3.15.0" + version = ">= 4.0" } } } From 5863db6829b572e0e401f8b48bcdbde8cbb6a3a1 Mon Sep 17 00:00:00 2001 From: Cris Daniluk Date: Thu, 1 Sep 2022 17:29:29 -0400 Subject: [PATCH 06/13] add support for creating cfn stack to generate assumable role --- .gitignore | 3 ++ .pre-commit-config.yaml | 2 +- .terraform.lock.hcl | 44 ++++++++++++++++++++++++++ README.md | 22 ++++++++++--- assumerole.tf | 36 ++++++++++++++++++++++ main.tf | 26 +++++++++------- template/addrole.sh.tftpl | 1 + template/tfassumerole.cfn.yml.tftpl | 26 ++++++++++++++++ variables.tf | 48 ++++++++++++++++++++++++----- versions.tf | 10 ++++++ 10 files changed, 194 insertions(+), 24 deletions(-) create mode 100644 assumerole.tf create mode 100644 template/addrole.sh.tftpl create mode 100644 template/tfassumerole.cfn.yml.tftpl diff --git a/.gitignore b/.gitignore index 428217a..18f9c57 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ # .tfstate files *.tfstate *.tfstate.* + +assumerole +assumerole/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 98dd84f..5ba9f36 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -47,7 +47,7 @@ repos: for DIR in $(printf "%s\n" "${DIRS[@]}" | sort -u) do cd $(dirname "$FILE") - terraform providers lock -platform=windows_amd64 -platform=darwin_amd64 -platform=linux_amd64 + terraform providers lock -platform=linux_amd64 cd .. done ' diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index 23c96e6..5734069 100644 --- a/.terraform.lock.hcl +++ b/.terraform.lock.hcl @@ -23,3 +23,47 @@ provider "registry.terraform.io/hashicorp/aws" { "zh:f5cee910b4db2da3c2a28dae9055cbca4273eb774c362bb7bb5bde04deff4557", ] } + +provider "registry.terraform.io/hashicorp/local" { + version = "2.2.3" + 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" + 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/README.md b/README.md index ac7bbd6..978bb8b 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,16 @@ See [Use AssumeRole to Provision AWS Resources Across Accounts](https://learn.ha |------|---------| | [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 @@ -55,28 +59,36 @@ No modules. | [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_acl.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_acl) | 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.versioning_example](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_versioning) | 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_canonical_user_id.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/canonical_user_id) | 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 | |------|-------------|------|---------|:--------:| -| [bucket\_name](#input\_bucket\_name) | Name of bucket to create | `string` | n/a | yes | +| [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)` |
[
"arn:aws:iam::aws:policy/AdministratorAccess"
]
| 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 | +| [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 | | [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) |
list(object(
{
id = string
enabled = bool
prefix = string
expiration = number
noncurrent_version_expiration = number
}))
|
[
{
"enabled": true,
"expiration": 90,
"id": "tfstate-expire",
"noncurrent_version_expiration": 90,
"prefix": 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 | -| [table](#input\_table) | Name of Dynamo Table to create | `string` | `"tf-locktable"` | no | | [tags](#input\_tags) | Mapping of any extra tags you want added to resources | `map(string)` | `{}` | no | ## Outputs diff --git a/assumerole.tf b/assumerole.tf new file mode 100644 index 0000000..44ef677 --- /dev/null +++ b/assumerole.tf @@ -0,0 +1,36 @@ +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 = "${path.module}/assumerole/addrole.sh" + + content = templatefile("${path.module}/template/addrole.sh.tftpl", { + stack_name = var.assumerole_stack_name + }) + +} + +resource "local_sensitive_file" "assumerole_tfassumerole" { + count = var.create_assumerole_template ? 1 : 0 + + filename = "${path.module}/assumerole/tfassumerole.cfn.yml" + + 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/main.tf b/main.tf index 25522b0..236eab6 100644 --- a/main.tf +++ b/main.tf @@ -1,7 +1,15 @@ -data "aws_caller_identity" "current" {} -data "aws_region" "current" {} +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 @@ -9,8 +17,6 @@ locals { kms_key_id = try(aws_kms_key.this[0].arn, var.kms_key_id) } -data "aws_canonical_user_id" "current" {} - # 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" { @@ -21,13 +27,11 @@ resource "aws_s3_bucket" "this" { }) } -resource "aws_s3_bucket_acl" "this" { +resource "aws_s3_bucket_ownership_controls" "this" { bucket = aws_s3_bucket.this.id - access_control_policy { - owner { - id = data.aws_canonical_user_id.current.id - } + rule { + object_ownership = "BucketOwnerEnforced" } } @@ -97,12 +101,12 @@ resource "aws_s3_bucket_public_access_block" "this" { } resource "aws_dynamodb_table" "this" { - name = var.table + name = var.dynamo_locktable_name billing_mode = "PAY_PER_REQUEST" hash_key = "LockID" tags = merge(var.tags, { - "Name" = var.table + "Name" = var.dynamo_locktable_name }) attribute { diff --git a/template/addrole.sh.tftpl b/template/addrole.sh.tftpl new file mode 100644 index 0000000..a83fc99 --- /dev/null +++ b/template/addrole.sh.tftpl @@ -0,0 +1 @@ +aws cloudformation create-stack --capabilities CAPABILITY_NAMED_IAM --template-body file://tfassumerole.cfn.yml --stack-name ${stack_name} diff --git a/template/tfassumerole.cfn.yml.tftpl b/template/tfassumerole.cfn.yml.tftpl new file mode 100644 index 0000000..ab71161 --- /dev/null +++ b/template/tfassumerole.cfn.yml.tftpl @@ -0,0 +1,26 @@ +AWSTemplateFormatVersion: '2010-09-09' +Description: + Creates a role that can be assumed by the designated account for terraform runs + +Resources: + FullAdminRole: + Type: 'AWS::IAM::Role' + Properties: + RoleName: ${role_name} + MaxSessionDuration: 7200 + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + AWS: 'arn:${partition}:iam::${parent_account_id}:root' + Action: + - sts:AssumeRole + Condition: + StringEquals: + sts:ExternalId: ${external_id} + Path: '/' + ManagedPolicyArns: +%{ for policy in policy_arns ~} + - ${policy} +%{ endfor ~} diff --git a/variables.tf b/variables.tf index ee8f718..f43dd18 100644 --- a/variables.tf +++ b/variables.tf @@ -1,5 +1,11 @@ variable "bucket_name" { - description = "Name of bucket to create" + description = "Name of bucket to hold tf state" + type = string +} + +variable "dynamo_locktable_name" { + default = "tf-locktable" + description = "Name of lock table for terraform" type = string } @@ -48,14 +54,42 @@ variable "logging_target_prefix" { type = string } -variable "table" { - default = "tf-locktable" - description = "Name of Dynamo Table to create" - type = string -} - variable "tags" { default = {} description = "Mapping of any extra tags you want added to resources" type = map(string) } + +######################################## +# Assume Role Template Vars +######################################## +variable "create_assumerole_template" { + default = false + description = "If true, create a CloudFormation template that can be run against accounts to create an assumable role" + type = bool +} + + +variable "assumerole_role_name" { + default = "Terraform" + description = "Name of role to create in assumerole template" + type = string +} + +variable "assumerole_role_external_id" { + default = null + description = "External ID to attach to role (this is required, a random ID will be generated if not specified here)" + type = string +} + +variable "assumerole_role_attach_policies" { + default = ["arn:aws:iam::aws:policy/AdministratorAccess"] + description = "Policy ARNs to attach to role (can be managed or custom but must exist)" + type = list(string) +} + +variable "assumerole_stack_name" { + default = "tf-assumerole" + description = "Name of CloudFormation stack" + type = string +} diff --git a/versions.tf b/versions.tf index 1740c7d..dad6472 100644 --- a/versions.tf +++ b/versions.tf @@ -7,5 +7,15 @@ terraform { source = "hashicorp/aws" version = ">= 4.0" } + + local = { + source = "hashicorp/local" + version = ">= 2.0" + } + + random = { + source = "hashicorp/random" + version = ">= 3.0" + } } } From 1de9da2387a8fd5a34199ac7779e1b6bb192dc36 Mon Sep 17 00:00:00 2001 From: Cris Daniluk Date: Thu, 1 Sep 2022 17:41:21 -0400 Subject: [PATCH 07/13] fix pathname --- assumerole.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assumerole.tf b/assumerole.tf index 44ef677..7f24d91 100644 --- a/assumerole.tf +++ b/assumerole.tf @@ -5,7 +5,7 @@ locals { resource "local_file" "assumerole_addrole" { count = var.create_assumerole_template ? 1 : 0 - filename = "${path.module}/assumerole/addrole.sh" + filename = "assumerole/addrole.sh" content = templatefile("${path.module}/template/addrole.sh.tftpl", { stack_name = var.assumerole_stack_name @@ -16,7 +16,7 @@ resource "local_file" "assumerole_addrole" { resource "local_sensitive_file" "assumerole_tfassumerole" { count = var.create_assumerole_template ? 1 : 0 - filename = "${path.module}/assumerole/tfassumerole.cfn.yml" + filename = "assumerole/tfassumerole.cfn.yml" content = templatefile("${path.module}/template/tfassumerole.cfn.yml.tftpl", { external_id = local.external_id From 63a692491d6fdd2f3983d7c0f43d96ee652ea845 Mon Sep 17 00:00:00 2001 From: Cris Daniluk Date: Tue, 20 Sep 2022 14:57:49 -0400 Subject: [PATCH 08/13] remove stupid lifecycle rule --- README.md | 2 +- variables.tf | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 978bb8b..96c26f3 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ No modules. | [dynamo\_locktable\_name](#input\_dynamo\_locktable\_name) | Name of lock table for terraform | `string` | `"tf-locktable"` | 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) |
list(object(
{
id = string
enabled = bool
prefix = string
expiration = number
noncurrent_version_expiration = number
}))
|
[
{
"enabled": true,
"expiration": 90,
"id": "tfstate-expire",
"noncurrent_version_expiration": 90,
"prefix": null
}
]
| no | +| [lifecycle\_rules](#input\_lifecycle\_rules) | lifecycle rules to apply to the bucket (set to null to skip lifecycle rules) |
list(object(
{
id = string
enabled = bool
prefix = string
expiration = number
noncurrent_version_expiration = number
}))
| `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 | diff --git a/variables.tf b/variables.tf index f43dd18..b90aad0 100644 --- a/variables.tf +++ b/variables.tf @@ -22,16 +22,9 @@ variable "kms_key_id" { } variable "lifecycle_rules" { + default = null description = "lifecycle rules to apply to the bucket (set to null to skip lifecycle rules)" - default = [{ - id = "tfstate-expire" - enabled = true - expiration = 90 - noncurrent_version_expiration = 90 - prefix = null - }] - type = list(object( { id = string From 5baab5dcbfed95da61524fe6e349e5339b2b47c9 Mon Sep 17 00:00:00 2001 From: Cris Daniluk Date: Wed, 21 Sep 2022 14:11:54 -0400 Subject: [PATCH 09/13] updates to examples/docs per pr feedback --- README.md | 7 ++- examples/basic/README.md | 28 +++++++----- examples/basic/main.tf | 36 ++------------- examples/external-logging/README.md | 33 -------------- examples/external-logging/main.tf | 37 ---------------- examples/workspaces/README.md | 68 +++++++++++++++++++++++++++++ examples/workspaces/main.tf | 7 +++ outputs.tf | 11 +++-- 8 files changed, 110 insertions(+), 117 deletions(-) delete mode 100644 examples/external-logging/README.md delete mode 100644 examples/external-logging/main.tf create mode 100644 examples/workspaces/README.md create mode 100644 examples/workspaces/main.tf diff --git a/README.md b/README.md index 96c26f3..2b799dd 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,10 @@ To use this bucket to manage the state for other AWS accounts, you must create I 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). + +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 this backend. It creates an IAM role with the AdministratorAccess policy attached and with an External ID. + ## Requirements @@ -95,6 +99,7 @@ No modules. | Name | Description | |------|-------------| +| [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 | -| [s3\_bucket\_backend](#output\_s3\_bucket\_backend) | S3 bucket | +| [s3\_bucket\_backend](#output\_s3\_bucket\_backend) | S3 bucket used to store TF state | diff --git a/examples/basic/README.md b/examples/basic/README.md index 53ed62e..22e2bf0 100644 --- a/examples/basic/README.md +++ b/examples/basic/README.md @@ -1,5 +1,17 @@ -# Basic Backend Example -Creates resources for a secure backend in AWS +# 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 @@ -8,23 +20,17 @@ No requirements. ## Providers -| Name | Version | -|------|---------| -| [aws](#provider\_aws) | n/a | +No providers. ## Modules | Name | Source | Version | |------|--------|---------| -| [backend](#module\_backend) | ../.. | | -| [tags](#module\_tags) | rhythmictech/tags/terraform | 1.0.0 | +| [tfstate](#module\_tfstate) | ../.. | n/a | ## Resources -| Name | Type | -|------|------| -| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | -| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | +No resources. ## Inputs diff --git a/examples/basic/main.tf b/examples/basic/main.tf index e6907b2..154befc 100644 --- a/examples/basic/main.tf +++ b/examples/basic/main.tf @@ -1,35 +1,7 @@ -data "aws_caller_identity" "current" {} -data "aws_region" "current" {} - -locals { - env = "sandbox" - name = "example" - namespace = "aws-rhythmic-sandbox" - owner = "Rhythmictech Engineering" - - extra_tags = { - delete_me = "please" - purpose = "testing" - } -} - -module "tags" { - source = "rhythmictech/tags/terraform" - version = "1.0.0" - - names = [local.name, local.env, local.namespace] - - tags = merge({ - "Env" = local.env, - "Namespace" = local.namespace, - "Owner" = local.owner - }, local.extra_tags) -} - -module "backend" { +module "tfstate" { source = "../.." - bucket = "${data.aws_caller_identity.current.account_id}-${data.aws_region.current.name}-${module.tags.name}" - kms_alias_name = "${data.aws_caller_identity.current.account_id}-${data.aws_region.current.name}-${module.tags.name}" - tags = module.tags.tags + bucket_name = "tf-state-remote" + create_assumerole_template = true + dynamo_locktable_name = "tf-locktable-remote" } diff --git a/examples/external-logging/README.md b/examples/external-logging/README.md deleted file mode 100644 index ff91b9c..0000000 --- a/examples/external-logging/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Backend Example with external Logging -Creates resources for a secure backend in AWS - - - -## Requirements - -No requirements. - -## Providers - -No providers. - -## Modules - -| Name | Source | Version | -|------|--------|---------| -| [backend](#module\_backend) | ../.. | | -| [bucket](#module\_bucket) | rhythmictech/s3logging-bucket/aws | 2.0.0 | -| [tags](#module\_tags) | rhythmictech/tags/terraform | 1.0.0 | - -## Resources - -No resources. - -## Inputs - -No inputs. - -## Outputs - -No outputs. - diff --git a/examples/external-logging/main.tf b/examples/external-logging/main.tf deleted file mode 100644 index 6790598..0000000 --- a/examples/external-logging/main.tf +++ /dev/null @@ -1,37 +0,0 @@ - -locals { - env = "sandbox" - name = "example" - namespace = "aws-rhythmic-sandbox" - owner = "Rhythmictech Engineering" - region = "us-east-1" - - extra_tags = { - delete_me = "please" - purpose = "testing" - } -} - -module "tags" { - source = "rhythmictech/tags/terraform" - version = "1.0.0" - - names = [local.name, local.env, local.namespace] - - tags = merge({ - "Env" = local.env, - "Namespace" = local.namespace, - "Owner" = local.owner - }, local.extra_tags) -} - -module "bucket" { - source = "rhythmictech/s3logging-bucket/aws" - version = "2.0.0" - bucket_suffix = "tfstate-logging" -} - -module "backend" { - source = "../.." - logging_target_bucket = module.bucket.s3_bucket_name -} 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/outputs.tf b/outputs.tf index e826e19..6135e5a 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,9 +1,14 @@ -output "s3_bucket_backend" { - description = "S3 bucket" - value = aws_s3_bucket.this.bucket +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 +} From ad8bf6d7f147c9e45eeef851c9a9f553c734d8b4 Mon Sep 17 00:00:00 2001 From: Steven Black Date: Thu, 22 Dec 2022 13:38:35 -0500 Subject: [PATCH 10/13] fix #14, appease tfsec --- .terraform.lock.hcl | 6 ++++-- README.md | 3 +++ main.tf | 18 ++++++++++++------ variables.tf | 18 ++++++++++++++++++ 4 files changed, 37 insertions(+), 8 deletions(-) diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl index 5734069..3d75310 100644 --- a/.terraform.lock.hcl +++ b/.terraform.lock.hcl @@ -25,7 +25,8 @@ provider "registry.terraform.io/hashicorp/aws" { } provider "registry.terraform.io/hashicorp/local" { - version = "2.2.3" + version = "2.2.3" + constraints = ">= 2.0.0" hashes = [ "h1:3bH88Z7tlWvcoubm6hQUBk3s9bSIJC8bVHQz749B87E=", "h1:FvRIEgCmAezgZUqb2F+PZ9WnSSnR5zbEM2ZI+GLmbMk=", @@ -47,7 +48,8 @@ provider "registry.terraform.io/hashicorp/local" { } provider "registry.terraform.io/hashicorp/random" { - version = "3.4.1" + version = "3.4.1" + constraints = ">= 3.0.0" hashes = [ "h1:NfxR2vBvEEfI0nA7FiL+QMuWFnC6rl2avzQ9+ykoSo4=", "h1:Q+1ZwDtRYtFZLrPfpe9wnz7ulNvotLfM6WmwXFCGnSA=", diff --git a/README.md b/README.md index 2b799dd..3d0f371 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,9 @@ No modules. | [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) |
list(object(
{
id = string
enabled = bool
prefix = string
expiration = number
noncurrent_version_expiration = number
}))
| `null` | no | diff --git a/main.tf b/main.tf index 236eab6..c86f8d0 100644 --- a/main.tf +++ b/main.tf @@ -1,11 +1,8 @@ -data "aws_caller_identity" "current" { -} +data "aws_caller_identity" "current" {} -data "aws_partition" "current" { -} +data "aws_partition" "current" {} -data "aws_region" "current" { -} +data "aws_region" "current" {} locals { account_id = data.aws_caller_identity.current.account_id @@ -113,4 +110,13 @@ resource "aws_dynamodb_table" "this" { name = "LockID" type = "S" } + + 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/variables.tf b/variables.tf index b90aad0..dbb9040 100644 --- a/variables.tf +++ b/variables.tf @@ -9,6 +9,24 @@ variable "dynamo_locktable_name" { type = string } +variable "dynamodb_kms_key_arn" { + default = null + description = "KMS key arn to enable encryption on dynamodb table. Defaults to `alias/aws/dynamodb`" + type = string +} + +variable "dynamodb_point_in_time_recovery" { + default = true + description = "DynamoDB point-in-time recovery." + type = bool +} + +variable "dynamodb_server_side_encryption" { + default = true + description = "Bool to enable encryption on dynamodb table" + type = bool +} + variable "kms_alias_name" { default = null description = "Name of KMS Alias" From f00b1026f10a27f3381da4b2afe8757857009c61 Mon Sep 17 00:00:00 2001 From: Steven B Date: Thu, 22 Dec 2022 14:44:06 -0500 Subject: [PATCH 11/13] add docs (#16) --- .github/terraform-backend.drawio.png | Bin 0 -> 62576 bytes README.md | 72 +++++++++++++++++++++++---- outputs.tf | 27 ++++++++++ 3 files changed, 89 insertions(+), 10 deletions(-) create mode 100644 .github/terraform-backend.drawio.png diff --git a/.github/terraform-backend.drawio.png b/.github/terraform-backend.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..dcad3798b4c9ff523b176a44746fe93d56361b36 GIT binary patch literal 62576 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

b^Up`(`#TyZaY3&jl|)G;c#> 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°=US!e$Axrgg&HAGU@uUN89*R%YUXRE^CAi{Y@LE#~YjoCw|@&0}wA~Fd! ze9iILc}NNCLHObr?H#ZWbGL@zx~gYbHdF!npjUQF2;!XQAOUeCH(deqf5q(T-ryYO z^`$}$oI|3Pv(Qj0?e%o&hE)xItNRB3RkV|VS||372zq+>q`4V^cldyV;bd2%QC zJvfMqRaPE7;{h!Xd)hBoA^#);-MX`H7%kk?M#m!^y*rxEQim2M(}3ayvz5bR-(?xq zcAQUd&};p3(>u-1V`jagZ))>LS)a{N>9+Pn<5r}H)r}mh;aN-XQRBWB{iXYo>oQ_~4L7()+!R6qE zg5?rR#oZA_Y5$YGtXJ^f{_zBjTD!ajM}c^!J7ls%-m60Q(Jy()R>4bN&a>$eaU+p0 zzIzII|8>xAC(tyAJeO8*gUrt$U{GO$*Q*Pv_NTMLt(YjRrEr)n=5rN+c!UoFg!_hk zq{4&AXKgx(Vltq3Cg~hamFq1}XxA+JU0j#L=;`n?%PfvcCWJF%k*Q~C!ervEQL~9V zYetCGjgL0H=44%eyq7zD#@>A_O4)y*z=I1U5rG1+o6|v0P;&s$6Gyt+Jey^bYVn!; zZJNYGS?t>@bns5w!fKUn5}QL;1}apR-1qpYD7=)IoL2 z$RY=wgmI1yXh~1l(j?*adauZU&yhJep|mDJD`4WA`j67Frv=&hwu*Sy^G>|LJff`Z zne0TPd`R|=={$eQU+czHI&0BIM7_{9NZ*WeAGjYZPZ*UuLVSVCAWMqwjdqVvp#tIU zV5`A}`1`gvnlEl;U}@K9ZY{|{PJEg(@i$oAuOJ>F=Q4@cn1oo%A>67>h?aq$g-_3Z z2`I9)6t9R&u-Z|3*OxiK7yPgrdMo<@3-U{gJ?xXV8{qAdcPRXN>4;9TaXA9T*Bf`) z(?HjC8D#$xP)6b%o ztUlFmo`Z^^sk|~Vw^p!8nb<^~$fou zeiY28y4rLgX_ulaPZ6_v&{YPo9FCJ8uioVzJ=uEk_{D?U!phD(6$f=hW5w{P>uZ}| zS5vtLb7AIFlj6J9Br&WCAmAbIg%OIaO0S1ygX2bcRV_rYyHsR5zM)6PUk2V+5)^fk zY_47T72wVrM_-$E&HP;@*ne}yEOz2% z4fq`d2`E^>@OTs{s)!8dFW0XD#s|AKq32CE#qy@?|Dy%K3Sgz1j(aL~TE)+Ryb+S7 zTrYWPRW^>RQ#f}^yzYJ#JjDY9J;u-F4K#ZZY_%@O`Jdux$8@AyzX1b zCcwg-;3E{kjoEFapB@xL>LJFJC(cE6&Ins7Tfn8W>xxV1xwir`Z`5XKc_cddesiCf zxCB*d*wmlFyXwa?zCJM^ga!OIo_>!>(I+UNvP7MRfve2@>2bE<5-3YoJL@3w%5!f{ zh6sD-tzy}3!Yh>6WS@PE$KBvr0ic64@qc)*25ml0!mbqF!WDKj7o{Kt;=1`HfKJfc z%`=I-aSX|~FrTGD<|)*fD18QmT0$&?SY0Rl96Q2s3iH)hR0>KXy}7@zB7XW5cD#w` z20nun`D^Kdlmt&x%Qqtd_zG1spq!WIR-JbdkrkUFP7F&;pu+W|t&r~u9;tk$W@ zq&wL7C#7$?u9G@tl}A%~;`9Mmut~f8He{x7>aU=^xk1}Z*tM8E59G%qKx!|C`1RZ*dEMz_Q~gWCh2zQiC_3vNE`+o=fYjMxS7o;ABko zhrs-!0yi zb-KW|oZUMrU|lX6?Aqw*0rCX`#@-~&9!>dfX#{;}Nqr>`ts@>Zs9tGGw0PRsAeuv? zs#z?SgX+mB^KRowH;({I)%PSL`6hs~=}fGcc=RNgN+>LRTjMW`4dW&=|-}f)BXDs{&jGy)I`KB zTOPbk?q!)?*L8`R^%G#7vom=pn5ifNPUFLmMjv!RC2*T0oD#c%4##%Cla4j}^y=D7 zWG(@`T<1@smt|rmv^^61D%u(Hb}xe2uJM^q9OZU8s(0U4J9%_(1>eP6W~edl_c8i4 zd3B!H-}?ixKylSunEhUS;OGgQn5|iJ0Jidu1)F&{nvfSjHkR$FkBoa#M0Vdw_W|ze zkR!T4+^wO$(&8`Ewg+_DZVGD}vmW5rH_k{&v3GbD_yQK*KGcDiKRKTh`F1DRLZDf; ztH9U>!I|}j{4Y3@!2w>g(?>33=`3J(TL#$VGyll{Z4!J0k%L2Mp#Ik1{NF#h3F@p( zoze?RD#%3$lLb%tzg5w{7avP8BrTr!`QK^^!h4>8g*lOJe$K`YwV40o9fW@N4SbqU z%}Mxj4Z{CZ*`}bAD;7H4Mj#h}0 z>VIy?f2>-v|4*OgRPzFC1OLOS{{1x1yk?TtZD0>EgzPx{2t%6$yVq`$Uc+=CzQdpl zKn~CM4U_Vv~`FZ-ok$UFJ}`&S^retYAfJxl@7$8Kr=>n1~96;VB>7d`(A4*Yu!D>~pv zx=HwZ>rc@9#a>heXpaA7XoW7U7`zJK2CHY+?uJ;&Qh>Mq-#^RCY2F1_ly}g+UH=X0 zF-OSy+|QvuzF99Od&>n>44$oBrU&JgZFG;<&lV_AX9MXMdJ`aS3%|)(e*lc-)h+v_ zqefTa(9k>T^=?bHY7MW ztu?0Gi-1RF1Y7~E+}1PDH#6x+FaVv~H1LhJ-Dh~B=XWxYesgNS_&FT}35bFCwV{(Y zakXV?H*2$MfNg}B5(`{|3XuvI+p%pR6i&_)3ADwC!+{8*PZZd|%hTy7U^10kt&;#; z3{^wZN9dlfK!p?ZbOfOW$;*J^3JUz;-Z??DW7U>Z?C$+eF)8$1QaCN>X-|7Uy0&nrv0OkRUlb#7kvTFMjEB_wt5B6VSz1OoE zO`t33m#beCcHswAOWm9yFs_cQ_B8+bk3lteIpO;0f843wl0FkuE|uTMd4xyzlD>o) z17~Uzzg}jh({aH2Tbzc{laT25*R3 zzti#Qo1l@c{}pbRYsHKHvjy&X8<-WX4>^1_nF|5+(KZfjv}Mb`9{$&7I{7kS6tvg_ zfz>DLAZ_ht<3UwP_V1ge&3J9#XnxWS@LOT*)E#o+_(t9y&&zH*dEF!V1z+G+OgF9v z$TfRiI_fr@k_^1jMo>qsznS;b0PI5NaA2ux1(9~4DaK`BrO|aC00h?d6wy9dHh=tenz8yTIVe> zBlSWQvSl))pDpmxm~(*1&~2~e_c93t;N!)CtJz8PWkDO@0Hli=NQem5-!B z&9PFq#Wrl)cUaLb0~*pA!$i?r0f%)19yoG8g!i)O7$ZvvO$$mIUz9W`wIq;kLt8%!_NV0mD@hjLjK+jb z9))HA1O5el1<0ySE-HT)fWnZ3@ZZNz+PlY)XlxufvdZVc4N;={;Je+w zFwcNT*0ZFU$?=%QXYq+Od}#V~>M#=M4Q?qRWCu3M1Pk8;fwam_q;|#Qcf@qBzaApY zeSk@n_l@@jiLb3g85;U|=GnIR5CCi4GZch>)wr6gRO8DA1eZ)GH?eE%m(XeauvF;ZiCc52kO;>*#WR|;@?#7{DmKTmO0S&XJH86)Ya zeRPGl2)u|%9y&yLMYyTlzd-KWu;(Pd+Ye$~{J76!lFH+JX7W!RfyHpcgHbuxwfi^! zLf9efRcZYAIs>Xevh=+xFz@HOlJVTTN8v6D`+}$R+|-$UyZ+j2qXq)N0~}e}B%}Jw zkJ*jLg994J!jwYC=~I`>MHG0PzFo;FVC@ zfEPEfynV>7qBa6TZpM>~k-$Y*EB(i7R*wlXl*AE^$&`y&M&bytTyLPzxlF7fxxnvG2)yL@ z*9yr=XIGaT`Y_L&xw*6#?@K$(urY887v4)IQ%s}_B{n+qR~|W z@~LJOoT$$02Wiv%>)?+(WufTeS-;2kG6Kg#!*m`4TVmCEc5UVYIoCFPQr9F#{m`zj zzf*ign%xSIt1yI^hPGT&4vQi@Jb4&?HBovG=m<{uT;2n-alxrrO9vtw7bgMva=Uk| z9&B-JUBA~~$V8EiR6gqW7t2(voGG(8+4#h9Za>}^`t&ui9mcG-FEvKa)$!dg&F3tv z$ehWF2n(yT7rfI2|I++`ccz9tEmuzv!W0gF%=l0*erlV3IsHI{X^Z2Ch_h9S`HC`r z>f?qr{k|g#^&K2U+OQ_l4ZGPA5;(v7+;_i^Vcdsw`KDC_1^>hKmD8eFL0o<@3XVNS_Zih`g>3n zj5djhsIqnEfA!b&NloUN19};|+ij1YfWI^mW^m4NyWCaCH>k#Ha4etVXtdjAKCQ>< zn>b6E;};z-trsO9>W~H-wa!gWVnTwarBtbPuUXw-Fe0t zfMQf-0!gz!1hvO0Ktu46Np)G^Bj=4oJW>|N(v}hYPF=x-q4UFOGpNGlUkIw!c`82G zN4xrF*bC%`XqY};9kA_Aex#h#_PZ-<`&|g4O``T`m@Xf4>q4%6m2j%m0x40iU{R%x zz}+-CY%%}C;_~i0H)(%q4|e-Y)VhTUw&x0BMRh-XGHL&qzifnbL)z(Y$MqT7zq*ET z@f{!6v4{kf)C?25txWLlG*l~F0{3FrhaY5t?b(IO1iyzp3Q3|O!r%@6>i9$PK-#_Q zM4G9aCoyz?Is0cQnFLrvB!A>qii+wJ8HmN0v}xc*gM>KgwsX9@8N_IPHCfMkuL2vn(ygETF3}=X(DT7>K&!QfE#qzHrU#q|B_ZSu_G_V(7kv&Bpo-Kzc4)h&{*U|H z7=+UomfMkq$-h2xCUqHA%ks+qyP-aUb|x!H3Me=zSZ!?W<=t2FkcUk_Z+@0Aby2)^ z#=u0hRG6i&WR70oRhdLDj2r0*sLh3ip9H1w4uLRITpy4nqbsvmehb4YC|oF1`m7fO zc9J$%#my!!>)XKDt-Lp?hIy!VK$;4ioK^7gWWMXY>IUMc3GD|I_3at)w!Z6S58b-O zuvz(j&4#xcC%wmoTO|0Fu9|uD+|m{DsBXLaev-SSp?oCBEW1(ZNwu-Y+|l1wl&1nh zdTI}ORvG~9G$sa!Y3kHq4^Sd&m=h;5!4N7=px^nhn3*6&r_i61wzY^D0Ex`r0ugko;qZYo@pZ;Y($?}~? zP{a^zNm|3i`BcWAD>UI46}6_}nBBi{Kr%$zX@!{k5)uI zOuQ8`4{+c@D9t4mwNMTnLKtxm&e=yHlQNMj+uN^zq>CU_f2Tu5t`3ypnU zN6-8Z?Khr>7B`Byf8C|n!^$6&1yi8g5n|L#^Zx)eNAy0}k{?HEeoMzcQc7RMnF>6 z46gZ`!3`pj?aFekpn0W~-ztkkj=E_S496ZR*TbFQjm`Dr8Ub)j;L$gZcZYD_IUqoT zw6Re@2qW}FbA2~buo(7P4f`*AiV+E3w1bek2p4VlY8mvO@`pFwoB_2RjtGiY`#`?lMA20mh|JNQQdcI`F&@umo zGn64N`1x|oDC1`V&Wc&*AE|S+XRwuw5S^=6S7);#{spfB6vPbt>5E&@g5aAjGlD}` zYu~uiNn!$IamDF$@e_Tl+~w4;k-x8DA|239oabl)&=SI&uXx+3w+A0KQ==8&*t^tK z$2Y+DPN+M@KumUgI`W4rYnc4kttc4x-$kkkEz*10upB{f@9N$aOo#AZXT1dY3IH_P zMCd_sypKTi_SfNtoBDBah-m&DF^{G01^ooubr5W2*BRLoAAvB`MB0BDGJ2L>-tBmK zekkG%a6~=*Sbl}`x3PZLt1@-aN5+4_lRT1eRj*DN3A**0pc$eKFFl%z%JneYj(qHy z)g%@Fx^bsdP}eOMXyb0u9cuq``kh8g2rS=XPbwE=mvJNz{yHbNvh>Ut1k;9T!%J8@ zB3}xDt74ms4T+yzdJ6zGg^6t$H}ik<@4n!Y#-GtX^n~&Ul3wmVKLP|i;CP#~U;hn( z6L^EWpofBzYyX8Wh)9iE$LaZNW!HebC(x0*=lc=ooSz31voqqGP+xiKabGTBMQ;j! z1e~?z`CoEhPJx5gxaI=H^}JU#od$OPRd%p+b1i{>Nke~w-oQNg*8|?1Cqw~N8^=dxXXHnZDJba(n)%c3cwI&Rv<#^Cc?~rUoU{!^KyY#K_z1{BCmAWFb(u_ zsJs;F6Y&uFSLpv>vTSSVPR=S9%1%zJyMp-Iahf-E`H! zWl~eH|Hma&m90(L$?vA<%~D!REj@N7#)8}|+o$tYCH_u$dNXH`=KlOjy#Q~IPhrzx z!xmjG`o+_qDs*xi#igxer1o7o@n773K*&eo=(8zjOvfr{b0sTHq$@N((C&lV z1GjdrS6Mm(Me077Sg?02oDZ2+>ys0UF3ny03phttf^W_}*)YHB3f9S*JlE_INQe`p zgyhhmG1DpqvbvWH^%>9>{tgBKrY{j(lu-U1(02LK0-VkDL|vIER_HTPz?KhIHWJL9 z5cVG;L)kdH_CV3CEoJM}s%H4Mr2+&NWo`7S*&b5>eaQLSLa(QSShupfN{yvgW~q-a zLf-)#m8K1hwivsHLRQ<9Q#&8ehnr5KquN5 zfI%lrKfyk#K}&_Uu<09EDk0w(8dmO;YBHJogQ@^K15*&E+9c)u@CIie05ItL5g-cf z@Gss5g|%6+o1%xH<6cQXnbsv0G?pzOu5S0kGW=7FW;QXlmGL`sQcnZPm5$6SP}*7) zSnCo<&kD%#!C!yZ;Nce+zj>Bb7t4b5{X#ph7c3R5b~_;G(8H6JxnGytP3P~Mud59< z4*rqVZvhKI^A>{eu6^)D@s-Lj=07oiBgG&fHf_d$bsOTvEkUfQCKo+~A4P$G6s$P? zA^m^s8UW=JrGD~T2vagm1OS*<(}KFb3IYYlVpa%HPzW8bmS8goHwP`POc+mL!@(Gv z7eWF?bPjO{ZV;Vjz;LZ~Kv+tKrKC0{H2vs1?>mOctl;M+(Uqm|u1HUa;_iTdzk&al z%atu_NBoRX{c92d)hTqV8*JSkuz*|rjO`y9@SH-yRD~6S%nSr;5zNxnQG%(4utPm3 z9Gs_gQ9|X)CxNDRsmC99!BP2@qO6q^1yW)?Hvu$zQW9 zEdY7VfsKXbH|z2ikOQ!J(ZiOqzqFJ(&Ly`=q>_jZFWhpDw)%qm2LN@daX5$2jtD<* zYC2`10pq92usL?C-#-X)W5K;2s5q>R^PWro`;O$i2d}wq5%!tSRM0&0y^AIup6jg3 z<1~Tf0W5ckeWGm~ahmh7x0v5t0JPq;ehtJqVeHIx3f%{h8}SU!*U=xy?@T@nAISHR z9W7jjb8#v*j`z5YC3W`HPRhrU{&n9tp8`OzxmoZ})qwcFs#l? zdIei0@bj(}h%Qj;#7=ZOE~T~;t?SRlDSRDpjgB~jySe;bltDC3)-lMzv9TlTh@+kLOztUpNE5)+HEH!aJ#V!MaaZ1l1^E zb^&X7xjJ-@!rp~j6bFJBz1%Akn3yZQQ!IN|VC>;@x@YgfhC>+?G=Ir^D0euaYXV@6 zJjHMhJ^sPg@RFdjU$rNhd+tCy`cPm_oUW&9A0T^bZT6UPby^z*XjscY5nn0@y55WL z*Ou|(r%j0#FUKkilaO`FeD$Zy_b}Y`KN#iJM|ab4?MmPcn^>NM=uXqJnU~}_ehamw zTD(Rd02Rxh8toZX3$NdG{CEqU<X{;NV6ypUF?W_^Xq&S4;TzgV+0fzIi}U^A+>5uUa)#aWcz2zrDmS(Y z)F1Ki3t+o&3btXO0nCZe);~yBnq<+idHBxO15%^2q|*87G~5DgngG@d^S2ZcC190q z_y2zNVOG8`FcL<819xd41*>MHAk%;y@QFGERp{TGO$c+1KheJbK&Fai7REu2LS7=x z7jcM>`o%i4-pM6w%rJ7|&w;8~Xi=ParhL%TG9xr#X1u{p7Nnr*Qo0`UY8`8oD1jpv{qJ^_fd0+eMPZ}8%FX;~7-qyLTO6a35 zV4t;Dx4d4Jti?OFV6`H+VI$)D;neU%RZzBkK{U=V`?rz;r)2Hhr|$LZU|9v^KQ+P< z$bzcv1v2srL{4L4ue0vbJA#hjZ?-8MbQF2wli>VGRaM=cIbw0SQlc?`&07M7&|ZKV zSP?l9#Eonu&sd6IE8GaKV#=H`4$R9OZaA)>zG0{z&Oi5uq%Tu43VSnifL&1!)2O$X zb*Pw>)5z*GZYi_mJt433S?xCm-I%(mG=>JNRs}d!27nKH8$H;c29t{1Iv)v_NQ%y- z92|NL8QQJ?RJx|cNG(oTe(Tvlek(o|{7wB`n|Wk+;II}HmAbG>WNvO9b8CoE_6+2L zC?DtH+SSI_;71#pYWC$AXw;p$eGs1mgMeJ{=;jSho5w1a%a@AA9CKPA@isi)-B&Hi z;0wal5t(9BN*K7fJKj>Z6~7gJfmfJTJhSi-2LHM#_ZXG64E5%aQ>qQbbDl(7h*Zn4 z(Xl+WZSK3)uw^aH!vV#WoB9EMk*y1O)|&W1iqN) zag#qL#473!$`jrfs(nF<#Ko=h&iN%_k5e^9z@}=tT>m}b2QKO(H{7fFX~-2R<-t>& z^rqu(H4AVJC4L>%b+0S}DQ~L7n0u~CiaOYs7ON{nOz-OSDCSMq2Pxi+&6js&T`{l3 zgPDd_BKHf-(q?dcnDI?JO)Z1))p63+GrMr?ka9`<&wFg0A_pZAii)E?xJcO;e%ED~ zRZrA}+Sm1Le5aFrpjGmHU!M?z?e{mD69Gwt{W$mVnb9iq6ng48HK@ZvXTCCOXQ_KdUn8_AFNj)(VQg zjO9Dfd<(UTVa|nuE18=L4bzX-=b}V3|7=y;0ZF2ICZ~JtZuf8B`KeG#Of*wR6#A~& z&Nuy*ZQzD@A3z+Y^14S~<5y^_S9!|FDf@P>wB}m=45PZhi|^kyeYbzyrOI}jmlo36 zOg-Q1;Fw;QIKA`!oIRQ3xEF#aR*~+(IdCQW!|~}tjW|h~0#L|`R&B^SznrgbCzt~- zp#UUKkR08fTzV~ri~xG7>9M2wXHW^2SeQ=pfF6aL&GFsw_2nUh=-qOWAA zbJvqu|MBrDDI%Kyk_Lm@{CixJEPpB_NehwOph3^`6dpeKG+jgC1L#2>Snrgyi12cf zQgJ$pW1$va6C;*byp=tK!PUq4|&wd??%_*NG;ZZMF_3n2?vsNVae zgmsHf*_+yAJUUo7EzH*tF&0s!BP=5LLx0LrBH<#=7XTul6EgG+xAI@!OdNEo@o+3& zF=$-16~p#O;=IAUWdH%Ui`IeoT}&VxepDTQSR7d~)^E?bTsM903~`USU{dB*E5Hhr zw@IGQm3gJ}a4AMJ3=wqR8&ZwFBW<4ZGiq&0Lx;JmN?Lxabao_pE~4AQ1h~t>=P|A{ zg4O}N>Y<8;Lo8~0+Sv>2 zH?)ayy#MqB92%YTNS!)=MM!bU@U-&yOr9M`*KwR2HPDCjdEDHbnpSVlZ~RsZlom>j z7IQ|BtILSEwY}BtQxg6Myy^1cp{R(Y#AP1PFznL*QJ1tW{qexG(Rt{Z=}v$ik^u?B z)kfwG#+EY}d}0{uBJo_{K$gYAJp(@QzRyD_r$3n2a9~IuT{8^2-X|2(7WL9~%DnX3 zj#|w`t_}4M7o-P0HJ*QBhOZM1LC8(CYFQPAOi`)~_glR(XV@@ZD2v&oqd>vSc?2_# zMJ5mYckNj%%&dTAQEYOki z&|3t@-f;zU_I~F$QLvzb6G=Zrn8@N|>kkm3olDg%wsmLE3q;@@F@2LOGE$l`k61Xn z$|iXpd?1Q`{(gAGr~KM4dwtjLs4Y`wtJF4ulNNj#Jxo?=QRUPQ>#=Y znrZ`l?xO=_NXoXA*M&yEy(u~#`ZtNNJ|WwFMtV5vCm z@EI&acKaKeI6&CB18LFIl-h(NOQp$&2L?vB56cf*dk>c$j;i?seMHKlP8=yp&T1+{ zH-JQ>H9$)0r9@ZZsIlfl?J*K8PR`Ge5;Nl#G0W6vG6Q2v_YgaUz?f|pj)q)pFwV9j-%@6R`Z z==4Ox@zD1N1xogK!^K+x#7)lJ{~)Uk_6K4CySgY1CK%DJprpgrCgFyNfy%DiBIj4T zqKJ}>RPj5cUN0n3kt|%9)vw@gVnsc=;RztF{_rb$kU+S-MJ##yqm!gsDToZA8^%hI za0)%!u^)4lB7r&6kB{bSp#9W_PbPILulSd9i0nnhSIQr%Tah zE)8t+#uChqr`=VcIN7dW99ic`r{mvY&iNfbV|#Bo0>_2tHJwOm&oU={XtHuJKehd? zk<5dtQL2;#LYhykR;nJe!gIyQ+im9LAy*G9bnsaHbBj2wOLu)aF+<<)`#H)?ePMv* z1Z%U?a35M0GFFdCc`Kpaguh3U2HG=_Pm`f`v&M6Dtp-%Xj|}E3>dSgv(zdv@N>y^N zcwX2x1qspbxe24~nmjOWC>!q$uvo072?Z_C#%JX)t8@PE0^i(4TqiaUGniPP?~DUm z4JF;&t{Y@uAG!(HlP_G~-91Ld{d|JU1!776RHd?>EwL6^{#l%~ zfO11I{=k%yiQMSAuz6;}{-NE1?i+I%ycB+X@?8yGP46k$>Sl!?I2@=6onS<~ogHEP z$uVkFMfy+kO)U@y6gHm{PZ)yL3D7rh_Q{fPU3g}vy;wvOm?Esz$n3RHgm2U%+3pHL zcnG2?>%&?F+&?Q;NL3B0^Kl$VS2f(>azF4)ZRWWMqGFIJrEKl@ zh#|*);`pdAqTG|ag@txKh>MGe+*>NWO{A7pyHj{rkk^y8Btw7gTdFZ-&(lHP9yoS$ z;DKDa#B@Ubw1Ob}fyy6)aRXv=rLQWvZs|R{16&~@?auV_iLnkQ4Oj8FUJ-M8qq@(vfjeoOH@`Z@krDMc$O3z~Ag#*JiUoY|)^?j;N!#KL15ZKt(6F zeB+a7zuf6#CHv`(TRulEH?{n$r|H^E)#3hqJ6rTP1f`4A70z27hb=DCoJ~y)0SqU{ z8(Rmp_^3L4A(YF16Qklo=9MCOZQf*0+uAzFK|8Mjp>OW}igD2pl9&OFK`80xGyiWyV z&zR7;HA=zlq-$VDrYvI<7Z-ZcudKq&Q-HlEeaQbQBqk-Mjgsf2A$ItDbX&yB*h-A; zP{I(~;^V-yq3H;9&)%2nopn%x{y5li_^SJ>`d>H!t0#W_n!TCo*CLZ^MxA^z?p@iH zZ9b}6Im{Ndk;6Xu|MaB$zU%C8z6c;oi(dBN3l`3;w&eFp{zwf2KA(wx}=OZHvS3FE@?!(H7t{pbq>kiNYTrwE{$S_`I~mRY2Hsqd5a{Sq$%IWJSP;Nh}xdzn19D~*S&p*FK`B* zBRSh4Om%14ST0wmfk#{*KMmjmDuR~{z*n8S+>N&4l8n^igJo)sYrK2GWbCHNCu$0j z5lK7T6VBkdq{+Iw7Um~2NM@8>-wLBV`(BmLOlaSnAYPX5zj&>RQO)1`AeIl<~yQX0`+_m}zBL>PC2Fe%(?fBKi8@eA#7oyNpQE!8#pHxMlh zuEY%+0wIg;WdfP}alacAzI(;0w}~Wtrgi?oQz|nXN~05uW8<07LbL?b@h)W`C@?qM ztMv1W#8ch<9*=?khJ>zFM2zOf%f1J?jl<_v;!|n|&_o9fdz>d|lOWSEJl?^-r-sY|&k36Isrp5Yi@)09Eu1LA4r3w>piZ@{l}wQKcTu%zKJ}LtA{JT*D~b%% zmx(o19lHIPiy&j!#WMwo@Hf58>ytE+Kv;J88TW2qYxdP*T6~PUom=MH# zTEIz^DGq(5cpmY-QiAkojpS0y7NSq(q5gNAYJq2s+$YpHcCegXFoED*r|j}m&mxRc zQPXR{@r&X>wF0N5iRIsc7;IO$iLv!D?o?5q7P31N&BlVH?`{CSWJ2eWbJZY%lrzDc z5Svl4-XQ!H^28ZxgaxyN-3LKgf6g z`LZa$Lr$8!KwIE(;LmIAyDtPS$iK#W+oFlj5Mc^F0h}f%m)<@SAr3GsQ<7i2wq5M! zu?PAQO%%oS1!b>;{a|a9Skj;z@cdj;29-N{8{uzx|6@Mcr)A6hWc`>B@A=7EeG9tI zbAhO$JrS+ijvIZjlJf05OVfho?v3~A237nlFsYltwWMmMxS#hKM1aHRDJ1G4dYfzX zcNV}AD3wNV;LYL02Fkc26|~*lL&=6g%e_&|v_~cwQVW8N(Ra_K(9Mu$>%8RI2RgU)*tpMj@1a%b!U;1dUdSmep$;t@4Pl z!EoC{t9q&5H0KWGppFB4wIQ?aOwCi)N|vq7T*GcL5~%~b-VZ6}BaB4tnAiH@h`tmM z(y~8*caZK|xW((X3$bSu<&$s50%%W_+U8^%)+8t_l6{ z+WN3wVP7sbavf^{97P}iy zCSlfq?`3I3eXy`dLsS)=?N5sD%}pa#KYHV)VjQ&UVr=_=y70UL;hM8NcOC{YYdKXg zI(_{rO@jjQJIFuXHnMv^?#8{E3=RHyl@kno5Tt7LREMhXuMC+- zoWD8!@|2Z?$w7LmrL6Sop^4P|xB*>wEBO}Eq;Gm$&Mw@_)K6^AW_MD;ViF7j>bH&o zd3-!Vw2ZgFvU*tbkhggk!j?(Fs)F0rN5B*3{=deqJRHjP{STwb-Xx+7O&n`>HD-#C z7DNd}$ToBm!wj<1SduK$##lo~qHI~R6hpQtW672!985#UK_n!p-}8=~bN#OC`@OF3 z{5kW^``*ue-_QF#?{j}XpF0Nh&i*beebp@^`A(H^HfIYcEomyRN4l1RjvAOMR<37h z1w*Oa#0Ifjs|KLvb31~Zla=>Ch)d-OF-L3rsRt{YvNrG~1n-=fEDBB_EIb=_85E?H zC-;<}NyjMX%EKLG_so6%ns$);#Fc{+f~ zss|!x&7O=GK}DV*wR{_k)=h7XzXfDB_4}tZ_iNS`Iw!kUz82qG;23I6R2rl82jm%0 z-eWLP8y{$8ulgP}`Fy1XM1%I|&j$ftuqS1_$a5uQZXE0N8fxinUVZx$xg!qB2LQRz zHm<|zXUc%pGeryt-LAo9$Wd1PpPwabm%FG?&AGS1ZM36cunK$7&KHk{Bck@Gi6_Gd znDN)r83u@`6Xd&PAULOi%nvCq(5-ehm38q0u5+y4Q-_YDJy!=BHl+dQ5bdM7XCLtB zQ&d;9Gi_%T1sc@lxgue5=>is*3ki8|gc=W=W&)5+pi=ap3*6C_5GVsRZT+y;LG+Nq z`&`Ni$8UTEA_}3@+uAP^l4K29kzG%)Lw>|rDd+5#W17>LN&$-aPXq?K{f=tpW5l-? zwFc8v77jge#GM)BdXoa{$ozA)LDqE067+(IX3pxX{&^j*s8zsG7WuK<>GJVCU8UlL zv!)`#60*l| z#lc9zOiAlv_nT`3{&sx+C1cu|Vyzf_TEc2rvCht{@~r7wfb^#i%~!)i%&kofu<$P8 z6?~}v5UFv(GS!R>Fb5voMXfTw%7F-oWrDVhcW?g#)MAU^Qcx}AhmcHnq2{G}j5 z#+<<#>DX|<6+^VBBCyj>SyU%NPdOF00zL#Px$iFD#d*8_<<|FllMtFf688^Y3rCe0o-Edr^J$duA5p*X{EQ;aZtyoCcyL@s?@5n$ zr&~ILpqKdB)YG-5$W=dI56cPxZVvi!SEhtO{)E% z`iM`vvrP7X-gDr{1JgII12dJ18730mh-A|jhr6B<0&k{rs`iZ54AeSV$O^7i=f3o* zos{Pdi2`_vt(J5apV`z*APi|Ve_5pI>fGIEHo~C-S0aj~k~pmy_qy9<0pR%F80+9w z@5MH${SsMaZxvezplz6fs;c}CsOQVkrIFexS%mqFt5=1|Z#{z>%zZyoat~*Eh-%z5 zEI=G&HBzU=qE0`j-&Y;o2Q)uyTkqK+bNCk&Ki6Q8RMBb&>eSuV$#Ou*)FLa(Tt(4DIUAyvY{iWlnnIu7Fdi zm9LyIZgn9mU|&{7;*tJ)-(FL>N7L9Q^|PcTHeM{Q@Dt2eW%%Chj{fBkdleT>ihkut zR|23+N_{Bn@m`!c`z-j?HqqaRR4rSPS0}NZH;e*q2BtFl*Maoa^tw+G)U}Kn!WjPw zwFlb8=?Ch|kFWaI4zyZ;X;PmHEx90kD1US~)NO=lWP6GDXxw`CsT5zV4@?-wjfRjX zvUdP%`cbU6Ji=Ka49NT#?YprAmzQ3D5~foHpgq|?BMHTW{MF7Rg=aEABWqX6*FGZm=K za1zPO@HXaEfVDfPZ7^%T@e~k4za2{JX#m)00|rNjZI_9eu%>#=PRRz15b7&GAm3)Z zE7N262ZWqBO}cYtJY@{=ceb5ZI zcf(P@v=hqK$h^q95%^8G1!h6MtA#`-8wjS|I8PQ(FRXoGa>RJnrWH|DGiv8lI1)KN z*rS`xEJB#pwnIpY!zTS&j8%YTba^oV=q1++C9=TgM$N|46#RnezVS4jf{jM?uE6s% zIJK!Pd*!Cm;Eu~asWXv70mk}{H4(OQME?Bq`t68@aB1wjEa&v)gxVh1&?5wD$+AE! zPd6b?163eDeOUH#Z9iF;E;qnIm)$)~+6QU~b|q!>KnTB*2g>82yx?F9=%xQuiy`Q6 zH3~I`V+b8S)RN9y@xN!cr(d7mn(oeqU+UJ z)+f*qpiKBt{{1MVpK{$r7-@4(#ZB7~+o@(Js0$0K9o-0^+_?HSuUlzuTIhXU{EVNI z$T3%Pftu*Ef=`TozF>g@qJa1iY;msY&&&&Zn?obr++b-fM? zcUP&K+I<}KAs;2-sGB;_hDPftVEbZs=p%Mpqu^7#6ae2Di%7PzZU&XPul)N7iId$H z<`)OU#XflRY7&(~IAKYq2)KBu%Z2lT&};7JGx2(=efmep?^?^lbm)`SEh<`RJhwVL>TCdm0cZNUP44L8 zt%^0%-8a31>l#S_E4yd+V`(Lh1)zRVDP__c6+M7LF%*Gbe!TOOT&nSHzIZ}5oQ^@h z10(8H`Cx!GgK*aIw?SB#IqQa1dqJ|TQiM^q2+_)g;Q<;(KkE)pW$263Fc{C2(Q$pt z#pl$OR~ik%?pF=cDx5HLYoGWlw6itP)>P^+l zUrl1WtvhPbMKdjBHYLV77e;Rg1PO?8-!)jpcZ6ezjce51^Ls1j?b?w6ob8QzKzk46 z>1?TqixY&fii?0N9Pqtj-w+kT`wicXz_*2Sl{S|dtCi+xoQ$C-QcpkTTzox#Ww^pk zVMM=>hHc=kN~{qQ_11eLzQeNsGb-Jcy*}IAkmi$i5pR8}pF|eR?*OX_ zc<ga1Uo-yd#-n0Wo4Tzyd~6ZsJtU5cp%T7PQP-_cFmoB@#Od zFY&wo^ha8WJ>qPO(z4}2u9$by)L;8CZNSY&O&ctEZsyG;)$QQy#{jg)qG75k1E&}Cd<*n!=BNR&C-wxn-5bS&%SvFJCQ^8N@)T6|pRn5EG{~#% zfq*ppbqC+fgC0rm1k>LVLjZ74#F*HAA4R-*djM=o@+S&*`7VZfUL-Q9( zv9uyE^$9Y2KXB~YOZfyK5tA{}6nTiA3UQg062Jbd#0W&afeb^9zwBRikPHI}{GlH3 zLPa1&GE(2UQ~ip@|91QR0`&lfqzO&Ig^I5cO|D=m9wN^vH~!&~zF zG8s$xArr%tuVVk8=Ep(OUJ;iZ51GWqjvb=t9z7bOBo*`^dM8?s-0+rjGT@`Q*hB~k zRo7Qc3i0#1ASo%?*4cRmhr`{HY*&MPBJmSXR{iVX`3Q=!x8S-S8#8ck9sW7ANyGMdsm$)|P} zbxO4+7*$WBQFC%~yh)^U7%i=wo0{>^Fu5OXYbM!*a7^7|ew6XQ&HT&laXqoBs>*6* zb(P_waZB>;re$}8k#wWEZK*Pp98rl|%=Ptk@8DqD$GY?C8!;{O^PXSsu*V4tPb6c{ z7^%KRfpN|nqO!8GlEKHDNL*O!j%Be}ANLDL>p>1y;2>q(v(Ls&VuOZ8nO`?H>QIuB z)ZX6Svbs8n|1yO2%Gi&gd)e`U^Tja3RUPA*z*84q(iguhwSw!=rm;aqn+`Vv4wqls z*(E~nX3XuSTUlAL=I4t&JUl|elG44N#*7thMvV+S4hx-?2Iq+e9%nM~sXydO%gTgw z5;xuGC=cSA_&sJimidw8m%#jJ#J@QLA2gaWJucL`au%eB_}p%Kx5cmA;YNxTfaDk} z#8SM)H`9q5nGZae?-yNjB zfq&wWP6n10jLdbAWKvjLk;?}G_WmE;*QtF882KylxwtKX6O5l#vL>rLx;(eW;lM!} z85l@s=9g@y9f$`8d^KfN^FEzvtj5@i+&K;q+};1deQUp?s&pE+&Ib!G=DcsTwzzIJ fuv7Af%)<7JR7u}mth8follow on Twitter -Creates a backend 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. 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, which is always preferred. +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). + +![visualization](.github/terraform-backend.drawio.png) *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. -## Usage +## 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" - - bucket = "project-tfstate" - region = "us-east-1" - table = "tf-locktable" + 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 -To use this bucket to manage the state for other AWS accounts, you must create IAM roles in those accounts and allow the users who run Terraform to assume them. 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). +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. -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 this backend. It creates an IAM role with the AdministratorAccess policy attached and with an External ID. ## Requirements @@ -102,7 +152,9 @@ No modules. | 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/outputs.tf b/outputs.tf index 6135e5a..c7abb70 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,3 +1,4 @@ + output "external_id" { description = "External ID attached to IAM role in managed accounts" value = local.external_id @@ -12,3 +13,29 @@ 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 = < Date: Fri, 10 Feb 2023 15:35:00 -0500 Subject: [PATCH 12/13] add var for template name (#17) --- .gitignore | 2 ++ README.md | 1 + assumerole.tf | 2 +- variables.tf | 7 ++++++- 4 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 18f9c57..b948591 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ assumerole assumerole/* + +.DS_Store diff --git a/README.md b/README.md index 72e3e7a..9117714 100644 --- a/README.md +++ b/README.md @@ -135,6 +135,7 @@ No modules. | [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 | diff --git a/assumerole.tf b/assumerole.tf index 7f24d91..5dbfb16 100644 --- a/assumerole.tf +++ b/assumerole.tf @@ -16,7 +16,7 @@ resource "local_file" "assumerole_addrole" { resource "local_sensitive_file" "assumerole_tfassumerole" { count = var.create_assumerole_template ? 1 : 0 - filename = "assumerole/tfassumerole.cfn.yml" + filename = var.assumerole_template_name content = templatefile("${path.module}/template/tfassumerole.cfn.yml.tftpl", { external_id = local.external_id diff --git a/variables.tf b/variables.tf index dbb9040..c2d8409 100644 --- a/variables.tf +++ b/variables.tf @@ -80,7 +80,6 @@ variable "create_assumerole_template" { type = bool } - variable "assumerole_role_name" { default = "Terraform" description = "Name of role to create in assumerole template" @@ -104,3 +103,9 @@ variable "assumerole_stack_name" { description = "Name of CloudFormation stack" type = string } + +variable "assumerole_template_name" { + default = "assumerole/tfassumerole.cfn.yml" + description = "File name of assumerole cloudformation template" + type = string +} From 38df706257950e2ecc1d9d5d08007aeed9ac9035 Mon Sep 17 00:00:00 2001 From: Steven B Date: Mon, 4 Mar 2024 14:06:40 -0500 Subject: [PATCH 13/13] fix addrole script name mismatch (#18) * Update assumerole.tf * Update addrole.sh.tftpl --- assumerole.tf | 1 + template/addrole.sh.tftpl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/assumerole.tf b/assumerole.tf index 5dbfb16..9fcede2 100644 --- a/assumerole.tf +++ b/assumerole.tf @@ -9,6 +9,7 @@ resource "local_file" "assumerole_addrole" { content = templatefile("${path.module}/template/addrole.sh.tftpl", { stack_name = var.assumerole_stack_name + assumerole_template_name = var.assumerole_template_name }) } diff --git a/template/addrole.sh.tftpl b/template/addrole.sh.tftpl index a83fc99..5838315 100644 --- a/template/addrole.sh.tftpl +++ b/template/addrole.sh.tftpl @@ -1 +1 @@ -aws cloudformation create-stack --capabilities CAPABILITY_NAMED_IAM --template-body file://tfassumerole.cfn.yml --stack-name ${stack_name} +aws cloudformation create-stack --capabilities CAPABILITY_NAMED_IAM --template-body file://${assumerole_template_name} --stack-name ${stack_name}