本示例使用两种不同的策略创建 Zone 和其他资源:
- 使用 Pulumi 管理 Cloudflare Pulumi 提供商支持的部分资源。
- 使用 Wrangler 创建其他类型的资源。
示例代码涉及创建 Workers、Zero Trust 应用程序、Zero Trust 策略和 D1 数据库等资源。
本示例中的代码展示了如何通过直接调用 Wrangler(而非使用 Cloudflare Pulumi 提供商直接支持的资源)来创建 Workers。使用此方法的优点在于,你可以将其用于任何部署相关任务,例如执行 D1 迁移。当你运行基础设施即代码(IaC)脚本时,Wrangler 将创建或更新 Workers 和 D1,其中包括执行数据库迁移。
在当前示例中,Pulumi 中的 D1 迁移状态仅在 migrations 目录的哈希值发生变化时才会改变,在这种情况下,Pulumi 命令才会被执行。
本示例还提供了一个针对 Cloudflare Pulumi 提供商尚不直接支持的资源(此处为 Vectorize)的动态资源提供商示例。
"use strict";
const pulumi = require("@pulumi/pulumi");
const cloudflare = require("@pulumi/cloudflare");
const command = require("@pulumi/command");
const path = require("path");
const axios = require("axios");
const https = require("https");
const crypto = require("crypto");
const fs = require("fs");
// Load configuration
const config = new pulumi.Config();
const domainName = config.require("domainName");
const accountId = config.require("accountId");
const apiToken = config.requireSecret("apiToken");
// Function to compute hash of a file
function computeFileHashSync(filePath) {
const fileBuffer = fs.readFileSync(filePath);
const hash = crypto.createHash("sha256");
hash.update(fileBuffer);
return hash.digest("hex");
}
// Function to compute the hash of a directory
async function hashDirectory(dirPath) {
const files = await fs.promises.readdir(dirPath);
const fileHashes = [];
for (const file of files) {
const filePath = path.join(dirPath, file);
const fileStat = await fs.promises.stat(filePath);
if (fileStat.isFile()) {
const fileData = await fs.promises.readFile(filePath);
const hash = crypto.createHash("sha256").update(fileData).digest("hex");
fileHashes.push(hash);
}
}
// Combine all file hashes and hash the result to get a unique hash for the directory
const combinedHash = crypto
.createHash("sha256")
.update(fileHashes.join(""))
.digest("hex");
return combinedHash;
}
// Instantiate Cloudflare provider
// https://www.pulumi.com/registry/packages/cloudflare/
//-----------------------------------------------------------------------------
const cloudflareProvider = new cloudflare.Provider("cloudflare", {
apiToken: apiToken,
});
// Create a Cloudflare Zone
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zone/
//-----------------------------------------------------------------------------
const myZone = new cloudflare.Zone(
"myZone",
{
zone: domainName,
plan: "enterprise",
accountId: accountId,
},
{ provider: cloudflareProvider },
);
// Create a Cloudflare Queue (used as a binding in Worker)
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/queue/
//-----------------------------------------------------------------------------
const myqueue = new cloudflare.Queue(
"myqueue",
{
zoneId: myZone.id,
name: "myqueue",
description: "Queue for my messages",
accountId: accountId,
},
{ provider: cloudflareProvider },
);
// Create a Cloudflare Queue (used as a binding in Worker)
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/queue/
//-----------------------------------------------------------------------------
const myqueuedeadletter = new cloudflare.Queue(
"myqueuedeadletter",
{
zoneId: myZone.id,
name: "myqueuedeadletter",
description: "Queue for messages that were not processed correctly",
accountId: accountId,
},
{ provider: cloudflareProvider },
);
// Create a D1 Database
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/d1database/
//-----------------------------------------------------------------------------
const myD1Database = new cloudflare.D1Database(
"myD1Database",
{
accountId: accountId,
name: "mydb",
},
{ provider: cloudflareProvider },
);
// Deploy Changes to D1 Schema
// - Cloudflare Wrangler stores a list of migrations in the D1 database.
// - To check which migrations were run, go to the Cloudflare dashboard
// and run "SELECT * FROM d1_migrations" on the console of the D1 database.
//-----------------------------------------------------------------------------
const d1Dir = "../../mydb/";
const d1Migration = new command.local.Command(
"d1Migration",
{
dir: d1Dir,
create: `npx wrangler d1 migrations apply mydb --remote`,
triggers: [hashDirectory(`${d1Dir}migrations`)],
},
{ dependsOn: [myD1Database] },
);
// Run 'wrangler' command
// https://www.pulumi.com/registry/packages/command/api-docs/local/command/
//-----------------------------------------------------------------------------
const workerDir = "../../worker-test/";
const workerTest = new command.local.Command(
"worker-test",
{
dir: workerDir,
create: "npx wrangler deploy",
triggers: [
// A unique trigger vector to force recreation
computeFileHashSync(`${workerDir}src/index.js`),
computeFileHashSync(`${workerDir}wrangler.toml`),
],
},
{ dependsOn: [myZone, myqueue, myqueuedeadletter, myD1Database] },
);
// Create "Add" group Service Auth Token
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zerotrustaccessservicetoken/
//-----------------------------------------------------------------------------
const myServiceToken = new cloudflare.ZeroTrustAccessServiceToken(
"myServiceToken",
{
zoneId: myZone.id,
name: "myServiceToken",
},
{ provider: cloudflareProvider },
);
// Create an Access "Add" Group
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zerotrustaccessgroup/
//-----------------------------------------------------------------------------
const myAccessGroup = new cloudflare.ZeroTrustAccessGroup(
"myAccessGroup",
{
accountId: accountId,
name: "myAccessGroup",
// Define the group criteria (e.g., email domains, identity providers, etc.)
// This example adds users from the specified email domain.
includes: [{ serviceTokens: [myServiceToken.id] }],
},
{ provider: cloudflareProvider, dependsOn: [myServiceToken] },
);
// Create an Access App for "Add"
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zerotrustaccessapplication/
//-----------------------------------------------------------------------------
const myAccessApp = new cloudflare.ZeroTrustAccessApplication(
"myAccessApp",
{
zoneId: myZone.id,
name: "myApp",
domain: `myapp.${domainName}`,
sessionDuration: "24h",
},
{ provider: cloudflareProvider, dependsOn: [myAccessGroup, myZone] },
);
// Create an Access App with Allow Policy for Access "Add" Group
// https://www.pulumi.com/registry/packages/cloudflare/api-docs/zerotrustaccesspolicy/
//-----------------------------------------------------------------------------
const myAddAccessPolicy = new cloudflare.ZeroTrustAccessPolicy(
"myAccessPolicy",
{
zoneId: myZone.id,
applicationId: myAccessApp.id,
name: "myAccessPolicy",
decision: "allow",
precedence: 1,
includes: [
{
groups: [myAccessGroup.id],
},
],
},
{ provider: cloudflareProvider, dependsOn: [myAccessApp] },
);
// Create a Vectorize Index
//-----------------------------------------------------------------------------
// Define a dynamic provider for Vectorize, since the Cloudflare Pulumi provider does not support
// this resource yet
const VectorizeIndexDynamicCloudflareProvider = {
async create(inputs) {
// Create an instance of the HTTPS Agent with SSL verification disabled to avoid WARP issues
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
const url = `https://api.cloudflare.com/client/v4/accounts/${inputs.accountId}/vectorize/v2/indexes`;
const data = {
config: { dimensions: 768, metric: "cosine" },
description: inputs.description,
name: inputs.name,
};
// Headers
const options = {
httpsAgent,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${inputs.apiToken}`,
},
};
// Make an API call to create the resource
const response = await axios.post(url, data, options);
// For now we use the Vectorize index name as id, because Vectorize does not
// provide an id for it
const resourceId = inputs.name;
// Return the ID and output values
return {
id: resourceId,
outs: {
name: inputs.name,
accountId: inputs.accountId,
apiToken: inputs.apiToken,
},
};
},
async delete(id, props) {
// Create an instance of the HTTPS Agent with SSL verification disabled to avoid WARP issues
const httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
const url = `https://api.cloudflare.com/client/v4/accounts/${props.accountId}/vectorize/v2/indexes/${id}`;
// Headers
const options = {
httpsAgent,
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${props.apiToken}`,
},
};
// Make an API call to delete the resource
await axios.delete(url, options);
},
async update(id, oldInputs, newInputs) {
// Vectorize once created does not allow updates
},
};
// Define a dynamic resource
class VectorizeIndex extends pulumi.dynamic.Resource {
constructor(name, args, opts) {
super(VectorizeIndexDynamicCloudflareProvider, name, args, opts);
}
}
// Use the dynamic resource in your Pulumi stack
// - Don't change properties after creation. Currently, Vectorize does not allow changes.
// - To delete this resource, remove or comment this block of code
const my_vectorize_index = new VectorizeIndex("myvectorizeindex", {
name: "myvectorize_index",
accountId: accountId,
namespaceId: myZone.id, // Set appropriate namespace id
vectorDimensions: 768, // This is an example - adjust dimensions as needed
apiToken: apiToken,
});
// Export relevant outputs
// Access these outputs after Pulumi has run using:
// $ pulumi stack output
// $ pulumi stack output zoneId
//-----------------------------------------------------------------------------
exports.zoneId = myZone.id;
exports.myqueueId = myqueue.id;
exports.myqueuedeadletter = myqueuedeadletter.id;
exports.myD1DatabaseId = myD1Database.id;
exports.workerTestId = workerTest.id;
exports.myServiceToken = myServiceToken.id;
exports.myServiceTokenClientId = myAddServiceToken.clientId;
exports.myServiceTokenClientSecret = myAddServiceToken.clientSecret;使用 pulumi up 运行 Pulumi 脚本后,你的资源将被创建或更新。
上面的示例脚本还导出了可以从其他工具访问的输出内容。例如,将 Pulumi 脚本集成到部署流水线中时非常有用。
你可以使用类似以下的命令:
pulumi stack output myServiceTokenClientSecret --show-secrets