PulumiのGet Started with Google Cloudを試してみた

2022.04.20

こんにちは!DA(データアナリティクス)事業本部 サービスソリューション部の大高です。

先日、マルチクラウドに対応している Infrastructure as Code のツール「Pulumi」のGet Started with AWSを試してみました。

今回、こちらのエントリと同様にGoogle Cloudも試してみたいと思います。

やりたいこと

以下の公式ドキュメントに記載されている「Get Started with Pulumi」の「Get Started with Google Cloud」に沿って進めていきたいと思います。

こちらでは簡単なリソースの作成から、変更、削除までの流れを試すことができます。

前提条件

実施する環境として、OSはMacOSを利用し、Nodeも事前に導入済みです。

% node -v
v16.14.2

また、Google Cloudのアカウントは作成済みで、以下に従って設定済みです。

なお、2022/04/20時点ではGoogle Cloudのパッケージは過渡期になっており、今後は以下がメインになっていくと思われます。(現在はPublic Previewでした)

今回はチュートリアルに従って「Google Cloud (GCP) Classic」を利用します。

Get Started with Pulumi

では、実際にドキュメントに従って始めていきます。

Before You Begin

Install Pulumi

まずはPulumiのインストールです。Homwbrew経由でのインストールなので特に問題ありません。

% brew install pulumi
% pulumi version
v3.29.1

Install Language Runtime

今回はTypeScriptで試したいと思いますので、Node.jsのインストールが必要となります。

こちらは「前提条件」に記載のとおり事前に導入済みなので詳細はスキップしますが、私はanyenvを利用して導入しています。

% node -v
v16.14.2

Configure Pulumi to access your Google Cloud account

このGet Startedでは roles/storage.admin または roles/storage.legacyBucketOwner の権限が付与されたIAM Userが必要となります。こちらは「前提条件」に記載のとおり事前に作成済みです。

Pulumiのアカウント作成とトークンの発行

ドキュメントには記載がありませんが、後で利用するので「Pulumiのアカウント作成」と「トークンの発行」がまだの場合、対応を実施しておきます。

下記のページの「Create an account」からアカウントを作成します。

2022/04/18現在では、GitHub、GitLab、Atlassian、Emailのいずれかからアカウント作成が可能でした。

ユーザーを作成したら、下記のアクセストークン発行ページのURLにアクセスし「Create token」からトークンを発行します。

Create a New Project

では、事前準備ができたのでプロジェクトを作成していきます。

以下のとおりディレクトリを作成して、プロジェクトの初期化を行います。なお、pulumiの初回利用時には初期化時に先程作成したアクセストークンを聞かれるので、入力をして進めます。

% mkdir quickstart && cd quickstart
% pulumi new gcp-typescript
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                   : 


  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 of the day: 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.

続けて、プロジェクトの設定を入力していきます。基本的にそのままENTERで進めますが、プロジェクト名は自身のGoogle Cloudのプロジェクト名を指定します。

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 TypeScript Pulumi program) 
Created project 'quickstart'

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) 
Created stack 'dev'

gcp:project: The Google Cloud project to deploy into: foo-bar
Saved config

あとはインストールが進むのを待ちます。

Installing dependencies...


added 115 packages, and audited 116 packages in 16s

33 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities
npm notice 
npm notice New minor version of npm available! 8.5.0 -> 8.7.0
npm notice Changelog: https://github.com/npm/cli/releases/tag/v8.7.0
npm notice Run npm install -g npm@8.7.0 to update!
npm notice 
Finished installing dependencies

Your new project is ready to go! ✨

To perform an initial deployment, run 'pulumi up'

完了しました。

Review the New Project

以下の3つのファイルが作成され、yamlファイルは想定どおりの設定になっていることを確認します。

  • Pulumi.yaml
  • Pulumi.dev.yaml
  • index.ts

設定ファイルは以下のようになっていました。

Pulumi.yaml

name: quickstart
runtime: nodejs
description: A minimal Google Cloud TypeScript Pulumi program

Pulumi.dev.yaml

config:
  gcp:project: "foo-bar"

また、リソース構築用のスクリプトは以下のようになっています。

index.ts

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

// Create a GCP resource (Storage Bucket)
const bucket = new gcp.storage.Bucket("my-bucket", {
    location: "US"
});

// Export the DNS name of the bucket
export const bucketName = bucket.url;

今回は、バケット名とロケーションを変更してみます。

index.ts

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

