Go Conference 2022 Spring: クイズ ISO 8583 Message Challange の紹介と解説

バンドルカードのバックエンドやインフラを担当しているエンジニアの summerwind です。最近は WebAssembly と JIT に興味があります。

4月23日に開催された Go Conference 2022 Spring ではカンムのメンバーが Go に関する内容で登壇しました。セッションで紹介したスライドは以下で参照できます。将棋プログラミングについては自分もまったく知らない世界の話だったのでとても興味深かったです。


今回のイベントにはカンムもスポンサーとして参加させていただき、今回もオフィスアワーの催しとして Go を使ったクイズ「ISO 8583 Message Challange」を公開しました。

クイズの問題

今回のクイズの問題は以下のリポジトリで参照できます。もし興味がありましたらぜひチャレンジしてみてください。

github.com

今回の問題のテーマは「バイナリ処理」です。インターネットでの通信やデータの保存などでは様々な形式のバイナリが使われていますが、一般的なプロダクト開発ではバイナリを直接扱うようなコードを書く機会は意外と少なかったりします。個人的には Go でバイナリを扱うコードを書くが好きなので、今回のクイズではバイナリのパースを通じてより多くの人にバイナリの操作を楽しんでもらえたらと思い、このテーマを設定してみました。

問題では、カンムの決済処理にも使われている ISO 8583 形式のメッセージをパースしてその中から答えとなる値を見つける、というゴールを設定しています。メッセージのバイナリファイルはリポジトリmessage.bin として保存されているので、これをパースしていくと答えが見つかる仕組みです。今回の問題では、より手軽にクイズにチャレンジしてもらえるよう以下のように parse.go ファイルに定義された Parse() 関数の中身だけを実装すれば回答を出せるようにしてみました。

問題の解説

ここからは実際にバイナリをパースしていく方法を解説してみたいと思います。

まず最初に、実装が必要な Parse() 関数の定義を見てみると次のようになっています。引数 buf にはバイナリの値が byte の配列として保存されており、*Message を返せばいいことが分かります。

// ISO 8583 メッセージのバイト列をパースして Message を返す関数
func Parse(buf []byte) *Message {
    var msg Message

    // TODO: バイト列 buf をパースして msg の各フィールドに値を設定してください

    return &msg
}

次に、戻り値である *Message の構造を見みてみると、次のようになっています。

// ISO 8583 メッセージ構造体
type Message struct {
    // メッセージ: 3桁の数字
    // ISO 8583 バージョンの 0 は含まないことに注意してください
    Type uint16

    // カード会員番号 (PAN): 16桁の数字
    PrimaryAccountNumber uint64

    // 処理コード: 6桁の数字
    ProcessingCode uint32

    // 取引金額: 任意の桁の数字
    AmountTransaction uint64

    // 有効期限: YYMM の4桁の数字
    ExpirationDate uint16
}

この構造から今回のクイズでは、バイナリから以下の5つのデータをパースして抽出すればよいことが分かります。

  • メッセージタイプ
  • カード会員番号
  • 処理コード
  • 取引金額
  • 有効期限

データの抽出には ISO 8583 メッセージのバイナリフォーマットの知識が必要になります。弊社の hiroakis が以前発表した資料にフォーマットの詳細と詳しい解説がありますので、ここからはこの資料とあわせて読み進めてみてください。

最初にメッセージタイプをパースします。これはメッセージの種類などを示す値でメッセージの先頭2バイトを4ビットのパック10進数 (Packed BCD) として扱う必要があります。今回の解説では処理を簡略化するため、BCD の処理については公開されているパッケージである https://github.com/albenik/bcd を利用していきます。

// メッセージタイプを取得
msg.Type = bcd.ToUint16(bin[0:2])

次にカード会員番号のパースに取り掛かりたいところですが、最初にビットマップと呼ばれる領域を取得しておく必要があります。

ビットマップはメッセージにどんな種類のデータが存在するかを示すビットを保存している64ビットの領域です (64ビットより長い場合もあります) 。この領域の各ビットの値が1だった場合はそのビット位置に対応するデータの存在を示しています。例えば、ビットマップの値が2進数で 01110010 であった場合、2、3、4、7番目のデータが存在していることを示しています。

// ビットマップ領域を取得
bitmap := binary.BigEndian.Uint64(bin[2:10])

ビットマップが取得できたので、カード会員番号を抽出します。カード会員番号の存在を示すのはビットマップの左から2ビット目の値なので AND 演算で確認します。ビットが存在した場合は値を抽出しますが、カード番号の値は可変長の値なので、最初に先頭1バイトの長さを抽出し、その長さ分のバイト数を Packed BCD として読み出します。

// バイナリのオフセット
offset := 10

// カード会員番号のビットの存在を確認
if (bitmap & 0x4000000000000000) > 0 {
    // カード会員番号の長さを取得
    length, _ := binary.Varint(buf[offset : offset+1])
    offset += 1

    // カード会員番号を取得
    msg.PrimaryAccountNumber = bcd.ToUint64(buf[offset : offset+int(length)])
    offset += int(length)
}

次に処理コードを抽出します。カード会員番号と同じようにビットマップで存在を確認してから、処理コードの値を取得します。処理コードの値は3バイトの固定長なので、そのまま読み出します。

// 処理コードのビットの存在を確認
if (bitmap & 0x2000000000000000) > 0 {
    // 処理コードを取得
    msg.ProcessingCode = bcd.ToUint32(buf[offset : offset+3])
    offset += 3
}

残りは取引金額と有効期限になりますが、これらは処理コードと同じ固定長のフィールドなので、次のように処理コードと同じように値を取得できます。

// 取引金額の取得
if (bitmap & 0x1000000000000000) > 0 {
    msg.AmountTransaction = bcd.ToUint64(buf[offset : offset+6])
    offset += 6
}

// 有効期限の取得
if (bitmap & 0x0004000000000000) > 0 {
    msg.ExpirationDate = bcd.ToUint16(buf[offset : offset+2])
    offset += 2
}

実はビットマップの存在確認に使用する各ビットの値は定数として事前に定義されているので、それらを使用すると最終的な Parse() の実装は次のようになります。

