Repository: piksel-ina/piksel-infra
Pull request #40: Setup resolver endpoint and resolver rule in Centralized shared DNS
Author: login: taufik-shf
Created: 2025-05-09T10:50:22Z | Merged: 2025-05-11T12:44:31Z



Diff:
diff --git a/dev/providers.tf b/dev/providers.tf
index 4d53a83..e1f4dd6 100644
--- a/dev/providers.tf
+++ b/dev/providers.tf
@@ -18,5 +18,6 @@ terraform {
 }
 
 provider "aws" {
+  alias  = "dev-account"
   region = var.aws_region
 }
diff --git a/shared/.terraform.lock.hcl b/shared/.terraform.lock.hcl
index 458bcbd..3a02944 100644
--- a/shared/.terraform.lock.hcl
+++ b/shared/.terraform.lock.hcl
@@ -24,6 +24,25 @@ provider "registry.terraform.io/hashicorp/aws" {
   ]
 }
 
+provider "registry.terraform.io/hashicorp/null" {
+  version = "3.2.4"
+  hashes = [
+    "h1:hkf5w5B6q8e2A42ND2CjAvgvSN3puAosDmOJb3zCVQM=",
+    "zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2",
+    "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+    "zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43",
+    "zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a",
+    "zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991",
+    "zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f",
+    "zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e",
+    "zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615",
+    "zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442",
+    "zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5",
+    "zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f",
+    "zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f",
+  ]
+}
+
 provider "registry.terraform.io/hashicorp/tls" {
   version     = "4.1.0"
   constraints = ">= 3.0.0"
diff --git a/shared/README.md b/shared/README.md
index ee2e076..d03e404 100644
--- a/shared/README.md
+++ b/shared/README.md
@@ -100,6 +100,7 @@ For details on the specific resources created, input variables, and outputs gene
 | Name | Version |
 |------|---------|
 | <a name="provider_aws"></a> [aws](#provider\_aws) | 5.95.0 |
+| <a name="provider_terraform"></a> [terraform](#provider\_terraform) | n/a |
 
 ## Modules
 
@@ -110,10 +111,13 @@ For details on the specific resources created, input variables, and outputs gene
 | <a name="module_github_actions_ecr_policy"></a> [github\_actions\_ecr\_policy](#module\_github\_actions\_ecr\_policy) | terraform-aws-modules/iam/aws//modules/iam-policy | 5.55.0 |
 | <a name="module_github_actions_role"></a> [github\_actions\_role](#module\_github\_actions\_role) | terraform-aws-modules/iam/aws//modules/iam-assumable-role-with-oidc | 5.55.0 |
 | <a name="module_github_oidc_provider"></a> [github\_oidc\_provider](#module\_github\_oidc\_provider) | terraform-aws-modules/iam/aws//modules/iam-github-oidc-provider | 5.55.0 |
+| <a name="module_inbound_resolver_endpoint"></a> [inbound\_resolver\_endpoint](#module\_inbound\_resolver\_endpoint) | terraform-aws-modules/route53/aws//modules/resolver-endpoints | ~> 5.0 |
+| <a name="module_internal_domains_resolver_rule"></a> [internal\_domains\_resolver\_rule](#module\_internal\_domains\_resolver\_rule) | AutomateTheCloud/route53_resolver_rule/aws | 1.11.0 |
+| <a name="module_outbound_resolver_endpoint"></a> [outbound\_resolver\_endpoint](#module\_outbound\_resolver\_endpoint) | terraform-aws-modules/route53/aws//modules/resolver-endpoints | ~> 5.0 |
 | <a name="module_piksel_core_ecr"></a> [piksel\_core\_ecr](#module\_piksel\_core\_ecr) | terraform-aws-modules/ecr/aws | 2.4.0 |
-| <a name="module_private_zones"></a> [private\_zones](#module\_private\_zones) | terraform-aws-modules/route53/aws//modules/zones | ~> 3.0 |
 | <a name="module_public_records"></a> [public\_records](#module\_public\_records) | terraform-aws-modules/route53/aws//modules/records | ~> 3.0 |
 | <a name="module_public_zone"></a> [public\_zone](#module\_public\_zone) | terraform-aws-modules/route53/aws//modules/zones | ~> 3.0 |
+| <a name="module_resolver_rule_associations"></a> [resolver\_rule\_associations](#module\_resolver\_rule\_associations) | terraform-aws-modules/route53/aws//modules/resolver-rule-associations | ~> 5.0 |
 | <a name="module_tgw"></a> [tgw](#module\_tgw) | terraform-aws-modules/transit-gateway/aws | ~> 2.0 |
 | <a name="module_vpc"></a> [vpc](#module\_vpc) | terraform-aws-modules/vpc/aws | 5.21.0 |
 | <a name="module_vpc_endpoints"></a> [vpc\_endpoints](#module\_vpc\_endpoints) | terraform-aws-modules/vpc/aws//modules/vpc-endpoints | 5.21.0 |
@@ -122,8 +126,11 @@ For details on the specific resources created, input variables, and outputs gene
 
 | Name | Type |
 |------|------|
+| [aws_route.private_to_dev_via_tgw](https://registry.terraform.io/providers/hashicorp/aws/5.95/docs/resources/route) | resource |
+| [aws_route53_zone.private_hosted_zones_shared](https://registry.terraform.io/providers/hashicorp/aws/5.95/docs/resources/route53_zone) | resource |
 | [aws_availability_zones.available](https://registry.terraform.io/providers/hashicorp/aws/5.95/docs/data-sources/availability_zones) | data source |
 | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/5.95/docs/data-sources/caller_identity) | data source |
+| [terraform_remote_state.dev](https://registry.terraform.io/providers/hashicorp/terraform/latest/docs/data-sources/remote_state) | data source |
 
 ## Inputs
 
@@ -134,11 +141,13 @@ For details on the specific resources created, input variables, and outputs gene
 | <a name="input_ecr_image_tag_mutability"></a> [ecr\_image\_tag\_mutability](#input\_ecr\_image\_tag\_mutability) | Image tag mutability setting for the repository (MUTABLE or IMMUTABLE) | `string` | `"IMMUTABLE"` | no |
 | <a name="input_ecr_max_tagged_images"></a> [ecr\_max\_tagged\_images](#input\_ecr\_max\_tagged\_images) | Maximum number of tagged images to keep | `number` | `5` | no |
 | <a name="input_ecr_untagged_image_retention_days"></a> [ecr\_untagged\_image\_retention\_days](#input\_ecr\_untagged\_image\_retention\_days) | Days to keep untagged images before expiration | `number` | `7` | no |
-| <a name="input_environment"></a> [environment](#input\_environment) | Environment name (should be 'shared' for this directory) | `string` | n/a | yes |
+| <a name="input_environment"></a> [environment](#input\_environment) | Environment name (should be 'Shared' for this directory) | `string` | n/a | yes |
 | <a name="input_internal_domains"></a> [internal\_domains](#input\_internal\_domains) | Map of internal domain names for private hosted zones | `map(string)` | <pre>{<br/>  "dev": "dev.piksel.internal",<br/>  "prod": "prod.piksel.internal",<br/>  "staging": "staging.piksel.internal"<br/>}</pre> | no |
 | <a name="input_project"></a> [project](#input\_project) | Project name used for resource naming and tagging | `string` | n/a | yes |
 | <a name="input_public_dns_records"></a> [public\_dns\_records](#input\_public\_dns\_records) | List of DNS records to create in the public hosted zone | <pre>list(object({<br/>    name    = string<br/>    type    = string<br/>    ttl     = number<br/>    records = list(string)<br/>  }))</pre> | `[]` | no |
 | <a name="input_public_domain_name"></a> [public\_domain\_name](#input\_public\_domain\_name) | List of public domains to create | `string` | `"piksel.big.go.id"` | no |
+| <a name="input_resolver_rule_domain_name"></a> [resolver\_rule\_domain\_name](#input\_resolver\_rule\_domain\_name) | The domain name for which the central FORWARD resolver rule will apply (e.g., company.internal). This domain (and its subdomains) will be resolvable by spoke VPCs. | `string` | n/a | yes |
+| <a name="input_spoke_vpc_cidrs"></a> [spoke\_vpc\_cidrs](#input\_spoke\_vpc\_cidrs) | List of CIDR blocks for spoke VPCs that need to query the inbound resolver | `list(string)` | <pre>[<br/>  "10.0.0.0/16"<br/>]</pre> | no |
 | <a name="input_tgw_ram_principals"></a> [tgw\_ram\_principals](#input\_tgw\_ram\_principals) | List of AWS Account IDs or OU ARNs to share the TGW with. | `list(string)` | `[]` | no |
 | <a name="input_transit_gateway_amazon_side_asn"></a> [transit\_gateway\_amazon\_side\_asn](#input\_transit\_gateway\_amazon\_side\_asn) | Private Autonomous System Number (ASN) for the Amazon side of a BGP session. Required if creating a new TGW. | `number` | `64512` | no |
 | <a name="input_vpc_cidr"></a> [vpc\_cidr](#input\_vpc\_cidr) | CIDR block for the Shared VPC | `string` | n/a | yes |
@@ -153,12 +162,25 @@ For details on the specific resources created, input variables, and outputs gene
 | <a name="output_github_actions_role_arn"></a> [github\_actions\_role\_arn](#output\_github\_actions\_role\_arn) | ARN of the GitHub Actions role for ECR access |
 | <a name="output_github_oidc_provider_arn"></a> [github\_oidc\_provider\_arn](#output\_github\_oidc\_provider\_arn) | ARN of the GitHub OIDC provider |
 | <a name="output_github_oidc_provider_url"></a> [github\_oidc\_provider\_url](#output\_github\_oidc\_provider\_url) | URL of the GitHub OIDC provider |
+| <a name="output_inbound_resolver_arn"></a> [inbound\_resolver\_arn](#output\_inbound\_resolver\_arn) | The ARN of the Inbound Resolver Endpoint. |
+| <a name="output_inbound_resolver_id"></a> [inbound\_resolver\_id](#output\_inbound\_resolver\_id) | The ID of the Inbound Resolver Endpoint. |
+| <a name="output_inbound_resolver_ip_addresses"></a> [inbound\_resolver\_ip\_addresses](#output\_inbound\_resolver\_ip\_addresses) | IP Addresses of the Inbound Resolver Endpoint. |
+| <a name="output_inbound_resolver_security_group_id"></a> [inbound\_resolver\_security\_group\_id](#output\_inbound\_resolver\_security\_group\_id) | Security Group ID used by the Inbound Resolver Endpoint. |
+| <a name="output_internal_domains_target_ips_list"></a> [internal\_domains\_target\_ips\_list](#output\_internal\_domains\_target\_ips\_list) | List of IP addresses for the inbound resolver endpoint in the shared VPC. |
+| <a name="output_outbound_resolver_id"></a> [outbound\_resolver\_id](#output\_outbound\_resolver\_id) | The ID of the Outbound Resolver Endpoint. |
+| <a name="output_outbound_resolver_ip_addresses"></a> [outbound\_resolver\_ip\_addresses](#output\_outbound\_resolver\_ip\_addresses) | IP Addresses of the Outbound Resolver Endpoint. |
+| <a name="output_outbound_resolver_security_group_id"></a> [outbound\_resolver\_security\_group\_id](#output\_outbound\_resolver\_security\_group\_id) | Security Group ID used by the Outbound Resolver Endpoint. |
 | <a name="output_private_subnets"></a> [private\_subnets](#output\_private\_subnets) | List of IDs of private subnets in the Shared VPC |
-| <a name="output_private_zone_arns"></a> [private\_zone\_arns](#output\_private\_zone\_arns) | The ARNs of the private hosted zones |
-| <a name="output_private_zone_ids"></a> [private\_zone\_ids](#output\_private\_zone\_ids) | The IDs of the private hosted zones |
 | <a name="output_public_subnets"></a> [public\_subnets](#output\_public\_subnets) | List of IDs of public subnets in the Shared VPC |
 | <a name="output_public_zone_id"></a> [public\_zone\_id](#output\_public\_zone\_id) | The ID of the public hosted zone |
 | <a name="output_public_zone_name_servers"></a> [public\_zone\_name\_servers](#output\_public\_zone\_name\_servers) | Name servers for the public hosted zone (needed for delegation) |
+| <a name="output_ram_resource_share_arn"></a> [ram\_resource\_share\_arn](#output\_ram\_resource\_share\_arn) | The ARN of the RAM Resource Share used for this resolver rule (if applicable). |
+| <a name="output_resolver_rule_arn"></a> [resolver\_rule\_arn](#output\_resolver\_rule\_arn) | The ARN of the created Route 53 Resolver Rule (from AutomateTheCloud module). |
+| <a name="output_resolver_rule_association_id"></a> [resolver\_rule\_association\_id](#output\_resolver\_rule\_association\_id) | ID of Route53 Resolver rule associations |
+| <a name="output_resolver_rule_association_name"></a> [resolver\_rule\_association\_name](#output\_resolver\_rule\_association\_name) | Name of Route53 Resolver rule associations |
+| <a name="output_resolver_rule_association_resolver_rule_id"></a> [resolver\_rule\_association\_resolver\_rule\_id](#output\_resolver\_rule\_association\_resolver\_rule\_id) | ID of Route53 Resolver rule associations resolver rule |
+| <a name="output_resolver_rule_id"></a> [resolver\_rule\_id](#output\_resolver\_rule\_id) | The ID of the created Route 53 Resolver Rule (from AutomateTheCloud module). |
+| <a name="output_resolver_rule_name"></a> [resolver\_rule\_name](#output\_resolver\_rule\_name) | The actual name of the Route 53 Resolver Rule as created by the module. |
 | <a name="output_transit_gateway_arn"></a> [transit\_gateway\_arn](#output\_transit\_gateway\_arn) | The ARN of the Transit Gateway |
 | <a name="output_transit_gateway_id"></a> [transit\_gateway\_id](#output\_transit\_gateway\_id) | The ID of the Transit Gateway |
 | <a name="output_transit_gateway_subnets"></a> [transit\_gateway\_subnets](#output\_transit\_gateway\_subnets) | List of IDs of private subnets used for TGW attachments in the Shared VPC |
diff --git a/shared/main.tf b/shared/main.tf
index ef709d5..20ebde7 100644
--- a/shared/main.tf
+++ b/shared/main.tf
@@ -2,6 +2,40 @@
 data "aws_caller_identity" "current" {}
 data "aws_availability_zones" "available" {}
 
+data "terraform_remote_state" "dev" {
+  backend = "remote"
+
+  config = {
+    organization = "piksel-ina"
+    workspaces = {
+      name = "piksel-infra-dev"
+    }
+  }
+}
+
+## Uncomment if staging and prod remote states are configured
+# data "terraform_remote_state" "staging" {
+#   backend = "remote"
+
+#   config = {
+#     organization = "piksel-ina"
+#     workspaces = {
+#       name = "piksel-infra-staging"
+#     }
+#   }
+# }
+
+# data "terraform_remote_state" "prod" {
+#   backend = "remote"
+
+#   config = {
+#     organization = "piksel-ina"
+#     workspaces = {
+#       name = "piksel-infra-prod"
+#     }
+#   }
+# }
+###############################################################
 locals {
   name     = "${lower(var.project)}-${lower(var.environment)}" # Should evaluate to piksel-shared
   vpc_cidr = var.vpc_cidr
@@ -151,35 +185,21 @@ module "vpc_endpoints" {
   tags = local.tags # Apply common tags to endpoints
 }
 
-################################################################################
-# Routing (Example: Allow Shared Private Subnets to reach Spoke VPCs via TGW)
-# Note: Spoke VPC routes pointing back to Shared VPC via TGW are configured
-#       in the respective dev/staging/prod Terraform configurations.
-################################################################################
-
-# Explicit routes within the Shared VPC pointing to the TGW might still be needed
-# depending on your setup and whether you rely solely on propagation.
-# The TGW module typically creates necessary routes in the TGW route tables.
-# You might need routes in your *VPC* route tables (e.g., private)
-# pointing destination CIDRs (like spoke VPCs) to the TGW.
-
-# Example (if needed): Route traffic destined for Dev VPC (e.g., 10.0.0.0/16) via TGW
-# resource "aws_route" "private_to_dev_via_tgw" {
-#   count = length(module.vpc.private_route_table_ids)
-#
-#   route_table_id         = module.vpc.private_route_table_ids[count.index]
-#   destination_cidr_block = "10.0.0.0/16" # Replace with actual Dev VPC CIDR
-#   # Reference the TGW ID output from the TGW module
-#   transit_gateway_id     = module.tgw.ec2_transit_gateway_id
-#
-#   # Ensure attachment exists before creating route
-#   depends_on = [module.tgw]
-# }
-# Repeat for staging, prod CIDRs...
-
+#####################################################################
+# Routing (Allow Shared Private Subnets to reach Spoke VPCs via TGW)
+#####################################################################
 
+# Route traffic destined for Dev VPC (10.0.0.0/16) via TGW
+resource "aws_route" "private_to_dev_via_tgw" {
+  count = length(module.vpc.private_route_table_ids)
 
+  route_table_id         = module.vpc.private_route_table_ids[count.index]
+  destination_cidr_block = "10.0.0.0/16"
+  transit_gateway_id     = module.tgw.ec2_transit_gateway_id
 
+  # Ensure attachment exists before creating route
+  depends_on = [module.tgw]
+}
 
 ####################################################################
 # GitHub OIDC Provider
@@ -187,9 +207,8 @@ module "vpc_endpoints" {
 module "github_oidc_provider" {
   source  = "terraform-aws-modules/iam/aws//modules/iam-github-oidc-provider"
   version = "5.55.0"
-
-  create = true
-  tags   = local.tags
+  create  = true
+  tags    = local.tags
 }
 
 ####################################################################
@@ -407,24 +426,6 @@ module "public_zone" {
   }
 }
 
-# Create private hosted zones for internal domains
-module "private_zones" {
-  source  = "terraform-aws-modules/route53/aws//modules/zones"
-  version = "~> 3.0"
-
-  zones = {
-    for env, domain in var.internal_domains : domain => {
-      comment = "${var.project} ${env} private hosted zone"
-      vpc = [
-        {
-          vpc_id = module.vpc.vpc_id
-        }
-      ]
-      tags = local.tags
-    }
-  }
-}
-
 module "public_records" {
   source  = "terraform-aws-modules/route53/aws//modules/records"
   version = "~> 3.0"
@@ -434,3 +435,128 @@ module "public_records" {
 
   records = var.public_dns_records
 }
+
+####################################################################
+# Route53 - Private Hosted Zones
+####################################################################
+
+resource "aws_route53_zone" "private_hosted_zones_shared" {
+  for_each = var.internal_domains # Map of internal domains
+  name     = each.value
+  comment  = "${var.project} ${each.key} private hosted zone (Shared Account)"
+
+  vpc {
+    vpc_id = module.vpc.vpc_id # CRITICAL: This associates the PHZ with SHARED VPC
+  }
+
+  tags = local.tags
+}
+
+####################################################################
+# Route53 Resolver Endpoints and Rules
+####################################################################
+
+# --- INBOUND RESOLVER ENDPOINT ---
+module "inbound_resolver_endpoint" {
+  source  = "terraform-aws-modules/route53/aws//modules/resolver-endpoints"
+  version = "~> 5.0"
+
+  create    = true
+  name      = "${local.name}-inbound-resolver"
+  direction = "INBOUND"
+  vpc_id    = module.vpc.vpc_id
+
+  protocols = ["Do53"]
+
+  # Provide at least two subnets in different AZs
+  ip_address = [
+    { subnet_id = module.vpc.private_subnets[0] },
+    { subnet_id = module.vpc.private_subnets[1] }
+  ]
+
+  # Create security group
+  create_security_group              = true
+  security_group_name                = "${var.project}-resolver-inbound-sg"
+  security_group_description         = "Allow DNS queries to Inbound Resolver Endpoint for ${var.project}"
+  security_group_ingress_cidr_blocks = concat(var.spoke_vpc_cidrs, [module.vpc.vpc_cidr_block])
+
+  tags                = merge(local.tags, { Name = "${var.project}-inbound-resolver" })
+  security_group_tags = merge(local.tags, { Name = "${var.project}-resolver-inbound-sg" })
+}
+
+# --- OUTBOUND RESOLVER ENDPOINT ---
+module "outbound_resolver_endpoint" {
+  source  = "terraform-aws-modules/route53/aws//modules/resolver-endpoints"
+  version = "~> 5.0" # Or your specific desired version
+
+  create    = true
+  name      = "${local.name}-outbound-resolver"
+  direction = "OUTBOUND"
+  vpc_id    = module.vpc.vpc_id
+
+  protocols = ["Do53"]
+
+  ip_address = [
+    { subnet_id = module.vpc.private_subnets[0] },
+    { subnet_id = module.vpc.private_subnets[1] }
+  ]
+
+  # Let the module create and manage the security group
+  create_security_group              = true
+  security_group_name                = "${local.name}-resolver-outbound-sg"
+  security_group_description         = "Allow DNS queries from Outbound Resolver Endpoint for ${var.project}"
+  security_group_ingress_cidr_blocks = [module.vpc.vpc_cidr_block]
+
+  tags                = merge(local.tags, { Name = "${local.name}-outbound-resolver" })
+  security_group_tags = merge(local.tags, { Name = "${local.name}-resolver-outbound-sg" }) # Tags for the SG
+}
+
+##########################################################################
+# RESOLVER RULE
+##########################################################################
+locals {
+  internal_domains_target_ips_list = [
+    for addr in module.inbound_resolver_endpoint.route53_resolver_endpoint_ip_addresses : addr.ip if try(addr.ip, null) != null
+  ]
+}
+
+module "internal_domains_resolver_rule" {
+  source  = "AutomateTheCloud/route53_resolver_rule/aws"
+  version = "1.11.0"
+  providers = {
+    aws.this = aws.shared
+  }
+
+  name = "${local.name}-internal-domains-resolver-rule"
+
+  details = {
+    scope           = "Infrastructure"
+    purpose         = "Route53 Resolver Rule"
+    environment     = var.environment
+    additional_tags = local.tags
+  }
+
+  # --- Rule Configuration ---
+  domain_name          = var.resolver_rule_domain_name
+  resolver_endpoint_id = module.outbound_resolver_endpoint.route53_resolver_endpoint_id
+  rule_type            = "FORWARD" # Default is FORWARD, but explicit is good
+  target_ip            = local.internal_domains_target_ips_list
+
+  # --- List of AWS Account IDs to share this rule with ---
+  account_share = var.tgw_ram_principals
+}
+
+module "resolver_rule_associations" {
+  source  = "terraform-aws-modules/route53/aws//modules/resolver-rule-associations"
+  version = "~> 5.0"
+  create  = true
+
+  resolver_rule_associations = {
+    "dev_vpc_association" = {
+      resolver_rule_id = module.internal_domains_resolver_rule.metadata.route53_resolver_rule.id
+      vpc_id           = data.terraform_remote_state.dev.outputs.vpc_id
+      name             = "piksel-dev-vpc-rule-assoc"
+    }
+  }
+
+}
diff --git a/shared/outputs.tf b/shared/outputs.tf
index c09e7c1..4a1eb99 100644
--- a/shared/outputs.tf
+++ b/shared/outputs.tf
@@ -110,17 +110,80 @@ output "public_zone_name_servers" {
   value       = module.public_zone.route53_zone_name_servers
 }
 
-output "private_zone_ids" {
-  description = "The IDs of the private hosted zones"
-  value       = { for env, domain in var.internal_domains : env => module.private_zones.route53_zone_zone_id[domain] }
+
+## Resolver Outputs
+output "inbound_resolver_id" {
+  description = "The ID of the Inbound Resolver Endpoint."
+  value       = module.inbound_resolver_endpoint.route53_resolver_endpoint_id
+}
+
+output "inbound_resolver_arn" {
+  description = "The ARN of the Inbound Resolver Endpoint."
+  value       = module.inbound_resolver_endpoint.route53_resolver_endpoint_arn
+}
+
+output "inbound_resolver_ip_addresses" {
+  description = "IP Addresses of the Inbound Resolver Endpoint."
+  value       = module.inbound_resolver_endpoint.route53_resolver_endpoint_ip_addresses
+}
+
+output "inbound_resolver_security_group_id" {
+  description = "Security Group ID used by the Inbound Resolver Endpoint."
+  value       = module.inbound_resolver_endpoint.route53_resolver_endpoint_security_group_ids
+}
+
+output "outbound_resolver_id" {
+  description = "The ID of the Outbound Resolver Endpoint."
+  value       = module.outbound_resolver_endpoint.route53_resolver_endpoint_id
+}
+
+output "outbound_resolver_ip_addresses" {
+  description = "IP Addresses of the Outbound Resolver Endpoint."
+  value       = module.outbound_resolver_endpoint.route53_resolver_endpoint_ip_addresses
+}
+
+output "outbound_resolver_security_group_id" {
+  description = "Security Group ID used by the Outbound Resolver Endpoint."
+  value       = module.outbound_resolver_endpoint.route53_resolver_endpoint_security_group_ids
 }
 
-output "private_zone_arns" {
-  description = "The ARNs of the private hosted zones"
-  value       = { for env, domain in var.internal_domains : env => module.private_zones.route53_zone_zone_arn[domain] }
+output "internal_domains_target_ips_list" {
+  description = "List of IP addresses for the inbound resolver endpoint in the shared VPC."
+  value       = local.internal_domains_target_ips_list
 }
 
-# output "ram_resource_share_arn" {
-#   description = "ARN of the RAM resource share for DNS zones"
-#   value       = aws_ram_resource_share.dev_zone_share.arn
-# }
+output "resolver_rule_id" {
+  description = "The ID of the created Route 53 Resolver Rule (from AutomateTheCloud module)."
+  value       = module.internal_domains_resolver_rule.metadata.route53_resolver_rule.id
+}
+
+output "resolver_rule_arn" {
+  description = "The ARN of the created Route 53 Resolver Rule (from AutomateTheCloud module)."
+  value       = module.internal_domains_resolver_rule.metadata.route53_resolver_rule.arn
+}
+
+output "resolver_rule_name" {
+  description = "The actual name of the Route 53 Resolver Rule as created by the module."
+  value       = module.internal_domains_resolver_rule.metadata.route53_resolver_rule.name
+}
+
+output "ram_resource_share_arn" {
+  description = "The ARN of the RAM Resource Share used for this resolver rule (if applicable)."
+  value       = try(module.internal_domains_resolver_rule.metadata.ram_resource_share.arn, null)
+  # Use try() in case sharing is disabled and ram_resource_share is null
+}
+
+output "resolver_rule_association_id" {
+  description = "ID of Route53 Resolver rule associations"
+  value       = module.resolver_rule_associations.route53_resolver_rule_association_id
+}
+
+output "resolver_rule_association_name" {
+  description = "Name of Route53 Resolver rule associations"
+  value       = module.resolver_rule_associations.route53_resolver_rule_association_name
+}
+
+output "resolver_rule_association_resolver_rule_id" {
+  description = "ID of Route53 Resolver rule associations resolver rule"
+  value       = module.resolver_rule_associations.route53_resolver_rule_association_resolver_rule_id
+}
diff --git a/shared/providers.tf b/shared/providers.tf
index c3c0303..911d540 100644
--- a/shared/providers.tf
+++ b/shared/providers.tf
@@ -19,4 +19,5 @@ terraform {
 
 provider "aws" {
   region = var.aws_region
+  alias  = "shared"
 }
diff --git a/shared/shared.auto.tfvars b/shared/shared.auto.tfvars
index fff754b..bc70ea3 100644
--- a/shared/shared.auto.tfvars
+++ b/shared/shared.auto.tfvars
@@ -18,15 +18,15 @@ tgw_ram_principals = [
   "236122835646" # Dev Account ID, add staging and production ID here
 ]
 
-
-# development_account_id = "236122835646"
-
 internal_domains = {
-  dev     = "dev.piksel.internal"
-  staging = "staging.piksel.internal"
-  prod    = "prod.piksel.internal"
+  main_internal = "piksel.internal",
+  dev           = "dev.piksel.internal"
+  # staging = "staging.piksel.internal"
+  # prod    = "prod.piksel.internal"
 }
 
+resolver_rule_domain_name = "piksel.internal"
+
 # Define DNS records
 # public_dns_records = [
 # Uncomment and modify when domain have been registered and set up services
@@ -43,3 +43,7 @@ internal_domains = {
 #   records = ["piksel.big.go.id"]
 # }
 # ]
+
+spoke_vpc_cidrs = [
+  "10.0.0.0/16", # CIDR for Spoke Dev VPC
+]
diff --git a/shared/variables.tf b/shared/variables.tf
index 46b6917..7aeaa45 100644
--- a/shared/variables.tf
+++ b/shared/variables.tf
@@ -12,13 +12,8 @@ variable "project" {
 }
 
 variable "environment" {
-  description = "Environment name (should be 'shared' for this directory)"
+  description = "Environment name (should be 'Shared' for this directory)"
   type        = string
-  validation {
-    # Adjust validation if needed, or rely on tfvars
-    condition     = var.environment == "Shared"
-    error_message = "Environment must be Shared for this configuration."
-  }
 }
 
 variable "common_tags" {
@@ -74,11 +69,6 @@ variable "ecr_untagged_image_retention_days" {
 # DNS Variables
 #########################################################
 
-# variable "development_account_id" {
-#   description = "AWS Account ID for the Development environment"
-#   type        = string
-# }
-
 variable "public_domain_name" {
   description = "List of public domains to create"
   type        = string
@@ -105,3 +95,14 @@ variable "public_dns_records" {
   }))
   default = [] # Empty default so it doesn't create any records if not specified
 }
+
+variable "resolver_rule_domain_name" {
+  description = "The domain name for which the central FORWARD resolver rule will apply (e.g., company.internal). This domain (and its subdomains) will be resolvable by spoke VPCs."
+  type        = string
+}
+
+variable "spoke_vpc_cidrs" {
+  description = "List of CIDR blocks for spoke VPCs that need to query the inbound resolver"
+  type        = list(string)
+  default     = ["10.0.0.0/16"]
+}


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-09T10:51:26Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: NONE
submitted_at: 2025-05-11T12:25:39Z

Review comments:
diff_hunk: @@ -1048,3 +1048,41 @@
     Name = "${local.name}-analyzer"
   })
 }
+
+
+
+################################################################################
+# Route53 Cross Account Role
+################################################################################
+resource "aws_iam_role" "cross_account_route53_role" {
+  name = "CrossAccountRoute53Role"
+
+  assume_role_policy = jsonencode({
+    Version = "2012-10-17"
+    Statement = [{
+      Action = "sts:AssumeRole"
+      Effect = "Allow"
+      Principal = {
+        AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root"
+      }
+    }]
+  })
+}
+
+resource "aws_iam_role_policy" "route53_permissions" {
+  name = "Route53Permissions"
+  role = aws_iam_role.cross_account_route53_role.id
+
+  policy = jsonencode({
+    Version = "2012-10-17"
+    Statement = [{
+      Action = [
+        "route53:AssociateVPCWithHostedZone",
+        "route53:DisassociateVPCWithHostedZone",
+        "ec2:DescribeVpcs"
+      ]
+      Effect   = "Allow"
+      Resource = "*"
+    }]
+  })
+}
path: dev/main.tf
user: login: github-advanced-security[bot]
body: ## IAM policy should avoid use of wildcards and instead apply the principle of least privilege

IAM policy document uses sensitive action 'route53:AssociateVPCWithHostedZone' on wildcarded resource '*'

[Show more details](https://github.com/piksel-ina/piksel-infra/security/code-scanning/33)
created_at: 2025-05-09T10:51:25Z
author_association: NONE
diff_hunk: @@ -434,3 +415,155 @@
 
   records = var.public_dns_records
 }
+
+####################################################################
+# Route53 - Private Hosted Zones
+####################################################################
+
+resource "aws_route53_zone" "private_hosted_zones_shared" {
+  for_each = var.internal_domains # Map of internal domains
+  name     = each.value
+  comment  = "${var.project} ${each.key} private hosted zone (Shared Account)"
+
+  vpc {
+    vpc_id = module.vpc.vpc_id # CRITICAL: This associates the PHZ with SHARED VPC
+  }
+
+  tags = local.tags
+}
+
+####################################################################
+# Route53 Resolver Endpoints and Rules
+####################################################################
+
+resource "aws_security_group" "resolver_inbound_sg" {
+  name        = "${var.project}-resolver-inbound-sg"
+  description = "Allow DNS queries to Inbound Resolver Endpoint"
+  vpc_id      = module.vpc.vpc_id
+
+  ingress {
+    description = "DNS from Spoke VPCs"
+    from_port   = 53
+    to_port     = 53
+    protocol    = "tcp"
+    cidr_blocks = concat(var.spoke_vpc_cidrs, [module.vpc.vpc_cidr_block])
+  }
+  ingress {
+    description = "DNS from Spoke VPCs"
+    from_port   = 53
+    to_port     = 53
+    protocol    = "udp"
+    cidr_blocks = concat(var.spoke_vpc_cidrs, [module.vpc.vpc_cidr_block])
+  }
+  egress { # Typically, inbound resolvers don't need much egress unless they forward elsewhere
+    from_port   = 0
+    to_port     = 0
+    protocol    = "-1"
+    cidr_blocks = ["0.0.0.0/0"] # Or restrict as needed
+  }
+  tags = merge(local.tags, { Name = "${var.project}-resolver-inbound-sg" })
+}
path: shared/main.tf
user: login: github-advanced-security[bot]
body: ## Missing description for security group rule.

Security group rule does not have a description.

[Show more details](https://github.com/piksel-ina/piksel-infra/security/code-scanning/34)
created_at: 2025-05-09T10:51:26Z
author_association: NONE
diff_hunk: @@ -434,3 +415,155 @@
 
   records = var.public_dns_records
 }
+
+####################################################################
+# Route53 - Private Hosted Zones
+####################################################################
+
+resource "aws_route53_zone" "private_hosted_zones_shared" {
+  for_each = var.internal_domains # Map of internal domains
+  name     = each.value
+  comment  = "${var.project} ${each.key} private hosted zone (Shared Account)"
+
+  vpc {
+    vpc_id = module.vpc.vpc_id # CRITICAL: This associates the PHZ with SHARED VPC
+  }
+
+  tags = local.tags
+}
+
+####################################################################
+# Route53 Resolver Endpoints and Rules
+####################################################################
+
+resource "aws_security_group" "resolver_inbound_sg" {
+  name        = "${var.project}-resolver-inbound-sg"
+  description = "Allow DNS queries to Inbound Resolver Endpoint"
+  vpc_id      = module.vpc.vpc_id
+
+  ingress {
+    description = "DNS from Spoke VPCs"
+    from_port   = 53
+    to_port     = 53
+    protocol    = "tcp"
+    cidr_blocks = concat(var.spoke_vpc_cidrs, [module.vpc.vpc_cidr_block])
+  }
+  ingress {
+    description = "DNS from Spoke VPCs"
+    from_port   = 53
+    to_port     = 53
+    protocol    = "udp"
+    cidr_blocks = concat(var.spoke_vpc_cidrs, [module.vpc.vpc_cidr_block])
+  }
+  egress { # Typically, inbound resolvers don't need much egress unless they forward elsewhere
+    from_port   = 0
+    to_port     = 0
+    protocol    = "-1"
+    cidr_blocks = ["0.0.0.0/0"] # Or restrict as needed
+  }
+  tags = merge(local.tags, { Name = "${var.project}-resolver-inbound-sg" })
+}
path: shared/main.tf
user: login: github-advanced-security[bot]
body: ## An egress security group rule allows traffic to /0.

Security group rule allows egress to multiple public internet addresses.

[Show more details](https://github.com/piksel-ina/piksel-infra/security/code-scanning/37)
created_at: 2025-05-09T10:51:26Z
author_association: NONE
diff_hunk: @@ -434,3 +415,155 @@
 
   records = var.public_dns_records
 }
+
+####################################################################
+# Route53 - Private Hosted Zones
+####################################################################
+
+resource "aws_route53_zone" "private_hosted_zones_shared" {
+  for_each = var.internal_domains # Map of internal domains
+  name     = each.value
+  comment  = "${var.project} ${each.key} private hosted zone (Shared Account)"
+
+  vpc {
+    vpc_id = module.vpc.vpc_id # CRITICAL: This associates the PHZ with SHARED VPC
+  }
+
+  tags = local.tags
+}
+
+####################################################################
+# Route53 Resolver Endpoints and Rules
+####################################################################
+
+resource "aws_security_group" "resolver_inbound_sg" {
+  name        = "${var.project}-resolver-inbound-sg"
+  description = "Allow DNS queries to Inbound Resolver Endpoint"
+  vpc_id      = module.vpc.vpc_id
+
+  ingress {
+    description = "DNS from Spoke VPCs"
+    from_port   = 53
+    to_port     = 53
+    protocol    = "tcp"
+    cidr_blocks = concat(var.spoke_vpc_cidrs, [module.vpc.vpc_cidr_block])
+  }
+  ingress {
+    description = "DNS from Spoke VPCs"
+    from_port   = 53
+    to_port     = 53
+    protocol    = "udp"
+    cidr_blocks = concat(var.spoke_vpc_cidrs, [module.vpc.vpc_cidr_block])
+  }
+  egress { # Typically, inbound resolvers don't need much egress unless they forward elsewhere
+    from_port   = 0
+    to_port     = 0
+    protocol    = "-1"
+    cidr_blocks = ["0.0.0.0/0"] # Or restrict as needed
+  }
+  tags = merge(local.tags, { Name = "${var.project}-resolver-inbound-sg" })
+}
+
+resource "aws_security_group" "resolver_outbound_sg" {
+  name        = "${var.project}-resolver-outbound-sg"
+  description = "Allow DNS queries from Outbound Resolver Endpoint"
+  vpc_id      = module.vpc.vpc_id
+
+  # Ingress for outbound is usually not strictly needed unless for health checks from within VPC
+  # egress is the important part for outbound resolvers
+  egress {
+    description = "DNS to internal and external resolvers (TCP)"
+    from_port   = 53
+    to_port     = 53
+    protocol    = "tcp"
+    cidr_blocks = ["0.0.0.0/0"] # Allows forwarding to any DNS server
+  }
+  egress {
+    description = "DNS to internal and external resolvers (UDP)"
+    from_port   = 53
+    to_port     = 53
+    protocol    = "udp"
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+  tags = merge(local.tags, { Name = "${var.project}-resolver-outbound-sg" })
+}
path: shared/main.tf
user: login: github-advanced-security[bot]
body: ## An egress security group rule allows traffic to /0.

Security group rule allows egress to multiple public internet addresses.

[Show more details](https://github.com/piksel-ina/piksel-infra/security/code-scanning/35)
created_at: 2025-05-09T10:51:26Z
author_association: NONE
diff_hunk: @@ -434,3 +415,155 @@
 
   records = var.public_dns_records
 }
+
+####################################################################
+# Route53 - Private Hosted Zones
+####################################################################
+
+resource "aws_route53_zone" "private_hosted_zones_shared" {
+  for_each = var.internal_domains # Map of internal domains
+  name     = each.value
+  comment  = "${var.project} ${each.key} private hosted zone (Shared Account)"
+
+  vpc {
+    vpc_id = module.vpc.vpc_id # CRITICAL: This associates the PHZ with SHARED VPC
+  }
+
+  tags = local.tags
+}
+
+####################################################################
+# Route53 Resolver Endpoints and Rules
+####################################################################
+
+resource "aws_security_group" "resolver_inbound_sg" {
+  name        = "${var.project}-resolver-inbound-sg"
+  description = "Allow DNS queries to Inbound Resolver Endpoint"
+  vpc_id      = module.vpc.vpc_id
+
+  ingress {
+    description = "DNS from Spoke VPCs"
+    from_port   = 53
+    to_port     = 53
+    protocol    = "tcp"
+    cidr_blocks = concat(var.spoke_vpc_cidrs, [module.vpc.vpc_cidr_block])
+  }
+  ingress {
+    description = "DNS from Spoke VPCs"
+    from_port   = 53
+    to_port     = 53
+    protocol    = "udp"
+    cidr_blocks = concat(var.spoke_vpc_cidrs, [module.vpc.vpc_cidr_block])
+  }
+  egress { # Typically, inbound resolvers don't need much egress unless they forward elsewhere
+    from_port   = 0
+    to_port     = 0
+    protocol    = "-1"
+    cidr_blocks = ["0.0.0.0/0"] # Or restrict as needed
+  }
+  tags = merge(local.tags, { Name = "${var.project}-resolver-inbound-sg" })
+}
+
+resource "aws_security_group" "resolver_outbound_sg" {
+  name        = "${var.project}-resolver-outbound-sg"
+  description = "Allow DNS queries from Outbound Resolver Endpoint"
+  vpc_id      = module.vpc.vpc_id
+
+  # Ingress for outbound is usually not strictly needed unless for health checks from within VPC
+  # egress is the important part for outbound resolvers
+  egress {
+    description = "DNS to internal and external resolvers (TCP)"
+    from_port   = 53
+    to_port     = 53
+    protocol    = "tcp"
+    cidr_blocks = ["0.0.0.0/0"] # Allows forwarding to any DNS server
+  }
+  egress {
+    description = "DNS to internal and external resolvers (UDP)"
+    from_port   = 53
+    to_port     = 53
+    protocol    = "udp"
+    cidr_blocks = ["0.0.0.0/0"]
+  }
+  tags = merge(local.tags, { Name = "${var.project}-resolver-outbound-sg" })
+}
path: shared/main.tf
user: login: github-advanced-security[bot]
body: ## An egress security group rule allows traffic to /0.

Security group rule allows egress to multiple public internet addresses.

[Show more details](https://github.com/piksel-ina/piksel-infra/security/code-scanning/36)
created_at: 2025-05-09T10:51:26Z
author_association: NONE
diff_hunk: @@ -434,3 +449,131 @@
 
   records = var.public_dns_records
 }
+
+####################################################################
+# Route53 - Private Hosted Zones
+####################################################################
+
+resource "aws_route53_zone" "private_hosted_zones_shared" {
+  for_each = var.internal_domains # Map of internal domains
+  name     = each.value
+  comment  = "${var.project} ${each.key} private hosted zone (Shared Account)"
+
+  vpc {
+    vpc_id = module.vpc.vpc_id # CRITICAL: This associates the PHZ with SHARED VPC
+  }
+
+  tags = local.tags
+}
+
+####################################################################
+# Route53 Resolver Endpoints and Rules
+####################################################################
+
+# --- INBOUND RESOLVER ENDPOINT ---
+module "inbound_resolver_endpoint" {
+  source  = "terraform-aws-modules/route53/aws//modules/resolver-endpoints"
+  version = "~> 5.0"
+
+  create    = true
+  name      = "${local.name}-inbound-resolver"
+  direction = "INBOUND"
+  vpc_id    = module.vpc.vpc_id
+
+  protocols = ["Do53"]
+
+  # Provide at least two subnets in different AZs
+  ip_address = [
+    { subnet_id = module.vpc.private_subnets[0] },
+    { subnet_id = module.vpc.private_subnets[1] }
+  ]
+
+  # Create security group
+  create_security_group              = true
+  security_group_name                = "${var.project}-resolver-inbound-sg"
+  security_group_description         = "Allow DNS queries to Inbound Resolver Endpoint for ${var.project}"
+  security_group_ingress_cidr_blocks = concat(var.spoke_vpc_cidrs, [module.vpc.vpc_cidr_block])
+
+  tags                = merge(local.tags, { Name = "${var.project}-inbound-resolver" })
+  security_group_tags = merge(local.tags, { Name = "${var.project}-resolver-inbound-sg" })
+}
+
+# --- OUTBOUND RESOLVER ENDPOINT ---
+module "outbound_resolver_endpoint" {
+  source  = "terraform-aws-modules/route53/aws//modules/resolver-endpoints"
+  version = "~> 5.0" # Or your specific desired version
+
+  create    = true
+  name      = "${local.name}-outbound-resolver"
+  direction = "OUTBOUND"
+  vpc_id    = module.vpc.vpc_id
+
+  protocols = ["Do53"]
+
+  ip_address = [
+    { subnet_id = module.vpc.private_subnets[0] },
+    { subnet_id = module.vpc.private_subnets[1] }
+  ]
+
+  # Let the module create and manage the security group
+  create_security_group = true
+  security_group_name   = "${local.name}-resolver-outbound-sg"
+  # security_group_description = "Allow DNS queries from Outbound Resolver Endpoint for ${var.project}" # Optional
+  # Ingress: Allow VPC resources to query this outbound endpoint
+  security_group_ingress_cidr_blocks = [module.vpc.vpc_cidr_block] # Or more specific CIDRs from your VPC
+  # Egress for the SG will default to allow TCP/UDP 53 to 0.0.0.0/0, allowing it to forward to any DNS server.
+  security_group_egress_cidr_blocks = ["0.0.0.0/0"] # Explicitly set, or rely on module default
+
+  tags                = merge(local.tags, { Name = "${local.name}-outbound-resolver" })
+  security_group_tags = merge(local.tags, { Name = "${local.name}-resolver-outbound-sg" }) # Tags for the SG
+}
path: shared/main.tf
user: login: github-advanced-security[bot]
body: ## An egress security group rule allows traffic to /0.

Security group rule allows egress to multiple public internet addresses.

[Show more details](https://github.com/piksel-ina/piksel-infra/security/code-scanning/38)
created_at: 2025-05-11T12:25:39Z
author_association: NONE
diff_hunk: @@ -434,3 +449,131 @@
 
   records = var.public_dns_records
 }
+
+####################################################################
+# Route53 - Private Hosted Zones
+####################################################################
+
+resource "aws_route53_zone" "private_hosted_zones_shared" {
+  for_each = var.internal_domains # Map of internal domains
+  name     = each.value
+  comment  = "${var.project} ${each.key} private hosted zone (Shared Account)"
+
+  vpc {
+    vpc_id = module.vpc.vpc_id # CRITICAL: This associates the PHZ with SHARED VPC
+  }
+
+  tags = local.tags
+}
+
+####################################################################
+# Route53 Resolver Endpoints and Rules
+####################################################################
+
+# --- INBOUND RESOLVER ENDPOINT ---
+module "inbound_resolver_endpoint" {
+  source  = "terraform-aws-modules/route53/aws//modules/resolver-endpoints"
+  version = "~> 5.0"
+
+  create    = true
+  name      = "${local.name}-inbound-resolver"
+  direction = "INBOUND"
+  vpc_id    = module.vpc.vpc_id
+
+  protocols = ["Do53"]
+
+  # Provide at least two subnets in different AZs
+  ip_address = [
+    { subnet_id = module.vpc.private_subnets[0] },
+    { subnet_id = module.vpc.private_subnets[1] }
+  ]
+
+  # Create security group
+  create_security_group              = true
+  security_group_name                = "${var.project}-resolver-inbound-sg"
+  security_group_description         = "Allow DNS queries to Inbound Resolver Endpoint for ${var.project}"
+  security_group_ingress_cidr_blocks = concat(var.spoke_vpc_cidrs, [module.vpc.vpc_cidr_block])
+
+  tags                = merge(local.tags, { Name = "${var.project}-inbound-resolver" })
+  security_group_tags = merge(local.tags, { Name = "${var.project}-resolver-inbound-sg" })
+}
+
+# --- OUTBOUND RESOLVER ENDPOINT ---
+module "outbound_resolver_endpoint" {
+  source  = "terraform-aws-modules/route53/aws//modules/resolver-endpoints"
+  version = "~> 5.0" # Or your specific desired version
+
+  create    = true
+  name      = "${local.name}-outbound-resolver"
+  direction = "OUTBOUND"
+  vpc_id    = module.vpc.vpc_id
+
+  protocols = ["Do53"]
+
+  ip_address = [
+    { subnet_id = module.vpc.private_subnets[0] },
+    { subnet_id = module.vpc.private_subnets[1] }
+  ]
+
+  # Let the module create and manage the security group
+  create_security_group = true
+  security_group_name   = "${local.name}-resolver-outbound-sg"
+  # security_group_description = "Allow DNS queries from Outbound Resolver Endpoint for ${var.project}" # Optional
+  # Ingress: Allow VPC resources to query this outbound endpoint
+  security_group_ingress_cidr_blocks = [module.vpc.vpc_cidr_block] # Or more specific CIDRs from your VPC
+  # Egress for the SG will default to allow TCP/UDP 53 to 0.0.0.0/0, allowing it to forward to any DNS server.
+  security_group_egress_cidr_blocks = ["0.0.0.0/0"] # Explicitly set, or rely on module default
+
+  tags                = merge(local.tags, { Name = "${local.name}-outbound-resolver" })
+  security_group_tags = merge(local.tags, { Name = "${local.name}-resolver-outbound-sg" }) # Tags for the SG
+}
path: shared/main.tf
user: login: github-advanced-security[bot]
body: ## An egress security group rule allows traffic to /0.

Security group rule allows egress to multiple public internet addresses.

[Show more details](https://github.com/piksel-ina/piksel-infra/security/code-scanning/39)
created_at: 2025-05-11T12:25:39Z
author_association: NONE
diff_hunk: @@ -434,3 +449,131 @@
 
   records = var.public_dns_records
 }
+
+####################################################################
+# Route53 - Private Hosted Zones
+####################################################################
+
+resource "aws_route53_zone" "private_hosted_zones_shared" {
+  for_each = var.internal_domains # Map of internal domains
+  name     = each.value
+  comment  = "${var.project} ${each.key} private hosted zone (Shared Account)"
+
+  vpc {
+    vpc_id = module.vpc.vpc_id # CRITICAL: This associates the PHZ with SHARED VPC
+  }
+
+  tags = local.tags
+}
+
+####################################################################
+# Route53 Resolver Endpoints and Rules
+####################################################################
+
+# --- INBOUND RESOLVER ENDPOINT ---
+module "inbound_resolver_endpoint" {
+  source  = "terraform-aws-modules/route53/aws//modules/resolver-endpoints"
+  version = "~> 5.0"
+
+  create    = true
+  name      = "${local.name}-inbound-resolver"
+  direction = "INBOUND"
+  vpc_id    = module.vpc.vpc_id
+
+  protocols = ["Do53"]
+
+  # Provide at least two subnets in different AZs
+  ip_address = [
+    { subnet_id = module.vpc.private_subnets[0] },
+    { subnet_id = module.vpc.private_subnets[1] }
+  ]
+
+  # Create security group
+  create_security_group              = true
+  security_group_name                = "${var.project}-resolver-inbound-sg"
+  security_group_description         = "Allow DNS queries to Inbound Resolver Endpoint for ${var.project}"
+  security_group_ingress_cidr_blocks = concat(var.spoke_vpc_cidrs, [module.vpc.vpc_cidr_block])
+
+  tags                = merge(local.tags, { Name = "${var.project}-inbound-resolver" })
+  security_group_tags = merge(local.tags, { Name = "${var.project}-resolver-inbound-sg" })
+}
+
+# --- OUTBOUND RESOLVER ENDPOINT ---
+module "outbound_resolver_endpoint" {
+  source  = "terraform-aws-modules/route53/aws//modules/resolver-endpoints"
+  version = "~> 5.0" # Or your specific desired version
+
+  create    = true
+  name      = "${local.name}-outbound-resolver"
+  direction = "OUTBOUND"
+  vpc_id    = module.vpc.vpc_id
+
+  protocols = ["Do53"]
+
+  ip_address = [
+    { subnet_id = module.vpc.private_subnets[0] },
+    { subnet_id = module.vpc.private_subnets[1] }
+  ]
+
+  # Let the module create and manage the security group
+  create_security_group = true
+  security_group_name   = "${local.name}-resolver-outbound-sg"
+  # security_group_description = "Allow DNS queries from Outbound Resolver Endpoint for ${var.project}" # Optional
+  # Ingress: Allow VPC resources to query this outbound endpoint
+  security_group_ingress_cidr_blocks = [module.vpc.vpc_cidr_block] # Or more specific CIDRs from your VPC
+  # Egress for the SG will default to allow TCP/UDP 53 to 0.0.0.0/0, allowing it to forward to any DNS server.
+  security_group_egress_cidr_blocks = ["0.0.0.0/0"] # Explicitly set, or rely on module default
+
+  tags                = merge(local.tags, { Name = "${local.name}-outbound-resolver" })
+  security_group_tags = merge(local.tags, { Name = "${local.name}-resolver-outbound-sg" }) # Tags for the SG
+}
path: shared/main.tf
user: login: github-advanced-security[bot]
body: ## An egress security group rule allows traffic to /0.

Security group rule allows egress to multiple public internet addresses.

[Show more details](https://github.com/piksel-ina/piksel-infra/security/code-scanning/40)
created_at: 2025-05-11T12:25:39Z
author_association: NONE
diff_hunk: @@ -434,3 +449,131 @@
 
   records = var.public_dns_records
 }
+
+####################################################################
+# Route53 - Private Hosted Zones
+####################################################################
+
+resource "aws_route53_zone" "private_hosted_zones_shared" {
+  for_each = var.internal_domains # Map of internal domains
+  name     = each.value
+  comment  = "${var.project} ${each.key} private hosted zone (Shared Account)"
+
+  vpc {
+    vpc_id = module.vpc.vpc_id # CRITICAL: This associates the PHZ with SHARED VPC
+  }
+
+  tags = local.tags
+}
+
+####################################################################
+# Route53 Resolver Endpoints and Rules
+####################################################################
+
+# --- INBOUND RESOLVER ENDPOINT ---
+module "inbound_resolver_endpoint" {
+  source  = "terraform-aws-modules/route53/aws//modules/resolver-endpoints"
+  version = "~> 5.0"
+
+  create    = true
+  name      = "${local.name}-inbound-resolver"
+  direction = "INBOUND"
+  vpc_id    = module.vpc.vpc_id
+
+  protocols = ["Do53"]
+
+  # Provide at least two subnets in different AZs
+  ip_address = [
+    { subnet_id = module.vpc.private_subnets[0] },
+    { subnet_id = module.vpc.private_subnets[1] }
+  ]
+
+  # Create security group
+  create_security_group              = true
+  security_group_name                = "${var.project}-resolver-inbound-sg"
+  security_group_description         = "Allow DNS queries to Inbound Resolver Endpoint for ${var.project}"
+  security_group_ingress_cidr_blocks = concat(var.spoke_vpc_cidrs, [module.vpc.vpc_cidr_block])
+
+  tags                = merge(local.tags, { Name = "${var.project}-inbound-resolver" })
+  security_group_tags = merge(local.tags, { Name = "${var.project}-resolver-inbound-sg" })
+}
+
+# --- OUTBOUND RESOLVER ENDPOINT ---
+module "outbound_resolver_endpoint" {
+  source  = "terraform-aws-modules/route53/aws//modules/resolver-endpoints"
+  version = "~> 5.0" # Or your specific desired version
+
+  create    = true
+  name      = "${local.name}-outbound-resolver"
+  direction = "OUTBOUND"
+  vpc_id    = module.vpc.vpc_id
+
+  protocols = ["Do53"]
+
+  ip_address = [
+    { subnet_id = module.vpc.private_subnets[0] },
+    { subnet_id = module.vpc.private_subnets[1] }
+  ]
+
+  # Let the module create and manage the security group
+  create_security_group = true
+  security_group_name   = "${local.name}-resolver-outbound-sg"
+  # security_group_description = "Allow DNS queries from Outbound Resolver Endpoint for ${var.project}" # Optional
+  # Ingress: Allow VPC resources to query this outbound endpoint
+  security_group_ingress_cidr_blocks = [module.vpc.vpc_cidr_block] # Or more specific CIDRs from your VPC
+  # Egress for the SG will default to allow TCP/UDP 53 to 0.0.0.0/0, allowing it to forward to any DNS server.
+  security_group_egress_cidr_blocks = ["0.0.0.0/0"] # Explicitly set, or rely on module default
+
+  tags                = merge(local.tags, { Name = "${local.name}-outbound-resolver" })
+  security_group_tags = merge(local.tags, { Name = "${local.name}-resolver-outbound-sg" }) # Tags for the SG
+}
path: shared/main.tf
user: login: github-advanced-security[bot]
body: ## An egress security group rule allows traffic to /0.

Security group rule allows egress to multiple public internet addresses.

[Show more details](https://github.com/piksel-ina/piksel-infra/security/code-scanning/41)
created_at: 2025-05-11T12:25:39Z
author_association: NONE

PR comments:
user: login: infracost[bot]
created_at: 2025-05-09T10:50:37Z
body: [//]: <> (infracost-ghapp-comment, valid-at=2025-05-11T12:42:00Z)
<h3>💰 Infracost report</h3>

This pull request is aligned with your company's FinOps policies and the Well-Architected Framework.
<details >
  <summary><b>Monthly estimate increased by $244 📈</b></summary>
  <br/>

<table>
  <thead>
    <td>Changed project</td>
    <td><span title="Baseline costs are consistent charges for provisioned resources, like the hourly cost for a virtual machine, which stays constant no matter how much it is used. Infracost estimates these resources assuming they are used for the whole month (730 hours).">Baseline cost</span></td>
    <td><span title="Usage costs are charges based on actual usage, like the storage cost for an object storage bucket. Infracost estimates these resources using the monthly usage values in the usage-file.">Usage cost</span>*</td>
    <td>Total change</td>
    <td>New monthly cost</td>
  </thead>
  <tbody>
    <tr>
      <td>dev</td>
      <td align="right">-$49</td>
      <td align="right">-</td>
      <td align="right">-$49 (-29%)</td>
      <td align="right">$121</td>
    </tr>
    <tr>
      <td>shared</td>
      <td align="right">+$293</td>
      <td align="right">-</td>
      <td align="right">+$293 (+99%)</td>
      <td align="right">$589</td>
    </tr>
  </tbody>
</table>

*Usage costs can be estimated by updating [Infracost Cloud settings](https://dashboard.infracost.io/org/tudpt/settings/usage-cost-defaults), see [docs](https://www.infracost.io/docs/features/usage_based_resources/#infracost-usageyml) for other options.

  <details>
  <summary>Estimate details (includes details of unsupported resources)</summary>

```
Key: * usage cost, ~ changed, + added, - removed

──────────────────────────────────
Project: dev
Module path: dev

~ aws_sns_topic.monitoring_alerts
  Monthly cost depends on usage

    ~ Kinesis Firehose notifications
      Monthly cost depends on usage
        -$0.064 per 1M notifications ($0.25 → $0.19)

    ~ Mobile Push notifications
      Monthly cost depends on usage
        -$0.12 per 1M notifications ($0.62 → $0.50)

    ~ MacOS notifications
      Monthly cost depends on usage
        -$0.12 per 1M notifications ($0.62 → $0.50)

~ module.s3_bucket_data.aws_s3_bucket.this[0]
  Monthly cost depends on usage

    ~ Standard

        ~ Storage
          Monthly cost depends on usage
            -$0.002 per GB ($0.025 → $0.023)

        ~ Select data scanned
          Monthly cost depends on usage
            -$0.0005 per GB ($0.0025 → $0.002)

        ~ Select data returned
          Monthly cost depends on usage
            -$0.0002 per GB ($0.0009 → $0.0007)

~ module.s3_bucket_notebooks.aws_s3_bucket.this[0]
  Monthly cost depends on usage

    ~ Standard

        ~ Storage
          Monthly cost depends on usage
            -$0.002 per GB ($0.025 → $0.023)

        ~ Select data scanned
          Monthly cost depends on usage
            -$0.0005 per GB ($0.0025 → $0.002)

        ~ Select data returned
          Monthly cost depends on usage
            -$0.0002 per GB ($0.0009 → $0.0007)

~ module.s3_bucket_web.aws_s3_bucket.this[0]
  Monthly cost depends on usage

    ~ Standard

        ~ Storage
          Monthly cost depends on usage
            -$0.002 per GB ($0.025 → $0.023)

        ~ Select data scanned
          Monthly cost depends on usage
            -$0.0005 per GB ($0.0025 → $0.002)

        ~ Select data returned
          Monthly cost depends on usage
            -$0.0002 per GB ($0.0009 → $0.0007)

~ module.s3_log_bucket.aws_s3_bucket.this[0]
  Monthly cost depends on usage

    ~ Standard

        ~ Storage
          Monthly cost depends on usage
            -$0.002 per GB ($0.025 → $0.023)

        ~ Select data scanned
          Monthly cost depends on usage
            -$0.0005 per GB ($0.0025 → $0.002)

        ~ Select data returned
          Monthly cost depends on usage
            -$0.0002 per GB ($0.0009 → $0.0007)

~ module.vpc.aws_cloudwatch_log_group.flow_log[0]
  Monthly cost depends on usage

    ~ Data ingested
      Monthly cost depends on usage
        -$0.20 per GB ($0.70 → $0.50)

    ~ Insights queries data scanned
      Monthly cost depends on usage
        -$0.002 per GB ($0.007 → $0.005)

~ module.vpc_endpoints.aws_vpc_endpoint.this["ecr_api"]
  -$4 ($19 → $15)

    ~ Endpoint (Interface)
      -$4 ($19 → $15)

~ module.vpc_endpoints.aws_vpc_endpoint.this["ecr_dkr"]
  -$4 ($19 → $15)

    ~ Endpoint (Interface)
      -$4 ($19 → $15)

~ module.vpc.aws_nat_gateway.this[0]
  -$10 ($43 → $33)

    ~ NAT gateway
      -$10 ($43 → $33)

    ~ Data processed
      Monthly cost depends on usage
        -$0.014 per GB ($0.059 → $0.045)

~ module.odc_rds.module.db_instance.aws_db_instance.this[0]
  -$30 ($87 → $57)

    ~ Database instance (on-demand, Multi-AZ, db.t3.small)
      -$29 ($82 → $53)

    ~ Storage (general purpose SSD, gp3)
      -$0.92 ($6 → $5)

    ~ Performance Insights API
      Monthly cost depends on usage
        +$0.01 per 1000 requests ($0.00 → $0.01)

Monthly cost change for dev (Module path: dev)
Amount:  -$49 ($170 → $121)
Percent: -29%

──────────────────────────────────
Project: shared
Module path: shared

+ module.inbound_resolver_endpoint.aws_route53_resolver_endpoint.this[0]
  +$183

    + Resolver endpoints
      +$183

    + DNS queries (first 1B)
      Monthly cost depends on usage
        +$0.40 per 1M queries

+ module.outbound_resolver_endpoint.aws_route53_resolver_endpoint.this[0]
  +$183

    + Resolver endpoints
      +$183

    + DNS queries (first 1B)
      Monthly cost depends on usage
        +$0.40 per 1M queries

+ aws_route53_zone.private_hosted_zones_shared["dev"]
  +$0.50

    + Hosted zone
      +$0.50

+ aws_route53_zone.private_hosted_zones_shared["main_internal"]
  +$0.50

    + Hosted zone
      +$0.50

~ module.vpc.aws_cloudwatch_log_group.flow_log[0]
  Monthly cost depends on usage

    ~ Data ingested
      Monthly cost depends on usage
        -$0.20 per GB ($0.70 → $0.50)

    ~ Insights queries data scanned
      Monthly cost depends on usage
        -$0.002 per GB ($0.007 → $0.005)

- module.private_zones.aws_route53_zone.this["dev.piksel.internal"]
  -$0.50

    - Hosted zone
      -$0.50

- module.private_zones.aws_route53_zone.this["prod.piksel.internal"]
  -$0.50

    - Hosted zone
      -$0.50

- module.private_zones.aws_route53_zone.this["staging.piksel.internal"]
  -$0.50

    - Hosted zone
      -$0.50

~ module.vpc_endpoints.aws_vpc_endpoint.this["ecr_api"]
  -$7 ($28 → $22)

    ~ Endpoint (Interface)
      -$7 ($28 → $22)

~ module.vpc_endpoints.aws_vpc_endpoint.this["ecr_dkr"]
  -$7 ($28 → $22)

    ~ Endpoint (Interface)
      -$7 ($28 → $22)

~ module.vpc_endpoints.aws_vpc_endpoint.this["logs"]
  -$7 ($28 → $22)

    ~ Endpoint (Interface)
      -$7 ($28 → $22)

~ module.vpc_endpoints.aws_vpc_endpoint.this["sts"]
  -$7 ($28 → $22)

    ~ Endpoint (Interface)
      -$7 ($28 → $22)

~ module.vpc.aws_nat_gateway.this[0]
  -$10 ($43 → $33)

    ~ NAT gateway
      -$10 ($43 → $33)

    ~ Data processed
      Monthly cost depends on usage
        -$0.014 per GB ($0.059 → $0.045)

~ module.vpc.aws_nat_gateway.this[1]
  -$10 ($43 → $33)

    ~ NAT gateway
      -$10 ($43 → $33)

    ~ Data processed
      Monthly cost depends on usage
        -$0.014 per GB ($0.059 → $0.045)

~ module.vpc.aws_nat_gateway.this[2]
  -$10 ($43 → $33)

    ~ NAT gateway
      -$10 ($43 → $33)

    ~ Data processed
      Monthly cost depends on usage
        -$0.014 per GB ($0.059 → $0.045)

~ module.tgw.aws_ec2_transit_gateway_vpc_attachment.this["shared_vpc"]
  -$15 ($51 → $37)

    ~ Transit gateway attachment
      -$15 ($51 → $37)

Monthly cost change for shared (Module path: shared)
Amount:  +$293 ($296 → $589)
Percent: +99%

──────────────────────────────────
Key: * usage cost, ~ changed, + added, - removed

*Usage costs can be estimated by updating Infracost Cloud settings, see docs for other options.

175 cloud resources were detected:
∙ 33 were estimated
∙ 141 were free
∙ 1 is not supported yet, see https://infracost.io/requested-resources:
  ∙ 1 x aws_cloudfront_monitoring_subscription
```
  </details>
</details>

<hr/>

There are also [10 pre-existing issues](https://dashboard.infracost.io/org/tudpt/repos/0db7dd4d-264d-458f-a5b0-7b281856fd1b?utm_source=pr_comment&utm_content=preexisting_issues) in the `main` branch. Fix some to climb your [org’s leaderboard](https://dashboard.infracost.io/org/tudpt?utm_source=pr_comment&utm_content=org_leaderboard#leaderboard) 🥇

<sub>
  This comment will be updated when code changes.
</sub>

author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-09T10:51:19Z
body: [//]: <> (infracost-comment, valid-at=2025-05-11T12:42:46Z)

<h4>💰 Infracost report</h4>
<h4>Monthly estimate generated</h4>
<details>

<summary>Estimate details (includes details of unsupported resources)</summary>

```
──────────────────────────────────
2 projects have no cost estimate changes.
Run the following command to see their breakdown: infracost breakdown --path=/path/to/code

──────────────────────────────────
175 cloud resources were detected:
∙ 33 were estimated
∙ 141 were free
∙ 1 is not supported yet, see https://infracost.io/requested-resources:
  ∙ 1 x aws_cloudfront_monitoring_subscription
```
</details>
<sub>This comment will be updated when code changes.
</sub>

author_association: NONE