---
title: "Cloud Workstations: building reusable development environments in cloud"
description: "A minimal Google Cloud Workstations setup with Pulumi, TypeScript and Bun: VPC, cluster, config and a first workstation, from project bootstrap to pulumi up."
canonical_url: "https://serhiichuk.dev/blog/cloud-workstations-building-reusable-development-environments-in-cloud/"
published: "2025-10-26T00:00:00.000Z"
tags:
  - "Google Cloud"
  - "Cloud Workstations"
  - "Pulumi"
  - "Infrastructure as Code"
  - "DevOps"
  - "TypeScript"
---

There are multiple ways to solve the dev environment question, whereas having a local development environment may be superior to any other setup, but a cloud-based environment that is close to your workloads has its set of benefits as well.

## Local vs Cloud environment

Performance and smoothness of your local environment is something no other setup can beat (assuming you have good-enough hardware) but even with top-notch hardware you will face a lot of tedious things to set up, configure and follow to make your environment ready. To name a few — configure and whitelist your IP to talk to the databases or maybe configure VPN. Install a particular version of Python or Node JS your team is working with and don't forget about all the extensions in your favourite IDE to make it a smooth ride. And don't forget that you may need some specific piece of software that only runs under a particular operating system or maybe can't run outside of a secure perimeter.

On the other hand, a cloud-based and repeatable setup may give you peace of mind with the majority of these. You can have the VMs whitelisted for proper access from the get-go without having to ask anyone for extra assistance. You may install all your favourite or just required toolsets and you can also pre-package extensions of your choice and build them into the setup.

## Google Cloud Workstations customizable setup

