PythonでIaCかきたい?それならPulumiだ!
今回はPulumiに入門して、PythonでIaCを実現する方法に触れてみました。今までTerraformしか使ったことがなかったですが、使い慣れたPythonでIaCが書けるということで使ってみました。
Pulumiとは?
Pulumiは、Pythonなどのプログラミング言語を使ってクラウドインフラストラクチャを構築、デプロイ、管理するためのオープンソースプラットフォームです。あらゆるクラウドをまたいで、統一されたワークフローでインフラストラクチャ、シークレット、構成を管理できます。IaCといえばTerraformなどが有名かつよく利用されていると思いますが、普段使い慣れている言語でインフラを記述できるのはとても魅力的です。
利用可能言語については、ドキュメントを見る限り如何対応していそうです!
- TypeScript
- Python
- Go
- C#
- Java
- YAML
今回は一番使い慣れているPythonのチュートリアルを進めます!
早速使ってみよう!
それでは早速使っていきましょう!今回はGoogle Cloudを対象に、Pythonを使って環境構築をするためのチュートリアルを実施してみます!このチュートリアルの目標としては以下になります。なお、Google Cloudのプロジェkぅとは作成済みであり、すでに認証ができている前提で進めます!
Pulumiのインストール
早速Pulumiをインストールしたいと思います。インストールにはnpmを利用してインストールします。
brew install pulumi/tap/pulumi
Pulumiのプロジェクトの作成
Pulumiのインストールが完了したので、早速プロジェクトを立ち上げます。以下のようにフォルダを作成しpulumi new gcp-pythonのようにすることで初期化できます。
mkdir quickstart && cd quickstart
pulumi new gcp-python
実行するとまず以下のようにPulumiへのログインが求められます。私はGitHubアカウントを連携しました。
Manage your Pulumi stacks by logging in.
Run `pulumi login --help` for alternative login options.
Enter your access token from https://app.pulumi.com/account/tokens
or hit <ENTER> to log in using your browser
認証が終わると以下のようなメッセージが表示されます。
認証後のメッセージ
We've launched your web browser to complete the login process.
Waiting for login to complete...
Welcome to Pulumi!
Pulumi helps you create, deploy, and manage infrastructure on any cloud using
your favorite language. You can get started today with Pulumi at:
https://www.pulumi.com/docs/get-started/
Tip: Resources you create with Pulumi are given unique names (a randomly
generated suffix) by default. To learn more about auto-naming or customizing resource
names see https://www.pulumi.com/docs/intro/concepts/resources/#autonaming.
次にプロジェクト名を指定します。今回はquickstartのままにしたいのでEnterを押します。
This command will walk you through creating a new Pulumi project.
Enter a value or leave blank to accept the (default), and press <ENTER>.
Press ^C at any time to quit.
Project name (quickstart):
次にプロジェクトの説明を入力します(ブランクにしました)。
Project description (A minimal Google Cloud Python Pulumi program):
このあとはスタック名を指定しますが、今回はデフォルトのdevのままにします。
Please enter your desired stack name.
To create a stack in an organization, use the format <org-name>/<stack-name> (e.g. `acmecorp/dev`).
Stack name (dev):
Pythonのパッケージ管理方法を指定します。なんと、uvがデフォルトでリストに載っているのでuvにしました!
The toolchain to use for installing dependencies and running the program [Use arrows to move, type to filter]
pip
poetry
> uv
最後に対象とするGoogle Cloudのプロジェクト名を入力します。
The Google Cloud project to deploy into (gcp:project):
ここまできたら完了です!処理が完了すると以下のようなファイルが生成されます!
drwxr-xr-x@ - user 4 Dec 19:37 .venv
drwxr-xr-x@ - user 4 Dec 19:38 __pycache__
.rw-r--r--@ 12 user 4 Dec 19:37 .gitignore
.rw-r--r--@ 273 user 4 Dec 19:38 __main__.py
.rw-r--r--@ 40 user 4 Dec 19:37 Pulumi.dev.yaml
.rw-r--r--@ 196 user 4 Dec 19:37 Pulumi.yaml
.rw-r--r--@ 155 user 4 Dec 19:37 pyproject.toml
.rw-r--r--@ 2.6k user 4 Dec 19:37 README.md
.rw-r--r--@ 36k user 4 Dec 19:37 uv.lock
IaC実装の確認
デフォルトで作成された__main__.pyをみると、Cloud Storageを作成するための設定がデフォルトで記載されています。
"""A Google Cloud Python Pulumi program"""
import pulumi
from pulumi_gcp import storage
# Create a GCP resource (Storage Bucket)
bucket = storage.Bucket('my-bucket-from-pulumi', location="US")
# Export the DNS name of the bucket
pulumi.export('bucket_name', bucket.url)
コードを見るとpulumi_gcp.storage.Bucketを利用するとCloud Storageのバケットを指定できるようです。この設定ではストレージ名をmy-bucket-from-pulumi、ロケーションをUSに指定しています。
bucket = storage.Bucket('my-bucket-from-pulumi', location="US")
また、pulumi.exportを利用することで、IaC実行後に情報を取得できるようになります。今回の例ではバケット名が取得できるようになります(Terraformでいうoutputsに該当)。
pulumi.export('bucket_name', bucket.url)
スタックのデプロイ
それでは早速デプロイしてCloud Storageを作成してみましょう。pulumi upを実行するとまずは実行予定の確認が行われます。
Previewing update (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/...
Type Name Plan
+ pulumi:pulumi:Stack quickstart-dev create
+ └─ gcp:storage:Bucket my-bucket-from-pulumi create
Outputs:
bucket_name: [unknown]
Resources:
+ 2 to create
Do you want to perform this update? yes
ここでyesと入力すると早速構成が反映されます。
Updating (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/...
Type Name Status
+ pulumi:pulumi:Stack quickstart-dev created (4s)
+ └─ gcp:storage:Bucket my-bucket-from-pulumi created (2s)
Outputs:
bucket_name: "gs://my-bucket-from-pulumi-58f17c0"
Resources:
+ 2 created
Duration: 5s
実行すると問題なく終了し、Cloud Storageを見るとバケットが作成されていました。

作成されたバケット
また、先ほど設定したpulumi.exportのバケット名を取得するには、以下のコマンドを実行します。
pulumi stack output bucket_name
# 結果
gs://my-bucket-from-pulumi-58f17c0
バケットのSuffixの除去
先ほどの例ではmy-bucket-from-pulumiという名前で指定したいうつもりでしたがsuffixがついていました。一つ目の引数はリソース識別子のようで、それだけ指定した場合はsuffixが自動でつくようです。名前を指定した値にしたい場合、name属性を指定します。
bucket = storage.Bucket(
'my-bucket-from-pulumi',
name='my-bucket-from-pulumi',
location="US",
)
こちらの設定でname='my-bucket-from-pulumi'とすることで、バケット名からsuffixが覗かれることを想定します。それでは早速pulumi upで更新してみます。実行すると、以下のようにバケット名が変更されようとしているのが確認できます。
Previewing update (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/...
Type Name Plan Info
pulumi:pulumi:Stack quickstart-dev
+- └─ gcp:storage:Bucket my-bucket-from-pulumi replace [diff: ~name]
Resources:
+-1 to replace
1 unchanged
Do you want to perform this update? details
pulumi:pulumi:Stack: (same)
[urn=urn:pulumi:dev::quickstart::pulumi:pulumi:Stack::quickstart-dev]
--gcp:storage/bucket:Bucket: (delete-replaced)
[id=my-bucket-from-pulumi-58f17c0]
[urn=urn:pulumi:dev::quickstart::gcp:storage/bucket:Bucket::my-bucket-from-pulumi]
[provider=urn:pulumi:dev::quickstart::pulumi:providers:gcp::default_9_6_0::85796500-e20b-4c88-ad42-217c3f7167bf]
+-gcp:storage/bucket:Bucket: (replace)
[id=my-bucket-from-pulumi-58f17c0]
[urn=urn:pulumi:dev::quickstart::gcp:storage/bucket:Bucket::my-bucket-from-pulumi]
[provider=urn:pulumi:dev::quickstart::pulumi:providers:gcp::default_9_6_0::85796500-e20b-4c88-ad42-217c3f7167bf]
~ name: "my-bucket-from-pulumi-58f17c0" => "my-bucket-from-pulumi"
++gcp:storage/bucket:Bucket: (create-replacement)
[id=my-bucket-from-pulumi-58f17c0]
[urn=urn:pulumi:dev::quickstart::gcp:storage/bucket:Bucket::my-bucket-from-pulumi]
[provider=urn:pulumi:dev::quickstart::pulumi:providers:gcp::default_9_6_0::85796500-e20b-4c88-ad42-217c3f7167bf]
~ name: "my-bucket-from-pulumi-58f17c0" => "my-bucket-from-pulumi"
Do you want to perform this update? [Use arrows to move, type to filter]
> yes
no
details
explain ✨
実行すると以下のように処理が正常終了したことが確認できます。
Updating (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/...
Type Name Status Info
pulumi:pulumi:Stack quickstart-dev
+- └─ gcp:storage:Bucket my-bucket-from-pulumi replaced (2s) [diff: ~name]
Outputs:
~ bucket_name: "gs://my-bucket-from-pulumi-58f17c0" => "gs://my-bucket-from-pulumi"
Resources:
+-1 replaced
1 unchanged
Duration: 7s

Suffixが除去されたバケット
HTMLファイルをアップロード
次にローカルにあるファイルをCloud Storageにアップロードしてみます。
HTMLファイルとして以下のようなファイルを作成します。
<html>
<body>
<h1>Hello, Pulumi!</h1>
</body>
</html>
ここで作成したファイルを先ほど作成したバケットにアップロードするには、以下のようにコードを書き換えます。
"""A Google Cloud Python Pulumi program"""
import pulumi
from pulumi_gcp import storage
# Create a GCP resource (Storage Bucket)
bucket = storage.Bucket(
'my-bucket-from-pulumi',
name='my-bucket-from-pulumi',
location="US",
)
bucket_object = storage.BucketObject(
"index.html",
bucket=bucket.name,
source=pulumi.FileAsset("index.html")
)
bucket_iam_binding = storage.BucketIAMBinding(
"my-bucket-binding",
bucket=bucket.name,
role="roles/storage.objectViewer",
members=["allUsers"],
)
# Export the DNS name of the bucket
pulumi.export('bucket_name', bucket.url)
追加しているところとしてしているところとしては、bucket_objectとbucket_iam_bindingの部分です。オブへくとの部分ではbucket=bucket.nameでCloud Storageバケットを、source=pulumi.FileAsset("index.html")でHTMLファイルを指定しています。
bucket_object = storage.BucketObject(
"index.html",
bucket=bucket.name,
source=pulumi.FileAsset("index.html")
)
また、全ユーザからファイルを参照できるようにIAMロールを付与しています。
bucket_iam_binding = storage.BucketIAMBinding(
"my-bucket-binding",
bucket=bucket.name,
role="roles/storage.objectViewer",
members=["allUsers"],
)
こちらで変更してみましょう。早速pulumi upの実行結果を載せておきます!
Previewing update (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/...
Type Name Plan
pulumi:pulumi:Stack quickstart-dev
+ ├─ gcp:storage:BucketIAMBinding my-bucket-binding create
+ └─ gcp:storage:BucketObject index.html create
Resources:
+ 2 to create
2 unchanged
# 反映結果
Updating (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/...
Type Name Status
pulumi:pulumi:Stack quickstart-dev
+ ├─ gcp:storage:BucketObject index.html created (0.85s)
+ └─ gcp:storage:BucketIAMBinding my-bucket-binding created (6s)
Outputs:
bucket_name: "gs://my-bucket-from-pulumi"
Resources:
+ 2 created
2 unchanged
Duration: 8s
ストレージを見るとファイルがアップロードされていることを確認できました。なお、ファイル名には先ほどのバケットと同様にsuffixがついています。

HTMLファイルのアップロード
HTMLファイルを静的ファイルとして公開
先ほどアップロードしたHTMLファイルを静的ファイルとして公開するための設定をしてみます。Aまずはソースコードを以下のように変更します。
"""A Google Cloud Python Pulumi program"""
import pulumi
from pulumi_gcp import storage
# Create a GCP resource (Storage Bucket)
bucket = storage.Bucket(
'my-bucket-from-pulumi',
name='my-bucket-from-pulumi',
location="US",
website={
"main_page_suffix": "index.html"
},
uniform_bucket_level_access=True,
)
bucket_object = storage.BucketObject(
"index.html",
bucket=bucket.name,
source=pulumi.FileAsset("index.html")
)
bucket_iam_binding = storage.BucketIAMBinding(
"my-bucket-binding",
bucket=bucket.name,
role="roles/storage.objectViewer",
members=["allUsers"],
)
# Export the DNS name of the bucket
pulumi.export('bucket_name', bucket.url)
pulumi.export(
"bucket_endpoint",
pulumi.Output.concat(
"http://storage.googleapis.com/", bucket.id, "/", bucket_object.name
),
)
変更点は二つあり、一つ目はバケットの設定としてwebsiteにメインページの指定を、uniform_bucket_level_accessを有効にすることです。次にアウトプット情報としてHTMLファイルにアクセスするためのエンドポイントをbucket_endpointとして指定しています。
それでは早速こちらの変更を反映してみます。
Previewing update (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/...
Type Name Plan Info
pulumi:pulumi:Stack quickstart-dev
~ └─ gcp:storage:Bucket my-bucket-from-pulumi update [diff: +website~uniformBucketLevelAccess]
Outputs:
+ bucket_endpoint: "http://storage.googleapis.com/my-bucket-from-pulumi/index.html-632c5e9"
Resources:
~ 1 to update
3 unchanged
Do you want to perform this update? details
pulumi:pulumi:Stack: (same)
[urn=urn:pulumi:dev::quickstart::pulumi:pulumi:Stack::quickstart-dev]
~ gcp:storage/bucket:Bucket: (update)
[id=my-bucket-from-pulumi]
[urn=urn:pulumi:dev::quickstart::gcp:storage/bucket:Bucket::my-bucket-from-pulumi]
[provider=urn:pulumi:dev::quickstart::pulumi:providers:gcp::default_9_6_0::85796500-e20b-4c88-ad42-217c3f7167bf]
~ uniformBucketLevelAccess: false => true
+ website : {
+ mainPageSuffix: "index.html"
}
--outputs:--
+ bucket_endpoint: "http://storage.googleapis.com/my-bucket-from-pulumi/index.html-632c5e9"
# 反映結果
Updating (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/...
Type Name Status Info
pulumi:pulumi:Stack quickstart-dev
~ └─ gcp:storage:Bucket my-bucket-from-pulumi updated (1.00s) [diff: +website~uniformBucketLevelAccess]
Outputs:
+ bucket_endpoint: "http://storage.googleapis.com/my-bucket-from-pulumi/index.html-632c5e9"
bucket_name : "gs://my-bucket-from-pulumi"
Resources:
~ 1 updated
3 unchanged
Duration: 4s
コンテンツの出力エンドポイントを見るためにpulumi stack outputを実行してみると以下のような結果になりました。
pulumi stack output bucket_endpoint
# 結果
http://storage.googleapis.com/my-bucket-from-pulumi/index.html-...
早速HTMLファイル情報を取得すると、先ほど作成したindex.htmlファイルの内容を取得できました。
curl $(pulumi stack output bucket_endpoint)
# 結果
<html>
<body>
<h1>Hello, Pulumi!</h1>
</body>
</html>
構成の削除
これまで作成してきたリソースを削除するにはpulumi destroyを実行します。実行すると以下のような確認が表示されます。
Previewing destroy (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/...
Type Name Plan
- pulumi:pulumi:Stack quickstart-dev delete
- ├─ gcp:storage:Bucket my-bucket-from-pulumi delete
- ├─ gcp:storage:BucketIAMBinding my-bucket-binding delete
- └─ gcp:storage:BucketObject index.html delete
Outputs:
- bucket_endpoint: "http://storage.googleapis.com/my-bucket-from-pulumi/index.html-632c5e9"
- bucket_name : "gs://my-bucket-from-pulumi"
Resources:
- 4 to delete
Do you want to perform this destroy? details
- gcp:storage/bucketIAMBinding:BucketIAMBinding: (delete)
[id=b/my-bucket-from-pulumi/roles/storage.objectViewer]
[urn=urn:pulumi:dev::quickstart::gcp:storage/bucketIAMBinding:BucketIAMBinding::my-bucket-binding]
[provider=urn:pulumi:dev::quickstart::pulumi:providers:gcp::default_9_6_0::85796500-e20b-4c88-ad42-217c3f7167bf]
- gcp:storage/bucketObject:BucketObject: (delete)
[id=my-bucket-from-pulumi-index.html-632c5e9]
[urn=urn:pulumi:dev::quickstart::gcp:storage/bucketObject:BucketObject::index.html]
[provider=urn:pulumi:dev::quickstart::pulumi:providers:gcp::default_9_6_0::85796500-e20b-4c88-ad42-217c3f7167bf]
- gcp:storage/bucket:Bucket: (delete)
[id=my-bucket-from-pulumi]
[urn=urn:pulumi:dev::quickstart::gcp:storage/bucket:Bucket::my-bucket-from-pulumi]
[provider=urn:pulumi:dev::quickstart::pulumi:providers:gcp::default_9_6_0::85796500-e20b-4c88-ad42-217c3f7167bf]
- pulumi:pulumi:Stack: (delete)
[urn=urn:pulumi:dev::quickstart::pulumi:pulumi:Stack::quickstart-dev]
--outputs:--
- bucket_endpoint: "http://storage.googleapis.com/my-bucket-from-pulumi/index.html-632c5e9"
- bucket_name : "gs://my-bucket-from-pulumi"
yesを選択するとリソースが削除されました。先ほど作成したHTMLファイルにもアクセスできなくなりました。
curl $(pulumi stack output bucket_endpoint)
# 結果
error: current stack does not have output property 'bucket_endpoint'
curl: try 'curl --help' or 'curl --manual' for more information
まとめ
今回はPulumiを使ってCloud StorageのバケットとHTMLファイルの静的ホスティングを試してみました。使い慣れたPythonで直感的に書けるところが個人的にとても良かったです。モジュール化をどのようにすると良いかやCI/CDのパイプラインの組み方などまだまだ調べることはありますが、Terraformと比較することもどんどんしていきたいと思い間していきたいと思います!
Discussion