はじめに
OpenStandiaアドベントカレンダー16日目になります。
今回は、Infrastructure as Code(IaC)ツールで気になっていた Pulumi について紹介します。
私自身、AWS Cloud Development Kit(AWS CDK)や Azure Bicep を触れたことがありますが、クラウドに縛られない IaC ツールには触れたことがありませんでした。
クラウドに縛られない IaC ツールの例としては、Terraform が有名かと思いますが、個人的には TypeScript で書きたい派なので、以前から Pulumi に興味を持っていました。
そこで今回は、Pulumiを使いながら、その特徴や使い方について色々と触れてみたいと思います。
Pulumiとは
Pulumi は、IaC ツールの一つで、プログラミング言語(TypeScript、JavaScript、Python、Go、.NET、Java、YAML)を使用して、クラウド環境のプロビジョニング、更新、管理を行うことができます。
Terraform と同様、クラウド環境に縛られず、AWS、Azure、Google Cloud など複数のクラウドプロバイダーやオンプレミス環境にも対応しており、統一されたインターフェースでインフラをコードとして管理できます。
PulumiとTeraformの違いについては、以下を参照して頂ければと思います。
Pulumiの構成
さっそくですが、Pulumiの構成を見ていきます。
Pulumiの構成は以下になります。
引用:https://www.pulumi.com/docs/iac/concepts/
- Project
- Programのソースコードとメタデータを格納するディレクトリ
- Program
- インフラストラクチャのあるべき構成を定義
- Resource
- インフラストラクチャを構成するオブジェクト。オブジェクトの設定値は、他のオブジェクトと共有可能
- Stack
- Project ディレクトリ内で Program を pulumi up(Pulumi CLI)した後のインスタンス。本番環境やステージング環境など、環境に応じてインスタンスを作成可能。
Pulumiの動作
公式ドキュメントによると、Pulumiの動作は以下のようになります。
引用:https://www.pulumi.com/docs/intro/concepts/how-pulumi-works/
上記の図に出てくる用語の定義は以下の通りです。
- Language Host
- Pulumiプログラムを実行し、デプロイメントエンジンにリソースを登録できる環境
- Pulumiプロジェクトに相当
- CLI and Engine(Deployment Engine)
- Deployment Engine は Pulumi CLI 自体に組み込まれている
- インフラストラクチャの現在の状態(State)をプログラムによって表現されるStateにするため必要な操作を計算する役割
Pulumi で定義したソースコードはpulumi up
コマンドを実行することで、各プロバイダー(AWS/Azure/GCP/Kubernetes)にデプロイされます。
pulumi up
コマンド実行時のPulumiの内部的な動作はざっくりですが、以下のようになります。
- Language Host がリソースを Deployment Engine に登録
- Deployment Engine は最新のデプロイ情報(State)を確認し、ソースコードで定義された状態と比較
- 比較結果に基づいて、クラウド上のリソースの作成・更新・削除を行い、その結果をデプロイの最終状態(State)として保存
Pulumi Chalenge
ここまで Pulumi の構成や動作について説明してきましたが、Pulumi は Pulumi AI というサービスも提供しています。
Pulumi AI は生成 AI を活用して、自然言語の指示に基づき Pulumi のコードを自動生成することが可能です。
今回は、Pulumi Challenge という Pulumi AI を学べるワークショップを通じて、Pulumi を触っていきます。
セットアップ
まずは環境のセットアップです。今回は、AWS環境を使用するので、AWSのアクセスキーとシークレットアクセスキー、Open AIのキーを設定します。
export AWS_ACCESS_KEY_ID=<YOUR_ACCESS_KEY_ID>
export AWS_SECRET_ACCESS_KEY=<YOUR_SECRET_ACCESS_KEY>
export OPENAI_API_KEY=<YOUR_API_KEY>
Pulumiのアカウントは作成済みということを前提としています。
Pulumi AI
以下のサイトにアクセスし、Pulumi AIを利用します。
Deploy a Kinesis Data Stream, RDS database, DynamoDB database, and Timestream database.
とコンソールに入力します。入力後、Pulumi AIが以下のコードを生成してくれました。
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Create a Kinesis Data Stream
const kinesisStream = new aws.kinesis.Stream("myKinesisStream", {
shardCount: 1,
});
// Create an RDS database
const dbSubnetGroup = new aws.rds.SubnetGroup("myDbSubnetGroup", {
subnetIds: ["subnet-12345678", "subnet-87654321"], // Replace with your subnet IDs
});
const rdsInstance = new aws.rds.Instance("myRdsInstance", {
instanceClass: "db.t3.micro",
allocatedStorage: 20,
engine: "mysql",
engineVersion: "8.0",
username: "admin",
password: "password", // Replace with a secure password
dbSubnetGroupName: dbSubnetGroup.name,
skipFinalSnapshot: true,
});
// Create a DynamoDB database
const dynamoDbTable = new aws.dynamodb.Table("myDynamoDbTable", {
attributes: [
{ name: "Id", type: "S" },
],
hashKey: "Id",
billingMode: "PAY_PER_REQUEST",
});
// Create a Timestream database
const timestreamDatabase = new aws.timestreamwrite.Database("myTimestreamDatabase", {});
export const kinesisStreamName = kinesisStream.name;
export const rdsEndpoint = rdsInstance.endpoint;
export const dynamoDbTableName = dynamoDbTable.name;
export const timestreamDbName = timestreamDatabase.databaseName;
Pulumiプロジェクトの作成
次に、Pulumi AI が生成したコードをデプロイするために、Pulumi のプロジェクトを作成します。
以下のコマンドで Pulumi プロジェクトを作成できます。
mkdir ai-challenge(任意名) && cd ai-challenge(任意名)
pulumi new aws-typescript
今回は例でTypeScriptを選択していますが、他の言語(JavaScript、Python、Go、.NET、Java、YAML)でも選択可能です。
Pulumiプロジェクトを作成すると、index.ts
に先ほどのPulumi AIが作成したコードを貼り付けます。
デプロイ
ここまで進めると、実際に Pulumi で定義したリソースをデプロイできるようになります。
Pulumi では、pulumi up
コマンドを使用してリソースをデプロイすることが可能です。
しかし、上記のコードをデプロイすると、以下のエラーが発生しました。
pulumi up
Previewing update (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/yasomaru/ai-challenge/dev/previews/a4702df2-1be4-47cc-a4f6-5308c01b7cbb
Type Name Plan Info
+ pulumi:pulumi:Stack ai-challenge-dev create 1 error
Diagnostics:
pulumi:pulumi:Stack (ai-challenge-dev):
error: Running program '/Users/syasoda/development/pulumi/ai-challenge/index.ts' failed with an unhandled exception:
TSError: ⨯ Unable to compile TypeScript:
index.ts(35,85): error TS2345: Argument of type '{}' is not assignable to parameter of type 'DatabaseArgs'.
Property 'databaseName' is missing in type '{}' but required in type 'DatabaseArgs'.
このエラーはDatabaseArgs
の型には、databaseName
が必須のプロパティとして定義されており、空オブジェクト{}
を、DatabaseArgs
の型に渡しているためエラーが出ています。
このエラーを解消するために、以下のようにdatabaseName
プロパティを設定します。
// Create a Timestream database
const timestreamDatabase = new aws.timestream.Database("myTimestreamDatabase", {
-
+ databaseName: "myTimestreamDatabase" // Adding the required databaseName property
});
export const kinesisStreamName = kinesisStream.name;
こちらを修正し、再度デプロイ(pulumi up
)すると別のエラーが出ました。
Diagnostics:
aws:rds:Instance (myRdsInstance):
error: aws:rds/instance:Instance resource 'myRdsInstance' has a problem: only lowercase alphanumeric characters and hyphens allowed in "identifier". Examine values at 'myRdsInstance.identifier'.
RDSインスタンスの識別子には、英小文字、数字、ハイフン(-)のみが使用可能なのに対し、大文字を含んでいるためエラーが出ています。
以下のように、RDSインスタンスの識別子を変更します。
const rdsInstance = new aws.rds.Instance(
- "myRdsInstance"
+ "my-rds-instance", {
instanceClass: "db.t3.micro",
allocatedStorage: 20,
engine: "mysql",
engineVersion: "8.0",
username: "admin",
password: "password", // Replace with a secure password
dbSubnetGroupName: dbSubnetGroup.name,
skipFinalSnapshot: true,
});
最終ソースコード
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Create a Kinesis Data Stream
const kinesisStream = new aws.kinesis.Stream("myKinesisStream", {
shardCount: 1,
});
// Create an RDS database
const dbSubnetGroup = new aws.rds.SubnetGroup("my-db-subnet-group", {
subnetIds: ["subnet-12345678", "subnet-87654321"], // Replace with your subnet IDs
});
const rdsInstance = new aws.rds.Instance("my-rds-instance", {
instanceClass: "db.t3.micro",
allocatedStorage: 20,
engine: "mysql",
engineVersion: "8.0",
username: "admin",
password: "password", // Replace with a secure password
dbSubnetGroupName: dbSubnetGroup.name,
skipFinalSnapshot: true,
});
// Create a DynamoDB database
const dynamoDbTable = new aws.dynamodb.Table("my-dynamodb-table", {
attributes: [
{ name: "Id", type: "S" },
],
hashKey: "Id",
billingMode: "PAY_PER_REQUEST",
});
// Create a Timestream database
const timestreamDatabase = new aws.timestreamwrite.Database("my-timestream-database", {
databaseName: "myTimestreamDatabase"
});
export const kinesisStreamName = kinesisStream.name;
export const rdsEndpoint = rdsInstance.endpoint;
export const dynamoDbTableName = dynamoDbTable.name;
export const timestreamDbName = timestreamDatabase.databaseName;
サブネットグループのサブネットIDは、各々のサブネットのIDに書き換える必要があるので注意してください。
const dbSubnetGroup = new aws.rds.SubnetGroup("my-db-subnet-group", {
+ subnetIds: ["subnet-12345678", "subnet-87654321"], // Replace with your subnet IDs
});
修正後、再度pulumi up
コマンドを実施し、リソースをデプロイします。
% pulumi up
Previewing update (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/yasomaru/ai-challenge/dev/previews/86248c66-1ab8-4ea2-8f62-4e53054442d1
Type Name Plan
+ pulumi:pulumi:Stack ai-challenge-dev create
+ ├─ aws:timestreamwrite:Database my-timestream-database create
+ ├─ aws:dynamodb:Table my-dynamodb-table create
+ ├─ aws:rds:SubnetGroup my-db-subnet-group create
+ ├─ aws:kinesis:Stream myKinesisStream create
+ └─ aws:rds:Instance my-rds-instance create
Outputs:
dynamoDbTableName: "my-dynamodb-table-ad07034"
kinesisStreamName: "myKinesisStream-09b754a"
rdsEndpoint : output<string>
timestreamDbName : "myTimestreamDatabase"
Resources:
+ 6 to create
Do you want to perform this update? yes
Updating (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/yasomaru/ai-challenge/dev/updates/6
Type Name Status
+ pulumi:pulumi:Stack ai-challenge-dev created (267s)
+ ├─ aws:rds:SubnetGroup my-db-subnet-group created (2s)
+ ├─ aws:dynamodb:Table my-dynamodb-table created (15s)
+ ├─ aws:timestreamwrite:Database my-timestream-database created (2s)
+ ├─ aws:kinesis:Stream myKinesisStream created (23s)
+ └─ aws:rds:Instance my-rds-instance created (260s)
Outputs:
dynamoDbTableName: "my-dynamodb-table-8cbe815"
kinesisStreamName: "myKinesisStream-5e66aa4"
rdsEndpoint : "my-rds-instance4b0a2fb.cjm2qqq44112.us-east-1.rds.amazonaws.com:3306"
timestreamDbName : "myTimestreamDatabase"
Resources:
+ 6 created
Duration: 4m29s
pulumi up
コマンドを実行すると、まず更新のプレビューを実施します。
その後、更新が問題ないと判断されると、Do you want to perform this update?
と聞かれるのでYes
と回答することで、クラウド上へデプロイされます。
View in Browser
のリンクをブラウザで開いても、実行結果を確認することができます。
ドリフト検出
ここまで Pulumi を触っていて、気になったことがあります。
それは、AWS マネジメントコンソール側からリソースの設定を変更した場合、その差分を Pulumi が検出できるかです。
AWS の CloudFormation では、ドリフト検出という機能があり、コード以外から手動でクラウド上のリソースを変更した際に、コード側が実際のリソースとの差分を表示してくれます。
公式ドキュメントによると、pulumi refresh
でできそうです。
実際に、先ほど作成したDynamoDBのキャパシティーモードをオンデマンドからプロビジョンドに変更してみます。
その後、pulumi refresh
コマンドを実行してみます。
% pulumi refresh ✘ 255
Previewing refresh (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/yasomaru/ai-challenge/dev/previews/e18611bf-6bfb-404b-aeb3-4a1b6b8e3296
Type Name Plan Info
pulumi:pulumi:Stack ai-challenge-dev
~ ├─ aws:dynamodb:Table my-dynamodb-table update [diff: ~billingMode]
├─ aws:rds:Instance my-rds-instance
├─ aws:rds:SubnetGroup my-db-subnet-group
├─ aws:kinesis:Stream myKinesisStream
└─ aws:timestreamwrite:Database my-timestream-database
Resources:
~ 1 to update
5 unchanged
Do you want to perform this refresh?
No resources will be modified as part of this refresh; just your stack's state will be.
details
pulumi:pulumi:Stack: (same)
[urn=urn:pulumi:dev::ai-challenge::pulumi:pulumi:Stack::ai-challenge-dev]
~ aws:dynamodb/table:Table: (update)
[id=my-dynamodb-table-8cbe815]
[urn=urn:pulumi:dev::ai-challenge::aws:dynamodb/table:Table::my-dynamodb-table]
[provider=urn:pulumi:dev::ai-challenge::pulumi:providers:aws::default_6_61_0::f603c308-587a-4836-a768-8dddd6515fc0]
~ billingMode: "PAY_PER_REQUEST" => "PROVISIONED"
Do you want to perform this refresh?
No resources will be modified as part of this refresh; just your stack's state will be.
yes
Refreshing (dev)
View in Browser (Ctrl+O): https://app.pulumi.com/yasomaru/ai-challenge/dev/updates/9
Type Name Status Info
pulumi:pulumi:Stack ai-challenge-dev
├─ aws:rds:Instance my-rds-instance
~ ├─ aws:dynamodb:Table my-dynamodb-table updated (1s) [diff: ~billingMode]
├─ aws:timestreamwrite:Database my-timestream-database
├─ aws:rds:SubnetGroup my-db-subnet-group
└─ aws:kinesis:Stream myKinesisStream
Outputs:
dynamoDbTableName: "my-dynamodb-table-8cbe815"
kinesisStreamName: "myKinesisStream-5e66aa4"
rdsEndpoint : "my-rds-instance4b0a2fb.cjm2qqq44112.us-east-1.rds.amazonaws.com:3306"
timestreamDbName : "myTimestreamDatabase"
Resources:
~ 1 updated
5 unchanged
Duration: 6s
実行すると、変更したDynamoDBにupdate
と表示されているので差分を検出できていますね。
pulumi refresh
はPulumiの動作で紹介したPulumiのStateと実リソースの状態の同期を実行するコマンドとなるようです。
その後、Do you want to perform this update?
と聞かれるのでYes
と回答することでPulumiのStateが更新されます。
PulumiのStateを更新した状態で、pulumi preview --diff
と実行するとPulumiのStateとソースコードの差分を表示することができます。
% pulumi preview --diff ✘ 1
Previewing update (dev)
View Live: https://app.pulumi.com/yasomaru/ai-challenge/dev/previews/1af3dbda-c161-4b9e-a3b2-929efa071013
pulumi:pulumi:Stack: (same)
[urn=urn:pulumi:dev::ai-challenge::pulumi:pulumi:Stack::ai-challenge-dev]
~ aws:dynamodb/table:Table: (update)
[id=my-dynamodb-table-8cbe815]
[urn=urn:pulumi:dev::ai-challenge::aws:dynamodb/table:Table::my-dynamodb-table]
[provider=urn:pulumi:dev::ai-challenge::pulumi:providers:aws::default_6_61_0::f603c308-587a-4836-a768-8dddd6515fc0]
~ billingMode: "PROVISIONED" => "PAY_PER_REQUEST"
Resources:
~ 1 to update
5 unchanged
pulumi preview
はPulumi上のStateとソースコードで定義したResourceの内容との差分を表示してくれるコマンドとなるようです。
作成したリソースは以下のコマンドで削除できます。
% pulumi destory
さいごに
今回は個人的に気になっていたPulumiについて、調べてみました。
Pulumi Chalengeを通じて、Pulumiの基本的なコマンドの扱いや内部的な動きを理解することができました。
Pulumiには今回紹介したPulumi AIだけでなく、複数のソースに格納されたシークレットや変数、Configなどを統合管理できるPulumi ESCなど様々な機能があるので、色々触ってみたいと思います!