// ISO 8583 メッセージのバイト列をパースして Message を返す関数
func Parse(buf []byte) *Message {
    var msg Message

    // メッセージタイプを取得
    msg.Type = bcd.ToUint16(buf[0:2])

    // ビットマップ領域を取得
    bitmap := binary.BigEndian.Uint64(buf[2:10])

    // バイナリのオフセット
    offset := 10

    // カード会員番号を取得
    if (bitmap & BitPrimaryAccountNumber) > 0 {
        length, _ := binary.Varint(buf[offset : offset+1])
        offset += 1

        msg.PrimaryAccountNumber = bcd.ToUint64(buf[offset : offset+int(length)])
        offset += int(length)
    }

    // 処理コードを取得
    if (bitmap & BitProcessingCode) > 0 { 
        msg.ProcessingCode = bcd.ToUint32(buf[offset : offset+3])
        offset += 3
    }

    // 取引金額の取得
    if (bitmap & BitAmountTransaction) > 0 {
        msg.AmountTransaction = bcd.ToUint64(buf[offset : offset+6])
        offset += 6
    }

    // 有効期限の取得
    if (bitmap & BitExpirationDate) > 0 {
        msg.ExpirationDate = bcd.ToUint16(buf[offset : offset+2])
        offset += 2
    }

    return &msg
}

実装ができたのでクイズの README.md の記載に従いコードを実行してみると、バイナリファイルから正しい値を取得して正解が表示されました。

$ go run .
--------------------
Message Type: 100
PAN: 4019249999999999
Processing Code: 327327
Amount Transaction: 1138
Expiration Date: 2204
--------------------
Result: VALID: You have successfully parsed the ISO 8583 message! ...

回答判定の仕組み

問題が解けたところで、今回のクイズの回答判定の仕組みについても簡単に紹介します。

カンムではこれまでにも Go Conference の開催にあわせて CTF やクイズを公開してきました。これまでの問題では出題を担当するメンバーが独自に回答を判定する仕組みを実装していましたが、今回のクイズ作成にあたっては、より汎用的な回答判定をする仕組みを採用してみることにしました。

今回の回答判定に採用したのは Open Policy Agent (OPA) です。OPA を使用すれば正解判定を Rego で記述したポリシーとして扱うことができるため、非常に簡単に回答判定の仕組みが用意できました。今回の回答判定では次のようなポリシーを使用しています。

package iso8583

default validation = "INVALID"

validation = "VALID: You have successfully parsed the ISO 8583 message!" {
  input.Type == 100
  input.PrimaryAccountNumber == 4019249999999999
  input.ProcessingCode == 327327
  input.AmountTransaction = 1138
  input.ExpirationDate = 2204
}

目視によるパース

実は ISO 8583 形式のメッセージは16進数で見てみると、ある程度データの値を推測できます。実際に目視でパースして正解した、という方もいたようでした。

$ hexdump -C message.bin
00000000  01 00 70 04 00 00 00 00  00 00 10 40 19 24 99 99  |..p........@.$..|
00000010  99 99 99 32 73 27 00 00  00 00 11 38 22 04        |...2s'.....8".|
0000001e

おまけ

クイズのバイナリに含まれる 3271138 といった値はスターウォーズでたびたび登場するマジックナンバーに由来しています。これは Go のトリビアをリスペクトしてみました。

おわりに

Go Conference の開催中は、Twitter などでクイズの問題に実際に挑戦してバイナリパースの楽しさを感じてくれた方のコメントを見かけたりして嬉しかったです。

カンムでは実際に ISO 8583 のメッセージを処理するシステムを開発して決済サービスを提供しており、バイナリ処理に楽しさを感じるようなエンジニアを募集しています。カジュアル面談などは随時実施していますので、ぜひお気軽にお声がけください。

kanmu.co.jp

最後に、今回も素晴らしい Go Conference の場を提供してくれた運営のみなさま、参加者のみなさま、どうもありがとうございました!

カンムは SRE NEXT 2022 にスポンサーとして参加します #srenext

こんにちは!カンムでエンジニア採用を担当している @ayapoyo です。 ついに今週土曜・日曜は SRE NEXT の開催日ですね! 今回はじめての参加になるのでいまからワクワクしております!

sre-next.dev

ゴールドスポンサーとして協賛します!

今回カンムで初めて SRE NEXT にスポンサーとして参加させていただきます!

カンムは運営する『バンドルカード』は500万ダウンロードを突破、2022年には手元の資産形成に活用できるクレジットカード『Pool』をリリース予定と、サービスが拡大していく中でより信頼性の高い決済インフラを構築していくことが求められます。

今回のスポンサーをきっかけに、さまざまな業種・領域・フェーズにおける SRE 領域の知見を得るとともに、カンムとしても SRE コミュニティへ還元していければと思っています。

kanmu.co.jp

スポンサーセッションに登壇します!

今回はスポンサーセッションとして、インフラエンジニアの @sgwr_dts が登壇いたします!

もうすぐリリースされる新規事業『Pool』についてお話します。 Track B にてご視聴いただけます!ぜひ御覧ください。

そしてイベント当日はオンラインブースも出展予定です! CTO の @mururururu COO の @_achiku が滞在しているので、ぜひ遊びに来てくださいね。

参加申し込みは SRE NEXT HP から

sre-next.dev

当日たくさんの方とお話できることを楽しみにしております!イベントでお会いしましょう〜👋

カンムのセキュリティ事情

こんにちは、livaです。

カンムでセキュリティエンジニアやってます。入社してから半年程度経った今はPCI DSSの監査準備だったり優先度高めにした施策をOKRに落とし込んで手を動かしたりと慌ただしく動いてます。

初執筆のテックブログでなにを書こうかなと考えていて、3月の末に出たPCI DSSv4がいいかとも思ったんですが、読むだけで一苦労だったので諦めました。あとからゆっくり読みます。 今回はカンムの今と将来のセキュリティ事情を書こうと思います。

入社前の想定

面接や面談時にいくつか課題を聞いていて、大きく2つになるのかなーと考えてました。

1. PCI DSSの運用の課題

カンムはクレジットカードの決済フローでは「イシュア」にあたり、業界のセキュリティ基準であるPCI DSSに準拠している必要があります。これがないとそもそものビジネスが成り立ちません。毎年の準拠が必要なため、最低限のセキュリティの体制は出来上がっています。が、日々の運用や監査前の準備は慌ただしくなりがちでした。

2. プロダクトセキュリティ全般の課題

すでにいたエンジニアの良心に任せた運用とPCI DSSの要件に沿った運用がされています。防御機構も一通り入っているもののアラートに対する反応であったり、AWSで回しているスキャナ結果への反応だったりに対してあまり積極的に手を動かせていませんでした。

入社してから現在までの動き

PCI DSS

今までの自分は監査する方の手伝いはしたことあるんですが、受ける側はまともに経験してないので「とりあえず今年回してなにができるか考えて来年やりやすいように変える!」で動かしています。実質なにもできてない。v4のリリースもあって来年はこっちに揃えるつもりでもいるので「v4への対応&運用効率化」に課題が増えました。ナンテコッタイ。

プロダクトセキュリティ

PCI DSSと違って得意領域で、さらに時間をかけずに片付けられる課題もあったので以下の様なものはさっと片付けました。

