CLOUD NATIVE INFRASTRUCTURE AS CODE

一年来サーバレス環境を作るのにServerless Framework(a.k.a SLS)を愛用してきましたが、昨今は関数実行部分だけでなく周囲の様々なマネージドサービスも同様に設定する必要が生じています。SLSはAWSのCloud Formation Templateを吐き出して実行する作業を一連のCLI操作にて行ってくれるのですが、その辺を察してCloud Formationと各サービスの設定方法を色々逆算しながらYAMLを書くのも面倒になってきています。おそらく時代の雰囲気を察してSLSはServerless Componentsで代替わりを模索しているのかなと想像していますけど、まだまだイージーに使える段階まで開発が進んでいません。そんな中、突如としてAWSが政治的にダメな案件で非AWSでサーバレスやる必要が出てきました。政治的ってなんだよと思いつつ、ビッグビジネスに政治はつきものです。SLSは非AWSにも対応していますがどうしてもやはりAWSがメインな作りで、AzureやGCP/Firebaseでは比較的手薄な感じとなり存在意義もさほど感じません。

pulumi

github.com

まずはAzure(のもっぱら課金体系)を調べる傍で、このあたり何かソリューションがないか探してました。偶然GitHub Actionsのブログから行き当たったのがpulumiです。ビデオを見て良さそうと思い、GolangとTypeScriptという構成要素が私の大好きっ娘たちなので、ちょっとアガる。サイトトップのREADME.mdに思いっきりSLSを意識して書かれていたのが "Skip the YAML, and use standard language features like loops, functions, classes, and package management that you already know and love." 。YAMLじゃなくて好きなコード、結局それが最強DSL。アゲアゲ。

www.pulumi.com

およそのところOSSにはしてますが、SLSとは違ってライフサイクルマネジメントのランタイムはフリーミアムSaaSで提供している。価格ページを見ると、これは上手いこと考えたなと感心しました。一人で使うぐらいなら無償でOKなフリーミアムをやりつつ、組織開発で必要な機能を有償にしていました。GitHub EnterpriseのようにセルフホスティングSAMLの対応とかできる。やりたい場合には要問い合わせの見積もり価格としているのはビジネス模索中だからなのか。pulumiはEx-Microsoftの人たちで創業したとのことで、私はGitHub Enterprise、TypeScript、もしかしてこれからAzuruと意図せずしてMSに絡め取られてますね。WindowsやOfficeは使わないけど、こういう風になるとは思わなかった。

どのぐらいOSSだけでできて、SaaSに依存するのはどのぐらいかはまだわからないけど、WEBを一瞥しての印象は、さすが$15M調達した会社が本気で作っているモノであり、試してみて筋が良ければ$50/月は払ってもいいかなって気でいる。

Hello World@pulumi

とりあえずAzureは不案内なので、AWSチュートリアル実行。

$ brew pulumi
$ pulumi new hello-aws-javascript --dir ahoy-pulumi
# ここで対話作業
$ cd  ahoy-pulumi
$ yarn install
$ pulumi up
# ここでも対話作業、デプロイ完了後にアプリURLとコンソールURLが表示される

HomeBrewでCLIをインストールします。私の環境ではSLSのためにセットアップしているので、~/.awsに認証情報など格納されていますから、おもむろにpulumiコマンドを叩く。とりあえずチュートリアルの通りにやってみたけど、ここでpulumiのWEBコンソールサイトへの認証が求められます。GitHub・GitLab・Attlasianのアカウントどれかで入れます。その後カレントフォルダの下に"ahoy-pulumi"フォルダを作って動くアプリのプロジェクトを作ってくれていました。さらにその後にnpmが走るのですが、私は意図せず走られないようにnpmを削ってありyarnだけにしているので、yarn installを実行して依存するnodeパッケージをインストールしました。チュートリアル通り、pulumi upでデプロイします。途中コンソールで本当にデプロイするか聞かれるのでyesを選んで進む。コンソールに出てきたURLを呼び出してみたら、はい、ちゃんとサンプルアプリが動きました。

pulumiを探る

チュートリアルのコンソールの最後に、WEBコンソールサイトへのURLが出てるのでクリックすると!

f:id:masataka_k:20190216225910p:plain

今、デプロイしたアプリの状態がまとめられた画面があります。これは便利!SLSだとこういうものはなくて、AWSのWEBコンソールで確認するだけだった。何を作ったがキレイに出ててさらにはクリーンアップ方法などメンテナンスのための操作説明もありました。

プロジェクトのファイル構成を確認すると、pulumi固有のものはPulmi.yamlとPulumi.dev.yamlの二つだけ。

