GitHub Enterpriseユーザー会

GitHub Enterpriseのユーザー会が総代理店のマクニカネットワークスさん主催で開催されたので参加しました。今回はエンドユーザーとして登壇し、サイオステクノロジーでの導入事例も話してきました。写真は撮らなかった。。。開会から懇親会終わるまでで、7時間近く。長すぎ疲れたがとても有意義でした。登壇するとチヤホヤされて懇親会での情報交換捗る。ISMSがらみの話とか、コストの配賦のこととか(価格とか。。。)、技術的ではないところが生々しく貴重。

GitHubアップデート

  • 全ての企業はSoftware Companyではなく、Innovation Companyなのだ
  • ロードマップ
    • Issue Deletion: 古くなった課題を検索結果から削除する機能[beta]
    • Activity Dashboard: 組織のパフォーマンスを可視化する[WIP] <- これスクリーンショットしか見れなかったけどいい感じ!
    • Admin Visibility to Collaborator Invitations: Outside Collaboratorsの招待をコントロールする
    • Unrecognized Account Activity Notification
    • GitHub Actions [Beta]: Serverless DevOpsを実現する。まだアプライアンスには来ないし、おそらくアプライアンスに来てもインパクトは少ない。すでにサーバー持ってるのだからGitHub APIで普通に作ればいいだけだし。一方でSaaS版の方では強烈な機能ですね。エコシステムゴロシとなるのか、それともActionsの上にエコシステムが花開くのか。にわかに大好きなpulumiもActionsを調べてる時に見つけたのでした
    • Contents Attachement API [Beta]:
    • Issue Templateがどうにかしたって(聞き逃した)
  • InnerSource
    • 「書くべきコードはすでにもう書かれている」
    • InnerSourceとは、OpenSourceプロジェクトのように内部開発すること
    • 価値は、オープンなコラボレーション・開発者間の信頼関係・新しいアイディアの共有
    • 透明性、ガバナンスは大事

イントラマート事例

  • 概要
    • 開発本部 阿久沢さん
    • 合計100名ぐらい、70名が開発、サポート15名
    • organizationは100以上、機能単位で作ってる
    • リポジトリ1,150個、Jenkinsでビルドしてる。
  • GHE導入に至る課題
    • 動作しない不完全なコードがコミットされたり、Jenkinsビルドでエラーが発生。レビューが必要
    • レビューをしてもらうためにコミットしなければならない
  • 解決
    • AWS EC2上にGHEとJenkinsをおいて、ECSでビルド&テストするようにした
    • git-svnSubVersionから移行した。タグ付けされたものだけ移行して、過去の履歴はSVNを見る
    • チェッカーツールを独自に作った
      • モジュール名の命名規則、誤字脱字などもチェック
      • チェックが通らないとマージさせない
    • 他システム連携
      • Jenkins Organization Folder Pluginを利用。GHE操作に反応してJenkinsが動作
      • Redmine連携: pre-receive hookを利用してコミットコメントにリファレンス#が付いてないと弾く
      • Coverity連携: 静的解析ツール
      • API活用: pythonでJenkinsジョブとして実行している。
    • 新規リポジトリ作成ジョブを用意して、ルールに従ったプロジェクト立ち上げを強いている

