昨今話題?の Pulumi を使ってみた

昨今周りで [Pulumi](https://pulumi.io/) というものを良く聞くので実際使ってみました。

マルチクラウド向けのオーケストレーションツールのようですが、ページだけを見ると `ただのSDK` っぽくしか見えなかったんですよ。実際使ってみて違うということがわかりました。

## インストールから初期設定まで

Pulumiコマンド自体をまずはインストールするため以下を実行。

```
curl -fsSL https://get.pulumi.com | sh
```
brewでも入るらしい。

こんな感じでインストールが進行して、成功すると `now installed!` と出る。

```
=== Installing Pulumi v0.17.1 ===
+ Downloading https://get.pulumi.com/releases/sdk/pulumi-v0.17.1-darwin-x64.tar.gz...
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 36.9M  100 36.9M    0     0   902k      0  0:00:41  0:00:41 --:--:-- 3777k
+ Extracting to /Users/yuzu/.pulumi/bin
+ Adding $HOME/.pulumi/bin to $PATH in /Users/yuzu/.zshrc

=== Pulumi is now installed! 🍹 ===
+ Please restart your shell or add add /Users/yuzu/.pulumi/bin to your $PATH
+ If you're new to Pulumi, here are some resources for getting started:
      - Getting Started Guide: https://pulumi.io/quickstart
      - Examples Repo: https://github.com/pulumi/examples
      - Create a New Project: Run 'pulumi new' to create a new project using a template
```

上記の通り `/Users/yuzu/.pulumi/bin` にパスを通しておく。

次に作業用ディレクトリを作って、そこで `pulumi new` する。

```
mkdir -p ~/dev/pulumi_work
cd ~/dev/pulumi_work
pulumi new

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...
Please choose a template:
> aws-go                   A minimal AWS Go Pulumi program
  aws-javascript           A minimal AWS JavaScript Pulumi program
  aws-python               A minimal AWS Python Pulumi program
  aws-typescript           A minimal AWS TypeScript Pulumi program
  azure-go                 A minimal Azure Go Pulumi program
  azure-javascript         A minimal Azure JavaScript Pulumi program
  azure-python             A minimal Azure Python Pulumi program
  azure-typescript         A minimal Azure TypeScript Pulumi program
  gcp-go                   A minimal Google Cloud Go 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
  go                       A minimal Go Pulumi program
  hello-aws-javascript     A simple AWS serverless JavaScript Pulumi program
  javascript               A minimal JavaScript Pulumi program
  kubernetes-javascript    A minimal Kubernetes JavaScript Pulumi program
  kubernetes-python        A minimal Kubernetes Python Pulumi program
  kubernetes-typescript    A minimal Kubernetes TypeScript Pulumi program
  openstack-go             A minimal OpenStack Go Pulumi program
  openstack-javascript     A minimal OpenStack JavaScript Pulumi program
  openstack-python         A minimal OpenStack Python Pulumi program
  openstack-typescript     A minimal OpenStack TypeScript Pulumi program
  python                   A minimal Python Pulumi program
  typescript               A minimal TypeScript Pulumi program
```

すると認証が走る。自分の場合は先にブラウザ側でGithubログインしていたので、直接 `Thansk for logging in!` という画面だけが表示された。

で、上記画面で `>` が出ているが、これ実はカーソル。上下で選択するようになっている。

`openstack-python` を選択してみた。

するといろいろ質問される。すべてデフォルトで答えてみた。

```
Enter a value or leave blank to accept the (default), and press <ENTER>.
Press ^C at any time to quit.

project name: (pulumi_work)
project description: (A minimal OpenStack Python Pulumi program)
Created project 'pulumi_work'

stack name: (dev)
Created stack 'dev'

Your new project is configured and ready to go! ✨

To perform an initial deployment, run the following commands:
   1. virtualenv -p python3 venv
   2. source venv/bin/activate
   3. pip3 install -r requirements.txt
Then, run 'pulumi up'
```

以下のように出ていますね。

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

が、virtualenv直接使ってない人（pyenv + virtualenv) なので、それようにvirtualenv切ってみる。

3系が要求事項のようだし、3.6.6がレディだったのでそれにした。

```
pyenv virtualenv 3.6.6 pulumi
pyenv shell pulumi
pip install -r requirements.txt
```

とりあえず全部インストールOK。

## pulumi up

さて次は、

```
Then, run 'pulumi up'
```