// Create a GCP resource (Storage Bucket)
const bucket = new gcp.storage.Bucket("cm-ootaka-get-started-with-pulumi", {
    location: "ASIA"
});

// Export the DNS name of the bucket
export const bucketName = bucket.url;

Deploy the Stack

準備ができたのでデプロイに進みます。

以下のpulumi upコマンドを実行することで、何が作成されるかを表示されます。

% pulumi up
Previewing update (dev)

View Live: https://app.pulumi.com/ootaka-daisuke/quickstart/dev/previews/5fb5d79e-604b-4eca-8afe-957b82c8f912

     Type                   Name                                      Plan       
 +   pulumi:pulumi:Stack    quickstart-dev                            create     
 +   └─ gcp:storage:Bucket  cm-ootaka-get-started-with-pulumi         create     
 
Resources:
    + 2 to create

Do you want to perform this update?  [Use arrows to move, enter to select, type to filter]
  yes
> no
  details

問題がなければカーソルをyesに合わせて続けます。

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

View Live: https://app.pulumi.com/ootaka-daisuke/quickstart/dev/updates/2

     Type                   Name                                      Status      
     pulumi:pulumi:Stack    quickstart-dev                                        
 +   └─ gcp:storage:Bucket  cm-ootaka-get-started-with-pulumi         created     
 
Outputs:
  + bucketName: "gs://cm-ootaka-get-started-with-pulumi-b23262a"

Resources:
    + 1 created
    1 unchanged

Duration: 5s

GCSバケットが作成されました!なお、作成したリソースはOutputsに表示されており、以下のコマンドでも確認できます。

% pulumi stack output bucketName
gs://cm-ootaka-get-started-with-pulumi-b23262a

Modify the Program

次に、プログラムを修正して静的サイトホスティングができるようにしていきます。

まずは以下のようなindex.htmlファイルを追加します。

index.html

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

ファイルを追加したので、プログラムもこのファイルをGCSバケットに配置するように修正します。

index.ts

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

// Create a GCP resource (Storage Bucket)
const bucket = new gcp.storage.Bucket("cm-ootaka-get-started-with-pulumi", {
    location: "ASIA"
});

const bucketObject = new gcp.storage.BucketObject("index.html", {
    bucket: bucket.name,
    source: new pulumi.asset.FileAsset("index.html")
});

// Export the DNS name of the bucket
export const bucketName = bucket.url;

Deploy the Changes

では、もう一度デプロイします。

index.htmlをデプロイする

% pulumi up
Previewing update (dev)

View Live: https://app.pulumi.com/ootaka-daisuke/quickstart/dev/previews/5096fe6b-e004-4a06-9a24-1073f3640036

     Type                         Name                                      Plan       
     pulumi:pulumi:Stack          quickstart-dev                                       
 +   └─ gcp:storage:BucketObject  index.html                                create     
 
Resources:
    + 1 to create
    2 unchanged

Do you want to perform this update?  [Use arrows to move, enter to select, type to filter]
  yes
> no
  details

yesを選択して適用します。

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

View Live: https://app.pulumi.com/ootaka-daisuke/quickstart/dev/updates/3

     Type                         Name                                      Status      
     pulumi:pulumi:Stack          quickstart-dev                                        
 +   └─ gcp:storage:BucketObject  index.html                                created     
 
Outputs:
    bucketName: "gs://cm-ootaka-get-started-with-pulumi-b23262a"

Resources:
    + 1 created
    2 unchanged

Duration: 3s

実際にgsutilコマンドで確認もできます。

% gsutil ls $(pulumi stack output bucketName)
gs://cm-ootaka-get-started-with-pulumi-b23262a/index.html-a277eb1

ちゃんとファイルが置かれていますね。

静的サイトホスティングをする

続けて、改めて静的サイトホスティングがされるようにコードを修正していきます。

index.ts

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

// Create a GCP resource (Storage Bucket)
const bucket = new gcp.storage.Bucket("cm-ootaka-get-started-with-pulumi", {
  location: "ASIA",
  website: {
    mainPageSuffix: "index.html",
  },
  uniformBucketLevelAccess: true,
});

const bucketIAMBinding = new gcp.storage.BucketIAMBinding(
  "cm-ootaka-get-started-with-pulumi-IAMBinding",
  {
    bucket: bucket.name,
    role: "roles/storage.objectViewer",
    members: ["allUsers"],
  }
);

const bucketObject = new gcp.storage.BucketObject("index.html", {
  bucket: bucket.name,
  contentType: "text/html",
  source: new pulumi.asset.FileAsset("index.html"),
});

// Export the DNS name of the bucket
export const bucketName = bucket.url;

export const bucketEndpoint = pulumi.concat(
  "http://storage.googleapis.com/",
  bucket.name,
  "/",
  bucketObject.name
);

バケットへのパブリック・アクセスを許可し、index.htmlのコンテツタイプ指定を行っています。

また、bucketEndpointとしてウェブサイトにアクセスするためのエンドポイントURLを設定しています。

この状態で改めてデプロイします。

% pulumi up
Previewing update (dev)

View Live: https://app.pulumi.com/ootaka-daisuke/quickstart/dev/previews/fcfc13e3-e30d-40b7-a0c5-b966cfeeabc2

     Type                             Name                                          Plan        Info
     pulumi:pulumi:Stack              quickstart-dev                                            
 ~   ├─ gcp:storage:Bucket            cm-ootaka-get-started-with-pulumi             update      [diff: +website~uniformBucket
 +   ├─ gcp:storage:BucketIAMBinding  cm-ootaka-get-started-with-pulumi-IAMBinding  create      
 +-  └─ gcp:storage:BucketObject      index.html                                    replace     [diff: ~contentType]
 
Outputs:
  + bucketEndpoint: "http://storage.googleapis.com/cm-ootaka-get-started-with-pulumi-b23262a/index.html-990cebf"

Resources:
    + 1 to create
    ~ 1 to update
    +-1 to replace
    3 changes. 1 unchanged

Do you want to perform this update?  [Use arrows to move, enter to select, type to filter]
  yes
> no
  details

yesを選択します。

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

View Live: https://app.pulumi.com/ootaka-daisuke/quickstart/dev/updates/4

     Type                             Name                                          Status                   Info
     pulumi:pulumi:Stack              quickstart-dev                                **failed**               1 error
 ~   ├─ gcp:storage:Bucket            cm-ootaka-get-started-with-pulumi             updated                  [diff: +website~uniformBucketLevelAccess]
 +   ├─ gcp:storage:BucketIAMBinding  cm-ootaka-get-started-with-pulumi-IAMBinding  created                  
 +-  └─ gcp:storage:BucketObject      index.html                                    **replacing failed**     [diff: ~contentType]; 1 error
 
Diagnostics:
  pulumi:pulumi:Stack (quickstart-dev):
    error: update failed
 
  gcp:storage:BucketObject (index.html):
    error: 1 error occurred:
        * Error when reading or editing Storage Bucket Object "index.html-3dd354f": googleapi: Error 403: xxxxx does not have storage.objects.get access to the Google Cloud Storage object., forbidden
 
Outputs:
  - bucketName: "gs://cm-ootaka-get-started-with-pulumi-b23262a"

Resources:
    + 1 created
    ~ 1 updated
    2 changes. 1 unchanged

Duration: 10s

失敗してしまいました。ストレージ権限を設定していたつもりでしたが不足していたようなので、改めて権限を付与し直してリトライします。

% pulumi up
Previewing update (dev)

View Live: https://app.pulumi.com/ootaka-daisuke/quickstart/dev/previews/9efcbcd4-9175-40d3-8c6c-873f693566ec

     Type                         Name                                      Plan        Info
     pulumi:pulumi:Stack          quickstart-dev                                        
 +-  └─ gcp:storage:BucketObject  index.html                                replace     [diff: ~contentType]
 
Outputs:
  + bucketEndpoint: "http://storage.googleapis.com/cm-ootaka-get-started-with-pulumi-b23262a/index.html-8d16e39"

Resources:
    +-1 to replace
    3 unchanged

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

View Live: https://app.pulumi.com/ootaka-daisuke/quickstart/dev/updates/5

     Type                         Name                                      Status       Info
     pulumi:pulumi:Stack          quickstart-dev                                         
 +-  └─ gcp:storage:BucketObject  index.html                                replaced     [diff: ~contentType]
 
Outputs:
  + bucketEndpoint: "http://storage.googleapis.com/cm-ootaka-get-started-with-pulumi-b23262a/index.html-d3b904b"
    bucketName    : "gs://cm-ootaka-get-started-with-pulumi-b23262a"

Resources:
    +-1 replaced
    3 unchanged

Duration: 3s

今度は成功です!curlコマンドで確認してみましょう。

% curl $(pulumi stack output bucketEndpoint)
<html>
    <body>
        <h1>Hello, Pulumi!</h1>
    </body>
</html>

想定どおり、ホスティングされました!

Destroy the Stack

最後に後片付けをします。

以下のコマンドで作成したリソースを削除します。

% pulumi destroy
Previewing destroy (dev)

View Live: https://app.pulumi.com/ootaka-daisuke/quickstart/dev/previews/7ee572e9-1c8f-476b-8362-9347ee75f12a

     Type                             Name                                          Plan       
 -   pulumi:pulumi:Stack              quickstart-dev                                delete     
 -   ├─ gcp:storage:BucketIAMBinding  cm-ootaka-get-started-with-pulumi-IAMBinding  delete     
 -   ├─ gcp:storage:BucketObject      index.html                                    delete     
 -   └─ gcp:storage:Bucket            cm-ootaka-get-started-with-pulumi             delete     
 
Outputs:
  - bucketEndpoint: "http://storage.googleapis.com/cm-ootaka-get-started-with-pulumi-b23262a/index.html-d3b904b"
  - bucketName    : "gs://cm-ootaka-get-started-with-pulumi-b23262a"

Resources:
    - 4 to delete

Do you want to perform this destroy?  [Use arrows to move, enter to select, type to filter]
  yes
> no
  details

yesを選択します。

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

View Live: https://app.pulumi.com/ootaka-daisuke/quickstart/dev/updates/6

     Type                             Name                                          Status                  Info
     pulumi:pulumi:Stack              quickstart-dev                                **failed**              1 error
 -   ├─ gcp:storage:BucketIAMBinding  cm-ootaka-get-started-with-pulumi-IAMBinding  deleted                 
 -   ├─ gcp:storage:BucketObject      index.html                                    deleted                 
 -   └─ gcp:storage:Bucket            cm-ootaka-get-started-with-pulumi             **deleting failed**     1 error
 
Diagnostics:
  pulumi:pulumi:Stack (quickstart-dev):
    error: update failed
 
  gcp:storage:Bucket (cm-ootaka-get-started-with-pulumi):
    error: deleting urn:pulumi:dev::quickstart::gcp:storage/bucket:Bucket::cm-ootaka-get-started-with-pulumi: 1 error occurred:
        * Error trying to delete bucket cm-ootaka-get-started-with-pulumi-b23262a containing objects without `force_destroy` set to true
 
Resources:
    - 2 deleted

Duration: 8s

また失敗してしまったようです。

エラーメッセージに記載のあるとおり、バケットのforceDestroyオプションをtrueに設定します。

index.ts

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

// Create a GCP resource (Storage Bucket)
const bucket = new gcp.storage.Bucket("cm-ootaka-get-started-with-pulumi", {
  location: "ASIA",
  website: {
    mainPageSuffix: "index.html",
  },
  uniformBucketLevelAccess: true,
  forceDestroy: true
});

(...snip...)

次に、pulumi upで適用してからpulumi destroyを実行しました。

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

View Live: https://app.pulumi.com/ootaka-daisuke/quickstart/dev/updates/9

     Type                             Name                                          Status      
 -   pulumi:pulumi:Stack              quickstart-dev                                deleted     
 -   ├─ gcp:storage:BucketObject      index.html                                    deleted     
 -   ├─ gcp:storage:BucketIAMBinding  cm-ootaka-get-started-with-pulumi-IAMBinding  deleted     
 -   └─ gcp:storage:Bucket            cm-ootaka-get-started-with-pulumi             deleted     
 
Outputs:
  - bucketEndpoint: "http://storage.googleapis.com/cm-ootaka-get-started-with-pulumi-b23262a/index.html-147943a"
  - bucketName    : "gs://cm-ootaka-get-started-with-pulumi-b23262a"

Resources:
    - 4 deleted

Duration: 10s

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'.

今度は想定どおり削除されましたね!

念の為、以下のコマンドで削除されているか確かめます。

% gsutil ls gs://cm-ootaka-get-started-with-pulumi-b23262a
BucketNotFoundException: 404 gs://cm-ootaka-get-started-with-pulumi-b23262a bucket does not exist.

ちゃんと削除されていますね。

まとめ

以上、PulumiのGet Started with Google Cloudを試してみました。

Get Started with AWSと同様に、違和感なく試すことができました。注意点として、S3とは違いGCSのバケットオプションとしてforceDestroyを設定しておかないとバケット削除時にエラーになるようです。ここは、安全装置の側面もあるので適切なオプション設定が必要ですね。

どなたかのお役に立てば幸いです。それでは!