PythonやJavaScript、C#などでクラウドインフラを定義できる構成管理ツール「Pulumi」を使ってみる

昨今では、IaaSやPaaSといったさまざまなクラウドサービスが多くのシステムで使われている。こうしたクラウドサービスの多くはAPIを使ったインフラの操作をサポートしており、それらを活用するさまざまな支援ツールが登場している。今回はそういったツールの1つで、さまざまなプログラミング言語を使ってクラウド上のリソースを管理できる「Pulumi」を紹介する。

さまざまな言語でさまざまなクラウドプラットフォームを操作可能

昨今ではInfrastrcture as a Service(IaaS)やPaaS(Platform as a Service)といった、ソフトウェアを動かすためのインフラストラクチャ(インフラ)やプラットフォームを提供するクラウドサービスが広く普及している。こういったクラウドサービスには「使った分だけの料金支払いで済む」「ソフトウェアの実行環境や各種インフラを迅速に調達できる」といった利点に加えて、外部からインフラの操作を行えるAPIを利用してさまざまな処理を自動化できるという特徴もある。

そして、今日ではこうしたクラウドプラットフォームが提供するAPIを活用するツールが登場している。こういったAPIを活用するツールにはクラウド上のリソースを管理するものや監視を行うもの、ほかのサービスとの連携を行うものなどさまざまなものがあるが、その1つに設定管理のためのツール「Terraform」がある。Terraformについてはさくらのナレッジでも入門記事が公開されているが、簡単に言うと独自の書式でインフラの状態を記述することで、クラウド上のリソースをそれにあわせて自動的に作成できるというツールだ。

Terraformはすでに多くのユーザーを獲得しており、広く活用されているが、今回紹介するPulumiも、Terraformと同様にファイルに記述した情報を元にクラウド上のリソースを作成してくれるツールだ(図1)。

図1 PulumiのWebサイト

クラウド上のリソースの状態をファイルに記述してPulumiを実行すると、その通りの構成になるようリソースを自動的に作成してくれる、という点ではPulumiはTerraformと同じなのだが、Pulumiはリソースの状態を設定ファイルではなくプログラムとして定義する点が、Terraformとの大きな違いとなっている。

プログラミング言語を使ってインフラを記述するメリット

Pulumiでは現在JavaScriptおよびTypeScript、Pythonでのリソース記述をサポートしているほか、GoやC#についてもプレビューという段階でサポートが進められている。Pulumiではこれらの言語でリソースを定義するためのモジュール(ライブラリ)が提供されており、ソースコード中でこのモジュール(ライブラリ)を読み込み、作成したいリソースに対応するオブジェクトを作成することで作成するリソースやそれらの構成を定義するようになっている。

なお、Pulumiのモジュールが提供するのはクラウド上のリソースを定義する方法だけであって、そのモジュールによって直接リソースが作成されるわけではない。あくまでリソースを作成するのはPulumi本体であり、プログラムの記述後に「pulumi」コマンドを実行することで対応するリソースがクラウド上に作成されるようになっている。

このように独自の設定ファイルではなく、汎用のプログラミング言語を使ってインフラを記述できる点には、次のようなメリットがある。

  • ツール独自の設定ファイル文法などを覚える必要がない
  • 条件分岐などの処理を柔軟に記述できる
  • 外部アプリケーションや外部のデータとの連係をしやすい
  • プログラミング言語向けのツールによるサポートが期待できる

ただし、Pulumiはあくまでプログラミング言語とリソースの操作を結びつけるためのインターフェイスを提供するだけであり、利用するクラウドプラットフォームに関する知識は別途必要だ。また、Pulumiはさまざまなクラウドサービスに対応するが、それぞれのクラウドサービスごとにリソースを作成するために必要な情報は異なるため、複数のクラウドサービスを利用したい場合にはそれぞれのサービスごとに適切な流儀での記述が必要とある。そのため、たとえば単一のコードからAWSでもGCPでもAzureでも同じようにリソースを作成する、といったことが簡単にできるわけではない。

Pulumi社が提供するクラウドサービスとの連携機能も提供される

Pulumiのもう1つの特徴として、Pulumiを開発するPulumi社が提供するクラウドサービス(以下、「Pulumiサービス」と呼ぶ)上で設定や変更履歴などの管理を行える点がある。

Pulumiを使ってクラウド上にリソースを作成したり、それらを変更するような処理を実行すると、その履歴が自動的にPulumiサービスに送信され、Webブラウザ上でそれらの詳細を閲覧できるようになる(ローカルファイルのみで履歴を管理するように設定することも可能)。また、各種設定などもこのクラウド上で管理される。

Pulumiサービスは個人アカウントであれば無料で利用できるほか、チームでの開発・運用に向けた機能も有償で提供しており、これらを利用することでチームで変更履歴を管理できるようになる。ちなみに有償プランの料金は月額50ドルからという設定となっている(Pulumiの「Pricing」ページ)。また、Pulumiサービスでは継続的インテグレーション(CI)や継続的デリバリ(CD)との連携機能も提供しているほか、有償プランではWebフックを使ってクラウドへのリソース作成や変更時に通知などを送信できる機能も提供される。

なお、Pulumi自体はすべてオープンソースで提供されており、独自に履歴管理を行うようなサーバーを作成・運用することもできるという。また、「Enterprise」プランでは公式に履歴管理などのサービスを行うサーバーのセルフホスティング機能を提供している。

Pulumiが対応するクラウドサービス

Pulumiが現時点で公式にサポートしているクラウドサービスとしては下記が挙げられている。

  • Amazon Web Services(AWS)
  • Microsoft Azure
  • Google Cloud
  • Kubernetes

また、プラグインで対応するプラットフォームを拡充できる仕組みになっており、さくらのクラウドをPulumiで操作するためのプラグインも公開されている。

Pulumiのインストールと初期設定

それでは、実際にPulumiを使ってクラウド上のリソース管理を行う流れを紹介していこう。

pulumiコマンドのインストール