それ以外にある腰を据えて取り組まないといけない課題については洗い出してざっくりな優先順位を出しました。

その他

なにかが起きた時、全社としてのなにかしら判断基準があるわけではなくて各チームの判断で動いているので、一体感のある何かは作りたいなぁというのを感じたので課題に積みました。そのための指標としてカンムで持っているいろんな情報資産のレベル分けをして、それをベースに意思決定できるような仕組みを作ろうとしてます。前職であったんですが色んなことが進めやすくなったのでカンムでも作ろうかと思って動いてます。

セキュリティOKR

立てようとしたきっかけが完全に覚えてないんですが、CTOと話してるときにそういう話をしたと思います。 この話の前後で同僚が退職したのもあり「1人だとどんだけできんだべ?」と考えた結果、Objective1つにKeyResult3つを立てました。この時気にしていたのは以下の点です。

  • 自分が楽しめること
  • 進捗0.7が完了
  • 0.7から1.0は趣味
  • チェックポイントの到達条件は明確に

OKRの立て方としては基本的なところですよね。基本に忠実に、やりやすいようにしました。自分が楽しめるのも個人的にはずっと大事にしているので織り込んでます。やりたいことをやるために先に手を付けなきゃいけない部分があるのでやる、みたいなことですね。先に面白いことが待ってるなら意外とやれるもんです。

これから

今は「目の前にあるもの片付けて隙間時間で新しいことの仕込み!」って状態になってますが、将来も見据えてはいます。

チーム

今は1人ですが、今後2人3人と増えていって、できることが増えた時も、各自が主担当領域を持ち、他の領域にもオーバーラップしていくのが自分としては好きなので、そういうチームを目指したいなぁと思います。例えば監査、インフラ、アプリと分けたとき、それぞれに担当を分けるけど、それぞれがなんとなく全部を把握していてバックアップとして機能できる、という。色んなチーム体系があるけど、自分の中ですんなりいきそうな体制はこうなるのかなぁと思ってます

PCI DSSの運用

ここは最近界隈でホットなOPA(Open Policy Agent)と相性がいいんじゃないかという話になっていて、今年度の監査が終わってから本格的に着手しようかなぁなんて考えてます。個人的にも「運用できたら3割くらいは心労が減るんじゃね?」って思ってたりするので手を付けたいですね。このビジネスをしている以上、監査からは逃げられないのでなるべく準備に手がかからない未来を目指したいところです。

プロダクトセキュリティ

エンジニアの個人技でどうにかなってるので、これを統一した仕組みに持っていきたいと思ってます。 「PR作ったらCIでDAST/SASTの各種スキャナ動かして、その結果を適当なとこに集めて、Dashboardで状況把握ができる」なんて形を作れたらなぁと思ってます。アプリもコンテナもインフラもそこを見ればどんな脆弱性があって具体的なリスクはこれで対応状況はどうなっていてというのが見れたら視覚的にも楽しいと思うんですよね、多分。あまりに検出結果が多くてげんなりすることが多いかも。 カードにありがちな不正利用にもセキュリティが齧る余地があるので、対応チームの動向をチラ見しながら自分なりに考えてます。自分がやられて嫌なこと考えてると楽しいのもあり、タスクで煮詰まった時の気分転換になってます。

全体

基本的なものはあるのだけど、それを日常的に動かそうとなると不足しているものがそこそこあるので、そういったものの整備や社内広報をして…なんてのをぼんやりと考えてます。カンムのいいところとして、エンジニア以外の職種でもエンジニアチックな動きができるので、そういった文化は活用しながらカンムに合ったセキュリティ体制を作っていきたいという思いが強いです。

最後に

現状と未来は出せる範囲で伝えられたかなぁと思います。「どこから手を付けるかはだいたい見えてるけどやることいっぱいで手が足りない!」って状況なので、一緒にやってくれるエンジニアを募集中です。「セキュリティに興味がある」ってだけでも大丈夫です。自分が必要なことを教えます。もちろん「こういうのやりたい!」って飛び込んできてくれる人も大歓迎です。

kanmu.co.jp

カンムは Go Conference 2022 Spring にスポンサーとして参加します #GoConference #gocon

こんにちは!カンムでエンジニア採用を担当している @ayapoyo です。 ついに今週土曜日は Go Conference 2022 Spring の開催日! 今回もスポンサーとして参加させていただきます。

gocon.jp

シルバースポンサーやります ʕ◔ϖ◔ʔ

2021年に引き続き、今回もスポンサーとして Go Conference に参加させていただきます!

カンムが展開する "バンドルカード" そして新規事業の "Pool" のバックエンドは Go メインで開発しており、このイベントを通して Go コミュニティの発展に寄与できればと思っています 💳

kanmu.co.jp

エンジニアがセッションに登壇します ʕ◔ϖ◔ʔ

今回の GoCon では、CTO の @mururururu バックエンドエンジニアの @pongzu がセッションに登壇します! 以下スケジュールで登壇するので、ぜひご覧ください。

16:50 Go で始める将棋AI @mururururu

17:55 外部コマンドの実行を含む関数のテスト @pongzu

オフィスアワーで クイズ を開催 ʕ◔ϖ◔ʔ

今回の GoCon も完全リモート開催!Remo でオフィスアワーを実施します。 カンムのブースではこんなことをやります!

決済処理に使われている ISO 8583 のバイナリを解析するクイズに挑戦!

カンムが提供するクレジットカードの決済処理にも使われている ISO 8583 形式のバイナリを解析するクイズを当日公開予定。 ブースやTwitterで随時ヒントの発表や解説などを行います!

カンムのエンジニアが作成したこのクイズ、なかなか解き応えのあるものに仕上がっています…! セッションの休憩時間、ぜひクイズにもチャレンジしてみてくださいね!

このほかにも

  • カンムやバンドルカード・Pool について聞いてみたい!
  • 登壇していたエンジニアと話してみたい!
  • Go についてわいわい話したい!

などなども大歓迎です!Remo のブースに遊びに来てみてくださいね!

参加登録は connpass から ʕ◔ϖ◔ʔ

gocon.connpass.com

gopher のみなさんとお会いできることを楽しみにしております👋

カンム流朝会最適化#1 『確率モデル編』

はじめに f:id:fkubota_owl:20220331123312p:plain

こんにちは、カンムでバンドルカードの機械学習部分を担当している fkubota です。(ちなみに機械学習エンジニアめっちゃ探してます👀)
前回の記事から日が経ってしまいました。
あれから、沖縄に移住する(2月に)などプライベートで大きめのイベントが多発して忙しく過ごしていました。
そのせいもあり新しい技術に触れるような時間が少なくてなかなかテックブログネタが思いつかなかったのですが、朝会で不便を感じていたのでテックでいい感じにしたろ!と思い立ち勢いで書きます。

