1クール続けるブログ

とりあえず1クール続けるソフトウェアエンジニアの備忘録

お休み1ヶ月振り返り

記事一覧はこちら

なぜ1ヶ月お休み?

7月末で3年3ヶ月勤めていた会社を退職することになったためです。7月1日が最終出社日で、以降はお休みを頂いておりました。8月1日から新しい会社で働くことになるため、お休みが約1ヶ月ある状態だったということになります。

この1ヶ月でやったこと

このようなご時世でなければ、6年間住んでいた北海道や5年間住んでいた新潟とかに一人旅して、昔食べていたラーメンとかアイス食べたり酒飲んだりしたかったのですが叶わず…。仏教関連の本読んでいたので奈良行くのも有りだったなあ。

どこにも行けないなら勉強しちゃうぞ!と息巻いていましたが、やろうとしていることの4分の1も出来ないとは…(絶望)。

読書

本当はこの時間があるときにElegant Puzzleとか、英語でしか読めない本を読むぞと思っていましたが触る余裕がありませんでしたね。
後、貞本エヴァもこの期間で読もうと思ってたのに読めなかった。Twitter眺めている時間をここに投資すれば良かったよ…。

データ指向アプリケーションデザイン

www.oreilly.co.jp

きっかけとしては、最終出社日の少し前にfukabori.fmというPodcastを知り、ずっと聴いていました。t_wadaさんのデザインパターンの回を最初に聴いたのですが、あまりに面白くて遡って結構聴いていました。
1年前ほどに、こばさんがゲストとして出演していたNewSQL回がこれまた面白く、その話の中でデータ指向アプリケーションデザインという書籍について触れられていたので、この期間に読みました。
自分が今まで読んだ本の中で一番ボリュームがあるであろう600ページ超えでして、読む前に心が折れて一瞬枕にしそうだったのですが。何とか踏みとどまりました。

自分は基礎的な部分に関してもちゃんとは理解していない箇所が多かったため、第一部から面白かったように感じます。
3章でLSMツリーやBツリーについての説明があったのですが、最初Bashのkey-Valueストアの実装から始まる説明は非常にとっつきやすくて良かったです。また、4章ではエンコーディングについての説明がありますが、Protocol BuffersやThriftに疎かったため(特にThriftはUberGatewayの設定で利用されていたというイメージしか無かった)、非常に勉強になりました。

第2部はデータを分散をさせることによるConsistencyに関する問題とかをどう解決していくかという所が非常に面白かったです。
マルチリーダーやリーダーレスのDBには馴染みが無かったので、理解しきれていない部分も多い。衝突を解決するためのCRDTというMisreading Chatで聴いた内容が出てきて、点と点がつながった感覚になった。
あと8章は勉強していたゆるーく勉強していた分散システムの知識が凄く分かりやすくまとまっていて有り難かった。

なにはともあれ再読は必要!

Site Reliability Workbook

www.oreilly.com

ずっと読もう読もうと思いつつ読めていなかった書籍。e34.fmで良いぞという話が出ていたので、ちゃんと読もうと思って読みました。
自分がDevOpsへの理解が浅かったこともありますが、第1章で語られていたDevOpsとSREの関係性というのは、自分の頭の中を整理する上でとても良かったです。書籍の中でも触れられていますが、class SRE implements devops というのは自分の中でしっくりきたものはありました。

読んでて面白かった章は、第2章のimplemeting SLOsですね。サンプルのシステムアーキテクチャをベースにどのようなSLIが考えられるかあったり、SLO/エラーバジェットポリシーを作成する手順などが記載されており、これをベースにSLOの運用を始めていけると確信できるような内容になっていると思いました。
後は、第5章Alerting on SLOsも良かったです。SLOのアラート設定をシンプルなアプローチから始めていき、それをprecisionやrecallといった指標を元に評価し、burn rateなどを用いて改善していくのは読み進めやすかったですね。
そして飛んで、第20章の SRE Team Lifecycles は、SRE組織をmatureにしていくためのロードマップが示されているのですが、SREを始めるときのイメージが運用チームがSREに転換するというパターンしか自分の頭には無かったので、いくつかパターンがあることに驚きを覚えました。
これも、First SRE Bookと合わせて再読必須だなと思いました。

スイッチ!

www.hayakawa-online.co.jp

これは、お休み入る前にkindleの半額セールで購入したものです。
人間の感情的な部分を「象」に、そして理性的な面を「象使い」に例えて、「象使いに方向を教える」「象にやる気を与える」「道筋を定める」の3つのステップから行動を変えるというのを説明した本。事例と、それを裏付ける実験を紹介していく形式になっている。納得感はあるけど実践できるのか?みたいなところはある。1つ確実なのは、この休みが始まる前に読めば良かったってこと。
この他にも早川の本で買ったものがあるので読めたらなと思っている。

あ、後ブクログがちゃんと使えてないので、進捗が入力できるorkindleなら勝手に記録してくれるものが良いなあと思って代わりを探しています。良いのがあれば教えて下さい。個人的にはgoodreadsってどうなのかなと思っていたり…。

趣味

ちょっと前にもブログに書いたんですが、Travis JapanとINIというボーイズグループを細々と追っているので、その辺の動画やWEB RADIOをずっと観てしまいました。

44smkn.hatenadiary.com

IMAGE NATIONというトラジャの全国ツアーの最終日に配信がありましたが…最高でしたね。去年のライブも配信で観ることが出来たのですが、今年は特に歌がより進化している気がしました。INTERACTIONALもっとちゃんと観たかったなあ。円盤化を何卒…何卒…!
INIのデビューも待ち遠しい!みんな言っていますが、Wanna Oneのenergiticみたいな曲がデビュー曲だと嬉しいな!

分散システムの勉強(中途半端)

ここは中々苦戦しました。deeeetさんのブログ記事を読んで、UCSCの2021年の分散システムの講義動画を観ていましたが、シンプルに「僕のリスニング能力低すぎ…」となり辛みでした。

