maybe daily dev notes

私の開発日誌

酒場: お山の大将

はい、ということで今回はお山の大将に行ってまいりました。以下レポートです。

tabelog.com

往路

平日の19時にお店到着。店はそれほど混んでいませんでしたが、カウンター席に案内されました。おそらくテーブル席は予約で埋まっていたのでしょう。今日も大人気です。

お通し

今回のお通しはゆで卵にカレーを掛けたものでした。 大抵はちょっとしたもつ料理が出てくるのですが、このパターンは初めてです。

ここのカレーは初めて食べまして、予想以上に美味しかった。多分モツも煮込まれているんでしょうか、独特の旨味がありました。

いつもどおり酎ハイ180円を頼みます。月火水木金土いつも180円です。安い! この日は酎ハイ2杯とビー酎半分でフィニッシュしました。

おまかせで6本頼みました。カシラ、シロ、テッポウかな… 相変わらず臭みもなくてうまいです。この前自宅で猿真似の砂肝串を手作りしたのですが、 割とクセがあって食べづらかったんですよね。この点、やはりプロの技術は違います。

モツのことは以下の記事も参照。私はまだ識別能力低いです。お山の大将は豚モツがほとんどなので、その事前知識も使いながら識別していく感じになります。

tmokmss.hatenablog.com

サイド

モツ煮込みを頼みました。小・中で量を選べたのですが、中を選択。意外と多かったので、1人だったら小一択だと思います。これも様々なモツが煮込まれていて、見た目にも楽しい。味もやさしい味噌汁のような感じでとても良かったです。

あとは生ピーマン。ここの生ピーマンは豪快で、縦半分に切ったピーマン4つに味の素と塩が振られて出てきます。キンキンに冷えたみずみずしいピーマンと味の素が存外マッチして、大変うまいです。

他にもマグロぶつとか色々食べたような気がします。写真残しておくべきでした。

シメ

野菜炒めを初めて頼んでみました。もやし・ニラ・キャベツだけで構成される、いわく美人になる炒めものらしいです。味は黒コショウと魚粉かな?このお店は魚粉を多用しがちなので、初めてのメニューですが不思議と慣れた味ですね。良い肴になる野菜炒めでした。

会計

2人でたらふく飲み食いして4350円でした。安い!このお店はいつも端数を切り捨ててくれるので、さらにお得感がありますね。

帰路

帰りは近場のおふろの王様に行きました。おふろの王様とお山の大将、名前似てますね。おふろの王様では若者が夢を語り、お山の大将ではおじさんが競馬を語ります。私もいつか語れるものができればと思います。

それでは、また来週。

ワタミ系列店チートシート

背景

以前町の飲み屋に入ったのだが、実はワタミ系列店だったということがあった。 特にワタミがきらいな訳ではないが、そこはかとないガッカリ感は否めない。今後はワタミの店はワタミの店だと覚悟してから入りたいので、この記事ではワタミ系列店の列挙を目指す。

この外観は町の歴史ある大衆居酒屋という感じしかない だがワタミ

結論

ワタミ系列店は以下の15種類くらい。 (2022/5/23現在)

  1. 焼肉の和民
  2. 幸せの焼肉食べ放題 かみむら牧場
  3. 居食屋「炭旬」
  4. Restaurant & American Bar「T.G.I. Friday's」
  5. CHINA BISTRO「WANG'S GARDEN」
  6. 炉ばたや「銀政」
  7. にくスタ
  8. ミライザカ
  9. 三代目 鳥メロ
  10. から揚げの天才
  11. すしの和
  12. こだわりのれん街
  13. テキサス風メキシカン「TEXMEX FACTORY」
  14. しろくまストア
  15. 厳選煮干しらーめん にぼ助

ただし、他にもある可能性は捨てきれないので注意。

調査

まず簡単な調査の結果、ワタミ系列店を列挙する問題は、意外にも自明ではないことが分かった。公式サイトに掲載されていない系列店もあるためだ。

公式サイト

ワタミ公式サイトに列挙されている外食事業(国内) は、以下の12種類。からあげの天才もワタミ系列だったのか…

見ればわかるが、ここには上のしろくまストアが含まれていない。しろくまストアの公式サイトのお知らせによればしろくまストアは間違いなくワタミ系列店である。この点で、ワタミ公式サイトの記述は完全に信頼できるものではないことがわかる。

Wikipedia

公式が当てにならないと集合知に頼るしかない。Wikipediaをみてみよう。

ja.wikipedia.org

現行の展開店舗 というセクションには、10種の店舗が列挙されている。これは公式サイトの12よりも少ないため、これだけでは当てにならなそう。他には、グループ沿革 のセクションに、店舗の種別ごとの出店・撤退イベントが記されていることがわかる。このイベントを追っていけば、今残存しているワタミ系列店舗がわかるかもしれない。

