Skip to content

Commit c9cb49b

Browse files
authored
Add TypeScript OIDC setup for Pulumi Cloud/Google Cloud (#2166)
2 parents 5441718 + ede8247 commit c9cb49b

File tree

6 files changed

+161
-0
lines changed

6 files changed

+161
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/bin/
2+
/node_modules/
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
name: gcp-ts-oidc-provider-pulumi-cloud
2+
description: Enables Pulumi Cloud to authenticate with an OIDC provider in Google Cloud
3+
runtime:
4+
name: nodejs
5+
options:
6+
packagemanager: npm
7+
config:
8+
pulumi:tags:
9+
value:
10+
pulumi:template: typescript
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# gcp-ts-oidc-provider-pulumi-cloud
2+
3+
This Pulumi program enables [Pulumi Cloud](https://app.pulumi.com) to authenticate with an OIDC provider in a Google Cloud project, and creates a Pulumi ESC environment that allows both the [`gcloud` CLI](https://cloud.google.com/sdk/gcloud) and the [Pulumi Google Cloud provider](https://www.pulumi.com/registry/packages/gcp/) to consume temporary (admin) credentials.
4+
5+
This project is generally useful as a baseline setup for using ESC with Google Cloud. You may want to refine the scope of the accounts permissions (e.g. from `roles/admin` to `roles/writer` or `roles/reader`), or you may want to [import](https://www.pulumi.com/docs/esc/get-started/import-environments/) the generated ESC environment into a new ESC environment to enable scenarios like [accessing Google Secret Manager secrets](https://www.pulumi.com/docs/esc/integrations/dynamic-secrets/gcp-secrets/).
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
// Copyright 2025, Pulumi Corporation. All rights reserved.
2+
3+
import * as gcp from "@pulumi/gcp";
4+
import * as pulumi from "@pulumi/pulumi";
5+
import * as pcloud from "@pulumi/pulumiservice";
6+
import * as random from "@pulumi/random";
7+
8+
const config = new pulumi.Config();
9+
const gcpConfig = new pulumi.Config("gcp");
10+
11+
const gcpProjectName = gcpConfig.require("project");
12+
13+
// In most cases, it's safe to assume that this stack is run in the same Pulumi
14+
// org in which the OIDC environment is being configured. If not, set the
15+
// escEnvOrg config to the name of the org where the environment is going to be
16+
// configured.
17+
const escEnvOrg = config.get("escEnvOrg") || pulumi.getOrganization();
18+
const escEnvProject = config.get("escEnvProject") || `gcloud`;
19+
const escEnvName = config.get("escEnvName") || `${gcpProjectName}-admin`;
20+
21+
// We use a shorter name for the Workload Identity Pool and Service Account IDs
22+
// because they have character limits of 30 and 32 respectively, and the Google
23+
// Cloud project name is redundant in this context anyway, since we already know
24+
// what Google Cloud project we are in:
25+
const workloadIdentityPoolId = `${escEnvOrg}-admin`;
26+
const serviceAccountId = workloadIdentityPoolId.replace("-", "");
27+
28+
const randomSuffix = new random.RandomString(`random-suffix`, {
29+
length: 5,
30+
lower: true,
31+
upper: false,
32+
special: false,
33+
});
34+
35+
// The Workload Identity Pool id uses a random suffix so that this stack can be
36+
// brought up and down repeatably: Workload Identity Pools only soft deletes and
37+
// will auto-purge after 30 days. It is not possible to force a hard delete:
38+
const identityPool = new gcp.iam.WorkloadIdentityPool(`identity-pool`, {
39+
workloadIdentityPoolId: pulumi.interpolate`${workloadIdentityPoolId}-${randomSuffix.result}`,
40+
});
41+
42+
const oidcProvider = new gcp.iam.WorkloadIdentityPoolProvider(`identity-pool-provider`, {
43+
workloadIdentityPoolId: identityPool.workloadIdentityPoolId,
44+
workloadIdentityPoolProviderId: `pulumi-cloud-${pulumi.getOrganization()}-oidc`,
45+
oidc: {
46+
issuerUri: "https://api.pulumi.com/oidc",
47+
allowedAudiences: [`gcp:${pulumi.getOrganization()}`],
48+
},
49+
attributeMapping: {
50+
"google.subject": "assertion.sub",
51+
},
52+
});
53+
54+
const serviceAccount = new gcp.serviceaccount.Account("service-account", {
55+
accountId: serviceAccountId,
56+
project: gcpProjectName,
57+
});
58+
59+
// tslint:disable-next-line:no-unused-expression
60+
new gcp.projects.IAMMember("service-account", {
61+
member: pulumi.interpolate`serviceAccount:${serviceAccount.email}`,
62+
role: "roles/admin",
63+
project: gcpProjectName,
64+
});
65+
66+
// tslint:disable-next-line:no-unused-expression
67+
new gcp.serviceaccount.IAMBinding("service-account", {
68+
serviceAccountId: serviceAccount.id,
69+
role: "roles/iam.workloadIdentityUser",
70+
members: [pulumi.interpolate`principalSet://iam.googleapis.com/${identityPool.name}/*`],
71+
});
72+
73+
// fn::open::gcp-login requires project number instead of project name:
74+
const projectNumber = gcp.projects.getProjectOutput({
75+
filter: `name:${gcpProjectName}`,
76+
}).projects[0].number
77+
.apply(projectNumber => +projectNumber); // this casts it from string to a number
78+
79+
const envYaml = pulumi.interpolate`
80+
values:
81+
gcp:
82+
login:
83+
fn::open::gcp-login:
84+
project: ${projectNumber}
85+
oidc:
86+
workloadPoolId: ${oidcProvider.workloadIdentityPoolId}
87+
providerId: ${oidcProvider.workloadIdentityPoolProviderId}
88+
serviceAccount: ${serviceAccount.email}
89+
subjectAttributes:
90+
- currentEnvironment.name
91+
pulumiConfig:
92+
gpc:project: \${gcp.login.project}
93+
environmentVariables:
94+
# The Google Cloud SDK (which is used by the Pulumi provider) requires the project to be set by number:
95+
GOOGLE_CLOUD_PROJECT: \${gcp.login.project}
96+
# The gcloud CLI requires the project be set by name, and via a different env var.
97+
# See: https://cloud.google.com/sdk/docs/properties#setting_properties_using_environment_variables
98+
CLOUDSDK_CORE_PROJECT: ${gcpProjectName}
99+
GOOGLE_OAUTH_ACCESS_TOKEN: \${gcp.login.accessToken}
100+
CLOUDSDK_AUTH_ACCESS_TOKEN: \${gcp.login.accessToken}
101+
USE_GKE_GCLOUD_AUTH_PLUGIN: True
102+
`;
103+
104+
const environment = new pcloud.Environment("environment", {
105+
organization: escEnvOrg,
106+
project: escEnvProject,
107+
name: escEnvName,
108+
yaml: envYaml,
109+
});
110+
111+
112+
export const escEnvId = environment.id;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "gcp-ts-oidc-provider-pulumi-cloud",
3+
"main": "index.ts",
4+
"devDependencies": {
5+
"@types/node": "^18",
6+
"typescript": "^5.0.0"
7+
},
8+
"dependencies": {
9+
"@pulumi/gcp": "^8.25.1",
10+
"@pulumi/pulumi": "^3.113.0",
11+
"@pulumi/pulumiservice": "^0.29.1",
12+
"@pulumi/random": "^4.18.0"
13+
}
14+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true,
4+
"outDir": "bin",
5+
"target": "es2020",
6+
"module": "commonjs",
7+
"moduleResolution": "node",
8+
"sourceMap": true,
9+
"experimentalDecorators": true,
10+
"pretty": true,
11+
"noFallthroughCasesInSwitch": true,
12+
"noImplicitReturns": true,
13+
"forceConsistentCasingInFileNames": true
14+
},
15+
"files": [
16+
"index.ts"
17+
]
18+
}

0 commit comments

Comments
 (0)