Pulumi SDKとGoogle Cloud SDKを組み合わせてみる

この記事は NTTコミュニケーションズ [Advent Calendar 2019](https://qiita.com/advent-calendar/2019/nttcom) の20日目です。
昨日は @kirikei さんの [Googleのデータ可視化&モデル分析ツール What-if Toolで覗いてみるTitanic生存者予測](https://qiita.com/kirikei/items/2a4ef062fadd4a93203a) でした。

# はじめに
入社6年目、主にインフラエンジニアな仕事をしています。
今回は、最近盛り上がりつつある[Pulumi](https://www.pulumi.com/)という Infrastructure as Code(IaC)のツールの簡単な説明と、プログラミング言語でIaCができるというPulumiの特性を生かした利用方法について、紹介したいと思います。

# Pulumiとは
本家HPの[Architecture & Concepts](https://www.pulumi.com/docs/intro/concepts/)には、以下のように書かれてます。
>The Pulumi Cloud Development Platform is a combination of tools, libraries, runtime, and service that delivers a consistent development and operational >?control plane for cloud-native infrastructure. Not only does Pulumi enable you to manage your infrastructure as code, it also lets you define and manage your infrastructure using real programming languages (and all of their supporting tools) instead of YAML.

要するに、Infrastructure as Codeを実際のプログラミング言語を用いて、定義・管理できるよ、ということのようです。現在サポートされている言語は、以下の4種類で、.NETとGoはまだPreviewのようです。バージョン2.0から正式サポートらしいです。([Pulumi 2.0 Roadmap](https://www.pulumi.com/blog/pulumi-2-0-roadmap/)より)

- Node.js - JavaScript, TypeScript, or any other Node.js compatible language
- Python - Python 3.6 or greater
- .NET Core - C#, F#, and Visual Basic on .NET Core 3.0 or greater
- Go - statically compiled Go binaries (documentation coming soon)

また、デプロイできる[Cloud Provider](https://www.pulumi.com/docs/intro/cloud-providers/
) は続々と増えているようです。

# Kubernetesにデプロイしてみる
今回は例として、以下のような構成をKubernetesにデプロイしてみようと思います。
nginxのweb server(nginx)を3台をロードバランスし、外部からアクセスできるようにする構成です。

```nginx.yml
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: nginx
spec:
  backend:
    serviceName: nginx
    servicePort: 80

---
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: NodePort
  ports:
  - port: 80
    targetPort: http-port
  selector:
    app: nginx

---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers: 
      - name: nginx
        image: nginx:alpine
        ports:
        - name: http-port
          containerPort: 80
```

## PulumiのKubernetes SDKを利用してデプロイする

Kubernetes の Manifestの内容を、ほぼそのままObjectとして定義し、newでinstanceがデプロイされるようなイメージです。

```index.ts
import * as k8s from "@pulumi/kubernetes";

const appName = "nginx";
const appLabels = { app: appName };
const deployment = new k8s.apps.v1.Deployment(appName, {
  metadata: { name: appName },
  spec: {
    selector: { matchLabels: appLabels},
    replicas: 3,
    template: {
      metadata: { labels: appLabels},
      spec: { 
        containers: [{ name: appName,
                       image: "nginx:alpine",
                       ports: [{ name: "http-port", containerPort: 80 }]
        }]
      }
    }
  }
});

new k8s.core.v1.Service(appName, {
  metadata: { name: appName,
              labels: deployment.spec.template.metadata.labels
  },
  spec: {
    type: "NodePort",
    ports: [{ port: 80, targetPort: "http-port" }],
    selector: appLabels
  }
});

new k8s.networking.v1beta1.Ingress(appName, {
  metadata: { name: appName },
  spec: { 
    backend: { serviceName: appName, servicePort: 80 }
  }
});
```

## YAML形式のManifestを読み込んでデプロイする
Kubernetesでは、ある程度YAMLでエコシステムが回ってる場合もあると思うので、再度Pulumiで焼き直しをすることが面倒なこともあると思います。その場合、YAMLをそのまま読み込んでデプロイすることも可能です。

[Deploying a YAML Manifest](https://github.com/pulumi/pulumi-kubernetes#deploying-a-yaml-manifest)にあるサンプルコードのように、PulumiのKubernetes SDKには、ローカルにあるYAMLファイルを読み込み、YAML内のリソース種別(KubernetesのKind)を自己解決しデプロイする機能、を用意してくれているのでかなりシンプルなコードで済みます。

```index.ts
import * as k8s as "@pulumi/kubernetes";

const manifest = "nginx.yml"
new k8s.yaml.ConfigFile(`k8s/app/${manifest}`, {
  file: manifest
});
```


## Google Cloud StorageにあるYAML読み込んでデプロイする
エコシステムの中で、別のソフトウェアがYAMLのCreate/Validationを担っており、Google Cloud Storageなどの別のストレージを介してYAMLがやり取りされる場合、さらに状況が複雑になります。

現状、Node.jsのPulumi SDKでは、ローカルのファイルから読み込む機能以外はサポートされていません。代わりに、YAML形式のDataがあれば、そこから同様のことをしてくれる機能はあるようです。
そこで、 Google Cloud のSDKと組み合わせて利用してみます。

```index.ts
import * as gcs from "@google-cloud/storage";
import * as k8s from "@pulumi/kubernetes";

async function readFileFromGcs(bucket: gcs.Bucket, file: string): Promise<string> {
  const remoteFile = bucket.file(file);
  return new Promise((resolve) => {
    let yamlData = '';
    remoteFile.createReadStream()
      .on('data', function(data) {
        yamlData += data;
      }).on('end', function() {
        resolve(yamlData);
      });
  });
};

async function main(): Promise<any> {
  const storage = new gcs.Storage({keyFilename: './key.json'});
  const bucket = storage.bucket('test-pulumi');

  const manifest = "nginx.yml"
  const yamlData = await readFileFromGcs(bucket, manifest)

  new k8s.yaml.ConfigGroup(`k8s/app/${manifest}`, {
    yaml: yamlData });
};

main();
```

## 実行結果
Google Cloud のSDKと組み合わせて利用した場合の実行結果が以下です。

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

     Type                                        Name                 Plan
 +   pulumi:pulumi:Stack                         manifest_on_gcs-dev  create
 +   └─ kubernetes:yaml:ConfigGroup              k8s/app/nginx.yml    create
 +      ├─ kubernetes:core:Service               nginx                create
 +      ├─ kubernetes:networking.k8s.io:Ingress  nginx                create
 +      └─ kubernetes:apps:Deployment            nginx                create

Resources:
    + 5 to create

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

     Type                                        Name                 Status
 +   pulumi:pulumi:Stack                         manifest_on_gcs-dev  created
 +   └─ kubernetes:yaml:ConfigGroup              k8s/app/nginx.yml    created
 +      ├─ kubernetes:networking.k8s.io:Ingress  nginx                created
 +      ├─ kubernetes:core:Service               nginx                created
 +      └─ kubernetes:apps:Deployment            nginx                created

Resources:
    + 5 created

Duration: 17s
```
きちんとYAMLが読み込まれ、デプロイできました。今回は省略しましたが、前述した 「PulumiのKubernetes SDKを利用してデプロイする」、「ローカルにあるYAML形式のManifestを読み込んでデプロイする」のパターンも同じ実行結果になります。

# おわりに
今回は、 Pulumiを用いてKubernetesにデプロイする方法の紹介をしました。特に最後の `Google Cloud StorageにあるYAML読み込んでデプロイする`では、PulumiのKubernetes SDKと、単なる Node.jsのGoogle Cloud SDKを組み合わせて利用しました。
PulumiのSDKの中だけだとできないことも、純粋なプログラミングとして実現できることは、Pulumiでは実装可能だというのが、terraformなどの別のツールとの違いな気がします。

今回、Pulumiを触ってみて感じたのは、普段使っている言語でインフラをデプロイするという、よりアプリケーション開発者がCloudを使うためのツールとしての１つのアプローチだなと思いました。企業のサービスやシステムでは、まだまだまインフラエンジニアと呼ばれる人たちが多く存在しますが、どんどんシームレスになり、ソフトウェアを書くことが当たり前になるべきだと思います。NTTコミュニケーションズの中でも、そう言う流れが徐々に強くなりつつあるので、自身も含め、精進していきたいなと思っています。

以上です。明日は、 @kanatakita さんの記事です。