これらのイベントを手で分析し、「撤退イベントが明記されてない店舗」 でかつ 「ワタミ公式サイトにも、Wikipedia展開終了の店舗リストにも 現行の展開店舗リストにも記載されていない店舗」 を割り出した。これが以下の6種類:

  1. 仰天酒場 和っしょい2
  2. まる焼きチキン&セルフBAR「GABURI」
  3. テキサス風メキシカン「TEXMEX FACTORY」
  4. しろくまストア
  5. もつ★りき
  6. 厳選煮干しらーめん にぼ助

これらが、少なくとも今のWikipediaから推測できる、ワタミの公式サイトには載っていないもののワタミ系列店である可能性がある店ということ。

さて、それぞれの行く末をググってみると、店舗が今の現存するものはさらに少なく、次の3種類に絞られる。これらはリンクも貼っているが、2021・22年頃にワタミ系列店であることを確認できるサイトが見つかっている。

一旦、このリスト+ワタミ公式サイトのリストが、ワタミ系列店の完全なリストであるとみなして良いだろう!

この方法の限界は、Wikipediaが出店イベントを取りこぼしている可能性があること。より正確な方法としては、ワタミヒストリー を参照することが考えられる。しかし、これも2020年版までしかなかったり、そもそも面倒くさすぎる、またワタミが特定のイベントを削除している可能性も否めないので、確認は読者の宿題としておく。

まとめ

強烈なワタミアンチみたいになってしまったけど、ブラック企業であったこと以外にワタミにネガティブな印象はない。しろくまストアのてっぺん串(100円) もうまかった。

てっぺん串赤 (100円)

それにしてもワタミ、ステルスしすぎである。しろくまストアの看板は紛うことなく歴史ある大衆居酒屋、TEXMEX FACTORYの公式サイトからにじみ出る中小チェーン店臭は異常だし、にぼ助に至っては関係を裏付ける根拠が株主向けの資料しかない。それほどワタミとの関係性を隠しておきたいのか、あるいは店の運営が安定するまでは喧伝したくないのか、どちらだろうか。

とはいえ、ものすごいスピードで店舗のスクラップアンドビルドを繰り返す姿勢にはリスペクトあるのみ。今後とも安旨の飲食店を提供し続けてください。

帰納的プログラミング

定義

プログラムに変更Xを加えるとき、類似の機能A,B,Cの実装を確認し、その共通部分から、変更Xに必要な実装を導くことがあります。

この一連の作業を、この記事では帰納的プログラミングと呼びます。

帰納(きのう、英: Induction、希: επαγωγή(エパゴーゲー))とは、個別的・特殊的な事例から一般的・普遍的な規則・法則を見出そうとする論理的推論の方法のこと。 出典: Wikipedia

帰納的プログラミングの例

このコミット帰納的プログラミングにより作成されました。

github.com

帰納する際は、次のコミットを参考にしています。

MDEV-28007 Deprecate Spider plugin variables regarding statistics per… · MariaDB/server@f31642e · GitHub

MDEV-28297 Deprecate spider_internal_offset · MariaDB/server@1866fb0 · GitHub

MDEV-27981 Deprecate spider_internal_limit · MariaDB/server@e87c710 · GitHub

帰納的プログラミングの良いところ

帰納的プログラミングを用いることで、開発者は対象のシステム・コードベースに対する知識をほとんど持たない場合でも正しそうな実装をすることができます。そして、知識を取得するコストを掛ける必要がないので、作業スピードも比較的速いです。

これはYAGNI原則に従っていると言えるかもしれません。ある作業が実際に必要になるまでは実行を遅延させることで、将来の不確定さに対するコストを最適化する考え方です。

上の例で言えば、作業者はMariaDBに関する周辺知識を習得するコストを将来に遅延させ、必要最小限のコストでPRを作成したのです。今回は間違いなく良く機能した例でしょう。

一方で、私の過去を振り返ると、必ずしもこれに頼りすぎるのは良くないと思うことがあります。次のセクションでまとめます。

帰納的プログラミングの欠点

帰納的プログラミングが悪く機能する場合も考えられます。この方法の最大の欠点は、既存のコードのパターンから抜け出せないということです。

すべての機能が、帰納プログラミングで既存のコードをコピペしたようなパターンで実装されていったとしましょう。この時、実はこの繰り返しパターンは抽出して共通化すべきだったかもしれません。DRY原則を常に実践する必要は一般的にありませんが、メンテナンス性やコードのアーキテクチャを考えると共通化すべき場面はやはり存在します。帰納的プログラミングを行使して他の方法を検討する手間を省いている場合、このような可能性を見逃すことになります。

