🍒

もうこれにする。Pulumi AI で Cloudflare + Route 53 を楽チン IaC。

2023/12/25に公開

はじめに

仕事上、Cloudflare を CNAME セットアップでデモをする環境を用意しており、権威 DNS には Route 53 を使っています。

こいつ、新しいホストを Cloudflare に作るたびに Route 53 でも作業が発生して、地味にタルいなと感じつつ、タマにだからいいか、とそのままにしておりました。
(ワイルドカードもいやだし、Teraform も何か面倒くさいしなぁ…)

そんななか、Devdoc に Pulumi を見つけました。
恥ずかしながら、Devdoc を見るまで Pulumi って知りませんでしたが、試したところ、そのタルさをサクッと解消してくれたので、メモしておきます。

Pulumi AI

Devdoc のチュートリアルに沿ってもいいのですが、試している中で Pulumi AI に気づきまして、これを利用するのが普段インフラを管理しない私レベルの作業効率化にはウッテツケでした。

Pulumi にアカウント登録して Pulumi AI に行くと、やりたいことを入れるだけで、いろんな言語でサンプルを表示してくれる のです。今どき普通なの?すごい。

さっそく、例の面倒な作業(Route 53 に CNAME レコード作って Workers の Custom Domains で公開)を頼んでみます。
Create or modify an AWS route53 CNAME resource record and create a cloudflare workers script with a workers custom domain と入れて、 TypeScript と Python の二種をご注文です。ポチッ。流れるようにコードや注意点を示してくれます。

その後気づいたんですが、日本語での注文も余裕で対応しておりました。素晴らしい。。
ハナから英語だろうという思い込みが足を引っ張ってました、、、反省です。

日本語でも英語でも、多少内容を変えるだけで、サンプルスクリプトも変わってくるのが面白いですね。いい感じのサンプルが来るまで、いくつか入れてみるのがいいかもです。

ということで、Pulumi AI が教えてくれたサンプルと、Devdoc および Pulumi の Docs があればなんとかなりそうです。

実際にやってみます。
GUI を使わなくても、CLI のなかで AI に注文できる ところも気に入りました。

ありたい姿にする

Pulumi でプロジェクトを作成するには、テンプレートを利用するのが定番のようで、Devdoc の Hello World チュートリアルも外部 URL をソースに呼んでます。

ただ、今回は AI を選びます。

~/Pulumi/cname-setup/r53 $ pulumi new -h
:
To create a project using Pulumi AI, either select `ai` from the first selection, or provide any of the following:
* `pulumi new --ai "<prompt>"`
* `pulumi new --language <language>`
* `pulumi new --ai "<prompt>" --language <language>`
:

Route 53

  1. プロジェクト作成

pulumi new から ai オプションを選びます。
TypeScriptmodify DNS resource records of existing AWS Route53 zones を注文します。
Project Name や Stack Name(デフォルトは dev)は指定のままで行きました。

~/Pulumi/cname-setup/r53 $ pulumi new
 Would you like to create a project from a template or using a Pulumi AI prompt? ai
 Please select a language for your project: TypeScript
 Please input your prompt here ("a static website on AWS behind a CDN"):
 modify DNS resource records of existing AWS Route53 zones
Sending prompt to Pulumi AI...
:
:

後追いですが、こちらも日本語で注文 OK でした。

  1. pulumi config 作成

AWS のユーザープロファイルを Config に set しました。

ただ、Route 53 の権限だけ与えた AWS ユーザーを利用しているためか、後述 4 のデプロイ時に AWS から認証まわりのエラー retrieving caller identity from STSretrieving account information via iam:ListRoles が出ました。
回避のために 2 つの skip を true で Config Options に追加しました。

~/Pulumi/cname-setup/r53 $ pulumi config set aws:profile r53admin -s dev
~/Pulumi/cname-setup/r53 $ pulumi config set aws:skipCredentialsValidation true -s dev
~/Pulumi/cname-setup/r53 $ pulumi config set aws:skipRequestingAccountId true -s dev
~/Pulumi/cname-setup/r53 $ cat Pulumi.dev.yaml
config:
  aws:profile: r53admin
  aws:skipCredentialsValidation: "true"
  aws:skipRequestingAccountId: "true"
~/Pulumi/cname-setup/r53 $
  1. index.ts の編集

AI に作成してもらった index.ts を Pulumi Doc など見ながら調整します。

import * as aws from "@pulumi/aws";
import * as pulumi from "@pulumi/pulumi";

// Modify DNS resource records of an existing AWS Route53 Zone
const zoneName = 'oyama.cf.';
const zoneId = pulumi.output(aws.route53.getZone({ name: zoneName }, { async: true }))
    .apply(zone => zone.zoneId);
