denoが楽しい

TypeScriptの実装系として爆速進化中のdenoです。ちょっとしたシェルスクリプトで書いたりするようなことを、denoで書くのが楽しい。今朝は花粉症の鼻づまりが原因で早起きしてしまったので、午前中は遊びコード書いてた。前に書いたコード断片をかき集めてきて半分ぐらい終わったのですぐ。

# ~/.bash_profile
export PATH=~/.deno/bin:~/bin:$PATH

denoのインストール後、上記の通りパスを通しておく。~/binをスクリプト置き場とする。

今はまだIDEの支援も得られないのでコードどうやって書くかと言うと、テキストエディタでゴリゴリ書くしかない。そのうち愛用中エディタATOMプラグインでtslintやPrettierを使ってみようかと思うけどまだやってない。TDDで書くために、deno_stdと呼ばれる標準のTypeScriptライブラリ中のテストツールを用いた。簡単なテストランナーとアサーションが用意されている。書いたのは写真ファイルのごちゃごちゃしたファイル名を整理するスクリプト

// ~/bin/renames_test.ts
import { test, runTests } from 'https://deno.land/std/testing/mod.ts';
import { assertEquals } from 'https://deno.land/std/testing/asserts.ts';
import { getFileDescriptor, getNumberStr, getOption } from './renames_lib.ts';

test(function NO_EXT() {
  const f = getFileDescriptor('example#1');
  assertEquals(f.skip, true);
});

test(function MULTI_DOT() {
  const f = getFileDescriptor('example.#1.jpeg');
  assertEquals(f.name, 'example.#1');
  assertEquals(f.ext, '.jpg');
});

test(function DOT_FILE_Large() {
  const f = getFileDescriptor('.IGNORE');
  assertEquals(f.skip, true);
});

test(function SKIP() {
  assertEquals(getFileDescriptor('test.bmp').skip, true);
  assertEquals(getFileDescriptor('test.jpeg').skip, false);
});

test(function GET_NUMBER_STRING() {
  assertEquals(getNumberStr('t3st#12p.png'), '012');
  assertEquals(getNumberStr('t3st#12p.png', true, 0, 4), '0003');
  assertEquals(getNumberStr('t3st#12p.png', false, 0, 4), '0012');
  assertEquals(getNumberStr('t3st#12p.png', false, 5, 4), '0017');
  assertEquals(getNumberStr('t3st#12p.png', false, -5, 4), '0007');
});

test(function GET_OPTION() {
  assertEquals(
    getOption('P', 'pre', ['CMD', '-P', 'test']),
    { flag: true, value: 'test' },
  );
  assertEquals(
    getOption('P', 'pre', ['CMD', '--pre', 'test']),
    { flag: true, value: 'test' },
  );
  assertEquals(
    getOption('P', 'pre', ['CMD', 'x']),
    { flag: false, value: '' },
  );
  assertEquals(
    getOption('P', 'pre', ['CMD', '-P', '-test']),
    { flag: true, value: '' },
  );
});

runTests();

面白いのは、testに渡すテスト関数の名前がテスト名になるところ。「NO_EXT」とか「MULTI_DOT」とかが用いられる。無駄がない。

// ~/bin/renames_lib.ts
const KNOWN_EXTS = ['.jpg', '.jpeg', '.png', '.tiff', '.gif', '.svg'];

type Descriptor = {
  ext: string;
  name: string;
  skip: boolean;
}

export function getFileDescriptor(
  fileName: string,
): Descriptor {
  const result = fileName.match(/(.*)(\..*)$/);
  if (result && result[1].length !== 0) {
    let ext = result[2].toLowerCase();
    if (ext === '.jpeg') {
      ext = '.jpg';
    }
    if (KNOWN_EXTS.includes(ext)) {
      return {
        ext,
        name: result[1],
        skip: false,
      };
    }
  }
  return {
    ext: '',
    name: fileName,
    skip: true,
  }
}

