Skip to content

Commit 3628b97

Browse files
authored
Merge pull request #7 from rhythmictech/add-replication
Add bucket replication
2 parents e2f9ee1 + a0ab3a0 commit 3628b97

File tree

8 files changed

+314
-15
lines changed

8 files changed

+314
-15
lines changed

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,11 @@
77

88
# .tfvars files
99
*.tfvars
10+
11+
# macs
12+
.DS_Store
13+
14+
# temp folders
15+
tmp
16+
17+
.terraform.lock.hcl

.pre-commit-config.yaml

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
exclude: ".terraform"
22
repos:
33
- repo: https://github.com/antonbabenko/pre-commit-terraform
4-
rev: v1.50.0
4+
rev: v1.73.0
55
hooks:
66
- id: terraform_docs
77
always_run: true
8-
args:
9-
- --args=--sort-by-required
108
- id: terraform_fmt
119
- id: terraform_tflint
1210
alias: terraform_tflint_nocreds
@@ -36,8 +34,29 @@ repos:
3634
verbose: true
3735
files: \.tf(vars)?$
3836
exclude: examples
37+
- id: tflock
38+
name: provider_locks
39+
entry: |
40+
bash -c '
41+
AWS_DEFAULT_REGION=us-east-1
42+
declare -a DIRS
43+
for FILE in "$@"
44+
do
45+
DIRS+=($(dirname "$FILE"))
46+
done
47+
for DIR in $(printf "%s\n" "${DIRS[@]}" | sort -u)
48+
do
49+
cd $(dirname "$FILE")
50+
terraform providers lock -platform=windows_amd64 -platform=darwin_amd64 -platform=linux_amd64
51+
cd ..
52+
done
53+
'
54+
language: system
55+
verbose: true
56+
files: \.tf(vars)?$
57+
exclude: examples
3958
- repo: https://github.com/pre-commit/pre-commit-hooks
40-
rev: v3.4.0
59+
rev: v4.3.0
4160
hooks:
4261
- id: check-case-conflict
4362
- id: check-json
@@ -66,3 +85,5 @@ repos:
6685
args:
6786
- --markdown-linebreak-ext=md
6887
exclude: README.md
88+
ci:
89+
skip: [terraform_docs, terraform_fmt, terraform_tflint, terraform_tfsec, tflock]

.terraform-version

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
latest:^0.13
1+
latest:^1.1