Pulumiは各種操作を実行するフロントエンドの「pulumi」コマンドと、各種プログラミング言語向けのモジュール(「Cloud Providers」)から構成されており、それぞれのインストールが必要となる。pulumiコマンドのインストール手順は、Pulumi公式サイトの「Download and Install」ページに記載されている。

このページではシェルスクリプトを使ったインストール方法やパッケージマネージャ経由でのインストール方法が説明されているが、ダウンロードページからバイナリを含むアーカイブを直接ダウンロードしても問題ない。なお、本記事では執筆時点での最新版である1.11.1を利用している。

このページで公開されているアーカイブは、LinuxおよびmacOS向けのものはtar.gz形式、Windows向けのものはZIP形式で圧縮されているので、適宜適当なディレクトリに展開する。アーカイブ中にはpulumiコマンドのほか複数のバイナリが含まれているので、これらを適当な(パスの通っている)ディレクトリに移動もしくはコピーすれば良い。

$ wget https://get.pulumi.com/releases/sdk/pulumi-v1.11.1-linux-x64.tar.gz
$ tar xvzf pulumi-v1.11.1-linux-x64.tar.gz
$ cd pulumi
$ ls -1
pulumi
pulumi-analyzer-policy
pulumi-language-dotnet
pulumi-language-go
pulumi-language-nodejs
pulumi-language-python
pulumi-language-python-exec
pulumi-resource-pulumi-nodejs
pulumi-resource-pulumi-python

$ ./pulumi version
v1.11.1+dirty

必要なプログラミング言語環境の準備

前述のように、PulumiはTypeScriptもしくはJavaScript、Python、C#、Go言語を使ってリソースの定義を行う。そのため、使用する言語のコンパイラやランタイム環境が必要となる。TypeScriptもしくはJavaScriptを利用する場合はNode.js 8.0以降、PythonではPython 3.6以降、C#では.NET Core 3.0 SDK以降が必要だ。

なお、GoおよびC#はまだプレビュー段階であり公式にはサポートされていない。そのため、Kubernetesなど一部のプラットフォームについては、現時点ではこれら言語との組み合わせでは利用できない。

クラウドプラットフォームごとの事前準備

Pulumiは各クラウドプラットフォームの操作を行う際、それらプラットフォームが提供するフロントエンドを経由してAPIを呼び出す。そのため、事前に使用するクラウドプラットフォームのフロントエンド(Google Cloudの場合「gcloud」コマンド、Microsoft Azureの場合「az」コマンド、AWSの場合「aws」コマンド、Kubernetesの場合「kubectl」コマンド)でクラウドにアクセスできるよう設定を行っておく必要がある(さくらのクラウドの場合は後述する)。

Pulumiサービスへのログイン

Pulumiでは、同社が提供しているクラウドサービス(Pulumiサービス)上で履歴などを管理できるようになっている。この機能を利用するためには、まずPulumiサービスのアカウント作成とログインが必要となる。アカウントの作成は、Pulumiのサービスサイトから行える(図2)。

図2 Pulumiサービスのトップページ

ここではGitHubやGitLab、Atlassianのアカウントを使っての認証でログインができるほか、メールアドレスを登録してのアカウント作成も可能だ。使用する認証方法を選び、アカウントを作成しておこう。

ローカルマシンからのログイン

pulumiコマンドを使ってクラウドプラットフォームの管理を行うマシン上では、「pulumi login」コマンドを実行することでPulumiサービス上のアカウントとの紐付けが行われ、設定履歴などがそのアカウントに対して記録されるようになる。このとき、トークン文字列の入力が求められるが、これはPulumiサービス上の「https://app.pulumi.com/account/tokens」というURLにアクセスすることで作成できる(図3)。

図3 Pulumiサービスのアクセストークン作成ページ

ここで、「NEW ACCESS TOKEN」をクリックすると作成するトークンの説明を入力するフォームが表示されるので、適当なものを入力して「CREATE」をクリックする(図4)。

図4 「CREATE」をクリックすると説明文(description)を入力するフォームが表示される

するとトークン文字列が表示されるので、これをコピーしておこう(図5)。

図5 アクセストークン文字列が表示される

pulumiコマンドをインストールしたマシン上で「pulumi login」コマンドを実行すると、次のようにアクセストークンの入力が求められるので、ここでコピーしたトークンを入力して「Enter」を押すとログインが完了する。

$ pulumi login
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/programming-model/#autonaming.