前回の記事も朝会についてでした。 カンム流『朝会』をやってみたら予想以上にウケが良かった件
ざっと概要を話すと、

  • リモートで雑談減ったよね
  • 雑談する会を設けても継続的に行うことって難しいよね
  • 新しく入社した人が関係構築するのも難しいよね

という思いから、飽きない、形骸化しづらいしくみの朝会を開催しました。
2021年6月ごろにはじめて未だに(2022年03月31日現在) 続いているのでなかなか悪くない仕組みでは?と思っています。
仕組みは簡単で、

  • 週に2回、朝15分開催
  • メンバーは毎回ランダム(現在は18人から5人選ぶ感じ)
  • 聞き専禁止
  • テーマなしの雑談

という感じです。
短いこととランダムなことが効いて飽きづらい仕組みにしています。
biz寄りの人もいればデザイナーもいて普段仕事で会話しない人とも会話できて楽しいです:)

もうすこしいい感じにしたい朝会

f:id:fkubota_owl:20220331124101p:plain
この朝会を運用していてもう少しいい感じにしたいなぁと思うことがありました。
ランダムにメンバーを選んでいるのですが、あれーあの人全然選ばれてないなぁとかあの人とあの人が一緒の会に参加してるの見たこと無いなぁとかそんなことを思っていました。
20回参加している人もいれば、5回しか参加していない人もいます。
AさんとBさんはそれぞれ10回参加していますが、同時に参加したことは0回だったりもします。
朝会は月に多くても 10回程度しか開催されないので、そういう状況はまあありえますよね。

問題の形

f:id:fkubota_owl:20220331124847p:plain

現在の悩みのタネは

  • 全員の参加回数に偏りがある
  • 同時に会に参加したことのないペアが存在する

です。
前者だけであれば、簡単にできそうなのですがペアまで考慮するとちょっと複雑そうです。
こういう制約のある数理最適化問題みたいのって存在しそうなんですが、僕は詳しくないので知りません。
なにか知っている人いたらコメントで教えて下さい。

確率モデルを導入して解いてみる

ということで確率モデルを導入してみます。
確率モデルというと仰々しいですがそんなにかっこよくて難しい話ではありません。
もう少しお付き合いいただくとわかってくるかと思います。

僕は、問題を解くための土台として以下のような表を用意しました。

f:id:fkubota_owl:20220331125217p:plain

これは、A~Jの10人のメンバーが朝会に参加した記録です。
10人から3人が朝会に参加するとします。
[A, B, E] が選ばれた場合、表の A行B列、A行E列、B行E列のセルに1が加算されます。
A行B列とB行A列のセルは同等な意味を持ちますので上三角部分だけが意味を成します。

上の表を導入して話を進めていきます。
次に朝会を30回行った場合を見ていきます。
メンバーはランダムに選びます。(pythonで実装しています。)

1回目
f:id:fkubota_owl:20220331125705p:plain

5回目
f:id:fkubota_owl:20220331125829p:plain

15回目
f:id:fkubota_owl:20220331125900p:plain

30回目
f:id:fkubota_owl:20220331130408p:plain

こんな感じになりました。
BさんとEさん(B行E列)は5回同じ朝会に参加していますが、AさんとDさんは一度も同じ会に参加していません。
各人の合計参加回数はどうでしょうか?

f:id:fkubota_owl:20220331130618p:plain

となっており、明らかに偏りがあります。
Bさんの32回に対してDさんは6回なので、参加回数に5倍も差がありますね。
これはなんとかしたいところ。

やりたいことは明らかで、この参加回数表の値が小さいところが選ばれやすいような確率モデルを導入すればいいだけです。
現在のランダムな状況をモデルとして考えると、以下のようになります。

f:id:fkubota_owl:20220331131239p:plain

各セルの値が選ばれやすい確率を表しており、すべてのペアを等しい確率で選んでいるので0.022で一定値です。
ちなみにすべてのセルを足すと1になるように正規化しています。 100をかければ%になるので、 0.022×100 = 2.2%なのですべてのペアは2.2%の確率で選ばれます。

このすべて同じ値の表(確率モデル)を改良して、すべてのペアでバランス良く朝会を実現しようというのが今回解いている問題です。
ここまでこれば後は解けたようなものですね。
サクッとやってしまいましょう。

まずは、各セルの値ごとに 朝会に参加していない度 を定義したいと思います。
各セルの参加回数を  n _ {ij}、全体の朝会開催数を N とします。
以下のような指標 \alpha _ {ij}はどうでしょうか?


\alpha_{ij} = \frac{N - n_{ij}}{N}

Nが定数(今回だと30)なので、  n _ {i,j} が大きいほど、上の指標 \alphaは小さくなります。
つまりペアが実現した回数が大きいほど小さくなります。 朝会参加していない度 として使えそうです。
実際にセルに当てはめてみます。

参加回数表

f:id:fkubota_owl:20220331132618p:plain

上記から計算した  \alpha
f:id:fkubota_owl:20220331132655p:plain

回数が少ないほど、 \alpha が大きな 大きな値を取っています。
例えば、  n _ {A, D} = 0 だと  \alpha _ {A, D} = 1 であり、 n _ {B, E} = 5 だと  \alpha _ {B, E} = 0.833 となっています。
しかし、0回と5回で極端に差が開いているのに、1/0.833 ≒ 1.2 と仮に確率として扱うと、1.2倍程度しか差がなくて微妙です。
もう少し、回数の差に対して勾配をつけたいです。さらには、その勾配加減を調整できると嬉しいです。
実現方法はいくらでもありますが、僕は 指数関数を用いることにしました。

新しく定義する 朝会参加していない度 \beta とします。


\beta_{ij} = e^{\lambda \alpha_{ij}}

ここで、 \lambda \lambda >0 の実数であり、\lambdaが大きいときには n _ {ij}が小さい場合、 \beta _ {ij}をより小さく、大きい時より大きくします。(勾配をコントロールします)

\lambda=1 の場合と  \lambda=5 の場合の  \beta を見てみましょう。

\lambda=1 の場合
f:id:fkubota_owl:20220331134847p:plain

\lambda=5 の場合
f:id:fkubota_owl:20220331134920p:plain

\lambda=5 のほうが n _ {ij} の値の大きさに激しく値が反応している事がわかります。

あとは、これを正規化(すべてのセルを足して1にする)して確率  p _ {ij} として扱います。


p_{ij} = \frac{e^{\lambda \alpha_{ij}}}{\sum_{all\_cells} e^{\lambda \alpha_{ij}}}

という式になり、おなじみにの softmax関数となりました。

作ったモデルで遊んで見る

