Slide 1

Slide 1 text

Uni fi ed Di ff 形式の差分から Go AST を構築して feature fl ag を 自 動計装する 株式会社サイバーエージェント 岩 見 彰太 GitHub:@BIwashi X: @B_Sardine Go Conference 2024

Slide 2

Slide 2 text

自己 紹介 岩 見 彰太 / Iwamin 株式会社サイバーエージェント ೥౓৽ଔೖࣾ "*ࣄۀຊ෦ڠۀϦςʔϧϝσΟΞ%JW アプリ運 用 カンパニー @BIwashi @B_Sardine ࣗಈੜ੒Λ׆༻ͨ͠ɺӡ༻อकίετΛ཈͑Δ&SSPS "MFSU3VOCPPLͷҰݩू໿؅ཧ 'FBUVSF'MBH%FFQ%JWF ΦϒβʔόϏϦςΟݚम࣮ફฤ

Slide 3

Slide 3 text

自己 紹介 岩 見 彰太 / Iwamin 株式会社サイバーエージェント ೥౓৽ଔೖࣾ "*ࣄۀຊ෦ڠۀϦςʔϧϝσΟΞ%JW アプリ運 用 カンパニー @BIwashi @B_Sardine ࣗಈੜ੒Λ׆༻ͨ͠ɺӡ༻อकίετΛ཈͑Δ&SSPS "MFSU3VOCPPLͷҰݩू໿؅ཧ 'FBUVSF'MBH%FFQ%JWF ΦϒβʔόϏϦςΟݚम࣮ફฤ

Slide 4

Slide 4 text

1 .トランクベース開発と feature fl ag 2 .feature fl ag と QA 品質 3 .feature fl ag を 自 動計装したい 4 .uber-go/gopatch を活 用 する 5 .feature fl ag を 自 動計装する 6 .まとめ

Slide 5

Slide 5 text

トランクベース開発と feature fl ag

Slide 6

Slide 6 text

トランクベース開発とは • 直接 main に対してどんどん merge していくスタイル • branch は1~2 日 以内に merge する短命なものでなくてはいけない • 特徴 •マージに伴うコンフリクトを最 小 限にできる •変更が加えられた main branch は常に prod に リリース可能な状態になっている •開発単位を 小 さく保ち、頻繁に main に pushし、 それに伴い CI/CD を実 行 して 自 動テストや脆弱性 診断の FBK を素早く得ることで開発スピードと デプロイまでの時間短縮を実現している トランクベース開発の trunk は Subversion というバージョン管理システム における main のことを trunk と呼んでいたらしい トランクベース開発について - 赤 帽エンジニアブログ

Slide 7

Slide 7 text

トランクベース開発とは • 直接 main に対してどんどん merge していくスタイル • branch は1~2 日 以内に merge する短命なものでなくてはいけない • 特徴 •マージに伴うコンフリクトを最 小 限にできる •変更が加えられた main branch は常に prod に リリース可能な状態になっている •開発単位を 小 さく保ち、頻繁に main に pushし、 それに伴い CI/CD を実 行 して 自 動テストや脆弱性 診断の FBK を素早く得ることで開発スピードと デプロイまでの時間短縮を実現している トランクベース開発の trunk は Subversion というバージョン管理システム における main のことを trunk と呼んでいたらしい トランクベース開発について - 赤 帽エンジニアブログ

Slide 8

Slide 8 text

feature fl ag とは • コードを変更することなくシステムの振る舞いを変更可能にする仕組み • 利点と使い所や課題 •トランクベース開発においては修正を fl ag 付きで実装しておくことで、その機能の反映可否 をコードを変更することなく 行 えるようにできる •「 fl ag 付きで実装する = fl ag を含む if 分岐で実装を囲んでおくことによっていつでもその実装を有効化無効化できるよ うに実装をしておく」 •役割を終えたら feature fl ag を削除する、といいうのを徹底してやらないとコードが複雑化 してしまう •feature fl ag の実装は属 人 化しがちなので、早めに実装した 人 が削除しないと簡単に負債化してしまう The Go gopher was designed by Renée French. GO Feature Flag

Slide 9

Slide 9 text

