Skip to content

Commit 667e284

Browse files
domdomeggclaude
andauthored
feat(infra): Add Pulumi-based Kubernetes deployment infrastructure (#237)
Original PR: #227 - Add Pulumi-based infrastructure as code for deploying MCP Registry to Kubernetes - Support for both local development (minikube) and Azure Kubernetes Service (AKS) - Complete deployment orchestration including: - cluster setup: e.g. you point this at an Azure account, and it can set up and manage the cluster for you. e.g. K8s version, number of nodes, type of nodes, ... - cloud agnostic K8s services: cert-manager, nginx-ingress - app services: MongoDB, and registry application (currently using nginx as a placeholder, blocked on #225 (as is #190). but should be a 1 line change) ## How is this different to #190 - Supports cluster setup and management. This enables: - Non-hosting maintainers managing many devops workflows (e.g. scaling up the cluster, or bumping K8s versions). Without this, we'd need to bug/page the organisation hosting the registry when we need these things changed. - Makes it easy to spin up things like staging/temporary clusters, as well as enables contributors to replicate the stack exactly on their own Azure accounts. - Sets up cloud-agnostic services. For example, rather than using the Azure-managed ingresses and CA, we install nginx-ingress and cert-manager. This enables: - Running the entire infra stack can also run locally (e.g. in minikube, k3s, orbstack, colima) - making it much easier for contributors to test changes to infra stuff. - Moving between cloud providers much more easily, e.g. we could shift from Azure to GCP/AWS/other with minimal hassle. - Everything stays written in Go, rather than Helm templates. This means we get things like type-checking etc. for free (which from my experience makes AI tools wayyy better at editing K8s stuff), and contributors don't need to learn a new language if they're already using Go. ## Testing I've got this running well: - locally in minikube - on cloud in Azure (my personal Azure account) <details><summary>Claude written architecture and security review</summary> <p> ## Deployment Review & Assessment ### Current Architecture Strengths **Pulumi IaC Approach** - Well-structured infrastructure as code using Pulumi - Multi-provider support (AKS, local) with clean abstraction - Good separation of concerns in `pkg/` directory **Security Fundamentals** - Non-root container execution (`appuser` with UID 10001) - Secrets properly managed via Kubernetes secrets - TLS/SSL certificate management with cert-manager and Let's Encrypt ### Critical Issues & High-Priority Improvements **1. Production Deployment Not Ready** 🚨 The registry deployment uses `nginx:alpine` placeholder image instead of the actual MCP registry: - `deploy/pkg/k8s/registry.go:67` - TODO comments indicate incomplete setup - Health probes are commented out - Port mapping doesn't match actual application (80 vs 8080) **Fix:** Build and publish actual registry container image to GHCR, update deployment **2. Database Security Considerations** 🔒 - MongoDB deployed without authentication - No backup/disaster recovery strategy - Database credentials hardcoded *Note: MongoDB is not exposed externally (ClusterIP service), so this is not a critical security risk but should be addressed for production.* **3. Monitoring & Observability Gaps** 📊 - No Prometheus/Grafana monitoring stack - No log aggregation (ELK/Loki) - No application metrics/health dashboards - No alerting configured **4. High Availability & Reliability** ⚠️ - Single MongoDB instance (no replication) - No persistent volume backup strategy - Fixed 10Gi storage without growth planning - Only 2 replicas for registry service - No pod disruption budgets - No horizontal pod autoscaling ### Recommended Improvements **Immediate (High Priority)** 1. Complete Registry Deployment - Build proper container image pipeline, enable health checks 2. Secure MongoDB - Add authentication credentials, implement backup strategy **Medium Priority** 3. Add Monitoring Stack - Prometheus, Grafana deployment 4. Security Hardening (Nice to Have) - RBAC policies, Network Policies, Pod Security Standards 5. CI/CD Pipeline Enhancement - Container image building/publishing, automated deployment **Lower Priority** 6. High Availability - MongoDB replica set, HPA for registry pods 7. Operational Excellence - Kubernetes dashboard, cost optimization ### Configuration Issues - Production config has test credentials: `deploy/Pulumi.prod.yaml:4-5` - Missing environment-specific resource sizing - Hardcoded domain names (`example.com`) The deployment setup shows good architectural foundations but needs significant work before production readiness. The most critical issue is the placeholder nginx container - priority should be completing the actual registry application deployment before addressing the other improvements. Security measures like RBAC and Network Policies are nice to have but not strictly necessary given that MongoDB is not exposed externally. 🤖 Generated with [Claude Code](https://claude.ai/code) </p> </details> ## Metadata Working towards #91 --------- Co-authored-by: Claude <[email protected]>
1 parent 1821f68 commit 667e284

20 files changed

+1640
-1
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,5 @@ validate-schemas
1010
.idea/
1111
coverage.out
1212
coverage.html
13-
13+
deploy/infra/infra
1414
registry

deploy/.gitignore

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Pulumi
2+
*.pyc
3+
.pulumi/
4+
Pulumi.*.yaml.bak
5+
6+
# Go
7+
*.exe
8+
*.exe~
9+
*.dll
10+
*.so
11+
*.dylib
12+
*.test
13+
*.out
14+
vendor/
15+
16+
# IDE
17+
.idea/
18+
.vscode/
19+
*.swp
20+
*.swo
21+
*~
22+
23+
# OS
24+
.DS_Store
25+
Thumbs.db
26+
27+
# Build artifacts
28+
infra

deploy/Makefile

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
.PHONY: help init preview deploy destroy clean dev-init prod-init dev-deploy prod-deploy
2+
3+
help: ## Show this help message
4+
@echo "Usage: make [target]"
5+
@echo ""
6+
@echo "Targets:"
7+
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " %-15s %s\n", $$1, $$2}'
8+
9+
init: ## Initialize Pulumi stack
10+
pulumi stack init
11+
12+
preview: ## Preview infrastructure changes
13+
pulumi preview
14+
15+
deploy: ## Deploy infrastructure
16+
pulumi up --yes
17+
18+
destroy: ## Destroy infrastructure
19+
pulumi destroy --yes
20+
21+
clean: ## Clean up temporary files
22+
rm -rf .pulumi/
23+
rm -f Pulumi.*.yaml.bak
24+
25+
dev-init: ## Initialize development stack
26+
pulumi stack init dev
27+
pulumi stack select dev
28+
29+
prod-init: ## Initialize production stack
30+
pulumi stack init prod
31+
pulumi stack select prod
32+
33+
dev-deploy: ## Deploy to development environment
34+
pulumi stack select dev
35+
pulumi up --yes
36+
37+
prod-deploy: ## Deploy to production environment
38+
pulumi stack select prod
39+
pulumi up --yes
40+
41+
dev-destroy: ## Destroy development environment
42+
pulumi stack select dev
43+
pulumi destroy --yes
44+
45+
prod-destroy: ## Destroy production environment
46+
pulumi stack select prod
47+
pulumi destroy --yes

