Skip to content

Commit 7d50aef

Browse files
authored
add support for cross-acct state mgmt (#3)
1 parent 61b5508 commit 7d50aef

File tree

6 files changed

+225
-10
lines changed

6 files changed

+225
-10
lines changed

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2020 Rhythmic Technologies, Inc.
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
[![](https://github.com/rhythmictech/terraform-aws-backend/workflows/check/badge.svg)](https://github.com/rhythmictech/terraform-aws-backend/actions)
44

55
Creates a backend S3 bucket and DynamoDB table for managing Terraform state. Useful for bootstrapping a new
6-
environment.
6+
environment. This module supports cross-account state management, using a centralized account that holds the S3 bucket and KMS key.
7+
8+
_Note: A centralized DynamoDB locking table is not supported because terraform cannot assume more than one IAM role per execution._
79

810
## Usage
911
```
@@ -16,14 +18,60 @@ module "backend" {
1618
1719
```
1820

21+
## Cross Account State Management
22+
Managing state across accounts requires additional configuration to ensure that the S3 bucket is appropriately accessible and the KMS key is usable.
23+
24+
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:
25+
26+
```yaml
27+
module "backend" {
28+
source = "git::ssh://git@github.com/rhythmictech/terraform-aws-backend"
29+
allowed_account_ids = ["123456789012"]
30+
bucket = "012345678901-us-east-1-tfstate"
31+
region = "us-east-1"
32+
}
33+
```
34+
35+
In the target account, use this declaration to import the module:
36+
37+
```yaml
38+
module "backend" {
39+
source = "git::ssh://git@github.com/rhythmictech/terraform-aws-backend"
40+
kms_key_id = "arn:aws:kms:us-east-1:012345678901:key/59381274-af42-8521-04af-ab0acfe3d521"
41+
region = "us-east-1"
42+
remote_bucket = "012345678901-us-east-1-tfstate"
43+
}
44+
```
45+
46+
The module will automatically write to the source account S3 bucket using the KMS key with cross-account access.
47+
48+
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:
49+
50+
```
51+
bucket = "012345678901-us-east-1-tf-state"
52+
key = "project.tfstate"
53+
workspace_key_prefix = "123456789012"
54+
region = "us-east-1"
55+
```
56+
1957
<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
2058
## Inputs
2159

2260
| Name | Description | Type | Default | Required |
2361
|------|-------------|:----:|:-----:|:-----:|
24-
| bucket | Name of bucket to create | string | n/a | yes |
62+
| allowed\_account\_ids | Account IDs that are allowed to access the bucket/KMS key | list(string) | `[]` | no |
63+
| bucket | Name of bucket to create \(do not provide if using `remote\_bucket`\) | string | `""` | no |
64+
| kms\_key\_id | ARN for KMS key for all encryption operations. | string | `""` | no |
2565
| region | Region bucket will be created in | string | n/a | yes |
26-
| table | Name of Dynamo Table to create | string | n/a | yes |
66+
| remote\_bucket | If specified, the remote bucket will be used for the backend. A new bucket will not be created | string | `""` | no |
67+
| table | Name of Dynamo Table to create | string | `"tf-locktable"` | no |
2768
| tags | Mapping of any extra tags you want added to resources | map(string) | `{}` | no |
2869

70+
## Outputs
71+
72+
| Name | Description |
73+
|------|-------------|
74+
| kms\_key\_arn | ARN of KMS Key for S3 bucket |
75+
| s3\_bucket\_backend | S3 bucket |
76+
2977
<!-- END OF PRE-COMMIT-TERRAFORM DOCS HOOK -->

kms.tf

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
data "aws_iam_policy_document" "key" {
2+
3+
statement {
4+
effect = "Allow"
5+
actions = ["kms:*"]
6+
resources = ["*"]
7+
principals {
8+
type = "AWS"
9+
identifiers = ["arn:aws:iam::${local.account_id}:root"]
10+
}
11+
}
12+
13+
dynamic "statement" {
14+
for_each = var.allowed_account_ids
15+
16+
content {
17+
18+
actions = [
19+
"kms:Encrypt*",
20+
"kms:Decrypt*",
21+
"kms:ReEncrypt*",
22+
"kms:GenerateDataKey*",
23+
"kms:DescribeKey",
24+
]
25+
effect = "Allow"
26+
resources = ["*"]
27+
principals {
28+
type = "AWS"
29+
identifiers = ["arn:aws:iam::${statement.value}:root"]
30+
}
31+
}
32+
}
33+
34+
}
35+
36+
37+
resource "aws_kms_key" "this" {
38+
count = var.kms_key_id == "" ? 1 : 0
39+
deletion_window_in_days = 7
40+
description = "Terraform State KMS key"
41+
enable_key_rotation = true
42+
policy = data.aws_iam_policy_document.key.json
43+
tags = merge(
44+
{
45+
"Name" = "tf-backend-key"
46+
},
47+
var.tags
48+
)
49+
}
50+
51+
resource "aws_kms_alias" "this" {
52+
count = var.kms_key_id == "" ? 1 : 0
53+
name = "alias/tf_backend_config"
54+
target_key_id = aws_kms_key.this[0].id
55+
}

main.tf

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
11
data "aws_caller_identity" "current" {
22
}
33

4-
resource "aws_s3_bucket" "config_bucket" {
4+
locals {
5+
account_id = data.aws_caller_identity.current.account_id
6+
7+
# Account IDs that will have access to stream CloudTrail logs
8+
account_ids = concat([local.account_id], var.allowed_account_ids)
9+
10+
# Format account IDs into necessary resource lists.
11+
iam_account_principals = formatlist("arn:aws:iam::%s:root", local.account_ids)
12+
13+
# Resolve resource names
14+
bucket = var.remote_bucket == "" ? aws_s3_bucket.this[0].id : var.remote_bucket
15+
kms_key_id = var.kms_key_id == "" ? aws_kms_key.this[0].arn : var.kms_key_id
16+
}
17+
18+
resource "aws_s3_bucket" "this" {
19+
count = var.remote_bucket == "" ? 1 : 0
520
bucket = var.bucket
621
acl = "log-delivery-write"
7-
822
tags = merge(
923
var.tags,
1024
{
@@ -28,7 +42,8 @@ resource "aws_s3_bucket" "config_bucket" {
2842
server_side_encryption_configuration {
2943
rule {
3044
apply_server_side_encryption_by_default {
31-
sse_algorithm = "aws:kms"
45+
sse_algorithm = "aws:kms"
46+
kms_master_key_id = local.kms_key_id
3247
}
3348
}
3449
}
@@ -39,15 +54,16 @@ resource "aws_s3_bucket" "config_bucket" {
3954
}
4055
}
4156

42-
resource "aws_s3_bucket_public_access_block" "block_public_access" {
43-
bucket = aws_s3_bucket.config_bucket.id
57+
resource "aws_s3_bucket_public_access_block" "this" {
58+
count = var.remote_bucket == "" ? 1 : 0
59+
bucket = aws_s3_bucket.this[0].id
4460
block_public_acls = true
4561
block_public_policy = true
4662
ignore_public_acls = true
4763
restrict_public_buckets = true
4864
}
4965

50-
resource "aws_dynamodb_table" "terraform_statelock" {
66+
resource "aws_dynamodb_table" "this" {
5167
name = var.table
5268
billing_mode = "PAY_PER_REQUEST"
5369
hash_key = "LockID"
@@ -64,3 +80,49 @@ resource "aws_dynamodb_table" "terraform_statelock" {
6480
},
6581
)
6682
}
83+
84+
85+
data "aws_iam_policy_document" "this" {
86+
87+
88+
statement {
89+
actions = [
90+
"s3:ListBucket"
91+
]
92+
effect = "Allow"
93+
resources = [
94+
"arn:aws:s3:::${local.bucket}"
95+
]
96+
97+
principals {
98+
type = "AWS"
99+
identifiers = local.iam_account_principals
100+
}
101+
}
102+
103+
dynamic "statement" {
104+
for_each = var.allowed_account_ids
105+
106+
content {
107+
108+
actions = [
109+
"s3:GetObject",
110+
"s3:PutObject",
111+
"s3:DeleteObject"
112+
]
113+
effect = "Allow"
114+
resources = ["arn:aws:s3:::${local.bucket}/${statement.value}/*"]
115+
116+
principals {
117+
type = "AWS"
118+
identifiers = ["arn:aws:iam::${statement.value}:root"]
119+
}
120+
}
121+
}
122+
}
123+
124+
resource "aws_s3_bucket_policy" "this" {
125+
count = var.remote_bucket == "" ? 1 : 0
126+
bucket = aws_s3_bucket.this[0].id
127+
policy = data.aws_iam_policy_document.this.json
128+
}

outputs.tf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
output "s3_bucket_backend" {
2+
description = "S3 bucket"
3+
value = var.remote_bucket == "" ? aws_s3_bucket.this[0].bucket : var.remote_bucket
4+
}
5+
6+
output "kms_key_arn" {
7+
description = "ARN of KMS Key for S3 bucket"
8+
value = var.kms_key_id == "" ? aws_kms_key.this[0].arn : var.kms_key_id
9+
}

variables.tf

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
1+
variable "allowed_account_ids" {
2+
default = []
3+
description = "Account IDs that are allowed to access the bucket/KMS key"
4+
type = list(string)
5+
}
6+
17
variable "bucket" {
2-
description = "Name of bucket to create"
8+
default = ""
9+
description = "Name of bucket to create (do not provide if using `remote_bucket`)"
10+
type = string
11+
}
12+
13+
variable "kms_key_id" {
14+
default = ""
15+
description = "ARN for KMS key for all encryption operations."
316
type = string
417
}
518

@@ -8,7 +21,14 @@ variable "region" {
821
type = string
922
}
1023

24+
variable "remote_bucket" {
25+
default = ""
26+
description = "If specified, the remote bucket will be used for the backend. A new bucket will not be created"
27+
type = string
28+
}
29+
1130
variable "table" {
31+
default = "tf-locktable"
1232
description = "Name of Dynamo Table to create"
1333
type = string
1434
}

0 commit comments

Comments
 (0)