export function getNumberStr(
  name: string,
  start: boolean = false,
  offset: number = 0,
  len: number = 3,
): string {
  const result = name.match(/([0-9]+)/g);
  if (result) {
    const str1 = start ? result[0] : result[result.length - 1];
    const num = Number(str1) + offset
    const zero = new Array<string>(len - 1);
    zero.fill('0');
    const str2 = zero.join('') + num;
    return str2.slice(-(len)); // ほんまかいなコード
  }
  return '';
}

type Option = {
  flag: boolean;
  value: string;
}

export function getOption(
  s: string,
  l: string,
  args: string[],
): Option {
  let i = args.indexOf(`-${s}`);
  if (i === -1) {
    i = args.indexOf(`--${l}`);
  }
  if (i !== -1 && i < args.length - 1 && !args[i + 1].startsWith('-')) {
    return {
      flag: true,
      value: args[i + 1],
    }
  }
  return {
    flag: i !== -1,
    value: '',
  }
}

export function renames() {
  const dir = getOption('R', 'dir', Deno.args);
  const start = getOption('S', 'start', Deno.args);
  const offset = getOption('O', 'offset', Deno.args);
  const len = getOption('L', 'len', Deno.args);
  const prefix = getOption('P', 'prefix', Deno.args);
  const postfix = getOption('T', 'postfix', Deno.args);
  const dryrun = getOption('N', 'dryrun', Deno.args);

  console.log(Deno.args.join(' '));
  console.log(`dir: { flag: ${dir.flag}, value: ${dir.value} }`);
  console.log(`start: { flag: ${start.flag}, value: ${start.value} }`);
  console.log(`offset: { flag: ${offset.flag}, value: ${offset.value} }`);
  console.log(`len: { flag: ${len.flag}, value: ${len.value} }`);
  console.log(`prefix: { flag: ${prefix.flag}, value: ${prefix.value} }`);
  console.log(`postfix: { flag: ${postfix.flag}, value: ${postfix.value} }`);
  console.log(`dryrun: { flag: ${dryrun.flag}, value: ${dryrun.value} }`);

  for (const file of Deno.readDirSync(Deno.cwd())) {
    // 2019.5.21 deno v0.6.0からFileInfo.pathが廃止され、.nameに統一
    // const oldPath = file.path;
    const oldPath = file.name;
    if (file.isDirectory() && !dir.flag) {
      console.log(`${oldPath} skip: directory`);
      continue;
    }
    const descriptor = getFileDescriptor(file.name);
    if (descriptor.skip && !file.isDirectory()) {
      console.log(`${oldPath} skip: ext`);
      continue;
    }
    const o = offset.flag ? Number(offset.value) : 0;
    const l = len.flag ? Number(len.value) : 3;
    const num = getNumberStr(descriptor.name, start.flag, o, l);
    if (num.length === 0) {
      console.log(`${oldPath} skip: no number`);
      continue;
    }
    const p = prefix.flag ? prefix.value : '';
    const t = postfix.flag ? postfix.value : '';
    const newName = `${p}${num}${t}${descriptor.ext}`;
    if (dryrun.flag) {
      console.log(`Dry run: ${oldPath} -> ${newName}`);
      continue;
    }
    console.log(`${oldPath} -> ${newName}`);
    Deno.renameSync(oldPath, newName);
  }
}

グローバル変数「Deno」が、ファイル操作やネットワークI/O等のdenoが用意するAPIのエンドポイントになっている。importソースの表記はちょっと違うけど、あとは普通にTypeScript3.3。上記コードで考え込んだのは、文字列のフォーマットを行うところ。この環境で使えるスマートなやり方がわからなかったので、配列を0で初期化してjoinして文字列にくっつけてsliceする、ほんまかいなと。

#!/usr/bin/env deno --allow-read --allow-write
import { renames } from './renames_lib.ts';
renames();

