The deploy configuration table named two parameters that **no code reads**, and omitted two that a deploy cannot succeed without. ## Wrong names | Documented | Actual | Evidence | |---|---|---| | `gcpProjectId` | `gcp:project` | [provider.go:30](https://github.com/modelcontextprotocol/registry/blob/main/deploy/pkg/providers/gcp/provider.go#L30) — its error text literally says `Set gcp:project` | | `gcpRegion` | `gcp:region` | [provider.go:36](https://github.com/modelcontextprotocol/registry/blob/main/deploy/pkg/providers/gcp/provider.go#L36), default `us-central1` | `gcpProjectId` and `gcpRegion` appear **nowhere** in `deploy/` outside this table. All three stack files (`Pulumi.gcpProd.yaml`, `Pulumi.gcpStaging.yaml`, `Pulumi.local.yaml`) already use `gcp:project`. ## Missing required secrets Both are `RequireSecret`, so a deploy fails outright without them — yet neither was in the table: - `mcp-registry:jwtPrivateKey` — [registry.go:56](https://github.com/modelcontextprotocol/registry/blob/main/deploy/pkg/k8s/registry.go#L56) - `mcp-registry:googleOauthClientSecret` — [monitoring.go:463](https://github.com/modelcontextprotocol/registry/blob/main/deploy/pkg/k8s/monitoring.go#L463) ## Also - Documents `gcp:credentials` (read at [provider.go:42](https://github.com/modelcontextprotocol/registry/blob/main/deploy/pkg/providers/gcp/provider.go#L42), set in the staging and prod stacks) - Marks which values are secrets - **Adds the namespace prefixes.** The table listed bare names, which is part of why the two wrong ones survived: `gcpProjectId` reads as plausible right up until you notice the real key lives in a different config namespace altogether. ## Verification Checked both directions: - every key now in the table is read by code in `deploy/` (reference counts confirmed per key) - every `conf.*("...")` key found anywhere in `deploy/` now appears in the table 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
244 lines
12 KiB
Markdown
244 lines
12 KiB
Markdown
# MCP Registry Kubernetes Deployment
|
|
|
|
This directory contains Pulumi infrastructure code to deploy the MCP Registry service to a Kubernetes cluster. It supports deploying the infrastructure locally (using an existing kubeconfig, e.g. with minikube) or to Google Cloud Platform (GCP).
|
|
|
|
## Quick Start
|
|
|
|
### Local Development
|
|
|
|
Pre-requisites:
|
|
- [Pulumi CLI installed](https://www.pulumi.com/docs/iac/download-install/)
|
|
- Access to a Kubernetes cluster via kubeconfig. You can run a cluster locally with [minikube](https://minikube.sigs.k8s.io/docs/start/).
|
|
|
|
1. Ensure your kubeconfig is configured at the cluster you want to use. For minikube, run `minikube start && minikube tunnel`.
|
|
2. Run `make local-up` to deploy the stack.
|
|
3. Access the repository via the ingress load balancer. You can find its external IP with `kubectl get svc ingress-nginx-controller -n ingress-nginx`. Then run `curl -H "Host: local.registry.modelcontextprotocol.io" -k https://<EXTERNAL-IP>/v0/ping` to check that the service is up.
|
|
|
|
#### To change config
|
|
|
|
The stack is configured out of the box for local development. But if you want to make changes, run commands like:
|
|
|
|
```bash
|
|
PULUMI_CONFIG_PASSPHRASE="" pulumi config set mcp-registry:environment local
|
|
PULUMI_CONFIG_PASSPHRASE="" pulumi config set mcp-registry:githubClientSecret --secret <some-secret-value>
|
|
```
|
|
|
|
#### To delete the stack
|
|
|
|
`make local-destroy` and deleting the cluster (with minikube: `minikube delete`) will reset you back to a clean state.
|
|
|
|
### Production Deployment (GCP)
|
|
|
|
**Note:** Deployments are automatically handled by GitHub Actions with separate workflows for staging and production:
|
|
|
|
- **Staging:** All merges to the `main` branch automatically build and deploy the latest code to the `staging` environment via [deploy-staging.yml](../.github/workflows/deploy-staging.yml). Staging always uses the `:main` Docker image tag.
|
|
|
|
- **Production:** Deployment requires explicit configuration of the Docker image tag in `Pulumi.gcpProd.yaml` and is triggered automatically via [deploy-production.yml](../.github/workflows/deploy-production.yml) when this config file is pushed to `main`. This GitOps-style approach provides manual control and an audit trail for production versions.
|
|
|
|
**To deploy a specific version to production:**
|
|
|
|
1. Cut a release (if deploying a new version):
|
|
```bash
|
|
# Via GitHub UI: https://github.com/modelcontextprotocol/registry/releases
|
|
# Or via gh CLI:
|
|
gh release create v1.2.3 --generate-notes
|
|
```
|
|
This builds Docker images tagged as `1.2.3` and `latest` (note: image tags do not include the 'v' prefix)
|
|
|
|
2. Update the production image tag in `Pulumi.gcpProd.yaml`:
|
|
```bash
|
|
# Edit deploy/Pulumi.gcpProd.yaml
|
|
# Change line: mcp-registry:imageTag: 1.2.3 (note: no 'v' prefix for Docker image tags)
|
|
git add deploy/Pulumi.gcpProd.yaml
|
|
git commit -m "Deploy version 1.2.3 to production"
|
|
git push
|
|
```
|
|
|
|
3. The production deployment workflow will automatically trigger and deploy the specified version
|
|
|
|
**Manual Override:** The steps below are preserved if a manual deployment override is needed.
|
|
|
|
Pre-requisites:
|
|
- [Pulumi CLI installed](https://www.pulumi.com/docs/iac/download-install/)
|
|
- A Google Cloud Platform (GCP) account
|
|
- A GCP Service Account with appropriate permissions
|
|
|
|
1. Create a project: `gcloud projects create mcp-registry-prod`
|
|
2. Set the project: `gcloud config set project mcp-registry-prod`
|
|
3. Enable the bootstrap APIs needed before Pulumi can run. The remaining APIs
|
|
(`cloudresourcemanager`, `compute`, `container`, `logging`, `monitoring`) are
|
|
adopted as Pulumi-managed `projects.Service` resources by `ensureRequiredAPIs`
|
|
in `pkg/providers/gcp/provider.go`, so they self-heal against drift but still
|
|
need to be reachable on the very first deploy:
|
|
```bash
|
|
gcloud services enable storage.googleapis.com # for the Pulumi state bucket below
|
|
gcloud services enable cloudresourcemanager.googleapis.com # for the IAM bindings Pulumi creates
|
|
gcloud services enable container.googleapis.com # for the GKE cluster
|
|
```
|
|
4. Create the Pulumi service account and grant it the roles required to manage
|
|
the deploy. `projectIamAdmin` is needed because the deploy creates IAM bindings
|
|
on the project (for the default compute SA). The other four are general
|
|
resource management:
|
|
```bash
|
|
gcloud iam service-accounts create pulumi-svc
|
|
sleep 10
|
|
gcloud projects add-iam-policy-binding mcp-registry-prod --member="serviceAccount:pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com" --role="roles/container.admin"
|
|
gcloud projects add-iam-policy-binding mcp-registry-prod --member="serviceAccount:pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com" --role="roles/compute.admin"
|
|
gcloud projects add-iam-policy-binding mcp-registry-prod --member="serviceAccount:pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com" --role="roles/storage.admin"
|
|
gcloud projects add-iam-policy-binding mcp-registry-prod --member="serviceAccount:pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com" --role="roles/storage.hmacKeyAdmin"
|
|
gcloud projects add-iam-policy-binding mcp-registry-prod --member="serviceAccount:pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com" --role="roles/resourcemanager.projectIamAdmin"
|
|
gcloud iam service-accounts add-iam-policy-binding $(gcloud projects describe mcp-registry-prod --format="value(projectNumber)")-compute@developer.gserviceaccount.com --member="serviceAccount:pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com" --role="roles/iam.serviceAccountUser"
|
|
gcloud iam service-accounts keys create sa-key.json --iam-account=pulumi-svc@mcp-registry-prod.iam.gserviceaccount.com
|
|
```
|
|
5. Create a GCS bucket for Pulumi state: `gsutil mb gs://mcp-registry-prod-pulumi-state`
|
|
6. Set Pulumi's backend to GCS: `pulumi login gs://mcp-registry-prod-pulumi-state`
|
|
7. Get the passphrase file `passphrase.prod.txt` from the registry maintainers
|
|
8. Init the GCP stack: `PULUMI_CONFIG_PASSPHRASE_FILE=passphrase.prod.txt pulumi stack init gcpProd`
|
|
9. Set the GCP credentials in Pulumi config:
|
|
```bash
|
|
# Base64 encode the service account key and set it
|
|
pulumi config set --secret gcp:credentials "$(base64 < sa-key.json)"
|
|
```
|
|
10. Deploy: `make prod-up`
|
|
11. Access the repository via the ingress load balancer. You can find its external IP with: `kubectl get svc ingress-nginx-controller -n ingress-nginx`.
|
|
Then run `curl -H "Host: prod.registry.modelcontextprotocol.io" -k https://<EXTERNAL-IP>/v0/ping` to check that the service is up.
|
|
|
|
|
|
## Structure
|
|
|
|
```
|
|
├── main.go # Pulumi program entry point
|
|
├── Pulumi.yaml # Project configuration
|
|
├── Pulumi.local.yaml # Local stack configuration
|
|
├── Pulumi.gcpProd.yaml # GCP production stack configuration
|
|
├── Pulumi.gcpStaging.yaml # GCP staging stack configuration
|
|
├── Makefile # Build and deployment targets
|
|
├── go.mod # Go module dependencies
|
|
├── go.sum # Go module checksums
|
|
└── pkg/ # Infrastructure packages
|
|
├── k8s/ # Kubernetes deployment components
|
|
│ ├── backup.go # Database backup configuration
|
|
│ ├── cert_manager.go # SSL certificate management
|
|
│ ├── deploy.go # Deployment orchestration
|
|
│ ├── ingress.go # Ingress controller setup
|
|
│ ├── monitoring.go # Metrics and monitoring setup
|
|
│ ├── postgres.go # PostgreSQL database deployment
|
|
│ └── registry.go # MCP Registry deployment
|
|
└── providers/ # Kubernetes cluster providers
|
|
├── types.go # Provider interface definitions
|
|
├── gcp/ # Google Kubernetes Engine provider
|
|
└── local/ # Local kubeconfig provider
|
|
```
|
|
|
|
### Architecture Overview
|
|
|
|
#### Deployment Flow
|
|
1. Pulumi program starts in `main.go`
|
|
2. Configuration is loaded from Pulumi config files
|
|
3. Provider factory creates appropriate cluster provider (GCP or local)
|
|
4. Cluster provider sets up Kubernetes access
|
|
5. `k8s.DeployAll()` orchestrates complete deployment:
|
|
- Certificate manager for SSL/TLS
|
|
- Ingress controller for external access
|
|
- Database for data persistence
|
|
- Backup infrastructure for database
|
|
- Monitoring and metrics collection
|
|
- MCP Registry application
|
|
|
|
## Configuration
|
|
|
|
Configuration keys are namespaced. Set them with `pulumi config set <key> [--secret]`.
|
|
|
|
| Parameter | Description | Required |
|
|
|-----------|-------------|----------|
|
|
| `mcp-registry:environment` | Deployment environment (local/staging/prod) | Yes |
|
|
| `mcp-registry:provider` | Kubernetes provider (local/gcp) | No (default: local) |
|
|
| `mcp-registry:githubClientId` | GitHub OAuth Client ID | Yes |
|
|
| `mcp-registry:githubClientSecret` | GitHub OAuth Client Secret | Yes (secret) |
|
|
| `mcp-registry:jwtPrivateKey` | Ed25519 seed used to sign registry JWTs | Yes (secret) |
|
|
| `mcp-registry:googleOauthClientSecret` | Google OAuth client secret for Grafana login | Yes (secret) |
|
|
| `mcp-registry:imageTag` | Docker image tag for production environment | Yes (prod only) |
|
|
| `gcp:project` | GCP Project ID | Yes when `provider=gcp` |
|
|
| `gcp:region` | GCP Region | No (default: us-central1) |
|
|
| `gcp:credentials` | GCP service account credentials JSON | No (secret; set in staging/prod) |
|
|
|
|
## Database Backups
|
|
|
|
The deployment uses [K8up](https://k8up.io/) (a Kubernetes backup operator) that uses [Restic](https://restic.net/) under the hood.
|
|
|
|
When running locally they are stored in a Minio bucket. In staging and production, backups are stored in a GCS bucket.
|
|
|
|
### Accessing Backup Files
|
|
|
|
#### Local Development (MinIO)
|
|
|
|
```bash
|
|
# Expose MinIO web console
|
|
kubectl port-forward -n minio svc/minio 9000:9000 9001:9001
|
|
```
|
|
|
|
Then open [localhost:9001](http://localhost:9001), login with username `minioadmin` and password `minioadmin`, and navigate to the k8up-backups bucket.
|
|
|
|
##### Staging and Production (GCS)
|
|
|
|
- [Staging](https://console.cloud.google.com/storage/browser/mcp-registry-staging-backups?project=mcp-registry-staging)
|
|
- [Production](https://console.cloud.google.com/storage/browser/mcp-registry-prod-backups?project=mcp-registry-prod)
|
|
|
|
#### Decrypting and Restoring Backups
|
|
|
|
Backups are encrypted using Restic. To access the backup data:
|
|
|
|
1. **Download the backup files from the bucket:**
|
|
```bash
|
|
# Local (MinIO) - ensure port-forward is active: kubectl port-forward -n minio svc/minio 9000:9000 9001:9001
|
|
AWS_ACCESS_KEY_ID=minioadmin AWS_SECRET_ACCESS_KEY=minioadmin \
|
|
aws --endpoint-url http://localhost:9000 s3 sync s3://k8up-backups/ ./backup-files/
|
|
|
|
# GCS (staging/production)
|
|
gsutil -m cp -r gs://mcp-registry-{staging|prod}-backups/* ./backup-files/
|
|
```
|
|
2. **[Install Restic](https://restic.readthedocs.io/en/latest/020_installation.html)**
|
|
3. **Restore the backup:**
|
|
```bash
|
|
RESTIC_PASSWORD=password restic -r ./backup-files restore latest --target ./restored-files
|
|
```
|
|
PostgreSQL data will be in `./restored-files/data/registry-pg-1/pgdata/`
|
|
|
|
## Troubleshooting
|
|
|
|
To configure kubectl to access an existing GKE cluster:
|
|
|
|
```bash
|
|
# Login
|
|
gcloud auth login
|
|
gcloud auth application-default login
|
|
|
|
# For production
|
|
gcloud container clusters get-credentials mcp-registry-prod --zone us-central1-b --project mcp-registry-prod
|
|
|
|
# For staging
|
|
gcloud container clusters get-credentials mcp-registry-staging --zone us-central1-b --project mcp-registry-staging
|
|
```
|
|
|
|
### Check Status
|
|
|
|
```bash
|
|
kubectl get pods
|
|
kubectl get deployment
|
|
kubectl get svc
|
|
kubectl get ingress
|
|
kubectl get svc -n ingress-nginx
|
|
```
|
|
|
|
### View Logs
|
|
|
|
```bash
|
|
kubectl logs -l app=mcp-registry
|
|
kubectl logs -l app=postgres
|
|
```
|
|
|
|
### Check Backup Status
|
|
```bash
|
|
kubectl describe schedule.k8up.io
|
|
kubectl get backup
|
|
```
|