作ったモデルで実際にシミュレーションしてみます。
朝会のメンバーを選ぶプロセスは

  1. 参加回数表から確率の表を作成
  2. 表を元に3人の参加者を選ぶ
  3. 表を更新
  4. 1~3を繰り返す

とこんな感じです。

  • randomに選ぶパターン
  •  \lambda=5 で選ぶパターン
  •  \lambda=50 で選ぶパターン

をやってみました。
朝会の開催回数うは  N=50としています。
結果は

  • random
    f:id:fkubota_owl:20220331140510p:plain

  •  \lambda=5
    f:id:fkubota_owl:20220331140609p:plain]

  •  \lambda=50
    f:id:fkubota_owl:20220331140907p:plain

となり、  \lambdaが大きいほど極端に多い/少ないが見られません。
意図した動作になっているようです。
ちなみに、 \lambda=50 の終了時の確率の表は以下のようになっていました。

f:id:fkubota_owl:20220331141112p:plain

また、参加合計回数を randomと  \lambda=50 で比較すると \lambda=50 のほうが公平に参加できている事がわかります。

  • ramdom
    f:id:fkubota_owl:20220331142514p:plain

  •  \lambda=50
    f:id:fkubota_owl:20220331142359p:plain

評価してみる

上述したように  \lambda によって、参加回数をコントロールできるようになり、参加回数表も均一になっているように見えます。
どの程度均一になっているのか?を定量的に評価したくなったのでこれもやってみました。
まあ、分散でいいだろうと思ったのでちゃちゃっと計算した結果を見せます。
分散は、参加回数表の各セルの平均値を \mu、セル数を N_cとした時以下のような式で表されます。

\displaystyle{
var = \frac{1}{N_c}\sum_{all\_cells} \left( n_{ij} - \mu \right)^2
}

横軸に朝会の開催回数、縦軸に  var を取ったグラフを書きました。

f:id:fkubota_owl:20220331142112p:plain

こちらも \lambdaが大きいほど分散が小さいことがわかります。
意図した動作が実現できています。

おわり

以上です。 なんか朝会ガチ勢みたいな記事になってしまって申し訳ないですが、最後まで見てくださってありがとうございます。
頑張って考えましたが実践導入するかちょっと迷います。
めんどくさくなっちゃうといけないので、そこまでのメリットがあるのかは要検討です。
とはいえ、自分で問題を作って解くというはやっぱり面白いなと思いました。
結構楽しめてよかったです。

第二弾があるかはわかりませんが、面白そうな事ができそうならチャレンジしてみたいです。

定番ですが、積極採用中です!
機械学習エンジニアをめっちゃ探してますのでカジュアル面談からでも何卒!!!!

kanmu.co.jp

カンムにおけるGitHub Projects Beta活用方法

マニアックなSQLに続き2回目の登場、COOの achiku です。

これは

カンムでは GitHub Projects (Beta) を利用してプロダクト改善を推進している。Private Betaの時点から使い始めてから約4ヶ月、今の運用に落ち着いてから約2ヶ月程度経過したため、導入の目的、目的を鑑みた運用方法、現時点での状態をまとめる。誰かの参考になれば嬉しい。

※以降断りのない場合はGitHub ProjectsもしくはProjectsはGitHub Projects (Beta)を指す ※同様に以降断りのない場合はprはGitHub上のPull Requestを指す

前提(2022/03時点)

まずは前提の共有から。ぱっと見ても分かるように、小さくはないがとんでもないサイズでもない、という状況のチームの話であるという前提がある。

  • 作っているもの
  • 2016年ローンチ当時からGitHubを利用して開発している
  • アプリと通信するVandle API、決済処理を担うProcessor、ネイティブアプリのリポジトリは別れている
  • プロダクトのサイズ感が分かりそうな情報(as of 2022/03)
    • Vandel API
      • テーブル数: 310
      • APIエンドポイント数: 121
    • Processor
      • テーブル数: 47
      • APIエンドポイント数: 6
  • バンドルカードチーム構成: 約15名
    • ソフトウェアエンジニア(バックエンド/インフラ): 4
    • ソフトウェアエンジニア(モバイル): 1.5
    • データサイエンティスト/アナリスト: 2
    • デザイナー: 2.5
    • マーケター(広告運用含む): 3
    • PM: 1
    • 何でもやる人(achiku): 1
    • (0.5=poolという新プロダクト兼務)

2021/06に Introducing new GitHub issue が公開された。その3ヶ月後の2021/09のThe new GitHub Issues – September 29th updateにて、Workflowsが導入された事が確認できた。これで複数リポジトリを跨いでソフトウェアエンジニアは通常通りissue/prを中心に仕事をしていれば進捗がProjects上に反映される基礎が整った。

導入の目的

Projects、なんとなく良さそうとはいえ目的を明確にしなければ情報の設計、運用の設計、チームの納得感の醸成を行うことは出来ない。サービスありきで導入したが結局使われないというのは悲しい。よってまずは以下2点に絞って導入の目的を明確化した。

1. 進捗の把握/報告という行動を撲滅する

  • 普通に仕事をしていたら勝手に進捗が記録されて欲しい(or 最小限の手数で記録されて欲しい)
    • 進捗を報告するのも確認するのも極力緊急事態発生時のみにし、普段は誰が見てもサクッと分かるようにすることでより生産的な仕事に時間を使いたい
  • 誰がどの程度のボリュームの仕事に取り組んでおり、どのタイミングで次の大きめのタスクに取り組めそうか/今ちょっとお願い毎出来るのかを"全員"が確認する術を持ちたい
    • 一人が優先順位とチームの稼働状況をリアルタイムに把握して差配する形式だと、チームメンバーが5人を超えたあたりから非効率の方が大きいと感じている

2. チームが自律的に改善に取り組めるような情報の通り道を作る

  • 優先順位が明確になっており且つチームがその優先順位に納得感を持つことで自律的に動きを取りやすくする
    • あくまでも情報の通り道なのでGitHub Projectsを導入するだけで自律的に改善が出来るわけではないが、"最新情報が常にメンテされ続けている場所"として活用可能と考える
  • 優先順位によって事業インパクトが出る確率を上げる工夫、その優先順位が現時点で最善であるという思考プロセスの伝達の工夫は別途必要
    • プロダクトが提供する価値の言語化、事業計画とプロダクトが提供する価値の中間表現、プロダクト会議体の設計、優先順位の明示とその背景ストーリー、学習した事とその共有、全てのあわせ技

優先度は1の方を高く設定した。仕組みで解決可能っぽいが人数が増えれば増えるほど全体としての無駄が大きくなる性質を持っているからだ。1と比較して2はかなり複雑で、あわせ技と継続的努力で改善していく類のもの。ただし、GitHub Projectsの中でもissue/prに優先順位を分かりやすく付ける事ができる為一旦目標の中に入れた。あわせ技である「プロダクトが提供する価値の言語化」「事業計画とプロダクトが提供する価値の中間表現」「事業計画の四半期計画策定プロセス改善」等も同時に進めていたので、それらの情報の通り道としての役割を担ってもらえないかなぁという期待があった。