と書かれているが、そもそも `pulumi up` すると何が起きるのか説明されてない。

ので、同ディレクトリに存在するファイルをみてみる。

まず `Pulumi.yaml` 。

```yaml
name: pulumi_work
runtime: python
description: A minimal OpenStack Python Pulumi program
```

メタデータしか入ってない。
じゃあ、 `__main__.py` かな？

```python
import pulumi
from pulumi_openstack import compute

# Create an OpenStack resource (Compute Instance)
instance = compute.Instance('test',
	flavor_name='s1-2',
	image_name='Ubuntu 16.04')

# Export the IP of the instance
pulumi.export('instance_ip', instance.access_ip_v4)
```

**？？？**

OpenStackのAuthURLはここ！とか、認証情報はこれ！とかそういうのをどこに持っているのかさっぱりわからない。
これ、実行すると何が起きるんだ・・・と思いつつ試しに叩いてみる。

```
% pulumi up
Previewing update (dev):

     Type                           Name             Plan       Info
 +   pulumi:pulumi:Stack            pulumi_work-dev  create
     └─ openstack:compute:Instance  test                        1 error

Diagnostics:
  openstack:compute:Instance (test):
    error: One of 'auth_url' or 'cloud' must be specified

error: an error occurred while advancing the preview
```

`error: One of 'auth_url' or 'cloud' must be specified`

まぁ、そりゃそうだ・・・と思いつつどこで指定するのかを調べる。

`pulumi up --help` , `pulumi --help` 共にそれっぽい情報がない。