最後にコマンドとしてchmod +xしたファイルを用意する。テストしやすくするために分けた。この最後の実行ファイルの名前をrenames(拡張子なし)として、あとは普通にシェルでタイプすると使える。例外処理が甘いけど、自分で使う分には便利。

若いうちにやっとくべきこと

ameblo.jp

僭越ながら同窓にて旧知の教育評論家おおた氏の短文にいい事書いてあった。若い時に聞いても意味がワカンナイんだろうなあとも思うが、全くその通り。汗臭い思い出も甘酸っぱい思い出もそれぞれ糧として後には得られないほど貴重で大きい。微妙に老害ゾーンに入りつつある私のソフトウェアの仕事を振り返っても、説く内容について美しさが数段落ちるが構造は似ている。コードは汗臭く、カネは甘酸っぱい。

浪人したり留年したり長く親のスネをかじってて毎日ダラダラしていた学生時代、1995年初夏に就活終わった後でやってみた人生初のアルバイトがいきなり連結会計パッケージ開発だった。学校で会計を学んでいたので連結会計ならばともかく、プログラミングの方がわからない。それでも朝から晩まで貪欲に知識を吸収しながらひたすら書き続けた。ビジネストラストのWikipediaで沿革に残る1995年から96年にかけての行は、期間にして一年にもならないけど、20年以上経った今でも鮮明に覚えている。初めて触ったVisualBasicから繰り出す稚拙なロジックで会計なのにRDBMSも使わず(=使えず)に書き、ウィザードの初期名「Form1」を残したまま何十もの画面をひたすら作った。リリース後、クライマーズ・ハイが悪い作用をして就職を断りまたダラダラする。あまりに酸っぱくてその後すぐにDelphiに走り、偶然が重なりニフティサーブのBBSでFDelphiを作ったところから結局のところ現在まで繋がってる。技術もコミュニティも面白くて、のめり込むうちに気がついたら人脈ができ、連結大王で貰ったカネとでグルージェントを起業した。それまでまともに会社勤めた事がなかったけど、起業の後は切羽詰まった資金繰りでカネの事を考える毎日で、辛いも甘いも黒いも白いも様々な経験ができた。

起業して5年も経った時、ヒトでもめてカネにいろんな事があって、解決策として旧恩を頼りビジネストラスト(すでにデカくなってて、連結大王のファーストコードは書き直されて残ってなかった)にグルージェントを売った。ヒトの問題を解決し、程なくしてITバブル後の混乱も収まったのでカネ面でも安定し、そのおかげで余裕もでき、たまたまFDelphiの縁が蘇ってSeasarファウンデーションって言うコミュニティを作った(私の目線ではSeasarはFDelphi2.0)。Seasar傘下で、MayaaというJavaフレームワークを書いた。経産省未踏ソフトウェアに応募して開発人件費(実は15百万円/年)の支援が得られたのでそれこそ仕事として朝から晩まで書いてた。この辺はリアルタイムにずっとこのブログに書いてたので、読み返せば今でも土臭い感じで恥ずかしい。が、貴重なプログラミングの加圧トレーニングとなってた。その後、SeasarファウンデーションからOSS繋がりとして知己を得たサイオスへビジネストラストに無茶言ってグルージェントの株式を転売してもらって、サイオスでは役員になった。自己流で社内の空気と資金繰りだけなんとかしておけばいいそれまでの経営と違って、予算立案とか中期経営計画とか大学の教科書でしか読んだ事ないのが連続して求められる。初めは何度か失敗もしたが、小僧でも自分で会社何年もやってりゃ筋力ついてる。なんとかなった。

その後、グルージェントのビジネスモデルを大改造してSaaSサブスクリプションの会社にしたり、アメリカに4年行き、FDelphiを一緒に作った新井氏と20年ぶりの合流でまた会社作ったり、色々やってきての今。おおた氏が説くように汗臭い思い出があればなんとかなり、甘酸っぱい思い出があれば身を持ち崩さない。技術トレンドがどんどん変わってもなんとか追いつけるし、経営でも今後そうそうドブ板を踏み破ることはないと思う。