HOGE_FLAG := true if HOGE_FLAG == true { // flag ON ͷ࣌ʹ࣮ߦ͍ͨ͠ॲཧ // ex.) ൓ө͍ͨ͠मਖ਼ // ex.) AB ςετͷ A Λ༗ޮԽ͢Δ // ... } else { // flag OFF or flag Λઃఆ͍ͯ͠ͳ͍࣌ʹ // ࣮ߦ͍ͨ͠ॲཧ // ex.) मਖ਼લͷطଘͷ࣮૷ // ex.) AB ςετͷ B Λ༗ޮԽ͢Δ // ... } 実装イメージ • HOGE_FLAG とういう fl ag が ON だっ た場合と OFF だった場合の処理を if 文 で書く • AB テストや特定の実装を有効化する場 合などに使える The Go gopher was designed by Renée French. GO Feature Flag

Slide 10

Slide 10 text

feature fl ag とQA品質

Slide 11

Slide 11 text

feature fl ag と QA 品質 • QAによる品質担保を 高 めるためには、できるだけ細かい単位で feature fl ag を 入 れて QA をこまめに実 行 する必要がある 実装の複雑化、実装コストとQA品質はトレードオフ • 全てのテストが CI によって 自 動化できればいいが、現実はそう 甘 くない QAフローがある程度必要になってくる

Slide 12

Slide 12 text

feature fl ag と QA 品質

Slide 13

Slide 13 text

feature fl ag なし feature fl ag と QA 品質

Slide 14

Slide 14 text

feature fl ag 少 feature fl ag と QA 品質

Slide 15

Slide 15 text

feature fl ag 多 feature fl ag と QA 品質

Slide 16

Slide 16 text

feature fl ag 多 feature fl ag と QA 品質 これをなんとかしたい

Slide 17

Slide 17 text

feature fl ag を 自 動計装したい

Slide 18

Slide 18 text

理想 • 数が多くなると差し込む処理を書くのがめんどくさい 自 動で計装してくれる仕組みが欲しい • 実は feature fl ag の計装の仕 方 にブレがある時がある linter か 自 動計装で揃えたい • feature fl ag を 入 れる単位はどれくらい? ひとまず1PR単位 複数PRに渡って同じ fl ag を使 用 する場合は、 入 れる際に同名の fl ag を指定する • 1 PR における差分に対して feature fl ag を計装すればいい? git di f を使 用 してfeature fl ag を差し込みたい // ൱ఆܥͰೖΕΔύλʔϯ if !FLAG { ... } else { ... } // ෳ਺৚݅Λࠞͥͯ͠·͏ύλʔϯ if FLAG && hoge == foo && bar != 10 { ... } else { ... }

Slide 19

Slide 19 text

// diff file now source ------------- ------------- A A + B B - C D D 理想 // instrumented feature flag ------------- A if true { B } else { C } D

Slide 20

Slide 20 text

// diff file now source ------------- ------------- A A + B B - C D D 理想 // instrumented feature flag ------------- A if true { B } else { C } D + を if true 側配置し、- を if false 側に配置する

Slide 21

Slide 21 text

diff --git a/difftest/main.go b/difftest/main.go index 2678f63..1c0c6e8 100644 --- a/difftest/main.go +++ b/difftest/main.go @@ -3,7 +3,9 @@ package main import "fmt" func main() { - fmt.Println("Hello, World!") + fmt.Println("Hello, ੈք!") + + fmt.Println("New Message") fmt.Printf("testFn: %s\n", testFn("Hi!")) } 理想 • git di ff で出 力 される Uni fi ed Di ff 形式の差分を使っていい感じに feature fl ag を 入 れられないだろう か… uber-go/gopatchという リファクタリングツールを発 見 使えそうな気配

Slide 22

Slide 22 text

uber-go/gopatch を活 用 する

Slide 23

Slide 23 text

uber-go/gopatch • 大 規模なリファクタリングとリスタリングする ためのツール • di ff のような形式のパッチファイルにリファク タリングしたい内容を記載すると、対象ファイ ルから形式を探し出して修正してくれる VCFSHPHPQBUDI3FGBDUPSJOHBOEDPEFUSBOTGPSNBUJPOUPPMGPS(P @@ @@ -import "errors" -errors.New(fmt.Sprintf(...)) +fmt.Errorf(...) .-------. .-------. /_| |. /_| |. | ||. +---------+ | ||. | .go |||>-->| gopatch |>-->| .go ||| | ||| +---------+ | ||| '--------'|| ^ '--------'|| '--------'| | '--------'| '--------' | '--------' .-------. | /_| | | | +----' | .patch | | | '--------'

Slide 24

Slide 24 text

VCFSHPHPQBUDI3FGBDUPSJOHBOEDPEFUSBOTGPSNBUJPOUPPMGPS(P .-------. .-------. /_| |. /_| |. | ||. +---------+ | ||. | .go |||>-->| gopatch |>-->| .go ||| | ||| +---------+ | ||| '--------'|| ^ '--------'|| '--------'| | '--------'| '--------' | '--------' .-------. | /_| | | | +----' | .patch | | | '--------' uber-go/gopatch • 大 規模なリファクタリングとリスタリングする ためのツール • di ff のような形式のパッチファイルにリファク タリングしたい内容を記載すると、対象ファイ ルから形式を探し出して修正してくれる @@ @@ -import "errors" -errors.New(fmt.Sprintf(...)) +fmt.Errorf(...) git di ff の形式に似てる これどうやって利 用 してる…?

Slide 25

Slide 25 text

gopatch 内部構造を理解する

Slide 26

Slide 26 text

用 語解説

Slide 27

Slide 27 text

@@ var x identifier @@ -time.Now().Sub(x) +time.Since(x) セクション • @@ で囲まれたエリアの単位

Slide 28

Slide 28 text

@@ var x identifier @@ -time.Now().Sub(x) +time.Since(x) メタ変数(今回は使わない) • 任意の場所にマッチさせたい場合に 代替として使 用 でいる

Slide 29

Slide 29 text

@@ var x identifier @@ -time.Now().Sub(x) +time.Since(x) パッチ • Go 風 の構 文 で記載された di ff

Slide 30

Slide 30 text

gopatchの処理の流れ

Slide 31

Slide 31 text

patch の分割 • patch を分割して2つのバージョンを作成する • 各バージョンを個別に解析でき、両 方 が有効 な構 文 であることが保証される • 同じ解析ロジックを利 用 できる // patch -x, err := foo(...) +x, err := bar(...) if err != nil { ... - return err + return nil, err } Before(-) ------------------ x, err := foo(...) if err != nil { ... return err } After(+) ------------------ x, err := bar(...) if err != nil { ... return nil, err }

Slide 32

Slide 32 text

Patch Go (pgo) の構築と拡張 • Before、After は有効な Go の構 文 であるとは限らない • go/parserを使 用 するために、pgo を go/scanner を使 用 して go/parser で 使 用 可能な形に変換、拡張する x, err := foo(...) if err != nil { ... return err } package _ func _() { x, err := foo(dts) if err != nil { dts return err } }

Slide 33

Slide 33 text

Matcher/Replacer の Compile • Matcher Go のコードに 一 致するかどうかを検知 - 側のデータ • Replacer 一 致したコードの代わりにASTを配置 + 側のデータ // patch -err := f() -if err != nil { +if err := f(); err != nil { ... } // Matcher err := f() if err != nil { ... } // Replacer if err := f(); err != nil { ... }

Slide 34

Slide 34 text

Data share • Matcher と Replacer は Data オブジェクトを使 用 して情報を共有する context.Context に近いイメージ

Slide 35

Slide 35 text

ここまでの流れまとめ

Slide 36

Slide 36 text

feature fl ag を 自 動計装する

Slide 37

Slide 37 text

diff file A + B - C D git di ff による出 力 の意味を再確認 • + 今回新しく追加されたもの 現在の実装 if true に 入 れたい • - 元々実装されていたもの 今回の実装で削除された if false に 入 れたい

Slide 38

Slide 38 text

No content

Slide 39

Slide 39 text

No content

Slide 40

Slide 40 text

git di f • git di ff には 大 量の Option がある • できるだけ細切れのものではなく、 一 つの有効なファイル状態の 方 が gopatch の patch fi le として扱いやすい 将来的には細切れでも補完できるようにしたい 1 fi le の差分を丸々出 力 できるような Option を設定する

Slide 41

Slide 41 text

git di f • -U / --uni fi ed= 変更された 行 の前後に出 力 する、変更されていない 行 数を変更する デフォルトは3 0にすると変更された 行 しか出 力 されない とりあえず 大 きい値を設定しておけば差分ファイル全体が表 示 される 今回は repository 内のファイルの最 大行 数を設定した • -W / --function-context 変更があった 行 に関連する context 情報を表 示 する 変更があった 行 の所属している function 名が表 示 される、という想定だったが結構意図していないものが出 力 されることがあ るので今回はあまり使えなかった • --no-pre fi x ファイル名のような情報を表 示 しない 本来はここでのファイルと patch fi le を紐づけて gopach で検索する対象を絞った 方 がいいが、 一 旦ミニマムでは repository 全体に対して検索しにいくようにな形にした

Slide 42

Slide 42 text

No content

Slide 43

Slide 43 text

git di ff -> patch fi le に整形 • @@ とかの正規表現マッチングで header を整形する • - と + が連続する場合は順番を 入 れ替える git di ff の形式で「 行 を書き換える」という処理をした場合、- が先で + が後に出 力 される 後の処理(if 文 の挿 入 )のしやすさのために 入 れ替える 構 文 や処理には影響を与えないので 入 れ替えても問題はない(はず) // before @ test1 @ @@ func main() { testFn("test1") - testFn1("test") + fmt.Printf("%s", "test1") + testFn10("test") testFn2("test") } // after @ test1 @ @@ func main() { testFn("test1") + fmt.Printf("%s", "test1") + testFn10("test") - testFn1("test") testFn2("test") }

Slide 44

Slide 44 text

No content

Slide 45

Slide 45 text

+/- version を 生 成する • patch fi le を上から順番に 見 ていって、- version / + version の構造体を作成 • 振り分けていく際に、+/- がついていた 行 はその位置を記録しておく • - だった場合は、最後に - ではなかった位置を記録しておく - が連続していた場合でも、最後に - ではなかった位置を記録 後にここに if else の node を差し込む // patchVersion is one of the versions of // a patch specified in a unified diff. type patchVersion struct { // Contents of the file. Contents []byte // Positional information for each line in Contents. // // Each LinePos contains matches an offset // in Contents to a token.Pos in // the original patch file. Lines []section.LinePos } // LinePos contains positional information about a line in a buffer. type LinePos struct { // Offset of the first character of this line. Offset int // Original position from which this line was extracted. Pos token.Pos // * New Added // Type of the line: '+' for true, ‘-' for false, ' ' // for unchanged Type LineType // * New Added // position of the plus side when minus elements are embedded MinusToPlusPos token.Pos }

Slide 46

Slide 46 text

+/- version を 生 成する • - だった場合は、最後に - ではなかった 位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI

Slide 47

Slide 47 text

+/- version を 生 成する • - だった場合は、最後に - ではなかった 位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() { 1 func main() {

Slide 48

Slide 48 text

+/- version を 生 成する • - だった場合は、最後に - ではなかった 位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() { 2 testFn("test1") 1 func main() { 2 testFn("test1")

Slide 49

Slide 49 text

+/- version を 生 成する • - だった場合は、最後に - ではなかった 位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() { 2 testFn("test1") 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1")

Slide 50

Slide 50 text

+/- version を 生 成する • - だった場合は、最後に - ではなかった 位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() { 2 testFn("test1") 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test")

Slide 51

Slide 51 text

+/- version を 生 成する • - だった場合は、最後に - ではなかった 位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() { 2 testFn("test1") 3 - testFn1("test") 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 4 + version の位置を記録

Slide 52

Slide 52 text

+/- version を 生 成する • - だった場合は、最後に - ではなかった 位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() { 2 testFn("test1") 3 - testFn1("test") 4 testFn2("test") 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 testFn2("test") 4 + version の位置を記録

Slide 53

Slide 53 text

+/- version を 生 成する • - だった場合は、最後に - ではなかった 位置を記録しておく 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 - testFn1("test") 6 testFn2("test") 7 } QBUDI 1 func main() { 2 testFn("test1") 3 - testFn1("test") 4 testFn2("test") 5 } 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 testFn2("test") 6 } 4 + version の位置を記録

Slide 54

Slide 54 text

No content

Slide 55

Slide 55 text

+ version を Matcher に登録 • 本来は Matcher には - version を Matcher に 入 れる • 今回は現在の実装に対して 一 致を探したいので + version を設定する

Slide 56

Slide 56 text

+ version を Matcher に登録 • 本来は Matcher には - version を Matcher に 入 れる • 今回は現在の実装に対して 一 致を探したいので + version を設定する + version を設定

Slide 57

Slide 57 text

No content

Slide 58

Slide 58 text

+/- version を組み合わせて AST を 入 れ替える • + の場合は if true に 入 れる 記録しておいた+位置の node を if true に 入 れる 1 func main() { 2 testFn("test1") 3 - testFn1("test") 4 testFn2("test") 5 } 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 testFn2("test") 6 } 4 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 testFn2("test") 6 } OFXWFSTJPO

Slide 59

Slide 59 text

+/- version を組み合わせて AST を 入 れ替える • + の場合は if true に 入 れる 記録しておいた+位置の node を if true に 入 れる 1 func main() { 2 testFn("test1") 3 - testFn1("test") 4 testFn2("test") 5 } 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 testFn2("test") 6 } 4 1 func main() { 2 testFn(“test1") if true { 3 + fmt.Printf("%s", "test1") 4 + testFn10(“test") } 5 testFn2("test") 6 } OFXWFSTJPO

Slide 60

Slide 60 text

+/- version を組み合わせて AST を 入 れ替える • + の場合は if true に 入 れる 記録しておいた+位置の node をIfStmt にreplace • - の場合は if false に 入 れる 4 の node を IfStmt Else に replace 1 func main() { 2 testFn("test1") 3 - testFn1("test") 4 testFn2("test") 5 } 1 func main() { 2 testFn("test1") 3 + fmt.Printf("%s", "test1") 4 + testFn10("test") 5 testFn2("test") 6 } 4 1 func main() { 2 testFn(“test1") if true { 3 + fmt.Printf("%s", "test1") 4 + testFn10(“test") } else { - testFn1("test") } 5 testFn2("test") 6 } OFXWFSTJPO

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

new + version を Matcher に登録 • AST を書き変えた new + version を replacer に設定する

Slide 63

Slide 63 text

new + version を Matcher に登録 • AST を書き変えた new + version を replacer に設定する new + version を設定

Slide 64

Slide 64 text

計装できた "GUFS func main() { testFn("test") testFn("test1") fmt.Printf("%s", "test1") testFn10("test") testFn2("test") testFn4("test") } CFGPSF func main() { + testFn("test") testFn("test1") + fmt.Printf("%s", "test1") + testFn10("test") - testFn1("test") testFn2("test") - testFn3("test") testFn4("test") } HJUEJ f

Slide 65

Slide 65 text

計装できた func main() { if true { testFn("test") } testFn("test1") if true { fmt.Printf("%s", "test1") testFn10("test") } else { testFn1("test") } testFn2("test") if true { } else { testFn3("test") } testFn4("test") } "GUFS func main() { testFn("test") testFn("test1") fmt.Printf("%s", "test1") testFn10("test") testFn2("test") testFn4("test") } CFGPSF func main() { + testFn("test") testFn("test1") + fmt.Printf("%s", "test1") + testFn10("test") - testFn1("test") testFn2("test") - testFn3("test") testFn4("test") } HJUEJ f

Slide 66

Slide 66 text

計装できた func main() { if true { testFn("test") } testFn("test1") if true { fmt.Printf("%s", "test1") testFn10("test") } else { testFn1("test") } testFn2("test") if true { } else { testFn3("test") } testFn4("test") } "GUFS func main() { testFn("test") testFn("test1") fmt.Printf("%s", "test1") testFn10("test") testFn2("test") testFn4("test") } CFGPSF func main() { + testFn("test") testFn("test1") + fmt.Printf("%s", "test1") + testFn10("test") - testFn1("test") testFn2("test") - testFn3("test") testFn4("test") } HJUEJ f 否定系を使わないようにしている

Slide 67

Slide 67 text

まだできていない ・ 悩みどころ • 関数の引数 自 体を変更するパターン これは 自 動計装関係なしに feature fl ag を 入 れるの難しい • 関数 自 体の作成、削除するパターン 削除されていた場合は、- で呼ばれている場所があるかもしれないので復活させる + の場合はそのままにする • 構造体のフィールドを変更する フィールドを削除したり変更したりするパターン 元々の構造体を変更していない場合、丸々構造体を丸ごと if 文 に 入 れて残しておく ASTは構築できているので 対象 Node の構造体の型によって愚直に対応していくい

Slide 68

Slide 68 text

feature fl ag system

Slide 69

Slide 69 text

feature fl ag system Go Conference 2024

Slide 70

Slide 70 text

feature fl ag system CloudNative Days Summer 2 0 24

Slide 71

Slide 71 text

まとめ • gopatch のツールの内部実装を紹介した • di ff fi le を元に AST を構築して feature fl ag 用 に AST を差し替えることが できた • まだ実戦導 入 するには不 足 している部分が多いが、ここからさらに 色 々なパ ターンに対応していきたい The Go gopher was designed by Renée French. GO Feature Flag