[Google Cloud Workstations](https://cloud.google.com/workstations) provide a way to build exactly that setup for your engineers that gives you the ability to create different configurations with the desirable runtime container with all the things you need. Workstations are managed by Cloud Workstations control plane, but the virtual machines themselves reside in your VPC so you can control what and how they can access in your setup. You can e.g. disable public IP for a VM and configure Cloud NAT to ensure smooth access to restricted resources.

## Minimal Google Cloud Workstations setup with Pulumi

In order to give it a taste and spin it up ASAP, we'll go with a minimal setup while still utilizing infrastructure best practices by using [Pulumi](https://www.pulumi.com/) for our infrastructure as code and TypeScript to have a coherent dev-friendly setup. We will also use [Bun](https://bun.com/) for its ease and speed (you can use any other environment of choice).

Check out the complete setup [here](https://github.com/xSAVIKx/gcp-cloud-workstations-howto/tree/main/minimal).

## Bootstrapping a project

In order to bootstrap a new Pulumi project we'll need [Pulumi CLI](https://www.pulumi.com/docs/get-started/download-install/) and [Bun CLI](https://bun.com/docs/installation) and [gcloud CLI](https://docs.cloud.google.com/sdk/docs/downloads-interactive#linux-mac):

```bash
curl -fsSL https://get.pulumi.com | bash
curl -fsSL https://bun.com/install | bash
curl -fsSL https://sdk.cloud.google.com | bash
```

You will also need to [create a Pulumi account](https://app.pulumi.com/signup) and login with:

```bash
pulumi login
```

If you haven't done this before, you'll need to log in with application-default credentials in gcloud CLI:

```bash
gcloud auth application-default login
```

And now either create a new GCP project or set the one you're willing to use:

```bash
gcloud projects create <my-project-id> --name="My project Name"
# OR
gcloud config set project <my-project-id>
```

Now here's how you can bootstrap a project using Pulumi CLI:

```bash
mkdir gcp-cloud-workstations && cd gcp-cloud-workstations
pulumi new typescript \
  --language=typescript \
  --runtime-options=packagemanager=bun \
  --name=gcp-cloud-workstations \
  --description='Minimal setup of the GCP Cloud Workstations with Typescript and Bun' \
  --stack=main
```

You can also just use plain `pulumi new typescript` and follow the project creation wizard. So in the abovementioned command we've asked Pulumi to create a TypeScript-based project with Bun as a package manager with a defined name, description and [stack name](https://www.pulumi.com/docs/iac/concepts/stacks/).

It will take care of creating `index.ts`, `tsconfig` and `package.json` as well as `Pulumi.yaml`.

Let's now add the `@pulumi/gcp` provider:

```bash
bun add -E '@pulumi/gcp'
```

And set the required project ID config for the provider:

```bash
pulumi config set 'gcp:project' <my-project-id>
```

We're all set to start writing up our infrastructure as code now.

## Adding IaC to the project

Let's start by defining a dedicated GCP provider instance in our code.

```typescript
import * as gcp from "@pulumi/gcp";

const region = "us-central1";
const gcpProvider = new gcp.Provider("gcpProvider", {
  project: gcp.config.project,
  region: region,
  defaultLabels: { team: "devops" },
});
```

It's a recommended best practice that will ensure that all our resources are created with particular defaults and also has a handy feature such as adding labels to all eligible resources.

### Enabling GCP services

If you're going to add Cloud Workstations into a new GCP project, one will need the required GCP services to be enabled. We can do that from code as well. `new gcp.projects.Service` creates a service resource that manages [enablement of services in GCP](https://cloud.google.com/service-usage/docs/enable-disable).

```typescript
const enableServices = (services: string[]) => {
  return services.map((service) => {
    return new gcp.projects.Service(
      service,
      {
        service: service,
        project: gcp.config.project,
      },
      { provider: gcpProvider },
    );
  });
};

const requiredServices = [
  "compute.googleapis.com",
  "workstations.googleapis.com",
];

const services = enableServices(requiredServices);
```

You can also enable services using the `gcloud` CLI if you don't want to have this managed with IaC:

```bash
gcloud services enable compute.googleapis.com workstations.googleapis.com
```

### Setting up Workstations cluster and configuration

Now when the services are enabled, we can configure the workstations and related resources.

```typescript
export default async function main() {
  const wsNetwork = new gcp.compute.Network(
    "wsNetwork",
    {
      autoCreateSubnetworks: false,
    },
    {
      provider: gcpProvider,
      dependsOn: services,
    },
  );
  const wsSubnetwork = new gcp.compute.Subnetwork(
    "wsUsCentral1Subnet",
    {
      network: wsNetwork.id,
      region: region,
      ipCidrRange: "10.128.0.0/20",
    },
    {
      provider: gcpProvider,
      parent: wsNetwork,
    },
  );
  const wsCluster: gcp.workstations.WorkstationCluster =
    new gcp.workstations.WorkstationCluster(
      "developmentCluster",
      {
        workstationClusterId: "test-cluster",
        network: wsNetwork.id,
        subnetwork: wsSubnetwork.id,
        location: region,
        displayName: "Test Cluster",
        annotations: {
          description: "Minimal cluster for testing",
        },
        labels: {
          purpose: "test",
        },
      },
      { provider: gcpProvider, dependsOn: services },
    );
  const wsMinimalConfig = new gcp.workstations.WorkstationConfig(
    "wsMinimalConfig",
    {
      workstationConfigId: "minimal-config",
      workstationClusterId: wsCluster.workstationClusterId,
      location: region,
    },
    { provider: gcpProvider, dependsOn: services },
  );
  return {
    wsCluster: wsCluster.name,
    wsConfig: wsMinimalConfig.name,
  };
}
```

The `export default function main` gives us flexibility in using `await` inside our infrastructure code and also works as an entry point for Pulumi. The returned object will be converted to [stack outputs](https://www.pulumi.com/tutorials/building-with-pulumi/stack-outputs/).

First, we define a [GCP VPC network](https://docs.cloud.google.com/vpc/docs/vpc) and subnetwork — this will keep our cluster isolated from the start and that's where the virtual machines are going to be created.

Then we define a `WorkstationCluster` itself — this creates a new control plane of Cloud Workstations that manages instances.

The `WorkstationConfig` is a template for creating new virtual machines. The very minimal one as we have here just specifies the cluster to work with and the location. It will use Code OSS IDE by default, provision `e2-standard-4` virtual machines and use the project-default Compute Engine service account to run the VMs.

Now all that's left is to call `pulumi up` and see how your first workstations cluster is going to be created.

### Adding workstations

You can now add workstations with the same IaC setup using the following snippet:

```typescript
  const workstation = new gcp.workstations.Workstation(
    "test-workstation",
    {
      workstationId: "test-workstation",
      workstationConfigId: wsMinimalConfig.workstationConfigId,
      workstationClusterId: wsCluster.workstationClusterId,
      location: region,
    },
    { provider: gcpProvider, dependsOn: services },
  );
```

Or you can as well let your engineers create workstations using [Google Cloud UI](https://console.cloud.google.com/workstations/create).

![Cloud Workstations — Create workstation widget](./create-workstation-widget.png)

Or using `gcloud`:

```bash
gcloud workstations create test-workstation \
  --cluster=test-cluster \
  --config=minimal-config \
  --region=us-central1
```

---

Here is what the complete setup script looks like ([also available as a gist](https://gist.github.com/xSAVIKx/78eaeb461417f265fda71771cf8bb872)):

```typescript
import * as gcp from "@pulumi/gcp";

const region = "us-central1";
const gcpProvider = new gcp.Provider("gcpProvider", {
  project: gcp.config.project,
  region: region,
  defaultLabels: { team: "devops" },
});

const enableServices = (services: string[]) => {
  return services.map((service) => {
    return new gcp.projects.Service(
      service,
      {
        service: service,
        project: gcp.config.project,
      },
      { provider: gcpProvider },
    );
  });
};

const requiredServices = [
  "compute.googleapis.com",
  "workstations.googleapis.com",
];

const services = enableServices(requiredServices);

export default async function main() {
  const wsNetwork = new gcp.compute.Network(
    "wsNetwork",
    {
      autoCreateSubnetworks: false,
    },
    {
      provider: gcpProvider,
      dependsOn: services,
    },
  );
  const wsSubnetwork = new gcp.compute.Subnetwork(
    "wsUsCentral1Subnet",
    {
      network: wsNetwork.id,
      region: region,
      ipCidrRange: "10.128.0.0/20",
    },
    {
      provider: gcpProvider,
      parent: wsNetwork,
    },
  );
  const wsCluster: gcp.workstations.WorkstationCluster =
    new gcp.workstations.WorkstationCluster(
      "developmentCluster",
      {
        workstationClusterId: "test-cluster",
        network: wsNetwork.id,
        subnetwork: wsSubnetwork.id,
        location: region,
        displayName: "Test Cluster",
        annotations: {
          description: "Minimal cluster for testing",
        },
        labels: {
          purpose: "test",
        },
      },
      { provider: gcpProvider, dependsOn: services },
    );
  const wsMinimalConfig = new gcp.workstations.WorkstationConfig(
    "wsMinimalConfig",
    {
      workstationConfigId: "minimal-config",
      workstationClusterId: wsCluster.workstationClusterId,
      location: region,
    },
    { provider: gcpProvider, dependsOn: services },
  );
  const workstation = new gcp.workstations.Workstation(
    "test-workstation",
    {
      workstationId: "test-workstation",
      workstationConfigId: wsMinimalConfig.workstationConfigId,
      workstationClusterId: wsCluster.workstationClusterId,
      location: region,
    },
    { provider: gcpProvider, dependsOn: services },
  );
  return {
    wsCluster: wsCluster.name,
    wsConfig: wsMinimalConfig.name,
    workstation: workstation.host,
  };
}
```

---

### Starting workstations

A workstation is created in a stopped state, so you are only paying for the attached disk and not the CPU/RAM yet. Whenever you're ready you can start it from the UI or with the `gcloud workstations start` command.

:::full
![Cloud Workstations — Workstations overview](./workstations-overview.png)
:::

Upon launching a workstation you receive a full-featured development environment powered by [Code OSS](https://github.com/Microsoft/vscode).

:::full
![Cloud Workstations — Code OSS environment](./code-oss-environment.png)
:::

While being useful by itself, the biggest benefit of having cloud development environments, in my opinion, comes from making them your own, with a pre-configured set of tooling, extensions and access rights that suit you and your team the best.

We will cover the customization part of the Cloud Workstations setup in the [next article](/blog/cloud-workstations-building-reusable-development-environments-in-cloud-part-2/).

---

### Cleanup

If you want to delete the resources Pulumi created, just run `pulumi down` and it will take care of the cleanup.

## Useful resources

- [GCP Cloud Workstations How To repository](https://github.com/xSAVIKx/gcp-cloud-workstations-howto/tree/main/minimal)
- [Cloud Workstations home page](https://cloud.google.com/workstations)
- [Pulumi GCP provider Workstations page](https://www.pulumi.com/registry/packages/gcp/api-docs/workstations/workstation/)
- [Bun docs](https://bun.com/docs)