const prefix = 'pulumi.';
const suffix = 'cdn.cloudflare.net.';
const record = new aws.route53.Record(prefix, {
    zoneId: zoneId,
    name: prefix,
    type: "CNAME",
    ttl: 300,
    records: [ prefix + zoneName + suffix ],
    allowOverwrite: true,
});
  1. デプロイ

pulumi up ...

途中で details を選ぶとデプロイ内容を事前に確認できました。
no で取りやめ、yes でゴーです。
yes のあとにエラーが出ることもあります。

~/Pulumi/cname-setup/r53 $ pulumi up
Previewing update (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/*

     Type                   Name     Plan
 +   pulumi:pulumi:Stack    r53-dev  create
 +   └─ aws:route53:Record  pulumi.  create

Resources:
    + 2 to create

Do you want to perform this update? details
+ pulumi:pulumi:Stack: (create)
        :
        allowOverwrite: true
        name          : "pulumi."
        records       : [
            [0]: "pulumi.oyama.cf.cdn.cloudflare.net."
        ]
        ttl           : 300
        type          : "CNAME"
        zoneId        : *

Do you want to perform this update? yes
Updating (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/*

     Type                   Name     Status
 +   pulumi:pulumi:Stack    r53-dev  created (1s)
 +   └─ aws:route53:Record  pulumi.  created (46s)

Resources:
    + 2 created

Duration: 53s
  1. 動作確認

CNAME 拾えています。

~ $ dig pulumi.oyama.cf CNAME +norec +short @ns-418.awsdns-52.com
pulumi.oyama.cf.cdn.cloudflare.net.

Cloudflare

プロジェクトは Route 53 と別にしてます。

~/Pulumi/cname-setup/r53 $ cd ../cf
~/Pulumi/cname-setup/cf $
  1. プロジェクト作成

こちらも AI にサンプル作っていただきます。
a cloudflare worker with custom domains

~/Pulumi/cname-setup/cf $ pulumi new
 Would you like to create a project from a template or using a Pulumi AI prompt? ai
 Please select a language for your project: TypeScript
 Please input your prompt here ("a static website on AWS behind a CDN"):
 a cloudflare worker with custom domains
Sending prompt to Pulumi AI...
:
  1. pulumi config 作成

スクリプト内で利用する Cloudfalre の API Token と Account ID を Config に入れました。

~/Pulumi/cname-setup/cf $ pulumi config set accountId --secret
~/Pulumi/cname-setup/cf $ pulumi config set cloudflare:apiToken --secret
value:
~/Pulumi/cname-setup/cf $ cat Pulumi.dev.yaml
config:
  cf:accountId:
    secure: AAAB..
  cloudflare:apiToken:
    secure: AAAB..
  1. index.ts の編集

AI に作成してもらった index.ts を Pulumi Doc、Devdoc など見ながら調整します。

import * as cloudflare from "@pulumi/cloudflare";
import * as pulumi from "@pulumi/pulumi";

const config = new pulumi.Config();
const accountId = config.require("accountId");
const zoneName = "oyama.cf";
const prefix = "pulumi."
const zone = pulumi.output(cloudflare.getZone({
  accountId: accountId,
  name: zoneName,
}));
const zoneId = zone.id;

// Define a Worker script
const workerScript = new cloudflare.WorkerScript("my-worker", {
    name: "pulumi-1st",
    accountId: accountId, // Replace with your actual Cloudflare account ID
    content: `
        addEventListener("fetch", event => {
            event.respondWith(handleRequest(event.request))
        });
        async function handleRequest(request) {
            return new Response("Hello World", { status: 200 })
        }
    `,
});

// Assign the Worker script to a particular domain
const workerDomain = new cloudflare.WorkerDomain("my-worker-domain", {
    accountId: accountId,
    hostname: prefix + zoneName,
    service: workerScript.name,
    zoneId: zoneId,
});
// Export the live URL of the worker
export const workerUrl = pulumi.interpolate`https://${workerDomain.hostname}/`;
  1. デプロイ

pulumi up ...

~/Pulumi/cname-setup/cf $ pulumi up
Previewing update (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/*

     Type                              Name              Plan
 +   pulumi:pulumi:Stack               cf-dev            create
 +   ├─ cloudflare:index:WorkerScript  my-worker         create
 +   └─ cloudflare:index:WorkerDomain  my-worker-domain  create

Outputs:
    workerUrl: "https://pulumi.oyama.cf/"

Resources:
    + 3 to create

Do you want to perform this update? details
+ pulumi:pulumi:Stack: (create)
        :
    + cloudflare:index/workerScript:WorkerScript: (create)
        :
        accountId : "[secret]"
        content   : "\n        addEventListener(\"fetch\", event => {\n            event.respondWith(handleRequest(event.request))\n        });\n        async function handleRequest(request) {\n            return new Response(\"Hello World\", { status: 200 })\n        }\n    "
        name      : "pulumi-1st"
    + cloudflare:index/workerDomain:WorkerDomain: (create)
        :
        accountId  : "[secret]"
        environment: "production"
        hostname   : "pulumi.oyama.cf"
        service    : "pulumi-1st"
        zoneId     : *

    --outputs:--
    workerUrl: "https://pulumi.oyama.cf/"

Do you want to perform this update? yes
Updating (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/*

     Type                              Name              Status
 +   pulumi:pulumi:Stack               cf-dev            created (6s)
 +   ├─ cloudflare:index:WorkerScript  my-worker         created (1s)
 +   └─ cloudflare:index:WorkerDomain  my-worker-domain  created (1s)

Outputs:
    workerUrl: "https://pulumi.oyama.cf/"

Resources:
    + 3 created

Duration: 10s
  1. 動作確認

Outputs で表示した URL にアクセスしてみます。
無事 Worker が動いているようです。

~ $ curl https://pulumi.oyama.cf/
Hello World%
~ $

Devdoc のチュートリアルに習うとこんな感じでしょうか。pulumi stack output から引いてます。

~/Pulumi/cname-setup/cf $ curl `pulumi stack output workerUrl`
Hello World%
~/Pulumi/cname-setup/cf $

なかったことにする

デモなどの場合、終わったのに環境を消すのを忘れてしまい、もう使わない Cloudflare のゴミ設定が私のアカウントにずっと残りっぱなしになってます。。。

もとに戻すのも pulumi destroy ... で済むのがいいですね。

## Cloudflare
~/Pulumi/cname-setup/cf $ pulumi destroy
Previewing destroy (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/*

     Type                              Name              Plan
 -   pulumi:pulumi:Stack               cf-dev            delete
 -   ├─ cloudflare:index:WorkerDomain  my-worker-domain  delete
 -   └─ cloudflare:index:WorkerScript  my-worker         delete

Outputs:
  - workerUrl: "https://pulumi.oyama.cf/"

Resources:
    - 3 to delete

Do you want to perform this destroy? yes
Destroying (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/*

     Type                              Name              Status
 -   pulumi:pulumi:Stack               cf-dev            deleted (0.38s)
 -   ├─ cloudflare:index:WorkerDomain  my-worker-domain  deleted (1s)
 -   └─ cloudflare:index:WorkerScript  my-worker         deleted (0.70s)

Outputs:
  - workerUrl: "https://pulumi.oyama.cf/"

Resources:
    - 3 deleted

Duration: 5s

The resources in the stack have been deleted, but the history and configuration associated with the stack are still maintained.
If you want to remove the stack completely, run `pulumi stack rm dev`.

## Route 53
~/Pulumi/cname-setup/cf $ cd ../r53
~/Pulumi/cname-setup/r53 $
~/Pulumi/cname-setup/r53 $ pulumi destroy
Previewing destroy (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/*

     Type                   Name     Plan
 -   pulumi:pulumi:Stack    r53-dev  delete
 -   └─ aws:route53:Record  pulumi.  delete

Resources:
    - 2 to delete

Do you want to perform this destroy? yes
Destroying (dev)

View in Browser (Ctrl+O): https://app.pulumi.com/*

     Type                   Name     Status
 -   pulumi:pulumi:Stack    r53-dev  deleted (0.38s)
 -   └─ aws:route53:Record  pulumi.  deleted (57s)

Resources:
    - 2 deleted

Duration: 1m1s

The resources in the stack have been deleted, but the history and configuration associated with the stack are still maintained.
If you want to remove the stack completely, run `pulumi stack rm dev`.

まとめ

必要に駆られたときの AI の力を見せていただきました。

タルさがちょっとだったので見て見ぬふりしてましたけど、AI が解決に乗り出す負荷を軽くしてくれました。これからの作業が楽になり、ミライ時間を手に入れました。

軽く触っただけで、やりたいことができたので十分ですが、さらに奥は深そうですね。
色々なサービス向けのプロバイダーがあるところを見ると、実際の現場では、浅いところから深いところまで、多く活用されているのではないかと想像します。

私の環境でも S3 や R2、GCP や OCI などデモに使ってたりするので、そっちも楽ができそうです。

ありがとう Pulumi AI これからもよろしく。

Discussion