# Pulumi.yaml
name: ahoy-pulumi
runtime: nodejs
description: A simple AWS serverless JavaScript Pulumi program

これは、pulumi newを実行した時にコンソールで対話型に聞かれた内容。

# Pulumi.dev.yaml
config:
  aws:region: us-east-1

ファイル名の中にある「dev」はpulumi用語としてstackと呼ばれるものの名前で、これもAWSリージョンも先にコンソールで対話型に聞かれていました。あとはソースコードに色々環境を作ってそうな内容が書かれているだけ。他になんかあるだろうと探すと、~/.pulumiフォルダが作られていたのを見つけました。~/.pulumiフォルダを探ると以下のものがあります。

  • credentials.json:このファイルにはpulumiサーバーへのアクセストークンが保存されている。
  • templates:このフォルダの中にサブフォルダとしてhello-aws-javascriptがあった。結果わかったのはpulumi newコマンドの後の引数"hello-aws-javascript"はボイラープレートの名前で、そのボイラープレート実体はここにある。他に正規表現で表せば、/[aws|azure|gcp|kubernetes|openstack]-[go|javascript|python|typescript]/ という多数フォルダがあった。
  • plugins:このフォルダの中には、resource-aws-v0.16.8というフォルダがあってその中にはpulumi-resource-awsという名前の実行権限のついたバイナリファイル。
  • workspaces:このフォルダの中には長いファイル名の謎JSONファイルが。内容はシンプルに{ "stack": "dev" }とだけ。

ボイラープレートを眺めても、結構直感的に作れそうな感じ。オレオレプレートをすぐ作れそう。

Expressを載せる

pulumi newに頼らず書いてみます。

// (プロジェクトルート)/index.ts
import * as aws from '@pulumi/aws';
import { createServer, proxy } from 'aws-serverless-express';
import * as http from 'http';
import * as express from 'express';

let server: http.Server;

async function handler(event: any, context: any): Promise<aws.apigateway.x.Response> {
    if (!server) {
        const app = express();
        // 以下にExpressの作法でルーティングを設定する
        app.get('*', (req, res) => {
            res.send(`Hello World!\r\n${req.url}`);
        });
        server = createServer(app);
    }
    // この書き方を見つけるまで長時間ハマった!
    // これまでやってたproxy(server, event, context)はそもそもdeprecatedになっていて
    // pulumiの渡してくるcontextも結果を返すメソッド群が省略されているため動かない
    return await proxy(server, event, context, 'PROMISE').promise;
}

const api = new aws.apigateway.x.API('document-handling', {
    stageName: 'dev',
    routes: [
        // Expressで総受けするにあたって、/と/{route+}をそれぞれ登録必要
        // SLSではこの辺適当でも動いていたけど、AWSドキュメント的にはこちらが正しい
        {
            path: '/',
            method: 'ANY',
            eventHandler: handler,
        },
        {
            path: '/{route+}',
            method: 'ANY',
            eventHandler: handler,
        },
    ],
});

exports.endpoint = api.url;

私はこの手のものを書くのに、プロジェクトルートにソースを置くのを嫌い、必ずsrcフォルダとかlibフォルダを切ってその中にソースを置いていましたが、pulumiはルートのindex.tsを自動で見に行く仕掛けです。ドキュメントの該当箇所を読むとpackage.jsonのmainで指定すればいいように書いてありますが.tsを指定してもダメ。じゃあ何を指定するのかという点で悩まずに素直にフォルダ構造を浅くします。そこで「endpoint」という名前の文字列をエキスポートする必要があります。エキスポートしたものはコンソールならびにWEBの方にも出力される仕様。動的に生成される値を最後にレポートする仕組みで、これはスマートだと感心した。

# Pulumi.yaml
name: poc-document-handling
runtime: nodejs
description: One of the PoC project

上記のようにPulumi.yamlを書きます。こちらはプロジェクトのソースコードとしてリポジトリに保存して良いような恒久的設定をまとめるファイル。ここまでで書き物は終わり。

$ pulumi config set aws:region ap-northeast-1

CLI実行で、Pulumi.poc-document-handling.yaml が生成され、動作する。こちらはリージョンとかクレデンシャルとかのリポジトリに保存すべきでないような動的設定を逃すファイルでした。

さらにNestJSを載せ...られない

pulumi - AWS Lambda - Expressと来て、さらにNestJSを載せようとしましたが、どうにも動かない。pulumi - AWS Lambda - Expressが動き、AWS Lambda - Express - NestJSも問題なく動くようになったのに、全部入りでpulumi - AWS Lambda - Express - NestJSでルーティングすると404が出ちゃう。謎すぎる。