第10回くらいまでは観たのですが、ちゃんと頭に入っているかがかなり微妙で、結局は講義PDFも存在する、ケンブリッジ大学の分散システムの講義に乗り換えて勉強しています。お休み終わる前までにRaftにたどり着けないので一生合意できない人間になりつつある。
ケンブリッジ大学の授業の第3回leap secondsの話で日光東照宮の🙈 が出てきてビビりました。

また、分散システムと言えば、タネンバウム先生のDistribureted Systemsだと思います。一応、そちらを読み進めています。ですが、「僕、英語読むの遅すぎ…」となり心が挫けそうです。まだ第2章のARCHITECTUREの途中です。
第3版は日本語訳が出ていないですし、第2版は絶版とのことなので英語を読むしかない。僕には英語が分からぬ。この前、TOEICで750点取れたのは奇跡だったのか、それとも750点はこのレベルなのかどっちなの。

手動かしたもの

自作OS本

book.mynavi.jp

ブートローダーの部分までしかやっていないので、実質自作ブートローダー本状態だ。
みかんOSは「未完」というところから名付けられていますが、始まってもいない。始める前のWSL周りでハマって時間を結構使ってしまった感がある。
土台は作ったのでちょっとずつ進められると良いよね…そう…ちょっとずつね。

React/GatsbyJSに入門

新卒で入った会社にいた頃に趣味でAngularを触っていた。その頃の最新はv4だったように思います。mozaic.fmのep24でlacoさんの話を聞いて面白そうと思って、当時既にReact全盛だったと思うのですがAngularを触り始めていました。
業務でJSとSassを書いてたころも有りましたが、ReactやVueのようなライブラリを使った開発は行っていなかったので、この期間に触るかと思ってSSGと共に少し触っていました。
今度ブログ書こうと思います。まだローカルで動かしている段階でCloudflareとかにデプロイ出来ていないので。

式場探し

何気に時間が溶けていったもの。この期間に始めて良かった。

ブライダルフェアなるものが、あんなに一回で時間と精神を消費するものだとは思わなかった。なにも知らなかった頃は1回1時間くらいなのかなあというぼんやりした印象を持っていたが、実際は4時間前後かかるものだった。
2箇所の式場を回ったけど、それぞれの項目をスプレッドシートに書き出して比較検討するのは楽しくもあり疲れもしますね。

来年も新型コロナウイルスの影響を引きずるということも考えられるので、キャンセルポリシーとかもしっかり確認しつつ動かなきゃだ。
結婚式挙げた人たちマジで凄い。

新しいノートテイキングツール

Notionを使っていたのだけれど、僕はこの高機能を活かしきれていないと思っていました。後ローカルに保存できない。
もう少しシンプルでパフォーマンスの良いもの、かつローカルで利用できるものにしたいなと思っていました。というのも、仕事だとオンラインにアップロードするものはどうしても使いづらいというのがモチベーションでした。
そんなときに、Rebuild.fmの#308 を聴き、宮川さんが今はcraftというツールを利用しているということで気になり、1ヶ月使っていました。Macのネイティブのアプリが自然で良いのと、グループ化の機能がいい感じです。ローカル保存も上手く動くことを確認しました。WebはまだBetaでかつMaciPhoneのアプリでしか出てないので、自分のユースケースにはぴったり合いますが他の人にとっては微妙かもです。
難点としては、機能のドキュメントがあまり無いためにリリースノートを遡ることになる点と、画像を貼ると全体的にもっさりする点です。

結局のところ、課金してしまって継続利用しようかなと意気込んでいたのですが、自分のAppleIDで買ったので会社で使うためにはもう一回買わなきゃでは…?と戦々恐々としているところです。
(今の所ログインはAppleIDのみがサポートされていたはずのため…)

まとめ

リラックスできた1ヶ月だったのではなかろうか。専業主夫チックな生活も新鮮で良かったな。
料理する頻度はこの1ヶ月は確実に多かったので、今後の生活に響いてくるのは、なんだかんだそういう部分なのかもしれない。効率上がってたら良いなあ。計測しておけば良かった。以上!

WSL1でみかん本を進める上で躓いたところ

背景・モチベーション

低レイヤを理解したいというのは、ソフトウェアエンジニアをやっていると、どうしても沸き起こってしまう感情なのではと思ったりします。
自分は工学部出身ですが、コンピュータサイエンスを専門とした学科ではなかったので電子回路とかを除くとC言語くらいしか情報系の授業は受けておらず、OSの仕組みとかは全然知りません。
7月末に退職し8月から新しい会社で働くことになるのですが、有給だったりで約1ヶ月間のお休みができたので、この機会に、DMMセールのときに購入し積んでいたみかん本をやるぞと思い立ちました。

WSL1を使うに至った経緯

2015年に買ってずっと眠らせていたSurface3を引っ張り出し、Windowsのバージョンを最新化してWSL2をセットアップしようとしました。
指定された手順を行っていきましたが、特にエラーなど起こることなく完了しました。ubuntuを起動しようとしたときに、 Installation failed with error 0x80070003 or error 0x80370102 というエラーメッセージが出力され起動に失敗しました。

ドキュメントを確認したところ、下記のように記載がありました

  • Please make sure that virtualization is enabled inside of your computer's BIOS
  • WSL2 requires that your CPU supports the Second Level Address Translation (SLAT) feature, which was introduced in Intel Nehalem processors (Intel Core 1st Generation) and AMD Opteron.

「これからBIOSを学ぶってときにBIOSをいじるんか…」と思いつつ、ここを見ながらBIOSの画面に飛んだ。が、Virtualizationの項目は特にない。調べたところSurfaceでは元々Virtualizationは有効になっているらしい。
「タスクマネージャ」の「パフォーマンス」タブの「仮想化」の欄を見たら、ちゃんと有効になっていた。

ではHyper-Vの要件を満たしていない…?と思い確認してみる。powershellSysteminfo.exeを実行してみた。Hyper-Vの要件を満たしているかは一番最後に出てきますが、全てが「はい」になっていることが確認できた。

