fix(infra): address PR review feedback on tf modules

Deployer role:
- Add servicediscovery actions; module always creates Cloud Map namespace
  and service so the policy must grant CreatePrivateDnsNamespace etc.
- Make Route53 + ACM permissions unconditional. The server module always
  issues an ACM cert and writes Route53 records (no CloudFront default-cert
  path exists), so gating these on route53_zone_ids was broken. Split
  Route53 into hosted-zone management (always) plus record-set mutation
  (scoped to caller-supplied zones, falls back to *).
- Remove unused cloudfront:* statement; no CloudFront resources in module.
- Replace acm:* wildcard with explicit cert-management action set.

Server module:
- qdrant_image_tag is now nullable with default null and validated against
  use_external_qdrant, so external-qdrant callers can omit it instead of
  passing a sentinel "unused" value.
- task_role_arn and efs_id outputs marked sensitive; qdrant_dns_name returns
  null when use_external_qdrant = true.
- ALB SG now has matching IPv6 egress rule (was v4-only).
- nextcloud_url validates the https:// scheme.
- random_pet.subdomain keeper includes zone_name so a zone migration that
  preserves zone_id still triggers regeneration.
- Pin required_version >= 1.9 on both modules.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2026-05-01 23:24:09 +02:00
co-authored by Claude Opus 4.7
parent ccf4b91bf9
commit e4c552cd19
10 changed files with 151 additions and 62 deletions
@@ -3,6 +3,7 @@
| Name | Version |
| ---- | ------- |
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.9 |
| <a name="requirement_aws"></a> [aws](#requirement\_aws) | ~> 6.0 |
| <a name="requirement_random"></a> [random](#requirement\_random) | ~> 3.6 |
@@ -10,8 +11,8 @@
| Name | Version |
| ---- | ------- |
| <a name="provider_aws"></a> [aws](#provider\_aws) | 6.43.0 |
| <a name="provider_random"></a> [random](#provider\_random) | 3.8.1 |
| <a name="provider_aws"></a> [aws](#provider\_aws) | ~> 6.0 |
| <a name="provider_random"></a> [random](#provider\_random) | ~> 3.6 |
## Modules
@@ -58,6 +59,7 @@ No modules.
| [aws_service_discovery_private_dns_namespace.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_private_dns_namespace) | resource |
| [aws_service_discovery_service.qdrant](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/service_discovery_service) | resource |
| [aws_vpc_security_group_egress_rule.alb_all_v4](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource |
| [aws_vpc_security_group_egress_rule.alb_all_v6](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource |
| [aws_vpc_security_group_egress_rule.qdrant_all_v4](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource |
| [aws_vpc_security_group_egress_rule.qdrant_all_v6](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource |
| [aws_vpc_security_group_egress_rule.task_all_v4](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc_security_group_egress_rule) | resource |
@@ -100,7 +102,7 @@ No modules.
| <a name="input_qdrant_collection"></a> [qdrant\_collection](#input\_qdrant\_collection) | Qdrant collection name. Set to a stable value (anything other than upstream's default 'nextcloud\_content') so the upstream config doesn't fall through to its hostname-based auto-naming, which churns the collection on every rolling deploy. | `string` | `"nextcloud-mcp"` | no |
| <a name="input_qdrant_cpu"></a> [qdrant\_cpu](#input\_qdrant\_cpu) | Qdrant Fargate task vCPU units (1024 = 1 vCPU) | `number` | `512` | no |
| <a name="input_qdrant_image"></a> [qdrant\_image](#input\_qdrant\_image) | Qdrant container image (without tag) | `string` | `"qdrant/qdrant"` | no |
| <a name="input_qdrant_image_tag"></a> [qdrant\_image\_tag](#input\_qdrant\_image\_tag) | Qdrant container image tag (e.g., v1.15.0). Pin to a specific release; avoid :latest. Unused when use\_external\_qdrant = true. | `string` | n/a | yes |
| <a name="input_qdrant_image_tag"></a> [qdrant\_image\_tag](#input\_qdrant\_image\_tag) | Qdrant container image tag (e.g., v1.15.0). Pin to a specific release; avoid :latest. Required only when use\_external\_qdrant = false; omit (or pass null) when use\_external\_qdrant = true. | `string` | `null` | no |
| <a name="input_qdrant_memory"></a> [qdrant\_memory](#input\_qdrant\_memory) | Qdrant Fargate task memory (MiB) | `number` | `1024` | no |
| <a name="input_secret_arn"></a> [secret\_arn](#input\_secret\_arn) | ARN of the Secrets Manager secret holding JSON {host, client\_id, client\_secret, token\_encryption\_key, webhook\_secret} | `string` | n/a | yes |
| <a name="input_use_external_qdrant"></a> [use\_external\_qdrant](#input\_use\_external\_qdrant) | When true, skip the in-AWS Qdrant ECS task and source QDRANT\_URL/QDRANT\_API\_KEY from the Secrets Manager secret (keys: qdrant\_url, qdrant\_api\_key). When false, run an in-AWS Qdrant Fargate task and point the MCP server at it via Cloud Map DNS. | `bool` | `false` | no |
@@ -117,12 +119,12 @@ No modules.
| <a name="output_alb_dns_name"></a> [alb\_dns\_name](#output\_alb\_dns\_name) | n/a |
| <a name="output_ecs_cluster_name"></a> [ecs\_cluster\_name](#output\_ecs\_cluster\_name) | n/a |
| <a name="output_ecs_service_name"></a> [ecs\_service\_name](#output\_ecs\_service\_name) | n/a |
| <a name="output_efs_id"></a> [efs\_id](#output\_efs\_id) | n/a |
| <a name="output_efs_id"></a> [efs\_id](#output\_efs\_id) | EFS file-system ID. Marked sensitive — surfacing it in CI logs invites enumeration of mount targets. |
| <a name="output_fqdn"></a> [fqdn](#output\_fqdn) | Fully-qualified domain name |
| <a name="output_log_group_name"></a> [log\_group\_name](#output\_log\_group\_name) | n/a |
| <a name="output_qdrant_dns_name"></a> [qdrant\_dns\_name](#output\_qdrant\_dns\_name) | Internal DNS name where mcp-server reaches qdrant |
| <a name="output_qdrant_dns_name"></a> [qdrant\_dns\_name](#output\_qdrant\_dns\_name) | Internal DNS name where mcp-server reaches qdrant (null when use\_external\_qdrant = true). |
| <a name="output_qdrant_service_name"></a> [qdrant\_service\_name](#output\_qdrant\_service\_name) | Qdrant ECS service name (null when use\_external\_qdrant = true) |
| <a name="output_subdomain"></a> [subdomain](#output\_subdomain) | Generated random subdomain (label only, without the zone) |
| <a name="output_task_role_arn"></a> [task\_role\_arn](#output\_task\_role\_arn) | n/a |
| <a name="output_task_role_arn"></a> [task\_role\_arn](#output\_task\_role\_arn) | Task role ARN. Marked sensitive — knowing the ARN is the first step to abusing it via SSRF/role-confusion. |
| <a name="output_url"></a> [url](#output\_url) | Public HTTPS URL of the MCP server |
<!-- END_TF_DOCS -->
@@ -46,6 +46,12 @@ resource "aws_vpc_security_group_egress_rule" "alb_all_v4" {
ip_protocol = "-1"
}
resource "aws_vpc_security_group_egress_rule" "alb_all_v6" {
security_group_id = aws_security_group.alb.id
cidr_ipv6 = "::/0"
ip_protocol = "-1"
}
resource "aws_lb" "this" {
name = var.name
load_balancer_type = "application"
+4 -1
View File
@@ -3,8 +3,11 @@ resource "random_pet" "subdomain" {
separator = "-"
# Stable across applies; regenerate only if we point at a different zone.
# `zone_name` is in the keeper too so a zone migration that keeps the same
# zone_id (rare but possible across providers) still triggers regeneration.
keepers = {
zone_id = var.zone_id
zone_id = var.zone_id
zone_name = var.zone_name
}
}
@@ -1,4 +1,5 @@
terraform {
required_version = ">= 1.9"
required_providers {
aws = {
source = "hashicorp/aws"
@@ -22,7 +22,9 @@ output "ecs_service_name" {
}
output "efs_id" {
value = aws_efs_file_system.this.id
description = "EFS file-system ID. Marked sensitive — surfacing it in CI logs invites enumeration of mount targets."
value = aws_efs_file_system.this.id
sensitive = true
}
output "log_group_name" {
@@ -30,7 +32,9 @@ output "log_group_name" {
}
output "task_role_arn" {
value = aws_iam_role.task.arn
description = "Task role ARN. Marked sensitive — knowing the ARN is the first step to abusing it via SSRF/role-confusion."
value = aws_iam_role.task.arn
sensitive = true
}
output "alb_dns_name" {
@@ -43,6 +47,6 @@ output "qdrant_service_name" {
}
output "qdrant_dns_name" {
description = "Internal DNS name where mcp-server reaches qdrant"
value = "qdrant.${aws_service_discovery_private_dns_namespace.this.name}"
description = "Internal DNS name where mcp-server reaches qdrant (null when use_external_qdrant = true)."
value = var.use_external_qdrant ? null : "qdrant.${aws_service_discovery_private_dns_namespace.this.name}"
}
+13 -1
View File
@@ -22,6 +22,11 @@ variable "private_subnet_ids" {
variable "nextcloud_url" {
description = "Public URL of the Nextcloud instance the MCP server pairs with (e.g., https://cloud.example.com). Used to advertise the OIDC discovery endpoint via /api/v1/status so the astrolabe Nextcloud app can discover Nextcloud's oidc_provider as the IdP instead of falling back to http://localhost."
type = string
validation {
condition = startswith(var.nextcloud_url, "https://")
error_message = "nextcloud_url must include the https:// scheme; OIDC discovery is composed by appending /.well-known/openid-configuration."
}
}
variable "zone_id" {
@@ -117,8 +122,15 @@ variable "qdrant_collection" {
}
variable "qdrant_image_tag" {
description = "Qdrant container image tag (e.g., v1.15.0). Pin to a specific release; avoid :latest. Unused when use_external_qdrant = true."
description = "Qdrant container image tag (e.g., v1.15.0). Pin to a specific release; avoid :latest. Required only when use_external_qdrant = false; omit (or pass null) when use_external_qdrant = true."
type = string
nullable = true
default = null
validation {
condition = var.use_external_qdrant || var.qdrant_image_tag != null
error_message = "qdrant_image_tag is required when use_external_qdrant = false."
}
}
variable "use_external_qdrant" {