あるいは、ある日既存のコードを大規模にリファクタすることになったとしましょう。全体最適なリファクタをするためには、変更対象の箇所に関する知識のみならず、コードベースに対する広範な知識が求められることがしばしばです。 これまで帰納的プログラミングを行使して必要以上の知識習得を遅延してきた場合、この局面になって初めて多くの知識を習得する必要が生じるのです。そうなると、果たして今回のリファクタタスクはいつものスピードで達成可能でしょうか。

帰納的プログラミングに過度に頼ることは、上記のようなリスクをはらんでいます。

欠点を回避するために

帰納的プログラミングで素早くタスクを終えられるのは良いことです。一方で、いつも必要最小限の知識を学ぶだけだと、今後のより高度なタスクに立ち向かえないリスクもあります。

このリスクを回避するためには、その時のタスク遂行には必要ではない周辺知識を学ぶ時間を意識して確保すると良いでしょう。チリも積もれば山になります。個々のタスクで積み上げた知識は、いずれより難しいタスクに立ち向かうときに効果を発揮するに違いありません。

あるいはタスクの割り振り方が非常に優れている場合は、常にタスク遂行に最小限の知識を学んでいるだけでも、順当にステップアップできる場合があります。信頼できるメンターがいる場合は、これに乗っかってしまうのも良いでしょう。*1

まとめ

帰納的プログラミングという言葉を定義し、その良し悪しを考えました。 簡単なタスクだからといってすぐに完了させてしまうよりは、あえて寄り道してみるのも大事かもしれません。

*1:今回のMariaDBのケースもこれに当てはまる気がしています。

MariaDB にコントリビュートした話 - VSCode on EC2で開発環境の構築

数少ない読者へ愛を込めて

MariaDBに初めてコントリビュートした時の作業記録をまとめる。Issue自体は一番簡単なものを渡してもらったので、最も初歩的なことはじめの参考程度にご覧ください。

開発環境の用意

初めに開発環境を整える。MariaDBのビルドは重いとよく話に聞いていたので、手元のMacでは心もとない。仕事柄使いやすいEC2を採用。オレゴンに c5.2xlarge を建てた。

↓を参考にすればよいが、今回はAmazon Linux 2上で動かしたいので、UbuntuではなくCentOS向けの手順を使う。

nayuta-yanagisawa.hatenablog.com

sshログインしたら、これを打てばOK。

sudo yum-builddep mariadb-server
sudo yum install git \
      gcc \
      gcc-c++ \
      bison \
      libxml2-devel \
      libevent-devel \
      rpm-build

VSCodeで開発する

CLIでは開発できない人間なので、VSCodeをつなぐ。この手順を参考にする。

blog.serverworks.co.jp

これにより、エディタのみVSCodeでファイルの実体の管理やビルドの処理はEC2に任せることができる。

blameも見やすくて良い

ちなみによりセキュアにしたければ、SSMを経由してEC2に接続すると良い。これにより、EC2のセキュリティグループでポート22のIngressを許可しなくてもssh接続できるようになる。具体的な方法は以下のブログに詳しい。

blog.logical.co.jp

ビルドする

git clone --branch 10.3 https://github.com/MariaDB/server.git mariadb-server
mkdir mariadb-server/bld && cd $_
cmake -DCMAKE_BUILD_TYPE=Debug ..
cmake --build . --config Debug -- -j 4

作業ディレクトリに大量のファイルが出力されるので注意しよう。上のコマンドのように、最初に一時ディレクトリへ cd することをおすすめする。

また、うちの環境だと -j 4 の前に -- を付けないと動かなかった。-j の後の数字は並列数みたいなので、c5.2xlargeに合わせて8にした。これでコアを使い切れる。

何をやっているかよく分かってないが、エラーが出てないので問題ないのだろう。ちなみに重いと覚悟してたが、初回ビルドだと数分程度だった。今回はさほどイテレーションもなかったので、それほどつらくない。

Issueを直す

今回取り組むIssueはこちら。柳澤さんが認める一番いい good first issue である。

jira.mariadb.org

前職以来使ってないJIRAの見方を忘れたが、おそらく親も子もない単体Issueと思われるので、これだけを手がかりに着手する。

まず、タイトルにある SPIDER_HAS_MY_CHARLENgrepすると、2箇所しかでてこない。確かにこれは good first issueのようだ!

コードを見ると、 常に #define SPIDER_HAS_MY_CHARLEN されるようになったので、 #ifdef SPIDER_HAS_MY_CHARLEN の条件分岐を消して良いことがわかる。もちろん、絶対に通ることのない #else の方を消す。

消して、ビルドが通ることを確認する。

cmakeを3に上げる

と思ったらエラー!

実は作業ブランチを途中で切り替えたのだった。リポジトリ を見ると最新ブランチが10.9ぽいため。10.3のときはビルドが通っていたが、10.9では以下のエラーでビルドが失敗するようになっていた。

