deploy: Deploy the registry to GCP (#255)
Adds the Pulumi code to: - Deploy the registry (and associated services e.g. mongodb) to Google Cloud Platform (GCP), on top of Google Kubernetes Engine (GKE) - Sets up proper environments and secrets management - Uses the real container image, now that it's published in #225. At the moment attached to latest, we might want to pin the version later (or perhaps always use `latest` in staging, and pin prod) - Uses real domains (`staging.registry.modelcontextprotocol.io`) rather than examples (``) ## Motivation and Context Setting up infrastructure to deploy it. I set something up in Azure in #227, although not super robust (e.g. no service accounts etc.). Think we will use GCP as: - the maintainers have experience with GCP, but none with Azure - costs are quite low, and Anthropic is happy to cover them in the short term - means we only have to maintain one login system (just Google Cloud Identity), not two (Google Workspace + Azure) ## How Has This Been Tested? Deployed this to a staging and production cluster. Try it yourself at: ```bash curl -H "Host: staging.registry.modelcontextprotocol.io" -k https://35.222.36.75/v0/ping ``` (will be sorting out domains very soon) ## Breaking Changes NA - just adds support for GCP deployment ## Types of changes <!-- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation update ## Checklist <!-- Go over all the following points, and put an `x` in all the boxes that apply. --> - [x] I have read the [MCP Documentation](https://modelcontextprotocol.io) - [x] My code follows the repository's style guidelines - [ ] New and existing tests pass locally - [x] I have added appropriate error handling - [x] I have added or updated documentation as needed ## Additional context <!-- Add any other context, implementation notes, or design decisions --> Expected follow-ups: - GitHub Action setup to deploy things to the cluster from GitHub, to avoid gatekeeping to just the people with the secrets.
This commit is contained in:
parent
667e2840b8
commit
fe52766676
3 changed files with 395 additions and 144 deletions
|
|
@ -1,8 +1,10 @@
|
|||
package k8s
|
||||
|
||||
import (
|
||||
"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/helm/v3"
|
||||
"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes"
|
||||
"github.com/pulumi/pulumi-kubernetes/sdk/v4/go/kubernetes/apiextensions"
|
||||
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"
|
||||
|
||||
|
|
@ -41,5 +43,36 @@ func SetupCertManager(ctx *pulumi.Context, cluster *providers.ProviderInfo) erro
|
|||
return err
|
||||
}
|
||||
|
||||
_, err = apiextensions.NewCustomResource(ctx, "letsencrypt-prod", &apiextensions.CustomResourceArgs{
|
||||
ApiVersion: pulumi.String("cert-manager.io/v1"),
|
||||
Kind: pulumi.String("ClusterIssuer"),
|
||||
Metadata: &metav1.ObjectMetaArgs{
|
||||
Name: pulumi.String("letsencrypt-prod"),
|
||||
},
|
||||
OtherFields: kubernetes.UntypedArgs{
|
||||
"spec": pulumi.Map{
|
||||
"acme": pulumi.Map{
|
||||
"server": pulumi.String("https://acme-v02.api.letsencrypt.org/directory"),
|
||||
"email": pulumi.String("admin@modelcontextprotocol.io"),
|
||||
"privateKeySecretRef": pulumi.Map{
|
||||
"name": pulumi.String("letsencrypt-prod-key"),
|
||||
},
|
||||
"solvers": pulumi.Array{
|
||||
pulumi.Map{
|
||||
"http01": pulumi.Map{
|
||||
"ingress": pulumi.Map{
|
||||
"ingressClassName": pulumi.String("nginx"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, pulumi.Provider(cluster.Provider))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ func SetupIngressController(ctx *pulumi.Context, cluster *providers.ProviderInfo
|
|||
ingressType = "NodePort"
|
||||
}
|
||||
|
||||
nginxIngress, err := helm.NewChart(ctx, "nginx-ingress", helm.ChartArgs{
|
||||
nginxIngress, err := helm.NewChart(ctx, "ingress-nginx", helm.ChartArgs{
|
||||
Chart: pulumi.String("ingress-nginx"),
|
||||
Version: pulumi.String("4.13.0"),
|
||||
FetchArgs: helm.FetchArgs{
|
||||
|
|
@ -45,8 +45,8 @@ func SetupIngressController(ctx *pulumi.Context, cluster *providers.ProviderInfo
|
|||
"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{
|
||||
// Add Azure Load Balancer health probe annotation as otherwise it defaults to / which fails
|
||||
"service.beta.kubernetes.io/azure-load-balancer-health-probe-request-path": pulumi.String("/healthz"),
|
||||
},
|
||||
},
|
||||
|
|
@ -75,8 +75,8 @@ func SetupIngressController(ctx *pulumi.Context, cluster *providers.ProviderInfo
|
|||
// 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"),
|
||||
"ingress-nginx-controller-lookup",
|
||||
pulumi.ID("ingress-nginx/ingress-nginx-controller"),
|
||||
&v1.ServiceState{},
|
||||
pulumi.Provider(cluster.Provider),
|
||||
pulumi.DependsOn([]pulumi.Resource{nginxIngress}),
|
||||
|
|
|
|||
|
|
@ -62,43 +62,25 @@ func DeployMCPRegistry(ctx *pulumi.Context, cluster *providers.ProviderInfo, env
|
|||
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"),
|
||||
Name: pulumi.String("mcp-registry"),
|
||||
Image: pulumi.String("ghcr.io/modelcontextprotocol/registry:latest"),
|
||||
Ports: corev1.ContainerPortArray{
|
||||
&corev1.ContainerPortArgs{
|
||||
// TODO: Change to port 8080 once using registry image
|
||||
ContainerPort: pulumi.Int(80),
|
||||
ContainerPort: pulumi.Int(8080),
|
||||
Name: pulumi.String("http"),
|
||||
},
|
||||
},
|
||||
Env: corev1.EnvVarArray{
|
||||
&corev1.EnvVarArgs{
|
||||
Name: pulumi.String("DATABASE_URL"),
|
||||
Name: pulumi.String("MCP_REGISTRY_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"),
|
||||
Name: pulumi.String("MCP_REGISTRY_GITHUB_CLIENT_ID"),
|
||||
Value: pulumi.String(githubClientId),
|
||||
},
|
||||
&corev1.EnvVarArgs{
|
||||
Name: pulumi.String("GITHUB_CLIENT_SECRET"),
|
||||
Name: pulumi.String("MCP_REGISTRY_GITHUB_CLIENT_SECRET"),
|
||||
ValueFrom: &corev1.EnvVarSourceArgs{
|
||||
SecretKeyRef: &corev1.SecretKeySelectorArgs{
|
||||
Name: secret.Metadata.Name(),
|
||||
|
|
@ -107,23 +89,22 @@ func DeployMCPRegistry(ctx *pulumi.Context, cluster *providers.ProviderInfo, env
|
|||
},
|
||||
},
|
||||
},
|
||||
// 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),
|
||||
// },
|
||||
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),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -150,9 +131,8 @@ func DeployMCPRegistry(ctx *pulumi.Context, cluster *providers.ProviderInfo, env
|
|||
},
|
||||
Ports: corev1.ServicePortArray{
|
||||
&corev1.ServicePortArgs{
|
||||
Port: pulumi.Int(80),
|
||||
// TODO: Change to port 8080 once using registry image
|
||||
TargetPort: pulumi.Int(80),
|
||||
Port: pulumi.Int(80),
|
||||
TargetPort: pulumi.Int(8080),
|
||||
Name: pulumi.String("http"),
|
||||
},
|
||||
},
|
||||
|
|
@ -164,6 +144,7 @@ func DeployMCPRegistry(ctx *pulumi.Context, cluster *providers.ProviderInfo, env
|
|||
}
|
||||
|
||||
// Create Ingress
|
||||
domain := "%s.registry.modelcontextprotocol.io"
|
||||
ingress, err := networkingv1.NewIngress(ctx, "mcp-registry", &networkingv1.IngressArgs{
|
||||
Metadata: &metav1.ObjectMetaArgs{
|
||||
Name: pulumi.String("mcp-registry"),
|
||||
|
|
@ -173,23 +154,21 @@ func DeployMCPRegistry(ctx *pulumi.Context, cluster *providers.ProviderInfo, env
|
|||
"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("/"),
|
||||
"kubernetes.io/ingress.class": pulumi.String("nginx"),
|
||||
},
|
||||
},
|
||||
Spec: &networkingv1.IngressSpecArgs{
|
||||
Tls: networkingv1.IngressTLSArray{
|
||||
&networkingv1.IngressTLSArgs{
|
||||
Hosts: pulumi.StringArray{
|
||||
pulumi.Sprintf("mcp-registry-%s.example.com", environment),
|
||||
pulumi.Sprintf(domain, environment),
|
||||
},
|
||||
SecretName: pulumi.Sprintf("mcp-registry-%s-tls", environment),
|
||||
},
|
||||
},
|
||||
Rules: networkingv1.IngressRuleArray{
|
||||
&networkingv1.IngressRuleArgs{
|
||||
Host: pulumi.Sprintf("mcp-registry-%s.example.com", environment),
|
||||
Host: pulumi.Sprintf(domain, environment),
|
||||
Http: &networkingv1.HTTPIngressRuleValueArgs{
|
||||
Paths: networkingv1.HTTPIngressPathArray{
|
||||
&networkingv1.HTTPIngressPathArgs{
|
||||
|
|
|
|||
Loading…
Reference in a new issue