Logged into pulumi.com as hylom (https://app.pulumi.com/hylom)

なお、Pulumiサービスで使用するアカウントを切り替えたいといった場合は「pulumi logout」コマンドでログアウトを行える。

$ pulumi logout

なお、Pulumiサービスとの連携をしないことも可能だ。その場合、次のように「--local」オプション付きで「pulumi login」コマンドを実行する。

$ pulumi login --local
Logged into centos4x4 as hylom (file://~)

この場合、ホームディレクトリ上に各種ログや設定が保存されるようになる。

KubernetesクラスタをPulumiで操作する

Pulumiではさまざまなクラウドプラットフォームをさまざまな言語で操作できるが、まずは一例として、Kubernetesクラスタ上へのアプリケーションのデプロイをPulumiで行う例を紹介しよう。

プロジェクトとStackの作成

Pulumiでは、「プロジェクト(Project)」および「Stack」という単位でリソースの管理を行うようになっている。プロジェクトはPulumiの設定やリソースを記述したソースコードなどを管理する単位で、またStackは各種設定などを管理する単位だ。たとえば開発向けには「development」、検証向けには「staging」、運用向けには「production」のように異なるStackを作成することで、同じコードベースを元にしつつデプロイ先や設定などを切り替えられるようになっている。

Pulumiを利用するには、最初にプロジェクトを作成する必要がある。プロジェクトの作成は「pulumi new」コマンドを使用する。このコマンドではプロジェクトのひな形を作成するための「テンプレート」を引数として指定するようになっており、使用するクラウドや言語に応じたテンプレートを選択できる。

pulumi new <オプション> <テンプレート>

利用できるテンプレートは、「--help」オプション付きで「pulumi new」コマンドを実行することで確認できる。

$ pulumi new --help
Create a new Pulumi project and stack from a template.
  
  
Available Templates:
  alicloud-csharp            A minimal AliCloud C# Pulumi program
  alicloud-fsharp            A minimal AliCloud F# Pulumi program
  alicloud-go                A minimal AliCloud Go Pulumi program
  alicloud-javascript        A minimal AliCloud JavaScript Pulumi program
  
  

また、テンプレートを指定せずに「pulumi new」コマンドを実行することで、対話的にテンプレートを選択するこもできる。

たとえばPythonを使ってKubernetesクラスタを操作する場合は、テンプレートとして「kubernetes-python」を選択すれば良い。「pulumi new」コマンドの実行時にはプロジェクト名やプロジェクトの説明、stack名(後述)の入力も求められるが、特に入力せずにEnterキーを押すとデフォルトのもの(括弧内に表示されているもの)が使われる。また、デフォルト設定ではプロジェクトの作成と同時に「dev」というStackも作成される。

↓プロジェクトで使用するファイルを管理するディレクトリを作成する
$ mkdir test01
$ cd test01
↓「kubernetes-python」テンプレートからプロジェクトを作成する
$ pulumi new kubernetes-python
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: (test01)
↓プロジェクトの説明を入力する
project description: (A minimal Kubernetes Python Pulumi program)
Created project 'test01'
↓stack名を入力する
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'

Your new project is ready to go!

To perform an initial deployment, run the following commands:

   1. python3 -m venv venv
   2. source venv/bin/activate
   3. pip3 install -r requirements.txt

Then, run 'pulumi up'

なお、ローカルに履歴や設定を残す設定にしている場合、これらに加えて設定やデータなどを暗号化する際に使用するパスフレーズの入力も求められる。

Enter your passphrase to protect config/secrets: ←暗号化に使用するパスフレーズを入力する
Re-enter your passphrase to confirm: ←確認のため同じパスフレーズを再度入力する
Created stack 'dev'

Enter your passphrase to unlock config/secrets
    (set PULUMI_CONFIG_PASSPHRASE to remember): 入力したパスフレーズを再度入力する

さて、この例のように「pulumi new」コマンドでPython向けのプロジェクトを作成すると、次のように3つのファイルが作成される。

$ ls -1
Pulumi.yaml
__main__.py
requirements.txt

ここで「Pulumi.yaml」はPulumiの設定などを記述した設定ファイルで、「__main__.py」が作成・管理するリソースを定義するPythonのソースコード、「requirements.txt」がPythonコードで使用する依存モジュールを定義するファイルだ。

Pythonを利用する場合、Pyhon 3.3以降で利用できる「venv」という仕組みを使ってパッケージ管理を行うことが推奨されている。venvはシステム標準とは分離したPython環境をローカルディレクトリ下に作成するもので、これを利用することでシステムとは独立して依存パッケージを管理できるようになる。Pulumiでは多数の依存パッケージを必要とするが、これによってシステム全体に影響を及ぼさずに依存パッケージをインストールできるようになる。

venvを利用するには、次のようにまず「python3 -m venv venv」コマンドを実行してvenvを作成する。

$ python3 -m venv venv

すると、ディレクトリ内に「venv」というディレクトリが作成される。このディレクトリ内の「bin/activate」というスクリプトを実行すると仮想環境内でPythonのモジュールを管理したり、コードを実行できるようになるので、ここで「pip3 install」コマンドを実行して依存パッケージをインストールする。

$ ls -1
Pulumi.yaml
__main__.py
requirements.txt
venv

↓仮想環境を有効にする
$ . venv/bin/activate
(venv) $

↓依存関係を記述したrequirements.txtを確認する
(venv) $ cat requirements.txt
pulumi>=1.0.0
pulumi-kubernetes>=1.0.0

↓依存パッケージをインストールする
(venv) $ pip3 install -r requirements.txt
Collecting pulumi>=1.0.0 (from -r requirements.txt (line 1))
  Downloading https://files.pythonhosted.org/packages/40/4c/4d1660919a520c684b6af3421865cb9e86ba95474a3c408a4390dac58792/pulumi-1.11.1-py2.py3-none-any.whl (90kB)
  
  
  Running setup.py install for dill ... done
  Running setup.py install for pulumi-kubernetes ... done
Successfully installed arpeggio-1.9.2 attrs-19.3.0 certifi-2019.11.28 chardet-3.0.4 dill-0.3.1.1 grpcio-1.27.2 idna-2.8 parver-0.3.0 protobuf-3.11.3 pulumi-1.11.1 pulumi-kubernetes-1.5.6 requests-2.21.0 semver-2.9.1 six-1.14.0 urllib3-1.24.3

なお、筆者が試した環境では別途PulumiのKubernetesプラグインのインストールも必要だった。これは、PulumiのKubernetesプラグインがインストールされていない、もしくは最新版ではない場合に発生するようで、その際は後述する「pulumi up」コマンドの実行時などに次のようなメッセージが表示される

Diagnostics:
  pulumi:providers:kubernetes (default_1_5_7):
    error: no resource plugin 'kubernetes-v1.5.7' found in the workspace or on your $PATH, install the plugin using `pulumi plugin install resource kubernetes v1.5.7`

この場合、指示に従って「pulumi plugin install」コマンドを実行すれば良い。

(venv) $ pulumi plugin install resource kubernetes v1.5.7
[resource plugin kubernetes-1.5.7] installing
Downloading plugin: 19.93 MiB / 19.93 MiB [=========================] 100.00% 3s
Moving plugin... done.

次に、「pulumi new」コマンドで作成されたソースコードを編集して作成したいリソースを記述する。今回のようにkubernetes-pythonテンプレートからプロジェクトを作成した場合、__main__.pyには次のようなnginxをデプロイするコードが記述されているはずだ。

import pulumi
from pulumi_kubernetes.apps.v1 import Deployment

app_labels = { "app": "nginx" }

deployment = Deployment(
    "nginx",
    spec={
        "selector": { "match_labels": app_labels },
        "replicas": 1,
        "template": {
            "metadata": { "labels": app_labels },
            "spec": { "containers": [{ "name": "nginx", "image": "nginx" }] }
        }
    })

pulumi.export("name", deployment.metadata["name"])

「pulumi up」コマンドを実行すると、ここで定義されたリソースが実際に作成される。

(venv) $ pulumi up
Previewing update (dev):
     Type                           Name        Plan
 +   pulumi:pulumi:Stack            test01-dev  create
 +   mq kubernetes:apps:Deployment  nginx       create

Resources:
    + 2 to create

Do you want to perform this update? yes
Updating (dev):
     Type                           Name        Status
 +   pulumi:pulumi:Stack            test01-dev  created
 +   mq kubernetes:apps:Deployment  nginx       created

Outputs:
    name: "nginx-j9slxlsv"

Resources:
    + 2 created

Duration: 16s

Permalink: https://app.pulumi.com/hylom/test01/dev/updates/1

なお、「pulumi up」コマンドの実行時には次のように確認が求められる。ここでは矢印キーでカーソルを動かせるので、問題がなければカーソルを「yes」に動かしてEnterキーを押せば良い。

Do you want to perform this update?
> yes
  no
  details

また、ローカルに履歴を保存するように設定している場合は、ここでアンロックのためのパスフレーズの入力を求められる。

Enter your passphrase to unlock config/secrets
    (set PULUMI_CONFIG_PASSPHRASE to remember):

毎回このパスフレーズを入力するのが面倒な場合は、パスフレーズを「PULUMI_CONFIG_PASSPHRASE」環境変数に格納しておけば良い。そうすると、この環境変数を参照して自動的に認証が行われるようになる。

最後に表示されているURLはPulumiサービス上でこの操作の詳細情報を確認するためのものだ。このURLにアクセスすることで、Webブラウザで実行された処理を確認できる(図6)。

図6 表示されたURLにアクセスすると、pulumiサービス上でその操作内容を確認できる

ちなみにローカルに履歴を保存していた場合、このURLはローカルファイルへのパスになる。

Permalink: file:///home/hylom/.pulumi/stacks/dev.json

Stack情報の確認と削除

Pulumiで作成したリソースは、Pulumi上ではStack単位で管理され、その情報は「pulumi stack」コマンドで確認できる。

$ pulumi stack
Current stack is dev:
    Owner: hylom
    Last updated: 5 minutes ago (2020-03-10 17:33:49.444034418 +0900 JST)
    Pulumi version: v1.11.1+dirty
Current stack resources (3):
    TYPE                                  NAME
    pulumi:pulumi:Stack                   test01-dev
    tq kubernetes:apps/v1:Deployment      nginx
    mq pulumi:providers:kubernetes        default_1_5_6

Current stack outputs (1):
    OUTPUT  VALUE
    name    nginx-j9slxlsv

More information at: https://app.pulumi.com/hylom/test01/dev

Use `pulumi stack select` to change stack; `pulumi stack ls` lists known ones

また、Pulumiサービスからも、作成したStackの情報や履歴(activity)、作成されたリソースといった情報を確認できる(図7、8)。

図7 Pulumiサービスの「STACK」タブでは実行した処理の内容を確認できる
図8 「RESOURCES」タグでは作成されているリソースの情報を確認できる

このときkubectlコマンドで作成されたリソースを確認すると、次のように実際に対応するdeploymentが作成されていることが分かる。

(venv) $ kubectl get deploy
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
nfs-client-provisioner   1/1     1            1           32d
nginx-j9slxlsv           1/1     1            1           6m2s
(venv) $ kubectl get pod
NAME                                      READY   STATUS    RESTARTS   AGE
nfs-client-provisioner-78b7d65564-kkzw4   1/1     Running   0          32d
nginx-j9slxlsv-554b9c67f9-qb4kp           1/1     Running   0          6m5s

また、リソースの作成や削除といった操作は、基本的にはすべて履歴として記録されており、「pulumi history」コマンドで確認できる。

(venv) $ pulumi history
$ pulumi  history
UpdateKind: import
Status: succeeded
Message:
+0-1~0 0 Updated 3 minutes ago took 0s

UpdateKind: import
Status: succeeded
Message:
+0-1~0 1 Updated 4 minutes ago took 0s

UpdateKind: import
Status: succeeded
Message:
+0-1~0 2 Updated 8 minutes ago took 0s

UpdateKind: update
Status: succeeded
Message:
+2-0~0 0 Updated 18 minutes ago took 16s

この履歴はWebブラウザでPulumiサービスにアクセスすることでも確認できる(図9)。

図9 Pulumiサービス上でも履歴を確認できる

Stackとそれに関連付けられているリソースを削除するには「pulumi destroy」コマンドを使用する。「pulumi up」コマンドで作成されたリソースが存在している場合はそれらリソースは同時に削除される。コマンドの実行時には「pulumi up」コマンドの実行時と同様に確認が求められるので、「yes」を選択する。

(venv) $ pulumi destroy
Previewing destroy (dev):

Resources:

Do you want to perform this destroy? yes ←「yes」と入力する
Destroying (dev):

Permalink: https://app.pulumi.com/hylom/test01/dev/updates/5
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'.

なお、Stackを削除した場合でも、その履歴については削除されず残される。履歴までも含めてStackを削除したい場合は、「pulumi stack rm <スタック名>」コマンドを実行する。

(venv) $ 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名の入力を求められる
Stack 'dev' has been removed!

Stackを削除すると、同時にPulumiサービス上での履歴なども削除される。なお、稼働しているリソースが存在しない場合は、PulumiサービスからStackを削除することもできる(図10)。

図10 Pulumiサービス上からのStack削除も可能

JavaScriptでのリソース管理

ここまでではPythonでリソースを記述していたが、JavaScript(もしくはTypeScript)でリソースを記述する場合も作業の流れはほぼ同じだ。その場合、「pulumi new」コマンドで指定するテンプレートとして「kubernetes-javascript」(JavaScriptの場合)もしくは「kubernetes-typescript」(TypeScriptの場合)を選択すれば良い。

$ pulumi new kubernetes-javascript
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: (test02)
project description: (A minimal Kubernetes JavaScript Pulumi program)
Created project 'test02'

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'

Installing dependencies...
  
  
Finished installing dependencies

Your new project is ready to go!

To perform an initial deployment, run 'pulumi up'

これで、JavaScript(もしくはTypeScript)でのリソース管理に必要なファイル一式が作成される。

$ ls -1
Pulumi.yaml
index.js
node_modules
package-lock.json
package.json

ここでは「index.js」がリソースを定義するソースコードファイルとなっており、次のような内容になっている。

$ cat index.js
"use strict";
const k8s = require("@pulumi/kubernetes");

const appLabels = { app: "nginx" };
const deployment = new k8s.apps.v1.Deployment("nginx", {
    spec: {
        selector: { matchLabels: appLabels },
        replicas: 1,
        template: {
            metadata: { labels: appLabels },
            spec: { containers: [{ name: "nginx", image: "nginx" }] }
        }
    }
});
exports.name = deployment.metadata.name;

Pythonの場合はpip3コマンドで依存するモジュールのインストールを実行していたが、JavaScriptの場合は自動的にその処理が実行されるので、そのまま「pulumi up」コマンドを実行するとリソースの作成が行われる。

$ pulumi up
Previewing update (dev):
     Type                           Name        Plan
 +   pulumi:pulumi:Stack            test02-dev  create
 +   mq kubernetes:apps:Deployment  nginx       create

Resources:
    + 2 to create

Do you want to perform this update? yes
Updating (dev):
     Type                           Name        Status
 +   pulumi:pulumi:Stack            test02-dev  created
 +   mq kubernetes:apps:Deployment  nginx       created

Outputs:
    name: "nginx-0jtwanyd"

Resources:
    + 2 created

Duration: 13s

Permalink: https://app.pulumi.com/hylom/test02/dev/updates/1

独自のリソース定義を行う

続いては、「pulumi new」コマンドで作成された設定ファイルを修正して、作成するリソースを独自に定義してみよう。今回作成するのは、WordPressを実行するPodと、WordPressが使用するデータベース(MariaDB)を実行するPodの組み合わせという環境だ。具体的には、次のような構成となる。

  • WordPressはDockerHubで公開されている「wordpress」イメージを使用する
  • MariaDBは同じくDockerHubで公開されている「mariadb」イメージを使用する
  • MariaDBのストレージはPersistent Volume Claimを使用して確保する
  • MariaDBへのアクセスに使用するパスワードはKubernetesのSecretを使用して管理する
  • Pulumiでのリソースの定義にはPythonを利用する

なお、データベースアクセスに使用するパスワードは、次のように「kubectl create secret」コマンドでSecretリソースを作成して格納しておく。

$ kubectl create secret generic mariadb-cred --from-literal=MYSQL_ROOT_PASSWORD=<MariaDBのルートパスワード> --from-literal=MYSQL_PASSWORD=<WordPressで使用するデータベースへのアクセスに使用するパスワード>

ここで作成したSecretリソースは、次のようにして確認できる。

$ kubectl describe secret mariadb-cred
Name:         mariadb-cred
Namespace:    default
Labels:       <none>
Annotations:  <none>

Type:  Opaque

Data
====
MYSQL_ROOT_PASSWORD:  6 bytes
MYSQL_PASSWORD:       8 bytes

Pythonによるリソース定義

さて、それではプロジェクトを作成し、実際にリソース定義を行っていこう。今回はPythonを使用するので、テンプレートとして「kubernetes-python」を選択してプロジェクトを作成し、pyenvやpip3コマンドによる依存モジュールのインストールを行っておく。

$ mkdir wp_py
$ cd wp_py/
$ pulumi new kubernetes-python
$ python3 -m venv venv
$ . venv/bin/activate
(venv) $ pip3 install -r requirements.txt

続いて、メインのソースコードである__main__.pyを編集し、リソースの定義を行っていく。

モジュールのインポート

リソースの定義は、作成したいリソースに対応するクラスをインポートし、そのコンストラクタを実行してオブジェクトを作成することで行う。たとえばKubernetes関連のリソースは「pulumi_kubernetes.<グループ>.<バージョン>」という名前空間内で定義されているので、ここからクラスをインポートすることになる。ここでの「グループ」および「バージョン」はマニフェストファイルの「apiVersion」などでも使用される、Kubernetes APIのグループおよびバージョンに相当する。たとえば、「v1 core」APIで定義されているServiceリソースに対応するクラスは「pulumi_kubernetes.core.v1」という名前空間で定義されており、また「v1 apps」APIで定義されているDeploymentリソースに対応するクラスは「pulumi_kubernetes.apps.v1」という名前空間内で定義されている。

今回は「v1 core」APIに含まれるPersistentVolumeClaimリソースおよびServiceリソースと、「v1 apps」APIに含まれるDeploymentリソースおよびStatefulSetリソースを使用するので、次のようにimport文を記述する。

import pulumi
from pulumi_kubernetes.core.v1 import PersistentVolumeClaim, Service
from pulumi_kubernetes.apps.v1 import Deployment, StatefulSet

なお、モジュールについての詳細は公式ドキュメントの「API Reference」ページに記載されている。ここでは対象とするクラウドプロバイダおよび使用する言語ごとにAPIがまとめられており、たとえばKubernetesとPythonという組み合わせであればPulumi Kubernetesというページから各モジュールのドキュメントを参照できる。

インスタンスの作成

続いて、ソースコード内でこれらクラスのコンストラクタを実行してインスタンスを作成することで、作成するリソースを定義する。Kubernetesの場合、リソースに対応するクラスの多くは次のような引数を取る。

<クラス名>(<リソース名>, opts=<オプション>, metadata=<メタデータ>, spec=<Spec>)

ここで、「オプション(opts)」はPulumiがそのリソースを扱う際の振る舞いを指定するパラメータだ。基本的には特に指定する必要はないが、依存関係などを厳密に定義したい場合はこのパラメータを指定する(ドキュメント)。「メタデータ(metadata)」はそのリソースに与えるメタデータ、「Spec」はそのリソースの詳細情報を指定するもので、それぞれKubernetesのManifestファイルでの「metadata:」や「spec:」要素に相当する。

たとえば、MariaDBで使用する3306番ポートを使用するServiceの定義は次のようになる。ここでは「metadata」や「selector」の指定でmariadb_app_name変数を使っているが、このように自由に変数を利用できる点もPulumiを使ったリソース定義のメリットの1つとなっている。

# リソース名を変数で定義する
mariadb_app_name = "mariadb-wordpress"

# Serviceクラスのインスタンスを作成する
svc = Service(mariadb_app_name,
              metadata={
                  "name": mariadb_app_name,
              },
              spec={
                  "selector": { "app": mariadb_app_name },
                  "ports": [
                      { "protocol": "TCP",
                        "port": 3306,
                        "targetPort": 3306,
                    },
                  ],
              })

ちなみにこれをManifestファイルで記述すると次のようになる。これと比較すると分かるように、今回のコードではManifestで指定しているmetadataやspecを引数で与えている。

apiVersion: v1
kind: Service
metadata:
  name: mariadb-wordpress
spec:
  selector:
    app: mariadb-wordpress
  ports:
    - protocol: TCP
      port: 3306
      targetPort: 3306

manifest引数やspec引数で指定する値はPythonの辞書型データそのものであり、別の方法でこれらを構築することもできる。たとえば、次のようにYAML形式で記述したものを変換して与える、といったことも可能だ。

imort yaml

# YAML形式でManifestを記述する
wp_svc_yaml = '''
apiVersion: v1
kind: Service
metadata:
  name: wordpress
spec:
  selector:
    app: wordpress
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
'''

# YAML形式を辞書型データに変換する
wp_svc_manifest = yaml.safe_load(wp_svc_yaml)

# Serviceクラスのインスタンスを作成する
wp_svc = Service("wordpress",
                 metadata=wp_svc_manifest["metadata"],
                 spec=wp_svc_manifest["spec"])

Pythonでは標準ではYAMLをサポートしていないため、ここでは別途「pyyaml」というモジュールをインストールしておく必要がある。これは、requirement.txtに次のように「pyyaml>=5.3」という記述を追加した上で「pip3 install -r requirement.txt」コマンドを実行すれば良い。

pulumi>=1.0.0
pulumi-kubernetes>=1.0.0
pyyaml>=5.3

同様にして必要なすべてのリソースを定義したものが、次のソースコードになる。

import yaml
import pulumi
from pulumi_kubernetes.core.v1 import PersistentVolumeClaim, Service
from pulumi_kubernetes.apps.v1 import Deployment, StatefulSet

mariadb_app_name = "mariadb-wordpress"
pvc_name = "mariadb-pvc"
mariadb_ver = "10.4"

# MariaDB用のService定義
svc = Service(mariadb_app_name,
              metadata={
                  "name": mariadb_app_name,
              },
              spec={
                  "selector": { "app": mariadb_app_name },
                  "ports": [
                      { "protocol": "TCP",
                        "port": 3306,
                        "targetPort": 3306,
                    },
                  ],
              })

# MariaDB用のPersistentVolumeClaim定義
pvc = PersistentVolumeClaim("mariadb-pvc",
                            metadata={
                                "name": pvc_name,
                                "annotations": {
                                    "volume.beta.kubernetes.io/storage-class": "managed-nfs-storage",
                                },
                            },
                            spec={
                                "accessModes": ["ReadWriteMany",],
                                "resources": {
                                    "requests": {
                                        "storage": "1Gi",
                                    },
                                },
                            })

# MariaDBのPodを管理するためのStatefulSet定義
ss = StatefulSet(mariadb_app_name,
                 metadata={ "name": mariadb_app_name },
                 spec={
                     "selector": {
                         "matchLabels": {
                             "app": mariadb_app_name,
                         },
                     },
                     "serviceName": mariadb_app_name,
                     "replicas": 1,
                     "template": {
                         "metadata": {
                             "labels": {
                                 "app": mariadb_app_name,
                             },
                         },
                         "spec": {
                             "containers": [{
                                 "name": mariadb_app_name,
                                 "image": "mariadb:" + mariadb_ver,
                                 "volumeMounts": [
                                     { "name": "nfs-pvc",
                                       "mountPath": "/var/lib/mysql", },
                                 ],
                                 "envFrom": [
                                     { "secretRef": { "name": "mariadb-cred" } },
                                 ],
                                 "env": [
                                     { "name": "MYSQL_USER",
                                       "value": "wordpress" },
                                     { "name": "MYSQL_DATABASE",
                                       "value": "wordpress" },
                                 ],
                             }],
                             "volumes": [{
                                 "name": "nfs-pvc",
                                 "persistentVolumeClaim": {
                                     "claimName": pvc_name
                                },
                             }],
                         },
                     },
                 })

# WordPressのPodを管理するためのDeployment定義
wp_deploy_yaml = '''
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress-deployment
  labels:
    app: wordpress
spec:
  replicas: 1
  selector:
    matchLabels:
      app: wordpress
  template:
    metadata:
      labels:
        app: wordpress
    spec:
      containers:
      - name: wordpress
        image: wordpress:5.3-php7.3
        ports:
        - containerPort: 80
        env:
        - name: WORDPRESS_DB_HOST
          value: mariadb-wordpress
        - name: WORDPRESS_DB_USER
          value: wordpress
        - name: WORDPRESS_DB_NAME
          value: wordpress
        - name: WORDPRESS_DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: mariadb-cred
              key: MYSQL_PASSWORD
'''
wp_deploy_manifest = yaml.safe_load(wp_deploy_yaml)
wp_deploy = Deployment("wordpress-deployment",
                       metadata=wp_deploy_manifest["metadata"],
                       spec=wp_deploy_manifest["spec"])


# WordPressで使用するServiceの定義
wp_svc_yaml = '''
apiVersion: v1
kind: Service
metadata:
  name: wordpress
spec:
  selector:
    app: wordpress
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
'''
wp_svc_manifest = yaml.safe_load(wp_svc_yaml)
wp_svc = Service("wordpress",
                 metadata=wp_svc_manifest["metadata"],
                 spec=wp_svc_manifest["spec"])

# 出力ログに表示する内容の指定
pulumi.export("name", wp_deploy.metadata["name"])

最後に実行している「pulumi.export()」メソッドは、「pulumi up」コマンドの実行結果に任意の値を出力するためのものだ。引数として「<名前>, <値>」の組み合わせを与えてこのメソッドを実行することで、コマンド実行時の「Outputs:」以下にここで指定した名前と値の組み合わせが表示される。たとえば今回の例では、「name: <Deploymentリソースのmetadataで指定された名前>」という文字列が表示されるようになる。

さて、このソースコードを作成した後に「pulumi up」コマンドを実行すると、次のようにリソースが作成される。

$ pulumi up
Previewing update (dev):
     Type                                      Name                  Plan
 +   pulumi:pulumi:Stack                       wp_py-dev             create
 +   tq kubernetes:core:PersistentVolumeClaim  mariadb-pvc           create
 +   tq kubernetes:core:Service                mariadb-wordpress     create
 +   tq kubernetes:core:Service                wordpress             create
 +   tq kubernetes:apps:StatefulSet            mariadb-wordpress     create
 +   mq kubernetes:apps:Deployment             wordpress-deployment  create

Resources:
    + 6 to create

Do you want to perform this update? yes
Updating (dev):
     Type                                      Name                  Status
 +   pulumi:pulumi:Stack                       wp_py-dev             created
 +   tq kubernetes:core:PersistentVolumeClaim  mariadb-pvc           created
 +   tq kubernetes:core:Service                wordpress             created
 +   tq kubernetes:core:Service                mariadb-wordpress     created
 +   tq kubernetes:apps:StatefulSet            mariadb-wordpress     created
 +   mq kubernetes:apps:Deployment             wordpress-deployment  created

Outputs:
    name: "wordpress-deployment"

Resources:
    + 6 created

Duration: 19s

Permalink: https://app.pulumi.com/hylom/wp_py/dev/updates/1

なお、ソースコード中に文法エラーなどがある場合はリソースの作成前にエラーが発生する。また、リソースの作成/変更前には必ず確認も行われる。意図せずリソースが作成されないよう、そこで表示される内容をチェックしておこう。

リソースの更新

リソース定義を変更し、それを実環境に反映させる場合も「pulumi up」コマンドを使用する。たとえば、WordPressを実行させるPodのレプリカ数を次のように「1」から「2」に変更し、再度「pulumi up」コマンドを実行してみよう。

wp_deploy_yaml = '''
apiVersion: apps/v1
kind: Deployment
metadata:
  name: wordpress-deployment
  labels:
    app: wordpress
spec:
  replicas: 2

すると、次のように変更が行われたリソースが検出され、そのリソースだけが実際に変更される。

(venv) $ pulumi up
Previewing update (dev):
     Type                           Name                  Plan       Info
     pulumi:pulumi:Stack            wp_py-dev
 ~   mq kubernetes:apps:Deployment  wordpress-deployment  update     [diff: ~spec]

Resources:
    ~ 1 to update
    5 unchanged

Do you want to perform this update? yes
Updating (dev):
     Type                           Name                  Status      Info
     pulumi:pulumi:Stack            wp_py-dev
 ~   mq kubernetes:apps:Deployment  wordpress-deployment  updated     [diff: ~spec]

Outputs:
    name: "wordpress-deployment"

Resources:
    ~ 1 updated
    5 unchanged

Duration: 9s

Permalink: https://app.pulumi.com/hylom/wp_py/dev/updates/2

Pulumiによるさくらのクラウドの管理

PulumiではKubernetesだけでなく、さまざまなクラウドサービスを同じような手順で管理できるのが特徴だ。そこで続いては、Pulumiを使ってさくらのクラウド上のリソースを管理する方法を紹介しよう。

Pulumiは公式にはさくらのクラウドをサポートしていないが、コミュニティによってPulumiからさくらのクラウドを操作するためのプラグイン(pulumi-sakuracloud)が開発されており、こちらを導入することでPulumiからさくらのクラウドのリソースを作成したり、それらを操作することが可能になる。

プラグインのインストール

プラグインのインストールは、次のように「pulumi plugin install」コマンドで行える。

$ pulumi plugin install resource sakuracloud 0.1.0 --server https://github.com/sacloud/pulumi-sakuracloud/releases/download/0.1.0

このコマンドを実行すると、プラグインのダウンロードとインストールが実行される。なお、pulumi-sakuracloudではほかのPulumiがサポートしているクラウドインフラと同様、JavaScriptおよびTypeScript、Python、Go、.NET coreでリソースを定義できる。

プロジェクトの作成

続いてはプロジェクトの作成だが、pulumi-sakuracloudのリポジトリ上でテンプレートが公開されており、こちらを利用してプロジェクトを作成できるようになっている(表1)。これらのURLから使用する言語に対応するものを選び、それを引数として「pulumi new」コマンドを実行すれば良い。

表1 公開されているテンプレート
言語 URL
JavaScript https://github.com/sacloud/pulumi-sakuracloud/tree/master/templates/javascript
Python https://github.com/sacloud/pulumi-sakuracloud/tree/master/templates/python
TypeScript https://github.com/sacloud/pulumi-sakuracloud/tree/master/templates/typescript

たとえばJavaScriptを使用する場合、次のように実行する。

$ pulumi new https://github.com/sacloud/pulumi-sakuracloud/tree/master/templates/javascript

クラウドを指定しない「javascript」テンプレートを指定してプロジェクトを作成し、その後さくらのクラウド向けのモジュール(「@sacloud/pulumi_sakuracloud」)を手動でインストールしても良い。

$ pulumi new javascript
$ npm install @sacloud/pulumi_sakuracloud

なお、Pythonの場合も基本的な手順は同じだ。手動でモジュールをインストールする場合は、pip3コマンドで「pulumi_sakuracloud」モジュールをインストールすれば良い。

トークンの登録

さくらのクラウドでは、アクセストークンおよびアクセストークンシークレットを使ってユーザー認証を行う仕組みになっている。これらはさくらのクラウドのコントロールパネルの「API Key」画面から作成できる(図11

図11 さくらのクラウドではコントロールパネルの「API Key」画面でアクセストークンを作成できる

pulumi-sakuracloudでは、このアクセストークンおよびアクセストークンシークレットを環境変数もしくはPulumiの設定管理機能(「pulumi config」コマンド)経由で指定する。環境変数で指定する場合、次のように「SAKURACLOUD_ACCESS_TOKEN」および「SAKURACLOUD_ACCESS_TOKEN_SECRET」、「SAKURACLOUD_ZONE」という3つの環境変数にそれぞれの文字列を格納しておく。

$ export SAKURACLOUD_ACCESS_TOKEN=<アクセストークン>
$ export SAKURACLOUD_ACCESS_TOKEN_SECRET=<アクセストークンシークレット>
$ export SAKURACLOUD_ZONE=<ゾーンID>

なお、ゾーンIDについては表2のものを指定する(「さくらのクラウドAPI1.1」ページ)。

表2 ゾーンに対応するID
ゾーン ゾーンID
東京第1 tk1a
石狩第1 is1a
石狩第2 is1b
Sandbox tk1v

Pulumiの設定管理機能を利用する場合は、次のように「pulumi config set」コマンドを使って「sakuracloud:token」および「sakuracloud:secret」、「sakuracloud:zone」というキーに対しそれぞれの文字列を指定する。

$ pulumi config set --secret sakuracloud:token <アクセストークン>
$ pulumi config set --secret sakuracloud:secret <アクセストークンシークレット>
$ pulumi config set --secret sakuracloud:zone <ゾーンID>

リソースを記述する

以上でさくらのクラウドをPulumi経由で操作するための準備は完了だ。続いて、作成するリソースの定義を行っていこう。基本的には次のような形式でリソースを宣言できる。

<クラス名>(<リソース名>, <パラメータ>)

ここでリソース名はそのリソースを示す文字列で、またパラメータはリソース作成に必要な情報を辞書型で指定するものだ。指定するパラメータは「Terraform for さくらのクラウド」のドキュメントにまとめられているものと同じで、「必須」となっているものは必ず指定しなければならない。各種IDはAPI経由で取得できるほか、さくらのクラウドコントロールパネルなどからも参照できる。

たとえばCentOSのアーカイブからディスクを作成し、そのディスクと追加で作成したNICを搭載したサーバーを作成するコードは次のようになる。

"use strict";
const pulumi = require("@pulumi/pulumi");
const sakuracloud = require("@sacloud/pulumi_sakuracloud");

const archiveId = 113200099576;  // CentOSの 8.1のアーカイブに対応するID
const switchId = 112700601664;  // 接続するスイッチのID

// Diskリソースを作成する
const disk = new sakuracloud.Disk("pulumi-test-disk",
                                  {
                                      name: "pulumi-test-disk",
                                      plan: "ssd",
                                      source_archive_id: archiveId,
                                  });

// 作成したdiskリソースを接続したServerリソースを作成
const server = new sakuracloud.Server("pulumi-test",
                                      {
                                          name: "pulumi-test",
                                          disks: [ disk.id, ],
                                          core: 1,
                                          memory: 1,
                                          nic: "shared",
                                          additional_nics: [ switchId, ],
                                          description: "for pulumi test",
                                          hostname: "pulumi01",
                                          ssh_key_ids: [],
                                          disable_pw_auth: false,
                                      });

// 作成したServerおよびDiskのIDを出力する
module.exports.serverId = server.id;
module.exports.diskId = disk.id;

ここで、Diskリソースのコピー元となるアーカイブのIDはさくらのクラウドのコントロールパネルで確認できる(図12)。

図12 アーカイブIDはさくらのクラウドのコントロールパネルでサーバーを作成する画面から参照できるアーカイブ一覧画面で確認できる

また、ここでは標準の(共有ネットワークに接続された)NICに加えて、指定したスイッチに接続するNICを追加している。このIDも同様にコントロールパネルから確認可能だ(図13)。

図13 スイッチIDもさくらのクラウドのコントロールパネルで確認できる

リソースの記述完了後「pulumi up」コマンドを実行すると、定義したリソースが作成される。

$ pulumi up
Previewing update (dev):
     Type                         Name              Plan
 +   pulumi:pulumi:Stack          sakura-js-dev     create
 +   tq sakuracloud:index:Disk    pulumi-test-disk  create
 +   mq sakuracloud:index:Server  pulumi-test       create

Resources:
    + 3 to create

Do you want to perform this update? yes
Updating (dev):
     Type                         Name              Status
 +   pulumi:pulumi:Stack          sakura-js-dev     created
 +   tq sakuracloud:index:Disk    pulumi-test-disk  created
 +   mq sakuracloud:index:Server  pulumi-test       created

Outputs:
    diskId  : "113200417849"
    serverId: "113200417850"

Resources:
    + 3 created

Duration: 47s

Permalink: https://app.pulumi.com/hylom/sakura-js/dev/updates/1

ここで作成されたリソースは、Kubernetesの場合と同じように「pulumi destroy」コマンドで削除できる。

さまざまなクラウドを統一的に操作できる点が最大の利点

このように、Pulumiはプログラミング言語を使って作成・管理するリソースを定義できる点と、Pulumiサービスを使った管理機能が特徴となる。PythonやJavaScriptなどをすでに習得していれば、ツール独自の設定ファイルの書式を学習しなくて良いという点はメリットだろう。また、異なるクラウドプラットフォームでも基本的には同じような流れで操作が行えるため、複数のクラウドプラットフォームにまたがって管理を行っているようなケースでは特に有用だ。

一方の管理機能については、無料プランではチーム機能が利用できないこともあり、あまりメリットは感じられない。とはいえ、複数人でインフラ管理を行うようなケースにおいては活用できる可能性があるので、費用対効果を考えたうえで検討すると良いだろう。