CMake Error at CMakeLists.txt:107 (CMAKE_MINIMUM_REQUIRED):
  CMake 3.0.0 or higher is required.  You are running version 2.8.12.2

以下を参考に、cmakeのv3をインストールする。

www.matbra.com

インストール後再度ビルドすると、無事にビルドが完了した。

ちなみに実際は作業を開始すべきブランチがIssueにより異なる模様。今回も結局 10.10 が正解だった。ここはMaintainerに聞くか、IssueのFix Version/s を見るのが良さそう?

Pull requestを出す

テストの実行の仕方がわからないが、GitHubにPRを立てれば自動で実行されるようだ。それを使おう。

GitHubでフォークして、cloneしたリポジトリのURLを書き換える。

git remote set-url origin git@github.com:tmokmss/MariaDB-server.git

念の為 CONTRIBUTING.md を確認したが、特に変わったルールはない模様。他のPRも参考にしながら、PRを書いた

CI、通れ!

CIやレビューはどれくらいかかるかわからないので、今日はここまで。

追記) なんとものの1時間でレビューが完了し、マージされた! もちろん最小規模のPRであるのも理由だろうが、それでもこの速さはなかなかない。 一開発者としては気持ちの良い経験だった。

感想

まだ深淵の縁にも立ってないと思うので、何もわからない。git bisect の話をよく耳にするが、それを使う程のバグfix系Issueだと少し深淵を覗けるのかもしれない。とはいえ、今回のIssueくらいなら、手軽に初められる印象だった!

また時間があれば手を出したいと思う。

米を圧力鍋で炊くべき5つの理由

米は炊飯器より圧力鍋で炊くべきだと思う。理由は以下:

圧力鍋は速い

圧力鍋は普通の炊飯器よりも早く炊ける。これは高温高圧で一気に米を加熱するため。以下のグラフを見てほしい。

水のフェーズダイアグラム*1

水は気圧と温度が与えられれば、その時の状態(水、氷、水蒸気)が決まる。例えば1気圧 (100kPa)で100℃だと、水蒸気と水の境目に当たる。これがいわゆる沸点である。

今、圧力鍋は水蒸気を閉じ込めることで圧力を上げることができる。その時の気圧は物によるが、例えばうちのは115kPaが作動圧力となる。このときの沸点をグラフから読み取ると、なんと125℃!これだけの温度で加熱されれば、固い米も瞬時で柔らかくなるのだ!

具体的には、圧力鍋で白米を炊くと

  1. 米入れる
  2. 115kPaまで加圧する (8分位かかる)
  3. 加圧した状態で2分待つ
  4. 火を止めて減圧・蒸らさせる (10分くらい)

最短計20分で炊けてしまう!

圧力鍋はうまい

圧力鍋で炊く米は炊飯器と同等にうまい。特に玄米は炊飯器よりもふっくら炊けて好評である。おそらくは高温高圧が、炊飯器では不可能な効果をもたらしているのだろう!

ただし、誤って加熱しすぎるとお粥と化してしまうので注意しよう。この辺りの不便さもまた一興なのだ。

圧力鍋は役立つ

圧力鍋は多くの用途に活用できる。炊飯はもちろん、おでん、カレー、シチュー、肉じゃが、煮物全般などなど。しかも、これらは炊飯と同様に、他の手段でやるよりも圧倒的に早く仕上がる!おでんの大根を3時間も煮込む必要などもはやない。ただ125℃で30分も煮れば完成である。

また、圧力鍋は大抵大きい。この大きさを活かして、低温真空調理の水槽として使ったり、パスタを茹でる寸胴としても、何ならただのバケツとしても利用できるだろう。

寸胴があれば家二郎も作れたりして… (これは店二郎)

圧力鍋は長持ち

圧力鍋は高温高圧に耐える必要があるため、本体の作りがしっかりしている。このため、基本的に20年は使えると言われている (諸説あり)。

当然バルブやパッキンなどは消耗品のため定期的な交換が必要だが、それをあまり合って本体の頼もしさは一見の価値あり。ちなみに↓がうちで愛用している鍋で、もう買って2年になるがこの輝きである。ステンレスの鍋は焦げ付き・サビ知らずで良い。デカくて重くてイヤになることもままあるが、後18年使い続けたい。

圧力鍋は楽しい

圧力鍋が実現する高温高圧環境では、なかなか身近にないものである。特に115kPaで加圧中、鍋のフタはパンパンに張り、フタごと吹き飛んでくるんじゃないかと不安すら感じさせてくれる。実際は3重くらいの安全装置が実装されているので安心して良いが。

ある種の極限環境をキッチンで簡単に実現できるこの装置、一台持っていても損はないだろう。

以上。炊飯器は売って圧力鍋を買おう。

AWS CDK コンストラクトライブラリ開発に関する5つのTips

AWS CDK TIpsシリーズの記事。

