Skip to content

Commit 1e31523

Browse files
cf-jaspedrosousa
andauthored
[Pulumi] Added example with Wrangler and a dynamic provider (#18079)
--------- Co-authored-by: Pedro Sousa <[email protected]>
1 parent 2bb67af commit 1e31523

File tree

1 file changed

+339
-0
lines changed

1 file changed

+339
-0
lines changed
Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
---
2+
title: Create different resources using Pulumi and Wrangler
3+
pcx_content_type: tutorial
4+
updated: 2024-11-08
5+
sidebar:
6+
order: 5
7+
---
8+
9+
import { TabItem, Tabs } from "~/components";
10+
11+
This example creates a zone and other resources using two different strategies:
12+
13+
- Use Pulumi for some resources supported by the Cloudflare Pulumi provider.
14+
- Use Wrangler to create other types of resources.
15+
16+
The example code covers the creation of resources such as Workers, Zero Trust Applications, Zero Trust Policies, and D1 databases.
17+
18+
## Wrangler usage
19+
20+
The code in this example shows how you can create Workers by calling Wrangler directly instead of using the resource directly supported by the Cloudflare Pulumi provider. The advantage of using this method is that you can use it for any deployment-related task such as executing D1 migrations. When you run the infrastructure as code (IaC) script, Wrangler creates or updates Workers and D1, which includes performing database migrations.
21+
22+
In the current example, the D1 migrations state in Pulumi only changes when the hash of the `migrations` directory changes, in which case the Pulumi command will be executed.
23+
24+
## Dynamic resource provider for Vectorize
25+
26+
The example also provides an example of a dynamic resource provider for a resource that is not directly supported by the Cloudflare Pulumi provider (in this case, Vectorize).
27+
28+
## Example code
29+
30+
```js
31+
"use strict";
32+
33+
const pulumi = require("@pulumi/pulumi");
34+
const cloudflare = require("@pulumi/cloudflare");
35+
const command = require("@pulumi/command");
36+
const path = require("path");
37+
const axios = require("axios");
38+
const https = require("https");
39+
const crypto = require("crypto");
40+
const fs = require("fs");
41+
42+
// Load configuration
43+
const config = new pulumi.Config();
44+
const domainName = config.require("domainName");
45+
const accountId = config.require("accountId");
46+
const apiToken = config.requireSecret("apiToken");
47+
48+
// Function to compute hash of a file
49+
function computeFileHashSync(filePath) {
50+
const fileBuffer = fs.readFileSync(filePath);
51+
const hash = crypto.createHash("sha256");
52+
hash.update(fileBuffer);
53+
return hash.digest("hex");
54+
}
55+
56+
// Function to compute the hash of a directory
57+
async function hashDirectory(dirPath) {
58+
const files = await fs.promises.readdir(dirPath);
59+
const fileHashes = [];
60+
for (const file of files) {
61+
const filePath = path.join(dirPath, file);
62+
const fileStat = await fs.promises.stat(filePath);
63+
if (fileStat.isFile()) {
64+
const fileData = await fs.promises.readFile(filePath);
65+
const hash = crypto.createHash("sha256").update(fileData).digest("hex");
66+
fileHashes.push(hash);
67+
}
68+
}
69+
// Combine all file hashes and hash the result to get a unique hash for the directory
70+
const combinedHash = crypto
71+
.createHash("sha256")
72+
.update(fileHashes.join(""))
73+
.digest("hex");
74+
return combinedHash;
75+
}
76+
77+
// Instantiate Cloudflare provider
78+
// https://www.pulumi.com/registry/packages/cloudflare/
79+
//-----------------------------------------------------------------------------
80+
const cloudflareProvider = new cloudflare.Provider("cloudflare", {
81+
apiToken: apiToken,
82+
});
83+
84+
// Create a Cloudflare Zone
85+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zone/
86+
//-----------------------------------------------------------------------------
87+
const myZone = new cloudflare.Zone(
88+
"myZone",
89+
{
90+
zone: domainName,
91+
plan: "enterprise",
92+
accountId: accountId,
93+
},
94+
{ provider: cloudflareProvider },
95+
);
96+
97+
// Create a Cloudflare Queue (used as a binding in Worker)
98+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/queue/
99+
//-----------------------------------------------------------------------------
100+
const myqueue = new cloudflare.Queue(
101+
"myqueue",
102+
{
103+
zoneId: myZone.id,
104+
name: "myqueue",
105+
description: "Queue for my messages",
106+
accountId: accountId,
107+
},
108+
{ provider: cloudflareProvider },
109+
);
110+
111+
// Create a Cloudflare Queue (used as a binding in Worker)
112+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/queue/
113+
//-----------------------------------------------------------------------------
114+
const myqueuedeadletter = new cloudflare.Queue(
115+
"myqueuedeadletter",
116+
{
117+
zoneId: myZone.id,
118+
name: "myqueuedeadletter",
119+
description: "Queue for messages that were not processed correctly",
120+
accountId: accountId,
121+
},
122+
{ provider: cloudflareProvider },
123+
);
124+
125+
// Create a D1 Database
126+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/d1database/
127+
//-----------------------------------------------------------------------------
128+
const myD1Database = new cloudflare.D1Database(
129+
"myD1Database",
130+
{
131+
accountId: accountId,
132+
name: "mydb",
133+
},
134+
{ provider: cloudflareProvider },
135+
);
136+
137+
// Deploy Changes to D1 Schema
138+
// - Cloudflare Wrangler stores a list of migrations in the D1 database.
139+
// - To check which migrations were run, go to the Cloudflare dashboard
140+
// and run "SELECT * FROM d1_migrations" on the console of the D1 database.
141+
//-----------------------------------------------------------------------------
142+
const d1Dir = "../../mydb/";
143+
const d1Migration = new command.local.Command(
144+
"d1Migration",
145+
{
146+
dir: d1Dir,
147+
create: `npx wrangler d1 migrations apply mydb --remote`,
148+
triggers: [hashDirectory(`${d1Dir}migrations`)],
149+
},
150+
{ dependsOn: [myD1Database] },
151+
);
152+
153+
// Run 'wrangler' command
154+
// https://www.pulumi.com/registry/packages/command/api-docs/local/command/
155+
//-----------------------------------------------------------------------------
156+
const workerDir = "../../worker-test/";
157+
const workerTest = new command.local.Command(
158+
"worker-test",
159+
{
160+
dir: workerDir,
161+
create: "npx wrangler deploy",
162+
triggers: [
163+
// A unique trigger vector to force recreation
164+
computeFileHashSync(`${workerDir}src/index.js`),
165+
computeFileHashSync(`${workerDir}wrangler.toml`),
166+
],
167+
},
168+
{ dependsOn: [myZone, myqueue, myqueuedeadletter, myD1Database] },
169+
);
170+
171+
// Create "Add" group Service Auth Token
172+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zerotrustaccessservicetoken/
173+
//-----------------------------------------------------------------------------
174+
const myServiceToken = new cloudflare.ZeroTrustAccessServiceToken(
175+
"myServiceToken",
176+
{
177+
zoneId: myZone.id,
178+
name: "myServiceToken",
179+
},
180+
{ provider: cloudflareProvider },
181+
);
182+
183+
// Create an Access "Add" Group
184+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zerotrustaccessgroup/
185+
//-----------------------------------------------------------------------------
186+
const myAccessGroup = new cloudflare.ZeroTrustAccessGroup(
187+
"myAccessGroup",
188+
{
189+
accountId: accountId,
190+
name: "myAccessGroup",
191+
// Define the group criteria (e.g., email domains, identity providers, etc.)
192+
// This example adds users from the specified email domain.
193+
includes: [{ serviceTokens: [myServiceToken.id] }],
194+
},
195+
{ provider: cloudflareProvider, dependsOn: [myServiceToken] },
196+
);
197+
198+
// Create an Access App for "Add"
199+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zerotrustaccessapplication/
200+
//-----------------------------------------------------------------------------
201+
const myAccessApp = new cloudflare.ZeroTrustAccessApplication(
202+
"myAccessApp",
203+
{
204+
zoneId: myZone.id,
205+
name: "myApp",
206+
domain: `myapp.${domainName}`,
207+
sessionDuration: "24h",
208+
},
209+
{ provider: cloudflareProvider, dependsOn: [myAccessGroup, myZone] },
210+
);
211+
212+
// Create an Access App with Allow Policy for Access "Add" Group
213+
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zerotrustaccesspolicy/
214+
//-----------------------------------------------------------------------------
215+
const myAddAccessPolicy = new cloudflare.ZeroTrustAccessPolicy(
216+
"myAccessPolicy",
217+
{
218+
zoneId: myZone.id,
219+
applicationId: myAccessApp.id,
220+
name: "myAccessPolicy",
221+
decision: "allow",
222+
precedence: 1,
223+
includes: [
224+
{
225+
groups: [myAccessGroup.id],
226+
},
227+
],
228+
},
229+
{ provider: cloudflareProvider, dependsOn: [myAccessApp] },
230+
);
231+
232+
// Create a Vectorize Index
233+
//-----------------------------------------------------------------------------
234+
// Define a dynamic provider for Vectorize, since the Cloudflare Pulumi provider does not support
235+
// this resource yet
236+
const VectorizeIndexDynamicCloudflareProvider = {
237+
async create(inputs) {
238+
// Create an instance of the HTTPS Agent with SSL verification disabled to avoid WARP issues
239+
const httpsAgent = new https.Agent({
240+
rejectUnauthorized: false,
241+
});
242+
const url = `https://api.cloudflare.com/client/v4/accounts/${inputs.accountId}/vectorize/v2/indexes`;
243+
const data = {
244+
config: { dimensions: 768, metric: "cosine" },
245+
description: inputs.description,
246+
name: inputs.name,
247+
};
248+
// Headers
249+
const options = {
250+
httpsAgent,
251+
headers: {
252+
"Content-Type": "application/json",
253+
Authorization: `Bearer ${inputs.apiToken}`,
254+
},
255+
};
256+
// Make an API call to create the resource
257+
const response = await axios.post(url, data, options);
258+
// For now we use the Vectorize index name as id, because Vectorize does not
259+
// provide an id for it
260+
const resourceId = inputs.name;
261+
262+
// Return the ID and output values
263+
return {
264+
id: resourceId,
265+
outs: {
266+
name: inputs.name,
267+
accountId: inputs.accountId,
268+
apiToken: inputs.apiToken,
269+
},
270+
};
271+
},
272+
273+
async delete(id, props) {
274+
// Create an instance of the HTTPS Agent with SSL verification disabled to avoid WARP issues
275+
const httpsAgent = new https.Agent({
276+
rejectUnauthorized: false,
277+
});
278+
const url = `https://api.cloudflare.com/client/v4/accounts/${props.accountId}/vectorize/v2/indexes/${id}`;
279+
// Headers
280+
const options = {
281+
httpsAgent,
282+
headers: {
283+
"Content-Type": "application/json",
284+
Authorization: `Bearer ${props.apiToken}`,
285+
},
286+
};
287+
// Make an API call to delete the resource
288+
await axios.delete(url, options);
289+
},
290+
291+
async update(id, oldInputs, newInputs) {
292+
// Vectorize once created does not allow updates
293+
},
294+
};
295+
296+
// Define a dynamic resource
297+
class VectorizeIndex extends pulumi.dynamic.Resource {
298+
constructor(name, args, opts) {
299+
super(VectorizeIndexDynamicCloudflareProvider, name, args, opts);
300+
}
301+
}
302+
303+
// Use the dynamic resource in your Pulumi stack
304+
// - Don't change properties after creation. Currently, Vectorize does not allow changes.
305+
// - To delete this resource, remove or comment this block of code
306+
const my_vectorize_index = new VectorizeIndex("myvectorizeindex", {
307+
name: "myvectorize_index",
308+
accountId: accountId,
309+
namespaceId: myZone.id, // Set appropriate namespace id
310+
vectorDimensions: 768, // This is an example - adjust dimensions as needed
311+
apiToken: apiToken,
312+
});
313+
314+
// Export relevant outputs
315+
// Access these outputs after Pulumi has run using:
316+
// $ pulumi stack output
317+
// $ pulumi stack output zoneId
318+
//-----------------------------------------------------------------------------
319+
exports.zoneId = myZone.id;
320+
exports.myqueueId = myqueue.id;
321+
exports.myqueuedeadletter = myqueuedeadletter.id;
322+
exports.myD1DatabaseId = myD1Database.id;
323+
exports.workerTestId = workerTest.id;
324+
exports.myServiceToken = myServiceToken.id;
325+
exports.myServiceTokenClientId = myAddServiceToken.clientId;
326+
exports.myServiceTokenClientSecret = myAddServiceToken.clientSecret;
327+
```
328+
329+
## Access Pulumi exports
330+
331+
Once you run your Pulumi script with `pulumi up`, your resources will be created or updated.
332+
333+
The example script above also exports outputs that you can access from other tools. This is useful for example when integrating the Pulumi script into a deployment pipeline.
334+
335+
You could use a command similar to the following:
336+
337+
```bash
338+
pulumi stack output myServiceTokenClientSecret --show-secrets
339+
```

0 commit comments

Comments
 (0)