フォーク事例

  • Webの受託開発現場でのGHE導入&浸透
    • 取締役 大沼さん、札幌でニアショアをやってる
    • 企業のプロモーションに関わるキャンペーンサイトを作ってる。デザインもする。
    • 110ユーザー、16オーガ、1,000リポ(1サイト-1リポジトリ
  • 導入前
  • 浸透策
    • 自社サイトのリニューアルでGHEの利用に慣れる
    • 新卒向け教育に組み込み!研修課題の提出先をGHEにした
    • 二段階認証を必須
  • 効果
    • Issue、Pullrequestテンプレートを活用
    • Projectのカンバン機能を活用。
    • webhookを使ったデプロイ:公開後の切り戻しなども可能
    • OSSへの参加する機運:OSSへの距離感が縮まり、IssueやPull Requestをあげたりするようになった

IIJ事例

  • 概要
    • 社員2,000名ぐらい
    • 470ユーザー、304オーガ、8,000リポ
    • GitHub Enterprise + drone.io + Teams
    • 開発言語やツールは自由に選べる文化
  • drone.io
    • GitHub Enterpriseにすると困ること。SaaSじゃないので連携ツールに制限が入る
    • CIツール、貧者のCircleCIたるdrone.io
    • Golangで書かれた、Dockerベースのツール
    • システム管理者が中央でスケーリングさせる。利用者は何も考えなくて良い
  • Confluenceと連携
    • GISTを展開する機能を組み込み済み
    • ブログが書けるので社内SNS的にも使われている
  • 面白い使い方
    • 社内ツールの公開
    • サポート部門で、マニュアル執筆やアナウンス文面の共有などにも使われている。
  • 利用しなくなるユーザー(dormant user)がたくさんいる

サイオス事例でもらったフィードバック

JFrog 製品紹介

  • Artifact Registryとは
    • ソフトウェア開発の成果物を保存しておく場所。CI/CDでは必要なもの
    • 依存管理:ビルドの再現性が保証される。
    • セキュリティやコンプライアンス:誰がいつプッシュしたのかなどの活動履歴を保存、認証認可もサポート
  • Sonatype Nexusが競合
  • Harbor

CircleCI 製品紹介

  • Pull Requestに含まれるコミットが、CIでのビルドに成功しているかを表示。マージできるかを表示。
  • GitHubのOAuthで接続する。
  • ビルドの高速化
    • ワークフローの中間成果物をキャッシュする
    • ビルドを並列化する
  • 秘密情報の取り扱い
    • 設定YAMLに書きたくないクレデンシャルなどはGUIで皆に見えないよう値を変数参照させるようにできる

Sider 製品紹介

  • コードレビュー支援ツール「開発チームの情報共有と成長をサポート」
  • DevOpsのサイクルの中で、コードレビューのところはまだ効率化されていない
  • Sider独自のコード解析ツールを提供(OSSとしても公開)。プロジェクト固有のルールをチェックする。
  • ルールをYAMLに書くと、Pull Request画面でSiderが示唆する内容を見ることができる
    • 一致および正規表現の文字列パターンでの検知
    • コードで書く検知
      • 現在はRubyPHP向け
      • 近日にTypeScript向けをリリース、その後にJava
  • 競合?:checkstyle / spotbugs / SonarQube

Lychee Redmine

その他

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が出ちゃう。謎すぎる。

久々に見たら"googleapi"も様子が変わってた

github.com

1年以上(もしかしたら2年ぐらいかも)間を空けたら、Google APIのNodeクライアントがまるっきり様子が変わってた。前に書いたときは"googleapis": "^23.0.0"ってpackage.jsonに書いてあって、今は"googleapis": "^37.2.0"。23から37ってすごいメジャーの上げ方です。auth周辺からエンドポイントの取得までだけでも、相当様子が変わってる。最新ではTypeScriptにしっかりなってました。前もTypeScriptで書かれてはいたがゴミみたいな型定義だったのです。当時使ってたDrive API v3を例にあげると。

 // googleapis/apis/drive/v3.d.ts (23.0.0)

declare function Drive(options: any): void;
export = Drive;

// 本当に2行だけ。

おいおいこれだけかよっていうのが、サーバは変わらず全く同じRest APIを叩くにも関わらず、クライアントは時を経て以下のように変わってます。

// googleapis/build/src/apis/drive/v3.d.ts (37.2.0)

// 省略。たくさんimportしてる
export declare namespace drive_v3 {
    // 省略。たくさん型定義している
    class Drive {
        about: Resource$About;
        changes: Resource$Changes;
        channels: Resource$Channels;
        comments: Resource$Comments;
        files: Resource$Files;
        permissions: Resource$Permissions;
        replies: Resource$Replies;
        revisions: Resource$Revisions;
        teamdrives: Resource$Teamdrives;
        constructor(options: GlobalOptions, google?: GoogleConfigurable);
    }
   // 省略。たくさん型定義している
}

// 結局、このファイルは2874行もあった。

ということで、23.0.0ではわずか2行だった型定義が37.2.0では2874行と、物量からして違う。型定義が充実しただけでなく、そもそもに呼び方も変わってるので、また一通りドキュメント読まないとなあ。認証認可関連も当然違うので、JWTで入って指定ユーザーになりすます手順なんてのから調べないといけない。で、サンプルもわかりにくくて結局ソースを追った。

import { google } from 'googleapis';

export async function authorizeDrive(
    email: string,   // サービスアカウントのEメール
    key: string,     // 秘密鍵
    subject: string, // なりすましたいアカウントのEメール
): Promise<any> {
    return Promise.resolve(await google.auth.getClient({
        clientOptions: { email, key, subject },
        scopes: ['https://www.googleapis.com/auth/drive'],
    }));
}

もうちょい、裏取ってみないと。この先も含めてまだ怪しい。

denoが着々と開発されている

node作者であるRyan Dahl氏がアーキテクチャ設計におけるリベンジとして、nodeと同じくV8をスクリプトエンジンに採用して作ってるTypeScriptのネイティブ実行環境「deno」は、着々と開発されていて今や簡単にインストールできて、すぐ使えます。

curl -fL https://deno.land/x/install/install.sh | sh

ちょっと前までインストーラPythonだったのにそれも変わってた。Windows環境向けにはPowerShellで用意されています。

github.com github.com

nodeとの互換性を全否定してnpmを取り込まなかったので、エコシステムを発生からやり直しのため道のりは遠いと思いますが、すでにポツポツdenoを用いたライブラリやフレームワークが出現していました。denolibオーガナイゼーションにtypeormの空リポジトリがあったのが気になる。

denoのAPIリファレンスに見当たらなかったので正規表現はどうするのかなーと前々から思ってたのだけど、本体issueに要望リクエストが一切出てこないので不思議に思ってたら、実はサクっと動いた。

$ deno
> (() => {
  const m = '0123456789'.match(/(23).*(67)/);
  if (m) {
  console.log(m);
  }
  return 0;
  })();
[ "234567", "23", "67" ]
0
> exit();
$

バッチリ正規表現が使えました。なるほど正規表現ってV8のレイヤーで実装されているんですね。ECMAScriptの標準化範囲だから、そりゃそうかと。一方でファイルおよびネットワークのIOなどはdenoのレイヤーであって、そこだけリファレンスでは説明していました。

手元でファイル操作したりネット叩いたりするのに、普通ならPythonRubyとかでやるようなことを、これからはdenoでできるな。

ファイルの冒頭にこれまでshやpythonでやるように #!~/.deno/bin/deno と書いて、chmod +xしてみたけどそれでは「bad interpreter」エラーが出て動かなかった。#!deno でもダメで、#!/Users/masataka_k/.deno/bin/deno と書けば動く。「~」で動かないのはbashに詳しくなくてよくわからないけどホームフォルダの実名を書いちゃうとポータビリティが無くなるから、次のようにenvを通すようにした。

#!/usr/bin/env deno
(() => {
  const m = '0123456789'.match(/(23).*(67)/);
  if (m) {
    console.log(m);
  }
  return 0;
})();

超面白い。じゃあdenoの本質であるsandboxモデルはちゃんと効いてるか試すと...

#!/usr/bin/env deno
import { mkdir } from "deno";
(async () => {
  await mkdir("new_dir");
  return 0;
})();

以下のように聞かれます。

Deno requests write access to "new_dir". Grant? [yN]

いちいち聞かれないよう、一行目に #!/usr/bin/env deno --allow-write とドキュメント通りに書き込みを許可するフラグをつけるとOK。試してみたらシェルのカレントフォルダでAPIが動くので、上記スクリプトをパス通しておけば、普通にコマンドとして使える。面白い。

まとめとして、deno版のmkdirコマンドは以下の通り。

#!/usr/bin/env deno --allow-write
import { mkdir, args } from "deno";
(async () => {
  if (args.length === 2) {
    await mkdir(args[1]);
  }
  return 0;
})();

今後

開発目標に挙げられている、Top Level awaitはまだ実装されていません。

$ deno
> await deno.mkdir("new_dir");
SyntaxError: await is only valid in async function
>

これができると、コンソールで対話的に実行するのに無名関数で囲って実行するような手間が省ける。

  • Aims to be browser compatible.

ブラウザ互換性とは何か?元々のRyan Dhalのプレゼンテーションを解釈するに、ここでは機能として重なる場合にはブラウザでの振る舞いを優先するということなのですけど、詰めれば(プレゼンテーションでは直接そう言ってないけど)将来はdenoで書いたアプリケーションが簡単にブラウザで動くってことなのか?まずGlobalにロードされるブラウザ固有のオブジェクトにTypeScriptでアクセスする方法を用意するのと、今は~/.deno/genに出力しているトランスパイル結果を名前付け直して出力してあげるオプションを作るのかな?

Expressからの...NestJS

続き。何がしたいかというと、REST-fulなAPIをテストしやすく拡張しやすい手法で作りたいだけ。しかしAWSサーバレスの上でTypeScriptにと縛っていくとなかなか答えにたどり着けなかったのです。

NestJS

nestjs.com

サーバサイドのフレームワークとしてLoopBack 4の代わりを探していたら検索チェーンの果てで突き当たる。aws-serverless-expressに載せようとしてる中で、おそらくそれら用途で Expressのインスタンスを触れる方法 が用意されていました。さほどExpress感は無いが確かにExpressだったのと、バリバリTypeScript前提な作り。流行りなのかCLIツールも備えてますがまずは無視してフルスクラッチに行きます。LoopBack 4では初期ドキュメントを追っかける限りではCLIを無視できなかったので、これは良い。

// lambda.ts
import { createServer, proxy } from 'aws-serverless-express';
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
import { NestFactory } from '@nestjs/core';
import { Server } from 'http';
import * as express from 'express';
import module from './module';
// 追記(後述):実はこの無名クラス取り込みはダメ!!!
// 変更例としては「import { UserModule } from './module';」 

let server: Server;
export default function (event: APIGatewayProxyEvent, context: Context) {
    if (server) {
        return proxy(server, event, context);
    }
    (async () => {
        const app = express();
        const nest = await NestFactory.create(module, app);
        await nest.init();
        server = createServer(app);
        proxy(server, event, context);
    })();
}

NestFactory.createおよびinitが非同期なのでめんどくさい。コールドスタートした時にハンドラが先に動かないようにしないと。一方でホットスタートした時にまた非同期の初期化作業はさせたくなくこんな作り。もっと上手い書き方ありそうなので考え続けてみます。ExpressもLambdaも出てくるのはここまで。これから先はほとんど全てNestJSの上だけで作っていくことが可能。

// /module/user.ts
import * as nest from '@nestjs/common';
import * as express from 'express';

@nest.Controller('user')
export default class {
// 追記(後述):実はこの無名クラスはダメ!!!変更例「export class UserController {」
    // 非同期もPromiseを返すだけでOK。簡単に対応できる
    @nest.Get()
    async findAll(): Promise<string[]> {
        return Promise.resolve(['a', 'b', 'c', 'd', 'e']);
    }

    @nest.Get('ping')
    ping(@nest.Response() res: express.Response) {
        res.send('PING!');
    }

    @nest.Get(':id')
    findOne(@nest.Param('id') id: string): string {
        return `This action returns a #${id} user`;
    }
}

デコレータが出てきちゃいました。LoopBack 4でも出てきましたがこいつは10年以上前のJavaフレームワーク繚乱時代を思い出させます。そこではクラスに付加情報としてJavaではアノテーションと呼ばれていた言語機能を活用してフレームワークIDEによる介入を行なっていました。上記では@Controllerと@Getで実はルーティングを表現できちゃってる。すなわちfindAllメソッドは、/userで呼び出され、findOneメソッドは、/user/:id で。

pingメソッドでは引数に@Responseデコレータを用いてExpress由来のオブジェクトをDependency Injectionしています。まさにJavaでよくやられた懐かしい手法です。

// /module/index.ts
import * as nest from '@nestjs/common';
import user from './user';

@nest.Module({
    controllers: [user],
})
export default class {
// 追記(後述):実はこの無名クラスはダメ!!!変更例、「export class UserModule {」とすべき。
}

NestJS、シンプルにとてもいいんじゃないですかね。LoopBack 4よりは洗練されている気がする。ただ、モジュール間の依存性をDIで解決と言ってるのだけど、特にインターフェイス疎結合にして実装を分離するのではなく、実クラスをそのままとり回してるのだけどそういうものなのか?Angular由来というDIが昔のSpringやSeasarで馴染んでたDIと狙いというか概念がちょっと違う気がして戸惑っている。まあ、DIの根幹としてコンストラクターを直で呼ばせずにフレームワークインスタンス生成して、それがちゃんとシングルトンで管理されていれば良いってことかな。

無名クラスを複数個並べた場合の問題

@Module({imports})や@Module({controllers})の値が配列をとるので当然モジュールがグラフ状に広がる作りが可能なはずです。しかし、ここまで私の手元ではそれぞれ一つのモジュールに一つのコントローラーでサンプル程度を作ってる時には動いていましたが、二つ目のモジュールやコントローラを追加すると期待した通りには動かない、DIがきちんと動かないことがありました。

当初、非Lambdaブートストラップ版を書いたり、それをwebpackでビルドして動かしてみたり、公式のサンプルをいくつか持ってきたりして実験していましたが、どうにもうまく動いたり動かなかったり。。。と、しばらくしてトランスパイルされた結果を眺めていて突然閃く。

上記記事で書いた私のコードではモジュールやコントローラを無名クラスで書いてexport defaultしていたのです。これがダメ。NestJSのサンプルもドキュメントも全てそうは書かれてなかったのですが、私のいつもの好む書き癖から無名クラスで書いちゃってました。で、これがNestJSのDIの仕組みの中で一つ目のdefault exportな無名クラスに「default_1」という名前がつけられるのは良いとして、二つ目の無名クラスも同じ名前で被らせてしまうみたいなのです。おそらくここで複数ソースコードに渡って管理されていて「default_2」とでも添字をインクリメントするか、importでつけた名前に書き換えてくれれば問題ないんですけどね。これはトランスパイラーの仕様由来なのかな?現象の再現方法がわかったのでNestJSのコードを追って原因究明とフィードバックをいつかの課題としておきたいと思います。

おそらく私以外にもハマる人いるはず!いないかな?NestJSのインジェクション対象(自分でコンストラクター呼び出しでインスタンスを作らないもの)は無名クラスはダメ。お気をつけください。

supertestとnock

github.com

自然な流れで、supertest。結構軽量な作りでhttp.Serverベースのアプリケーションを直接テスト内でアクセスするアサーションライブラリ。

github.com

こちらのnockは外部APIをモック化するライブラリ。

常用のjestにてsupertestとnockを組み合わせたら便利じゃないかなと想像している。まだ必要になるところまで届いていないので、想像だけ、備忘録としてだけ。

サーバレスでExpressと再会

eslint-typescript (様子見)

qiita.com

tslintとtslint-config-airbnbを常用していますが、中の人による上記記事ではいずれ世の中はtslintからeslintに移行されていくってことが予見されています。eslintの方が設定をpackage.jsonの中に書けるってのが好き。IDEやビルドツール群との連携は元々eslintの方が強いので、ルールのTypeScript対応が成熟してくれば。現在困ってないので今はちょっと待ちで、近い将来に移行してみようと思う。過渡的にeslintでtslintのルールを使うブリッジが提供されているけど特に狙いもないので冗長なので、eslintネイティブにいい感じになるのを様子見ながら待ち。

serverless-webpack

github.com

今まで使ってたserverless-plugin-typescripitでは、コードのコンパイルについてゼロ設定で対応してくれるけど、node_modulesはそのままクラウドへ持ち上げるのでデプロイの際に無駄に大きなパッケージを作ります。Jestとか@types以下全部とか、明らかに不要なものが含まれています。webpackでまとめて小さくするととてもコンパクトになり、結果としてデプロイ速度もスピンアップも早くなりとても有用です。

SPAを作る時にはクソめんどくさいwebpack設定も、この用途ではTypeScriptをコンパイルするだけなのでシンプルです。特にプロジェクト毎に変わるものでは無いので一度作ったらしばらく使い回しできる。

// webpack.config.js
const path = require('path');
const serverlessWebpack = require('serverless-webpack');
const webpackNodeExternals = require('webpack-node-externals');

module.exports = {
    mode: serverlessWebpack.lib.webpack.isLocal ? "development" : "production",
    entry: serverlessWebpack.lib.entries,
    resolve: {
        extensions: [
            '.js',
            '.json',
            '.ts'
        ]
    },
    output: {
        libraryTarget: 'commonjs',
        path: path.join(__dirname, '.webpack'),
        filename: '[name].js'
    },
    target: 'node',
    externals: [webpackNodeExternals()],
    module: {
        rules: [
            {
                enforce: 'pre',
                test: /\.ts$/,
                use: [
                    {
                        loader: 'tslint-loader',
                        options: {
                            typeCheck: true,
                            emitErrors: true,
                        },
                    },
                ],
            },
            {
                test: /\.ts$/,
                use: [
                    {
                        loader: 'ts-loader'
                    }
                ],
            }
        ]
    }
};

さらに、serverless.ymlではプラグイン登録と、ちょっと設定が必要。

# serverless.ymlの該当部分
plugins:
    - serverless-webpack

custom:
    webpack:
        includeModules:
            forceExclude:
                - aws-sdk
        packager: 'yarn'

webpack設定の方でnode_modulesに入るものを取り除き、serverless設定の方でパッケージに追加しています。custom.webpack.includeModulesはtrueを設定すると依存パッケージを全部取り込みます。aws-sdkは入れなくてもAWS環境側で用意されてますので、forceExcludeにホワイトリスト登録するとパッケージから省く&ワーニングが無いとのこと。

パッケージマネージャーは惰性でyarnを使ってますが、その後の機能追随と速度向上および元々のデフォルト感から、折見てnpmへ戻して良いかもと思っています。

Express

Reactアプリの対となるAPIを作る際に、引き続きAWSのサーバーレス製品群でやろうと思っています。今回の手法としてはServerless Frameworkとaws-serverless-expressでExpressアプリをLambdaへ載せてみてます。

github.com

今まではLambda上でExpressを使うことはなんとなく遅くてダメなんじゃないかなと思い込んでたけど、よく考えたらExpressのミドルウェアによってちょっとぐらいコールスタックが増えてもパワフルな現代のコンピューターの前にはなんの影響もないだろうし、一個のLambdaでエンドポイントだけ面倒見てもらってそれ以下はExpressのコードで書いた方が見通しもよく、変更にも強く、テストもしやすく、他クラウドへの移植性もよいんじゃないかなと。さらにはServerless Framworkで管理対象が巨大になってくると、背後のCloudFormationの由来にてリソース上限数制限200個が来るから、Lambdaが一個になってれば当然制限を避けられる。

社内でちょっと話してた時に指摘されたのは、多数Lambaがそれぞれ立ち上がるよりはコールドスタートが掛かりにくいってメリットはあるかもと。そしてLambdaなのにExpressで書いて良いってことになればExpressの既存知識や膨大な世間のコード資産が有効活用できそう。

# serverless.yml
service: express

provider:
    name: aws
    runtime: nodejs8.10
    stage: dev
    region: ap-northeast-1

plugins:
    - serverless-webpack

custom:
    webpack:
        includeModules:
            forceExclude:
                - aws-sdk
        packager: 'yarn'

functions:
    app:
        handler: lambda.default
        events:
            - http:
                  method: ANY
                  path: '{proxy+}'
                  cors: true

serverless.ymlの方では、methodが「ANY」で、pathが「{proxy+}」を設定します。これはAWSのドキュメントではプロキシ統合と呼んでいる。これでAPI GatewayではルーティングをせずにExpressの方に全てが回って来ます。

// lambda.ts
import { createServer, proxy } from 'aws-serverless-express';
import { eventContext } from 'aws-serverless-express/middleware';
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
import * as express from 'express';

// 普通にExpressのインスタンスを作る
const app = express();

// aws-serverless-express/middlewareでAPI Gatewayのイベントを搭載してみる
// http.Requestですでにアプリを作るのに十分な情報があるので、実戦だったらいらない機能かなと思います。
app.use(eventContext());
type Event = {
    apiGateway: {
        event: any;
    };
};
const dumpEvent: express.Handler = (req, res) => {
    res.json((req as unknown as Event).apiGateway.event);
};

// ここでは例なのでワイルドカードによる全部受けハンドラ登録をExpressに対して行っている。
// 実戦では、app.get('/dump', dumpEvent); というようにメソッドとパスの組み合わせを具体的に設定する。
app.all('*', dumpEvent);

// aws-serverless-expressでLambdaと直結する。
const server = createServer(app);
export default function (event: APIGatewayProxyEvent, context: Context) {
    proxy(server, event, context);
}

上記ではとりあえず例としてExpressでもワイルドカード利用でリクエスト全部受けのコード書いてますが、実戦ではExpressのルーティング作法で細かく普通にやればよく、そうすれば想定外パスへのリクエストはExpressの機能として404が返ります。

Serverless Components (様子見)

github.com

Serverless Frameworkが定義ファイルからCloudFormationテンプレートを生成してクラウド操作するのではなく、定義ファイルからコンポーネントインスタンスが生成されてそれらがAPIを直接操作する仕組みになっていた。これは良いアイディアで良いアーキテクチャだと思うけど、実装として今はまだ未成熟。Serverless Frameworkの先としてうまく融合 or 進化してほしい。

CLIだけでなく コードでデプロイなどの操作を行う機能 も素敵。そのほかそここことなく、Gulpのようなものを目指してるのかなと思った。

Amplify (お蔵入り)

amplify-cliのfunctionやapi機能がTypeScriptにまだ対応していない。また吐き出すScaffoldの構造がちょっと好きじゃないかな。そもそもにExpressでいいじゃないかと思ったのは、AmplifyでAPIを定義する際にCLIが表示する選択肢の中にExpress利用を見つけたからなので、もうちょいTypeScript対応が進めば個人的な見え方は変わると思います。でも今日ではなかった。

LoopBack (お蔵入り)

5年ぐらい前に、自分的には第1期としてNodeに触れていた時期に、チューンドNodeバイナリを提供していたStrongLoopを見つけました。StrongLoopは当時住んでた近所のSan Mateoに会社が所在していたので、そこで開催されたNodeのMeetupに参加するのにオフィスに入ったこともあります。連想的に彼らが作ってたNode上でRESTful-APIを作るCLI/フレームワークとしてLoopBackを触ったことがありましたが当時APIはGo言語で書くことになったのと、モノとして時期尚早感があったので使うことはありませんでした。ニューストピックとしてその後すぐにStrongLoopがIBMに買収されたことまで追いましたが、しばらく忘れていました。Expressを検討する中で懐かしい名前としてStrongLoopとLoopBackがまた出てきた。

ちょうど新しいバージョン4を出す前夜ぐらいなタイミングで、しかもその4はそれまでのコードベースを捨ててTypeScriptで書き直しているということで、俄然興味がでてきます。

v4.loopback.io

CLIで吐き出すのはTypeScriptオンリーでそれは素敵なのですが、フレームワークの作法としてクラスを書いてデコレータでDIするなど昔のJavaフレームワークかのような。製品WEBでGraphQLとかも語ってたのでこれまでの実績捨ててゼロから作り直した動機はこの辺かと期待しましたが。。。分厚い。。。スタックが深いよ、これは。また4はそれまでと異なりExpressベースではなくなっていました。そしてさすがan IBM Company。IBMクラウドを基本に考えているので、サーバレスにはちょっと背を向けてる風な感じ。各クラウドベンダーの独自なマネージドサービスを多用するサーバレスは現時点での競争負け組であるIBMの戦略には合わないみたい。そして、4が出てくるこのタイミングでわざわざExpressベースとはいえ古い3を使うかというと、なんか気が進まない。よってまたLoopBackはそっと閉じて心の隅にしまう。

Tech-on MeetUp#04「APIでつなぐ・つながるFinTecher」

Tech-on MeetUp#04「APIでつなぐ・つながるFinTecher」というイベントに行ってきた。

f:id:masataka_k:20190116005629p:plain
Stripeセッション

KDDIの金融な人たちのホストで、じぶん銀行au WALLETと連携するカブドットコム証券のセッションに、二本立てのようにゲストStripeで、それぞれFintechの入口のところを事例中心に関係者掛け合いの体で盛りだくさんでした。それぞれのコマが30分程度なので掘り下げ浅めで聴きやすくはありましたが、もっと時間をかけて実装的なことも語られるともっとよかった。 じぶん銀行API(Open API+独自API)はとても興味深いが、法人が気合い入れて取り組まねば利用のためのハードルは超えられなさそうという。。。以下は聴きながら取ってたメモの流し込み。

カブドットコム証券、KDDI

  • 株取引関連のAPIを提供し、ARコンセプトモデルを提供した
    • 巷にある商品をスマホカメラで撮影すると、メーカーを識別してそこから株情報を表示する
    • リコノミカル株式会社:17年創業で、AI+AR+IoT
      • 安く、早く。APIを利用して作る
      • 金融 x ARで提案している
    • 実際の商品を起点として株取引が始まるのが実は新しいのではないかと着目した
  • 課題
    • 金融APIがオープンではない
      • イノベーションを起こすような機能利用ケースが想像しにくい
      • 法制上の制限。銀行情報に触れるためには業登録が必要。。。
      • 証券APIは上記どちらもないため、株のAPIは直接的な制限がない
    • インフラとデバイスの問題
      • ARでサービスを作ってもスマートグラスが普及してない、容易にしそうにない
      • UXの進化
    • クラウド
      • それまでのWEB化してフル機能を提供する時代が過ぎる。
      • 機能を細分化して提供する時代、API化、サードパーティによるソリューション提供
        • 金融のコア機能は一つか二つで、その他の内容を組み合わせてサービス創造

じぶん銀行KDDI

  • Open API
    • 17年の改正銀行法。OpenAPIの提供を努力義務...やらねばならない
      • 2年以内に体制を整備する努力義務
    • 改正前はスクレイピングで各種情報を取得する努力を各社が行なっていた
      • 電子決済代行業者
        • 金融機関との契約が重い、やってない。セキュリティなど利用者保護はどうなってるのか?
        • Open APIの登場、しかし審査のある金融機関との契約が必要。果たしてこれでオープンなのか?
          • 反社勢力排除などの必要があった
  • じぶん銀行
    • モバイル特化で330万口座を持つ。KDDIMUFGの協業
    • モバイルでフルバンクサービスを提供している
    • 2018年9月にOpen API提供スタート
      • 残高照会
      • 入出金明細照会
      • AI外貨予測
      • totoキャリーオーバー額照会
      • オンライン口座振込
    • Alexaやマネーフォワード
  • 独自APIを利用したサービス
    • au WALLETとカブドットコムの連携
    • au WALLETプリペイドカード
      • 2000枚以上発行済み
      • 機能:決済・チャージ・参照・送金/払出・口座開設
      • 朝と昼にコンビニで利用される、夕方にスーパーで利用。それぞれ実店舗であり、ECは少ない
      • プリペイドなのに残高不足の心配がない
        • 残高が一定額を下回ると、じぶん銀行の口座から自動チャージ(非同期にチャージ)
        • 決済時に不足金額をリアルタイムチャージする。デビットカードに近い使い勝手になる
          • 同期でチャージと決済を行うので、応答時間が課題。現在は1秒以下で機能提供できている
          • 現在のところは*Payサービスはさほどトランザクションを発生させていない
    • これからのサービス
      • QRコード決済を19年4月に提供開始
      • キャッシュレス化を推し進めようとする日本で、チャージのトランザクションが増加するだろう

Stripe、600株式会社

  • Stripe:クレジットカード周辺の機能をワンストップ&ペーパーレスでAPIサービス提供する
    • Stripeを用いるとクレジットカード情報を直接取り扱わないので、結果としてセキュア
  • 600:無人コンビニ、AWSとStripeを利用している
    • 17年6月創業、18年6月から一般的にサービス展開
      • 冷蔵庫にタブレットが貼ってある
        • クレジットカードをスワイプすると冷蔵庫のドアが開く
        • 欲しいものを取り出すとタブレットに自動集計する。
        • タブレットの購入ボタンを押して、決済完了
    • ユーザーの身近で商品提供する(50m、2分の距離)
  • Fintech/キャッシュレスをビジネスマンとしてどう見るか
    • テクノロジーだけでなく、マインドが必要。しかし最近はマインドが変化してきた
      • 5年前であれば実現できない雰囲気だっただろう
      • 日本はキャッシュレスについて後進国ということはないし、伸び代もある
      • コンプライアンス課題、現金を取り扱うリスクからキャッシュレスへの流れ
      • キャッシュレスの先には決済を意識しなくても良くなっていく。
        • 定額課金など。
        • あらゆるシーンから「決済」は透明化の流れ
      • 銀行以外が付加価値を持つ潮流も、一旦不況下では信頼を担保する銀行が強いか?
  • ハードウェアビジネス
    • ソフトウェアと違って、デプロイサイクルが長く重い
      • 不具合の検出の課題
      • 見つけた不具合を更新する際の課題
    • オンラインとオフラインの境界が曖昧になってきているように、ハードウェアとソフトウェアも融合
      • 自分の専門領域を持ちながら隣接領域にも詳しくなると有利。ハードウェアがその一つだと貴重