最近 deploy-time-build というCDK コンストラクトをリリースできたので、その過程で得られた知見を共有する。

github.com

Tips

プロジェクトの初期化はprojenで一発

プロジェクト生成ツールのprojenを使うことで、コマンド一発でリポジトリのベースを生成できる。詳しくはここを参照。コマンドはこんな感じ。

mkdir cool-construct
cd cool-construct
npx projen new awscdk-construct

これに共通で必要なもの (package.jsonやeslintrc.jsonGitHub Actionsの構成などまで!) はすべて入っているので、開発者は文字通りコンストラクトを定義するコードだけ書けば、自作コンストラクトを良い感じにリリースできる。 この開発体験はかなり良いと思った。

また、projenで管理するプロジェクトは基本的にすべて .projenrc.js というファイルで諸々の変更を管理する。例えば package.jsoneslintrc.json なども。対象のファイルは↓のようなコメントが記載されているので、そういったファイルは直接編集しないように注意しよう。 (CDK管理下のリソースは手動で変更しないのと似ている。)

~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen".

ではどう編集すれば良いのかというと、リファレンスがここにある。ただし、今のところCDKのReferenceほど見やすくないので、TSの型定義を追うほうが楽かも。projen、ドキュメントまわりは発展途上に見える。

ちなみに、Projenの使い方はこの記事も詳しい。 qiita.com

コミットメッセージは conventional commits に従うべし

www.conventionalcommits.org

Conventional commits は、機械的に解釈できる統一的なフォーマットをもつコミットメッセージを書くべしというルール。ざっと例を挙げると

  • fix: hoge: バグ修正のコミット
  • feat: hoge: 機能追加のコミット
  • fix!: hoge: APIに破壊的変更を伴うバグ修正のコミット

他にも↑のサイトに例があるので、ぜひ参照されたい。BREAKING CHANGE というフッターをつけたり、 chorerefactor といったコミットをすることも許可されている。

このルールは semantic versioning を採番する上で便利に機能する。例えば

  • fix のコミットだけある場合はパッチバージョンを上げる
  • feat のコミットだけある場合はマイナーバージョンを上げる
  • fix!feat! のコミットがある場合はメジャーバージョンを上げる

といったように機械的に判別できるので、結果としてconventional commitsに従うリポジトリでは自動的にsemantic versioningのバージョン番号を採番できる。

Projenで作成されるテンプレートでもこれが機能していて、コミットメッセージを conventional commits に従って書けば、↓のようにリリースノートやバージョン番号も良い感じにしてくれる。このため、CDK コンストラクトを作るときはとりあえずこのルールに従うのがお勧め。

ただし、projenではメジャーバージョンのbump (バージョンを1上げること) だけは機械的には実行せず、開発者が明示的にbumpする必要があるらしい。このあたりの挙動はメジャーバージョンが0か1以上かでも異なるらしいので、まだ試せていない。(少なくとも v0.x.yのうちは、破壊的変更があってもマイナーバージョンがbumpされるだけで済む。)

Lambda関数を含めたい時は?

コンストラクトにLambda関数を含めるときの方法をまとめる。

いつもの書き方で大丈夫?

お手製のコンストラクトライブラリにはLambda関数が含まれる場合もあるだろう。そのような時、通常のCDKアプリを書くときと同じ感覚でLambda関数を定義すると、好ましくない場合がある。

例えば、 NodejsFunctionPythonFunction を使って定義したとしよう。この場合、そのコンストラクトを利用する全ユーザーはCDKデプロイ時にそのLambda関数をビルドする必要が生じる。これは明らかに無駄である。できるなら最初からビルド済みのコードをパッケージとして提供する方が、余計なビルド時間をユーザーに課さなくても済む。

また、Custom resourceのハンドラーとしてLambda関数を使う場合、 SingletonFunciton という特殊なコンストラクトを利用することが推奨されている。これは同一スタック内で何度定義されても実体は必ず1つだけになるFunctionなので、ライブラリとして提供するCustom resourceのハンドラーには重複定義を避けられる点で都合が良い。

実際、CDK公式が提供しているCustom resourceハンドラの実装 (例: BucketDeployment) は大抵これが利用されている。ここで重要なのは SingletonFunctionlambda.Function を直に継承するリソースのため、 NodejsFunctionPythonFunction の機能を利用できないということ。この点でも、通常のCDKコードとは少し異なる方法でLambda関数を定義すべきであることがわかる。

Construct library way