で、  [ここ](https://pulumi.io/quickstart/openstack/index.html)  をみたら、

[OpenStack setup page](https://pulumi.io/quickstart/openstack/setup.html) というリンクがあり、そこにこう書かれている。

```
source openrc.sh
```

つまりバックエンドでOpenStack SDK使ってんだろうな・・・と思いつつ `cloud` と書かれている理由も理解。

とりあえずopenrcでいく。

```
export OS_AUTH_URL=https://<my-keystone-url>/v3/
export OS_USERNAME=<my-username>
export OS_PASSWORD=<my-password>
export OS_REGION_NAME=<my-regio>
export OS_TENANT_ID=<my-tenant-id>
export OS_IDENTITY_API_VERSION=3
export OS_PROJECT_DOMAIN_NAME=default
export OS_USER_DOMAIN_NAME=default
```

あとは、 `__main__.py` を少々修正する。

```
import pulumi
from pulumi_openstack import compute

# Create an OpenStack resource (Compute Instance)
instance = compute.Instance('test',
	flavor_name='m1.tiny', <-- ここを実際存在するフレーバー名に修正
	image_name='cirros-0.4.0-x86_64-disk') <-- ここを実際存在するイメージ名に修正
```

これで `pulumi up`

```
Previewing update (dev):

     Type                           Name                   Plan
 +   pulumi:pulumi:Stack            pulumi_work-dev        create
 +   └─ openstack:compute:Instance  pulumi-test-instance1  create

Resources:
    + 2 to create

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

おー、来ましたね。

これも矢印で（なんか斬新感）yesを選んでみる。


```
Previewing update (dev):

     Type                           Name                   Plan
     pulumi:pulumi:Stack            pulumi_work-dev
 +   └─ openstack:compute:Instance  pulumi-test-instance1  create

Resources:
    + 1 to create
    1 unchanged

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

     Type                           Name                   Status
     pulumi:pulumi:Stack            pulumi_work-dev        running... <-- ここがチカチカする
 +   └─ openstack:compute:Instance  pulumi-test-instance1  creating... <-- ここがチカチカする
```

上記のように `...` の部分がローディングを表してくれます。
 
で、最後に

```
% pulumi up
Previewing update (dev):

     Type                           Name                   Plan
     pulumi:pulumi:Stack            pulumi_work-dev
 +   └─ openstack:compute:Instance  pulumi-test-instance1  create

Resources:
    + 1 to create
    1 unchanged

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

     Type                           Name                   Status
     pulumi:pulumi:Stack            pulumi_work-dev
 +   └─ openstack:compute:Instance  pulumi-test-instance1  created

Outputs:
  + instance_ip: "10.0.0.33"

Resources:
    + 1 created
    1 unchanged

Duration: 19s

Permalink: https://app.pulumi.com/keiichi-hikita/pulumi_work/dev/updates/7
```

となりました。
Outputに書いておくと作成時に表示してくれるわけね。

実際インスタンスもできている。（対向はdevstackにしてたので、そこで確認）

```
stack@devstack:~/devstack$ openstack server list
+--------------------------------------+-------------------------------+--------+---------------------------------------------------------+--------------------------+---------+
| ID                                   | Name                          | Status | Networks                                                | Image                    | Flavor  |
+--------------------------------------+-------------------------------+--------+---------------------------------------------------------+--------------------------+---------+
| 0a3b4561-19c7-41a6-9090-2f9a7516fad0 | pulumi-test-instance1-0321052 | ACTIVE | private=10.0.0.33, fddf:6a77:2fa7:0:f816:3eff:fe51:5bf9 | cirros-0.4.0-x86_64-disk | m1.tiny |
+--------------------------------------+-------------------------------+--------+---------------------------------------------------------+--------------------------+---------+
```

名前に勝手にSuffixがつくのは、 [Auto-naming](https://pulumi.io/reference/programming-model.html) という仕様らしい。

### そのままもう一度pulumi up

Terraformとかの考え方だと、何も考えずにもう一度applyしても `差分無いよ` と言われるわけだが、Pulumiだとどうなるのかをやってみた。

```
% pulumi up
Previewing update (dev):

     Type                 Name             Plan
     pulumi:pulumi:Stack  pulumi_work-dev

Resources:
    2 unchanged

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

     Type                 Name             Status
     pulumi:pulumi:Stack  pulumi_work-dev

Outputs:
    instance_ip: "10.0.0.33"

Resources:
    2 unchanged

Duration: 2s

Permalink: https://app.pulumi.com/keiichi-hikita/pulumi_work/dev/updates/8
```

期待通り `2 unchanged` (0 changedでよくね？) と表示されたので、そういう差分管理はちゃんとやってくれる模様。

### 実際に変更の掛かるpulumi upをしてみる

ためしに、 ``__main.py__`` をこうしてみる。

```python
import pulumi
from pulumi_openstack import compute

# Create an OpenStack resource (Compute Keypair)
keypair = compute.Keypair('pulumi-keypair1')

# Create an OpenStack resource (Compute Instance)
instance = compute.Instance('pulumi-test-instance1',
	flavor_name='m1.tiny',
        key_pair=keypair.name,
	image_name='cirros-0.4.0-x86_64-disk')

# Export the IP of the instance
pulumi.export('instance_ip', instance.access_ip_v4)
```

Keypairを足して、それをInstanceの引数 `key_pair` に渡してみる。

で、pulumi up。どうなるか。

```
Previewing update (dev):

     Type                           Name                   Plan        Info
     pulumi:pulumi:Stack            pulumi_work-dev
 +   ├─ openstack:compute:Keypair   pulumi-keypair1        create
 +-  └─ openstack:compute:Instance  pulumi-test-instance1  replace     [diff: +keyPair~name

Resources:
    + 1 to create
    +-1 to replace
    2 changes. 1 unchanged

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

Keypairを変えるとTerraformでいうForceNewがかかる模様。（Terraformと同じ仕様だが多分Novaの仕様なんだろうな）
Replaceされると書かれているので。

同じようにyesを選択。

最終的にこうなりました。

```
% pulumi up
Previewing update (dev):

     Type                           Name                   Plan        Info
     pulumi:pulumi:Stack            pulumi_work-dev
 +   ├─ openstack:compute:Keypair   pulumi-keypair1        create
 +-  └─ openstack:compute:Instance  pulumi-test-instance1  replace     [diff: +keyPair~name

Resources:
    + 1 to create
    +-1 to replace
    2 changes. 1 unchanged

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

     Type                           Name                   Status       Info
     pulumi:pulumi:Stack            pulumi_work-dev
 +   ├─ openstack:compute:Keypair   pulumi-keypair1        created
 +-  └─ openstack:compute:Instance  pulumi-test-instance1  replaced     [diff: +keyPair~nam

Outputs:
  ~ instance_ip: "10.0.0.33" => "10.0.0.30"

Resources:
    + 1 created
    +-1 replaced
    2 changes. 1 unchanged

Duration: 29s

Permalink: https://app.pulumi.com/keiichi-hikita/pulumi_work/dev/updates/9
```

なるほど。
ちゃんと差分更新もできるようですね。

あと、リソースの依存関係を直接オブジェクトライクに（ `key_pair=keypair.name` みたいに）できるのは新鮮。

### 普通に間にロジック挟むとどうなる？

このPulumiを最初見た時に感じていたのは、

1. ただのSDKみたいなものでしか無いなら依存関係とかかけないじゃないか？それって便利？
2. ただ、テンプレートじゃなくてロジックをかけるということは、間に色々処理が挟めて良さげ

という２点でした。

前者はTerraformやHeatがやってくれる順序制御より劣るのでは？と思っていた点だし、後者はPulumi良さげと思っていた点。（.tfの中にHCLでロジックかけないしね）

上記１は何ら問題ないことがわかったので、今度は２を試してみる。

__main__.pyを以下のように修正。

```python

import pulumi
from pulumi_openstack import compute

def func():
    print("Hello World!!")

# Create an OpenStack resource (Compute Keypair)
keypair = compute.Keypair('pulumi-keypair1')

# Create an OpenStack resource (Compute Instance)
instance = compute.Instance('pulumi-test-instance1',
	flavor_name='m1.tiny',
        key_pair=keypair.name,
	image_name='cirros-0.4.0-x86_64-disk')

# Export the IP of the instance
pulumi.export('instance_ip', instance.access_ip_v4)

func()
```

足したのはfuncの部分。

で、pulumi up。

```
% pulumi up
Previewing update (dev):

     Type                 Name             Plan     Info
     pulumi:pulumi:Stack  pulumi_work-dev           1 message

Diagnostics:
  pulumi:pulumi:Stack (pulumi_work-dev):
    Hello World!!

Resources:
    3 unchanged

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

`Diagnostics:` ・・・というところに `Hello World!!` と表示されている。。。
つまり関数はupした際のプランニング時点で評価されて、`以上!!` なのだろうか。

試しにyesしてみる。

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

     Type                 Name             Status     Info
     pulumi:pulumi:Stack  pulumi_work-dev             1 message

Diagnostics:
  pulumi:pulumi:Stack (pulumi_work-dev):
    Hello World!!

Outputs:
    instance_ip: "10.0.0.30"

Resources:
    3 unchanged

Duration: 1s

Permalink: https://app.pulumi.com/keiichi-hikita/pulumi_work/dev/updates/10
```

とりあえずロジックは実行されるようですね。

でも、これだとあまり現実的ではない。

一旦もう少し踏み込んで見るために、一度Destroyしてみます。

```
% pulumi destroy
Previewing destroy (dev):

     Type                           Name                   Plan
 -   pulumi:pulumi:Stack            pulumi_work-dev        delete
 -   ├─ openstack:compute:Instance  pulumi-test-instance1  delete
 -   └─ openstack:compute:Keypair   pulumi-keypair1        delete

Resources:
    - 3 to delete

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

     Type                           Name                   Status
 -   pulumi:pulumi:Stack            pulumi_work-dev        deleted
 -   ├─ openstack:compute:Instance  pulumi-test-instance1  deleted
 -   └─ openstack:compute:Keypair   pulumi-keypair1        deleted

Resources:
    - 3 deleted

Duration: 15s

Permalink: https://app.pulumi.com/keiichi-hikita/pulumi_work/dev/updates/11
```

あー、ちゃんと消えましたね。

で、次にDiagnosticsに入る前にちょっと小休止。

### さっきから出ているURLはなんなのか？

ずっとさっきから、upしたりdestroyした後にこういうURLが出ています。

`Permalink: https://app.pulumi.com/keiichi-hikita/pulumi_work/dev/updates/11`

これなんなのだろう・・・とブラウザから接続してみて驚きました。


なんと作業履歴が全て残っているんですね!!

![task_list.png](https://qiita-image-store.s3.amazonaws.com/0/317673/8cc07de6-98c8-2cb4-e837-b7c309f82342.png)


例えば直近のURLにつなぐとこんな画面に。

![task_detail.png](https://qiita-image-store.s3.amazonaws.com/0/317673/76c671b0-8b11-7500-9db7-e65329de3ce3.png)


**こりゃすごい。**

ここまでは、そこまで便利なのかなぁと思っていたのですが、この機能はいいですね。
履歴全部残してくれるならもし商用適用した時に切り分けとか相当楽になるんじゃないか？これ。

### 話を戻してDiagnostics

本来のDiagnosticsの使い方はまだ勉強中なんですが、こんな事はできるのだろうか。

```
import pulumi
import random
import string

from pulumi_openstack import compute

def random_name(n):
   randlst = [random.choice(string.ascii_letters + string.digits) for i in range(n)]
   return ''.join(randlst)
  
# Create an OpenStack resource (Compute Keypair)
keypair = compute.Keypair('pulumi-keypair1')

# Create an OpenStack resource (Compute Instance)
instance = compute.Instance(random_name(10),
	flavor_name='m1.tiny',
        key_pair=keypair.name,
	image_name='cirros-0.4.0-x86_64-disk')

# Export the IP of the instance
pulumi.export('instance_ip', instance.access_ip_v4)
```

これでpulumi upしてみましたが、どうやら問題はないようです。

```
% pulumi up
Previewing update (dev):

     Type                           Name             Plan
 +   pulumi:pulumi:Stack            pulumi_work-dev  create
 +   ├─ openstack:compute:Keypair   pulumi-keypair1  create
 +   └─ openstack:compute:Instance  dcvnkfk5Iu       create

Resources:
    + 3 to create

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

     Type                           Name             Status
 +   pulumi:pulumi:Stack            pulumi_work-dev  created
 +   ├─ openstack:compute:Keypair   pulumi-keypair1  created
 +   └─ openstack:compute:Instance  hLTf397LGV       created

Outputs:
    instance_ip: "10.0.0.13"

Resources:
    + 3 created

Duration: 18s

Permalink: https://app.pulumi.com/keiichi-hikita/pulumi_work/dev/updates/12
```

ただ、planと実際の結果で名前が変わっちゃってますね。

これだと、毎回pulumi upの度に、pulumiは `差分がある！` と錯覚してしまうようでした。（錯覚と言うか文字列的に差があるんだから当然っちゃ当然）

試しにそのままpulumi upを実行してみてください。インスタンスのReplaceが発生しちゃいます。

とはいえ、これはなにかに使えそうな気はします。
TerraformのProvisionerみたいな使い方ができるのだろうか。ちょっと調べてみよう。

例えば、近隣のNW機器にnetconfでコンフィグを投げつつインスタンス起動！みたいな使い方できたら嬉しいのと、それをapply時とdestroy時にロジック的に分岐させれれば嬉しいな。
イメージ的に、

```python
import pulumi
import random
import string

from pulumi_openstack import compute

def netconf_set():
    if pulumi.is_apply: <-- こんな設定とかをどっかから引く。環境変数とかないかな
	    // set netconf settings <-- なんか設定
    else:
    	// remove netconf settings <-- 入れたもの消す
  
# Create an OpenStack resource (Compute Keypair)
keypair = compute.Keypair('pulumi-keypair1')

# Create an OpenStack resource (Compute Instance)
instance = compute.Instance('pulumi-test-instance',
	flavor_name='m1.tiny',
        key_pair=keypair.name,
	image_name='cirros-0.4.0-x86_64-disk')

# Export the IP of the instance
pulumi.export('instance_ip', instance.access_ip_v4)

netconf_set() <-- この中で勝手に分岐
```

・・・な感じなんですが多分無理だろな。


とりあえずざっと使ってみましたので、これからDocument読みます。

今わからないと思っているのは、

* terraform.tfstate的なものはどこにあるんだ？（作業ディレクトリにも ~/.pulumiにもそれっぽいのがないように見える）
* Diagnosticsって何？
* 他色々機能ありそう

とはいえ、Providerが増えてきたら便利そうですねぇこれ。

感じたメリットは、

* ロジックとして書けるのでAPI化とかは楽そう
* シングルPythonファイルで動くイメージなのでFaaS的な実行に向いている感
* 依存関係がちゃんとかけれる
* 主要クラウドはサポートしている

一方使い始めたら困りそうなのは以下ですがどんどん改善されていきそうな気も。

* Providerのリポジトリにgo/python/nodeと全部入っているからなのかわからないけどProviderのドキュメントがおざなり（読んでもよくわかんない）
* Providerのどこに実際のCRUDポーリングの処理が入ってんのかよく分かってない
** Classだけだと全然情報足らんよね
** 例えば [これ](https://github.com/pulumi/pulumi-openstack/blob/master/sdk/python/pulumi_openstack/compute/instance.py) とかほとんど情報量無い
** あとPycodestyleしておいてくれると見やすいのに


またなにか分かったら書きます。