導入前の実験

まずは優先度を高く設定した「進捗の把握/報告という行動を撲滅する」という目的を、GitHub Projects導入で解決できそうなのかを小さく実験することにした。vandleというProjectsを作成し、既存のissue/prをachikuが勝手にProjectsに登録しViewを作りながら動作確認していくという流れ。今各チームが沿っているフローの変更は最小限にして本当に目的が達成できるのかを見たかった。

余談だがProjectsをissue/prに紐付けるというのはよく出来た設計だなと思った。issue/prが適切に運用できているチームであればという前提はつくが、それぞれに対してメタデータを付与し一覧化/グループ化を試しながら小さく実践投入出来る。そしてそこまで大きなプロダクトでなければ1名GitHubに慣れた人が本気出せばいける(と思う)。ここで小さく価値を感じてもらえれば、まずは開発チーム内部での運用、ひいてはプロダクトチーム全体での運用につなげていける。そしてそれは開発者のみが参加するGitHubのシート数という市場を拡張することにつながる。実験が軽いのは本当に正義だし、市場をズラして拡大するのは大正義だなと思う。

この実験の中でやったことは以下。

Workflowsを設定する

いつもの仕事してたら勝手に進捗が記録されるようにする為には必要な設定。カンムではissue/prを中心として開発になっているのでこれらがclose/mergeされたら自動的にProjects側にも反映される。もちろん、issue/prに現れない仕事や大きなissue/prになると正確に記録し続ける事は難しい。限界はあるし、詳細にやろうとしすぎると効用が逓減していく類のものと認識しているので最初はおおらかな気持ちで良いんじゃないかと考えている。2022/03現在Default workflowsは "Code review approved" 以外全てEnabledにしている。

f:id:kanmu-tech:20220301190755p:plain
Workflows

StatusとViewを作る

カンムではBoard/Priority/Milestone/Archived/NoPriorityの5つのViewを作っている。Status属性はToDo/Planning/In Progress/In Review/Done/Archivedの6つに増やした。ToDo/In Progress/Done/Archivedだけでスタートしたが、機能の検討も入れたいよね(=Planningの追加)、レビュー依頼まで終わってるものは分かりたいよね(=In Review)を追加している。が、最初はシンプルに始めるのが良いと思う。

BoardはStatus属性でGroup Byしているだけなので飛ばして、Priority/NoPriority/Milestone/ArchiveというViewの役割とどうやってフィルタしているかを以下で解説する。

Priority/NoPriority

まず以下のような形でザクッとPriorityを定義した。あまり言い回しにこだわらず、この段階ではなんとなくこんなもんかなくらいで良いと思う。後で精緻化すれば良い。

  • Priorityの定義
    • P0🔥
      • ユーザー影響が出ている障害/影響が出かねない事案/セキュリティ関連の緊急対応はP0とする
    • P1💨
      • 四半期で定めた注力事項、事業計画上の必達事項、パートナーとの依存関係がありリリースの期限が存在はP1とする
    • P2😗
      • ユーザー/チーム/会社に取ってやった方が良い事は分かりきっているがP1ではないものはP2とする
    • P3🌴
      • 出来たらやりたいがそこまでインパクトなさそうなものはP3とする

Priority ViewはTable View、Priority属性でGroup ByしPriority属性昇順でソートし -status:Done -status:Archived とフィルタをかけてDone/Archivedなものを除外する。こうすることで今Q抱えている注力項目(P1)は何か、P1がブロックされているのであれば手をつけれるP2は何か、という事を常時更新し続けられているリストを見ながら考える事が出来る。

f:id:kanmu-tech:20220301201600p:plain
Priority View (1)

f:id:kanmu-tech:20220301201623p:plain
Priority View (2)

NoPriority ViewはTable View、 no:priority とフィルタをかけてPriorityがついていないものを表示する。Group Byやソートは設定していない。暫定運用時はこのViewを定期的に確認し、登録されたissue/prの優先度を一旦achikuが判断することにしていた。このViewがあることで「一応Priorityの定義はあるが最初は難しい事を考えずにProjectsに放り込んでほしい!」というお願いが可能になる。

Milestone

以前はIssueのMilestoneを利用して複数リポジトリを跨いだリリース時期定義をしていたが、現在はProjects内で定義しリポジトリ跨いで付与できる属性であるIterationという概念を利用している。(The new GitHub Issues – October 14th update Milestoneでgroup byするのはまぁまぁ面倒だったのでこのリリースノートを読んで小躍りして喜んだ事を覚えている。)

現在のMilestone ViewはTable View、Iteration属性でGroup ByしPriority属性でソートし -status:Archived というフィルタをかけてArchivedなものを除外する。自分はこのViewを最も頻繁に見ている。既存の流れを踏襲して1 Iteration = 1 weekとして運用しており、金曜の段階で全てがクリアされていると爽快な気分になる。

f:id:kanmu-tech:20220301201930p:plain
Milestone View

Archive

Archive ViewはTable View、Iteration属性でGroup ByしIteration属性を降順でソートし status:Archived -no:iteration とフィルタを掛けてStatus属性がArchivedでIteration属性が付与されているものだけ表示する。もちろん登録しDoneまで遷移したissue/prをProjectsから外すこともできるが、過去の振り返りも行いたい為このViewを作っている。前週あるいは前Q単位で振り返るのにもこのArchive Viewは便利だと思う。

f:id:kanmu-tech:20220301202159p:plain
Archive View

実験中のコミュニケーション

なるべくチームに負担を掛けないように、ただし「何やってんのか分からんな」とならない程度にやっている事を共有しつつフィードバックをもらい、「それ便利じゃん」となってもらえるように微調整していった。この辺はあまり言語化できることはなく、自分は今までの信頼貯金的な部分に助けられたと思う。各位自分のポジションを鑑みていい塩梅でよろしくやっていって欲しい。

実験中の学び

実験中は結構学び多かった。あまり客観的数値と共に示せるものがなく心苦しいが重要だなと思った学びをいくつか挙げる。各位割引ながら読んでほしい。

モメンタムが視えるッ!

毎週少しずつプロダクトが提供する価値が上がっていく、実験を通して分からない事が減っていく、というのを実感できる。また、チーム全体のスループットは大体これくらいなんだなというのが確認できる。それはかなり基礎的な事では?という指摘はそうなんだけど、一部の開発メンバーだけでなくプロダクトチーム全体が上記を認識できるようになったのは良かったと思う(Projectsを利用したプロダクト定例のあわせ技的な側面もあるが)。