ではどう定義すればよいかだが、まだベストプラクティスは定まっていなさそうに思える。基本的には、Lambda関数は外部ライブラリ(Lambda実行環境に標準装備のaws-sdk/boto3は除く)に依存するとデプロイが面倒になるので、できるだけ標準ライブラリだけで書くのが楽。とはいえ、少しの工夫で依存関係を含めることもできる。現実的な方法としては、以下の4つが考えられる:

  1. Lambdaを素のPythonで書く (Node.jsよりは標準ライブラリの機能が充実している)
  2. Lambdaを素のTypeScriptで書く (projen build中にtscのステップを挟む必要あり)
  3. Lambdaを素のJavaScriptで書く (tscも不要のため楽)
  4. LambdaをTypeScriptで書き、外部ライブラリの依存はesbuildでバンドルする

「素の」とは、「外部ライブラリに依存しない」という意図で書いた。 1〜3の方法は割と読んで字のごとしで、定義する際は単純にハンドラのコードのパスと関数名を指定するだけで良く、ビルドプロセスも単純。外部ライブラリの依存を避ける消極的な解決策とは思えるが、実際巷のコンストラクトライブラリを見るとこの方法を使っているものは多い。

とはいえどうしても外部ライブラリに依存したいときはあるので、そのときは4の方法を採る。基本的な方針は、NodejsFunction がCDK synth時にやっている処理を、projen build時にやればOK。実際の例は弊リポジトリを参照

あるいは、 lambda.ts という拡張子のファイルを配置することで、projenにバンドル処理やFunctionコンストラクトの定義を肩代わりさせることができる。こちらを参照。ただし、現状この方法ではSingletonFunctionは使えないようなので注意。

4の仕組み

簡単に方法4の仕組みを説明する。projenではCIのビルドステップもまたコードで管理されており、この辺りのプロパティから触ることができる。今回はcompileTaskの辺りでLambda関数をビルドすれば良いので、下記のコードを .projenrc.js に追加している。

project.projectBuild.compileTask.prependExec('npm ci && npm run build', {cwd: 'lambda/nodejs-build'});

これにより、projenのコンパイルタスクの際に、 lambda/nodejs-build ディレクトリで npm ci && npm run build のコマンドが実行される。さらに、lambda/nodejs-build/package.json を見てほしい。

  "scripts": {
    "build": "esbuild index.ts --bundle --outdir=./ --platform=node --external:aws-sdk"
  },
  "dependencies": {
    "adm-zip": "^0.5.9",
    "aws-sdk": "^2.1130.0",
    "extract-zip": "^2.0.1",
    "node-fetch": "^3.2.4"
  },

つまるところ、npm run build はesbuildでTypeScriptをトランスパイル・バンドルしている。ビルド生成物はlambda/nodejs-build/index.js に保存され、それを SingletonFunction から参照している

const handler = new SingletonFunction(this, 'CustomResourceHandler', {
  runtime: Runtime.NODEJS_14_X,
  code: Code.fromAsset(join(__dirname, '../lambda/nodejs-build')),
  handler: 'index.handler',
  uuid: '643fc8aa-9cdf-41ad-9b26-dc5b258cc071', // generated for this construct
  lambdaPurpose: 'NodejsBuildCustomResourceHandler',
});

これで、依存関係のあるLambda関数を定義でき、さらにライブラリのユーザー側ではLambda関数のビルド処理は不要となる。ちなみに ↑ の uuid はSingletonFunctionごとに生成する必要があるので、決して巷のサンプルコードからコピペしないように気をつけよう。UUIDは任意の方法で生成できるが、手軽なのはこれ

また、src以外にTypeScriptのファイルがあるとeslintが騒ぐので、projenから黙らせている。

  eslintOptions: {
    ignorePatterns: ['example/**/*', "lambda/**/*"],
  },

example フォルダを作ると親切

Constructの使い方は基本的には README.md に書くべき。しかしながら、動作する完全なサンプルがあるとさらに役立つものである。このため、CDK Constructでは example ディレクトリ以下にサンプルのスタック定義を含めることが多い模様。

一例はここ。最低限 cdk.jsoncdk.App の定義があれば動作するようなので、簡潔な記述・ファイル構成でサンプルを提供できる。

// index.ts
import { Stack, StackProps, App } from 'aws-cdk-lib';

class TestStack extends Stack {
  constructor(scope: Construct, id: string, props: StackProps = {}) {
    super(scope, id, props);
    // 自作Constructの利用例を書く
  }
}

class TestApp extends App {
  constructor() {
    super();

    new TestStack(this, 'TestStack');
  }
}

new TestApp().synth();

// cdk.json
{
    "app": "npx ts-node --prefer-ts-exts index.ts"
}

リリース前に確認すべき projen の設定項目

まず、authorauthorAddressrepositoryUrl は、正しい値が入っていることを確認しよう!

また、Construct Hub にライブラリを掲載するには、 keywordsaws-cdk を含める必要があるので注意。詳細はFAQ を参照。これだけで、npm に publishしてから30分以内に自動で掲載される。

最後に description も必ず書いておこう。Construct Hub のページで見出しの一言説明として利用されるため (下図) 。