README.md

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
# terraform-aws-helmrepo
2-
3-
[![tflint](https://github.com/rhythmictech/terraform-aws-helmrepo/workflows/tflint/badge.svg?branch=main&event=push)](https://github.com/rhythmictech/terraform-aws-helmrepo/actions?query=workflow%3Atflint+event%3Apush+branch%3Amain)
4-
[![tfsec](https://github.com/rhythmictech/terraform-aws-helmrepo/workflows/tfsec/badge.svg?branch=main&event=push)](https://github.com/rhythmictech/terraform-aws-helmrepo/actions?query=workflow%3Atfsec+event%3Apush+branch%3Amain)
5-
[![yamllint](https://github.com/rhythmictech/terraform-aws-helmrepo/workflows/yamllint/badge.svg?branch=main&event=push)](https://github.com/rhythmictech/terraform-aws-helmrepo/actions?query=workflow%3Ayamllint+event%3Apush+branch%3Amain)
6-
[![misspell](https://github.com/rhythmictech/terraform-aws-helmrepo/workflows/misspell/badge.svg?branch=main&event=push)](https://github.com/rhythmictech/terraform-aws-helmrepo/actions?query=workflow%3Amisspell+event%3Apush+branch%3Amain)
7-
[![pre-commit-check](https://github.com/rhythmictech/terraform-aws-helmrepo/workflows/pre-commit-check/badge.svg?branch=main&event=push)](https://github.com/rhythmictech/terraform-aws-helmrepo/actions?query=workflow%3Apre-commit-check+event%3Apush+branch%3Amain)
8-
<a href="https://twitter.com/intent/follow?screen_name=RhythmicTech"><img src="https://img.shields.io/twitter/follow/RhythmicTech?style=social&logo=twitter" alt="follow on Twitter"></a>
2+
[![tflint](https://github.com/rhythmictech/terraform-aws-helmrepo/workflows/tflint/badge.svg?branch=master&event=push)](https://github.com/rhythmictech/terraform-aws-helmrepo/actions?query=workflow%3Atflint+event%3Apush+branch%3Amaster)
3+
[![tfsec](https://github.com/rhythmictech/terraform-aws-helmrepo/workflows/tfsec/badge.svg?branch=master&event=push)](https://github.com/rhythmictech/terraform-aws-helmrepo/actions?query=workflow%3Atfsec+event%3Apush+branch%3Amaster)
4+
[![yamllint](https://github.com/rhythmictech/terraform-aws-helmrepo/workflows/yamllint/badge.svg?branch=master&event=push)](https://github.com/rhythmictech/terraform-aws-helmrepo/actions?query=workflow%3Ayamllint+event%3Apush+branch%3Amaster)
5+
[![misspell](https://github.com/rhythmictech/terraform-aws-helmrepo/workflows/misspell/badge.svg?branch=master&event=push)](https://github.com/rhythmictech/terraform-aws-helmrepo/actions?query=workflow%3Amisspell+event%3Apush+branch%3Amaster)
6+
[![pre-commit-check](https://github.com/rhythmictech/terraform-aws-helmrepo/workflows/pre-commit-check/badge.svg?branch=master&event=push)](https://github.com/rhythmictech/terraform-aws-helmrepo/actions?query=workflow%3Apre-commit-check+event%3Apush+branch%3Amaster)
97

108
Create an S3 bucket intended to serve as a Helm repo. Configures basic encryption and supports sharing the bucket across many accounts.
119

@@ -15,19 +13,23 @@ module {
1513
source = "rhythmictech/helmrepo/aws"
1614
}
1715
```
16+
1817
<!-- BEGINNING OF PRE-COMMIT-TERRAFORM DOCS HOOK -->
1918
## Requirements
2019

2120
| Name | Version |
2221
|------|---------|
2322
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 0.12.19 |
24-
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 3.0.0 |
23+
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | >= 4.0 |
24+
| <a name="requirement_random"></a> [random](#requirement\_random) | ~> 3.3 |
2525

2626
## Providers
2727

2828
| Name | Version |
2929
|------|---------|
30-
| <a name="provider_aws"></a> [aws](#provider\_aws) | >= 3.0.0 |
30+
| <a name="provider_aws"></a> [aws](#provider\_aws) | 4.22.0 |
31+
| <a name="provider_aws.destination"></a> [aws.destination](#provider\_aws.destination) | 4.22.0 |
32+
| <a name="provider_random"></a> [random](#provider\_random) | 3.3.2 |
3133

3234
## Modules
3335

@@ -37,10 +39,21 @@ No modules.
3739

3840
| Name | Type |
3941
|------|------|
42+
| [aws_iam_policy.replication_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
43+
| [aws_iam_policy_attachment.replication](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy_attachment) | resource |
44+
| [aws_iam_role.replication](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
45+
| [aws_s3_bucket.destination](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource |
4046
| [aws_s3_bucket.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket) | resource |
47+
| [aws_s3_bucket_policy.destination](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource |
4148
| [aws_s3_bucket_policy.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource |
49+
| [aws_s3_bucket_public_access_block.dest_block_public_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource |
4250
| [aws_s3_bucket_public_access_block.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) | resource |
51+
| [aws_s3_bucket_replication_configuration.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_replication_configuration) | resource |
52+
| [random_id.replication](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/id) | resource |
4353
| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
54+
| [aws_iam_policy_document.destination](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
55+
| [aws_iam_policy_document.replication_assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
56+
| [aws_iam_policy_document.replication_policy_doc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
4457
| [aws_iam_policy_document.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
4558
| [aws_region.region](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source |
4659

@@ -50,6 +63,9 @@ No modules.
5063
|------|-------------|------|---------|:--------:|
5164
| <a name="input_allowed_account_ids"></a> [allowed\_account\_ids](#input\_allowed\_account\_ids) | List of AWS account IDs to grant read-only access to the repo. Due to how policies are constructed, there's effectively a limit of about 9 accounts. | `list(string)` | `[]` | no |
5265
| <a name="input_allowed_account_ids_write"></a> [allowed\_account\_ids\_write](#input\_allowed\_account\_ids\_write) | List of AWS account IDs to grant write access to the repo. Due to how policies are constructed, there's effectively a limit of about 9 accounts. | `list(string)` | `[]` | no |
66+
| <a name="input_dest_logging_bucket"></a> [dest\_logging\_bucket](#input\_dest\_logging\_bucket) | S3 bucket name to log bucket access requests to (optional) | `string` | `null` | no |
67+
| <a name="input_dest_logging_bucket_prefix"></a> [dest\_logging\_bucket\_prefix](#input\_dest\_logging\_bucket\_prefix) | S3 bucket prefix to log bucket access requests to (optional). If blank but a `logging_bucket` is specified, this will be set to the name of the bucket | `string` | `null` | no |
68+
| <a name="input_dest_region"></a> [dest\_region](#input\_dest\_region) | Region to replicate repo bucket to (omit to disable replication) | `string` | `""` | no |
5369
| <a name="input_logging_bucket"></a> [logging\_bucket](#input\_logging\_bucket) | S3 bucket name to log bucket access requests to (optional) | `string` | `null` | no |
5470
| <a name="input_logging_bucket_prefix"></a> [logging\_bucket\_prefix](#input\_logging\_bucket\_prefix) | S3 bucket prefix to log bucket access requests to (optional). If blank but a `logging_bucket` is specified, this will be set to the name of the bucket | `string` | `null` | no |
5571
| <a name="input_name"></a> [name](#input\_name) | Bucket name for the helm repo. Specify to control the exact name of the bucket, otherwise use `name_suffix` | `string` | `null` | no |

iam.tf

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
data "aws_iam_policy_document" "replication_assume_role" {
2+
statement {
3+
effect = "Allow"
4+
actions = ["sts:AssumeRole"]
5+
6+
principals {
7+
type = "Service"
8+
identifiers = ["s3.amazonaws.com"]
9+
}
10+
}
11+
}
12+
13+
resource "aws_iam_role" "replication" {
14+
count = var.dest_region != "" ? 1 : 0
15+
16+
name_prefix = "replication"
17+
assume_role_policy = data.aws_iam_policy_document.replication_assume_role.json
18+
tags = var.tags
19+
}
20+
21+
data "aws_iam_policy_document" "replication_policy_doc" {
22+
count = var.dest_region != "" ? 1 : 0
23+
statement {
24+
effect = "Allow"
25+
actions = [
26+
"s3:GetReplicationConfiguration",
27+
"s3:ListBucket"
28+
]
29+
30+
resources = [
31+
aws_s3_bucket.this.arn
32+
]
33+
34+
}
35+
36+
statement {
37+
effect = "Allow"
38+
actions = [
39+
"s3:GetObjectVersion",
40+
"s3:GetObjectVersionForReplication",
41+
"s3:GetObjectVersionAcl"
42+
]
43+
44+
resources = [
45+
"${aws_s3_bucket.this.arn}/*"
46+
]
47+
48+
}
49+
50+
statement {
51+
effect = "Allow"
52+
actions = [
53+
"s3:ReplicateObject",
54+
"s3:ReplicateDelete"
55+
]
56+
57+
resources = [
58+
"${aws_s3_bucket.destination[0].arn}/*"
59+
]
60+
61+
}
62+
63+
}
64+
65+
resource "aws_iam_policy" "replication_policy" {
66+
count = var.dest_region != "" ? 1 : 0
67+
68+
name_prefix = "replication-policy"
69+
policy = data.aws_iam_policy_document.replication_policy_doc[0].json
70+
}
71+
72+
resource "aws_iam_policy_attachment" "replication" {
73+
count = var.dest_region != "" ? 1 : 0
74+
75+
name = "replication"
76+
roles = [aws_iam_role.replication[0].name]
77+
policy_arn = aws_iam_policy.replication_policy[0].arn
78+
}
79+
80+
resource "random_id" "replication" {
81+
count = var.dest_region != "" ? 1 : 0
82+
byte_length = 32
83+
}

main.tf

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
provider "aws" {
2+
alias = "destination"
3+
region = var.dest_region
4+
}
5+
16
data "aws_caller_identity" "current" {
27
}
38

@@ -11,6 +16,11 @@ locals {
1116
bucket = var.logging_bucket
1217
prefix = var.logging_bucket_prefix != null ? var.logging_bucket_prefix : local.bucket_name
1318
}]
19+
20+
dest_logging_map = var.dest_logging_bucket == null ? [] : [{
21+
bucket = var.dest_logging_bucket
22+
prefix = var.dest_logging_bucket_prefix != null ? var.dest_logging_bucket_prefix : "${local.bucket_name}-replica"
23+
}]
1424
}
1525

1626
# This bucket uses a dynamic block to generate logging. If users do not wish to log,
@@ -119,3 +129,143 @@ resource "aws_s3_bucket_policy" "this" {
119129
bucket = aws_s3_bucket.this.id
120130
policy = data.aws_iam_policy_document.this.json
121131
}
132+
133+
########################################
134+
# Source replication configuration
135+
########################################
136+
resource "aws_s3_bucket_replication_configuration" "this" {
137+
count = var.dest_region != "" ? 1 : 0
138+
139+
role = aws_iam_role.replication[0].arn
140+
bucket = aws_s3_bucket.this.id
141+
142+
rule {
143+
id = random_id.replication[0].b64_std
144+
priority = 0
145+
146+
delete_marker_replication {
147+
status = "Enabled"
148+
}
149+
150+
filter {
151+
prefix = ""
152+
}
153+
154+
status = "Enabled"
155+
156+
destination {
157+
bucket = aws_s3_bucket.destination[0].arn
158+
}
159+
}
160+
}
161+
162+
########################################
163+
# Replicated bucket
164+
########################################
165+
#tfsec:ignore:AWS002
166+
resource "aws_s3_bucket" "destination" {
167+
count = var.dest_region != "" ? 1 : 0
168+
provider = aws.destination
169+
170+
bucket = "${local.bucket_name}-replica"
171+
acl = "private"
172+
tags = var.tags
173+
174+
dynamic "logging" {
175+
for_each = local.dest_logging_map
176+
177+
content {
178+
target_bucket = logging.value.bucket
179+
target_prefix = logging.value.prefix
180+
}
181+
}
182+
183+
versioning {
184+
enabled = true
185+
}
186+
187+
server_side_encryption_configuration {
188+
rule {
189+
apply_server_side_encryption_by_default {
190+
sse_algorithm = "AES256"
191+
}
192+
}
193+
}
194+
195+
}
196+
197+
resource "aws_s3_bucket_public_access_block" "dest_block_public_access" {
198+
count = var.dest_region != "" ? 1 : 0
199+
provider = aws.destination
200+
201+
bucket = aws_s3_bucket.destination[0].id
202+
block_public_acls = true
203+
block_public_policy = true
204+
ignore_public_acls = true
205+
restrict_public_buckets = true
206+
}
207+
208+
data "aws_iam_policy_document" "destination" {
209+
count = var.dest_region != "" ? 1 : 0
210+
provider = aws.destination
211+
dynamic "statement" {
212+
for_each = var.allowed_account_ids
213+
214+
content {
215+
216+
sid = "Allow cross-account access to list objects (${statement.value})"
217+
actions = ["s3:ListBucket"]
218+
effect = "Allow"
219+
resources = [aws_s3_bucket.destination[0].arn]
220+
221+
principals {
222+
identifiers = ["arn:aws:iam::${statement.value}:root"]
223+
type = "AWS"
224+
}
225+
}
226+
}
227+
228+
dynamic "statement" {
229+
# Remove accounts with write access from this statement to keep policy size down
230+
for_each = setsubtract(var.allowed_account_ids, var.allowed_account_ids_write)
231+
232+
content {
233+
sid = "Allow Cross-account read-only access (${statement.value})"
234+
actions = ["s3:GetObject*"]
235+
effect = "Allow"
236+
resources = ["${aws_s3_bucket.destination[0].arn}/*"]
237+
238+
principals {
239+
identifiers = ["arn:aws:iam::${statement.value}:root"]
240+
type = "AWS"
241+
}
242+
}
243+
}
244+
245+
dynamic "statement" {
246+
for_each = var.allowed_account_ids_write
247+
248+
content {
249+
sid = "Allow Cross-account write access (${statement.value})"
250+
actions = [
251+
"s3:GetObject*",
252+
"s3:PutObject*"
253+
]
254+
effect = "Allow"
255+
resources = ["${aws_s3_bucket.destination[0].arn}/*"]
256+
257+
principals {
258+
identifiers = ["arn:aws:iam::${statement.value}:root"]
259+
type = "AWS"
260+
}
261+
}
262+
}
263+
}
264+
265+
resource "aws_s3_bucket_policy" "destination" {
266+
count = var.dest_region != "" ? 1 : 0
267+
provider = aws.destination
268+
269+
bucket = aws_s3_bucket.destination[0].id
270+
policy = data.aws_iam_policy_document.destination[0].json
271+
}

0 commit comments

Comments
 (0)