P0/P1が分かるからP2/P3がやりやすい

実験していた時期は自分もバックエンドの開発に参加しており、優先順位が明確になっていると細かい改善もやりやすいと感じた。「このP1は一旦フロントエンド側待ちなのでこっちのサクッと片付きそうなP2/P3やるかな」や「休憩の仕事としてのP2/P3」みたいな動きがやりやすくなっている(※休憩の仕事は自分の造語なのでチーム内で特に使われているわけではないが雰囲気伝わるかな...)。

同時にArchive Viewを振り返りながら「今回のIterationがP1だけになってしまっているのでP2/P3もう少し入れれないか」「このIterationのP1/P2/P3割合はいい塩梅」「このIterationはP0結構入ってしまったので他あまり出来なかったな...」等、どういう優先度の割合でタスクに取り組んでいるのかを見返せるのは良いなと思う。

もちろん、P1をしっかりとプロダクトが提供する価値の増大に紐付ける為の事前調査や実験設計はとても重要。だが往々にしてP1仕事は不確実性が高く複雑で、1週間やったら即時結果が出る類のものではない。そういう難しい問題をチームで解くためにもフォーカスを保つことは、事前調査や実験設計と同様に重要。ただ、これは個人的な話ではあるが、少しでも、しかし確実に良くなる改善があるのであれば、時間を見つけてサッとやる、しかも品質も高い、それが腕前だろうという思いがある。「"要はバランスおじさん"をしない為に腕磨いとるんじゃこちとらよォォ」と、心の野山に住んでいる山賊が言っているのだ。今回実施した優先順位付けのProjects登録はこの心の野山に住まう山賊の性に合っており自分は気に入っている。

システム的な改善を四半期注力事項にどうやって入れていくか

これは実験の振り返りをしていく中でソフトウェアエンジニア側から上った指摘。実験時に策定したP1は、事業計画を四半期に割った際に目指すべき事業の結果とプロダクトがユーザーに提供する価値向上を合わせて決めていく方式を取った。この際、システム的な安定性/開発容易性は事業計画上に現れにくい為、あまり考慮出来ていなかった。が、しっかりと商売していく為にはBS/PL双方が重要であるように、プロダクトが提供し続ける価値とプロダクト内部のシステム的/オペレーション的品質にも目を配り、適切にメンテナンスし続けていく事は重要だ。この部分はまだ決定打と言えるような解決策は練れていないが、以前から四半期単位で運用しているOKR運用に載せ、そこでプロダクトマネージャー(achiku)とソフトウェアエンジニアチームの議論を経て決めた四半期注力項目をソフトウェアエンジニアチームのP1とする、という形を取っている(OKR、2019年から運用しているんだけどその話は別途)。

今のところObjectiveとKey Resultの形に落とす事でリファクタをするにもパフォーマンスチューニングをするにも監視強化するにも地に足のついた議論が出来るので、結構良い気はしている。

Qの最後に2週間ほど開発は手を止めて細かい改善を行うのは良いリズムを作るかもしれない

以前 Shape Up を読んだ際にソフトウェアエンジニアは6週間で作りその後の2週間で作る際に荒れてしまった部分、作っていたら見つけた改善可能なポイント、リファクタをやるという話があった。自分はShape Upの「アイディア/機能を考える人」と「作る人」を明確に分けるスタンスにはそこまで共感できていないのだけど、このサイクル自体は良いなと思っていた。最初から内部的な品質には固定割合の労力を割く事を決めておき、対応が後手になるのをルールでカバーするというか。また、deeeet氏と話した際に彼のチームでも6週間+2週間のサイクルで回していて調子良いと言っていたので、まずフロントエンドチームで実験。2週間で細かいライブラリの更新、やろうと思って積み残していたリファクタ、CI/リリースプロセスの改善、等を経てみて話を聞いたが好評だった。

もちろんプロダクトの内部品質で重要な部分はOKRに載せてP1として対応するんだけど、どうしてもそこに載せるまでもない細かい改善ポイントは残ってしまう。よって、こういう時間を固定で定義して早めに倒しておくのは費用対効果としても良いのかもしれないと今は考えている。この辺みんなどうやって運用しているのか知りたい。

まとめ

再度目的をまとめる。

  • 進捗の把握/報告という行動を撲滅する
  • チームが自律的に改善に取り組めるような情報の通り道を作る

「進捗の把握/報告という行動を撲滅する」はProjects導入し、チームに便利さを体感してもらい、運用方法を周知し、振り返りを行い、今後もこのフローを改善しながらやっていこうという流れは作れている。もちろんこれで完成というわけではなく、プロダクトチームがより一体となって提供する価値を向上させる為に改善していきたい。

「チームが自律的に改善に取り組めるような情報の通り道を作る」に関してはPriorityで進む方向の大枠を示し、Iterationでリズム良く改善をしていける形は整った。再度になるが、この目的の中でProjectsが貢献できる部分は比較的小さな一部であり、プロダクトが提供する価値をより大きくしていく為にはより包括的な活動が必要になる。その部分に関しての工夫や学びもいつか共有できるようにしたいと考えている。

カンムにおけるGitHub Projects (Beta)の導入方法、運用方法、1周回してみての学びを書いた。プロダクトやチームのサイズによるが、GitHubを利用してさえいれば上記のViewを作りとりあえず始められる。すでに利用しているチームがあれば是非どうやっているのか教えてほしいし議論したいと思っているので achiku まで気軽に話しかけて欲しい。Meety もあるので是非!

TFLintを使ったterraformのチェックとカスタムルールの設定

インフラエンジニアの菅原です。

カンムはサービスの運用にAWSを使用し、そのリソースの管理にterraformを使用しています。 リソースの定義はGitHub上でコードとして管理されているので、何かリソースを追加する場合はプルリクエストを作成してレビューを受けることになるので、運用のポリシーに反するようなリソースの作成はある程度防ぐことができます。

しかしレビューはあくまで人の目によるものなので、チェックが漏れてしまうこともあります。 また「RDSは必ず暗号化すること」などのルールはCIで機械的にチェックして欲しいところです。

そこでカンムではtflintを導入してチェックの自動化を行うようにしました。

TFLintの導入

github.com

TFLintはterraform用のlinterで、非推奨な書式に警告を出してくれたり、ベストプラクティスを強制することができたりします。 メジャーなプロバイダー(AWS/Azure/GCP)のルールセットはすでに存在しており、カンムではtflint-ruleset-awsを利用しています。

tflintを導入するにはまず対象のtfファイルが置かれているフォルダに .tflint.hcl を作成し tflint --init を実行します。

