🙌

PythonでIaCかきたい?それならPulumiだ!

に公開

今回はPulumiに入門して、PythonでIaCを実現する方法に触れてみました。今までTerraformしか使ったことがなかったですが、使い慣れたPythonでIaCが書けるということで使ってみました。

Pulumiとは?

Pulumiは、Pythonなどのプログラミング言語を使ってクラウドインフラストラクチャを構築、デプロイ、管理するためのオープンソースプラットフォームです。あらゆるクラウドをまたいで、統一されたワークフローでインフラストラクチャ、シークレット、構成を管理できます。IaCといえばTerraformなどが有名かつよく利用されていると思いますが、普段使い慣れている言語でインフラを記述できるのはとても魅力的です。

利用可能言語については、ドキュメントを見る限り如何対応していそうです!

  • TypeScript
  • Python
  • Go
  • C#
  • Java
  • YAML

今回は一番使い慣れているPythonのチュートリアルを進めます!

https://app.pulumi.com/signin?reason=401

早速使ってみよう!

それでは早速使っていきましょう!今回はGoogle Cloudを対象に、Pythonを使って環境構築をするためのチュートリアルを実施してみます!このチュートリアルの目標としては以下になります。なお、Google Cloudのプロジェkぅとは作成済みであり、すでに認証ができている前提で進めます!

https://www.pulumi.com/docs/iac/get-started/gcp/

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を作成するための設定がデフォルトで記載されています。

__main__.py
"""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ファイルとして以下のようなファイルを作成します。

index.html
<html>
    <body>
        <h1>Hello, Pulumi!</h1>
    </body>
</html>

ここで作成したファイルを先ほど作成したバケットにアップロードするには、以下のようにコードを書き換えます。

__main__.py
"""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_objectbucket_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まずはソースコードを以下のように変更します。

__main__.py
"""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