このチュートリアルでは、Cloudflare Workers と Pulumi のインフラストラクチャ as Code(IaC)を使い、Hello World アプリケーションをデプロイする手順を進め、リソース管理のライフサイクルを把握します。具体的には Worker、Route、DNS レコードを作成し、アプリケーションへアクセスできるようにしてから、すべてのリソースを後始末します。
次を用意してください。
- このチュートリアルのリソースを編集できる権限を持つ Cloudflare アカウントと API トークン。まだの場合は、先に Cloudflare アカウント ↗ に登録します。トークンには次が必要です。
Account-Workers Scripts-Edit権限Zone-Workers Route-Edit権限Zone-DNS-Edit権限
- Pulumi Cloud アカウント。常時無料の個人プラン ↗ に登録できます。
- マシンに Pulumi CLI がインストールされていること。
- Pulumi が対応するプログラミング言語 ↗ が設定されていること(TypeScript、JavaScript、Python、Go、.NET、Java、または YAML)。
- Cloudflare が管理するドメイン。既存ドメインを Cloudflare に取り込むには、サイトを追加するチュートリアル を完了します。
Pulumi プロジェクトは、作成したいインフラを記述する専用フォルダー内のファイル群です。Pulumi プロジェクトフォルダーは、必須の Pulumi.yaml ファイルで識別します。新しいプロジェクトの作成と設定には Pulumi CLI を使います。
このチュートリアルでは、空の新しいディレクトリを使います。
mkdir serverless-cloudflare
cd serverless-cloudflarePulumi Cloud ↗ は、インフラストラクチャ as Code を管理するための安全でスケーラブルなホスト型サービスです。Pulumi のバックエンド設定の保存に使います。
プロンプトで Enter を押すと、ブラウザー経由で Pulumi Cloud アカウントにログインします。Pulumi Cloud のアクセストークン ↗ を渡すこともできます。
pulumi loginPulumi プログラムは、対応言語 ↗ で書いた、インフラリソースを定義するコードです。
プログラムを作成するには、使う言語を選び、pulumi コマンドを実行します。
pulumi new javascript --name serverless-cloudflare --yes
# wait a few seconds while the project is initializedpulumi new typescript --name serverless-cloudflare --yes
# wait a few seconds while the project is initializedpulumi new python --name serverless-cloudflare --yes
# wait a few seconds while the project is initializedpulumi new go --name serverless-cloudflare --yes
# wait a few seconds while the project is initializedpulumi new java --name serverless-cloudflare --yes
# wait a few seconds while the project is initializedpulumi new csharp --name serverless-cloudflare --yes
# wait a few seconds while the project is initializedpulumi new yaml --name serverless-cloudflare --yesPulumi の スタック ↗ は、Pulumi プログラムのインスタンスです。スタックは独立して設定でき、異なる環境(開発、ステージング、本番)や機能ブランチを表せます。このチュートリアルでは dev スタックを使います。
dev スタックを作成するには、次を実行します。
pulumi up --yes
# wait a few seconds for the stack to be instantiated.この時点ではリソースを定義していないため、空のスタックになります。
この手順では、設定とシークレットを含む YAML ファイルである Pulumi ESC Environment ↗ にアプリケーション設定を保存します。Pulumi プログラムを含む複数の方法でアクセスできます。ESC Environment はすべて Pulumi Cloud アカウントに安全に保存され、Pulumi CLI で管理できます。このチュートリアルでは次の値を保存します。
# Give your new ESC Environment a name
E=hello-world/dev-env
# Initialize the new ESC Environment
pulumi config env init --env $E --yesCreating environment hello-world/dev-env for stack dev...# Replace abc123 with your Cloudflare account ID
pulumi env set $E --plaintext pulumiConfig.accountId abc123
# Replace API_TOKEN with your Cloudflare API token
pulumi env set $E --secret pulumiConfig.cloudflare:apiToken API_TOKEN
# Replace example.com with your domain
pulumi env set $E --plaintext pulumiConfig.domain example.comPulumi プログラムで Cloudflare リソースを定義するには、使う言語向けの Cloudflare パッケージをインストールする必要があります。
次のコマンドで Cloudflare パッケージをインストールします。
npm install @pulumi/cloudflareadded 1 package ...npm install @pulumi/cloudflareadded 1 package ...echo "pulumi_cloudflare>=5.38,<6.0.0" >> requirements.txt
source venv/bin/activate
pip install -r requirements.txt...Collecting pulumi-cloudflare...go get github.com/pulumi/pulumi-cloudflare/sdk/v3/go/cloudflarego: downloading github.com/pulumi/pulumi-cloudflare ...以下は Apache Maven の手順です。Gradle など他の Java プロジェクトマネージャーについては、公式の Maven repository ↗ を参照してください。
pom.xmlファイルを開きます。<dependencies>セクション内に Pulumi Cloudflare の依存関係を追加します。
<dependency>
<groupId>com.pulumi</groupId>
<artifactId>cloudflare</artifactId>
<version>5.38.0</version>
</dependency>- 次を実行します。
mvn clean install...[INFO] BUILD SUCCESS...dotnet add package Pulumi.Cloudflare...
info : Adding PackageReference for package 'Pulumi.Cloudflare' into project
...YAML ではダウンロードする依存関係はありません。次に進んでください。
Cloudflare パッケージをインストールしたら、Pulumi プログラムで 対応する任意の Cloudflare リソース ↗ を定義できます。次に Worker、Route、DNS レコードを定義します。
Workers Script リソース ↗ は、Cloudflare ネットワークへデプロイできる Cloudflare Worker を表します。
エントリポイントファイルの内容を、次に置き換えます。
ファイル名: index.js
"use strict";
const pulumi = require("@pulumi/pulumi");
const cloudflare = require("@pulumi/cloudflare");
const config = new pulumi.Config();
const accountId = config.require("accountId");
const domain = config.require("domain");
const content = `export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response("Hello World!", options);
},
};`;
const worker = new cloudflare.WorkersScript("hello-world-worker", {
accountId: accountId,
name: "hello-world-worker",
content: content,
module: true, // ES6 module
});ファイル名: index.ts
import * as pulumi from "@pulumi/pulumi";
import * as cloudflare from "@pulumi/cloudflare";
const config = new pulumi.Config();
const accountId = config.require("accountId");
const domain = config.require("domain");
const content = `export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response("Hello World!", options);
},
};`;
const worker = new cloudflare.WorkersScript("hello-world-worker", {
accountId: accountId,
name: "hello-world-worker",
content: content,
module: true, // ES6 module
});ファイル名: __main__.py
"""Pulumi program """
import pulumi
import pulumi_cloudflare as cloudflare
CONFIG = pulumi.Config()
ACCOUNT_ID = CONFIG.get("accountId")
DOMAIN = CONFIG.require("domain")
CONTENT = """
export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response("Hello World!", options);
},
};
"""
worker = cloudflare.WorkersScript("hello-world-worker",
account_id=ACCOUNT_ID,
name="hello-world-worker",
content=CONTENT,
module=True # ES6 module
)ファイル名: main.go
package main
import (
"github.com/pulumi/pulumi-cloudflare/sdk/v5/go/cloudflare"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
conf := config.New(ctx, "")
accountID := conf.Get("accountId")
domain := conf.Get("domain")
content := `
export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response("Hello World!", options);
},
};
`
worker, err := cloudflare.NewWorkersScript(ctx, "hello-world-worker", &cloudflare.WorkersScriptArgs{
AccountId: pulumi.String(accountID),
Name: pulumi.String("hello-world-worker"),
Content: pulumi.String(content),
Module: pulumi.Bool(true), // ES6 module
})
if err != nil {
return err
}
return nil
})
}ファイル名: src/main/java/myproject/App.java
package myproject;
import com.pulumi.Pulumi;
import com.pulumi.cloudflare.WorkersScript;
import com.pulumi.cloudflare.WorkersScriptArgs;
import com.pulumi.core.Output;
public class App {
public static void main(String[] args) {
Pulumi.run(ctx -> {
var content = """
export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response("Hello World!", options);
},
};
""";
var accountId = ctx.config().require("accountId");
var domain = ctx.config().require("domain");
var worker = new WorkersScript("hello-world-worker", WorkersScriptArgs.builder()
.accountId(accountId)
.name("hello-world-worker")
.content(content)
.module(true)
.build());
return;
});
}
}ファイル名: Program.cs
using Pulumi;
using Cloudflare = Pulumi.Cloudflare;
return await Deployment.RunAsync(() =>
{
var config = new Config();
var accountId = config.Require("accountId");
var domain = config.Require("domain");
var content = @"
export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response(""Hello World!"", options);
},
};
";
var worker = new Cloudflare.WorkersScript("hello-world-worker", new()
{
AccountId = accountId,
Name = "hello-world-worker",
Content = content,
Module = true
});
return;
});ファイル名: Pulumi.yaml
name: serverless-cloudflare
runtime: yaml
resources:
worker:
type: cloudflare:WorkersScript
properties:
accountId: "${accountId}"
name: "hello-world-worker"
content: |
export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response("Hello World!", options);
},
};
module: trueWorkers スクリプトにエンドポイントを持たせ、有効にするため、Pulumi プログラムに Workers Route リソース ↗ を追加します。Route を正しく設定するため、ドメインのゾーン ID も取得します。
エントリポイントファイルの、Worker スクリプトリソースのあとに、次のコードを追加します。
ファイル名: index.js
const zone = cloudflare.getZone({
accountId: accountId,
name: domain,
});
const zoneId = zone.then((z) => z.zoneId);
const route = new cloudflare.WorkersRoute("hello-world-route", {
zoneId: zoneId,
pattern: "hello-world." + domain,
scriptName: worker.name,
});ファイル名: index.ts
const zone = cloudflare.getZone({
accountId: accountId,
name: domain,
});
const zoneId = zone.then((z) => z.zoneId);
const route = new cloudflare.WorkersRoute("hello-world-route", {
zoneId: zoneId,
pattern: "hello-world." + domain,
scriptName: worker.name,
});ファイル名: __main__.py
zone = cloudflare.get_zone(account_id=ACCOUNT_ID, name=DOMAIN)
zone_id = zone.zone_id
route = cloudflare.WorkersRoute("hello-world-route",
zone_id=zone_id,
pattern="hello-world." + DOMAIN,
script_name=worker.name
)ファイル名: main.go
zone, err := cloudflare.LookupZone(ctx, &cloudflare.LookupZoneArgs{
AccountId: &accountID,
Name: &domain,
}, nil)
if err != nil {
return err
}
route, err := cloudflare.NewWorkersRoute(ctx, "hello-world-route", &cloudflare.WorkersRouteArgs{
ZoneId: pulumi.String(zone.Id),
Pattern: pulumi.String("hello-world." + domain),
ScriptName: worker.Name,
})
if err != nil {
return err
}ファイル名: src/main/java/myproject/App.java
final var zone = CloudflareFunctions.getZone(GetZoneArgs.builder()
.accountId(accountId)
.name(domain)
.build());
var route = new WorkersRoute("hello-world-route", WorkersRouteArgs.builder()
.zoneId(zone.applyValue(getZoneResult -> getZoneResult.id()))
.pattern("hello-world." + domain)
.scriptName(worker.name())
.build());ファイル名: Program.cs
var zone = Output.Create(Cloudflare.GetZone.InvokeAsync(new()
{
AccountId = accountId,
Name = domain,
}));
var route = new Cloudflare.WorkersRoute("hello-world-route", new()
{
ZoneId = zone.Apply(z => z.Id),
Pattern = "hello-world." + domain,
ScriptName = worker.Name,
});ファイル名: Pulumi.yaml
runtime キーの下に、次のコードを追加します。
# new top-level section
variables:
zone:
fn::invoke:
function: cloudflare:getZone
arguments:
accountId: ${accountId}
name: ${domain}worker リソースの下に、次のコードを追加します。
route:
type: cloudflare:WorkersRoute
properties:
zoneId: ${zone.id}
pattern: "hello-world.${domain}"
scriptName: ${worker.name}先ほど設定した Route を解決する DNS レコードリソース ↗ を追加します。次の手順では、アクセスしやすいように Route の URL も出力します。
エントリポイントファイルの、Route リソースのあとに、次のコードを追加します。
ファイル名: index.js
const record = new cloudflare.Record("hello-world-record", {
name: route.pattern,
type: "A",
content: "192.0.2.1",
zoneId: zoneId,
proxied: true,
});
exports.url = pulumi.interpolate`https://${record.hostname}`;ファイル名: index.ts
const record = new cloudflare.Record("hello-world-record", {
name: route.pattern,
type: "A",
content: "192.0.2.1",
zoneId: zoneId,
proxied: true,
});
export const url = pulumi.interpolate`https://${record.hostname}`;ファイル名: __main__.py
record = cloudflare.Record("hello-world-record",
name=route.pattern,
type="A",
content="192.0.2.1",
zone_id=zone_id,
proxied=True
)
url = pulumi.Output.concat("https://", record.hostname)
pulumi.export('url', url)ファイル名: main.go
record, err := cloudflare.NewRecord(ctx, "hello-world-record", &cloudflare.RecordArgs{
Name: route.Pattern,
Type: pulumi.String("A"),
Content: pulumi.String("192.0.2.1"),
ZoneId: pulumi.String(zone.Id),
Proxied: pulumi.Bool(true),
})
if err != nil {
return err
}
ctx.Export("url", pulumi.Sprintf("https://%s", record.Hostname))ファイル名: src/main/java/myproject/App.java
var record = new Record("hello-world-record", RecordArgs.builder()
.name(route.pattern())
.type("A")
.content("192.0.2.1")
.zoneId(zone.applyValue(getZoneResult -> getZoneResult.id()))
.proxied(true)
.build());
ctx.export("url", Output.format("https://%s", record.hostname()));ファイル名: Program.cs
値をエクスポートするため、return 文が更新されています。インポートに using System.Collections.Generic; も含めてください。
var record = new Cloudflare.Record("hello-world-record", new()
{
Name = route.Pattern,
Type = "A",
Content = "192.0.2.1",
ZoneId = zone.Apply(z => z.Id),
Proxied = true
});
return new Dictionary<string, object?>
{
["url"] = Output.Format($"https://{record.Hostname}")
};トップレベルの outputs セクションが追加されます。
record:
type: cloudflare:Record
properties:
name: ${route.pattern}
type: A
content: "192.0.2.1"
zoneId: ${zone.id}
proxied: true
outputs:
url: "https://${record.hostname}"変更内容が、次の完成形と一致することを確認します。
ファイル名: index.js
"use strict";
const pulumi = require("@pulumi/pulumi");
const cloudflare = require("@pulumi/cloudflare");
const config = new pulumi.Config();
const accountId = config.require("accountId");
const domain = config.require("domain");
const content = `export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response("Hello World!", options);
},
};`;
const worker = new cloudflare.WorkersScript("hello-world-worker", {
accountId: accountId,
name: "hello-world-worker",
content: content,
module: true, // ES6 module
});
const zone = cloudflare.getZone({
accountId: accountId,
name: domain,
});
const zoneId = zone.then((z) => z.zoneId);
const route = new cloudflare.WorkersRoute("hello-world-route", {
zoneId: zoneId,
pattern: "hello-world." + domain,
scriptName: worker.name,
});
const record = new cloudflare.Record("hello-world-record", {
name: route.pattern,
type: "A",
content: "192.0.2.1",
zoneId: zoneId,
proxied: true,
});
exports.url = pulumi.interpolate`https://${record.hostname}`;ファイル名: index.ts
import * as pulumi from "@pulumi/pulumi";
import * as cloudflare from "@pulumi/cloudflare";
const config = new pulumi.Config();
const accountId = config.require("accountId");
const domain = config.require("domain");
const content = `export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response("Hello World!", options);
},
};`;
const worker = new cloudflare.WorkersScript("hello-world-worker", {
accountId: accountId,
name: "hello-world-worker",
content: content,
module: true, // ES6 module
});
const zone = cloudflare.getZone({
accountId: accountId,
name: domain,
});
const zoneId = zone.then((z) => z.zoneId);
const route = new cloudflare.WorkersRoute("hello-world-route", {
zoneId: zoneId,
pattern: "hello-world." + domain,
scriptName: worker.name,
});
const record = new cloudflare.Record("hello-world-record", {
name: route.pattern,
type: "A",
content: "192.0.2.1",
zoneId: zoneId,
proxied: true,
});
export const url = pulumi.interpolate`https://${record.hostname}`;ファイル名: __main__.py
"""Pulumi program """
import pulumi
import pulumi_cloudflare as cloudflare
CONFIG = pulumi.Config()
ACCOUNT_ID = CONFIG.get("accountId")
DOMAIN = CONFIG.require("domain")
CONTENT = """
export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response("Hello World!", options);
},
};
"""
worker = cloudflare.WorkersScript("hello-world-worker",
account_id=ACCOUNT_ID,
name="hello-world-worker",
content=CONTENT,
module=True # ES6 module
)
zone = cloudflare.get_zone(account_id=ACCOUNT_ID, name=DOMAIN)
zone_id = zone.zone_id
route = cloudflare.WorkersRoute("hello-world-route",
zone_id=zone_id,
pattern="hello-world." + DOMAIN,
script_name=worker.name
)
record = cloudflare.Record("hello-world-record",
name=route.pattern,
type="A",
content="192.0.2.1",
zone_id=zone_id,
proxied=True
)
url = pulumi.Output.concat("https://", record.hostname)
pulumi.export('url', url)ファイル名: main.go
package main
import (
"github.com/pulumi/pulumi-cloudflare/sdk/v5/go/cloudflare"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi/config"
)
func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
conf := config.New(ctx, "")
accountID := conf.Get("accountId")
domain := conf.Get("domain")
content := `
export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response("Hello World!", options);
},
};
`
worker, err := cloudflare.NewWorkersScript(ctx, "hello-world-worker", &cloudflare.WorkersScriptArgs{
AccountId: pulumi.String(accountID),
Name: pulumi.String("hello-world-worker"),
Content: pulumi.String(content),
Module: pulumi.Bool(true), // ES6 module
})
if err != nil {
return err
}
zone, err := cloudflare.LookupZone(ctx, &cloudflare.LookupZoneArgs{
AccountId: &accountID,
Name: &domain,
}, nil)
if err != nil {
return err
}
route, err := cloudflare.NewWorkersRoute(ctx, "hello-world-route", &cloudflare.WorkersRouteArgs{
ZoneId: pulumi.String(zone.Id),
Pattern: pulumi.String("hello-world." + domain),
ScriptName: worker.Name,
})
if err != nil {
return err
}
record, err := cloudflare.NewRecord(ctx, "hello-world-record", &cloudflare.RecordArgs{
Name: route.Pattern,
Type: pulumi.String("A"),
Content: pulumi.String("192.0.2.1"),
ZoneId: pulumi.String(zone.Id),
Proxied: pulumi.Bool(true),
})
if err != nil {
return err
}
ctx.Export("url", pulumi.Sprintf("https://%s", record.Hostname))
return nil
})
}ファイル名: src/main/java/myproject/App.java
package myproject;
import com.pulumi.Pulumi;
import com.pulumi.core.Output;
import com.pulumi.cloudflare.WorkersScript;
import com.pulumi.cloudflare.WorkersScriptArgs;
import com.pulumi.cloudflare.CloudflareFunctions;
import com.pulumi.cloudflare.inputs.GetZoneArgs;
import com.pulumi.cloudflare.WorkersRoute;
import com.pulumi.cloudflare.WorkersRouteArgs;
import com.pulumi.cloudflare.Record;
import com.pulumi.cloudflare.RecordArgs;
public class App {
public static void main(String[] args) {
Pulumi.run(ctx -> {
var content = """
export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response("Hello World!", options);
},
};
""";
var accountId = ctx.config().require("accountId");
var domain = ctx.config().require("domain");
var worker = new WorkersScript("hello-world-worker", WorkersScriptArgs.builder()
.accountId(accountId)
.name("hello-world-worker")
.content(content)
.module(true)
.build());
final var zone = CloudflareFunctions.getZone(GetZoneArgs.builder()
.accountId(accountId)
.name(domain)
.build());
var route = new WorkersRoute("hello-world-route", WorkersRouteArgs.builder()
.zoneId(zone.applyValue(getZoneResult -> getZoneResult.id()))
.pattern("hello-world." + domain)
.scriptName(worker.name())
.build());
var record = new Record("hello-world-record", RecordArgs.builder()
.name(route.pattern())
.type("A")
.content("192.0.2.1")
.zoneId(zone.applyValue(getZoneResult -> getZoneResult.id()))
.proxied(true)
.build());
ctx.export("url", Output.format("https://%s", record.hostname()));
return;
});
}
}ファイル名: Program.cs
using System.Collections.Generic;
using Pulumi;
using Cloudflare = Pulumi.Cloudflare;
return await Deployment.RunAsync(() =>
{
var config = new Config();
var accountId = config.Require("accountId");
var domain = config.Require("domain");
var content = @"
export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response(""Hello World!"", options);
},
};
";
var worker = new Cloudflare.WorkersScript("hello-world-worker", new()
{
AccountId = accountId,
Name = "hello-world-worker",
Content = content,
Module = true
});
var zone = Output.Create(Cloudflare.GetZone.InvokeAsync(new()
{
AccountId = accountId,
Name = domain,
}));
var route = new Cloudflare.WorkersRoute("hello-world-route", new()
{
ZoneId = zone.Apply(z => z.Id),
Pattern = "hello-world." + domain,
ScriptName = worker.Name,
});
var record = new Cloudflare.Record("hello-world-record", new()
{
Name = route.Pattern,
Type = "A",
Content = "192.0.2.1",
ZoneId = zone.Apply(z => z.Id),
Proxied = true
});
return new Dictionary<string, object?>
{
["url"] = Output.Format($"https://{record.Hostname}")
};
});ファイル名: Pulumi.yaml
name: serverless-cloudflare
runtime: yaml
variables:
zone:
fn::invoke:
function: cloudflare:getZone
arguments:
accountId: ${accountId}
name: ${domain}
resources:
worker:
type: cloudflare:WorkersScript
properties:
accountId: "${accountId}"
name: "hello-world-worker"
content: |
export default {
async fetch(request) {
const options = { headers: { 'content-type': 'text/plain' } };
return new Response("Hello World!", options);
},
};
module: true
route:
type: cloudflare:WorkersRoute
properties:
zoneId: ${zone.id}
pattern: "hello-world.${domain}"
scriptName: ${worker.name}
record:
type: cloudflare:Record
properties:
name: ${route.pattern}
type: A
content: "192.0.2.1"
zoneId: ${zone.id}
proxied: true
outputs:
url: "https://${record.hostname}"Cloudflare リソースをすべて定義したので、Pulumi CLI で Hello World アプリケーションを Cloudflare アカウントへデプロイできます。
変更をデプロイするには、次を実行します。
pulumi up --yeswait for the dev stack to become readyHello World アプリケーションを動かしてアクセスできるよう、Cloudflare リソースを段階的に追加しました。Pulumi スタックの url 出力を curl して、アプリケーションをテストできます。
curl $(pulumi stack output url)Hello, World!最後に、チュートリアルで使ったリソースとスタックを削除します。
pulumi destroypulumi stack rm devPulumi と Cloudflare で定義できるほかのリソースは、Cloudflare パッケージのドキュメント ↗ を参照してください。