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.
182 lines
5.5 KiB
Go
182 lines
5.5 KiB
Go
package gcp
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
|
|
"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp"
|
|
"github.com/pulumi/pulumi-gcp/sdk/v8/go/gcp/container"
|
|
"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 Google Kubernetes Engine
|
|
type Provider struct{}
|
|
|
|
// CreateCluster creates a Google Kubernetes Engine cluster
|
|
func (p *Provider) CreateCluster(ctx *pulumi.Context, environment string) (*providers.ProviderInfo, error) {
|
|
// Get configuration
|
|
gcpConf := config.New(ctx, "gcp")
|
|
|
|
// Get project ID from config or use default
|
|
projectID := gcpConf.Get("project")
|
|
if projectID == "" {
|
|
return nil, fmt.Errorf("GCP project ID not configured. Set gcp:project")
|
|
}
|
|
|
|
// Get region from config or use default
|
|
region := gcpConf.Get("region")
|
|
if region == "" {
|
|
region = "us-central1"
|
|
}
|
|
|
|
// Get credentials from config (base64 encoded service account JSON)
|
|
credentials := gcpConf.Get("credentials")
|
|
if credentials != "" {
|
|
// Decode the base64 credentials
|
|
decodedCreds, err := base64.StdEncoding.DecodeString(credentials)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode GCP credentials: %w", err)
|
|
}
|
|
credentials = string(decodedCreds)
|
|
}
|
|
|
|
// Create a GCP provider with explicit credentials if provided
|
|
var gcpProvider *gcp.Provider
|
|
var err error
|
|
if credentials != "" {
|
|
gcpProvider, err = gcp.NewProvider(ctx, "gcp-explicit", &gcp.ProviderArgs{
|
|
Project: pulumi.String(projectID),
|
|
Region: pulumi.String(region),
|
|
Credentials: pulumi.String(credentials),
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCP provider: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create GKE cluster
|
|
clusterName := fmt.Sprintf("mcp-registry-%s", environment)
|
|
|
|
// Use a specific zone instead of region for zonal cluster
|
|
zone := fmt.Sprintf("%s-b", region)
|
|
|
|
// Configure the GKE cluster
|
|
clusterArgs := &container.ClusterArgs{
|
|
Name: pulumi.String(clusterName),
|
|
Location: pulumi.String(zone),
|
|
Project: pulumi.String(projectID),
|
|
Description: pulumi.String(fmt.Sprintf("MCP Registry %s GKE Cluster", environment)),
|
|
|
|
// Initial node count (will be managed by node pool)
|
|
InitialNodeCount: pulumi.Int(1),
|
|
|
|
// Remove default node pool after cluster creation
|
|
RemoveDefaultNodePool: pulumi.Bool(true),
|
|
|
|
AddonsConfig: &container.ClusterAddonsConfigArgs{
|
|
// Disable as we use ingress-nginx
|
|
HttpLoadBalancing: &container.ClusterAddonsConfigHttpLoadBalancingArgs{
|
|
Disabled: pulumi.Bool(true),
|
|
},
|
|
},
|
|
}
|
|
|
|
// Add provider if we have explicit credentials
|
|
clusterOpts := []pulumi.ResourceOption{}
|
|
if gcpProvider != nil {
|
|
clusterOpts = append(clusterOpts, pulumi.Provider(gcpProvider))
|
|
}
|
|
|
|
cluster, err := container.NewCluster(ctx, clusterName, clusterArgs, clusterOpts...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GKE cluster: %w", err)
|
|
}
|
|
|
|
// Create a managed node pool for the cluster
|
|
nodePoolName := fmt.Sprintf("%s-nodepool", clusterName)
|
|
nodePoolArgs := &container.NodePoolArgs{
|
|
Cluster: cluster.Name,
|
|
Location: pulumi.String(zone),
|
|
Project: pulumi.String(projectID),
|
|
|
|
// Node pool configuration
|
|
NodeCount: pulumi.Int(2),
|
|
NodeConfig: &container.NodePoolNodeConfigArgs{
|
|
MachineType: pulumi.String("e2-small"),
|
|
DiskSizeGb: pulumi.Int(20),
|
|
DiskType: pulumi.String("pd-standard"),
|
|
},
|
|
|
|
// Node management configuration
|
|
Management: &container.NodePoolManagementArgs{
|
|
AutoRepair: pulumi.Bool(true),
|
|
AutoUpgrade: pulumi.Bool(true),
|
|
},
|
|
}
|
|
|
|
// Add provider if we have explicit credentials
|
|
nodePoolOpts := []pulumi.ResourceOption{}
|
|
if gcpProvider != nil {
|
|
nodePoolOpts = append(nodePoolOpts, pulumi.Provider(gcpProvider))
|
|
}
|
|
|
|
nodePool, err := container.NewNodePool(ctx, nodePoolName, nodePoolArgs, nodePoolOpts...)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create node pool: %w", err)
|
|
}
|
|
|
|
// Create Kubernetes provider using the cluster directly
|
|
k8sProvider, err := kubernetes.NewProvider(ctx, "k8s-provider", &kubernetes.ProviderArgs{
|
|
Kubeconfig: pulumi.All(cluster.Endpoint, cluster.MasterAuth).ApplyT(func(args []any) (string, error) {
|
|
endpoint := args[0].(string)
|
|
masterAuth := args[1].(container.ClusterMasterAuth)
|
|
context := fmt.Sprintf("%s_%s_%s", projectID, zone, clusterName)
|
|
|
|
// Extract CA certificate
|
|
caCert := ""
|
|
if masterAuth.ClusterCaCertificate != nil {
|
|
caCert = *masterAuth.ClusterCaCertificate
|
|
}
|
|
|
|
// Create kubeconfig using gke-gcloud-auth-plugin
|
|
kubeconfigYAML := fmt.Sprintf(`apiVersion: v1
|
|
clusters:
|
|
- cluster:
|
|
certificate-authority-data: %s
|
|
server: https://%s
|
|
name: %s
|
|
contexts:
|
|
- context:
|
|
cluster: %s
|
|
user: %s
|
|
name: %s
|
|
current-context: %s
|
|
kind: Config
|
|
preferences: {}
|
|
users:
|
|
- name: %s
|
|
user:
|
|
exec:
|
|
apiVersion: client.authentication.k8s.io/v1beta1
|
|
command: gke-gcloud-auth-plugin
|
|
installHint: Install gke-gcloud-auth-plugin for use with kubectl by following
|
|
https://cloud.google.com/blog/products/containers-kubernetes/kubectl-auth-changes-in-gke
|
|
provideClusterInfo: true
|
|
`, caCert, endpoint, context, context, context, context, context, context)
|
|
|
|
return kubeconfigYAML, nil
|
|
}).(pulumi.StringOutput),
|
|
}, pulumi.DependsOn([]pulumi.Resource{cluster, nodePool}))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create Kubernetes provider: %w", err)
|
|
}
|
|
|
|
return &providers.ProviderInfo{
|
|
Name: nodePool.Cluster,
|
|
Provider: k8sProvider,
|
|
}, nil
|
|
}
|