deploy/Pulumi.local.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
config:
2+
mcp-registry:environment: local
3+
mcp-registry:provider: local
4+
mcp-registry:githubClientId: test-client-id
5+
mcp-registry:githubClientSecret:
6+
secure: v1:gt5MBuW7QPiJymkh:1+I2eFChsrUH18cLELq9OAIN94MLH0SldbOuPp2C
7+
encryptionsalt: v1:ijIHaqhbXVA=:v1:7voX1Kv+Bunz33iN:fyVHMOhlGIymzJ+ILgUBy3ExTwUUnA==

deploy/Pulumi.prod.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
config:
2+
mcp-registry:environment: prod
3+
mcp-registry:provider: aks
4+
mcp-registry:githubClientId: test-client-id
5+
mcp-registry:githubClientSecret: some-secret-here
6+
encryptionsalt: v1:oDjcEdLMFwM=:v1:FdgMU4r7kVWUbaUt:1aTSUxrNy+JNW51KRXzoMACuAXruUg==

deploy/Pulumi.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name: mcp-registry-infra
2+
runtime:
3+
name: go
4+
options:
5+
binary: ./infra
6+
description: MCP Registry Kubernetes Infrastructure

deploy/README.md

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# MCP Registry Kubernetes Deployment
2+
3+
This directory contains Pulumi infrastructure code to deploy the MCP Registry service to a Kubernetes cluster. It supports multiple Kubernetes providers: Azure Kubernetes Service (AKS) and local (using existing kubeconfig).
4+
5+
## Quick Start
6+
7+
### Local Development
8+
9+
Pre-requisites:
10+
- [Pulumi CLI installed](https://www.pulumi.com/docs/iac/download-install/)
11+
- Access to a Kubernetes cluster via kubeconfig. You can run a cluster locally with [minikube](https://minikube.sigs.k8s.io/docs/start/).
12+
13+
1. Set Pulumi's backend to local: `pulumi login --local`
14+
2. Init the local stack: `pulumi stack init local` (fine to leave `password` blank)
15+
3. Set your config:
16+
```bash
17+
# General environment
18+
pulumi config set mcp-registry:environment local
19+
20+
# To use your local kubeconfig (default)
21+
pulumi config set mcp-registry:provider local
22+
# Alternative: To use AKS
23+
# pulumi config set mcp-registry:provider aks
24+
25+
# GitHub OAuth
26+
pulumi config set mcp-registry:githubClientId <your-github-client-id>
27+
pulumi config set --secret mcp-registry:githubClientSecret <your-github-client-secret>
28+
```
29+
4. Deploy: `go build && PULUMI_CONFIG_PASSPHRASE="" pulumi up --yes`
30+
5. Access the repository via the ingress load balancer. You can find its external IP with `kubectl get svc nginx-ingress-ingress-nginx-controller -n ingress-nginx` (with minikube, if it's 'pending' you might need `minikube tunnel`). Then run `curl -H "Host: mcp-registry-local.example.com" -k https://<EXTERNAL-IP>/v0/ping` to check that the service is up.
31+
32+
### Production Deployment (AKS)
33+
34+
**Note:** This is how the production deployment will be set up once. But then the plan will be future updates are effectively a login + `pulumi up` from GitHub Actions.
35+
36+
Pre-requisites:
37+
- [Pulumi CLI installed](https://www.pulumi.com/docs/iac/download-install/)
38+
- A Microsoft Azure account
39+
- [Azure CLI](https://learn.microsoft.com/en-gb/cli/azure/get-started-with-azure-cli) installed
40+
41+
1. Login to Azure: `az login`
42+
2. Create a resource group: `az group create --name official-mcp-registry-prod --location eastus`
43+
3. Add the storage resource provider: `az provider register --namespace Microsoft.Storage`
44+
4. Create a storage account: `az storage account create --name officialmcpregistryprod --resource-group official-mcp-registry-prod --location eastus --sku Standard_LRS`
45+
5. Add the 'Storage Blob Data Contributor' role assignment for yourself on the storage account: `az role assignment create --assignee $(az ad signed-in-user show --query id -o tsv) --role "Storage Blob Data Contributor" --scope "/subscriptions/$(az account show --query id -o tsv)/resourceGroups/official-mcp-registry-prod"`
46+
6. Create a container: `az storage container create --name pulumi-state --account-name officialmcpregistryprod`
47+
7. Set Pulumi's backend to Azure: `pulumi login 'azblob://pulumi-state?storage_account=officialmcpregistryprod'`
48+
8. Init the production stack: `pulumi stack init prod`
49+
- TODO: This has a password that maybe needs to be shared with select contributors?
50+
9. Deploy: `go build && PULUMI_CONFIG_PASSPHRASE="" pulumi up --yes`
51+
10. Access the repository via the ingress load balancer. You can find its external IP with `kubectl get svc nginx-ingress-ingress-nginx-controller -n ingress-nginx` or view it in the Pulumi outputs. Then run `curl -H "Host: mcp-registry-prod.example.com" -k https://<EXTERNAL-IP>/v0/ping` to check that the service is up.
52+
53+
## Structure
54+
55+
```
56+
├── main.go # Pulumi program entry point
57+
├── Pulumi.yaml # Project configuration
58+
├── Pulumi.local.yaml # Local stack configuration
59+
├── Pulumi.prod.yaml # Production stack configuration
60+
├── Makefile # Build and deployment targets
61+
├── go.mod # Go module dependencies
62+
├── go.sum # Go module checksums
63+
└── pkg/ # Infrastructure packages
64+
├── k8s/ # Kubernetes deployment components
65+
│ ├── cert_manager.go # SSL certificate management
66+
│ ├── deploy.go # Deployment orchestration
67+
│ ├── ingress.go # Ingress controller setup
68+
│ ├── mongodb.go # MongoDB deployment
69+
│ └── registry.go # MCP Registry deployment
70+
└── providers/ # Kubernetes cluster providers
71+
├── types.go # Provider interface definitions
72+
├── aks/ # Azure Kubernetes Service provider
73+
└── local/ # Local kubeconfig provider
74+
```
75+
76+
### Architecture Overview
77+
78+
#### Deployment Flow
79+
1. Pulumi program starts in `main.go`
80+
2. Configuration is loaded from Pulumi config files
81+
3. Provider factory creates appropriate cluster provider (AKS or local)
82+
4. Cluster provider sets up Kubernetes access
83+
5. `k8s.DeployAll()` orchestrates complete deployment:
84+
- Certificate manager for SSL/TLS
85+
- Ingress controller for external access
86+
- MongoDB for data persistence
87+
- MCP Registry application
88+
89+
## Configuration
90+
91+
| Parameter | Description | Required |
92+
|-----------|-------------|----------|
93+
| `environment` | Deployment environment (dev/prod) | Yes |
94+
| `provider` | Kubernetes provider (local/aks) | No (default: local) |
95+
| `githubClientId` | GitHub OAuth Client ID | Yes |
96+
| `githubClientSecret` | GitHub OAuth Client Secret | Yes |
97+
98+
## Troubleshooting
99+
100+
### Check Status
101+
102+
```bash
103+
kubectl get pods
104+
kubectl get deployment
105+
kubectl get svc
106+
kubectl get ingress
107+
kubectl get svc -n ingress-nginx
108+
```
109+
110+
### View Logs
111+
112+
```bash
113+
kubectl logs -l app=mcp-registry
114+
kubectl logs -l app=mongodb
115+
```

deploy/go.mod

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
module github.com/modelcontextprotocol/registry/deploy/infra
2+
3+
go 1.23.0
4+
5+
toolchain go1.24.1
6+
7+
require (
8+
github.com/pulumi/pulumi-azure-native-sdk/containerservice v1.104.0
9+
github.com/pulumi/pulumi-azure-native-sdk/resources v1.104.0
10+
github.com/pulumi/pulumi-kubernetes/sdk/v4 v4.18.2
11+
github.com/pulumi/pulumi/sdk/v3 v3.158.0
12+
)
13+
14+
require (
15+
dario.cat/mergo v1.0.0 // indirect
16+
github.com/BurntSushi/toml v1.2.1 // indirect
17+
github.com/Microsoft/go-winio v0.6.1 // indirect
18+
github.com/ProtonMail/go-crypto v1.1.3 // indirect
19+
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
20+
github.com/agext/levenshtein v1.2.3 // indirect
21+
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
22+
github.com/atotto/clipboard v0.1.4 // indirect
23+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
24+
github.com/blang/semver v3.5.1+incompatible // indirect
25+
github.com/charmbracelet/bubbles v0.16.1 // indirect
26+
github.com/charmbracelet/bubbletea v0.25.0 // indirect
27+
github.com/charmbracelet/lipgloss v0.7.1 // indirect
28+
github.com/cheggaaa/pb v1.0.29 // indirect
29+
github.com/cloudflare/circl v1.3.7 // indirect
30+
github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect
31+
github.com/cyphar/filepath-securejoin v0.3.6 // indirect
32+
github.com/djherbis/times v1.5.0 // indirect
33+
github.com/emirpasic/gods v1.18.1 // indirect
34+
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
35+
github.com/go-git/go-billy/v5 v5.6.1 // indirect
36+
github.com/go-git/go-git/v5 v5.13.1 // indirect
37+
github.com/gogo/protobuf v1.3.2 // indirect
38+
github.com/golang/glog v1.2.4 // indirect
39+
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
40+
github.com/google/uuid v1.6.0 // indirect
41+
github.com/grpc-ecosystem/grpc-opentracing v0.0.0-20180507213350-8e809c8a8645 // indirect
42+
github.com/hashicorp/errwrap v1.1.0 // indirect
43+
github.com/hashicorp/go-multierror v1.1.1 // indirect
44+
github.com/hashicorp/hcl/v2 v2.17.0 // indirect
45+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
46+
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
47+
github.com/kevinburke/ssh_config v1.2.0 // indirect
48+
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
49+
github.com/mattn/go-isatty v0.0.20 // indirect
50+
github.com/mattn/go-localereader v0.0.1 // indirect
51+
github.com/mattn/go-runewidth v0.0.15 // indirect
52+
github.com/mitchellh/go-ps v1.0.0 // indirect
53+
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
54+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
55+
github.com/muesli/cancelreader v0.2.2 // indirect
56+
github.com/muesli/reflow v0.3.0 // indirect
57+
github.com/muesli/termenv v0.15.2 // indirect
58+
github.com/opentracing/basictracer-go v1.1.0 // indirect
59+
github.com/opentracing/opentracing-go v1.2.0 // indirect
60+
github.com/pgavlin/fx v0.1.6 // indirect
61+
github.com/pjbgf/sha1cd v0.3.0 // indirect
62+
github.com/pkg/errors v0.9.1 // indirect
63+
github.com/pkg/term v1.1.0 // indirect
64+
github.com/pulumi/appdash v0.0.0-20231130102222-75f619a67231 // indirect
65+
github.com/pulumi/esc v0.10.0 // indirect
66+
github.com/pulumi/pulumi-azure-native-sdk v1.104.0 // indirect
67+
github.com/rivo/uniseg v0.4.4 // indirect
68+
github.com/rogpeppe/go-internal v1.12.0 // indirect
69+
github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect
70+
github.com/santhosh-tekuri/jsonschema/v5 v5.0.0 // indirect
71+
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
72+
github.com/skeema/knownhosts v1.3.0 // indirect
73+
github.com/spf13/cast v1.4.1 // indirect
74+
github.com/spf13/cobra v1.8.0 // indirect
75+
github.com/spf13/pflag v1.0.5 // indirect
76+
github.com/stretchr/objx v0.2.0 // indirect
77+
github.com/texttheater/golang-levenshtein v1.0.1 // indirect
78+
github.com/uber/jaeger-client-go v2.30.0+incompatible // indirect
79+
github.com/uber/jaeger-lib v2.4.1+incompatible // indirect
80+
github.com/xanzy/ssh-agent v0.3.3 // indirect
81+
github.com/zclconf/go-cty v1.13.2 // indirect
82+
go.uber.org/atomic v1.9.0 // indirect
83+
golang.org/x/crypto v0.33.0 // indirect
84+
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
85+
golang.org/x/mod v0.19.0 // indirect
86+
golang.org/x/net v0.35.0 // indirect
87+
golang.org/x/sync v0.11.0 // indirect
88+
golang.org/x/sys v0.30.0 // indirect
89+
golang.org/x/term v0.29.0 // indirect
90+
golang.org/x/text v0.22.0 // indirect
91+
golang.org/x/tools v0.23.0 // indirect
92+
google.golang.org/genproto/googleapis/rpc v0.0.0-20240311173647-c811ad7063a7 // indirect
93+
google.golang.org/grpc v1.63.2 // indirect
94+
google.golang.org/protobuf v1.33.0 // indirect
95+
gopkg.in/warnings.v0 v0.1.2 // indirect
96+
gopkg.in/yaml.v3 v3.0.1 // indirect
97+
lukechampine.com/frand v1.4.2 // indirect
98+
)

0 commit comments

Comments
 (0)