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 <noreply@anthropic.com>
This commit is contained in:
parent
1821f68360
commit
667e2840b8
8 changed files with 1640 additions and 1 deletions
45
deploy/pkg/k8s/cert_manager.go
Normal file
45
deploy/pkg/k8s/cert_manager.go
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package k8s
|
||||
|
||||
import (
|
||||
"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3"
|
||||
v1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
|
||||
metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers"
|
||||
)
|
||||
|
||||
// SetupCertManager sets up cert-manager for TLS certificates
|
||||
func SetupCertManager(ctx *pulumi.Context, cluster *providers.ProviderInfo) error {
|
||||
// Create namespace for cert-manager
|
||||
_, err := v1.NewNamespace(ctx, "cert-manager", &v1.NamespaceArgs{
|
||||
Metadata: &metav1.ObjectMetaArgs{
|
||||
Name: pulumi.String("cert-manager"),
|
||||
},
|
||||
}, pulumi.Provider(cluster.Provider))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Install cert-manager for TLS certificates
|
||||
_, err = helm.NewChart(ctx, "cert-manager", helm.ChartArgs{
|
||||
Chart: pulumi.String("cert-manager"),
|
||||
Version: pulumi.String("v1.18.2"),
|
||||
FetchArgs: helm.FetchArgs{
|
||||
Repo: pulumi.String("https://charts.jetstack.io"),
|
||||
},
|
||||
Namespace: pulumi.String("cert-manager"),
|
||||
Values: pulumi.Map{
|
||||
"installCRDs": pulumi.Bool(true),
|
||||
"ingressShim": pulumi.Map{
|
||||
"defaultIssuerName": pulumi.String("letsencrypt-prod"),
|
||||
"defaultIssuerKind": pulumi.String("ClusterIssuer"),
|
||||
},
|
||||
},
|
||||
}, pulumi.Provider(cluster.Provider))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
37
deploy/pkg/k8s/deploy.go
Normal file
37
deploy/pkg/k8s/deploy.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package k8s
|
||||
|
||||
import (
|
||||
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers"
|
||||
)
|
||||
|
||||
// DeployAll orchestrates the complete deployment of the MCP Registry to Kubernetes
|
||||
func DeployAll(ctx *pulumi.Context, cluster *providers.ProviderInfo, environment string) (service *corev1.Service, err error) {
|
||||
// Setup cert-manager
|
||||
err = SetupCertManager(ctx, cluster)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Setup ingress controller
|
||||
err = SetupIngressController(ctx, cluster, environment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Deploy MongoDB
|
||||
err = DeployMongoDB(ctx, cluster, environment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Deploy MCP Registry
|
||||
service, err = DeployMCPRegistry(ctx, cluster, environment)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
102
deploy/pkg/k8s/ingress.go
Normal file
102
deploy/pkg/k8s/ingress.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package k8s
|
||||
|
||||
import (
|
||||
v1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
|
||||
"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3"
|
||||
metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers"
|
||||
)
|
||||
|
||||
// SetupIngressController sets up the NGINX Ingress Controller
|
||||
func SetupIngressController(ctx *pulumi.Context, cluster *providers.ProviderInfo, environment string) error {
|
||||
conf := config.New(ctx, "mcp-registry")
|
||||
provider := conf.Get("provider")
|
||||
if provider == "" {
|
||||
provider = "local"
|
||||
}
|
||||
|
||||
// Create namespace for ingress-nginx
|
||||
_, err := v1.NewNamespace(ctx, "ingress-nginx", &v1.NamespaceArgs{
|
||||
Metadata: &metav1.ObjectMetaArgs{
|
||||
Name: pulumi.String("ingress-nginx"),
|
||||
},
|
||||
}, pulumi.Provider(cluster.Provider))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Install NGINX Ingress Controller
|
||||
ingressType := "LoadBalancer"
|
||||
if provider == "local" {
|
||||
ingressType = "NodePort"
|
||||
}
|
||||
|
||||
nginxIngress, err := helm.NewChart(ctx, "nginx-ingress", helm.ChartArgs{
|
||||
Chart: pulumi.String("ingress-nginx"),
|
||||
Version: pulumi.String("4.13.0"),
|
||||
FetchArgs: helm.FetchArgs{
|
||||
Repo: pulumi.String("https://kubernetes.github.io/ingress-nginx"),
|
||||
},
|
||||
Namespace: pulumi.String("ingress-nginx"),
|
||||
Values: pulumi.Map{
|
||||
"controller": pulumi.Map{
|
||||
"service": pulumi.Map{
|
||||
"type": pulumi.String(ingressType),
|
||||
// Add Azure Load Balancer health probe annotation as otherwise it defaults to / which fails
|
||||
"annotations": pulumi.Map{
|
||||
"service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path": pulumi.String("/healthz"),
|
||||
},
|
||||
},
|
||||
"config": pulumi.Map{
|
||||
// Disable strict path validation, to work around a bug in ingress-nginx
|
||||
// https://cert-manager.io/docs/releases/release-notes/release-notes-1.18/#acme-http01-challenge-paths-now-use-pathtype-exact-in-ingress-routes
|
||||
// https://github.com/kubernetes/ingress-nginx/issues/11176
|
||||
"strict-validate-path-type": pulumi.String("false"),
|
||||
|
||||
// Use forwarded headers for proper client IP handling
|
||||
"use-forwarded-headers": pulumi.String("true"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, pulumi.Provider(cluster.Provider))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Use the helm chart to get service information after deployment
|
||||
ingressIps := nginxIngress.Resources.ApplyT(func(resources interface{}) interface{} {
|
||||
if ctx.DryRun() {
|
||||
return []string{} // Return empty array on error during preview
|
||||
}
|
||||
|
||||
// Look up the service after the chart is ready
|
||||
svc, err := v1.GetService(
|
||||
ctx,
|
||||
"nginx-ingress-controller-lookup",
|
||||
pulumi.ID("ingress-nginx/nginx-ingress-ingress-nginx-controller"),
|
||||
&v1.ServiceState{},
|
||||
pulumi.Provider(cluster.Provider),
|
||||
pulumi.DependsOn([]pulumi.Resource{nginxIngress}),
|
||||
)
|
||||
if err != nil {
|
||||
return []string{} // Return empty array on error during preview
|
||||
}
|
||||
|
||||
// Return the LoadBalancer ingress IPs
|
||||
return svc.Status.LoadBalancer().Ingress().ApplyT(func(ingresses []v1.LoadBalancerIngress) []string {
|
||||
var ips []string
|
||||
for _, ingress := range ingresses {
|
||||
if ip := ingress.Ip; ip != nil && *ip != "" {
|
||||
ips = append(ips, *ip)
|
||||
}
|
||||
}
|
||||
return ips
|
||||
})
|
||||
})
|
||||
ctx.Export("ingressIps", ingressIps)
|
||||
|
||||
return nil
|
||||
}
|
||||
141
deploy/pkg/k8s/mongodb.go
Normal file
141
deploy/pkg/k8s/mongodb.go
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
package k8s
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1"
|
||||
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
|
||||
metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers"
|
||||
)
|
||||
|
||||
// DeployMongoDB deploys MongoDB to the Kubernetes cluster
|
||||
func DeployMongoDB(ctx *pulumi.Context, cluster *providers.ProviderInfo, environment string) error {
|
||||
// Create PersistentVolumeClaim for MongoDB data
|
||||
_, err := corev1.NewPersistentVolumeClaim(ctx, "mongodb-pvc", &corev1.PersistentVolumeClaimArgs{
|
||||
Metadata: &metav1.ObjectMetaArgs{
|
||||
Name: pulumi.String("mongodb-pvc"),
|
||||
Namespace: pulumi.String("default"),
|
||||
Labels: pulumi.StringMap{
|
||||
"app": pulumi.String("mongodb"),
|
||||
"environment": pulumi.String(environment),
|
||||
},
|
||||
},
|
||||
Spec: &corev1.PersistentVolumeClaimSpecArgs{
|
||||
AccessModes: pulumi.StringArray{
|
||||
pulumi.String("ReadWriteOnce"),
|
||||
},
|
||||
Resources: &corev1.VolumeResourceRequirementsArgs{
|
||||
Requests: pulumi.StringMap{
|
||||
"storage": pulumi.String("10Gi"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, pulumi.Provider(cluster.Provider))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create MongoDB Deployment
|
||||
_, err = v1.NewDeployment(ctx, "mongodb", &v1.DeploymentArgs{
|
||||
Metadata: &metav1.ObjectMetaArgs{
|
||||
Name: pulumi.String("mongodb"),
|
||||
Namespace: pulumi.String("default"),
|
||||
Labels: pulumi.StringMap{
|
||||
"app": pulumi.String("mongodb"),
|
||||
"environment": pulumi.String(environment),
|
||||
},
|
||||
},
|
||||
Spec: &v1.DeploymentSpecArgs{
|
||||
Selector: &metav1.LabelSelectorArgs{
|
||||
MatchLabels: pulumi.StringMap{
|
||||
"app": pulumi.String("mongodb"),
|
||||
},
|
||||
},
|
||||
Template: &corev1.PodTemplateSpecArgs{
|
||||
Metadata: &metav1.ObjectMetaArgs{
|
||||
Labels: pulumi.StringMap{
|
||||
"app": pulumi.String("mongodb"),
|
||||
},
|
||||
},
|
||||
Spec: &corev1.PodSpecArgs{
|
||||
Containers: corev1.ContainerArray{
|
||||
&corev1.ContainerArgs{
|
||||
Name: pulumi.String("mongodb"),
|
||||
Image: pulumi.String("mongo:7.0"),
|
||||
Ports: corev1.ContainerPortArray{
|
||||
&corev1.ContainerPortArgs{
|
||||
ContainerPort: pulumi.Int(27017),
|
||||
Name: pulumi.String("mongodb"),
|
||||
},
|
||||
},
|
||||
VolumeMounts: corev1.VolumeMountArray{
|
||||
&corev1.VolumeMountArgs{
|
||||
Name: pulumi.String("mongodb-data"),
|
||||
MountPath: pulumi.String("/data/db"),
|
||||
},
|
||||
},
|
||||
LivenessProbe: &corev1.ProbeArgs{
|
||||
TcpSocket: &corev1.TCPSocketActionArgs{
|
||||
Port: pulumi.Int(27017),
|
||||
},
|
||||
InitialDelaySeconds: pulumi.Int(30),
|
||||
TimeoutSeconds: pulumi.Int(5),
|
||||
},
|
||||
ReadinessProbe: &corev1.ProbeArgs{
|
||||
TcpSocket: &corev1.TCPSocketActionArgs{
|
||||
Port: pulumi.Int(27017),
|
||||
},
|
||||
InitialDelaySeconds: pulumi.Int(5),
|
||||
TimeoutSeconds: pulumi.Int(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
Volumes: corev1.VolumeArray{
|
||||
&corev1.VolumeArgs{
|
||||
Name: pulumi.String("mongodb-data"),
|
||||
PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSourceArgs{
|
||||
ClaimName: pulumi.String("mongodb-pvc"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, pulumi.Provider(cluster.Provider))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Create MongoDB Service
|
||||
_, err = corev1.NewService(ctx, "mongodb", &corev1.ServiceArgs{
|
||||
Metadata: &metav1.ObjectMetaArgs{
|
||||
Name: pulumi.String("mongodb"),
|
||||
Namespace: pulumi.String("default"),
|
||||
Labels: pulumi.StringMap{
|
||||
"app": pulumi.String("mongodb"),
|
||||
"environment": pulumi.String(environment),
|
||||
},
|
||||
},
|
||||
Spec: &corev1.ServiceSpecArgs{
|
||||
Selector: pulumi.StringMap{
|
||||
"app": pulumi.String("mongodb"),
|
||||
},
|
||||
Ports: corev1.ServicePortArray{
|
||||
&corev1.ServicePortArgs{
|
||||
Port: pulumi.Int(27017),
|
||||
TargetPort: pulumi.Int(27017),
|
||||
Name: pulumi.String("mongodb"),
|
||||
},
|
||||
},
|
||||
Type: pulumi.String("ClusterIP"),
|
||||
},
|
||||
}, pulumi.Provider(cluster.Provider))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create MongoDB service: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
220
deploy/pkg/k8s/registry.go
Normal file
220
deploy/pkg/k8s/registry.go
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
package k8s
|
||||
|
||||
import (
|
||||
v1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apps/v1"
|
||||
corev1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/core/v1"
|
||||
metav1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/meta/v1"
|
||||
networkingv1 "github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/networking/v1"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers"
|
||||
)
|
||||
|
||||
// DeployMCPRegistry deploys the MCP Registry to the Kubernetes cluster
|
||||
func DeployMCPRegistry(ctx *pulumi.Context, cluster *providers.ProviderInfo, environment string) (*corev1.Service, error) {
|
||||
conf := config.New(ctx, "mcp-registry")
|
||||
githubClientId := conf.Require("githubClientId")
|
||||
githubClientSecret := conf.RequireSecret("githubClientSecret")
|
||||
|
||||
// Create Secret with sensitive configuration
|
||||
secret, err := corev1.NewSecret(ctx, "mcp-registry-secrets", &corev1.SecretArgs{
|
||||
Metadata: &metav1.ObjectMetaArgs{
|
||||
Name: pulumi.String("mcp-registry-secrets"),
|
||||
Namespace: pulumi.String("default"),
|
||||
Labels: pulumi.StringMap{
|
||||
"app": pulumi.String("mcp-registry"),
|
||||
"environment": pulumi.String(environment),
|
||||
},
|
||||
},
|
||||
StringData: pulumi.StringMap{
|
||||
"GITHUB_CLIENT_SECRET": githubClientSecret,
|
||||
},
|
||||
Type: pulumi.String("Opaque"),
|
||||
}, pulumi.Provider(cluster.Provider))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create Deployment
|
||||
_, err = v1.NewDeployment(ctx, "mcp-registry", &v1.DeploymentArgs{
|
||||
Metadata: &metav1.ObjectMetaArgs{
|
||||
Name: pulumi.String("mcp-registry"),
|
||||
Namespace: pulumi.String("default"),
|
||||
Labels: pulumi.StringMap{
|
||||
"app": pulumi.String("mcp-registry"),
|
||||
"environment": pulumi.String(environment),
|
||||
},
|
||||
},
|
||||
Spec: &v1.DeploymentSpecArgs{
|
||||
Replicas: pulumi.Int(2),
|
||||
Selector: &metav1.LabelSelectorArgs{
|
||||
MatchLabels: pulumi.StringMap{
|
||||
"app": pulumi.String("mcp-registry"),
|
||||
},
|
||||
},
|
||||
Template: &corev1.PodTemplateSpecArgs{
|
||||
Metadata: &metav1.ObjectMetaArgs{
|
||||
Labels: pulumi.StringMap{
|
||||
"app": pulumi.String("mcp-registry"),
|
||||
},
|
||||
},
|
||||
Spec: &corev1.PodSpecArgs{
|
||||
Containers: corev1.ContainerArray{
|
||||
&corev1.ContainerArgs{
|
||||
Name: pulumi.String("mcp-registry"),
|
||||
// TODO: Replace with actual MCP registry image once on GHCR
|
||||
Image: pulumi.String("nginx:alpine"),
|
||||
Ports: corev1.ContainerPortArray{
|
||||
&corev1.ContainerPortArgs{
|
||||
// TODO: Change to port 8080 once using registry image
|
||||
ContainerPort: pulumi.Int(80),
|
||||
Name: pulumi.String("http"),
|
||||
},
|
||||
},
|
||||
Env: corev1.EnvVarArray{
|
||||
&corev1.EnvVarArgs{
|
||||
Name: pulumi.String("DATABASE_URL"),
|
||||
Value: pulumi.String("mongodb://mongodb.default.svc.cluster.local:27017"),
|
||||
},
|
||||
&corev1.EnvVarArgs{
|
||||
Name: pulumi.String("PORT"),
|
||||
Value: pulumi.String("8080"),
|
||||
},
|
||||
&corev1.EnvVarArgs{
|
||||
Name: pulumi.String("NODE_ENV"),
|
||||
Value: pulumi.String("production"),
|
||||
},
|
||||
&corev1.EnvVarArgs{
|
||||
Name: pulumi.String("LOG_LEVEL"),
|
||||
Value: pulumi.String("info"),
|
||||
},
|
||||
&corev1.EnvVarArgs{
|
||||
Name: pulumi.String("CORS_ORIGINS"),
|
||||
Value: pulumi.String("*"),
|
||||
},
|
||||
&corev1.EnvVarArgs{
|
||||
Name: pulumi.String("GITHUB_CLIENT_ID"),
|
||||
Value: pulumi.String(githubClientId),
|
||||
},
|
||||
&corev1.EnvVarArgs{
|
||||
Name: pulumi.String("GITHUB_CLIENT_SECRET"),
|
||||
ValueFrom: &corev1.EnvVarSourceArgs{
|
||||
SecretKeyRef: &corev1.SecretKeySelectorArgs{
|
||||
Name: secret.Metadata.Name(),
|
||||
Key: pulumi.String("GITHUB_CLIENT_SECRET"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
// TODO: uncomment when using registry image
|
||||
// LivenessProbe: &corev1.ProbeArgs{
|
||||
// HttpGet: &corev1.HTTPGetActionArgs{
|
||||
// Path: pulumi.String("/v0/health"),
|
||||
// Port: pulumi.Int(8080),
|
||||
// },
|
||||
// InitialDelaySeconds: pulumi.Int(30),
|
||||
// TimeoutSeconds: pulumi.Int(5),
|
||||
// },
|
||||
// ReadinessProbe: &corev1.ProbeArgs{
|
||||
// HttpGet: &corev1.HTTPGetActionArgs{
|
||||
// Path: pulumi.String("/v0/health"),
|
||||
// Port: pulumi.Int(8080),
|
||||
// },
|
||||
// InitialDelaySeconds: pulumi.Int(5),
|
||||
// TimeoutSeconds: pulumi.Int(3),
|
||||
// },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, pulumi.Provider(cluster.Provider))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create Service
|
||||
service, err := corev1.NewService(ctx, "mcp-registry", &corev1.ServiceArgs{
|
||||
Metadata: &metav1.ObjectMetaArgs{
|
||||
Name: pulumi.String("mcp-registry"),
|
||||
Namespace: pulumi.String("default"),
|
||||
Labels: pulumi.StringMap{
|
||||
"app": pulumi.String("mcp-registry"),
|
||||
"environment": pulumi.String(environment),
|
||||
},
|
||||
},
|
||||
Spec: &corev1.ServiceSpecArgs{
|
||||
Selector: pulumi.StringMap{
|
||||
"app": pulumi.String("mcp-registry"),
|
||||
},
|
||||
Ports: corev1.ServicePortArray{
|
||||
&corev1.ServicePortArgs{
|
||||
Port: pulumi.Int(80),
|
||||
// TODO: Change to port 8080 once using registry image
|
||||
TargetPort: pulumi.Int(80),
|
||||
Name: pulumi.String("http"),
|
||||
},
|
||||
},
|
||||
Type: pulumi.String("ClusterIP"),
|
||||
},
|
||||
}, pulumi.Provider(cluster.Provider))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create Ingress
|
||||
ingress, err := networkingv1.NewIngress(ctx, "mcp-registry", &networkingv1.IngressArgs{
|
||||
Metadata: &metav1.ObjectMetaArgs{
|
||||
Name: pulumi.String("mcp-registry"),
|
||||
Namespace: pulumi.String("default"),
|
||||
Labels: pulumi.StringMap{
|
||||
"app": pulumi.String("mcp-registry"),
|
||||
"environment": pulumi.String(environment),
|
||||
},
|
||||
Annotations: pulumi.StringMap{
|
||||
"kubernetes.io/ingress.class": pulumi.String("nginx"),
|
||||
"cert-manager.io/cluster-issuer": pulumi.String("letsencrypt-prod"),
|
||||
"nginx.ingress.kubernetes.io/rewrite-target": pulumi.String("/"),
|
||||
},
|
||||
},
|
||||
Spec: &networkingv1.IngressSpecArgs{
|
||||
Tls: networkingv1.IngressTLSArray{
|
||||
&networkingv1.IngressTLSArgs{
|
||||
Hosts: pulumi.StringArray{
|
||||
pulumi.Sprintf("mcp-registry-%s.example.com", environment),
|
||||
},
|
||||
SecretName: pulumi.Sprintf("mcp-registry-%s-tls", environment),
|
||||
},
|
||||
},
|
||||
Rules: networkingv1.IngressRuleArray{
|
||||
&networkingv1.IngressRuleArgs{
|
||||
Host: pulumi.Sprintf("mcp-registry-%s.example.com", environment),
|
||||
Http: &networkingv1.HTTPIngressRuleValueArgs{
|
||||
Paths: networkingv1.HTTPIngressPathArray{
|
||||
&networkingv1.HTTPIngressPathArgs{
|
||||
Path: pulumi.String("/"),
|
||||
PathType: pulumi.String("Prefix"),
|
||||
Backend: &networkingv1.IngressBackendArgs{
|
||||
Service: &networkingv1.IngressServiceBackendArgs{
|
||||
Name: service.Metadata.Name().Elem(),
|
||||
Port: &networkingv1.ServiceBackendPortArgs{
|
||||
Number: pulumi.Int(80),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, pulumi.Provider(cluster.Provider))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx.Export("ingressHost", ingress.Spec.Rules().Index(pulumi.Int(0)).Host())
|
||||
|
||||
return service, nil
|
||||
}
|
||||
99
deploy/pkg/providers/aks/provider.go
Normal file
99
deploy/pkg/providers/aks/provider.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
package aks
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"github.com/pulumi/pulumi-azure-native-sdk/containerservice"
|
||||
"github.com/pulumi/pulumi-azure-native-sdk/resources"
|
||||
"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers"
|
||||
)
|
||||
|
||||
// Provider implements the ClusterProvider interface for Azure Kubernetes Service
|
||||
type Provider struct{}
|
||||
|
||||
// CreateCluster creates an Azure Kubernetes Service cluster
|
||||
func (p *Provider) CreateCluster(ctx *pulumi.Context, environment string) (*providers.ProviderInfo, error) {
|
||||
// Get resource group name from config or use default
|
||||
conf := config.New(ctx, "mcp-registry")
|
||||
resourceGroupName := conf.Get("resourceGroupName")
|
||||
if resourceGroupName == "" {
|
||||
resourceGroupName = "official-mcp-registry-prod"
|
||||
}
|
||||
|
||||
// Get existing resource group
|
||||
resourceGroup, err := resources.LookupResourceGroup(ctx, &resources.LookupResourceGroupArgs{
|
||||
ResourceGroupName: resourceGroupName,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to find resource group '%s': %w", resourceGroupName, err)
|
||||
}
|
||||
|
||||
// Create AKS cluster
|
||||
clusterName := fmt.Sprintf("mcp-registry-%s", environment)
|
||||
|
||||
cluster, err := containerservice.NewManagedCluster(ctx, clusterName, &containerservice.ManagedClusterArgs{
|
||||
ResourceGroupName: pulumi.String(resourceGroup.Name),
|
||||
Location: pulumi.String(resourceGroup.Location),
|
||||
DnsPrefix: pulumi.String(clusterName),
|
||||
KubernetesVersion: pulumi.String("1.33.2"),
|
||||
Identity: &containerservice.ManagedClusterIdentityArgs{
|
||||
Type: containerservice.ResourceIdentityTypeSystemAssigned,
|
||||
},
|
||||
AgentPoolProfiles: containerservice.ManagedClusterAgentPoolProfileArray{
|
||||
&containerservice.ManagedClusterAgentPoolProfileArgs{
|
||||
Name: pulumi.String("nodepool1"),
|
||||
Count: pulumi.Int(2),
|
||||
VmSize: pulumi.String("Standard_B2s"),
|
||||
OsType: pulumi.String("Linux"),
|
||||
Mode: pulumi.String("System"),
|
||||
},
|
||||
},
|
||||
NetworkProfile: &containerservice.ContainerServiceNetworkProfileArgs{
|
||||
NetworkPlugin: pulumi.String("azure"),
|
||||
ServiceCidr: pulumi.String("10.0.0.0/16"),
|
||||
DnsServiceIP: pulumi.String("10.0.0.10"),
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Get AKS credentials
|
||||
creds := pulumi.All(cluster.Name, pulumi.String(resourceGroup.Name)).ApplyT(
|
||||
func(args []any) (string, error) {
|
||||
clusterName := args[0].(string)
|
||||
rgName := args[1].(string)
|
||||
credentials, err := containerservice.ListManagedClusterUserCredentials(ctx, &containerservice.ListManagedClusterUserCredentialsArgs{
|
||||
ResourceGroupName: rgName,
|
||||
ResourceName: clusterName,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Decode base64 kubeconfig
|
||||
kubeconfigData, err := base64.StdEncoding.DecodeString(credentials.Kubeconfigs[0].Value)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode kubeconfig: %w", err)
|
||||
}
|
||||
return string(kubeconfigData), nil
|
||||
},
|
||||
).(pulumi.StringOutput)
|
||||
|
||||
// Create Kubernetes provider for AKS
|
||||
k8sProvider, err := kubernetes.NewProvider(ctx, "k8s-provider", &kubernetes.ProviderArgs{
|
||||
Kubeconfig: creds,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &providers.ProviderInfo{
|
||||
Name: cluster.Name,
|
||||
Provider: k8sProvider,
|
||||
}, nil
|
||||
}
|
||||
25
deploy/pkg/providers/local/provider.go
Normal file
25
deploy/pkg/providers/local/provider.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package local
|
||||
|
||||
import (
|
||||
"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
|
||||
|
||||
"github.com/modelcontextprotocol/registry/deploy/infra/pkg/providers"
|
||||
)
|
||||
|
||||
// Provider implements the ClusterProvider interface for local Kubernetes clusters
|
||||
type Provider struct{}
|
||||
|
||||
// CreateCluster configures access to a local Kubernetes cluster via kubeconfig
|
||||
func (p *Provider) CreateCluster(ctx *pulumi.Context, environment string) (*providers.ProviderInfo, error) {
|
||||
// Create Kubernetes provider for local cluster
|
||||
k8sProvider, err := kubernetes.NewProvider(ctx, "k8s-provider", &kubernetes.ProviderArgs{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &providers.ProviderInfo{
|
||||
Name: pulumi.String("local").ToStringOutput(),
|
||||
Provider: k8sProvider,
|
||||
}, nil
|
||||
}
|
||||
18
deploy/pkg/providers/types.go
Normal file
18
deploy/pkg/providers/types.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package providers
|
||||
|
||||
import (
|
||||
"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes"
|
||||
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
|
||||
)
|
||||
|
||||
// ProviderInfo represents the information returned by a cluster provider
|
||||
type ProviderInfo struct {
|
||||
Name pulumi.StringOutput
|
||||
Provider *kubernetes.Provider
|
||||
}
|
||||
|
||||
// ClusterProvider defines the interface that all cluster providers must implement
|
||||
type ClusterProvider interface {
|
||||
// CreateCluster creates a Kubernetes cluster and returns provider info
|
||||
CreateCluster(ctx *pulumi.Context, environment string) (*ProviderInfo, error)
|
||||
}
|
||||
Loading…
Reference in a new issue