plugin "aws" {
  enabled = true
  version = "0.12.0"
  source  = "github.com/terraform-linters/tflint-ruleset-aws"
}
$ tflint --init
Installing `aws` plugin...
Installed `aws` (source: github.com/terraform-linters/tflint-ruleset-aws, version: 0.12.0)

tflintを実行するとルールに違反した箇所を表示してくれます。

$ tflint
3 issue(s) found:

Warning: resource `aws_acm_certificate` needs to contain `create_before_destroy = true` in `lifecycle` block (aws_acm_certificate_lifecycle)

  on route53.tf line 25:
  25: resource "aws_acm_certificate" "stg_example_com" {

Reference: https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.12.0/docs/rules/aws_acm_certificate_lifecycle.md

Notice: "default.redis6.x" is default parameter group. You cannot edit it. (aws_elasticache_replication_group_default_parameter_group)

  on redis.tf line 123:
 123:   parameter_group_name          = "default.redis6.x"

Reference: https://github.com/terraform-linters/tflint-ruleset-aws/blob/v0.12.0/docs/rules/aws_elasticache_replication_group_default_parameter_group.md

Error: "t1.2xlarge" is an invalid value as instance_type (aws_instance_invalid_type)

  on ec2.tf line 40:
  40:   instance_type           = "t1.2xlarge"

AWSのルールセットの場合、デフォルトで有効になっているルールはここにあるとおりです。

.tflint.hcl で個々のルールの有効・無効を指定することもできます。

rule  "aws_elasticache_replication_group_default_parameter_group" {
  enabled = false
}

また、tflint-ignore というコメントをつけることで特定の箇所のチェックを無視することもできます。

resource "aws_instance" "my-instance" {
  # tflint-ignore: aws_instance_invalid_type
  instance_type           = "t1.2xlarge"

GitHub Actionsへの組み込み

terraform-lintersからsetup-tflintアクションが提供されているので、簡単にGitHub Actionsに組み込むことができます。

      - uses: actions/checkout@v2
      - run: tflint --init
        env:
          GITHUB_TOKEN: ${{ secret.GITHUB_TOKEN }}
      - run: tflint

reviewdogというツールを使ったアノテーションを入れてくれるアクション reviewdog/action-tflintもありますが、そちらを試したことはないです。

カスタムルールの作成

カンムではRDSの暗号化など必須にしたいポリシーがいくつかあるため、カスタムルールを作成しています。

カスタムルールを作成する場合、まず tflint-ruleset-template をテンプレートとしてGitHubリポジトリを作成します。

たとえば aws_rds_cluster リソースの storage_encrypted をチェックするルールを作成する場合、チェックを書いたGoのソースコードを作成します。

  • rules/aws_rds_cluster_must_be_encrypted.go
package rules

import (
    "fmt"

    hcl "github.com/hashicorp/hcl/v2"
    "github.com/terraform-linters/tflint-plugin-sdk/terraform/configs"
    "github.com/terraform-linters/tflint-plugin-sdk/tflint"
)

type AwsRdsClusterMustBeEncryptedRule struct{}

func NewAwsRdsClusterMustBeEncryptedRule() *AwsRdsClusterMustBeEncryptedRule {
    return &AwsRdsClusterMustBeEncryptedRule{}
}

func (r *AwsRdsClusterMustBeEncryptedRule) Name() string {
    // ルール名
    return "aws_rds_cluster_must_be_encrypted"
}

func (r *AwsRdsClusterMustBeEncryptedRule) Enabled() bool {
    return true
}

func (r *AwsRdsClusterMustBeEncryptedRule) Severity() string {
    return tflint.ERROR
}

func (r *AwsRdsClusterMustBeEncryptedRule) Link() string {
    // ルールのドキュメントのリンク先
    return "https://rule-document.example.com/posts/12345"
}

func (r *AwsRdsClusterMustBeEncryptedRule) Check(runner tflint.Runner) error {
    err := runner.WalkResources("aws_rds_cluster", func(resource *configs.Resource) error {
        content, _, diags := resource.Config.PartialContent(&hcl.BodySchema{
            Attributes: []hcl.AttributeSchema{
                {Name: "storage_encrypted"},
            },
        })

        if diags.HasErrors() {
            return diags
        }

        // storage_encryptedが存在しない場合は違反
        if _, exists := content.Attributes["storage_encrypted"]; !exists {
            return runner.EmitIssue(r, "`storage_encrypted` attribute not found", resource.DeclRange)
        }

        return nil
    })

    if err != nil {
        return err
    }

    return runner.WalkResourceAttributes("aws_rds_cluster", "storage_encrypted", func(attribute *hcl.Attribute) error {
        var storageRncrypted string
        err := runner.EvaluateExpr(attribute.Expr, &storageRncrypted, nil)

        if err != nil {
            return err
        }

        if storageRncrypted == "true" {
            return nil
        }

        // storage_encryptedがtrueでない場合は違反
        return runner.EmitIssueOnExpr(
            r,
            fmt.Sprintf("`storage_encrypted` is %s", storageRncrypted),
            attribute.Expr,
        )
    })
}

そして main.go にルールを追加します。

  • main.go
func main() {
    plugin.Serve(&plugin.ServeOpts{
        RuleSet: &tflint.BuiltinRuleSet{
            Name:    "kanmu",
            Version: "0.2.0",
            Rules: []tflint.Rule{
                rules.NewAwsRdsClusterMustBeEncryptedRule(),
            },
        },
    })
}

これで aws_rds_cluster リソースが storage_encrypted = true でない場合はエラーになります。

Error: `storage_encrypted` is false (aws_rds_cluster_must_be_encrypted)

  on rds.tf line 70:
  70:   storage_encrypted                   = false

Reference: https://rule-document.example.com/posts/12345

カスタムルールセットの配布

カスタムルールをプラグインとして tflint --init でインストールする場合、terraform providerと同様にgpgでの署名が必要になります。

基本的にはterraformのドキュメントに従ってgpgの鍵を作成し、terraform-provider-scaffoldingのGoReleaserの設定を編集して使えば、ルールセットのCIで自動的にインストール可能なアーカイブファイルが作成できます。

また.tflint.hcl にはカスタムルールセットの設定を追加しておきます。

plugin "kanmu" {
  enabled = true
  version = "0.1.0"
  source  = "github.com/xxx/tflint-ruleset-kanmu"

  signing_key = <<-KEY
  -----BEGIN PGP PUBLIC KEY BLOCK-----
  ...
  -----END PGP PUBLIC KEY BLOCK-----
  KEY
}

まとめ

tflintを使うことで、単純なミスをチェックしたりベストプラクティスを強制することができるようになります。 また、独自のルールセットを作成することで、RDSの暗号化のような「作成後に変更できない」設定をCIでチェックできるようになりました。

カンムではインフラの自動化に興味のあるインフラエンジニアを絶賛募集中です。

open.talentio.com