Provisioning GCP Compute Engine with Pulumi

Learn how to launch a Compute Engine VM and a Managed Instance Group with Pulumi and TypeScript

Provisioning GCP Compute Engine with Pulumi

This guide launches a Compute Engine VM with Pulumi, building on the VPC guide - a single instance with SSH key metadata, then the Instance Template + Managed Instance Group pattern for anything beyond one box.

A Single Instance

// index.ts
import * as pulumi from "@pulumi/pulumi";
import * as gcp from "@pulumi/gcp";

const config = new pulumi.Config();
const sshPublicKey = config.require("sshPublicKey");

const webInstance = new gcp.compute.Instance("web", {
    name: "web-instance",
    machineType: "e2-medium",
    zone: `${region}-a`,
    tags: ["https-server"],
    bootDisk: {
        initializeParams: {
            image: "debian-cloud/debian-12",
        },
    },
    networkInterfaces: [{
        network: network.id,
        subnetwork: publicSubnet.id,
        accessConfigs: [{}],
    }],
    metadata: {
        "ssh-keys": `debian:${sshPublicKey}`,
    },
});

tags: ["https-server"] is what makes the allow-https firewall rule from the VPC guide apply to this instance - GCP firewall tags and Compute Engine instance tags are the same tag namespace, matched by string equality. accessConfigs: [{}] (an empty object, not omitted) is what actually gives the instance an ephemeral public IP - a networkInterfaces entry with no accessConfigs at all gets no external IP, which is the setting you’d use for anything in the private subnet instead.

metadata["ssh-keys"] in the username:public-key format is how Compute Engine’s guest agent provisions SSH access - there’s no separate key-pair resource to create and reference the way AWS’s aws_key_pair or Azure’s VM linuxConfiguration.ssh block work; the key is just instance metadata.

Reading webInstance’s assigned public IP back out requires .apply() since accessConfigs is typed as possibly undefined (an instance might have none):

export const instancePublicIp = webInstance.networkInterfaces.apply(
    nics => nics[0].accessConfigs?.[0]?.natIp
);

Instance Template and Managed Instance Group

For anything beyond a single instance, define an InstanceTemplate once and let a InstanceGroupManager stamp out and self-heal instances from it - GCP’s equivalent of an AWS Launch Template + Auto Scaling Group:

const instanceTemplate = new gcp.compute.InstanceTemplate("web", {
    name: "web-template",
    machineType: "e2-medium",
    tags: ["https-server"],
    disks: [{
        sourceImage: "debian-cloud/debian-12",
        autoDelete: true,
        boot: true,
    }],
    networkInterfaces: [{
        network: network.id,
        subnetwork: publicSubnet.id,
        accessConfigs: [{}],
    }],
    metadata: {
        "ssh-keys": `debian:${sshPublicKey}`,
    },
});

const mig = new gcp.compute.InstanceGroupManager("web", {
    name: "web-mig",
    zone: `${region}-a`,
    baseInstanceName: "web",
    targetSize: 2,
    versions: [{
        instanceTemplate: instanceTemplate.id,
    }],
});

Note the field name difference between the two resources for the same concept: InstanceTemplate calls it disks (with sourceImage/boot/autoDelete per disk), while the standalone Instance above calls the same thing bootDisk (singular, with initializeParams.image) - both are real, current, correctly-typed arguments, just named differently because they’re separate resource schemas.

Best Practices

  1. Use an Instance Template + Managed Instance Group for anything customer-facing, the same reasoning as an ASG on AWS or a scale set on Azure - a standalone Instance has no self-healing if it or its zone fails.
  2. Tag instances deliberately and match firewall rules to those tags rather than widening sourceRanges - the tag is the actual access-control boundary here, not the CIDR.
  3. Use debian-cloud/debian-12 or another current LTS image family, not a pinned specific image name - image families always resolve to the latest patched image in that family at deploy time.

Conclusion

Compare this to the EC2 guide: the Instance Template + Managed Instance Group pattern here is functionally the same idea as a Launch Template + Auto Scaling Group, just with GCP’s own field names and its tag-based (rather than security-group-based) firewall model.

For more Pulumi topics, check out: