インフラ構成ツール Pulumi を試してみた #Pulumi #GCP #Go
この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。
terraformやAnsible、AWS CloudFormationなど、クラウドリソースオーケストレーションツール(または構成管理ツール)の類似ツールとなる Pulumi について、公式ドキュメントのチュートリアルを実施してみましたので、こちらにまとめます。Pulumiのざっくりとしたarchitectureや既存クラウドリソースのPulumi管理方法については、こちらのblogでまとめていますので、よろしければご参考ください。
目次
Pulumiとは
Pulumiの概要 (参考)
Build, deploy, and manage modern cloud applications and infrastructure using familiar languages, tools, and engineering practices.
引用:Pulumi公式HPより
infrastructure as code プラットフォームのPulumiは普段開発エンジニアやインフラエンジニアが慣れ親しんでいる開発言語(Go/C#/Python/TypeScriptなど)やマークアップ言語(YAMLなど)で数多くのクラウドインフラリソースやクラウドアプリケーションをビルド/デプロイすることが可能となり、リソースの最終的なあるべき姿(冪等性)を好きな言語で表現することができます。仮にインフラリソースの管理でTerraformを利用するとなった場合はHCL(HashiCorp Configuration Language)について1から学習したり、Ansibleの場合はPlaybookなどの仕組みを理解したりと、それぞれのツールである程度の学習コストがかかるところを、Pulumiの場合は各々の開発言語が普段パッケージを扱うようにPulumiを利用できるため、学習コストが少なくて済み、かつプログラマブルな処理(ループの記載/特定インフラの関数化 etc…)が組みやすくなっています。またPulumiはGCP/AWS/Microsoft Azureなどのマルチクラウド管理や、kubernetesのdeployも対応しています。その他PulumiのGUIコンソール画面からソースコードのビルド、テスト、デプロイまでの一通りのパイプライン制御や、auditログの確認、RBAC制御やサポート(有償)などのサービスも利用可能です。 Pulumiを導入した企業の一部として、Atlassian、SANS、Skai、Mercedes-Benzなどが紹介されていて、GartnerよりPulumiが「Cool Vendor in the May 2020 Gartner Cool Vendors in Agile and DevOps」として認識されるなど、ここ数年で海外での注目度も上がっています。
Pulumiの主な構成 (参考)
Pulumiの主な構成は以下になります(公式HPより引用)
簡単にまとめると以下になります。
- Program → インフラストラクチャのあるべき姿を定義したもの
- Resource → インフラストラクチャを構成するオブジェクト(実際のインフラリソース)。オブジェクトのプロパティ(設定値)は他のオブジェクトに共有することが可能(図のInputs/Outputsの部分)
- Project → Programのソースコードとメタデータ(どのようにProgramを動かすかの情報)を格納するディレクトリ
- Stack → Projectディレクトリ内でProgramを「pulumi up」(Pulumi CLI) した後のインスタンス。同一のProgramから本番環境/ステージング環境など、用途に応じて複数の環境用にインスタンスを作成が可能
また、公式HPの図には記載されていませんが、Projectの上位の構成としてOrganizationを作成することもできます。Organizationは複数のProjectを1つにまとめ、Organizationに紐づいているメンバーにProjectへのアクセス権限を与えたり、Organization単位でのポリシー設定や料金管理をすることが可能です。
Pulumiの料金 (参考)
2023年1月17日現在
Pulumiはオープンソースとなり個人利用する分には料金は掛かりません。また15人までの複数人利用プラン(Teamプラン)では150kクレジットまで毎月無料で利用できます。15人を超える複数人利用や、企業単位で利用する場合には以下の有償プランが用意されています。料金やプラン内容について今後変動する可能性もあるので、実際に利用される際は公式HPをご確認ください。
Individual | Team | Enterprise | BusinessCritical |
---|---|---|---|
無料 | $1/2000クレジット 150Kクレジット/月まで無料 (毎月200リソース程度) |
要問い合わせ | 要問い合わせ |
|
Individualプランに以下追加
|
Teamプランに以下追加
|
|
※ 1 クレジット → Pulumi Programに定義された 1 つのリソースを 1 時間利用した時の 1 単位
実際に触ってみた
前提
今回はローカル開発環境(macOS)を利用してGoでGCPリソース(CloudStorageBucket)のデプロイを実施したいと思います。デプロイ後はhtmlファイルのbucketへの配置もPulumiで実施して、簡易な静的Webサーバーの動作確認を行います。基本的にデプロイ手順は公式HPに記載の手順に沿って実施します。
- macOS v11.6.4
- go1.18.3 darwin/amd64
- Pulumi v3.35.3
前準備 (参考)
- PulumiをmacOSにインストール
brew install pulumi/tap/pulumi
- Go公式HPに記載の手順に従ってインストール実施しました
- GCPコンソールにログイン後、今回は新しく「pulumi-test」projectを作成しました
- ローカル環境での開発となるのでGoogleCloudSDKをmacOSのターミナルにをあらかじめインストールしておきます
- GCPリソースの作成はPulumiが利用するGCPのServiceAccountと、適切なIAM設定が必要になります。今回はServiceAccount(pulumi-test-sa)に、GCS Bucket作成で必要なrole(roles/storage.admin)を付与しました。
$ gcloud iam service-accounts create pulumi-test-sa Created service account [pulumi-test-sa]. $ gcloud projects add-iam-policy-binding pulumi-test --member='pulumi-test-sa@pulumi-test-364012.iam.gserviceaccount.com' --role='roles/storage.admin' Updated IAM policy for project [pulumi-test]. bindings: 〜 - members: - serviceAccount:pulumi-test-sa@pulumi-test-364012.iam.gserviceaccount.com role: roles/storage.admin 〜
- gcloudのdefault application credentialsを設定します
$ gcloud auth application-default login --- -> CLIに表示されたリンクをクリックしてログイン
手順 (参考)
Pulumi Project 新規作成
- pulumiプロジェクト用のディレクトリを作成します。
$ mkdir quickstart && cd quickstart
$ pulumi new gcp-go 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 to log in using your browser :
$ pulumi new gcp-go 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 to log in using your browser
※ちなみに、pulumi newコマンドはProjectとStackをあらかじめ用意されたテンプレートから作成するコマンドになりますが、テンプレート名が分からない場合は引数を指定せず「pulumi new」コマンドを実行すると、以下のようにテンプレート一覧が表示されるので、この中から目的のテンプレートを選択することが可能です。
$ pulumi new [Use arrows to move, enter to select, type to filter] 〜〜(略)〜〜 gcp-csharp A minimal Google Cloud C# Pulumi program gcp-fsharp A minimal GCP F# Pulumi program > gcp-go A minimal Google Cloud Go Pulumi program gcp-java A minimal Google Cloud Java Pulumi program gcp-javascript A minimal Google Cloud JavaScript Pulumi program gcp-python A minimal Google Cloud Python Pulumi program gcp-typescript A minimal Google Cloud TypeScript Pulumi program gcp-visualbasic A minimal GCP VB.NET Pulumi program gcp-yaml A minimal Google Cloud Pulumi YAML program 〜〜(略)〜〜
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 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. This command will walk you through creating a new Pulumi project. Enter a value or leave blank to accept the (default), and press . Press ^C at any time to quit. project name: (quickstart) project description: (A minimal Google Cloud Go Pulumi program) Created project 'quickstart' Please enter your desired stack name. To create a stack in an organization, use the format / (e.g. `acmecorp/dev`). stack name: (dev) Created stack 'dev'
gcp:project: The Google Cloud project to deploy into: pulumi-test Saved config Installing dependencies... go: downloading github.com/pulumi/pulumi-gcp/sdk/v6 v6.26.0 go: downloading github.com/pulumi/pulumi/sdk/v3 v3.34.1 go: downloading github.com/blang/semver v3.5.1+incompatible go: downloading golang.org/x/net v0.0.0-20201021035429-f5854403a974 〜〜(略)〜〜 go: downloading github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 go: downloading github.com/kr/text v0.2.0 go: downloading github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 Finished installing dependencies Your new project is ready to go! ✨ To perform an initial deployment, run 'pulumi up'
Program 新規作成
- 先ほど新しくPulumi Projectを作成した際に生成されたファイルは以下の通りとなります
- Pulumi.yaml → Pulumi Projectの定義ファイル
- Pulumi.dev.yaml → stackの設定ファイル
- main.go → stack resourceを定義するPulumi Program
- 「main.go」に以下コードを記載して、実際のstack resource (storage bucket)を定義します。作成したstorage bucketのbucket nameを出力させます。
package main import ( "github.com/pulumi/pulumi-gcp/sdk/v6/go/gcp/storage" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" ) func main() { pulumi.Run(func(ctx *pulumi.Context) error { // Create a GCP resource (Storage Bucket) bucket, err := storage.NewBucket(ctx, "my-bucket", &storage.BucketArgs{ Location: pulumi.String("US"), }) if err != nil { return err } // Export the DNS name of the bucket ctx.Export("bucketName", bucket.Url) return nil }) }
Stack デプロイ
- Programの準備が整ったらStack(dev)を実際にデプロイします。
$ pulumi up
Previewing update (dev) View Live: https://app.pulumi.com/CL_Kenneth/quickstart/dev/previews/bdc6be2e-****-****-****-****** [resource plugin gcp-6.26.0] installing Downloading plugin: 40.40 MiB / 40.40 MiB [=========================] 100.00% 4s Type Name Plan + pulumi:pulumi:Stack quickstart-dev create + └─ gcp:storage:Bucket my-bucket 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
Do you want to perform this update? yes Updating (dev) View Live: https://app.pulumi.com/CL_Kenneth/quickstart/dev/updates/1 Type Name Status + pulumi:pulumi:Stack quickstart-dev created + └─ gcp:storage:Bucket my-bucket created Outputs: bucketName: "gs://my-bucket-198eadd" Resources: + 2 created Duration: 6s
$ pulumi stack output bucketName gs://(bucket dns name)
またはPulumi GUIからも確認できます。
Program 編集
- Storage BucketがPulumiを用いてデプロイ出来たので、今度はhtmlファイルをPulumiでBucketに格納します。
- Pulumi Project directory(quickstart)内で簡単なhtmlファイルを作成します。
cat <<EOT > index.html <html> <body> <h2>Hello, Pulumi!</h2> </body> </html> EOT
bucketObject, err := storage.NewBucketObject(ctx, "index.html", &storage.BucketObjectArgs{ Bucket: bucket.Name, Source: pulumi.NewFileAsset("index.html"), }) bucketEndpoint := pulumi.Sprintf("http://storage.googleapis.com/%s/%s", bucket.Name, bucketObject.Name) if err != nil { return err }
Program 変更 デプロイ
- 「main.go」Programを編集後は、StorageBucket新規作成時と同様に「pulumi up」を実施後、「yes」を選択します。
$ pulumi up --- Previewing update (dev) View Live: https://app.pulumi.com/CL_Kenneth/quickstart/dev/previews/a16a6685-****-****-****-****** 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
- 「pulumi up」時に「bucketEndpoint declared but not used」errorが出た場合は、一旦以下 bucketEndpoint変数をアンダースコア変数に格納します。
_ = bucketEndpoint
Do you want to perform this update? yes Updating (dev) View Live: https://app.pulumi.com/CL_Kenneth/quickstart/dev/updates/2 Type Name Status pulumi:pulumi:Stack quickstart-dev + └─ gcp:storage:BucketObject index.html created Outputs: bucketName: "gs://my-bucket-198eadd" Resources: + 1 created 2 unchanged Duration: 3s
$ gsutil ls $(pulumi stack output bucketName) --- gs://my-bucket-****/index.html-****
bucket, err := storage.NewBucket(ctx, "my-bucket", &storage.BucketArgs{ Location: pulumi.String("US"), Website: storage.BucketWebsiteArgs{ MainPageSuffix: pulumi.String("index.html"), }, UniformBucketLevelAccess: pulumi.Bool(true), })
_, err = storage.NewBucketIAMBinding(ctx, "my-bucket-IAMBinding", &storage.BucketIAMBindingArgs{ Bucket: bucket.Name, Role: pulumi.String("roles/storage.objectViewer"), Members: pulumi.StringArray{ pulumi.String("allUsers"), }, }) if err != nil { return err }
bucketObject, err := storage.NewBucketObject(ctx, "index.html", &storage.BucketObjectArgs{ Bucket: bucket.Name, ContentType: pulumi.String("text/html"), Source: pulumi.NewFileAsset("index.html"), })
※一時的に「bucketEndpoint」変数にアンダースコア変数を用いている場合はこのタイミングで削除します
ctx.Export("bucketEndpoint", bucketEndpoint)
$ pulumi up Previewing update (dev) View Live: https://app.pulumi.com/CL_Kenneth/quickstart/dev/previews/1bc49657-****-****-****-****** Type Name Plan Info pulumi:pulumi:Stack quickstart-dev ~ ├─ gcp:storage:Bucket my-bucket update [diff: +website~uniformBucket + ├─ gcp:storage:BucketIAMBinding my-bucket-IAMBinding create +- └─ gcp:storage:BucketObject index.html replace [diff: ~contentType] Outputs: + bucketEndpoint: "http://storage.googleapis.com/my-bucket-****/index.html-****" Resources: + 1 to create ~ 1 to update +-1 to replace 3 changes. 1 unchanged
detail (diff)
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-*****] [urn=urn:pulumi:dev::quickstart::gcp:storage/bucket:Bucket::my-bucket] ~ uniformBucketLevelAccess: false => true + website : { + mainPageSuffix: "index.html" } + gcp:storage/bucketIAMBinding:BucketIAMBinding: (create) [urn=urn:pulumi:dev::quickstart::gcp:storage/bucketIAMBinding:BucketIAMBinding::my-bucket-IAMBinding] bucket : "my-bucket-*****" members : [ [0]: "allUsers" ] role : "roles/storage.objectViewer" ++gcp:storage/bucketObject:BucketObject: (create-replacement) [id=my-bucket-****-index.html-****] [urn=urn:pulumi:dev::quickstart::gcp:storage/bucketObject:BucketObject::index.html] ~ contentType: "text/html; charset=utf-8" => "text/html" +-gcp:storage/bucketObject:BucketObject: (replace) [id=my-bucket-****-index.html-****] [urn=urn:pulumi:dev::quickstart::gcp:storage/bucketObject:BucketObject::index.html] ~ contentType: "text/html; charset=utf-8" => "text/html" --outputs:-- + bucketEndpoint: "http://storage.googleapis.com/my-bucket-*****/index.html-*****" --gcp:storage/bucketObject:BucketObject: (delete-replaced) [id=my-bucket-****-index.html-****] [urn=urn:pulumi:dev::quickstart::gcp:storage/bucketObject:BucketObject::index.html]
$ curl $(pulumi stack output bucketEndpoint) --- cat <<EOT > index.html <html> <body> <h2>Hello, Pulumi!</h2> </body> </html> EOT
Stack resource 削除
- 今回の検証で作成したStackのresourceを削除します。削除前に、bucketObjectを含んだままのbucketをそのまま削除出来るように、事前に「ForceDestroy: pulumi.Bool(true)」をProgramに追記して、「pulumi up」で一度更新します。
bucket, err := storage.NewBucket(ctx, "my-bucket", &storage.BucketArgs{ Location: pulumi.String("US"), Website: storage.BucketWebsiteArgs{ MainPageSuffix: pulumi.String("index.html"), }, UniformBucketLevelAccess: pulumi.Bool(true), ForceDestroy: pulumi.Bool(true), })
$ pulumi up Previewing update (dev) View Live: https://app.pulumi.com/CL_Kenneth/quickstart2/dev/previews/********-****-****-****-*********** Type Name Plan Info pulumi:pulumi:Stack quickstart2-dev ~ └─ gcp:storage:Bucket my-bucket update [diff: ~forceDestroy] Resources: ~ 1 to update 3 unchanged Do you want to perform this update? [Use arrows to move, enter to select, type to filter] > yes no details
$ pulumi destroy --- Previewing destroy (dev) View Live: https://app.pulumi.com/CL_Kenneth/quickstart/dev/previews/********-****-****-****-*********** Type Name Plan - pulumi:pulumi:Stack quickstart-dev delete - ├─ gcp:storage:BucketIAMBinding my-bucket-IAMBinding delete - ├─ gcp:storage:BucketObject index.html delete - └─ gcp:storage:Bucket my-bucket delete Outputs: - bucketEndpoint: "http://storage.googleapis.com/my-bucket-198eadd/index.html-******" - bucketName : "gs://my-bucket-198eadd" Resources: - 4 to delete
Stack 削除
- Stack自体に今までのstate情報(現在のresourceの状態や、どのresourceがどの時点で作成/削除されたかなどの履歴)が含まれているので、Stack自体を削除するとそれらのstate情報も削除されます。
- 削除前に現在のStackリストを表示。
$ pulumi stack ls NAME LAST UPDATE RESOURCE COUNT URL dev* 21 hours ago 3 https://app.pulumi.com/CL_Kenneth/quickstart/dev
$ pulumi stack rm dev This will permanently remove the 'dev' stack! Please confirm that this is what you'd like to do by typing ("dev"): dev Stack 'dev' has been removed!
$ pulumi stack ls NAME LAST UPDATE RESOURCE COUNT URL
まとめ
普段慣れ親しんだ言語でインフラリソースを管理できるのはとても便利だと思いました。本blogでは触れていないPulumiのその他の機能で、CI/CDのインテグレーションや監査ログ機能などの便利な機能や、PulumiのGUIについて今後も深掘りしてみたいと思います。