Files
Chris CoutinhoandClaude Opus 4.7 e4c552cd19 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>
2026-05-01 23:24:09 +02:00

251 lines
8.5 KiB
Markdown

# Terraform modules for `nextcloud-mcp-server` on AWS
POC Terraform modules to spin up a `nextcloud-mcp-server` on AWS ECS Fargate,
behind an ALB, with EFS-backed task storage. Two Qdrant modes are supported:
an in-VPC Qdrant ECS task (default) or an external/managed Qdrant service
(URL + API key supplied via Secrets Manager).
This directory ships **two** modules:
| Module | Purpose |
|---|---|
| [`nextcloud-mcp-deployer-role`](./nextcloud-mcp-deployer-role) | A least-privilege IAM role + policy scoped to deploying the MCP server module. Apply this **first**, in the target AWS account, with a privileged bootstrap principal. |
| [`nextcloud-mcp-server`](./nextcloud-mcp-server) | The actual MCP server: ECS cluster, ALB, EFS, IAM task/execution roles, Route53 record, ACM certificate, optional Qdrant ECS service. Apply this **second**, by assuming the deployer role. |
## Two-phase deploy
```
┌──────────────┐ ┌────────────────────────────┐ ┌─────────────────────┐
│ Bootstrap │────▶│ Apply deployer-role │────▶│ Apply mcp-server │
│ principal │ │ (creates an IAM role + │ │ (assume the role, │
│ (admin) │ │ scoped policy) │ │ provision the │
│ │ │ │ │ MCP server) │
└──────────────┘ └────────────────────────────┘ └─────────────────────┘
```
The split lets a customer create a stable, minimum-permission role in their
account once, and then re-apply the MCP server module repeatedly under that
role without ever exposing admin credentials to the deploy pipeline.
## Phase 1 — bootstrap the deployer role
### Prerequisite: bootstrap IAM policy
The principal that applies `nextcloud-mcp-deployer-role` only needs IAM
permissions to manage the role and its attached policy. The policy below is
**scoped to the module defaults** (`role_path = /clients/`,
`role_name = nextcloud-mcp-deployer`); widen the resource ARNs if you
override either input.
Copy this into the AWS console (IAM → Policies → Create policy → JSON) and
attach it to the user / role that runs `terraform apply` for the deployer-role
module:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "BootstrapManageDeployerRole",
"Effect": "Allow",
"Action": [
"iam:CreateRole",
"iam:GetRole",
"iam:DeleteRole",
"iam:UpdateRole",
"iam:UpdateAssumeRolePolicy",
"iam:TagRole",
"iam:UntagRole",
"iam:ListRoleTags",
"iam:AttachRolePolicy",
"iam:DetachRolePolicy",
"iam:ListAttachedRolePolicies",
"iam:ListRolePolicies",
"iam:PutRolePolicy",
"iam:GetRolePolicy",
"iam:DeleteRolePolicy"
],
"Resource": "arn:aws:iam::*:role/clients/nextcloud-mcp-deployer*"
},
{
"Sid": "BootstrapManageDeployerPolicy",
"Effect": "Allow",
"Action": [
"iam:CreatePolicy",
"iam:GetPolicy",
"iam:GetPolicyVersion",
"iam:ListPolicyVersions",
"iam:CreatePolicyVersion",
"iam:DeletePolicyVersion",
"iam:DeletePolicy",
"iam:TagPolicy",
"iam:UntagPolicy",
"iam:ListEntitiesForPolicy"
],
"Resource": "arn:aws:iam::*:policy/clients/nextcloud-mcp-deployer-policy"
},
{
"Sid": "BootstrapStsCallerIdentity",
"Effect": "Allow",
"Action": ["sts:GetCallerIdentity"],
"Resource": "*"
}
]
}
```
### Apply the deployer-role module
```hcl
terraform {
required_providers {
aws = { source = "hashicorp/aws", version = "~> 6.0" }
}
}
provider "aws" {
region = "eu-west-1"
}
module "nextcloud_mcp_deployer_role" {
source = "git::https://github.com/cbcoutinho/nextcloud-mcp-server.git//infra/terraform/nextcloud-mcp-deployer-role?ref=master"
trusted_principal_arns = [
"arn:aws:iam::123456789012:root", # principal that will assume the role
]
# Optional: set true if the Secrets Manager secret is managed in the same TF run as the MCP server.
# allow_secret_create = true
# Optional: required only if you use the module's custom-domain (Route53 + ACM) mode.
# route53_zone_ids = ["Z0123456789ABCDEFGHIJ"]
}
output "deployer_role_arn" {
value = module.nextcloud_mcp_deployer_role.role_arn
}
```
`terraform apply` produces a role ARN like
`arn:aws:iam::<account-id>:role/clients/nextcloud-mcp-deployer`. Hand that
ARN to whichever pipeline / human applies Phase 2.
## Phase 2 — deploy the MCP server
### Provider configured to assume the deployer role
```hcl
provider "aws" {
region = "eu-west-1"
assume_role {
role_arn = "arn:aws:iam::123456789012:role/clients/nextcloud-mcp-deployer"
# external_id = "..." # if you set one in the deployer-role trust policy
}
}
```
### Pre-existing requirements
- A VPC with **public subnets** (for the ALB and ECS task ENIs — the module
runs tasks with `assign_public_ip = true`, no NAT gateway needed) and
**private subnets** in matching AZs (for EFS mount targets).
- A Route53 **public hosted zone** the module can write to. The module
generates a random `<two-words>.<your-zone>` subdomain and provisions the
ACM cert + DNS records itself.
- A Secrets Manager secret with a **JSON value** containing the keys below.
Create it manually, or via Terraform (set `allow_secret_create = true` on
the deployer role and manage it in the same plan).
Required keys:
```json
{
"host": "https://your-nextcloud.example.com",
"client_id": "<oidc-client-id>",
"client_secret": "<oidc-client-secret>",
"token_encryption_key": "<random 32-byte url-safe base64>",
"webhook_secret": "<random shared secret>"
}
```
When `use_external_qdrant = true`, also include:
```json
{
"qdrant_url": "https://<your-qdrant-cluster>.cloud.qdrant.io:6333",
"qdrant_api_key": "<qdrant api key>"
}
```
### Mode A — in-VPC Qdrant (default)
Runs Qdrant as a second Fargate task on the same ECS cluster, exposed to the
MCP server via Cloud Map private DNS. Storage on EFS.
```hcl
module "nextcloud_mcp_server" {
source = "git::https://github.com/cbcoutinho/nextcloud-mcp-server.git//infra/terraform/nextcloud-mcp-server?ref=master"
vpc_id = "vpc-0123456789abcdef0"
public_subnet_ids = ["subnet-aaa", "subnet-bbb"]
private_subnet_ids = ["subnet-ccc", "subnet-ddd"]
zone_id = "Z0123456789ABCDEFGHIJ"
zone_name = "example.com"
nextcloud_url = "https://nextcloud.example.com"
image_tag = "0.75.2" # pin to a release; never :latest
qdrant_image_tag = "v1.17.1"
secret_arn = "arn:aws:secretsmanager:eu-west-1:123456789012:secret:nextcloud-mcp-aws-env-XXXXXX"
}
output "mcp_url" {
value = module.nextcloud_mcp_server.url
}
```
### Mode B — external / managed Qdrant
Skip the Qdrant ECS task entirely; point the MCP server at a managed Qdrant
cluster (e.g. Qdrant Cloud, or a Qdrant you run elsewhere). `qdrant_url` and
`qdrant_api_key` come from the same Secrets Manager secret.
```hcl
module "nextcloud_mcp_server" {
source = "git::https://github.com/cbcoutinho/nextcloud-mcp-server.git//infra/terraform/nextcloud-mcp-server?ref=master"
vpc_id = "vpc-0123456789abcdef0"
public_subnet_ids = ["subnet-aaa", "subnet-bbb"]
private_subnet_ids = ["subnet-ccc", "subnet-ddd"]
zone_id = "Z0123456789ABCDEFGHIJ"
zone_name = "example.com"
nextcloud_url = "https://nextcloud.example.com"
image_tag = "0.75.2"
secret_arn = "arn:aws:secretsmanager:eu-west-1:123456789012:secret:nextcloud-mcp-aws-env-XXXXXX"
use_external_qdrant = true
qdrant_collection = "nextcloud-mcp"
# qdrant_image_tag intentionally omitted — only required when use_external_qdrant = false.
}
```
## Pinning the module version
`?ref=master` is fine for a POC but pins to a moving target. Once a release
tag exists in this repo (e.g. `infra-tf-v0.1.0`), pin to it:
```hcl
source = "git::https://github.com/cbcoutinho/nextcloud-mcp-server.git//infra/terraform/nextcloud-mcp-server?ref=infra-tf-v0.1.0"
```
## Module-specific docs
Each module's full input/output reference lives in its own `README.md`:
- [`nextcloud-mcp-server/README.md`](./nextcloud-mcp-server/README.md)
- [`nextcloud-mcp-deployer-role/README.md`](./nextcloud-mcp-deployer-role/README.md)