まとめるとこんな感じになるだろう。

const project = new awscdk.AwsCdkConstructLibrary({
  author: 'tmokmss',
  authorAddress: 'hoge@example.com',
  cdkVersion: '2.20.0',
  defaultReleaseBranch: 'main',
  name: 'deploy-time-build',
  repositoryUrl: 'https://github.com/tmokmss/deploy-time-build.git',
  keywords: ['aws', 'cdk', 'lambda', 'aws-cdk'],
  description: 'Build your frontend apps during CDK deployment!',
});

パッケージレジストリにリリースするために

Construct Hubに掲載されるには、最低限 npm にパッケージを登録する必要がある。 これも簡単にできて、 ただGitHubリポジトリのSecretに NPM_TOKEN を登録すれば良い。 この辺りの記事が参考になるだろう。

なお、他の言語のライブラリを公開する場合も、同様に各パッケージレジストリのキーを取得し、登録すれば良いと思われる。現実的には結構面倒くさいので、Node.jsとPythonだけ公開しているライブラリが多い模様。この辺りは仕方ないね…

まとめ

ProjenでAWS CDKのConstructライブラリを開発する時に役立つTipsを紹介した。まだまだ駆け出しなので、今後開発が継続してさらに知見が溜まったら、また放出できればと思う。

EtherscanのContract verificationをAPIで実行する

渦中のEthereumネタ。

はじめに

Etherscanでコントラクトのアドレスを開くと、コードが表示されることがある。

Contractのコード

コントラクトのユーザーたちは、このコードを見ることで中の実装を把握でき、安心してコントラクトを利用することができる。 実はEthereum自体にはデプロイされたスマートコントラクトのコードを取得する機能はなく、これはEtherscanがオフチェインで提供している機能である。

この機能を使うためには、コントラクトをデプロイした後、Etherscanにコンパイルの構成情報とソースコードをEtherscanに教えれば良い。これによりEtherscanが改めてコードをコンパイルし、デプロイされたコントラクトとコードが合致することを検証する。これは次のページから申請することができる。Verify & Publish Contract Source Code

今回の記事では、この作業をプログラマブルかつhardhatなどのツールを使わずに行う方法を説明する。なお、この公式ドキュメントに同じタイトルのページがあるが、このページは情報が古く不完全である。今回紹介する方法は、最新でかつより美しい形 (後述) でCodeをEtherscanに登録することができる。

方法

公式ドキュメントどおりにやる(80点の)方法

Etherscan 公式ドキュメントに従えば、次の手順で実行できる:

  1. コントラクトをコンパイル・デプロイし、コントラクトのアドレスを得る
  2. コントラクトのソースコードをflattenする。flattenとは、すべてのインポートを削除し、インポート対象のコードを1ファイルに含めてしまうこと。これには solidity-flattener と呼ばれるツールを使えば良い。大抵各言語でプログラマブルに呼び出し可能なものが開発されており、例えば nodejs, python など。
  3. 下記をパラメータとして、Etherscan APIを実行
  4. VerificationリクエストのGUIDが返ってくる
  5. GUIDを使い、適宜こちらのAPIでVerificationの進行状況をポーリング
  6. 通常10秒程度でVerificationが完了する

この方法では、flattenされたコントラクトコードがそのままEtherscanに登録されるため、ユーザーたちはコントラクトの実装を読みづらい状態となってしまう。例えば、こちらのコントラクトがその状態。

100点の方法

こちらの方法を使えば、コードをflattenせずに登録することができる。例えばこのコントラクトのように。

この方法では、先に紹介した方法のうち2と3だけを変えれば良い。実はVerification用のAPIcodeformat: 'solidity-standard-json-input' と入力すると solcのstandard json input形式を入力することができるので、これを活用する。

まず、送るべきAPIリクエストは以下。

import fetch from 'node-fetch';
// ...
  await fetch(etherscanApiEndpoint, {
    method: 'POST',
    body: new URLSearchParams({
      apiKey: etherscanApiKey,
      module: 'contract',
      action: 'verifysourcecode',
      codeformat: 'solidity-standard-json-input',
      compilerversion,
      constructorArguements,
      sourcecode: standardJson,
      contractaddress,
      contractname,
    }).toString(),
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
  });

それぞれの変数の中身を解説していく。

  • etherscanApiEndpoint: Etherscan APIのエンドポイント。Endpoint URLs - Etherscan に掲載されている。Networkごとにエンドポイントが異なるので注意。
  • etherscanApiKey: Etherscan APIAPIキー。ユーザー登録すれば入手可能
  • compilerVersion: デプロイしたコントラクトをコンパイルしたsolcのバージョン。ここ に掲載されている文字列以外はエラーになるので注意。例えば solc-js は以下のような文字列を返すので、不要な部分を削除する必要あり (ここでは .Emscripten.clang の部分。)