技術者人生早いうちに一年ぐらい朝から晩までひたすらコード書く時期があったら、35歳過ぎても大丈夫だよ。そして若いうちから少々でも仕事でカネにまみれて清濁知れたら、年取ってから少々権限増えて忖度に囲まれても、浮ついて滑稽なことになったりはしない。

JP_Stripes 東京#11

f:id:masataka_k:20190222204551p:plain

最近続けて参加しているJP_Stripe東京に参加しました。Stripe Issuingを実装された方が登壇すると聞いていたのでワクワク参加。実は私も米国のアカウントを使ってAPI招待してもらってますので、一通り実装してみてました。ただ、API叩けてもそれ以上にユーザー管理や認証を作らなければならないし、UIも作らないとアプリとして成り立ちません。多忙にてなかなかそこに手が付いていなかった。そんな中、作りたかったものが製品レベルにすでに作りこまれてました。

以下、メモ貼り付け。

JP_Stripe Connect

  • 初の全国大会
  • 3/21 大阪で開催

Stripe アップデート

Staple 〜Stripe IssuingとAtlasを使ってる〜

  • クラウドキャスト株式会社
  • 国内キャッシュレス環境
  • ポジショニング
    • 法人、SMB向け。
    • 法人決済市場は900兆。個人の3倍。
    • 全ての事業ペイメントを集約したい。入社1日目に従業員へ配布され、会社と従業員とのお金のやり取りをシンプルに
    • 会社と従業員とのお金のやり取りが非効率。現金と紙(レシート/領収書)、特に経費精算。
    • 法人キャッシュレスで解決したい。
      • コーポレートカードを発行
        • 与信の問題
        • 振込手数料:個人が立て替えているから発生する。
    • 国内キャッシュレスのレイヤー
      • 国際ブランド
      • プロセッシング・API
      • フロントエンド、ブランド
  • Stripe Issuing
    • カード発行をAPIで行える、革命的!
    • 物理カードおよび仮想カードを発行できる
    • 始めるのに時間がかからない、長期の契約が不要、高額な手数料がいらない
    • 日本ではまだ使えない。招待オンリー

f:id:masataka_k:20190222205356p:plain

  • Stripe Atlas
    • 米国で会社の設立を簡単に行うサービス
    • WEB上で情報入力。書類を作ってくれる。銀行口座や納税者番号も払い出し
    • 初年度$1,000、次年度以降$500ぐらい必要。

Twilio + Stripeの事例

  • DIGITALJET(岡山) 古里さん
  • 「電話でペイ」Twilio
    • カード番号入力を音声で案内。番号・有効期限・セキュリティコードを入力
    • 決済を行う
  • XMLを書くだけで可能になる。日本円決済も設定するだけ。
  • 手数料は使った分、決済成立毎にTwillioが15円。Stripeが決済金額の3.6%

StripeでのCVCアタック対抗策

  • Cloud9 Holdings 森さん
  • Jupiterという電話占いサービス(Stripe禁止業務かもの危機)
  • Stripeではどうするか
    • Elementsでカード情報を送る場合:Tokenをそのまま使えばカード情報を登録されてないので都度入力。
  • Stripe Radar
    • ルールを設定することで、怪しい決済を防げる。
  • CVCアタック対策
    • CVCの間違いはAPIの戻りで返ってくる
    • カード情報入力から決済までを一括トランザクションにすればRadarで引っ掛けて永久ブロック可能
    • カード登録と決済を別トランザクションにすると、CVCエラーの回数を数えて決済を止める機能を実装する必要がある。

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に出力しているトランスパイル結果を名前付け直して出力してあげるオプションを作るのかな?