WSLのGitHubのIssueRedditを見つつ色々やりましたが駄目でした。Hyper-VはWSL2利用時には明示的に入れなくても良いと書いてありましたが、Windowsのイベントログには「Hyper-Vがインストールされていないため、仮想マシンの作成に失敗しました」と出力されていたので、Hyper-Vもインストールしましたが同じエラーが出てしまい八方塞がりになってしまいました。

もうこれはWSL1で動かすしかないと諦めて、WSL1で進めていくことにしました。

WSL1で躓いたところ

USBに書き込んでそこから起動させる方式ではなく、QEMUで起動させる方式を自分は取りました

ディスクイメージをマウント出来ない

BOOTX64.EFIのファイルを作成し、ディスクイメージに書き込むために mnt ディレクトリを作成しマウントさせようとしましたが下記のようにエラーとなりました。

$ sudo mount -o loop disk.img mnt
mount: ./mnt: mount failed: Operation not permitted.

こればWSL1と2でシステムコールファイルシステムの扱い方が違うからなのかな(参考)と思いつつ調べていたところ、GithubのIssueとして既にどなたかが挙げてくださっていました。とてもありがたい…。

github.com

mtoolsを利用することで、wsl1でもディスクイメージにファイルの書き込みが出来るようです。Issueの中ではubuntuに元々入っているということでしたが、自分が動かしているWSL1のUbuntu 20.04には入っていなかったので apt 経由でインストールしました。
下記のような手順でディスクイメージにファイルをコピーすることができました!

$ sudo apt install mtools
$ mmd -i disk.img ::/EFI
$ mmd -i disk.img ::/EFI/BOOT
$ mcopy -i disk.img BOOTX64.EFI ::/EFI/BOOT/BOOTX64.EFI

$HOME/osbook/devenv/make_image.sh の中身のマウント部分の処理も上記のように変更しておくと、後々もいい感じです。

仮想マシンで生成されたファイルの中身が見れない

P61のメモリマップを出力した後にファイルを確認するときの手段についてです。上に関連した話になっています。
run_qemu.shを動かしたディレクトリにdisk.imgが作成されていますが、マウントして観ることはできないのでmtoolsを利用します。

# ファイルの存在確認
mdir -i disk.img ::/ 

# ファイルをコピーして確認する
mcopy -i disk.img ::/memmap ./memmap
less memmap

番外編: QEMUが動かない

これはWSL1であること由来ではありません。なんらかの理由でansibleの実行で一部上手くいかないと起こる事象のようです。
qemu-system-x86_64 を実行すると、Unable to init server: Could not connect: Connection refusedというエラーメッセージが出力されてしまいました。XLaunchが起動していることは確認済の状態です。

下記のように作業すると解決しました(参考
DISPLAY という環境変数はXサーバが利用するもののようです(参考
xhostはXサーバへの接続許可リストに追加するコマンドっぽいのですが、Xサーバの仕組み周りを自分が理解していないのでよく分からないまま利用している状態です。普段使いはWindowsではないので、あまり深追いはしないつもりです。

$ sudo apt install x11-xserver-utils
$ export DISPLAY=:0
$ xhost +

やっとこさ進める状態になったので、ちょっとずつ進めていこう

s3selectを実行するGo製CLI を作ってみての気づきとか

背景・モチベーション

ALBのアクセスログとかをクエリしたいときがあります。例えばELB5xxエラーが発生していて、そのリクエストの詳細を知りたい時などです。しかも複数ファイルに対して、一括でクエリを投げられるとなお嬉しい。
1日待てばAthenaに放り込まれるが待っていられないぞーという自分がいたりします。

そこで、自分の要件に沿ったs3selectCLIを勉強がてら作ってみました。作る上で参考にしていたのは、GitHub CLIAWS LoadBalancer ControllerCopilot CLIあたりです。

気づきをこの記事でまとめて、後々の自分に役立ててもらおうと思います。

リポジトリはこちらになります。

github.com

CLIについて

cobra を使う前提です

エラーハンドリングについて

cobraSilenceErrorsSilenceUsagetrueにして、cmd.ExecuteC() の呼び出し元でSentinel Error方式を利用しエラーの種類によって出力を分岐させるのが良さそう。エラーによっては Usageを出力したりしなかったりとか諸々パターンが有ると思うので、cobraの設定でハンドリングするのが難しいからかもしれない。
CLIであれば、Error interfaceを実装した独自エラー構造体を作りerrors.Asを使って処理を分岐するといったところまで作り込まなくても良さそう。

func NewCmdRoot(f *cli.Factory, version, buildDate string) *cobra.Command {
    cmd := &cobra.Command{
        SilenceErrors: true,
        SilenceUsage:  true,
    }
}

func main() {
    if _, err := rootCmd.ExecuteC(); err != nil {
        if errors.Is(err, terminal.InterruptErr) {
            fmt.Fprint(stderr, "\n")
        }
    }
}

CLIで共通利用する設定はひとまとめにする

ターミナルの入出力設定やCLIが独自の設定ファイルを用意する場合には、cmd.AddCommandの引数に渡す *cobra.command を生成するコンストラクタに共通の設定を定義した構造体を渡してあげるようにするとスッキリする。

type Factory struct {
    In     io.ReadCloser
    Out    io.Writer
    ErrOut io.Writer

    Config     func() (config.Config, error)
    Logger     *zap.Logger
    Executable string
}

func main() {
    cliFactory := cli.NewFactory(buildVersion, logger)
    rootCmd := root.NewCmdRoot(cliFactory, buildVersion, buildDate)
    // ...
}

テストについて

外部APIを利用するテストにはgomockを使う

外部API、今回の文脈ではaws-sdk-goを利用して叩くAWSAPIをテストする場合についてです。スタブを用意しても良いのかもしれませんが、間接出力も合わせて検証したいのでモックを利用します。モック/スタブの定義は テストダブル - Wikipedia を参考にしています。

Goにおけるモックライブラリでよく使われていそうなのは、golang/mockもしくはstretchr/testifymockパッケージという印象があります。今回、自分は前者を利用しました。
自分で定義したinterfaceに、aws-sdk-goで定義されているインターフェース(下記のコードで言うところのs3iface.S3API)をEmbeddingすることで、mockgenが外部APIのモックコードも自動生成してくれます。

//go:generate mockgen -destination=../../../mocks/aws/services/mock_s3.go -package=mock_services github.com/44smkn/s3select/pkg/aws/services S3
type S3 interface {
    s3iface.S3API

    // wrapper to ListObjectsV2PagesWithContext, which aggregates paged results into list.
    ListObjectsV2AsList(ctx context.Context, input *s3.ListObjectsV2Input) ([]*s3.Object, error)
}

設定ファイル読み込み関数はテストはStubを用意するのも一つの手

テスト対象がメソッドかつ間接出力も検証したいという場合にはモックが必要になるかなと思いますが、テスト対象が関数かつ間接出力の検証も必須でない場合にはStubで十分というのがあります。設定ファイルの読み込み処理なんかは、もちろんメソッドに落とし込むことも出来るかと思いますが関数にしがちかと。
テスト対象から呼び出される関数は変数として宣言しておきます。

var ReadConfigFile = func(filename string) ([]byte, error) { ... }

// テスト関数から defer で呼び出す
func stubConfig(t *testing.T, configContents string) func() {
    t.Helper()
    original := config.ReadConfigFile
    config.ReadConfigFile = func(filename string) ([]byte, error) { ... }
    return func() {
        config.ReadConfigFile = original
    }
}

go testするときに -race flagつけるべし

Race Conditionを検出するフラグがgo testにあることを僕は知らなかったです。オライリーから出てるGo言語による並行処理のAppendixに記載ありましたが見逃していました。Go1.1からほぼ全てのgoコマンドに追加されたもので、検出された際には標準エラー出力にログを出力するようになっています。これをCIに組み込むのは有用なので、GitHub Actionで定義したテストにはこの引数を指定しています。

blog.golang.org

ビルドタスクについて

Goで書いても良いんじゃない?

Goではビルドするときに、go build の引数である -ldflagsというgo tool linkコマンドのオプションに渡されるフラグで、-X importpath.name=valueと指定してバージョンはdateを埋め込むことが多いと思います。後はWindowsの場合には、拡張子でexeを付けるとか。
自分はそのようなタスクをシェルスクリプトorMakefileで書くものと思っていましたが、GitHub CLIがgoでビルドタスクを書いてMakefileから呼び出すようにしていました。Windowsでも動きますし有用かもしれません。

.PHONY: bin/gh$(EXE)
bin/gh$(EXE): script/build
   @script/build $@

script/build: script/build.go
  GOOS= GOARCH= GOARM= GOFLAGS= CGO_ENABLED= go build -o $@ $<

goreleaserが便利

goreleaserは有名なので知ってはいましたが、今回始めて使いました。GitHubでのリリースが抽象化されていてあっという間にやりたいことが出来ました。GitHub Actionsとの組み合わせも容易でした。リリースノートもコマンドのオプションで渡せますしね。
自分はやっていないですが、nfpmを利用して、Linuxのパッケージマネージャからも扱えるように出来たりするのも良い点ではと思います。

goreleaser.com

雑多

awssdk利用する上でHandlerは理解しておいたほうが良さそう

今までちょこちょこ aws-sdk-go 使ってきたけど理解が足りなかったところで、aws-sdk-go/aws/request.Handlerにぶら下がるHanlderListをhttpリクエストの対応するフェーズで呼び出しています。

例えば、CloudTrailのレコードにUserAgentが記録されますが、そこにツール名を入れられると後で確認しやすそうです。その場合には、下記のようにHandlerを利用側から追加することが出来ます。
また、各リクエスト毎にもこの設定は可能で、かつHandlerを逆に削除することも可能です。リクエストの可変長引数に request.Optionを渡せるのですが、このOptionの型が func(*Request)なので割と好き勝手に出来てしまいます。

// ハンドラーの追加
func NewCloud(cfg CloudConfig) (Cloud, error) {
    sess := session.Must(session.NewSession(aws.NewConfig().WithRegion(cfg.Region)))
    sess.Handlers.Build.PushFrontNamed(awsrequest.NamedHandler{
        Name: fmt.Sprintf("%s/user-agent", appName),
        Fn:   awsrequest.MakeAddToUserAgentHandler(appName, build.Version),
    })
}

// ハンドラーの削除
func hoge() {
    resp, err := s.cloud.S3().SelectObjectContentWithContext(ctx, input, func(r *awsrequest.Request) {
        r.Handlers.Send.RemoveByName(awsclient.LogHTTPResponseHeaderHandler.Name)
    })
}

renovate入れとく

renovateは依存モジュールのバージョンを最新に保ってくれるためのサービスです。Githubリポジトリにはここから導入することが出来ます。依存しているモジュールがバージョンアップしたら、Pull Requestを発行してくれます。

アップデート後にgo mod tidyを実行するように設定ファイルにpostUpdateOptionsを宣言しておくと良いです。

{
  "postUpdateOptions": [
    "gomodTidy"
  ]
}

docs.renovatebot.com

VSCodeデバッグを行う上でちゃんと launch.json 使う

めっちゃ当たり前!なんだけど出来ていなかった。リクエストを受け付けるアプリケーションやシンプルなものだと、なんとなくでデバッグ出来てしまっていた。
CLIとかだと引数によって挙動が大きく変わるので、launch.jsonargsとかによって複数設定を管理しておくと楽ということに気づけた。

code.visualstudio.com

感想

作ってみたアプリケーション自体は、テストのカバレッジがまだまだ低かったりエラーハンドリングまだまだだったりするので、勉強がてらちまちま書いていこう
以上!

CDK for TerraformがGoをサポートしたので試してみた

記事一覧はこちら

背景・モチベーション

CDKには元々興味がありました!CloudFormationでのyaml記述はやはり辛いものがあるし、プログラミング言語を利用することでコード補完によるサポートを得られたり、抽象化やvalidateを入れたり出来るのではと期待があります。
CDK for Terraformのv0.0.4がリリースされ、Goのサポート(experimental)を開始したことをこの記事から知り、良い機会なのかなと思い試してみました。

参考文献

CDK For Terraform とは

ここにあるとおり、プログラミング言語でインフラストラクチャを記述したいというDeveloperの要望に応える形で、AWS CDKのチームと協力し、AWS CDKの2つの主要なコンポーネントを活用してTerraform用のCDKをサポートし始めたとのことです。

AWS CDKに関しては、こちらが分かりやすいです。
AWS CDKはCloudFormationテンプレートをjson形式で吐くのに対し、Terraform用CDKはTerraformの構成ファイルをjson形式で吐きます。元々、HCLだけでなくjsonもサポートしているのはあんまり意識したことなかったなあと思ったり。

CDKは多言語をサポートしてます。そこにはjsiiというライブラリとかが絡んできていて、掘るのが面白そうなのですがそれはまた別の記事にしようと思います。

Getting Started

リポジトリはこちら

github.com

※ Terraformはインストール済の前提

実装の初期段階まで

$ brew install cdktf
$ mkdir cdk-for-terraform-sample

# 雛形の作成
$ cdktf init --template="go" --local

# moduleの宣言が自動生成されているので修正する
$ vi go.mod 

プロジェクトルートにある cdktf.json に利用するTerraform ProviderとModuleを宣言します
例えば、AWSのProviderとEKSのModuleを利用するのであれば下記のように宣言する

{
    "language": "go",
    "app": "go run main.go",
    "codeMakerOutput": "generated",
    "terraformProviders": [
        "hashicorp/aws@~> 3.40.0"
    ],
    "terraformModules": [
        {
            "name": "aws-eks-module",
            "source": "terraform-aws-modules/eks/aws",
            "version": "~> 16.0"
        }
    ]
}

下記のコマンドを実行すると、上で設定した内容を元にコードを自動生成します
codeMakerOutput で設定したディレクトリに生成したコードが配置されます

$ cdktf get
⠇ downloading and generating modules and providers...  # めっちゃ時間かかりますし、メモリを6-7GB食います

Goのコードで実装

main.go に宣言された、NewMyStack 関数にてリソースの定義を行っていきます。
基本的な関数の構造としては下記のような形で、第一引数と第二引数は各関数で共通で、第三引数は各リソースごとのinputのパラメータを持った構造体を渡してあげるイメージ。殆どが参照渡しになるので、適宜jsiiパッケージのヘルパー関数を利用する。

vpc := aws.NewCamelCaseResourceName(stack cdktf.TerraformStack, id *string, config *aws.CamelCaseResourceNameConfig)

例えば、簡単なAWSのスタックを作成するとなると下記のようになると思います。

func NewMyStack(scope constructs.Construct, id string) cdktf.TerraformStack {
    stack := cdktf.NewTerraformStack(scope, &id)

    aws.NewAwsProvider(stack, jsii.String("aws"), &aws.AwsProviderConfig{
        Region: jsii.String(region),
    })

    vpc := aws.NewVpc(stack, jsii.String("isucon_vpc"), &aws.VpcConfig{
        CidrBlock: jsii.String("172.16.0.0/16"),
        Tags: &map[string]*string{
            "Name": jsii.String("isucon-training"),
        },
    })

    igw := aws.NewInternetGateway(stack, jsii.String("isucon_vpc_igw"), &aws.InternetGatewayConfig{
        VpcId: vpc.Id(),
    })

    subnet := aws.NewSubnet(stack, jsii.String("isucon9_subnet"), &aws.SubnetConfig{
        VpcId:            vpc.Id(),
        CidrBlock:        jsii.String("172.16.10.0/24"),
        AvailabilityZone: jsii.String("ap-northeast-1d"),
        Tags: &map[string]*string{
            "Name": jsii.String("isucon-training"),
        },
        DependsOn: &[]cdktf.ITerraformDependable{
            igw,
        },
    })

    // 他にもリソースを作成(詳しくはリポジトリをご覧ください)

    return stack
}

デプロイしてみる

下記のコマンド実行時に cdktf synth も実行されて、cdktf.outディレクトリにjson形式でterraformの構成ファイルが生成されます。
注意点としては、cdktfのv0.4.0の時点では cdktf destroy時にエラーが発生します(Issue)。

# terrafrom planとほぼ同じような出力が得られる
$ cdktf diff
Stack: cdk-for-terraform-sample
Resources
 + AWS_DEFAULT_ROUTE_TA isucon9_subnet_rout aws_default_route_table.isucon9_subnet_
   BLE                  e_table             route_table
 + AWS_EC2_MANAGED_PREF default_prefix      aws_ec2_managed_prefix_list.default_pre
   IX_LIST                                  fix
 + AWS_EIP              isucon9_qualify_ins aws_eip.isucon9_qualify_instance_eip
                        tance_eip
 + AWS_INSTANCE         isucon9_qualify_ins aws_instance.isucon9_qualify_instance
                        tance
 + AWS_KEY_PAIR         developer_keypair   aws_key_pair.developer_keypair
 + AWS_ROUTE_TABLE_ASSO isucon9_subnet_rout aws_route_table_association.isucon9_sub
   CIATION              e_table_d           net_route_table_d
 ~ AWS_SECURITY_GROUP   isucon9_qualify_ins aws_security_group.isucon9_qualify_inst
                        tance_sg            ance_sg
 + AWS_SUBNET           isucon9_subnet      aws_subnet.isucon9_subnet

Diff: 7 to create, 1 to update, 0 to delete.

# デプロイ
$ cdktf deploy

# お片付け(エラーが発生します)
$ cdktf destroy
ypeError: Cannot read property 'startsWith' of undefined

使ってみての所感

文字列をポインタで渡さなくてはいけない関係上、jsii.String関数を毎回使うのでスッキリ見えないのがあまり良くないかもというのはあります。
また、この段階だからだと思うのですが、Terraformのresourceやmoduleを元にコード生成する過程でかなり時間とリソースを食います。これはネックになりそうです。
規模感が大きくならないと、中々恩恵は受けづらいのかなと思う面もありました。
(でもyaml書くより楽しい…)
以上!

sqlcを利用してGoのDB周りの処理を扱う

記事一覧はこちら

背景・モチベーション

以前にsqlcの作者のブログ記事を読んだことがきっかけです。だいぶ前におそらくredditで話題になっていたものを拾った気がします。
さっと見て開発生産性が上がりそうだなと思ったので試してみました。

sqlcとは

github.com

SQLからtype-safeなコードを生成してくれるツールです。
長い間、ソフトウェアエンジニアはプログラミング言語のアノテートされたオブジェクトからSQLのクエリを生成してきていました。SQLは既に構造化・型付けされた言語のため、SQL自体から正しいtype-safeなコードを生成すべきというのが、作者のモチベーションのようです。
現在のところはPostgreSQLMySQLのみをサポートしており、SQLiteも今後はサポート予定のようです。

sqlcを利用する方法

  1. SQLを書きます
  2. sqlcを実行し、1.で作成したクエリに対するtype-safeなinterfaceを提供するGoのコードを生成します
  3. sqlcが生成したメソッドをユーザのコードから呼びます

sqlcのメリット

コード生成をしている過程で、全てのクエリとDDL文を解析します。テーブルの列の名前や型が一致しない場合には、sqlcはクエリのコンパイルに失敗することで実行時エラーを未然に防ぐことができます。
sqlcが生成するメソッドは厳密なGoの型定義を持つため、クエリの引数やカラムの型を変更した時にコードを更新しなければコンパイルに失敗するようになっています。
安全ですね…!

database/sqlパッケージを直接利用することのデメリット

クエリが増えてくると、マッピングをメンテナンスするのが面倒になり生産性に大きな影響を与えます。
更に良くない点として、下記の3点のような実行時まで気づけないミスを犯してしまう可能性があります。

  • クエリのパラメータの順番を入れ替えた時に、マッピングの更新が漏れる
  • テーブルにカラムが追加された時に、追加したカラムの値を返却するようにクエリを変更する作業に漏れが生じる
  • SQLの方とコードの型に相違がある

gormなどの高レベルライブラリを利用することのデメリット

先程言及したdatabase/sqlパッケージを直接利用することのデメリットは解消されますが、クエリテキストやstructタグを使用して手動でのマッピングをせざるを得ず、間違った場合の実行時エラーが起こる可能性は大きくは減っていない状況と言えます。

sqlcを実際使ってみての感想

この後に動かしてみたことについて記載しますが、感想を先に述べておこうと思います。

当初からサポートしているPostgresはともかく、MySQLはまだまだ実用には至れない状況だと思います。
確認できた限りで下記のような未サポート/不具合があります(Issueに上がってなさそうなものは自分の方でIssue上げてみました)。

  • [Postgres] bulk impot(COPY句)は未サポート
  • [Postgres/MySQL] 動的where句/order by句は未サポート
    • #364 によると、「CASE句で対応可能では」という提案がされていますが、Postgresは5回以上のクエリで実行計画をキャッシュすることになるがCASE句を利用した場合にはキャッシュが利用できないのでパフォーマンスが悪化する
    • MySQLではCASE文の中にあるパラメータを認識してくれない
  • [MySQL] LIMITとOFFSETで名前付きパラメータを書けない
  • [MySQL] sqlc.arg(foo) IS NULLのパラメータを無視する
  • [MySQL] 同名の名前付きパラメータを別々に認識する

sqlcを実際に動かしてみる

サンプルのリポジトリは下記です。isucon10のisuumoの実装を参考にしながらsqlcを取り入れてみました。
(謎のスイッチが入ってしまってDDDライクに作ってしまった…)

github.com

sqlcのバージョンはv1.8.0です。
まず、sqlcを実行するディレクトリ配下に置く設定ファイルの生成を行います。
queriesschemaで指定したディレクトリパスには、それぞれアプリケーション内で実行するSQLとテーブルを定義するSQLを格納しておきます。
このpathパラメータの配下にファイルが生成されます。

$ cat <<EOF > sqlc.yaml
version: "1"
packages:
  - name: "persistence"
    path: "pkg/persistence"
    queries: "_sql/queries"
    schema: "_sql/schema"
    engine: "mysql"
    emit_prepared_queries: false
EOF

それでは、コードを生成していきます。
先程作成したsqlc.yamlが存在するディレクトリで、sqlc generateを実行することでコードを生成することができます。syntaxにミスがある場合には、このときにエラーという形で確認できます。

  • ${SQL_FILENAME}.sql.go: アプリケーションのコードから呼び出すデータアクセスメソッド郡
  • db.go: sql.DBsql.Txのためのinterfaceを用意
  • models.go: テーブル定義をGoのstructとして表現したもの
$ sqlc generate
$ ls -l pkg/persistence
total 32
-rw-r--r--  1 44smkn  staff   4.5K  5 23 18:06 chair.sql.go
-rw-r--r--  1 44smkn  staff   567B  5 23 18:06 db.go
-rw-r--r--  1 44smkn  staff   415B  5 23 18:06 models.go

定義したSQLと生成されたコードの一例としては下記のようになります。このGetChair()メソッドをアプリケーションから呼び出してデータベースから情報を取得します。
拙いコードですが、上述のリポジトリでレコードのInsertや検索もやってみていますので、良かったら覗いてみてください。

CREATE TABLE chair (
    id          BIGINT  NOT NULL AUTO_INCREMENT,
    name        TEXT    NOT NULL,
    description TEXT    NOT NULL,
    thumbnail   TEXT    NOT NULL,
    price       INT             ,
    height      INT             ,
    width       INT             ,
    depth       INT             ,
    color       TEXT            ,
    features    TEXT            ,
    kind        TEXT            ,
    popularity  INT     NOT NULL,
    stock       INT     NOT NULL,
    PRIMARY KEY (id)
);

-- name: GetChair :one
SELECT * FROM chair WHERE id = ?;
const getChair = `-- name: GetChair :one
SELECT id, name, description, thumbnail, price, height, width, depth, color, features, kind, popularity, stock FROM chair WHERE id = ?
`

func (q *Queries) GetChair(ctx context.Context, id int64) (Chair, error) {
    row := q.db.QueryRowContext(ctx, getChair, id)
    var i Chair
    err := row.Scan(
        &i.ID,
        &i.Name,
        &i.Description,
        &i.Thumbnail,
        &i.Price,
        &i.Height,
        &i.Width,
        &i.Depth,
        &i.Color,
        &i.Features,
        &i.Kind,
        &i.Popularity,
        &i.Stock,
    )
    return i, err
}

sqlcはとてもイケてるなと思うツールの反面、まだまだ枯れておらず実践で利用するのには難しい印象があります。
データベースを扱うのってやっぱり難しいなという平易な感想で締めさせていただきます、以上!

kustomizeでCRDのpatchesStrategicMergeを動かしてみる

記事一覧はこちら

背景・モチベーション

以前に記事を書いて、kustomizeでbuildする時にCRDでpatchesStrategicMergeが利用できない悲しみを文字に起こしました。
(紛らわしいので当該記事は非公開にしてあります)

ところが、v4.1.0のリリースで可能になったようです!うれしいですね…!ということで早速試してみた記事です。

参考文献

どのようにCRDでpatchesStrategicMergeを実現するのか

CRDでのpatchesStrategicMergeはkustomizeでどのように実現されるのか確認していきます

Kubernetes本体ではv1.16からCRDのapplyがJMPからSMPに変わっている

kubernetesのv1.15までは、CRDをapplyしたときの挙動がkubernetes/kustomizeで足並み揃っていて、下記のような挙動になっていました。

  • kubernetesのnativeオブジェクトはstrategicMergePatch(SMP)を使う
  • CRDはjsonMergePatch(JMP)を使う

例えば、PodTemplateのコンテナのリストをマージする時などはその違いが顕著です。SMPであれば、各要素のnameをキーとして利用して、同一のキーが存在すれば更新し、無ければ配列に追加するという挙動を取ります。対して、JMPの場合には配列ごと置き換えます。

この状況は、v1.16のServer-Side Applyのリリースによって変わります。
詳しくは、Merge Strategyを参照いただければと思うのですが、API開発者がList/Map/structのマージ方法をOpenAPIスキーマで定義できるmarker(extension)が追加されました。
ちなみにstrategic merge patch で以前から利用されているx-kubernetes-patch-strategy: mergex-kubernetes-patch-merge-keyは、それぞれx-kubernetes-list-type: mapx-kubernetes-list-map-keysとして解釈されます。

そして、Server-Side Applyを利用したCRDで定義されたリソース更新では、markerが存在すれば、その定義に基づいてフィールドの更新を行うようになりました。

しかしながら、kustomizeは従来どおりのJsonMergePatchをCRDのマージで利用する挙動になっていたため、kubernetes v1.16のリリース時からkubernetesとkustomizeの間で挙動の違いが生まれることになってしまいました。

kustomizeでCRDのSMPを行う方法

KubernetesネイティブオブジェクトのSMPに関しては、kustomizeのバイナリにOpenAPIの定義が含まれるので、Merge Strategyはその定義を参照することで確定することができます。対してCRDの定義はバンドルしていません。kustomize自体がどうにかしてCRDの定義を知る必要があります。

そこで、kustomizeはkustomization.yamlファイルにopenapiフィールドを追加して、そのフィールドを読み込んでスキーマを取得するように機能追加を行いました。openapiフィールドが指定されていれば指定されたスキーマを読み込み、そうでなければビルトインの定義を利用するような動きをするようになっています。
この機能追加により、CRDを含む全体のスキーマファイルが手元にあれば、より柔軟なマージ戦略を行うことができるようになりました。

ではスキーマの取得が必要になってきます。これはKuberneteのAPIサーバに対して、/openapi/v2のパスでリクエストすることで可能です。kustomizeはopenapi fetchというサブコマンドの内部でそれを実行することでスキーマの取得をサポートしています。

流れとしては下記のような流れとなります。

  • (CRDのファイルをapplyする)
  • kubectl openapi fetchスキーマのファイルを取得する
  • kustomizationにopenapiのフィールドを追加してファイルを指定する

スキーマのfetchの問題点

kubectl openapi fetch の際の内部処理で/openapi/v2にリクエストしていると先ほど言及させていただきました(参考)。これはOpenAPIのv2での公開です。
OpenAPI v3で定義されたCRDはv2でサポートされていないフィールドを含んだり、表現できないnullableを利用したりします(参考)。それらはkubectl v1.13より前のバージョンでは解釈できないため、互換性を守るために公開時に削除されます。
もしかしたら、この挙動がkustomize buildで影響を及ぼす可能性があります。

実際にCRDでpatchesStrategicMergeできるか確かめてみる

自前のGKEクラスタで確認していきます。バージョンは1.18.16です。

CRDのマニフェストのmergeStrategyを確認する

事前の準備としてCRDのマニフェストのmergeStrategyを確認しておく必要があります。今回はArgoCDのRolloutリソースで確認していきますが、2つのキーの組み合わせで一意に解釈させたい.spec.template.spec.containers.portsなどは既にmarkerが利用されていますが、.spec.template.spec.containersのarrayはそうなっていません。
今回、自分の方でmarkerを追加して試してみたいと思います。.spec.template.spec.containersのarrayのみにmarkerを追加いたしました。ちなみにkustomizeの実装を見ていると、x-kubernetes-list-typeを確認せずにx-kubernetes-patch-strategyのmarkerを確認しに行きます。そのmarkerが見つからないとx-kubernetes-list-map-keysを確認してくれないみたいなので注意してください(該当コード)。
…本家のServer-Side Applyでもこうなっているのかな…?後で確認してみようと思います。

CRDのリファレンスを確認すると、x-kubernetes-patch-strategyが許容するフィールドに存在しないため、schemaをfetchした後に追加してあげる必要があります。辛い…。

確認手順

利用するファイル群はこちらにまとめています。

github.com

自前のGKEクラスタで試してみます。

# CRDの登録
$ cd /tmp
$ git clone https://github.com/44smkn/kustomize-crd-smp-sample.git
$ cd kustomize-crd-smp-sample
$ gcloud container clusters get-credentials my-gke-cluster
$ kubectl apply -f crds/rollout.yaml --validate=false

# kustomizeのインストール
$ curl -s "https://raw.githubusercontent.com/\
kubernetes-sigs/kustomize/master/hack/install_kustomize.sh"  | bash
$ ./kustomize version   # v4.1.0以上であることを確認

# スキーマのfetchとキーの挿入
$ ./kustomize openapi fetch | sed -e '1d' > tmp.json
$ cat tmp.json | jq  '.definitions["io.argoproj.v1alpha1.Rollout"].properties.spec.properties.template.properties.spec.properties.containers |= .+ {"x-kubernetes-patch-strategy": "merge"}' > schema.json


# 確認
$ ./kustomize build > actual.yaml
$ diff -h actual.yaml expected.yaml

上手く動くところまで確認できましたが、仕様の問題なのか自分のドキュメントの読み込みが甘いのかわかりませんが、まだ実用に持っていくのは難しいような要素を感じました。
細かいところを確認して必要そうならIssue挙げてみようと思います。以上!

conftestで複数ファイルを横断してチェックする

記事一覧はこちら

背景・モチベーション

以前にconftestの概要をざっくり知って試してみたという記事を出しました。

44smkn.hatenadiary.com

実際に実務で利用してみると、1ファイル内だけのテストだけでなく、複数のファイルを横断的に見てチェックを行いたいという需要がありました。
例えば、Serviceで指定しているラベルがDeploymentでのpodのlabelに含まれるかといった検査です。

--combineオプションを利用する

contestのv0.24.0で試しています

--combineオプションとは

www.conftest.dev

上記のドキュメントから確認できるように、--combineというオプションをconftest実行時に付けてあげると複数ファイルを一度にロードしてくれます。しかしながら、単一のファイル向けに宣言していたRuleは正常に動かなくなるため、このオプションを利用する場合には書き直す必要がありそうです。
--combineと毎回宣言するのも手間なので、自分はconftestを実行するディレクトリに、conftest.tomlを配置して設定を行っています。

combine = true

--combineオプションを利用した時のinputの内容が変わる

このcombineオプションを利用すると、inputに入ってくるドキュメントの内容が変わってきます。inputの中身を確かめるだけのpolicyを書いて実行してみます。

# policy/combine.rego
package main

deny[msg] {
    msg = json.marshal(input)
}

入力となるマニフェストは下記です。

# manifests.yaml
kind: Service
metadata:
  name: hello-kubernetes
spec:
  type: LoadBalancer
  ports:
    - port: 80
      targetPort: 8080
  selector:
    app: goodbye-kubernetes
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-kubernetes
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hello-kubernetes

実行すると下記のような結果になります。

$ conftest test  manifests.yaml  --combine
FAIL - Combined - main - [{"contents":{"apiVersion":"v1","kind":"Service","metadata":{"name":"hello-kubernetes"},"spec":{"ports":[{"port":80,"targetPort":8080}],"selector":{"app":"goodbye-kubernetes"},"type":"LoadBalancer"}},"path":"manifests.yaml"},{"contents":{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"hello-kubernetes"},"spec":{"replicas":3,"selector":{"matchLabels":{"app":"hello-kubernetes"}}}},"path":"manifests.yaml"}]

出力結果のjsonを整形してあげると下記のようになります。
それぞれのResourceが配列となっています。ドキュメントにあるようにpathキーとcontentsキーをそれぞれのアイテムが持っています。pathキーはファイル名でcontentsは実際のドキュメントの中身です。

 [{
     "contents": {
         "apiVersion": "v1",
         "kind": "Service",
         "metadata": {
             "name": "hello-kubernetes"
         },
         "spec": {
             "ports": [{
                 "port": 80,
                 "targetPort": 8080
             }],
             "selector": {
                 "app": "goodbye-kubernetes"
             },
             "type": "LoadBalancer"
         }
     },
     "path": "manifests.yaml"
 }, {
     "contents": {
         "apiVersion": "apps/v1",
         "kind": "Deployment",
         "metadata": {
             "name": "hello-kubernetes"
         },
         "spec": {
             "replicas": 3,
             "selector": {
                 "matchLabels": {
                     "app": "hello-kubernetes"
                 }
             }
         }
     },
     "path": "manifests.yaml"
 }]

複数ドキュメントを横断してチェックしてみる

inputのファイルなどはまとめてこちらのリポジトリに置いてあります。

github.com

最初の方でも言及したようにServiceリソースの .spec.selectorが Deploymentの.spec.template.labelsに含まれるかをチェックしてみたいと思います。
※ ここではlabelのkeyをappとします

package main

deny[msg] {
    input[deploy].contents.kind == "Deployment"
    deployment := input[deploy].contents

    input[svc].contents.kind == "Service"
    service := input[svc].contents

    service.spec.selector.app != deployment.spec.template.metadata.labels.app
    msg := sprintf("Labels are different! Deployment %v has 'app: %v', Service %v has 'app: %v'", [deployment.metadata.name, deployment.spec.template.metadata.labels.app, service.metadata.name, service.spec.selector.app])
}

ちなみにconftestのドキュメントのExampleにあったように実行すると、下記のようなエラーが出てしまいました。同名の変数ではまずいのではと思い変更して実行したところ上手くいきました。
※ Issue上げたところ対応してもらったので現在はドキュメント上直っていると思います。

Error: running test: load: loading policies: get compiler: 1 error occurred: policy/combine.rego:5: rego_compile_error: var deployment referenced above

これで実行すると下記のようになります。
期待したチェックが機能しています。

$ conftest test  manifests.yaml 
FAIL - Combined - main - Labels are different! Deployment hello-kubernetes has 'app: hello-kubernetes', Service hello-kubernetes has 'app: goodbye-kubernetes'

Rego言語は今まで学習してきた言語とだいぶ違うので感覚を掴みづらいですね…!ただめっちゃ便利なので今後も上手くつかって円滑にチェックしていこうと思います。