> const solc = require('solc')
> solc.version()
'0.8.13+commit.abaa5c0e.Emscripten.clang'
  • constructorArguements: コントラクトをデプロイした際に指定した、コンストラクタの引数をABIエンコードしたもの。これは web3.js を使うと、次のコードで取得できる。
import Web3 from 'web3';
// ...
  // コントラクトをコンパイルして得られるABIとBytecode
  const abi = {...};
  const bytecode = '0x...';
  // コントラクトをデプロイ
  const contract = new Web3.eth.Contract(abi);
  const call = contract.deploy({
    data: bytecode,
    arguments: [1, 2, 3, 4], // コンストラクタ引数
  });
  const constructorArguments = call.encodeABI().slice(bytecode.length);
  // ....

call.encodeABI() で得られる文字列は以下のフォーマットになっている

0x[コントラクトのバイトコード][ABIエンコードされたコンストラクタ引数]

ので、最初のコントラクトのバイトコードを削除すれば、ABIエンコードされたコンストラクタ引数が得られるという仕組み。

ちなみにArguementsはArgumentsのtypoと思われるが、API後方互換性を保つために今となっては直すに直せないのだろう。

  • standardJson: これが肝。このドキュメント のとおりにJSONを作る必要がある。シンプルにすると、概ね以下のようなJSONを作ればOK。
{
    "language": "Solidity",
    "sources": {
        "input.sol": {
            "content": "// SPDX-License-Identifier: MIT\n\npragma solidity ^0.8.0;\n\nimport \"@openzeppelin/contracts/access/Ownable.sol\";\nimport \"@openzeppelin/contracts/security/ReentrancyGuard.sol\";\nimport \"erc721a/contracts/ERC721A.sol\";\nimport \"@openzeppelin/contracts/utils/Strings.sol\";\n\ncontract MyContract is Ownable, ERC721A, ..."
        },
        "@openzeppelin/contracts/security/ReentrancyGuard.sol": {
            "content": "..."
        },
        "erc721a/contracts/ERC721A.sol": {
            "content": "..."
        },
        ...
    },
    "settings": {
        "optimizer": {
            "enabled": true,
            "runs": 200
        },
        "outputSelection": {
            "input.sol": {
                "*": [
                    "evm.bytecode.object",
                    "abi",
                    "evm.deployedBytecode"
                ]
            }
        }
    }
}

要は sources フィールドに必要なすべてのファイルのソースコードを含めればOK。↑の例で input.sol がデプロイするコントラクト、それ以外が import で依存しているコントラクト。依存するコントラクトを含めるのが少し厄介なところだが、一応次のコードで達成することができる。

import * as fs from 'fs';
import solc from 'solc';
let standardJson: any;

const getStandardJson = async (fileName: string, contractName: string) => {
  const file = fs.readFileSync(fileName).toString();
  const output = 'input.sol';
  const input = {
    language: 'Solidity',
    sources: {
      [output]: {
        content: file,
      },
    },
    settings: {
      optimizer: {
        enabled: true,
        runs: 200,
      },
      outputSelection: {
        [output]: {
          '*': ['evm.bytecode.object', 'abi', 'evm.deployedBytecode'],
        },
      },
    },
  };
  // standardJson を初期化
  standardJson = input;
  solc.compile(JSON.stringify(input), { import: findImports });
  return standardJson;
};

const importCache: { [key: string]: { contents: string } } = {};
const findImports = (path: string) => {
  if (importCache[path] == null) {
    const file = fs.readFileSync(`node_modules/${path}`);
    importCache[path] = {
      contents: file.toString(),
    };
  }
  // standardJson に依存するソースコードを追加
  standardJson.sources[path] = { content: importCache[path].contents };
  return importCache[path];
};

要は、コンパイルして依存するソースコードを拾っていこうというアイデア。明らかに無駄の多い方法ではあるが少なくとも動作するので、一旦こうしている。ベターな方法を知っている方いれば教えて下さい。

また、もちろん settings.optimizer の部分は実際にコントラクトをコンパイルしたときの設定と同一にすること。これで standardJson は得られた。

  • contractaddress: デプロイしたコントラクトのアドレス
  • contractname: コントラクトのエントリポイントを特定するための文字列。↑の例の場合は input.sol:MyContract とする。要はメインのコードのファイル名とコントラクトの名前を : でつなげればOK。

 これでEtherscanへのAPIリクエストを作ることができた!あとは↑のコードを参考にPOSTリクエストを送信すればOK!

まとめ

Hardhatなどを使わずにAPI経由でEtherscanでコントラクトをverifyする方法を紹介した。現状、この方法を見つけるためだけにわざわざHardhatのコードを読み込む必要があるという悲しい現実もある。この方法が正しくEtherscanのドキュメントにも明記されることを望む。