Skip to content

Commit 9d5e9b1

Browse files
authored
feat: Create S3 bucket and KMS key. (#2)
1 parent 7e2200e commit 9d5e9b1

File tree

9 files changed

+325
-36
lines changed

9 files changed

+325
-36
lines changed

README.md

Lines changed: 54 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,23 @@
1-
# Code for America OpenTofu Module Template
1+
# AWS S3 Uploads Bucket Module
22

33
[![Main Checks][badge-checks]][code-checks] [![GitHub Release][badge-release]][latest-release]
44

5-
Use this template repository to create new OpenTofu modules. Follow the steps
6-
below to use this repository:
7-
8-
1. Click the "Use this template" button to create a new repository
9-
1. Name your new repository using the format `todu-modules-<provider>-<module>`
10-
1. Add the files necessary to support your module to the root of your new
11-
repository
12-
1. Update the `README.md` file with the appropriate information for your module.
13-
Make sure you update any references to this template repository with your new
14-
repository
15-
1. Update the [codeforamerica/tofu-modules][tofu-modules] repository to include
16-
your new module in the main `README.md` and the documentation
5+
This module creates an S3 bucket for file uploads. The bucket is configured with
6+
logging, encryption, verisioning and a lifecycle configuration.
177

188
## Usage
199

2010
Add this module to your `main.tf` (or appropriate) file and configure the inputs
2111
to match your desired configuration. For example:
2212

23-
[//]: # (TODO: Update to match your module's name and inputs)
24-
2513
```hcl
2614
module "module_name" {
27-
source = "github.com/codeforamerica/tofu-modules-template?ref=1.0.0"
15+
source = "github.com/codeforamerica/tofu-modules-aws-s3-uploads-bucket?ref=1.0.0"
2816
29-
project = "my-project"
30-
environment = "development"
17+
project = "my-project"
18+
environment = "development"
19+
logging-bucket = "my-logging-bucket"
20+
name = "documents"
3121
}
3222
```
3323

@@ -46,31 +36,59 @@ tofu init -upgrade
4636

4737
## Inputs
4838

49-
[//]: # (TODO: Replace the following with your own inputs)
50-
51-
| Name | Description | Type | Default | Required |
52-
|-------------|-----------------------------------------------|----------|---------|----------|
53-
| project | Name of the project. | `string` | n/a | yes |
54-
| environment | Environment for the project. | `string` | `"dev"` | no |
55-
| tags | Optional tags to be applied to all resources. | `list` | `[]` | no |
39+
| Name | Description | Type | Default | Required |
40+
| -------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- | ----------------------------------------------- | -------- |
41+
| logging_bucket | S3 bucket to send access logs to. | `string` | n/a | yes |
42+
| name | Name of the bucket. The project and environment will be prepended to this automatically. | `string` | n/a | yes |
43+
| project | Project that these resources are supporting. This is used in the prefix to all resource names. | `string` | n/a | yes |
44+
| abort_incomplete_multipart_upload_days | Number of days to abort incomplete multipart uploads. | `number` | `7` | no |
45+
| allowed_principals | List of AWS principal ARNs to allow to use the KMS key. This is used to grant access to other resources that need to use the key, such as ECS task roles. | `list(string)` | `[]` | no |
46+
| encryption_key_arn | ARN of the KMS key to use for S3 bucket encryption. If not provided, a new KMS key will be created. | `string` | `null` | no |
47+
| environment | The environment for the deployment. This is used in the prefix to all resource names. | `string` | `"development"` | no |
48+
| force_delete | Whether to force delete the bucket and its contents. Must be set to `true` _and_ applied before the bucket can be deleted. | `bool` | `false` | no |
49+
| key_recovery_period | Number of days to recover the created KMS key after deletion. Must be between `7` and `30`. | `number` | `30` | no |
50+
| noncurrent_version_expiration_days | Number of days to expire noncurrent versions of objects. | `number` | `30` | no |
51+
| [storage_class_transitions] | List of storage class transitions to apply to the buckets lifecycle configuration. | `list(object)` | `[{days = 30, storage_class = "STANDARD_IA"}]` | no |
52+
| tags | Optional tags to be applied to all resources. | `map(string)` | `{}` | no |
53+
54+
### storage_class_transitions
55+
56+
You can define multiple [storage class][storage-class] transitions for the
57+
objects in the S3 bucket. This allows you to use a reduced cost storage option
58+
for objects that need to be retained for a longer time, but won't be regularly
59+
accessed.
60+
61+
By default, objects added to the bucket will transition to the infrequent access
62+
storage tier after 30 days. To disable transitions entirely, you can set this
63+
input to an empty list (`[]`).
64+
65+
| Name | Description | Type | Default | Required |
66+
| ------------- | --------------------------------------- | -------- | ------- | -------- |
67+
| days | Number of days | `number` | n/a | yes |
68+
| storage_class | Storage class to transition objects to. | `string` | n/a | yes |
69+
70+
Possible values for `storage_class` are `DEEP_ARCHIVE`, `GLACIER`, `GLACIER_IR`,
71+
`INTELLIGENT_TIERING`, `ONEZONE_IA`, `STANDARD_IA`. For more information on the
72+
different storage classes, see the [Amazon S3 documentation][storage-class].
5673

5774
## Outputs
5875

59-
[//]: # (TODO: Replace the following with your own outputs)
60-
61-
| Name | Description | Type |
62-
|----------|-----------------------------------|----------|
63-
| id | Id of the newly created resource. | `string` |
64-
76+
| Name | Description | Type |
77+
| ------------------ | ------------------------------------------------------------------------------- | -------- |
78+
| bucket_name | Name of the created bucket. | `string` |
79+
| bucket_arn | Full ARN of the created bucket. | `string` |
80+
| bucket_domain_name | Domain name of the created bucket, in the format `bucketname.s3.amazonaws.com`. | `string` |
81+
| kms_key_arn | ARN of the KMS key used for bucket encryption. | `string` |
6582

6683
## Contributing
6784

6885
Follow the [contributing guidelines][contributing] to contribute to this
6986
repository.
7087

71-
[badge-checks]: https://github.com/codeforamerica/tofu-modules-template/actions/workflows/main.yaml/badge.svg
72-
[badge-release]: https://img.shields.io/github/v/release/codeforamerica/tofu-modules-template?logo=github&label=Latest%20Release
73-
[code-checks]: https://github.com/codeforamerica/tofu-modules-template/actions/workflows/main.yaml
88+
[badge-checks]: https://github.com/codeforamerica/tofu-modules-aws-s3-uploads-bucket/actions/workflows/main.yaml/badge.svg
89+
[badge-release]: https://img.shields.io/github/v/release/codeforamerica/tofu-modules-aws-s3-uploads-bucket?logo=github&label=Latest%20Release
90+
[code-checks]: https://github.com/codeforamerica/tofu-modules-aws-s3-uploads-bucket/actions/workflows/main.yaml
7491
[contributing]: CONTRIBUTING.md
75-
[latest-release]: https://github.com/codeforamerica/tofu-modules-template/releases/latest
76-
[tofu-modules]: https://github.com/codeforamerica/tofu-modules
92+
[latest-release]: https://github.com/codeforamerica/tofu-modules-aws-s3-uploads-bucket/releases/latest
93+
[storage-class]: https://docs.aws.amazon.com/AmazonS3/latest/userguide/storage-class-intro.html
94+
[storage_class_transitions]: #storage_class_transitions

data.tf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
data "aws_caller_identity" "identity" {}
2+
3+
data "aws_partition" "current" {}

locals.tf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
locals {
2+
bucket_name = join("-", [var.project, var.environment, var.name])
3+
kms_key_arn = var.encryption_key_arn != null ? var.encryption_key_arn : aws_kms_key.bucket["this"].arn
4+
logs_path = "/AWSLogs/${data.aws_caller_identity.identity.account_id}"
5+
tags = merge({ use : "file-uploads" }, var.tags)
6+
}

main.tf

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
resource "aws_kms_key" "bucket" {
2+
for_each = var.encryption_key_arn != null ? toset([]) : toset(["this"])
3+
4+
description = "Encryption key for bucket ${local.bucket_name}"
5+
deletion_window_in_days = var.key_recovery_period
6+
enable_key_rotation = true
7+
policy = jsonencode(yamldecode(templatefile("${path.module}/templates/key-policy.yaml.tftpl", {
8+
account : data.aws_caller_identity.identity.account_id
9+
bucket : local.bucket_name
10+
partition : data.aws_partition.current.partition
11+
principals : var.allowed_principals
12+
})))
13+
14+
tags = local.tags
15+
}
16+
17+
resource "aws_kms_alias" "bucket" {
18+
for_each = var.encryption_key_arn != null ? toset([]) : toset(["this"])
19+
20+
name = "alias/${local.bucket_name}"
21+
target_key_id = aws_kms_key.bucket["this"].arn
22+
}
23+
24+
module "this" {
25+
source = "boldlink/s3/aws"
26+
version = "2.6.0"
27+
28+
bucket = local.bucket_name
29+
force_destroy = var.force_delete
30+
31+
bucket_policy = jsonencode(yamldecode(templatefile("${path.module}/templates/bucket-policy.yaml.tftpl", {
32+
partition : data.aws_partition.current.partition
33+
bucket : local.bucket_name
34+
})))
35+
36+
lifecycle_configuration = [{
37+
id = "state"
38+
status = "Enabled"
39+
40+
filter = {
41+
prefix = ""
42+
}
43+
44+
abort_incomplete_multipart_upload_days = var.abort_incomplete_multipart_upload_days
45+
46+
noncurrent_version_expiration = [{
47+
noncurrent_days = var.noncurrent_version_expiration_days
48+
}]
49+
50+
transition = var.storage_class_transitions
51+
}]
52+
53+
sse_bucket_key_enabled = true
54+
sse_kms_master_key_arn = local.kms_key_arn
55+
sse_sse_algorithm = "aws:kms"
56+
57+
versioning_status = "Enabled"
58+
59+
s3_logging = {
60+
target_bucket = var.logging_bucket
61+
target_prefix = "${local.logs_path}/s3accesslogs/${local.bucket_name}"
62+
}
63+
64+
tags = local.tags
65+
}

outputs.tf

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
output "bucket_name" {
2+
description = "Name of the created bucket."
3+
value = module.this.bucket
4+
}
5+
6+
output "bucket_arn" {
7+
description = "Full ARN of the created bucket."
8+
value = module.this.arn
9+
}
10+
11+
output "bucket_domain_name" {
12+
description = <<-EOT
13+
Domain name of the created bucket, in the format
14+
`bucketname.s3.amazonaws.com`.
15+
EOT
16+
value = module.this.bucket_domain_name
17+
}
18+
19+
output "kms_key_arn" {
20+
description = "ARN of the KMS key used for bucket encryption."
21+
value = local.kms_key_arn
22+
}

templates/bucket-policy.yaml.tftpl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Version: "2012-10-17"
2+
Statement:
3+
- Sid: AllowSSLRequestsOnly
4+
Effect: Deny
5+
Principal: "*"
6+
Action:
7+
- s3:*
8+
Resource:
9+
- arn:${partition}:s3:::${bucket}
10+
- arn:${partition}:s3:::${bucket}/*
11+
Condition:
12+
Bool:
13+
aws:SecureTransport: false

templates/key-policy.yaml.tftpl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
Version: "2012-10-17"
2+
Statement:
3+
- Sid: Enable IAM User Permissions
4+
Effect: Allow
5+
Principal:
6+
AWS: arn:${partition}:iam::${account}:root
7+
Action: kms:*
8+
Resource: "*"
9+
- Sid: Allow S3 to encrypt and decrypt objects
10+
Effect: Allow
11+
Principal:
12+
AWS: "*"
13+
Action:
14+
- kms:Decrypt
15+
- kms:Encrypt
16+
- kms:GenerateDataKey
17+
- kms:ReEncrypt*
18+
Resource: "*"
19+
Condition:
20+
StringLike:
21+
kms:CallerAccount: "${account}"
22+
kms:EncryptionContext:aws:s3:arn:
23+
- "arn:${partition}:s3:::${bucket}"
24+
- "arn:${partition}:s3:::${bucket}/*"
25+
%{ if length(principals) > 0 ~}
26+
- Sid: Allow other resources to use the key
27+
Effect: Allow
28+
Principal:
29+
AWS:
30+
%{ for principal in principals ~}
31+
- "${principal}"
32+
%{ endfor }
33+
Action:
34+
- kms:GenerateDataKey
35+
- kms:Decrypt
36+
Resource: "*"
37+
%{ endif ~}

variables.tf

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
variable "abort_incomplete_multipart_upload_days" {
2+
type = number
3+
description = "Number of days to abort incomplete multipart uploads."
4+
default = 7
5+
6+
validation {
7+
condition = var.abort_incomplete_multipart_upload_days > 0
8+
error_message = "Abort incomplete multipart upload days must be greater than 0."
9+
}
10+
}
11+
12+
variable "allowed_principals" {
13+
type = list(string)
14+
description = <<-EOT
15+
List of AWS principal ARNs to allow to use the KMS key. This is used to
16+
grant access to other resources that need to use the key, such as ECS task
17+
roles.
18+
EOT
19+
default = []
20+
}
21+
22+
variable "encryption_key_arn" {
23+
type = string
24+
description = <<-EOT
25+
ARN of the KMS key to use for S3 bucket encryption. If not provided, a new
26+
KMS key will be created.
27+
EOT
28+
default = null
29+
}
30+
31+
variable "environment" {
32+
type = string
33+
description = <<-EOT
34+
The environment for the deployment. This is used in the prefix to all
35+
resource names.
36+
EOT
37+
default = "development"
38+
}
39+
40+
variable "force_delete" {
41+
type = bool
42+
description = <<-EOT
43+
Whether to force delete the bucket and its contents. Must be set to `true`
44+
_and_ applied before the bucket can be deleted.
45+
EOT
46+
default = false
47+
}
48+
49+
variable "key_recovery_period" {
50+
type = number
51+
description = <<-EOT
52+
Number of days to recover the created KMS key after deletion. Must be
53+
between `7` and `30`.
54+
EOT
55+
default = 30
56+
57+
validation {
58+
condition = var.key_recovery_period > 6 && var.key_recovery_period < 31
59+
error_message = "Key recovery period must be between 7 and 30."
60+
}
61+
}
62+
63+
variable "logging_bucket" {
64+
type = string
65+
description = "S3 bucket to send access logs to."
66+
}
67+
68+
variable "name" {
69+
type = string
70+
description = <<-EOT
71+
Name of the bucket. The project and environment will be prepended to this
72+
automatically.
73+
EOT
74+
}
75+
76+
variable "noncurrent_version_expiration_days" {
77+
type = number
78+
description = "Number of days to expire noncurrent versions of objects."
79+
default = 30
80+
81+
validation {
82+
condition = var.noncurrent_version_expiration_days > 0
83+
error_message = "Noncurrent version expiration days must be greater than 0."
84+
}
85+
}
86+
87+
variable "project" {
88+
type = string
89+
description = <<-EOT
90+
Project that these resources are supporting. This is used in the prefix to
91+
all resource names.
92+
EOT
93+
}
94+
95+
variable "storage_class_transitions" {
96+
type = list(object({
97+
days = number
98+
storage_class = string
99+
}))
100+
101+
description = <<-EOT
102+
List of storage class transitions to apply to the buckets lifecycle
103+
configuration.
104+
EOT
105+
default = [{
106+
days = 30
107+
storage_class = "STANDARD_IA"
108+
}]
109+
}
110+
111+
variable "tags" {
112+
type = map(string)
113+
description = "Optional tags to be applied to all resources."
114+
default = {}
115+
}

versions.tf

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
terraform {
2+
required_version = ">= 1.10"
3+
4+
required_providers {
5+
aws = {
6+
source = "hashicorp/aws"
7+
version = ">= 6.28"
8+
}
9+
}
10+
}

0 commit comments

Comments
 (0)