diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e34bbd4..ea7cc6c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,12 @@ on: push: branches: - main +permissions: + actions: read + checks: read + contents: read + pull-requests: write + security-events: write jobs: ci: uses: SPHTech-Platform/reusable-workflows/.github/workflows/terraform.yaml@main diff --git a/README.md b/README.md index 2854f8d..262238b 100644 --- a/README.md +++ b/README.md @@ -1 +1,68 @@ # Terraform Modules Template + + +## Requirements + +No requirements. + +## Providers + +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | n/a | + +## Modules + +| Name | Source | Version | +|------|--------|---------| +| [cdn](#module\_cdn) | terraform-aws-modules/cloudfront/aws | n/a | +| [s3](#module\_s3) | terraform-aws-modules/s3-bucket/aws | 3.5.0 | + +## Resources + +| Name | Type | +|------|------| +| [aws_cloudfront_function.viewer_request](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudfront_function) | resource | +| [aws_s3_bucket_policy.docs](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_policy) | resource | +| [aws_iam_policy_document.s3_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.s3_policy_merge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [acl](#input\_acl) | Private or Public ACL | `string` | `"private"` | no | +| [attach\_policy](#input\_attach\_policy) | Controls if S3 bucket should have bucket policy attached (set to `true` to use value of `policy` as bucket policy) | `bool` | `true` | no | +| [block\_public\_acls](#input\_block\_public\_acls) | Whether Amazon S3 should block public ACLs for this bucket. | `bool` | `true` | no | +| [block\_public\_policy](#input\_block\_public\_policy) | Whether Amazon S3 should block public bucket policies for this bucket. | `bool` | `true` | no | +| [bucket\_name](#input\_bucket\_name) | bucket name | `string` | `""` | no | +| [cors\_rule](#input\_cors\_rule) | List of maps containing rules for Cross-Origin Resource Sharing. | `any` |
{| no | +| [create\_origin\_access\_identity](#input\_create\_origin\_access\_identity) | Whether Amazon S3 should restrict public bucket policies for this bucket. | `bool` | `true` | no | +| [default\_cache\_behavior](#input\_default\_cache\_behavior) | The default cache behavior for this distribution | `any` | `{}` | no | +| [ignore\_public\_acls](#input\_ignore\_public\_acls) | Whether Amazon S3 should ignore public ACLs for this bucket. | `bool` | `true` | no | +| [lifecycle\_rule](#input\_lifecycle\_rule) | List of maps containing configuration of object lifecycle management. | `any` | `[]` | no | +| [logging](#input\_logging) | Map containing access bucket logging configuration. | `map(string)` | `{}` | no | +| [ordered\_cache\_behavior](#input\_ordered\_cache\_behavior) | An ordered list of cache behaviors resource for this distribution. List from top to bottom in order of precedence. The topmost cache behavior will have precedence 0. | `any` | `[]` | no | +| [origin](#input\_origin) | One or more origins for this distribution (multiples allowed). | `any` | `{}` | no | +| [origin\_access\_identities](#input\_origin\_access\_identities) | Map of CloudFront origin access identities (value as a comment) | `map(string)` | `{}` | no | +| [policy](#input\_policy) | (Optional) A valid bucket policy JSON document. Note that if the policy document is not specific enough (but still valid), Terraform may view the policy as constantly changing in a terraform plan. In this case, please make sure you use the verbose/specific version of the policy. For more information about building AWS IAM policy documents with Terraform, see the AWS IAM Policy Document Guide. | `string` | `""` | no | +| [price\_class](#input\_price\_class) | The price class for this distribution. One of PriceClass\_All, PriceClass\_200, PriceClass\_100 | `string` | `"PriceClass_All"` | no | +| [restrict\_public\_buckets](#input\_restrict\_public\_buckets) | Whether Amazon S3 should restrict public bucket policies for this bucket. | `bool` | `true` | no | +| [server\_side\_encryption\_configuration](#input\_server\_side\_encryption\_configuration) | Map containing server-side encryption configuration. | `any` | `{}` | no | +| [versioning](#input\_versioning) | Map containing versioning configuration. | `map(string)` |
"cors_rule": {
"allowed_headers": [
"*"
],
"allowed_methods": [
"PUT",
"POST",
"GET",
"DELETE"
],
"allowed_origins": [
"*"
],
"expose_headers": [
"ETag"
],
"max_age_seconds": 3000
}
}
{| no | +| [wait\_for\_deployment](#input\_wait\_for\_deployment) | Whether Amazon S3 should restrict public bucket policies for this bucket. | `bool` | `false` | no | +| [website](#input\_website) | Map containing static web-site hosting or redirect configuration. | `any` |
"enabled": true
}
{| no | + +## Outputs + +| Name | Description | +|------|-------------| +| [cloudfront\_distribution\_arn](#output\_cloudfront\_distribution\_arn) | The ARN (Amazon Resource Name) for the distribution. | +| [cloudfront\_distribution\_domain\_name](#output\_cloudfront\_distribution\_domain\_name) | The domain name corresponding to the distribution. | +| [cloudfront\_distribution\_id](#output\_cloudfront\_distribution\_id) | The Arn of the cloudfront distribution | +| [cloudfront\_origin\_access\_identity\_iam\_arns](#output\_cloudfront\_origin\_access\_identity\_iam\_arns) | The IAM arns of the origin access identities created | +| [s3\_bucket\_arn](#output\_s3\_bucket\_arn) | The ARN of the bucket. Will be of format arn:aws:s3:::bucketname. | +| [s3\_bucket\_bucket\_domain\_name](#output\_s3\_bucket\_bucket\_domain\_name) | The bucket domain name. Will be of format bucketname.s3.amazonaws.com. | +| [s3\_bucket\_bucket\_regional\_domain\_name](#output\_s3\_bucket\_bucket\_regional\_domain\_name) | The bucket region-specific domain name. The bucket domain name including the region name, please refer here for format. Note: The AWS CloudFront allows specifying S3 region-specific endpoint when creating S3 origin, it will prevent redirect issues from CloudFront to S3 Origin URL. | +| [s3\_bucket\_id](#output\_s3\_bucket\_id) | The name of the bucket. | + diff --git a/cloudfront.tf b/cloudfront.tf new file mode 100644 index 0000000..61b87fa --- /dev/null +++ b/cloudfront.tf @@ -0,0 +1,67 @@ +module "cdn" { + source = "terraform-aws-modules/cloudfront/aws" + version = "~> 3.1.0" + + comment = "Distribution for static website" + is_ipv6_enabled = true + price_class = var.price_class + wait_for_deployment = var.wait_for_deployment + create_origin_access_identity = var.create_origin_access_identity + + origin_access_identities = merge({ + origin_access_identity = module.s3.s3_bucket_id + }, var.origin_access_identities) + + origin = merge({ + origin_access_identity = { + domain_name = module.s3.s3_bucket_bucket_regional_domain_name + s3_origin_config = { + origin_access_identity = "origin_access_identity" + # key in `origin_access_identities` + } } + }, var.origin) + + default_cache_behavior = merge({ + target_origin_id = "origin_access_identity" # key in `origin` above + viewer_protocol_policy = "redirect-to-https" + + default_ttl = 360 + min_ttl = 300 + max_ttl = 600 + + allowed_methods = ["GET", "HEAD"] + cached_methods = ["GET", "HEAD"] + compress = true + query_string = false + + function_association = { + viewer-request = { + function_arn = aws_cloudfront_function.viewer_request.arn + } + } + }, var.default_cache_behavior) + + ordered_cache_behavior = var.ordered_cache_behavior + + default_root_object = "index.html" + + custom_error_response = [ + { + error_code = 404 + response_code = 404 + response_page_path = "/errors/404.html" + }, + { + error_code = 403 + response_code = 403 + response_page_path = "/errors/403.html" + } + ] +} + +resource "aws_cloudfront_function" "viewer_request" { + name = "default_viewer_request" + runtime = "cloudfront-js-1.0" + publish = true + code = file("${path.module}/templates/viewer-request-default.js") +} diff --git a/data.tf b/data.tf new file mode 100644 index 0000000..2109428 --- /dev/null +++ b/data.tf @@ -0,0 +1,21 @@ +data "aws_iam_policy_document" "s3_policy" { + statement { + actions = ["s3:GetObject"] + resources = ["${module.s3.s3_bucket_arn}/*"] + principals { + type = "AWS" + identifiers = module.cdn.cloudfront_origin_access_identity_iam_arns + } + } + depends_on = [ + module.cdn.cloudfront_distribution_id + ] +} + +data "aws_iam_policy_document" "s3_policy_merge" { + + source_policy_documents = [ + data.aws_iam_policy_document.s3_policy.json, + var.policy + ] +} diff --git a/output.tf b/output.tf new file mode 100644 index 0000000..d831e8d --- /dev/null +++ b/output.tf @@ -0,0 +1,39 @@ +output "s3_bucket_id" { + description = "The name of the bucket." + value = module.s3.s3_bucket_id +} + +output "s3_bucket_arn" { + description = "The ARN of the bucket. Will be of format arn:aws:s3:::bucketname." + value = module.s3.s3_bucket_arn +} + +output "s3_bucket_bucket_domain_name" { + description = "The bucket domain name. Will be of format bucketname.s3.amazonaws.com." + value = module.s3.s3_bucket_bucket_domain_name +} + +output "s3_bucket_bucket_regional_domain_name" { + description = "The bucket region-specific domain name. The bucket domain name including the region name, please refer here for format. Note: The AWS CloudFront allows specifying S3 region-specific endpoint when creating S3 origin, it will prevent redirect issues from CloudFront to S3 Origin URL." + value = module.s3.s3_bucket_bucket_regional_domain_name +} + +output "cloudfront_distribution_id" { + description = "The Arn of the cloudfront distribution" + value = module.cdn.cloudfront_distribution_id +} + +output "cloudfront_distribution_arn" { + description = "The ARN (Amazon Resource Name) for the distribution." + value = module.cdn.cloudfront_distribution_arn +} + +output "cloudfront_distribution_domain_name" { + description = "The domain name corresponding to the distribution." + value = module.cdn.cloudfront_distribution_domain_name +} + +output "cloudfront_origin_access_identity_iam_arns" { + description = "The IAM arns of the origin access identities created" + value = module.cdn.cloudfront_origin_access_identity_iam_arns +} diff --git a/s3.tf b/s3.tf new file mode 100644 index 0000000..8fe30a7 --- /dev/null +++ b/s3.tf @@ -0,0 +1,31 @@ +module "s3" { + source = "terraform-aws-modules/s3-bucket/aws" + version = "~> 3.5.0" + + bucket = var.bucket_name + acl = var.acl + block_public_acls = var.block_public_acls + block_public_policy = var.block_public_policy + ignore_public_acls = var.ignore_public_acls + restrict_public_buckets = var.restrict_public_buckets + + server_side_encryption_configuration = var.server_side_encryption_configuration + cors_rule = var.cors_rule + lifecycle_rule = var.lifecycle_rule + + versioning = var.versioning + logging = var.logging + + website = var.website + +} + + +resource "aws_s3_bucket_policy" "docs" { + bucket = module.s3.s3_bucket_id + policy = data.aws_iam_policy_document.s3_policy_merge.json + + depends_on = [ + module.cdn.cloudfront_distribution_id + ] +} diff --git a/templates/viewer-request-default.js b/templates/viewer-request-default.js new file mode 100644 index 0000000..49c03a0 --- /dev/null +++ b/templates/viewer-request-default.js @@ -0,0 +1,15 @@ +function handler(event) { + var request = event.request; + var uri = request.uri; + + // Check whether the URI is missing a file name. + if (uri.endsWith('/')) { + request.uri += 'index.html'; + } + // Check whether the URI is missing a file extension. + else if (!uri.includes('.')) { + request.uri += '/index.html'; + } + + return request; +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..0fbfd97 --- /dev/null +++ b/variables.tf @@ -0,0 +1,138 @@ +variable "bucket_name" { + description = "bucket name" + type = string + default = "" +} + +variable "acl" { + description = "Private or Public ACL" + type = string + default = "private" +} + +variable "attach_policy" { + description = "Controls if S3 bucket should have bucket policy attached (set to `true` to use value of `policy` as bucket policy)" + type = bool + default = true +} + +variable "policy" { + description = "A valid bucket policy JSON document (Optional)" + type = string + default = "" +} + +variable "block_public_acls" { + description = "Whether Amazon S3 should block public ACLs for this bucket." + type = bool + default = true +} + +variable "block_public_policy" { + description = "Whether Amazon S3 should block public bucket policies for this bucket." + type = bool + default = true +} + +variable "ignore_public_acls" { + description = "Whether Amazon S3 should ignore public ACLs for this bucket." + type = bool + default = true +} + +variable "restrict_public_buckets" { + description = "Whether Amazon S3 should restrict public bucket policies for this bucket." + type = bool + default = true +} + +variable "server_side_encryption_configuration" { + description = "Map containing server-side encryption configuration." + type = any + default = {} +} + +variable "lifecycle_rule" { + description = "List of maps containing configuration of object lifecycle management." + type = any + default = [] +} + +variable "cors_rule" { + description = "List of maps containing rules for Cross-Origin Resource Sharing." + type = any + default = { + cors_rule = { + allowed_headers = ["*"] + allowed_methods = ["PUT", "POST", "GET", "DELETE"] + allowed_origins = ["*"] + expose_headers = ["ETag"] + max_age_seconds = 3000 + } + } +} + +variable "versioning" { + description = "Map containing versioning configuration." + type = map(string) + default = { + enabled = true + } +} + +variable "logging" { + description = "Map containing access bucket logging configuration." + type = map(string) + default = {} +} + +variable "website" { + description = "Map containing static web-site hosting or redirect configuration." + type = any # map(string) + default = { + index_document = "index.html" + error_document = "error.html" + } +} + +variable "wait_for_deployment" { + description = "Whether Amazon S3 should restrict public bucket policies for this bucket." + type = bool + default = false +} + +variable "create_origin_access_identity" { + description = "Whether Amazon S3 should restrict public bucket policies for this bucket." + type = bool + default = true +} + +variable "ordered_cache_behavior" { + description = "An ordered list of cache behaviors resource for this distribution. List from top to bottom in order of precedence. The topmost cache behavior will have precedence 0." + type = any + default = [] +} + +variable "price_class" { + description = "The price class for this distribution. One of PriceClass_All, PriceClass_200, PriceClass_100" + type = string + default = "PriceClass_All" +} + +variable "origin_access_identities" { + description = "Map of CloudFront origin access identities (value as a comment)" + type = map(string) + default = {} +} + +variable "origin" { + description = "One or more origins for this distribution (multiples allowed)." + type = any + default = {} +} + +variable "default_cache_behavior" { + description = "The default cache behavior for this distribution" + type = any + default = {} +} diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..b3f404e --- /dev/null +++ b/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 4.50" + } + } +}
"error_document": "error.html",
"index_document": "index.html"
}