diff --git a/modules/aws/route53-dns-alias-record/backplane/README.md b/modules/aws/route53-dns-alias-record/backplane/README.md new file mode 100644 index 0000000..88490b8 --- /dev/null +++ b/modules/aws/route53-dns-alias-record/backplane/README.md @@ -0,0 +1,76 @@ +# AWS Route53 DNS Alias Record Backplane + +This will deploy an IAM user (or role only in case of using `workload_identity_federation`) with Route53 access for managing DNS alias records. + +## Usage + +```hcl +provider "aws" { + region = "eu-central-1" # or any other region +} + +module "aws_route53_dns_alias_record_backplane" { + source = "git::https://github.com/meshcloud/meshstack-hub.git//modules/aws/route53-dns-alias-record/backplane" + + # List of Route53 hosted zone IDs that the building block can manage + hosted_zone_ids = [ + "", + "" + ] + + workload_identity_federation = { + issuer = "https://your-oidc-issuer" + audience = "your-audience" + subjects = [ + "system:serviceaccount:your-namespace:your-service-account-name", # Exact match + "system:serviceaccount:your-namespace:*", # Wildcard match + ] + } # Optional, if not provided, IAM access keys will be created instead +} + +output "aws_route53_dns_alias_record_backplane" { + value = module.aws_route53_dns_alias_record_backplane +} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [aws](#requirement\_aws) | ~> 6.32 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_access_key.buildingblock_route53_alias_record_access_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key) | resource | +| [aws_iam_openid_connect_provider.buildingblock_oidc_provider](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_openid_connect_provider) | resource | +| [aws_iam_policy.buildingblock_route53_alias_record_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.assume_federated_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.buildingblock_route53_alias_record](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_user.buildingblock_route53_alias_record_user](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource | +| [aws_iam_user_policy_attachment.buildingblock_route53_alias_record_user_policy_attachment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy_attachment) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.route53_alias_record_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.workload_identity_federation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [hosted\_zone\_ids](#input\_hosted\_zone\_ids) | List of Route53 hosted zone IDs that the building block can manage. Example: ['', ''] | `list(string)` | n/a | yes | +| [workload\_identity\_federation](#input\_workload\_identity\_federation) | Set these options to add a trusted identity provider from meshStack to allow workload identity federation for authentication which can be used instead of access keys. Supports multiple subjects and wildcard patterns (e.g., 'system:serviceaccount:namespace:*'). |
object({
issuer = string,
audience = string,
subjects = list(string)
})
| `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [credentials](#output\_credentials) | n/a | +| [workload\_identity\_federation\_role](#output\_workload\_identity\_federation\_role) | n/a | + diff --git a/modules/aws/route53-dns-alias-record/backplane/main.tf b/modules/aws/route53-dns-alias-record/backplane/main.tf new file mode 100644 index 0000000..d4e90c1 --- /dev/null +++ b/modules/aws/route53-dns-alias-record/backplane/main.tf @@ -0,0 +1,103 @@ +data "aws_caller_identity" "current" {} + +resource "aws_iam_user" "buildingblock_route53_alias_record_user" { + count = var.workload_identity_federation == null ? 1 : 0 + + name = "buildingblock-route53-alias-record-user" +} + +data "aws_iam_policy_document" "route53_alias_record_access" { + # Global Route53 actions that don't support resource-level permissions + statement { + effect = "Allow" + actions = [ + "route53:GetChange", + "route53:ListHostedZones" + ] + resources = ["*"] + } + + # Zone-specific actions scoped to specific hosted zones + statement { + effect = "Allow" + actions = [ + "route53:ListTagsForResource", + "route53:GetHostedZone", + "route53:ChangeResourceRecordSets", + "route53:ListResourceRecordSets" + ] + resources = [ + for zone_id in var.hosted_zone_ids : "arn:aws:route53:::hostedzone/${zone_id}" + ] + } +} + +resource "aws_iam_policy" "buildingblock_route53_alias_record_policy" { + name = var.workload_identity_federation == null ? "Route53AliasRecordBuildingBlockPolicy" : "Route53AliasRecordBuildingBlockFederatedPolicy" + description = "Policy for the Route53 DNS Alias Record Building Block" + policy = data.aws_iam_policy_document.route53_alias_record_access.json +} + +resource "aws_iam_user_policy_attachment" "buildingblock_route53_alias_record_user_policy_attachment" { + count = var.workload_identity_federation == null ? 1 : 0 + + user = aws_iam_user.buildingblock_route53_alias_record_user[0].name + policy_arn = aws_iam_policy.buildingblock_route53_alias_record_policy.arn +} + +resource "aws_iam_access_key" "buildingblock_route53_alias_record_access_key" { + count = var.workload_identity_federation == null ? 1 : 0 + + user = aws_iam_user.buildingblock_route53_alias_record_user[0].name +} + +# Workload Identity Federation + +resource "aws_iam_openid_connect_provider" "buildingblock_oidc_provider" { + count = var.workload_identity_federation != null ? 1 : 0 + + url = var.workload_identity_federation.issuer + client_id_list = [var.workload_identity_federation.audience] +} + +data "aws_iam_policy_document" "workload_identity_federation" { + count = var.workload_identity_federation != null ? 1 : 0 + version = "2012-10-17" + + statement { + effect = "Allow" + principals { + type = "Federated" + identifiers = [aws_iam_openid_connect_provider.buildingblock_oidc_provider[0].arn] + } + actions = ["sts:AssumeRoleWithWebIdentity"] + + condition { + test = "StringEquals" + variable = "${trimprefix(var.workload_identity_federation.issuer, "https://")}:aud" + + values = [var.workload_identity_federation.audience] + } + + condition { + test = "StringLike" + variable = "${trimprefix(var.workload_identity_federation.issuer, "https://")}:sub" + + values = var.workload_identity_federation.subjects + } + } +} + +resource "aws_iam_role" "assume_federated_role" { + count = var.workload_identity_federation != null ? 1 : 0 + + name = "BuildingBlockRoute53AliasRecordIdentityFederation" + assume_role_policy = data.aws_iam_policy_document.workload_identity_federation[0].json +} + +resource "aws_iam_role_policy_attachment" "buildingblock_route53_alias_record" { + count = var.workload_identity_federation != null ? 1 : 0 + + role = aws_iam_role.assume_federated_role[0].name + policy_arn = aws_iam_policy.buildingblock_route53_alias_record_policy.arn +} diff --git a/modules/aws/route53-dns-alias-record/backplane/outputs.tf b/modules/aws/route53-dns-alias-record/backplane/outputs.tf new file mode 100644 index 0000000..1f8b801 --- /dev/null +++ b/modules/aws/route53-dns-alias-record/backplane/outputs.tf @@ -0,0 +1,11 @@ +output "credentials" { + sensitive = true + value = { + AWS_ACCESS_KEY_ID = var.workload_identity_federation == null ? aws_iam_access_key.buildingblock_route53_alias_record_access_key[0].id : "N/A; workload identity federation in use" + AWS_SECRET_ACCESS_KEY = var.workload_identity_federation == null ? aws_iam_access_key.buildingblock_route53_alias_record_access_key[0].secret : "N/A; workload identity federation in use" + } +} + +output "workload_identity_federation_role" { + value = var.workload_identity_federation == null ? null : aws_iam_role.assume_federated_role[0].arn +} diff --git a/modules/aws/route53-dns-alias-record/backplane/variables.tf b/modules/aws/route53-dns-alias-record/backplane/variables.tf new file mode 100644 index 0000000..f8c65b7 --- /dev/null +++ b/modules/aws/route53-dns-alias-record/backplane/variables.tf @@ -0,0 +1,14 @@ +variable "hosted_zone_ids" { + type = list(string) + description = "List of Route53 hosted zone IDs that the building block can manage. Example: ['', '']" +} + +variable "workload_identity_federation" { + type = object({ + issuer = string, + audience = string, + subjects = list(string) + }) + default = null + description = "Set these options to add a trusted identity provider from meshStack to allow workload identity federation for authentication which can be used instead of access keys. Supports multiple subjects and wildcard patterns (e.g., 'system:serviceaccount:namespace:*')." +} diff --git a/modules/aws/route53-dns-alias-record/backplane/versions.tf b/modules/aws/route53-dns-alias-record/backplane/versions.tf new file mode 100644 index 0000000..d5d7768 --- /dev/null +++ b/modules/aws/route53-dns-alias-record/backplane/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.3.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.32" + } + } +} diff --git a/modules/aws/route53-dns-alias-record/buildingblock/APP_TEAM_README.md b/modules/aws/route53-dns-alias-record/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..702df7b --- /dev/null +++ b/modules/aws/route53-dns-alias-record/buildingblock/APP_TEAM_README.md @@ -0,0 +1,22 @@ +# AWS Route53 DNS Alias Record + +## Description +This building block creates Route53 alias records, which are AWS-specific DNS records that can only route traffic to AWS resources (load balancers, CloudFront distributions, S3 websites, etc.). + +## When to Use +- Point custom domains to AWS load balancers (ALB/NLB) +- Route traffic to CloudFront distributions +- Create apex/root domain records (e.g., example.com) + +## Shared Responsibility + +| Responsibility | Platform Team | Application Team | +|----------------|---------------|------------------| +| Managing Route53 hosted zones | ✅ | ❌ | +| Provisioning DNS alias records | ❌ | ✅ | +| Managing record names and target resources | ❌ | ✅ | + +## Key Recommendations +- Use descriptive DNS names (e.g., `api.example.com`, `www.example.com`) +- Enable health checks for automatic failover when appropriate +- Coordinate with your platform team before modifying production DNS records diff --git a/modules/aws/route53-dns-alias-record/buildingblock/README.md b/modules/aws/route53-dns-alias-record/buildingblock/README.md new file mode 100644 index 0000000..cb03e4f --- /dev/null +++ b/modules/aws/route53-dns-alias-record/buildingblock/README.md @@ -0,0 +1,75 @@ +--- +name: AWS Route53 DNS Alias Record +supportedPlatforms: + - aws +description: Provides AWS Route53 DNS alias records +--- + +# AWS Route53 DNS Alias Record + +This Terraform module provisions AWS Route53 DNS alias records. + +## Providers + +```hcl +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.77.0" + } + } +} + +provider "aws" { + region = var.region + allowed_account_ids = var.allowed_account_ids # Optional +} +``` + + +## Backend configuration +Here you can find an example of how to create a backend.tf file on this [Wiki Page](https://github.com/meshcloud/building-blocks/wiki/%5BUser-Guide%5D-Setting-up-the-Backend-for-terraform-state#how-to-configure-backendtf-file-for-these-providers) + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | ~> 6.32 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_route53_record.record](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | +| [aws_route53_zone.zone](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [alias\_evaluate\_target\_health](#input\_alias\_evaluate\_target\_health) | When set to true, an alias resource record set inherits the health of the referenced AWS resource, such as an ELB load balancer or another resource record set in the hosted zone. | `bool` | `false` | no | +| [alias\_name](#input\_alias\_name) | Alias target DNS name. | `string` | n/a | yes | +| [alias\_zone\_id](#input\_alias\_zone\_id) | AWS Route53 hosted zone id for the alias target. Note: These can be magic constants, see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html | `string` | n/a | yes | +| [allowed\_account\_ids](#input\_allowed\_account\_ids) | List of allowed AWS account IDs to prevent operations on the wrong account | `list(string)` | `null` | no | +| [private\_zone](#input\_private\_zone) | Set to true if the AWS Route 53 zone is a Private Hosted Zone. | `bool` | `false` | no | +| [region](#input\_region) | The AWS region | `string` | `"eu-central-1"` | no | +| [sub](#input\_sub) | DNS record name, excluding the `zone_name`. Use the value '@' to create an apex record. | `string` | n/a | yes | +| [type](#input\_type) | n/a | `string` | n/a | yes | +| [zone\_name](#input\_zone\_name) | AWS Route53 zone name in which the record should be created. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [alias\_target](#output\_alias\_target) | The alias target | +| [record\_name](#output\_record\_name) | The FQDN of the DNS record | +| [record\_type](#output\_record\_type) | The type of the DNS record | +| [summary](#output\_summary) | Summary of the created DNS alias record | + \ No newline at end of file diff --git a/modules/aws/route53-dns-alias-record/buildingblock/logo.png b/modules/aws/route53-dns-alias-record/buildingblock/logo.png new file mode 100644 index 0000000..a8ea892 Binary files /dev/null and b/modules/aws/route53-dns-alias-record/buildingblock/logo.png differ diff --git a/modules/aws/route53-dns-alias-record/buildingblock/main.tf b/modules/aws/route53-dns-alias-record/buildingblock/main.tf new file mode 100644 index 0000000..d01867b --- /dev/null +++ b/modules/aws/route53-dns-alias-record/buildingblock/main.tf @@ -0,0 +1,22 @@ +data "aws_route53_zone" "zone" { + name = var.zone_name + private_zone = var.private_zone +} + +locals { + # meshStack doesn't support empty strings as inputs right now, so we treat @ (which is common to denote apex records + # in zonefiles) as a special value to indicate "empty" + record_name = var.sub == "@" ? "" : var.sub +} + +resource "aws_route53_record" "record" { + zone_id = data.aws_route53_zone.zone.zone_id + name = join(".", compact([local.record_name, data.aws_route53_zone.zone.name])) + type = var.type + + alias { + name = var.alias_name + evaluate_target_health = var.alias_evaluate_target_health + zone_id = var.alias_zone_id + } +} diff --git a/modules/aws/route53-dns-alias-record/buildingblock/outputs.tf b/modules/aws/route53-dns-alias-record/buildingblock/outputs.tf new file mode 100644 index 0000000..8ee3c5c --- /dev/null +++ b/modules/aws/route53-dns-alias-record/buildingblock/outputs.tf @@ -0,0 +1,43 @@ +output "record_name" { + description = "The FQDN of the DNS record" + value = aws_route53_record.record.name +} + +output "record_type" { + description = "The type of the DNS record" + value = aws_route53_record.record.type +} + +output "alias_target" { + description = "The alias target" + value = var.alias_name +} + +output "summary" { + description = "Summary of the created DNS alias record" + value = <<-EOT +# Route53 DNS Alias Record Created + +✅ **Your DNS alias record is ready!** + +## Record Details + +| Property | Value | +|----------|-------| +| **DNS Name** | `${aws_route53_record.record.name}` | +| **Type** | `${var.type}` | +| **Alias Target** | `${var.alias_name}` | +| **Health Check** | ${var.alias_evaluate_target_health ? "✅ Enabled" : "⚠️ Disabled"} | +| **Zone** | `${var.zone_name}` | + +--- + +## Resolution + +``` +${aws_route53_record.record.name} → ${var.alias_name} +``` + +${var.private_zone ? "⚠️ **Note:** This is a private hosted zone record, only resolvable within your VPC." : "🌐 **Note:** This is a public DNS record, resolvable globally."} +EOT +} diff --git a/modules/aws/route53-dns-alias-record/buildingblock/provider.tf b/modules/aws/route53-dns-alias-record/buildingblock/provider.tf new file mode 100644 index 0000000..5119ff8 --- /dev/null +++ b/modules/aws/route53-dns-alias-record/buildingblock/provider.tf @@ -0,0 +1,4 @@ +provider "aws" { + region = var.region + allowed_account_ids = var.allowed_account_ids +} diff --git a/modules/aws/route53-dns-alias-record/buildingblock/variables.tf b/modules/aws/route53-dns-alias-record/buildingblock/variables.tf new file mode 100644 index 0000000..fe8f6d4 --- /dev/null +++ b/modules/aws/route53-dns-alias-record/buildingblock/variables.tf @@ -0,0 +1,64 @@ +variable "region" { + description = "The AWS region" + type = string + default = "eu-central-1" +} + +variable "allowed_account_ids" { + description = "List of allowed AWS account IDs to prevent operations on the wrong account" + type = list(string) + default = null +} + +variable "zone_name" { + type = string + description = "AWS Route53 zone name in which the record should be created." + nullable = false +} + +variable "private_zone" { + type = bool + default = false + description = "Set to true if the AWS Route 53 zone is a Private Hosted Zone." +} + +variable "sub" { + type = string + description = "DNS record name, excluding the `zone_name`. Use the value '@' to create an apex record." + nullable = false +} + +variable "type" { + type = string + nullable = false + # unfortunately validation blocks in HCL don't support locals... + validation { + condition = contains([ + "A", + "AAAA" + ], var.type) + error_message = "The type value must be one of ${join(", ", [for x in [ + "A", + "AAAA", + ] : "'${x}'"])} but was '${var.type}'." + } +} + +variable "alias_name" { + type = string + description = "Alias target DNS name." + nullable = false +} + +variable "alias_zone_id" { + type = string + description = "AWS Route53 hosted zone id for the alias target. Note: These can be magic constants, see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-aliastarget.html" + nullable = false +} + +variable "alias_evaluate_target_health" { + type = bool + nullable = false + description = "When set to true, an alias resource record set inherits the health of the referenced AWS resource, such as an ELB load balancer or another resource record set in the hosted zone." + default = false +} diff --git a/modules/aws/route53-dns-alias-record/buildingblock/versions.tf b/modules/aws/route53-dns-alias-record/buildingblock/versions.tf new file mode 100644 index 0000000..d4521bd --- /dev/null +++ b/modules/aws/route53-dns-alias-record/buildingblock/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.32" + } + } +} diff --git a/modules/aws/route53-dns-record/backplane/README.md b/modules/aws/route53-dns-record/backplane/README.md new file mode 100644 index 0000000..5b8ce01 --- /dev/null +++ b/modules/aws/route53-dns-record/backplane/README.md @@ -0,0 +1,76 @@ +# AWS Route53 DNS Record Backplane + +This will deploy an IAM user (or role only in case of using `workload_identity_federation`) with Route53 access for managing DNS records. + +## Usage + +```hcl +provider "aws" { + region = "eu-central-1" # or any other region +} + +module "aws_route53_dns_record_backplane" { + source = "git::https://github.com/meshcloud/meshstack-hub.git//modules/aws/route53-dns-record/backplane" + + # List of Route53 hosted zone IDs that the building block can manage + hosted_zone_ids = [ + "", + "" + ] + + workload_identity_federation = { + issuer = "https://your-oidc-issuer" + audience = "your-audience" + subjects = [ + "system:serviceaccount:your-namespace:your-service-account-name", # Exact match + "system:serviceaccount:your-namespace:*", # Wildcard match + ] + } # Optional, if not provided, IAM access keys will be created instead +} + +output "aws_route53_dns_record_backplane" { + value = module.aws_route53_dns_record_backplane +} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | ~> 6.32 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_iam_access_key.buildingblock_route53_record_access_key](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_access_key) | resource | +| [aws_iam_openid_connect_provider.buildingblock_oidc_provider](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_openid_connect_provider) | resource | +| [aws_iam_policy.buildingblock_route53_record_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource | +| [aws_iam_role.assume_federated_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.buildingblock_route53_record](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_iam_user.buildingblock_route53_record_user](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) | resource | +| [aws_iam_user_policy_attachment.buildingblock_route53_record_user_policy_attachment](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy_attachment) | resource | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy_document.route53_record_access](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.workload_identity_federation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [hosted\_zone\_ids](#input\_hosted\_zone\_ids) | List of Route53 hosted zone IDs that the building block can manage. Example: '', ''] | `list(string)` | n/a | yes | +| [workload\_identity\_federation](#input\_workload\_identity\_federation) | Set these options to add a trusted identity provider from meshStack to allow workload identity federation for authentication which can be used instead of access keys. Supports multiple subjects and wildcard patterns (e.g., 'system:serviceaccount:namespace:*'). |
object({
issuer = string,
audience = string,
subjects = list(string)
})
| `null` | no | + +## Outputs + +| Name | Description | +|------|-------------| +| [credentials](#output\_credentials) | n/a | +| [workload\_identity\_federation\_role](#output\_workload\_identity\_federation\_role) | n/a | + diff --git a/modules/aws/route53-dns-record/backplane/main.tf b/modules/aws/route53-dns-record/backplane/main.tf new file mode 100644 index 0000000..1a9bf3e --- /dev/null +++ b/modules/aws/route53-dns-record/backplane/main.tf @@ -0,0 +1,103 @@ +data "aws_caller_identity" "current" {} + +resource "aws_iam_user" "buildingblock_route53_record_user" { + count = var.workload_identity_federation == null ? 1 : 0 + + name = "buildingblock-route53-record-user" +} + +data "aws_iam_policy_document" "route53_record_access" { + # Global Route53 actions that don't support resource-level permissions + statement { + effect = "Allow" + actions = [ + "route53:GetChange", + "route53:ListHostedZones" + ] + resources = ["*"] + } + + # Zone-specific actions scoped to specific hosted zones + statement { + effect = "Allow" + actions = [ + "route53:ListTagsForResource", + "route53:GetHostedZone", + "route53:ChangeResourceRecordSets", + "route53:ListResourceRecordSets" + ] + resources = [ + for zone_id in var.hosted_zone_ids : "arn:aws:route53:::hostedzone/${zone_id}" + ] + } +} + +resource "aws_iam_policy" "buildingblock_route53_record_policy" { + name = var.workload_identity_federation == null ? "Route53RecordBuildingBlockPolicy" : "Route53RecordBuildingBlockFederatedPolicy" + description = "Policy for the Route53 DNS Record Building Block" + policy = data.aws_iam_policy_document.route53_record_access.json +} + +resource "aws_iam_user_policy_attachment" "buildingblock_route53_record_user_policy_attachment" { + count = var.workload_identity_federation == null ? 1 : 0 + + user = aws_iam_user.buildingblock_route53_record_user[0].name + policy_arn = aws_iam_policy.buildingblock_route53_record_policy.arn +} + +resource "aws_iam_access_key" "buildingblock_route53_record_access_key" { + count = var.workload_identity_federation == null ? 1 : 0 + + user = aws_iam_user.buildingblock_route53_record_user[0].name +} + +# Workload Identity Federation + +resource "aws_iam_openid_connect_provider" "buildingblock_oidc_provider" { + count = var.workload_identity_federation != null ? 1 : 0 + + url = var.workload_identity_federation.issuer + client_id_list = [var.workload_identity_federation.audience] +} + +data "aws_iam_policy_document" "workload_identity_federation" { + count = var.workload_identity_federation != null ? 1 : 0 + version = "2012-10-17" + + statement { + effect = "Allow" + principals { + type = "Federated" + identifiers = [aws_iam_openid_connect_provider.buildingblock_oidc_provider[0].arn] + } + actions = ["sts:AssumeRoleWithWebIdentity"] + + condition { + test = "StringEquals" + variable = "${trimprefix(var.workload_identity_federation.issuer, "https://")}:aud" + + values = [var.workload_identity_federation.audience] + } + + condition { + test = "StringLike" + variable = "${trimprefix(var.workload_identity_federation.issuer, "https://")}:sub" + + values = var.workload_identity_federation.subjects + } + } +} + +resource "aws_iam_role" "assume_federated_role" { + count = var.workload_identity_federation != null ? 1 : 0 + + name = "BuildingBlockRoute53RecordIdentityFederation" + assume_role_policy = data.aws_iam_policy_document.workload_identity_federation[0].json +} + +resource "aws_iam_role_policy_attachment" "buildingblock_route53_record" { + count = var.workload_identity_federation != null ? 1 : 0 + + role = aws_iam_role.assume_federated_role[0].name + policy_arn = aws_iam_policy.buildingblock_route53_record_policy.arn +} diff --git a/modules/aws/route53-dns-record/backplane/outputs.tf b/modules/aws/route53-dns-record/backplane/outputs.tf new file mode 100644 index 0000000..25bbc5e --- /dev/null +++ b/modules/aws/route53-dns-record/backplane/outputs.tf @@ -0,0 +1,11 @@ +output "credentials" { + sensitive = true + value = { + AWS_ACCESS_KEY_ID = var.workload_identity_federation == null ? aws_iam_access_key.buildingblock_route53_record_access_key[0].id : "N/A; workload identity federation in use" + AWS_SECRET_ACCESS_KEY = var.workload_identity_federation == null ? aws_iam_access_key.buildingblock_route53_record_access_key[0].secret : "N/A; workload identity federation in use" + } +} + +output "workload_identity_federation_role" { + value = var.workload_identity_federation == null ? null : aws_iam_role.assume_federated_role[0].arn +} diff --git a/modules/aws/route53-dns-record/backplane/variables.tf b/modules/aws/route53-dns-record/backplane/variables.tf new file mode 100644 index 0000000..066ca7d --- /dev/null +++ b/modules/aws/route53-dns-record/backplane/variables.tf @@ -0,0 +1,14 @@ +variable "hosted_zone_ids" { + type = list(string) + description = "List of Route53 hosted zone IDs that the building block can manage. Example: '', '']" +} + +variable "workload_identity_federation" { + type = object({ + issuer = string, + audience = string, + subjects = list(string) + }) + default = null + description = "Set these options to add a trusted identity provider from meshStack to allow workload identity federation for authentication which can be used instead of access keys. Supports multiple subjects and wildcard patterns (e.g., 'system:serviceaccount:namespace:*')." +} diff --git a/modules/aws/route53-dns-record/backplane/versions.tf b/modules/aws/route53-dns-record/backplane/versions.tf new file mode 100644 index 0000000..d4521bd --- /dev/null +++ b/modules/aws/route53-dns-record/backplane/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.32" + } + } +} diff --git a/modules/aws/route53-dns-record/buildingblock/APP_TEAM_README.md b/modules/aws/route53-dns-record/buildingblock/APP_TEAM_README.md new file mode 100644 index 0000000..7f49b74 --- /dev/null +++ b/modules/aws/route53-dns-record/buildingblock/APP_TEAM_README.md @@ -0,0 +1,23 @@ +# AWS Route53 DNS Record + +## Description +This building block creates standard DNS records for mapping domain names to IP addresses or other values. + +## When to Use +- Create DNS records (A, AAAA, CNAME, TXT, MX, SRV, etc.) +- Point subdomain names to IP addresses or other domains +- Configure email routing, domain verification, or service discovery + +## Shared Responsibility + +| Responsibility | Platform Team | Application Team | +|----------------|---------------|------------------| +| Managing Route53 hosted zones | ✅ | ❌ | +| Provisioning DNS records | ❌ | ✅ | +| Managing record values and TTL | ❌ | ✅ | + +## Key Recommendations +- Choose appropriate TTL: Lower (e.g., 300s) for frequent changes, higher (e.g., 3600s) for stable records +- Use descriptive DNS names that clearly indicate the service +- Test DNS changes in development environments first +- Remember: DNS changes take time to propagate (up to TTL duration) diff --git a/modules/aws/route53-dns-record/buildingblock/README.md b/modules/aws/route53-dns-record/buildingblock/README.md new file mode 100644 index 0000000..a78c71a --- /dev/null +++ b/modules/aws/route53-dns-record/buildingblock/README.md @@ -0,0 +1,70 @@ +--- +name: AWS Route53 DNS Record +supportedPlatforms: + - aws +description: Provides AWS Route53 DNS records for mapping domain names to IP addresses or other values. +--- + +# AWS Route53 DNS Record + +This Terraform module provisions AWS Route53 DNS records. + +## Providers + +```hcl +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.77.0" + } + } +} + +provider "aws" { + region = var.region + allowed_account_ids = var.allowed_account_ids # Optional +} +``` + + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >= 1.0 | +| [aws](#requirement\_aws) | ~> 6.32 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [aws_route53_record.record](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record) | resource | +| [aws_route53_zone.zone](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone) | data source | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [allowed\_account\_ids](#input\_allowed\_account\_ids) | List of allowed AWS account IDs to prevent operations on the wrong account | `list(string)` | `null` | no | +| [private\_zone](#input\_private\_zone) | Set to true if the AWS Route 53 zone is a Private Hosted Zone. | `bool` | `false` | no | +| [record](#input\_record) | DNS record value | `string` | n/a | yes | +| [region](#input\_region) | The AWS region | `string` | `"eu-central-1"` | no | +| [sub](#input\_sub) | DNS record name, excluding the `zone_name`. Leave empty to create apex records. | `string` | n/a | yes | +| [ttl](#input\_ttl) | TTL of the record in seconds. | `string` | `"300"` | no | +| [type](#input\_type) | DNS Record type | `string` | n/a | yes | +| [zone\_name](#input\_zone\_name) | AWS Route53 zone name in which the record should be created. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [record\_name](#output\_record\_name) | The FQDN of the DNS record | +| [record\_type](#output\_record\_type) | The type of the DNS record | +| [record\_value](#output\_record\_value) | The value of the DNS record | +| [summary](#output\_summary) | Summary of the created DNS record | + diff --git a/modules/aws/route53-dns-record/buildingblock/logo.png b/modules/aws/route53-dns-record/buildingblock/logo.png new file mode 100644 index 0000000..a8ea892 Binary files /dev/null and b/modules/aws/route53-dns-record/buildingblock/logo.png differ diff --git a/modules/aws/route53-dns-record/buildingblock/main.tf b/modules/aws/route53-dns-record/buildingblock/main.tf new file mode 100644 index 0000000..c83f3df --- /dev/null +++ b/modules/aws/route53-dns-record/buildingblock/main.tf @@ -0,0 +1,12 @@ +data "aws_route53_zone" "zone" { + name = var.zone_name + private_zone = var.private_zone +} + +resource "aws_route53_record" "record" { + zone_id = data.aws_route53_zone.zone.zone_id + name = "${var.sub}.${data.aws_route53_zone.zone.name}" + type = var.type + ttl = parseint(var.ttl, 10) + records = [var.record] +} diff --git a/modules/aws/route53-dns-record/buildingblock/outputs.tf b/modules/aws/route53-dns-record/buildingblock/outputs.tf new file mode 100644 index 0000000..bab6a31 --- /dev/null +++ b/modules/aws/route53-dns-record/buildingblock/outputs.tf @@ -0,0 +1,45 @@ +output "record_name" { + description = "The FQDN of the DNS record" + value = aws_route53_record.record.name +} + +output "record_type" { + description = "The type of the DNS record" + value = aws_route53_record.record.type +} + +output "record_value" { + description = "The value of the DNS record" + value = var.record +} + +output "summary" { + description = "Summary of the created DNS record" + value = <<-EOT +# Route53 DNS Record Created + +✅ **Your DNS record is ready!** + +## Record Details + +| Property | Value | +|----------|-------| +| **DNS Name** | `${aws_route53_record.record.name}` | +| **Type** | `${var.type}` | +| **Value** | `${var.record}` | +| **TTL** | `${var.ttl}` seconds | +| **Zone** | `${var.zone_name}` | + +--- + +## Resolution + +``` +${aws_route53_record.record.name} → ${var.record} +``` + +${var.private_zone ? "⚠️ **Note:** This is a private hosted zone record, only resolvable within your VPC." : "🌐 **Note:** This is a public DNS record, resolvable globally."} + +**Propagation:** DNS changes may take up to ${var.ttl} seconds to fully propagate. +EOT +} diff --git a/modules/aws/route53-dns-record/buildingblock/provider.tf b/modules/aws/route53-dns-record/buildingblock/provider.tf new file mode 100644 index 0000000..5119ff8 --- /dev/null +++ b/modules/aws/route53-dns-record/buildingblock/provider.tf @@ -0,0 +1,4 @@ +provider "aws" { + region = var.region + allowed_account_ids = var.allowed_account_ids +} diff --git a/modules/aws/route53-dns-record/buildingblock/variables.tf b/modules/aws/route53-dns-record/buildingblock/variables.tf new file mode 100644 index 0000000..2913fbc --- /dev/null +++ b/modules/aws/route53-dns-record/buildingblock/variables.tf @@ -0,0 +1,90 @@ +variable "region" { + description = "The AWS region" + type = string + default = "eu-central-1" +} + +variable "allowed_account_ids" { + description = "List of allowed AWS account IDs to prevent operations on the wrong account" + type = list(string) + default = null +} + +variable "zone_name" { + type = string + description = "AWS Route53 zone name in which the record should be created." +} + +variable "private_zone" { + type = bool + default = false + description = "Set to true if the AWS Route 53 zone is a Private Hosted Zone." +} + +variable "sub" { + type = string + description = "DNS record name, excluding the `zone_name`. Leave empty to create apex records." + nullable = false + + # note: "CNAME records are illegal at apex, value must be a non-empty string." + # terraform can't validate this right now + +} + +# Legal DNS record types. Consider shortening this list if you want to constain allowed records +# (e.g. SOA, NS) as they allow sub-delegation of DNS zones +# See https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html +variable "type" { + type = string + description = "DNS Record type" + + # unfortunately validation blocks in HCL don't support locals so we have to copy/paste literals here + validation { + condition = contains([ + "A", + "AAAA", + # "CAA", + "CNAME", + # "DS", + "MX", + # "NAPTR", + # "NS", + # "PTR", + # "SOA", + "SPF", + "SRV", + "TXT", + ], var.type) + error_message = "The type value must be one of ${join(", ", [for x in [ + "A", + "AAAA", + # "CAA", + "CNAME", + # "DS", + "MX", + # "NAPTR", + # "NS", + # "PTR", + # "SOA", + "SPF", + "SRV", + "TXT", + ] : "'${x}'"])} but was '${var.type}'." + } +} + +variable "record" { + type = string + description = "DNS record value" + nullable = false +} + +variable "ttl" { + type = string + default = "300" + description = "TTL of the record in seconds." + validation { + condition = parseint(var.ttl, 10) > 0 + error_message = "The ttl value must be larger than 0 but was ${var.ttl}." + } +} diff --git a/modules/aws/route53-dns-record/buildingblock/versions.tf b/modules/aws/route53-dns-record/buildingblock/versions.tf new file mode 100644 index 0000000..d4521bd --- /dev/null +++ b/modules/aws/route53-dns-record/buildingblock/versions.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">= 1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.32" + } + } +}