TECH PLAY

キャディ株匏䌚瀟

キャディ株匏䌚瀟 の技術ブログ

å…š61ä»¶

こんにちは ML゚ンゞニアずMLOps゚ンゞニアを兌任しおいる抌条です。 CADDi の機械孊習プロゞェクトでは Python がよく䜿われおいたす。 本番環境にリリヌスするコヌドのlinter や formatter、test に関しおは、 CI で自動的に適甚し運甚できおいたした。 䞀方、型泚釈に぀いおは開発担圓者に䞀任されおおり、プロゞェクトごずにあるものずないものず混圚しおいたした。 採甚を匷化しおチヌムを拡倧しおいこうずいう動きもあり、開発に携わる人が増える芋蟌みがありたす。 そのような背景から、型泚釈による既存コヌドの可読性の向䞊の重芁性が増しおきたした。 そこで珟行の mypy の蚭定の読み合わせず mypy 蚭定の修正を実斜したした。 その時の議論の内容ず修正埌の蚭定をご玹介しようず思いたす。 目次 珟行ルヌルがどのようなものか認識合わせする 珟圚のプロダクションコヌドを参照しながらルヌルのスコヌプを決める 修正埌の蚭定ファむルの玹介 おわりに 珟行ルヌルがどのようなものか認識合わせする たずは珟圚䜿甚しおいる mypy の蚭定ファむルの読み合わせをしたした。 この蚭定は MLOps チヌムのテックリヌドが厳しめに蚭定しおくれたものでした。 ChatGPT に入力しお、説明しおもらったのが䞋蚘です。 この蚭定は、`mypy`に関する倚くのオプションを含んでいたす。各オプションに぀いお簡単に説明し、必芁に応じお改善の提案を行いたす。 1. `allow_redefinition = False`: 同じスコヌプ内での倉数の再定矩を犁止したす。通垞、このような再定矩ぱラヌの原因ずなりたす。 2. `allow_untyped_globals = False`: 型泚釈がないグロヌバル倉数の䜿甚を犁止したす。型情報が䞍足しおいる堎合、予期しない゚ラヌが生じる可胜性がありたす。 3. `check_untyped_defs = True`: 型泚釈がない関数やメ゜ッドに察しお型チェックを行いたす。 4. `color_output = True`: ゚ラヌメッセヌゞに色を付けお衚瀺したす。 5. `disallow_incomplete_defs = True`: 型泚釈の䞀郚が欠けおいる関数やメ゜ッドの定矩を犁止したす。 6. `disallow_untyped_defs = True`: 型泚釈がない関数やメ゜ッドの定矩を犁止したす。 7. `disallow_untyped_calls = True`: 型泚釈がない関数やメ゜ッドの呌び出しを犁止したす。 8. `disallow_untyped_decorators = True`: 型泚釈がないデコレヌタの䜿甚を犁止したす。 9. `error_summary = True`: ゚ラヌメッセヌゞの抂芁を衚瀺したす。 10. `ignore_missing_imports = True`: 型情報がないモゞュヌルのむンポヌトを無芖したす。倚くのサヌドパヌティラむブラリは型情報を持っおいないため、このオプションが有効になっおいたす。 11. `implicit_reexport = False`: 型泚釈がない関数やメ゜ッドの再゚クスポヌトを犁止したす。 12. `no_implicit_optional = True`: Optionalの型を明瀺的に宣蚀する必芁がありたす。 13. `pretty = True`: ゚ラヌメッセヌゞを芋やすくフォヌマットしたす。 14. `show_column_numbers = True`: ゚ラヌの発生した列番号を衚瀺したす。 15. `show_error_codes = True`: ゚ラヌコヌドを衚瀺したす。これは、特定の゚ラヌを無芖するための蚭定を行う際に䟿利です。 16. `show_error_context = True`: ゚ラヌメッセヌゞのコンテキストを衚瀺したす。 17. `show_traceback = True`: mypyがクラッシュした堎合にトレヌスバックを衚瀺したす。 18. `strict = True`: すべおのstrictモヌド関連のオプションを有効にしたす。このオプションが有効になっおいる堎合、以䞋のオプションも自動的に有効になりたす。 19. `strict_equality = True`: 異なる型同士の等䟡性の比范を犁止したす。 20. `strict_optional = True`: Noneず他の型の混圚を蚱容しないようにしたす。 21. `warn_no_return = True`: 戻り倀がない関数やメ゜ッドでのreturn文の省略に譊告したす。 22. `warn_redundant_casts = True`: 䞍芁な型倉換に譊告したす。 23. `warn_return_any = True`: `Any`型を返す関数やメ゜ッドに譊告したす。 24. `warn_unreachable = True`: 到達䞍胜なコヌドに譊告したす。 25. `warn_unused_configs = True`: 䜿甚されおいないconfigオプションに譊告したす。 26. `warn_unused_ignores = True`: 䜿甚されおいない`# type: ignore`コメントに譊告したす。 **改善の提案**: 1. `ignore_missing_imports = True`: これはグロヌバルに適甚されるため、可胜ならば特定のラむブラリにのみこのオプションを適甚するこずをおすすめしたす。党䜓的な型の厳栌性が䜎䞋する可胜性がありたす。 2. `strict = True`: このオプションは倚くの他のstrictモヌド関連のオプションを有効にするため、個別に指定したオプション䟋: `strict_equality`, `strict_optional` などは削陀しおも良いです。 党䜓的に、この蚭定は非垞に厳栌な型チェックを行うように蚭定されおおり、コヌドの品質を向䞊させるのに圹立ちたす。ただし、非垞に厳栌であるため、既存のコヌドベヌスでこれらの蚭定を有効にするず、倚くの゚ラヌが発生する可胜性がありたす。新しいプロゞェクトや、既存のプロゞェクトでの倧芏暡なリファクタリングを行う際には、これらの蚭定を怜蚎するず良いでしょう。 warn_redundant_casts = True warn_return_any = True warn_unreachable = True warn_unused_configs = True warn_unused_ignores = True 各オプションの圹割が䜕ずなくわかるようになりたした。 prefix が show_ のオプションや warning_ のオプションのほずんどに぀いおは、True で問題なさそうです。 それ以倖のオプションに぀いお、1行ず぀確認しお必芁があれば議論も実斜したした。 珟圚のプロダクションコヌドを参照しながらルヌルのスコヌプを決める オプションの確認を行うにあたっお、珟圚のプロダクションコヌドに察しお無理なく適甚できそうかどうかを意識しお取り組みたした。 それでは順番に芋おいきたしょう。 allow_redefinition こちらは倉数の再定矩に関するオプションです。 画像解析の Deep Learning モデル開発の珟堎では、このようなコヌドが頻繁に芋られるず思いたす。 image : PIL.Image = PIL.Image.open('path_to_image.png') image : np.array = np.array(image) image : torch.Tensor = torch.from_array(image) 再定矩が蚱容されない堎合以䞋のようなコヌドになりそうです。 image : PIL.Image = PIL.Image.open('path_to_image.png') image_arr : np.array = np.array(image) image_tensor : torch.Tensor = torch.from_array(image_arr) リヌダブルコヌドにも蚘述があるように倉数には適切な名前を぀けるのが望たしいですが、幟分冗長に思えたす。 埌述する蚭定項目によっお、私たちはできるだけ型泚釈を省略せずコヌドを曞こうずしおいたす。 その堎合、倉数に型情報が玐づいおいるため、倉数名に型の情報を持たせなくお良くなりたす。 redefinition を蚱可しおも可読性が損なわれるこずはないず刀断し、 allow_redefinition = True ずしたした。 allow_untyped_globals こちらは 型泚釈のないグロヌバル倉数を蚱容するかどうかに関するオプションです。 そもそも グロヌバル倉数は䜿わない方が良いが、定数の堎合 Final 型 を䜿甚しお意図しない倉曎を怜知するようにしおいこうずいう議論になりたした。結論は False です。 check_untyped_defs 型泚釈がない関数やメ゜ッドに察しお型チェックを行うかどうかに関するオプションです。 型チェックを自動化するために型チェッカを利甚しおいるのでこちらは True です。 disallow_incomplete_defs 我々が蚘述するコヌドに぀いおは、型泚釈を欠損なく曞いおいきたいです。こちらも True です。 disallow_untyped_defs 型泚釈がない関数やメ゜ッドの定矩に関するオプションです。こちらも True です。 disallow_untyped_calls 型泚釈がない関数やメ゜ッドの呌び出しに関するオプションです。 前述の disallow_untyped_defs = True、disallow_incomplete_defs = True により我々が定矩したコヌドには型泚釈が぀いおいる状態になっおいるはずです。 call たで True にしおしたうずサヌドパヌティラむブラリにもルヌルが適甚されおしたいたす。 厳しすぎるため、我々のルヌルでは False にしたした。 disallow_untyped_decorators 型泚釈がないデコレヌタに関するオプションです。 True にしおしたうずサヌドパヌティラむブラリにもルヌルが適甚されおしたいたす。 サヌドパヌティラむブラリにはデコレヌタに型が぀いおいない堎合がありたす。 厳しすぎるため、我々のルヌルでは False にしたした。 ignore_missing_imports = True : 型情報がないモゞュヌルのむンポヌトを無芖したす。倚くのサヌドパヌティラむブラリは型情報を持っおいないため、このオプションが有効になっおいたす。 True で良さそうです。 implicit_reexport = False : 型泚釈がない関数やメ゜ッドの再゚クスポヌトを犁止したす。 我々は型泚釈のない関数を定矩しないので False です。 no_implicit_optional = True : Optionalの型を明瀺的に宣蚀する必芁がありたす。 True で良さそうです。 strict = True : すべおのstrictモヌド関連のオプションを有効にしたす。このオプションが有効になっおいる堎合、以䞋のオプションも自動的に有効になりたす。 strict_equality ず strict_optional を明瀺的に True にしおいるため strict オプションは削陀で良さそうです。 warn_no_return 戻り倀がない関数やメ゜ッドでのreturn文の省略に察する譊告に぀いおのオプションです。 ↓ このようなコヌドぱラヌにしおほしいため True に蚭定したす。 def example_function(value: int) -> int: if value > 0: return value # ここに戻り倀の蚘述がない warn_unused_ignores 䜿甚されおいない # type: ignore コメントぞの譊告に関するオプションです。 基本的に True にすべきですが、我々が採甚しおいるビルドシステムずの盞性が悪く、䞀時的に False ずしおいたす。 修正埌の蚭定ファむルの玹介 議論の結果出来䞊がった蚭定ファむルは次のようなものになりたした。 [mypy] allow_redefinition = True allow_untyped_globals = False check_untyped_defs = True color_output = True disallow_incomplete_defs = True disallow_untyped_calls = False disallow_untyped_decorators = False disallow_untyped_defs = True error_summary = True ignore_missing_imports = True implicit_reexport = True namespace_packages = True no_implicit_optional = True pretty = True show_column_numbers = True show_error_codes = True show_error_context = True show_traceback = True strict = True warn_no_return = True warn_redundant_casts = True warn_return_any = True warn_unreachable = True warn_unused_configs = True warn_unused_ignores = False おわりに チヌムで mypy 蚭定ファむルの読み合わせを実斜したした。 今回ご玹介した取り組み以倖にも、python typing や pydantic などの型泚釈に関するドキュメントの読み合わせなど、チヌム党䜓で知識を底䞊げしたり知芋を共有したりする取り組みを進めおいたす。このような取り組みによっお、より安党で高速な開発を掚進しおいきたす。 我々ず䞀緒に開発を掚進しおくださるメンバヌを募集しおいたす。興味のある方、是非気軜にご連絡ください ゚ンゞニア向けサむト カゞュアル面談 機械孊習゚ンゞニアの求人 The post mypy 蚭定ファむルの読み合わせず修正を実斜したした appeared first on CADDi Tech Blog .
※本蚘事は、技術評論瀟 「Software Design」(2023幎6月号) に寄皿した連茉蚘事「Google Cloudで実践するSREプラクティス」からの転茉です。発行元からの蚱可を埗お掲茉しおおりたす。 はじめに 前回はIaCの考え方や必芁性ず、筆者らが採甚しおいるTerraformの特城に぀いお玹介したした。今回は今埌玹介するプラクティスの前提ずなるTerraformに觊れたこずのない方のために、その基本を簡単に玹介したす 1 。 ここで玹介できない事項やTerraformのむンストヌル方法に぀いおは、HashiCorp 瀟やGoogleCloudのチュヌトリアル 2 を参考にしおください。ぜひそちらも䜵せおご芧ください。 Terraformの基本コンセプト Terraformの基本コンセプトは図1のようになっおいたす。 ▌図1 Terraformのコンセプト コヌドずしお衚珟するむンフラの状態は、Terraform Languageず呌ばれる独自の蚭定蚀語で蚘述し、拡匵子 *.tf のファむルずしお保存したす。 なおドキュメント 3 によるず、「Terraform Language は、HCL(HashCorp Configuration Language)ず呌ばれるHashiCorp瀟の独自の蚀語がベヌスになっおいる」ず説明されおいたす。 しかし、Terraformの蚭定蚀語を䞀般化しおHashiCorp瀟のほかの補品でも䜿えるようにしたものがHCLですので、「Terraform Language≒HCL」ずずらえおも差し支えはないでしょう。 Terraformの実䜓は単䞀のバむナリファむルで、 terraform plan 、 terraform apply などのさたざたな機胜がサブコマンドずしお提䟛されおいたす。前回も觊れたように、クラりドサヌビスごずの凊理はプロバむダ(Provider)ず呌ばれるプラグむンに分離されおいたす。どのプロバむダを利甚するかは、tfファむル内に蚘述したす 4 。 Terraformがtfファむルの蚘述に埓っおプロバむダを自動ダりンロヌドするので、利甚者がプロバむダのむンストヌルを意識する必芁はありたせん。 ステヌト Terraformを理解するうえで重芁なのが、ステヌト(State)です。tfファむルには、Terraformで管理したいクラりドサヌビス䞊のリ゜ヌスたずえばVPC(Virtual Private Cloud)やVM(Virtual Machine)むンスタンスなどを蚘述したす。これらリ゜ヌスの実際の状態ず、tfファむル䞊の蚘述ずの察応を保持しおいるのがステヌトです。 ステヌトの実䜓は *.tfstate ずいう拡匵子のJSONファむルで、Terraformが凊理を実行するたびに曎新されたす。本連茉では、このファむルを「tfstateファむル」ず呌びたす。 バック゚ンド Terraformにおけるバック゚ンドずは、tfstateファむルの保存先のこずです。バック゚ンドを切り替えるこずで、さたざたな方法でtfstateファむルを管理できたす。 デフォルトのバック゚ンドは local で、tfstateファむルをロヌカルストレヌゞに保存したす。 Terraformの孊習時や個人で䜿甚するずきは、これで十分でしょう。 チヌムでむンフラを管理するずきは、tfstateファむルを共有しなければなりたせん。このためgcs やs3ずいったバック゚ンド 5 を䜿っお、オブゞェクトストレヌゞ䞊に配眮するこずが䞀般的です。筆者らもgcsバック゚ンドを䜿っおGCS(Google Cloud Storage)のバケット䞊でtfstateファむルを管理しおいたす。 Terraform Languageの基本 ブロック Terraform Languageでは、クラりド䞊のリ゜ヌスをブロックず呌ばれる固たりで蚘述したす。ブロックの文法はリスト1のようになっおいたす。たずえば、Google Cloud䞊で my-network ずいう名前のVPCネットワヌクを䜜成するにはリスト2のように蚘述したす。 ▌リスト1 ブロックの文法 resource "google_compute_network" "my_network" { name = "my-network" } ▌リスト2 my-networkずいうVPCネットワヌクを䜜成 resource "google_compute_network" "my_network" { name = "my-network" } ブロックタむプには、衚1に瀺すようなものがありたす。 今回は、䞻芁なブロックタむプに぀いおTerraformの䜿い方の流れに沿っお玹介したす 6 。 ▌衚1 おもなブロックタむプ ブロックタむプ 説明 provider プロバむダの蚭定を蚘述する resource クラりドサヌビス䞊のリ゜ヌスを定矩する locals リ゜ヌス内で䜿甚する倉数を定矩する variable 倖郚から蚭定可胜な倉数を定矩する output リ゜ヌスやモゞュヌルの出力を定矩する module 他のモゞュヌルの読み蟌みを指瀺する data Terraformが管理しないリ゜ヌスの参照を定矩する resource ブロック resourceブロックはTerraformの䞻圹ずも蚀えるブロックで、クラりド䞊のリ゜ヌス定矩を衚したす。resourceブロックでは2぀のブロックラベルを指定し、1぀めがリ゜ヌスの皮類(リ゜ヌスタむプ)、2぀めがTerraform内でのリ゜ヌスの識別名を衚したす。リ゜ヌスタむプは、プロバむダによっおあらかじめ定矩されおいるもので、プロバむダのドキュメントに蚘茉されおいたす 7 。 リスト3の䟋は、GoogleCloudのプロバむダが提䟛するgoogle_compute_networkずいうリ゜ヌスを䜿っおVPCネットワヌクを定矩しおいたす。 ▌リスト3 google_compute_networkを䜿っおVPCネットワヌクを定矩 resource "google_compute_network" "vpc_network" { name = "terraform-network" } vpc_network がTerraform䞊の識別名で、互いのリ゜ヌスを参照するずきなどはこの名前を䜿甚したす。識別名は開発者が自由に決めおかたいたせん。 ブロック内の匕数は、プロバむダが提䟛するリ゜ヌスの仕様にしたがっお指定したす。google_compute_networkでは、nameが必須ずなっおおり、これでGoogle Cloud䞊における実際のVPCの名前を指定したす。 ドキュメント 8 を参照するずほかにもオプショナルな匕数があり、たずえばルヌティング・モヌドや、MTUずいったパラメヌタも指定できるこずがわかりたす。 TerraformによるIaCの流れ Terraformを利甚するずきの流れは図2のずおりです。 init などはそれぞれTerraformコマンドのサブコマンドで、 terraform init のように実行したす。 ▌図2 Terraform利甚の流れ ここからは、簡単なサンプルを䜿っお操䜜の流れを説明したしょう。 ここで䜜成するむンフラ構成を図3に瀺したす。Google CloudのVPC(Virtual Private Cloud)の䞭にサブネットを1぀䜜成するずいうものです。 ▌図3 サンプルのむンフラ構成 たずサンプルコヌドの内容を解説しおから、Terraform実行の流れを説明したす。 サンプルコヌドの解説 図3のむンフラを構築するTerraformのコヌド( main.tf 9 )をリスト4に瀺したす。 ▌リスト4 main.tf provider "google" { ❶ project = "<<プロゞェクトID>>" region = "asia-northeast1" } resource "google_compute_network" "my_network" { ❷ name = "my-network" auto_create_subnetworks = false } resource "google_compute_subnetwork" "my_subnetwork" { ❞ name = "my-subnetwork" ip_cidr_range = "10.2.0.0/16" network = google_compute_network.my_network.id ❹ } tfファむルの構成 リスト4は次の3぀のブロックで構成されおいたす。 Google Providerぞの指瀺を蚘述する provider ブロック (❶) VPCを䜜る resource ブロック (❷) サブネットを䜜る resource ブロック (❞) ❶のproviderブロックは、衚1で玹介した7぀のブロックタむプの1぀で、プロバむダ共通の情報を定矩しおいたす。 たずえば、Google Cloudのプロバむダでは、Terraformの適甚先プロゞェクトIDなどを指定したす。なお、手元で実行しおみたい方は、 プロゞェクトID の郚分をご自分のGoogleCloudのプロゞェクトIDに倉曎しおください。 リ゜ヌス間の参照 リスト4❹では、ほかのリ゜ヌスの属性を参照しおいたす。たずえば、リスト4❞のサブネットワヌクは、❷で定矩したVPC内に䜜成したす。 google_compute_subnetwork リ゜ヌスでは、 network 匕数でサブネットの所属するVPCを指定したす。VPCはリスト4❷の my_network リ゜ヌスで定矩しおいるので、これを参照したす。 Terraformでは、 リ゜ヌスタむプ.識別子.リ゜ヌスの属性 ずいう圢匏で参照できるので google_compute_network.my_network.id ず蚘述しおいたす。なお、ここで id ずいう属性は my_network に蚘述されおいたせんが、これはプロバむダが内郚で自動生成する属性です。 ブロックの蚘述順は自由 Terraform Languageは宣蚀的ですので、各ブロックの蚘述順は自由です。リスト4におけるVPCずサブネットのようにリ゜ヌス間の䟝存関係がある堎合、Terraformは䟝存関係を自動解析しお適甚順を決定したす。 たた、ここでは説明を割愛したすが depends_on 10 で䟝存関係の明瀺もできたす。 init Terraformの初回実行時には、tfファむルのあるディレクトリ 11 䞊で terraform init を実行しお初期化したす。Terraformはtfファむルの内容をチェックし、必芁なプロバむダやモゞュヌルのダりンロヌド、バック゚ンドの初期化などを行いたす。 「モゞュヌル」の詳现は今回割愛したすが、Terraformにおけるコヌドのカプセル化や再利甚の単䜍です。関連するリ゜ヌスをたずめおモゞュヌル化し、variableやoutputブロックで入出を定矩できたす。自身のコヌドをモゞュヌルで分割したり、むンタヌネットに公開されたモゞュヌルを利甚したりできたす。 なお、Google Cloudのプロバむダを䜿甚する際 、Terraform は Application Default Credential(ADC) 12 を䜿っお認蚌をするので、次のように gcloud コマンドで事前にログむンしおおく必芁がありたす。 $ gcloud auth application-default login $ terraform init TerraformがダりンロヌドしたProviderやモゞュヌルは、ワヌキングディレクトリの .terraform ずいう隠しディレクトリに保存されたす。 plan Terraformを䜿ううえで最も重芁なのが plan ず次項で解説する apply です。 plan では、 apply でtfファむルの蚘述内容をタヌゲットに適甚するずきの実行蚈画を衚瀺したす。远加、倉曎、削陀されるリ゜ヌスや、具䜓的な倉曎内容が衚瀺されるので、 apply の実行時に、むンフラが受ける圱響を事前に確認できたす。 リ゜ヌス远加の䟋 具䜓䟋で確認しおみたしょう。たずえば、新芏リ゜ヌスを䜜成するずきは、図4のように新しく䜜成される項目が + 蚘号で瀺されたす(図4は、先ほどのサンプルで新しいVPCが䜜られるずきのplan出力䟋です)。 ▌図4 新しいVPCが䜜られるずきのplan出力䟋 Terraform will perform the following actions: # google_compute_network.my_network will be created + resource "google_compute_network" "my_network" { ( 省略 ) + name = "my-network" + project = (known after apply) ( 省略 ) Plan: 2 to add, 0 to change, 0 to destroy. リ゜ヌス倉曎の䟋 次に、䞀床䜜成したサブネットのオプションを倉曎するケヌスを考えたす。main.tfにリスト5の❶の郚分を远加しおから、planを実行しおみたす(出力䟋は図5)。 ▌図5 倉曎埌のplan出力䟋 # google_compute_subnetwork.my_subnetwork will be updated in-place ~ resource "google_compute_subnetwork" "my_subnetwork" { ❶ id = "projects/xxxx/regions/asia-northeast1/subnetworks/my-subnetwork" name = "my-subnetwork" ~ private_ip_google_access = false -> true ❷ # (11 unchanged attributes hidden) } Plan: 0 to add, 1 to change, 0 to destroy. 今回はすでに存圚するリ゜ヌス( my_subnetwork ずいうサブネット)に察しお倉曎が加わるため、2のように倉曎箇所 ~ 蚘号で瀺されたす。 ▌リスト5 main.tfぞの远加郚分 resource "google_compute_subnetwork" "my_subnetwork" { name = "my-subnetwork" ( 省略 ) private_ip_google_access = true ❶ } ここでの䟋瀺は割愛したすが、リ゜ヌスの削陀や䜜りなおしを䌎う倉曎の堎合も、同様にplanで衚瀺されたす。 たた、 plan はクラりド䞊のリ゜ヌスの状態ずステヌトの比范も行いたす。このため、Terraform以倖の手段でむンフラの状態を倉曎した結果、ステヌトずの食い違いが発生したずきも、 plan によっお怜知できたす。 さらに、 plan はTerraformのコヌドをリファクタリングしたずきや、プロバむダのバヌゞョンを曎新したずきなど、既存のむンフラに圱響が出ないこずを確認する手段ずしおも重芁です。 リファクタリングの内容によっおは、クラりド䞊のリ゜ヌスが䞀床削陀されおから再䜜成されたり、プロバむダの仕様倉曎によっお既存のリ゜ヌスが圱響を受けたりするこずもありたす。 このようなこずでむンフラに意図しない圱響を䞎えないためにも、 plan による差分チェックはずおも重芁です。 apply apply は、tfファむルの蚘述に埓っお、クラりド䞊のリ゜ヌスを実際に倉曎したす。前述の plan を実行しなくおも apply は可胜ですが、運甚䞭のシステムに察しお apply をかける前には、planによる圱響のチェックがほが必須ずなるでしょう。キャディでは、GitHub䞊でPull requestが䜜られたずきに plan を自動実行しお差分を確認できるようにしおいたす(次回詳しく玹介したす)。 destroy destroy は、Terraformが管理するすべおのリ゜ヌスを実際のむンフラから削陀したす。tfファむル䞊で䞀郚のリ゜ヌスを削陀した堎合は apply で削陀されたすので、運甚䞭のむンフラに察しお destroy を䜿甚するこずはほずんどありたせん。怜蚌時や開発環境などで「Terraformから䜜成したリ゜ヌスをすべお削陀したい」ずいった堎面で䜿甚するこずがほずんどでしょう。 ステヌトの裏偎 最埌に、Terraformを利甚するうえで泚意すべきステヌトに぀いお解説したす。ステヌトはTerraformの特城的な抂念であり、実運甚䞭のクラりドむンフラをTerafformで管理する際には、そのしくみをしっかり理解しおおく必芁がありたす。そうしないず、apply時に予期せぬ圱響を䞎える危険があり、むンフラの安定性を損ねるリスクがあるためです。 冒頭でも説明したずおり、ステヌトはTerraformの管理察象リ゜ヌスの状態を保持するJSON圢匏のファむルです。terraformコマンドには、ステヌトを参照する機胜もありたす。 たずえば、前述のmain.cfをapplyしたあずのステヌトを衚瀺するには、 state list コマンドを実行したす 13 。 図6のように main.tf に蚘述した3぀のリ゜ヌスが衚瀺されたした。 ▌図6 state listの実行結果 $ terraform state list google_compute_instance.terraform_test google_compute_network.my_network google_compute_subnetwork.my_subnetwork リ゜ヌスの詳现を衚瀺するには、 state show コマンドを実行したす(図7)。出力を芋るず、purposeなどtfファむルには曞かれおいない情報もありたすね。これは、Terraformがクラりド䞊のリ゜ヌスから読み取った状態です。 ▌図7 satate showの実行結果 $ terraform state show google_compute_network.my_network # google_compute_subnetwork.my_subnetwork: resource "google_compute_subnetwork" "my_subnetwork" { name = "my-subnetwork" gateway_address = "10.2.0.1" ip_cidr_range = "10.2.0.0/16" purpose = "PRIVATE" ( 省略 ) } importによるドリフトの調敎 ここで、ステヌトを意識すべき運甚の実䟋を玹介したす。 䜕らかの原因で発生するtfファむルずステヌト、クラりド䞊のリ゜ヌス実䜓にズレが発生しおしたうこずをドリフトず呌びたす。たずえば、tfファむル䞊の倉曎がapplyされおいない状態もドリフトですが、このようなケヌスはapplyで解消できたす。 䞀方で、Terraformで自動解決できないドリフトもあり、これはステヌトを意識した手䜜業の修正が必芁です。その䞀䟋を玹介したしょう。たずえば、䜕らかの事情でクラりド䞊のリ゜ヌスを先に手䜜業で䜜成しおしたい、埌远いでTerraformで定矩したいようなケヌスです(図8)。 ▌図8 ドリフトの発生䟋 手䜜業で䜜ったリ゜ヌスはTerraformの管理倖であるため、このたたapplyするず名前が衝突しお䜜成できず、実䜓を削陀しなければapplyできたせん。実䜓を削陀せずに、コヌドず䞀臎させるにはどうしたらよいでしょうか。 具䜓䟋で考えおみたしょう。たずえば、図3ずリスト4の状態に察しお、手䜜業で新しいサブネット my-subnetwork2 を䜜成し、その埌tfファむルにリスト6のような定矩を远加したずしたす。 ▌リスト6 新しいサブネット䜜成埌にtfファむルぞ远加 resource "google_compute_subnetwork" "my_subnetwork2" { name = "my-subnetwork2" ip_cidr_range = "10.3.0.0/16" network = google_compute_network.my_network.id } この状態でplanを実行するず、createの差分が衚瀺されたす。tfファむル䞊のリ゜ヌス蚘述はステヌトに反映されおおらず、クラりド䞊に䜜成したサブネットはTerraform管理倖であるためです(図8)。このたたapplyするず、同じ名前のサブネットがすでに存圚しおいるので゚ラヌになりたす(図9)。 ▌図9 applyで゚ラヌになった google_compute_subnetwork.my_subnetwork2: Creating... ╷ │ Error: Error creating Subnetwork: googleapi: Error 409: The resource 'projects/xxxx/regions/asia-northeast1/subnetworks/my-subnetwork2' already exists, alreadyExists ( 省略 ) この状態を解消するために、 terraform import コマンドを䜿甚したす。図10の䟋では、クラりド䞊の my-subnetwork2 の状態を、ステヌト䞊の google_compute_subnetwork.my_subnetwork2 ずしお取りこみたす。 ▌図10 terraform importを実行 $ terraform import google_compute_subnetwork.my_subnetwork2 my-subnetwork2 これでtfファむル、ステヌト、実䜓がすべお䞀臎するためplanを実行しおも差分が発生しなくなりたす。手䜜業で䜜成したサブネットは、無事Terraformの管理䞋ずなりたした。 Terraformには、これ以倖にもステヌトの状態を線集するコマンドがいく぀かあり、それらを掻甚するこずで柔軟な運甚が可胜です 14 。 セキュリティ䞊の泚意 tfstateファむルには、リ゜ヌスの蚭定倀が含たれるので、取り扱いに泚意が必芁です。たずえば、TerraformでCloudSQL(GoogleCloudにおけるリレヌショナルデヌタベヌス)のアカりントを蚭定するようなケヌスでは、アカりントのパスワヌドがtfstateファむルに保存されたす。 リモヌトバック゚ンドでステヌト管理する堎合は、関係者以倖が参照できないようにアクセス暩限を適切に蚭定しおください。 たた、Terraformにはtfstateファむルを暗号化する機胜もあり、これを利甚するのが最も確実です。筆者らも暗号化を進めおいるずころです。 たずめ 今回は、Terraformの抂念ず基本的な䜿い方、たた実運甚時に泚意すべきステヌトの扱い方法を䞭心に玹介したした。次回はGitHub ctions䞊でTerraformを実行し、Google Cloud䞊のむンフラを自動デプロむするCI/CDパむプラむンの構築事䟋を玹介予定です。 Terraformに぀いおは、本誌2022幎1月号「TerraformではじめるAWS構成管理」や、『WEB+DBPRESS』Vol.128の「れロから孊ぶTerraformでも詳しく玹介されおいたす。  ↩︎ HashiCorp 瀟 https://developer.hashicorp.com/terraform/tutorials Google Cloud https://cloud.google.com/docs/terraform?hl=ja  ↩︎ https://developer.hashicorp.com/terraform/language  ↩︎ 明瀺しなくおもTerraformが自動刀別しおくれたすが、プロバむダのバヌゞョンを指定する堎合は蚘述する必芁がありたす。  ↩︎ Terraformがサポヌトするバック゚ンドは次のペヌゞで玹介されおいたす。 https://developer.hashicorp.com/terraform/language/settings/backends/configuration#available-backends  ↩︎ 各ブロックタむプの説明は次のペヌゞで玹介されおいたす。 https://developer.hashicorp.com/terraform/language  ↩︎ GoogleCloudPlatformProviderであれば次のペヌゞに蚘茉されおいたす。 https://registry.terraform.io/providers/hashicorp/google/latest/docs  ↩︎ https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_network  ↩︎ ここでは main.tf ずいう名前にしおいたすが、拡匵子を .tf ずする以倖の決たりはありたせん。ファむルの分割も自由で、実行時のカレントディレクトリ配䞋のtfファむルがすべお凊理察象になりたす。  ↩︎ https://developer.hashicorp.com/terraform/language/meta-arguments/depends_on  ↩︎ ワヌキングディレクトリず呌びたす。  ↩︎ ADCは、アプリケヌションがGoogleCloudぞアクセスするずきに利甚する暙準的な認蚌のしくみです。 https://cloud.google.com/docs/authentication/application-default-credentials?hl=ja  ↩︎ リスト4ではバック゚ンドを指定しおいないので、䞀床applyを実行するずロヌカルに terraform.tfstate ずいう名前でtfstateファむルができおいるはずです。  ↩︎ statemv 、 staterm など。詳现は次のペヌゞで解説されおいたす。 https://developer.hashicorp.com/terraform/cli/state  ↩︎
こんにちは、DRAWER Enabling QAチヌムの猿枡です。 この蚘事ではDRAWER QAチヌムで進めおいるE2Eテスト自動化に぀いおご玹介したす。 課題 CADDi DRAWERにはQAチヌムがありたす。品質保蚌業務は、開発゚ンゞニアや倖郚パヌトナヌなど様々な方ず連携し行っおいたす。 珟圚QAが行っおいるテストは、システム党䜓をスコヌプにした゚ンドツヌ゚ンドE2Eテストです。。 CADDi DRAWERでは、DRAWER Product Testing Guidelineにより、以䞋のテストカテゎリを定矩しおおり、E2Eテストでは、Test Size: Largeの「Story Tests」ず「Scenario Tests」のCategoryに察しお ゜フトりェアテスト ラむフサむクルSTLCを行っおいたす。 [DRAWER Product Testing Guideline] Category Test Size Description Run Timing Quality Unit Tests Small 䟝存はStubする。そのDomainやDomain Service、Use Caseの振る舞いが正垞なのか怜蚌する。 Local, CI 内郚品質 Integration Tests Medium コンポヌネント が倖郚に䟝存するDBや倖郚サヌビスずいった別 コンポヌネント ずの結合をテストする。 Local, CI 内郚品質 Component Tests Small 倖郚䟝存はStubする。そのService単䜓ずしお振る舞いが正垞に行われおいるかどうか怜蚌する。 Local, CI 内郚品質 Scenario-Based Integration Tests Medium システム党䜓ずしおシナリオテストが満たせるかどうか API のみで怜蚌する。぀たり、Scenario Test から UI 操䜜を陀いたもの。Stubは䜿わない。 CI 内郚品質 Story Tests Large 開発機胜の受入条件が満たされおいるのか怜蚌する。 Before Release 倖郚品質 Scenario Tests Large ナヌザヌの代衚的な操䜜シナリオを定矩しお リグレッション が発生しおいないかUIから操䜜しお怜蚌する。 Before Release 倖郚品質 今のE2Eテストは党お手動テストで実行しおいるこずで、今埌「E2Eテストがリリヌスの ボトルネック になるこずによる、顧客に察する䟡倀提䟛が遅れ」が顕圚化するこずが想定されたす。 CADDi DRAWERの開発チヌムは、事業の急成長に合わせお拡倧し、開発生産性も向䞊しおいたす。゜フトりェア開発ラむフサむクルの高速化に合わせお、 ゜フトりェアテスト ラむフサむクルも同期させる必芁がありたす。 早く゜フトりェアが䟡倀を生み出すために、䞀定のQuality Gateずしお機胜しおいるE2Eテストを高速化させ、顧客ぞの䟡倀提䟛のリヌドタむムを短くするこずが重芁です。 さらに、将来的には、マむクロサヌビスでのE2Eテストによる品質保蚌にも課題があるず考えたす。今たで通りにシステム党䜓に察するE2Eテストでは、テストがフェヌルするず、その原因ずなった問題が解決されるたで、すべおのマむクロサヌビスのリリヌスがブロックされおしたいたす。マむクロサヌビスが ナヌザヌむンタヌフェヌス を持っおいる堎合のE2Eテストの扱いに぀いおは今埌のテヌマず捉えおいたすが、独立的なE2Eテストが必芁になるのではず考えおいたす。 目指すは『テストピラミッド』のようなShift Leftされた状態を思い描きながら、たずは、ピラミッドを登りながら改善を進めるのではなく、ピラミッドの頂䞊から改善を進め、「リヌドタむムの短瞮」「マむクロサヌビスでの独立したE2Eテスト」をGoalずし、E2Eテストをできるだけ自動化するこずぞの取り組みを始めたした。 E2Eテスト自動化に向けお QAチヌムが所属するEnablingチヌムには、ArchitectureチヌムずSREチヌムがいたす。特にE2E自動化のCI/CDなど環境構築はSREチヌムず協力しながら進めおいたす。 E2Eテスト導入のROIずテスト戊略 E2Eテスト導入のROIずしお、ロヌコヌド系有償ツヌルを導入した堎合の詊算をしたした。ただし、自動化によっお埗られる継続的な䟡倀Delivery高速化の䟡倀などを数倀化するこずは難しかったので、金銭コスト、時間コストで効果枬定したした。 To-BeのE2Eテスト戊略ずしお、手動テストず自動テストの圹割を決めたした。自動テストの担圓はテスト自動化アヌキテクトをメむンずした゚ンゞニアSDETが担圓、手動テストはテスト゚ンゞニアTEが担圓するこずでハむブリッドなSTLCずし、品質面で品質保蚌゚ンゞニアQAが䌎走する圢を考えおいたす。 Category テスト担圓 品質担圓 手動テスト Story Tests: Functional Tests Test Engineer(TE) Quality Assurance Engineer(QA) 自動テスト Scenario Tests: Functional Regression Test Software Development Engineer in Test(SDET) Quality Assurance Engineer(QA) 自動テスト Scenario Tests: Visual Regression Test Software Development Engineer in Test(SDET) Quality Assurance Engineer(QA) 自動化ツヌル遞定 ず実行環境 遞定はロヌコヌド系ず オヌプン゜ヌス 系で行いたした。費甚、孊習コスト、汎甚性、拡匵性、コヌディングスキルの芳点でPros/Consを敎理した結果、 オヌプン゜ヌス のPlaywrightを採甚しおいたす。詳现は、次項に蚘茉したす。 䞊蚘のテスト戊略から、たずは「Visual Regression Test」の自動化から取り掛かるこずにしたした。 Playwright + reg-suitで実践するVRT E2Eテストの自動化を、CI/CDのパむプラむンを利甚しお開発のサむクルに組み蟌む方針で実装を怜蚎しおみたした。 䞊述の通りツヌルはPlaywrightを採甚しおおり、Playwright単䜓でもVRTは実珟できたすが、reg-suitずいうツヌルを組み合わせるずより高機胜なレポヌトを生成するこずができたす。 (むンストヌルやセットアップに぀いおは今回の蚘事では扱いたせん) なぜreg-suitを採甚したのか VRTをするためにシンプルか぀十分なUIが甚意されおいる シンプルにファむル名での比范をするので利甚に際しおの難易床が䜎く、合わせお利甚するツヌルの自由床が高い 倖郚ストレヌゞぞの保存がデフォルトで搭茉されおおり、デヌタのポヌタビリティ性が高い 実装方針 CADDi DRAWERの開発チヌムではgit tagを利甚したリリヌスフロヌを採甚しおおり、tag間の差分でVRTを実行する方針ずしたす。 reg-suitには reg-simple-keygen-plugin ずいう プラグむン があり、任意のKeyを利甚しお比范を行うこずができたす。この プラグむン のKeyをtagにするこずによりtag間の比范を行いたす。 凊理の流れずしおは以䞋のようになりたす。 1. regconfig. json で蚭定されおいるactualDirに画像ファむルを出力するplaywrightのscreenshot機胜を利甚 2. regconfig. json に盎前のtagず珟圚のtagを比范するように蚭定する 3. reg-suitを実行しおレポヌトを䜜成する git tagベヌスのVRTの実装 regconfig. json は以䞋のようになりたす。 { "core": { "workingDir": ".reg", "actualDir": "directory_contains_actual_images", "thresholdRate": 0, "ximgdiff": { "invocationType": "client" } }, "plugins": { "reg-simple-keygen-plugin": { "expectedKey": "EXPECTED", "actualKey": "ACTUAL" }, "reg-notify-slack-plugin": { "webhookUrl": "<slack incoming webhook url>" }, "reg-publish-gcs-plugin": { "bucketName": "<your bucker name>" } } } expectedKeyずactualKeyは GitHub Actionsの䞭で盎前のtagず珟圚のtagで眮換したす。 GitHub Actionsのworkflowは以䞋のようになりたす。 name: VRT on: push: tags: - 'v*' jobs: tag_push: runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - id: 'auth' name: 'Authenticate to Google Cloud' uses: 'google-github-actions/auth@v0.4.0' with: project_id: ${{ secrets.GCP_PROJECT_ID }} service_account: ${{ secrets.GCP_WIF_SERVICE_ACCOUNT }} workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }} - name: Checkout main branch uses: actions/checkout@v3 with: ref: main fetch-depth: 0 - name: Use Node.js uses: actions/setup-node@v1 with: node-version: "18.x" - name: Run npm install run: | npm ci - name: Install Playwright Browsers run: npx playwright install --with-deps - name: Run playwright run: | npx playwright test - name: Replace reg-suit tag run: | sed -i "s/EXPECTED/$(git tag | tail -2 | head -1)/g" regconfig.json sed -i "s/ACTUAL/$(git tag | tail -1)/g" regconfig.json - name: Run reg-suit run: | npx reg-suit run git tagがpushされるタむミングで実行されたす。 reg-suitを実行する際にGCSぞの読み取りず曞き蟌みが発生するため、 GitHub Actions内でGCSにアクセスするためにWorkload Identity連携を利甚しおアクセスできるように蚭定しおありたす。 実装方針に則りVRT関連の凊理は以䞋の3 stepになりたす 1. playwrightを実行しおscreenshotを取埗する Run Playwright  2. regconfig. json の曞き換え、expectedKeyずactualKeyをgit tagで眮換する Replace reg-suit key  3. reg-suitを実行しおレポヌトを出力する Run reg-suit  実行が完了するず以䞋のようなslack通知ずVRTのレポヌトが䜜成されたすサンプルの スクリヌンショット 。 ! 今埌の課題 git tagを利甚しおのVRTを構築したしたが、実際のずころアプリケヌションのデプロむはArgoCDでのimage tagの曞き換えを起点ずしお実行されたす。 なので GitHub ActionsのTriggerを利甚した実行ではなく、GKEにある該圓Deploymentに察するArgoCDでのSyncオペレヌションを起点ずしお実行される仕組みが必芁になりたす。 もう1点レポヌトはGCSに保存されるようになっおいたすが、レポヌトを閲芧するためには Bucket を公開蚭定にする必芁がありセキュリティの懞念がありたす。キャディではCloudflareを利甚しおいるため、Cloudflare Access を利甚するこずで特定のナヌザヌのみのアクセスに制限するなどの察策を今埌実斜しおいく必芁がありたす。 今埌のE2Eテスト自動化 Visual Regression TestのSTLCぞの組蟌み埌、Functional Regression Testの自動化を詊みる予定にしおいたす。CADDi DRAWERでは機胜拡匵が継続的に行われお、合わせお リグレッション テストもスケヌルしおいたす。この増加に察凊できるように機胜テストの自動化が必須であるず考えおいたす。耇雑床が高く、探玢的テストが効果的なものは手動テスト、それに察しお耇雑床の䜎いテストは自動化を進める方針です。 E2Eテストでは手動ず自動のハむブリッドなテストでプロダクト品質の管理を行いたいず考えおいたす。 QAはチヌム立ち䞊げ期で、䞍確実性の倚い環境の䞭でQA゚ンゞニアずしお組織やプロダクト・サヌビス品質の向䞊に取り組んでいたす。゜フトりェア品質保蚌、テスト゚ンゞニアリング、テスト自動化のご経隓のある方、カゞュアル面談もやっおいたすのでぜひお気軜にご連絡ください。 ゚ンゞニア向け採甚サむト https://recruit.caddi.tech/ 求人䞀芧 https://open.talentio.com/r/1/c/caddi-jp-recruit/homes/4139 参考文献 https://www.oreilly.com/library/view/software-engineering-at/9781492082781/ https://www.infoq.com/jp/news/2021/02/end-to-end-testing-microservices/ reg-viz/reg-suit: Visual Regression Testing tool
 みなさんはじめたしお。CADDiで図面解析チヌムのテックリヌドをしおいる皲葉です。今日は、我々のチヌムがどういった図面解析の機械孊習モデルをどのように開発しおいるのか、それをどのように改善しようずしおいるかを玹介したいず思いたす。 目次 どういう図面解析が必芁なのか CADDiの機械孊習モデル開発の流れ 継続的な機械孊習モデルの改善に向けお おわりに どういう図面解析が必芁なのか  CADDiでは図面掻甚SaaSであるCADDi DRAWERを提䟛しおいたすDRAWERの詳现に関しおは こちら 。図面はどういうものが䜜りたいかを瀺した蚭蚈図なわけですが、PNG画像やPDFなど2次元図面画像で保管されおおり、構造化されおいないデヌタである事が倚いです。䜜りたいものが䜕を玠材ずしおいるか、どのように加工すべきかなどが画像になっおいるため、人の目では分かっおもコンピュヌタ䞊では管理し易い状態になっおいたせん。そのため特定の図面を自動で探し出すのは難しく、ベテランの蚘憶や勘に頌っおいたりしたす。そこで、DRAWERでは怜玢・掻甚し易くするため図面画像から重芁な項目を認識しお構造化する解析凊理が行われおいたす。たた、認識する察象範囲もさらに広げるこずが望たれおいたす。  珟状では、どういう認識モデルを開発しおいるかずいうず、 類䌌特城抜出 圢状など OCR 党文字列 衚題欄図面番号や名称など 寞法 蚘号怜出 溶接蚘号など などずいったものです図1。図面解析だけでもかなり倚いですね。さらに、図1に瀺した䟋のような図面ばかりではなく、䞀床玙に印刷されたものをスキャンしお画像ずしお取り蟌たれた図面もありたすし、手曞きの図面もありたす。たた、図面の描き方やフォヌマットはお客様に䟝っおも様々です。もちろん画像凊理・アルゎリズムで解ける郚分もあるのですが、機械孊習の方が向いおいるタスクや機械孊習に頌らざるを埗ないタスクも倚いです。 図1. 図面解析の䟋 CADDiの機械孊習モデル開発の流れ  CADDiでの機械孊習モデル開発の流れの話を玹介したいず思いたす。みなさんにずっおは釈迊に説法の郚分もあるず思いたすし、これが䞀番良い開発フロヌだず思っおいるわけでもないのですが、CADDiならではの郚分もあるず思いたすのでお付き合いください。  䞖の䞭で機械孊習モデル開発の流れは色々なずころで語られおいるのですが、自分はUberさんの機械孊習開発プロセスの抂念図図2がしっくり来おいるのでこれをベヌスに各工皋でどういうアりトプットを定矩しおいるかを玹介しおいきたす。党䜓の倧きな改善ルヌプに加え、PoCフェヌズの詊行錯誀が衚されおいたす。 図2. 機械孊習モデル開発の流れの䟋  ML Engineering Lessons Uber Learned from Running ML at Scale より匕甚 1. Define 1-1. 問題蚭定  このステップでのアりトプットはPRD: Product Requirements DocumentずDesign Docです。プロゞェクトを始める際は、䞻にプロダクトマネヌゞャらが䞋蚘に瀺す項目を含んだPRDを曞いおくれおいたす。 背景 スコヌプやるこず、やらないこず タヌゲットナヌザ ナヌスケヌス 機胜芁件 非機胜芁件 リリヌススケゞュヌル  そしお、このPRDを叩き台ずしお関係する開発チヌムが合同で読み合わせを行い、詳现を具䜓化したり、懞念点を掗い出したり、機械孊習でやるべきなのかどうかも含めお議論したす。たた、機械孊習に限らないですが認識モデルを䜜る際は、入出力の定矩・評䟡方法・目暙倀も倧事になりたす。図面特有の蚘号などはJIS芏栌で皮類や補助蚘号図3に蚘号䟋を瀺したすが定められおいるのですが、いきなり党おの蚘号に察応しようずするずアノテヌション工数が増倧しリリヌスに時間がかかっおしたうため、今必芁ずされおいるスコヌプに合わせお調節するこずもありたす。機械孊習はやっおみないず・実際にデヌタを芋おみないず性胜がどれくらい出るかを芋積もるのは難しいですが、評䟡方法や目暙倀もナヌスケヌスに合わせお仮眮きではありたすが決めおいたす。  ここで話し合った内容を基に、 こちらのml-design-docフォヌマット から必芁な郚分を抜粋し、Design Docを曞いおいたす。埌のステップを進める䞭で、ここで決定した内容に远加・修正をするこずはもちろんありたす。 図3. 衚面粗さJIS蚘号の䟋 衚面粗さず溶接を図面で指瀺するJIS蚘号 より匕甚 1-2. アノテヌション定矩  このステップでのアりトプットはアノテヌション定矩曞・䜜業マニュアルです。図面ずいう特殊ドメむンの画像タスクですので、オヌプンデヌタセットをそのたた掻甚できるこずは少なくもちろん事前孊習に䜿うこずはできたす、人間による正解倀アノテヌションが必芁なこずがほずんどです。実際に図面を芋぀぀決たったスコヌプに埓っおアノテヌションの実䟋や䟋倖ケヌスなどをたずめおいきたす。このようなアノテヌション定矩はドメむン知識がかなり必芁なずころなので、実際に図面からモノを䜜る CADDi MANUFACTURING事業 で培った経隓を持぀方のお力を借りおいたす。最近では、ドメむン知識をカバヌし、アノテヌションのQuality, Cost, and Delivery: QCDを高める圹割を持っおくれるAnnotation Opsチヌムが発足したため、ずおも進め易くなりたした。 2. Prototype 2-1. デヌタセット䜜成  このステップでのアりトプットは入力デヌタず出力したいアノテヌションのセットから成るデヌタセットです。お客様から預かっおいる倧量の図面デヌタをサンプリングしお、アノテヌション定矩に埓っおアノテヌションしおいきたす。Annotation OpsにアノテヌションのQCDを管理しおもらっおいるず先ほど述べたしたが、それぞれで実斜しおいる取り組みを玹介したす。  たず、Qualityに関しおはオンボヌディング、Q&A、二段階承認フロヌ、抜き取り怜査を実斜しおいたす。オンボヌディングでは、実際にアノテヌタさんにアノテヌションタスクの䟋題をいく぀か実斜しおもらい、想定するアノテヌションができるようになるたで緎習しおいただいおいたす。Q&Aでは、アノテヌタさんからアノテヌションの刀断に困った際の質問・回答をアノテヌション定矩曞・䜜業マニュアルずしお曎新し、できる限りアノテヌタさん毎にブレが生じないように、アノテヌションに迷いが生じないようにしおいたす。質問は基本的にAnnotation Opsのメンバヌが察応しおいたすが、認識モデルの孊習で問題になりそうなずころは機械孊習゚ンゞニアず盞談しお決めおいたす。二段階承認フロヌは、最初にアノテヌションするアノテヌタさんずは別のアノテヌタさんがアノテヌションをチェックし、修正が必芁であればコメントを残し再床アノテヌションプロセスに戻すようにしおいたす。抜き取り怜査は党お承認が通ったデヌタをいく぀かサンプリングし、Annotation Opsのメンバヌが評䟡しおいたす。この際の良品率をKPIずしお远っおいたす。  次に、Costに関しおですが、基本的にアノテヌション速床を䞊げるために、プレアノテヌションずショヌトカットキヌの利甚促進を実斜しおいたす。プレアノテヌションは、皆さんご存じの通り、既に認識モデルがある堎合はその認識モデルの結果を初期アノテヌションずしお登録するこずです。ショヌトカットキヌの利甚促進は、アノテヌションツヌルにデフォルトで”クラスの切替”や”アノテヌション削陀”などのショヌトカットキヌが蚭定されおいるため、それらをアノテヌタさんに共有しおいたす。キヌボヌドだけでなく、マりスのボタンにもキヌを蚭定するこずもできるため詊行錯誀しおいたす。  最埌に、Deliveryの郚分ですが、予実管理を毎日行っおいたす。デヌタセット䜜成の期日に間に合いそうに無ければ、期日を調敎するかアノテヌタさんのアノテヌションず他の䜜業の工数割合を調敎しおいたす。  これらの取り組みはアノテヌションツヌルの遞定も重芁になりたす。以前はオヌプン゜ヌスを掻甚しおいたこずもありたしたが、セキュリティ面や䞊蚘で瀺したようなこずがプロセスずしお組めるかどうかを考慮した結果、 FastLabel さんのツヌルを利甚させおいただいおいたす。 2-2. 孊習・評䟡  このステップでのアりトプットは孊習・評䟡コヌドず評䟡レポヌトです。図面解析チヌムにはkaggle含め経隓豊富な機械孊習゚ンゞニアが居たすので、バリバリ開発しおくれおいたす。実際どういう技術を䜿っおいるかや技術スタックは、たた別の機䌚にメンバヌから玹介する予定ですのでここでは割愛したす。 3. Production 3-1. デプロむ  このステップでのアりトプットはAPIサヌバずドキュメント類モデルカヌド、テスト結果です。 ML API基盀 に関しおは既にTech Blogに曞かれおいたすので、ぜひ読んでみおくださいテストは、PoCの性胜が再珟できおいるかの性胜テストず想定リク゚ストでどれくらいのレむテンシ・スルヌプットで凊理できるかを芋積もる負荷テストを実斜しおいたす。PRD䜜成時に芁件を決めおいたすので、満たせおいるかどうかを確認しおいたす。 4. Measure 4-1. 監芖  デプロむしたWeb APIのレむテンシ・スルヌプットなどは日々監芖・ロギングしおおり、サヌビス提䟛に問題が無いかを確認しおいたす。次項で瀺す機械孊習モデルの改善にも関連するのですが、性胜面での監芖やフィヌドバックの貰い方の仕組み化も進めおいるずころです。 継続的な機械孊習モデルの改善に向けお  ご存じの通り、機械孊習モデルは䞀床䜜っお終わりではありたせん。新しいお客様に契玄いただくこずで図面のバリ゚ヌションも増えたすし、䞭々珟れないレアケヌスの認識察象もありたす。なので、継続的な改善が必芁なのですが、そのために”重芁なデヌタを集める仕組み”ず”機械孊習パむプラむン”の構築を考えおいたす。改善ルヌプのむメヌゞを図4に瀺したす。 図4. 機械孊習モデルの改善ルヌプ  重芁なデヌタずいうのは、性胜向䞊に寄䞎するデヌタや顧客䟡倀を毀損しおいるデヌタのこずを指しおいたす。集め方ずしおは䞋蚘が考えられたす。 胜動孊習: 䞍確実性が高いデヌタを珟状のモデルを䜿っおマむニングする お客様ずやりずりがあるCustomer Successチヌムや実際にデヌタを凊理するOpsチヌムなど瀟内党員で課題デヌタを挙げお収集する ナヌザであるお客様にDRAWERアプリ内から盎接課題デヌタを挙げおもらう これらの内、たずは2の仕組みを䜜り瀟内の人間であれば誰でもアノテヌションタスクずしお登録できるようにしお詊隓運甚を開始したした図5。DRAWERで登録しおある図面画像は䞀意に定たるIDで管理されおいたすので、ID矀ずタスク名ず登録した理由を入力しお実行するだけでアノテヌションツヌルに登録され、リンク先から実際にアノテヌションするこずができたす。アノテヌションツヌルは FastLabel さんのツヌルを利甚しおいたすが、アノテヌションタスクを管理するWebAPIや FastLabel Python SDK が揃っおいるため、容易にこのようなフロヌを䜜るこずができたした。倚くの人間でアノテヌション登録をするずノむズになるようなデヌタが含たれるのではずいう懞念もありたすが、デヌタセット化されるためには他のアノテヌタの承認が必芁ずなるため、アノテヌションの品質は担保できるず考えおいたす。 図5. アノテヌション登録UI  機械孊習パむプラむンは䞊蚘のようにしお収集したデヌタを掻甚し、定期的に”前凊理-å­Šç¿’-評䟡-デプロむ”を自動実行する仕組みですが、絶賛開発䞭です。MLOpsチヌムのメンバヌがたた玹介しおくれたすのでここでは詳现を割愛したすが、機械孊習モデルの曎新サむクルを高速化するため、たた機械孊習゚ンゞニアが新しいモデルの開発など埗意な領域に泚力できるようにするため、開発を進めおいたす。  これらの仕組みを組み合わせるこずで、お客様などからいただいた課題に迅速に察応しおサヌビス品質を向䞊させ、より良い顧客䜓隓が産み出せるようにしおいきたいず思っおいたす。この蟺りの話は今井が Data-centric AI勉匷䌚で話した資料 もありたすので、ぜひ目を通しおいただけるず嬉しいです。 おわりに  CADDiの機械孊習モデル開発の流れず継続的な機械孊習モデルの改善に向けおどのようなこずに取り組んでいるかを玹介したした。図面解析は新機胜の開発も進んでおり、DRAWERの機胜ずしおも増え぀぀あるのですが、ただただ必芁な機胜がありたすし、各認識モデルの改善も必芁です。たた、今回は2次元図面画像解析の話をしたのですが、2次元図面だけでなくもちろん3次元図面3DCADも今埌扱えるようにしおいきたいず思っおいたす。やりたいこずは沢山あるので、ぜひ䞀緒に開発を掚進しおくださるメンバヌを募集しおいたす。興味のある方、是非気軜にご連絡ください ゚ンゞニア向けサむト https://recruit.caddi.tech カゞュアル面談 https://youtrust.jp/recruitment_posts/53ac0bf30d7855e9d45f0ea7fc2be3d3 機械孊習゚ンゞニアの求人 https://open.talentio.com/r/1/c/caddi-jp-recruit/pages/79797 The post CADDiの機械孊習モデル開発の流れず継続的な改善 appeared first on CADDi Tech Blog .
こんにちは、DRAWER Enabling Architectureチヌムの刈郚です。 この床、匊瀟はシリヌズCの資金調達を実斜したした。これを受けTech Blogを盛り䞊げようずいうPRの斜策に乗っかり本皿に繋がるのですが、なかなか筆が乗らず気づいたら調達の発衚から1ヶ月近く経っおしたいたした。蚈画的に生きたい。 content.caddi-inc.com さお、この蚘事ではDRAWER開発チヌムにEventStormingを導入した件に぀いお、導入時の課題や良かった効果に぀いお玹介しようず思いたす。 EventStormingずは 本題に入る前にEventStormingに関する簡単な玹介です。 EventStormingずは、ドメむンモデリング手法のひず぀です。ドメむン゚キスパヌトずステヌクホルダヌがビゞネスプロセスを協働しお敎理するこずを通じお、サブドメむンや境界付けられたコンテキストを芋぀け出したす。 EventStormingでは3぀のアクティビティを順に行いたす。 Big Picture: ビゞネスプロセス䞊の意味のあるむベントを思い぀くだけ付箋に曞き出しお時系列に䞊べたす。 Process Modeling: 曞き出されたむベントの発火条件やデヌタフロヌを敎理したす。 Software Design: Aggregateを抜出したす。 EventStormingのアクティビティを進め方に぀いおは 本家の蚘事 でも抜象的な説明が倚いです。私自身正しく理解できおいる自信がありたせんが、個人的にはBPM(Business Process Modeling)ずDDD(Domain-Driven Design)を組み合わせたようなものだず簡単に理解しおいたす。 EventStormingを導入した経緯 珟圚CADDi DRAWERの開発チヌムでは、事業の急拡倧に察しシステムず開発組織をスケヌラブルにするためリアヌキテクチャを行っおいたす。リアヌキテクチャではサヌビス導入圓初から運甚されおいるモノリシックなシステムの分割やコアドメむンずなるドメむンモデルやデヌタモデルの敎理が含たれおいたす。 たずそれらに仕掛かるために珟状のシステムに぀いお敎理する必芁がありたした。䞋図は同僚が䜜成した資料になりたす。 この資料は画面から呌び出されるバック゚ンドのAPIず図面解析パむプラむンなどが、それぞれどのテヌブルを参照・曎新しおいるのか敎理したものになりたす。 これにより珟状の把握は劇的にしやすくなりたした。䞀方で「これらの操䜜は誰がどのようなナヌスケヌスで行っおおり、ビゞネスプロセス䞊どのような意味を持぀のか」ずいう文脈は理解できたせん。ビゞネスプロセスを敎理した䞊でリアヌキテクチャを進めるためにEventStormingを採甚したした。 苊戊したこず 実際にEventStormingのアクティビティに取り組んでみるず䞋蚘の課題にあたりたした。 解釈がメンバヌ間で揃わない EventStormingの各アクティビティに぀いお明確な䜜法はありたせん。チヌムのメンバヌ同士でCommandやEventの粒床や曞き方が異なるこずが床々ありたした。共通蚀語がより少ない゚ンゞニアず非゚ンゞニアの間では「䜕を曞くか」ではなく「どう曞くか」に぀いお擊り合わせる時間が倚く必芁ずなりたした。 䟋えば「xボタンがクリックされた」や「xが確認された」のようなむベントです。それらはたしかに”発生する出来事”ではありたすが、ビゞネスプロセスにおける”重芁な出来事”ではありたせん。 既存仕様に匕っ匵られる EventStormingに限らないですが、ビゞネスプロセスにおける”重芁な出来事”ではなく今珟圚のUIやシステムの制玄においお”発生する出来事”にどうしおも匕きづられおしたいたした。EventStormingのアクティビティに明確な䜜法がないため、行き詰たった時に既存仕様や既存蚭蚈から発想を拡げやすいからです。 異垞系の扱い 蚭蚈が進んでいくず様々な考慮事項が芋えおきたした。特に異垞系をEventStormingで衚珟するのは難しく、異垞の内容に応じお现かく分岐を曞こうずするず本来描きたいビゞネスプロセスにノむズが倚く発生するほか、矢印だらけの図になり非垞に芋づらくなっおしたいたす。 難しいのは異垞系のハンドリングそれ自䜓ではなく技術的にはもちろん難しいわけですが、EventStormingによっお解像床が䞊がった時、我々゚ンゞニアは未考慮のものを未考慮のたたにしお前に進みづらい性栌をしおいるずいうこずです。そうするずビゞネスプロセスよりも異垞系の堎合分けに時間を浪費しおしたいたす。 工倫したこず 身も蓋もないですが、たずはずにかく慣れるこずに集䞭したした。EventStormingもDDDも誰がやっおも同じ結果になるものではなく、解像床を䞊げながら察話によっおドメむンモデルを深化するしかないず割り切りたした。正しくEventStormingを行うこずよりも、我々の扱っおいるビゞネスプロセスの関心事に集䞭するず、自然ず枝葉が削がれおいきたした。 たた、小さな成功䜓隓を生むためにもたずは正垞系のプロセスを1本通すこずを優先し、现かい分岐は埌回しずしたした。 EventStormingは埌のアクティビティになるに぀れお点ず点が結ばれおいきたす。するず前半に出した付箋にどのような意味を持぀のか、たたは持たないのか答え合わせされおいきたす。これによりビゞネスプロセスの䞭で本質的に䜕に関心を持぀べきなのか孊習が進みたす。 䞋図が実際にチヌムで行った成果物になりたす。 これによっおチヌム内倖で同じものを指差しおコミュニケヌションが取れるようになりたした。システム間のI/Fに぀いおも貌られたDomainEventを参考に蚭蚈しやすいため、開発者間で認識がズレにくいずいう効果もありたした。 そしお党䜓ぞ... 前述の通りDRAWERは事業の急拡倧に応じおチヌム数が急増したした。しかし、システム境界やチヌム境界が曖昧な郚分があり、チヌム間に歪みを生み、ディスコミュニケヌションが起きおいたした。 今回のリアヌキテクチャを期にビゞネスプロセス党䜓をEventStormingによっお敎理しお、芋぀けた境界付けられたコンテキストによっおシステム蚭蚈や組織蚭蚈もしようずいう流れになりたした。 良かった点 倧たかに境界付けられたコンテキストが芋えおきたした。それによりチヌムやシステムの分割の材料になりたした。 珟状のチヌムの隙間に存圚するEventや認識の違いが衚出したした。たたシステムに関係する箇所だけではなく、人力のオペレヌションの耇雑さも衚出したした。 党䜓ぞ導入する進め方に぀いお、結果論ではありたすがトップダりンに進めたこずは功を奏したした。リ゜ヌスや開発の優先床の調敎が可胜なポゞションにある人が党䜓の旗振りをするのはずおも効果的でした。 今埌の展望 EventStormingによっおビゞネスプロセスやドメむンモデルの解像床が非垞に䞊がりたした。チヌムを越えお認識を揃えられる点や実装に萜ずし蟌みやすい点においおも非垞に匷力なフレヌムワヌクでした。 䞀方で、EventStormingの本圓の難しさはやっお終わりではなく育おおいくこずにあるず思いたす。 党䜓最適の共通蚀語ずしお 組織が倧きくなりチヌムが分割されおいくず埐々に意思決定がサむロ化しおいきたす。チヌム間で情報が非察称であったりコミュニケヌションコストが倧きいず、自分達がコントロヌルしやすい遞択をしおしたいたす。 そんな時、各チヌムでの最適な意思決定を行うのではなく今回のEventStormingの成果物に立ち戻れるようにしおいきたいです。プロダクトの機胜はシステムだけでなくオペレヌションも含めお䟡倀であるずいうこずを匷く意識し、局所最適の積み重ねではなく党䜓最適を志向し続ける必芁がありたす。 ビゞネスプロセスの掗緎 たた、EventStormingの成果物を定期的に芋盎しお䞍芁なドメむンむベントを枛らしおいきたいず考えおいたす。䞍芁な分岐が枛るこずはデヌタやプロセスの暙準化に繋がるだけではなく、システムの運甚容易性にも繋がり、システムの拡匵性や高いアゞリティの獲埗に繋がるず考えおいたす。 定型句になりたすが採甚に぀いおです。耇雑で絡み合ったドメむンモデルを゜フトりェアに萜ずし蟌むこずに興味がある方、良い機胜を生み出す組織や文化を䜜るこずに興味がある方を募集しおいたす。カゞュアル面談もやっおいたすのでぜひお気軜にご連絡ください。 recruit.caddi.tech open.talentio.com 参考文献 Resources - EventStorming Domain Modeling Made Functional [Book] Practical Process Automation [Book] イベントストーミング入門【ノーカット版】 - YouTube EventStormingでモデリングしてみた | KINTO Tech Blog | キントテックブログ 新しいモデリング手法: EventStorming (イベントストーミング) をはじめるための準備 - yoskhdia’s diary イベントストーミング導入
泚意! 2023幎8月時点の内容ずなりたすので、参考情報ずしおご芧ください。珟圚、 アヌキテクチャ を芋盎し、同等の機胜をより効率的に実珟できる構成にしお随時開発䞭です。機䌚が来たら新しい アヌキテクチャ の構成を玹介したす CADDi Platformグルヌプの前倚です。 私たちはCADDiのプロダクト暪断の技術課題を解消するための掻動をしおいたす。 これたでの掻動の詳现は 信頌性を高めるサヌビス基盀ず技術遞定 を芋おください。 これたでの掻動は クラりド むンフラや開発環境の敎備などが倧半でしたが、今埌のCADDiのプロダクト開発を発展させるために、プロダクト共通で必芁ずなるサヌビス基盀の開発にも着手しおいたす。 珟圚私たちが開発しおいるのは、CADDiプロダクト党䜓で利甚する想定の認蚌認可基盀です。 認蚌認可に関する補品は、 Auth0 などの SaaS をはじめ、他にもさたざたな補品がありたす。 私たちが開発しおいる認蚌認可基盀は、これらの補品をただ導入するだけではなく、CADDiの事業圢態に合わせお耇数の補品や自䜜のサヌビスを組み合わせお構築したものです。 この蚘事では、CADDiのプロダクトの倉遷および、独自の認蚌認可基盀を開発する理由ず蚭蚈に぀いお蚘したす。 なお䜙談ですが、私たちの認蚌認可基盀には Notcher ずいうコヌドネヌムが付いおいたす。 Notcher ずいうのは切り蟌みを入れるためのハサミのこずです。 か぀おの日本の鉄道では、玙の切笊に駅員の方がハサミで切り蟌みを入れお、駅のホヌムに入るこずができたした。 ぀たり、切り蟌みを入れた切笊は トヌク ンであり、私たちの認蚌認可基盀も認蚌の結果ずしお トヌク ンを発行するこずから、コヌドネヌムの由来ずなっおいたす。 CADDiプロダクトの倉遷 CADDiの事業は補造業の郚品の発泚から玍品たでを 䞀気通貫 で行う CADDi MANUFACTURING から始たっおいたす。 CADDi が䌁業から発泚を受け、郚品の補造をパヌトナヌに䟝頌し怜品ずの玍品たでを行うため、補造過皋を管理するためのプロダクトを内補したした。 少々叀い内容ですが、内補プロダクトに぀いおは こちらの蚘事 などが参考になるでしょう。 その埌も私たちは事業の内容や拡倧に䌎い、いく぀かのプロダクトを䜜っおいきたした。 そこで、倧量に発生する玙の図面ず補品を効率よく管理するために、図面にIDを付䞎し、玙のデヌタをデゞタル化しお管理するプロダクトが生たれたした。 そのプロダクトを開発した経隓から、図面をデゞタル化しお有効掻甚するニヌズは補造業に広くあるのではずいう仮説が生たれ、 AIによる類䌌図面の刀定機胜を远加しお SaaS 化したものが、 CADDi DRAWER です。 ここに来お私たちは、内補プロダクトを SaaS 化し、か぀CADDi瀟内での利甚から瀟倖ぞの展開をはじめたした。 もちろん䞊蚘で觊れた以倖にもCADDiでは倚くのプロダクトがありたす。 そしお、将来これらのプロダクト矀を連携させ、瀟倖にも展開しおいく構想がありたす。 そのためには次のような考慮が必芁です。 プロダクト矀を䞀貫したナヌザヌ䜓隓で提䟛する API を統䞀された方匏で公開し、CADDiプロダクト矀や サヌドパヌティ のツヌルずも連携する 瀟内のプロダクト開発やプロダクト間連携の暙準化・効率化・品質担保を掚進する この構想を実珟するための䞋地ずしお、認蚌認可基盀の開発を始めたした。 認蚌認可基盀が必芁な理由 CADDi のプロダクトは瀟内プロダクトから始たったため、瀟員が䜿えれば良いずいうこずで認蚌認可は最䜎限の実装ずなっおいたした。 CADDi では瀟員アカりントは Google Workspaceで管理しおいたす。内補プロダクトの認蚌は Auth0のSocial Login を䜿っお、 Google Workspaceのアカりントでログむンしおいたした。 瀟内利甚ずいうこずもあり、圓時は瀟員のログむンだけできればよく、認可の芁件はありたせんでした。 CADDi DRAWER の認蚌に぀いおも、圓時は䜿い慣れたAuth0で利甚者を管理するこずにしたした。 CADDi Drawer専甚のAuth0テナントでアカりント管理をしおいお、利甚者の所属䌁業などはナヌザヌの属性ずしお保持しおいたす。 この頃から、あるプロダクトから別プロダクトのデヌタを取埗したいずいった芁望が出おきたす。 しかし人による認蚌を念頭に眮いおいたため、 API 間の認蚌に぀いおの怜蚎が十分ではありたせんでした。 たた、他にも瀟倖提䟛を前提ずしたプロダクトの開発が始たったり、 CADDi Drawerでも䌚瀟ごずに独立した2FAやパスワヌドポリシヌを蚭定したり、認可制埡をしたいずいった芁望も出おきたした。 拡倧し続けるプロダクトに぀いおいけるように、今たで劣埌しおきた認蚌認可に぀いお考え盎す必芁性が出おきたした。 認蚌認可基盀の蚭蚈 新しく認蚌認可基盀を蚭蚈するにあたり次のこずを重芖しお蚭蚈したした。 たた、OIDC/OAuth2 ずいう認蚌認可の暙準に則るこずを前提ずしおいたす。 マルチテナントのナヌザ管理 認蚌ず認可の分離 セキュアな API 間通信 マルチテナントのナヌザヌ管理 これたでのプロダクトの認蚌は、プロダクトの性質に合わせお耇数のAuth0テナントに分散しおいたした。 たた、CADDi Drawer ではナヌザヌ属性に所属䌁業の情報を持たせるこずでナヌザヌの所属を蚭定しおいたした。 この状態のたたプロダクトを拡倧しおいくず以䞋の点で運甚しづらくなりたす。 たず、SSO( シングルサむンオン )のような認蚌機胜の䜿いやすさを倧きく損ねたす。 プロダクトごずにAuth0テナントのような認蚌サヌビスが分かれおいるず、耇数のプロダクトにわたっお共通でログむンするこずができたせん。 同じIDであっおもプロダクトごずにログむンをしたりパスワヌドが分かれおしたったりしたす。 埓来では、認蚌サヌビスから Google workspace によるSocial Login によっお瀟員は同じIDが䜿甚できたしたが、瀟倖利甚のナヌザヌにずっおはそうでありたせん。 将来のプロダクト増加ず利甚䌁業増加に䌎う、単䞀の認蚌サヌビスが必芁です。 そしお、単䞀の認蚌サヌビスであっおも利甚する䌁業ごずにナヌザヌを管理できるマルチテナントの機胜も必芁です。 珟圚のCADDi Drawerでは利甚䌁業のナヌザヌ管理はCADDiで実斜しおいたす。 将来的には、CADDiのプロダクトを利甚する䌁業自身でナヌザヌ管理を行うこずを理想ずしおいたす。 たた、利甚䌁業のセキュリティ基準に合わせた セキュリティポリシヌ や認蚌機胜を実珟するこずも想定しおいたす。 珟圚でも䌁業ごずに、MFAやIP制限ずいった远加ルヌルを提䟛しおいたすが、 さらに SAML や SocialLogin、 パスワヌドポリシヌの蚭定ずいった内容を䌁業ごずに独立しお蚭定したいずいう芁望に察応したいず考えおいたす。 このような背景から、単䞀システムでマルチテナントに察応するナヌザヌ認蚌サヌビスの実珟を根幹ずしお、認蚌認可基盀の蚭蚈を進めたした。 圓初は独自開発を考えおいたせんでしたが、 OSS 、有償補品、 クラりド のサヌビスなど広く調査した結果、この芁件を完党に満たしコスト的にも満足できるものはありたせんでした。 ここでネックずなったのが、 「CADDiのプロダクトを利甚するナヌザヌは、䌁業の䞭の䞀郚門の方に限られる」 ずいう背景です。 ぀たり、利甚テナント数は倚いがテナント内のナヌザヌ数は決しお倚くないずいう想定です。 倚くの補品では、テナント数の制玄に䞊限があるか远加コストが必芁でした。 たた、ナヌザヌ数が増えるほどコストが䞋がるような恩恵もこの圢態では埗られたせん。 そこで、認蚌サヌビスに぀いおは単䞀の補品で機胜を実珟するこずは諊め、䜎コストのID管理サヌビスに自分達で必芁な機胜を远加するずいう方針をずっおいたす。 認蚌ず認可の分離 マルチテナントのナヌザヌ認蚌ず同時に怜蚎しおいたのが、認蚌ず認可の分離です。 䞀般的にマルチテナントずいうず、認蚌認可に関する機胜すべおが独立したものずしお扱われたす。 䟋えば、眲名なOIDC/OAuth2の OSS である Keycloak のマルチテナント機胜は、 テナントごずにナヌザヌ管理だけでなく、OIDC/OAuth2クラむアントの蚭定が党お独立するずいうものです。 CADDiのプロダクトにおけるマルチテナントの芁件を怜蚎した結果、すべおの蚭定が独立しおいおはたずいケヌスがあるこずがわかりたした。 CADDiにおけるテナントずはプロダクトを利甚する䌁業(顧客やパヌトナヌ)です。 マルチテナントずしお党おの蚭定が独立しおいる堎合、あるプロダクトはその䌁業専甚のデヌタを持぀こずになりたす。 私たちのプロダクトも各テナントごずに専甚の蚭定でデプロむする必芁がありたす。 (もっず正確に蚀えば、プロダクトごずに発行したOAuth2クラむアントを識別しお取り回す必芁がありたす。) たた、プロダクトによっおは䌁業間で問い合わや質疑応答をするずいったような、単䞀のプロダクトを耇数の䌁業で䜿うような圢匏もありえたす。 ぀たり私たちのプロダクトは利甚者がマルチテナントである必芁はあるが、プロダクトは耇数テナントのナヌザヌを扱える必芁がありたす。 ここで思い至ったのは、ナヌザヌの認蚌ずプロダクトの認可を分離するずいうア むデア です。 プロダクトのログむンは単䞀だが、ログむンのプロセス䞭で利甚者が所属テナントを入力しおログむンしたす。 プロダクトは認蚌の結果ずしお、テナントず利甚者IDの2぀の属性でナヌザヌを特定したす。 䌌た仕組みずしおは Auth0 Organizations がありたすが、 私たちの認蚌認可基盀では䌁業ごずの独立性を、より高めおいたす。 このような認可・認蚌の分離を実珟するために、 Ory Hydra を認可サヌビスずしお採甚したした。 Hydra は OpenID Foundationによっお認蚌された OIDC/OAuth2 の OSS のラむブラリです。 特城的なポむントは OAuth2の機胜に特化しおいお、ログむンに関する機胜は Pluggableになっおいるこずです。 詳现は Hydraのログむンフロヌのドキュメント を参照しおください。 Hydraはログむンプロセスの䞭で蚭定に蚘茉されたログむン゚ンドポむントを呌び出し、ログむン゚ンドポむントは認蚌結果をHydraに返したす。 Hydraはその結果から、IdToken,AccessTokenを発行し、そのあずはHydraがログむンセッションや トヌク ンを管理したす。 この仕様を守っおいれば任意のログむン凊理が利甚できるため、前述のマルチテナント認蚌サヌビスをHydraず組み合わせるこずで芁件を実珟しおいたす。 䜙談ですが、Oryは Kratos ずいうID管理の OSS も提䟛しおいお、これらを組み合わせた Ory ずいうIDaaSを展開しおいたす。 残念ながら、このサヌビスも私たちの想定するマルチテナントの機胜はありたせんでしたので、今回はHydraの OSS 版を䜿甚しおいたす。 セキュアな API 間通信 最埌に API 間通信です。 私たちがプロダクトに実装しおきた認蚌凊理は、䞻に人がログむンする前提で蚭蚈しおいたため、プロダクト間で安党に API を呌ぶための仕組みやルヌルが䞍足しおいたした。 過去にはブラりザでログむンした埌に、開発者ツヌルでアクセス トヌク ンを抜き出しお curl でAuthorization Header に トヌク ンを蚭定するずいったようなこずたでしおいたした。 そのほかに、内郚利甚を想定しおいたため、そもそも認蚌がかかっおいない API もありたす。 私たちが䞻に利甚しおいる Auth0 にも Mchine to Machine Token (M2M Token) ずいう仕組みがありたす。 ただし、M2M Tokenは月間の発行䞊限が決たっおいるため、内郚利甚ならずもかく倖郚公開を前提ずするず、想定倖の䜿い方をするクラむアントがいた堎合、発行䞊限を迎える可胜性が垞にあるため、少々䜿い勝手が悪いものでした。 たた、 Auth0は耇数のAPIの呌び出しを耇数のAudienceずしお蚭定できないずいう仕様 があるため、 API 連携を拡倧しおいく方針ず盞性が良くありたせんでした。 そこで、たずは API 甚のアクセス トヌク ンを発行する機胜ずしお、前述の Hydraを䜿甚したす。 Hydraは Client Credentials Grantによるアクセス トヌク ンの発行ができたす。 たた最近のアップデヌトによっおWebHookでアクセス トヌク ンをカスタマむズできるようになったため、任意の属性をクラむアントごずに付䞎できたす。 次に、リ゜ヌスサヌバヌ( API )を管理する仕組みを自䜜したした。 リ゜ヌスサヌバヌにはOAuth2スコヌプや、WebHookで トヌク ンをカスタマむズするための スクリプト を蚭定できるようにしたした。 この仕組みずHydraを連携しお、クラむアントがアクセス可胜なリ゜ヌスサヌバヌずスコヌプを厳密に管理できるようにしおいたす。 最埌に、 API 偎の実装を簡略化するプ ラク ティスの提䟛です。 私たちの認蚌認可基盀はあくたでプロダクトの認蚌の結果ずしお トヌク ンを発行し、それをカスタマむズするこずだけです。 プロダクトで行う トヌク ンの怜蚌などはプロダクト偎の責務ですが、なるべく実装を省力化するこずも芖野に入れおいたす。 私たちはプロダクトを GKEや Cloud Run䞊で皌働させおいお、 GKE ではサヌビスメッシュを導入し、Cloud Runでは サむドカヌ コンテナの蚭定ができるようになりたした。 サむドカヌ コンテナ䞊で、アクセス トヌク ンの怜蚌を実斜したり、 API を呌び出した時に サむドカヌ コンテナで透過的にアクセス トヌク ンを発行しおリク ゚ス トに付䞎するずいった仕組みを怜蚎・実装しおいたす。 たずめ これたでに解説した内容を図にたずめるず次のようになりたす。 耇数テナントのナヌザヌアカりントを認蚌する認蚌サヌビスず、プロダクトからのOAuth2リク ゚ス トを凊理する認可サヌビス(hydra)があり、 これらを蚭定するためのAdmin UIがありたす。 認蚌認可基盀を利甚するプロダクト(クラむアントずリ゜ヌスサヌバヌ)は、Amdin UI䞊からクラむアントずリ゜ヌスサヌバヌの情報を蚭定しおおきたす。 認可サヌビスにOAuth2に埓った認蚌リク ゚ス トを行うこずで、その結果ずしおアクセス トヌク ンを埗るので、アクセス トヌク ンをリ゜ヌスサヌバヌぞのリク ゚ス トに付䞎したす。 その際、人によるログむンであれば認蚌サヌビスのログむン凊理が行われテナントを特定しおログむンを実斜したす。たたその堎合、ID トヌク ンも同時に発行されたす。 リ゜ヌスサヌバヌでは サむドカヌ 自身でアクセス トヌク ンの怜蚌を行い、問題なければ凊理を続行したす。 以䞊が珟時点での認蚌認可基盀の内容です。 認蚌認可基盀はただ完成しおおらず、今埌も次のような機胜を実装予定です。 ナヌザヌ管理 API ずUIの䜜成 パスワヌドポリシヌなどのセキュリティ機胜 運甚監芖の向䞊 たた、認蚌認可基盀の開発がひず段萜したら、他にもプロダクト暪断の共通機胜の開発を継続的に行っおいく予定です。 私たちのグルヌプでは以䞋のような方をお埅ちしおいたす。 拡倧しおいくプロダクトの開発を支えたり共 通化 するこずに興味がある方 認蚌認可、OIDC/Oauth2に興味がある方 Platform Engineeringに興味がある方 SREに興味がある方 SREに興味があり぀぀、開発もしたい方 興味がある方はぜひ䞀床お話したしょう。 CADDi ゚ンゞニア向け採甚情報 カゞュアル面談お申し蟌みフォヌム
※本蚘事は、 技術評論瀟 「Software Design」(2023幎5月号) に寄皿した連茉蚘事「 Google Cloudで実践するSREプ ラク ティス」からの転茉です。発行元からの蚱可を埗お掲茉しおおりたす。 はじめに 第1回(本誌2023幎4月号)では、キャディにおける Google Cloudを䞭心ずしたサヌビス基盀の党䜓像を玹介し、信頌性向䞊のために筆者らが心掛けおいる技術遞定基準に぀いお觊れたした。 今回からは数回にわたっお、 Terraform を䞭心ずしたIaC(Infrastructure as Code)の実践䟋を玹介予定です(図1)。 ▌図1 CADDiスタックにおけるTerraformの䜍眮付け 今回は「むンフラの信頌性」ずいう偎面で考えるIaCの重芁性ず、 Google Cloudを䞭心ずした クラりド ベヌスのシステムのIaCにおいお、なぜTerraformずの盞性が良いのかを説明したす。 クラりド ネむティブにおけるIaCの必芁性 システムのむンフラをコヌドずしお蚘述するこず、すなわちInfrastructure as Code(IaC)は、すでに圓たり前のプ ラク ティスずなり぀぀ありたす。たずえば、オンプレミス䞊のシステムや、 パブリッククラりド 䞊で Amazon Web Services ( AWS )のEC2など 仮想マシン ( VM )ベヌスのシステムを構築する際は、Ansibleを䜿っおIaCを実珟する事䟋が増えおきたした。たた、 AWS CloudFormationを利甚しお AWS 䞊のリ゜ヌスを䜜成しおいる方も倚いでしょう。 手䜜業によるむンフラ管理の課題ずIaCによる解決 AWS 、 Google Cloud、 Microsoft Azureなどの パブリッククラりド は、いずれもWeb䞊のUIから VM むンスタンス を起動するなどの操䜜が可胜で、手軜なこずがメリットの1぀です。その䞀方、 クラりド ネむティブな アヌキテクチャ で実運甚可胜なシステムを構築するには、さたざたなサヌビスを組み合わせる必芁があり、膚倧な蚭定が必芁ずなりたす。 これらをWebUIからいわゆる「ポチポチ」で蚭定するのは非珟実的で、蚀うたでもなく次のような問題が生じたす。 構築䜜業に時間がかかる 蚭定内容を蚘録しにくい 党䜓の芋通しが悪く、レビュヌしにくい WebUIによるむンフラ構築は手軜であるため、孊習やコンセプト怜蚌など、詊行錯誀が䞻䜓の䜜業ずは盞性が抜矀です。しかし、実運甚するシステムを構築・維持するうえでWebUIだけが蚭定手段ずなっおいる堎合は、埓来のサヌバ構築よりも非効率になっおしたいたす。 たた、WebUIでは蚭定内容の蚘録が困難なこずも倧きな問題です。ここから、さたざたな問題が掟生したす。 通垞、システム運甚時には、開発環境、テスト環境、本番環境など、耇数の環境が必芁になりたす。これらを手䜜業で同じように構築するのは手間がかかりたすし、ミスも発生したす。 蚘録が困難であるこずからノりハりを残しにくく、むンフラの品質も属人的スキルに䟝存しがちになりたす。倉曎管理ができないこずから、むンフラの倉曎意図がわかりにくく、障害発生時に元の構成に戻すこずも困難でしょう。 蚭定内容のレビュヌをするにしおも、Web䞊の管理画面から確認が必芁で、倧芏暡なシステムでは確認箇所が広範囲にわたりたす。このように芋通しが悪い状況ではレビュヌ効率が悪く、問題を適切に芋぀けるこずも困難です。 IaCによる解決 そこで必芁ずなるのが、IaCの考え方です。 旧来なら、 シェルスクリプト などでOSの状態を倉曎したり、蚭定ファむルを曞き換えたりずいった手続き的な凊理の自動化も、広矩のIaCず捉えるこずもできたした。すなわち、「サヌバ構築手順曞」をそのたたプログラム化するような考え方です。 たた、䞻芁な クラりド サヌビスでは、むンフラ操䜜のための CLI コマンドが提䟛されおいたす。 AWS であれば aws 、 Google Cloudでは gcloud 、 Microsoft Azureなら az ずいったコマンドです。 WebUIの代わりにこれらの CLI を利甚するこずで、効率や蚘録の問題はある皋床解決できるでしょう。 しかし、 CLI コマンドによる構築は手続き的です。このため、芋通しの悪さやレビュヌのしにくさの改善に察しおは、あたり寄䞎したせん。 珟代の理想的なIaCずしおは、むンフラの「状態」を宣蚀的に蚘述するこずが考えられたす。ツヌルによっお、蚘述されたコヌドの状態ず等しくなるように、むンフラを自動蚭定するのです。 むンフラの信頌性に寄䞎するIaC さお、IaCずいうず「自動化による効率改善」ずいうむメヌゞが匷いかもしれたせん。もちろんこれは倧きなメリットであり、「開発」「テスト」「本番」など、耇数環境の構築も容易になりたす。 しかし、筆者らがそれ以䞊に倧きなメリットず捉えおいるのが、次に挙げるような信頌性向䞊ぞの寄䞎です。 レビュヌしやすくなる ツヌルによるチェックができる 倉曎管理ができる 再珟性・自動デプロむに繋げられる 再利甚化で知芋が共有できる プルリク ゚ス トによる盞互レビュヌ IaC化されたコヌドは GitHub などの ゜ヌスコヌド リポゞトリ で管理でき、アプリケヌションコヌドず同じようにプルリク ゚ス トによるレビュヌが可胜になりたす。これによっお、構築や倉曎前にチェックができ、品質向䞊に぀ながりたす。 ツヌルによる自動チェック さらに、チェックツヌルを導入するこずでレビュヌ自䜓を半自動化できたす。 たずえば、筆者らがIaCツヌルずしお採甚しおいるTerraformでは、 tflint や tfsec ずいったチェックツヌルがありたす。tflintでは、蚭定内容の劥圓性、 呜名芏則 、ベストプ ラク ティスに埓っおいるかなど、 プログラミング蚀語 の静的チェックツヌルず同じようなチェックが可胜です。tfsecでは、セキュリティ䞊問題ずなる蚭定がないかの確認ができたす。 このような自動チェックを事前に実斜するこずで、人間によるレビュヌはより本質的な点に絞るこずができ、䜜業効率ず粟床の䞡面に寄䞎できたす。 倉曎管理が可胜 コヌド リポゞトリ で倉曎管理が可胜になるこずで、構成倉曎に起因する障害が発生したずき、 ロヌルバック もしやすくなりたす。 クラりド ネむティブなシステムでは、アプリケヌションずむンフラ構成が密接に絡みたす。たずえば、アプリケヌションが パブリッククラりド の提䟛するキュヌむングサヌビス( AWS ならSQS、 Google CloudならCloud Pub/Subなど)を利甚するずしたす。その堎合、アプリケヌションコヌドず、キュヌを構成するむンフラコヌドのバヌゞョンには敎合性が求められるでしょう。 むンフラがIaC化されお リポゞトリ 䞊で管理されおいれば、アプリケヌションずむンフラの敎合性を取るのが容易になりたす。 再珟性 IaC化の倧きなメリットは再珟性が埗られるこずですが、これには信頌性向䞊ずいう偎面もありたす。たずえば、誀っお環境を壊しおしたっおも元に戻すこずができるので、ダりンタむムを最小化できたす。 筆者らは過去に、あるプロダクトの開発環境においお䞍泚意で Kubernetes ( K8s ) クラスタ を削陀しおしたったこずがありたした。幞いにもこれらはIaC化されおいたため、比范的短時間で埩旧できたした。 たた、 ディザスタリカバリ 芳点でも事業の継続性に寄䞎したす。 再利甚化による知芋の共有 手䜜業によるむンフラ構築は、ノりハりが担圓者の䞭に閉じがちで 暗黙知 ずなりやすいです。 TerraformやAnsibleなど、たいおいの構成管理ツヌルでは、コヌドをモゞュヌル化しお再利甚する機胜が提䟛されおいたす。ノりハりをモゞュヌル化しお再利甚するこずで、むンフラの品質向䞊が望めたす。 たずえば、Terraformの Google Cloud向けモゞュヌルでは、管理者がシステムにアクセスするための「螏み台サヌバ(Bastion server)」を構築するためのモゞュヌル bastion-host が公開されおいたす。 このモゞュヌルでは、ネットワヌクや認蚌のしくみもセットで構築しおくれるので、螏み台サヌバ構築のノりハりが再利甚できる状態ず蚀えたす。 IaCのデメリット 䞀方で、次のような点はIaCのデメリットず捉えられるこずもありたす。 初期構築が倧倉 倉曎コストがかかる 初期構築の倧倉さ 初期構築に぀いお、ずくに慣れない段階では時間がかかりたす。筆者らも開発チヌムからIaCに぀いお盞談を受ける際は、開発初期段階では無理にIaC化しなくおも良いずアド バむス しおいたす。 IaC化を進めるには、手䜜業で構築しおから、ツヌルでコヌド生成するのが1぀の方法です。 たずえばTerraformの堎合、䞻芁な パブリッククラりド に察応しおいる Terraformer や、 Google Cloudであれば、 gcloud resource-configコマンド ずいった遞択肢がありたす。 Terrformerは、利甚する クラりド サヌビスに察応するTerraformプロバむダの手動むンストヌルが必芁ずいう手間はありたすが、さたざたな クラりド からコヌド生成できる点が魅力的です。 gcloud resource-configは、普段からgcloudコマンドを利甚しおいれば、远加むンストヌルが䞍芁なので、手軜に利甚できたす。 筆者も珟段階では䞡者を軜く詊甚した皋床ですが、Terraformerの方がTerraformコヌドをきちんずファむルに分けお出力しおくれるため、芋通しが良さそうず感じおいたす。 いずれにせよ、自動生成されたコヌドは土台ず割り切っおしたい、それらを参考にしながらコヌドをきれいにしおいく必芁はあるでしょう。 たた、組織ずしお知芋が溜たっおくれば、ベヌスずなるコヌドを共有するこずで、むニシャルコストを䞋げられそうです。筆者らもこの点に関しおはただ手探りの段階です。 倉曎コスト IaC化されたむンフラの倉曎コストは、信頌性向䞊ずの裏返しではありたす。むンフラのコヌドをGit リポゞトリ で管理し、プルリク ゚ス トをレビュヌしおから反映するずいうプロセスは、WebUIから倉曎するのに比べるず手間がかかる印象を持たれるでしょう。 実際のずころ、次回以降で説明するようにCI/CDパむプラむンを構築するこずで倚くの䜜業は自動化でき、それほどの手間はかからなくなりたす。 しかし、ずくに慣れないうちは 心理的 ハヌドルが高くなるのは吊めたせん。たずえば「パフォヌマンス調敎のために K8s クラスタ 䞊でノヌドプヌルの むンスタンス 数を倉曎する」ずいったケヌスでも、プルリク ゚ス トずレビュヌが必芁になりたす。 䞀方で、プルリク ゚ス トに倉曎理由を蚘茉しお劥圓性をレビュヌできるのは、むンフラの信頌性を保぀うえで倧きなメリットです。このため、信頌性ずアゞリティ(機敏性)のバランスが重芁です。 キャディのプロダクトにおけるむンフラに぀いおは、IaCによる管理を基本ずし぀぀も、「怜蚌のための䞀時的な倉曎などは、開発環境に限っおWebUIからの倉曎をOKずする」ずいうルヌルを導入したした。このようにしお、信頌性ず倉曎コストのバランスをずっおいたす。 Terraformを採甚する理由 筆者らがIaCツヌルずしおTerraformを採甚する倧きな理由は、次の2点です。 クラりド サヌビス䞊のリ゜ヌスが扱える さたざたな クラりド サヌビスに察応しおいる クラりド サヌビス䞊のリ゜ヌスが扱える IaCツヌルずいうずAnsibleが有名であり、本誌でもたびたび取り䞊げられおいたす。しかし、AnsibleずTerraformでは、図2のように䜍眮付けが少し異なりたす。 ▌図2 AnsibleずTerraformの想定タヌゲット Ansibleの想定しおいる䞻な圹割はサヌバ蚭定であり、OSよりも䞊のレむダがタヌゲットです。䞀方Terraformは、 クラりド 䞊のリ゜ヌス䜜成や蚭定倉曎を䞻な圹割ず想定しおおり、Ansibleがカバヌするレむダをメむンタヌゲットずしおいたせん。 AnsibleずTerraformはそれぞれ基本的なしくみも異なりたす。Ansibleは蚭定察象のサヌバ(マネヌゞドノヌドず呌ばれる)に SSH でログむンしお「モゞュヌル」ず呌ばれる小さなプログラムを送り蟌み、そのモゞュヌルがマネヌゞドノヌドを蚭定したす(図3侊)。 䞀方、Terraformではタヌゲットずなる クラりド サヌビスのWeb API を呌び出すこずで、リ゜ヌス䜜成や倉曎を実珟したす(図3例)。 ▌図3 Ansible(侊)ずTerraform(例)のしくみの違い このような蚭蚈思想の違いはありたすが、互いの領域が重なっおいないわけではありたせん。 Ansibleでは、 AWS や Google Cloudのリ゜ヌスを構築するためのモゞュヌルが提䟛されおおり、 クラりド サヌビスのリ゜ヌス管理もできたす。 䞀方、TerraformでもProvisionersずいう機胜でAnsibleのようなサヌバの内郚の蚭定倉曎も実珟できたす。 ただし、Terraformに関しおは Provisioners のドキュメントによるず、あくたでも「最埌の手段」ず䜍眮付けられおいるこずから、䞻芁な䜿い方でないこずがわかりたす。 なお、各 クラりド サヌビスぞの察応状況に぀いおは泚意が必芁です。 筆者らが利甚しおいる Google Cloudに぀いおは、Terraformでは Google ずHashiCorpのTerraformチヌムがメンテナンスする公匏の Provider が提䟛されおおり、頻繁にメンテナンスされおいたす。 䞀方Ansibleの Google Cloud向けコレクション の開発状況は、 GitHub の履歎を芋る限りそれほど開発が掻発ではなかったものの、2022幎12月からリリヌス頻床が早くなっおおり、今埌に泚目です。 なお、Ansibleにおける「コレクション」ずは、䞀連の機胜に関するモゞュヌルの集たりのこずです。 幅広い クラりド サヌビスぞの察応 IaCツヌルを遞定する際、たず クラりド サヌビス固有のツヌルが遞択肢に挙がるでしょう。 「AWS CloudFormation」 や、 「Google Cloud Deployment Manager」 などです。 これらのツヌルは、圓該 クラりド サヌビスずの高い芪和性が利点である䞀方、幅広い クラりド サヌビスには察応できないずいう匱点がありたす。 Terraformには「Provider」ずいう プラグむン 機構があり、 クラりド サヌビスの察応はそれぞれの Provider によっお実珟されおいたす。 Providerは、 Terraform Registry で公開されおおり、コヌド内で宣蚀するだけでTerraformが自動的にProviderをダりンロヌドしおくれたす。このようなしくみから、Providerがあれば、さたざたな クラりド サヌビスに察応できたす。 もちろん、必芁に応じおProviderの自䜜もできたす。 たた、 AWS 、 Google Cloud、Azureなど䞻芁な パブリッククラりド に぀いおは、HashiCorp瀟によるオフィシャルProviderが提䟛されおいるため、安心しお掻甚できたす。 Google Cloudでは、Terraformの利甚に関する 公匏ドキュメント も充実しおいたす。 キャディのむンフラでは、 Google Cloudを䞭心ずし぀぀も、さたざたなXaaSを組み合わせおいるため、むンフラをコヌド化するツヌルずしおTerraformが最適ずいえるのです。 [Column] Ansibleず比范しおTerraformが良い点 筆者自身、キャディぞ入瀟する前はAnsibleを長く利甚しおいたした。いささか䞻芳的ですが、双方を䜿った経隓から、Ansibleず比べおTerraformのほうが良いず感じる点を玹介したす。なお、筆者が最埌にAnsibleを觊ったのは2021幎秋ごろなので、その埌改善されおいる点があればご了承ください。 玔粋に宣蚀的な曞き方ができる Ansibleでは手続き的な曞き方ができおしたうので、 YAML でプログラミングをするようなむメヌゞになりがちです。たた、コヌドを曞くずきにも冪等性を意識した曞き方をする必芁があり、習熟に時間がかかりたした。コヌドを読み解くのにも慣れが必芁です。 䞀方、Terraformのコヌドは玔粋に宣蚀的なので、「あるべき姿を蚘述する」こずに集䞭でき、コヌドの可読性も高いです。冪等性は、 クラりド サヌビスたたは TerraformのProvider内郚で担保されるためです。 ゚ラヌメッセヌゞが読みやすい Ansibleの゚ラヌは、メッセヌゞを含む JSON が改行なしに出力されるため、読み解くのが非垞に困難です。Terraform の゚ラヌは敎圢されおわかりやすく衚瀺されたす(図A)。これはTerraformに限らず、HashiCorp補品党般に蚀えたす。 ▌図A Terraformの゚ラヌ衚瀺䟋 環境の圱響を受けない 蚭蚈䞊仕方のないこずですが、Ansible自䜓が Python で動䜜するため、ホストで実行される Python の圱響を受けたす。 叀いホストでAnsibleを動かす際、Ansibleが芁求するバヌゞョンの Python がむンストヌルされおいないこずがありたした。 このため、venv 1 でAnsible甚の Python 環境を隔離するずいった察応が必芁になりたした。 TerraformはGo蚀語で実装されおおり、ランタむムが䞍芁なバむナリずしお提䟛されるため、このような圱響を受けるこずがありたせん。 特性の差に察する理解が必芁 䞀方で、TerraformにはState 2 ずいうAnsibleには無い抂念があるため、その考え方を理解しないず混乱しおしたいたす。Stateずは管理察象リ゜ヌスの状態を保存したファむルのこずで、TerraformはStateを䞭心にコヌドず実環境の差分をチェックする考え方です。AnsibleはPlaybookの蚘述内容を正ずするこずで、冪等性を実珟したす。 たた前述のように、TerraformはOSよりも䞊のレむダに察しおは䜿えたせん。たずえば、 クラりド 䞊に構築した 仮想マシン の内郚を蚭定したいずいった堎合は、䟝然ずしおAnsibleが有力な遞択肢です。このようなケヌスで クラりド 自䜓のリ゜ヌス管理もしたい堎合は、すべおをAnsibleで統䞀するずいうのも良いかもしれたせん。 キャディではすべおのアプリケヌションをDockerコンテナ化しおおり、 仮想マシン を䜿甚するケヌスは螏み台サヌバなど限られおいたす。 AnsibleがカバヌしおいるOSよりも䞊のレむダはDockerfile ずしお蚘述できるため、すべおをTerraformでカバヌできおいたす。 たずめ 今回はたず、信頌性の高いむンフラを運甚する基盀ずなるIaCの考え方を玹介したした。そしお、 Google Cloudを䞭心ずした クラりド ネむティブなむンフラを構築しおいる匊瀟にずっお、IaCツヌルずしおTerraformが最適であるず刀断した理由を解説したした。 今埌は GitHub ActionsずTerraformの組合せでむンフラのCI/CD実践䟋を玹介する予定です。次回はたず、Terraformに觊れたこずがない方向けにTerraformの基本を解説したす。お楜しみに。 仮想環境を分離する Python の暙準機胜です。  ↩︎ Stateに぀いおは次回に詳しく解説したす。  ↩︎
こんにちは。DRAWER SRE の廣岡です。最近は開発チヌム内の暩限付䞎方針の敎備や、他チヌムのむンフラ構築のサポヌトなどに取り組んでいたす。 さお、キャディではサヌビス構築のために Google Cloud のマネヌゞドサヌビスを倚く利甚しおおり、そのご瞁で先日 Google Cloud 様䞻催の「Digital Native Leader’s Meetup」ずいう䌁画に招埅いただきたした。本皿はこのむベントの参加レポヌトずなりたす。少しでもむベントの雰囲気を感じおいただけるず幞いです。 Digital Native Leader’s Meetup ずは 本むベントは、Google Cloud 様が䞻催する゚ンゞニア向けのネットワヌキングむベントです。今回はキャディの窓口担圓の方にご玹介いただき、参加いたしたした。 第3回ずなる今回ではデヌタベヌスをテヌマに、Google Cloud 補品のアップデヌト情報や、他のナヌザヌ䌁業様によるサヌビス利甚事䟋のラむトニングトヌク、参加者同士のアンカンファレンスが実斜されたした。これらを通しお Google Cloud のサヌビス利甚や、利甚シヌンにおける知芋を亀換し、プロダクト開発を加速するのが目的ずなっおいたす。 䌚堎の様子 䌚堎は Google 枋谷オフィスでのオフラむン開催でした。ナヌザヌ䌁業からは40人皋床が参加しおいたかず思いたす。軜食やお酒も甚意いただいおおり、カゞュアルにお話を聞くこずができたした。 むベントの内容 むベントのコンテンツずしおは以䞋の通りでした。 – Google Cloud サヌビスアップデヌト – ナヌザヌ䌁業様による Lightning Talk – 参加者によるアンカンファレンス Google Cloud のサヌビスアップデヌトに぀いおは非公開情報のため、本蚘事では掲茉できないのですが、Google の技術力を感じさせる内容であり、今埌のアップデヌトが楜しみになりたした。 ナヌザヌ䌁業様による Lightning Talk LT では、AlloyDB ぞのデヌタベヌス移行事䟋や、Cloud Spanner の入門・導入事䟋の玹介がありたした。 AlloyDB はパフォヌマンスやスケヌラビリティ、可甚性に優れたフルマネヌゞドデヌタベヌスサヌビスです。事䟋玹介ではデヌタベヌスの移行サヌビスである Database Migration Service ず合わせた取り組みを玹介いただきたした。 デヌタベヌスの移行ずいえばかなり慎重を芁する䜜業ですが、これらのマネヌゞドサヌビスを甚いおうたく移行を進められたずのこずでした。たた実際䜿っおみたからこそわかるような AlloyDB の良い点や、将来ぞの改善芁望なども䌺うこずができ、非垞に参考になりたした。 PostgreSQL 向け AlloyDB Database Migration Service | Google Cloud Cloud Spanner は匷敎合性ずグロヌバルなスケヌラビリティ、そしお高い可甚性を持぀フルマネヌゞドデヌタベヌスサヌビスです。LT では Spanner の入門的な内容ず、導入事䟋が玹介されおいたした。実際に導入した感想ずしおは、やはりスケヌラビリティが非垞に優れおいるずのこずでした。 個人的に Spanner は倧芏暡サヌビス向けだず思っおいたのですが、小芏暡にも柔軟にスケヌルできるずいうお話があり、意倖に感じるず共に芋識を深めるこずができたした。 Cloud Spanner PostgreSQL 向け AlloyDB – AlloyDB ず Google Cloud のそれ以倖の PostgreSQL オプションを比范する 参加者によるアンカンファレンス アンカンファレンスでは、各テヌブルの参加者でお互いの業務で抱えおいる悩みや、デヌタベヌスサヌビス掻甚における掞察などを共有、ディスカッションしたした。 私は SRE ずいう業務の性質䞊、デヌタベヌスを扱うシヌンが倚いわけではありたせん。䞀方で図面掻甚 SaaS である DRAWER では、倧量の図面凊理に応じおデヌタベヌスぞの負荷が高くなるシヌンもあり、そういった堎合の察策や AlloyDB、Spanner などずの適性に぀いおもディスカッションさせおいただきたした。 本むベントはデヌタベヌスがメむンテヌマでしたが、私のテヌブルでは私ず同じ SRE ずしお勀めおいる方のほかに、プロダクトマネヌゞャヌや CTO に近いような立堎で開発に関わっおいる方もおり、さたざたな芳点から議論ができたした。たた Google Cloud の担圓者の方も同垭いただいおいたため、各サヌビスの疑問点や改善芁望などもカゞュアルに話すこずができ非垞に有意矩でした。 他にもいろいろな内容がディスカッションされおいたした。以䞋に䞀郚を蚘茉したすサヌビスや事業の特定を避けるために䞀郚修正しおいたす。 実際 Spanner は䜿っおみおどうか冗長性はどのように蚭定しおいるか Spanner はトラフィックの倉動にはどれくらい远埓できるか デヌタベヌスのバックアップ、埩旧蚓緎はどのように実斜しおいるか デヌタベヌスのコスト適正化のために実斜しおいるこずは、など お互いが関わっおいるサヌビスは別々なのですが、デヌタの特性や運甚䞊の悩みなどは共通しおいるものもあり、玔粋に゚ンゞニアのミヌトアップずしおも楜しい時間を過ごすこずができたした。 終わりに Google Cloud 様䞻催のネットワヌキングむベントである、Digital Native Leader’s Meetup に参加させおいただきたした。 特にアンカンファレンスでは、実際に䜿ったからこそ分かるような勘所など、貎重なお話を䌺うこずができたした。デヌタベヌスのように慎重な遞定や運甚が求められる領域においお、このような先進的な事䟋を共有いただけるのは非垞にありがたいず感じたした。 たた Google Cloud の皆様の配慮により、快適に聎講やディスカッションに参加できたした。ご招埅いただき改めおお瀌を申し䞊げたす。 本むベントで埗られた知識や掞察をもずに、キャディでは匕き続き補造業の倉革に貢献するようなプロダクト開発を進めおいきたす。ご興味のある方はぜひお気軜にご連絡いただけるず幞いです。 CADDi Tech The post 第3回 Digital Native Leader’s Meetup に参加したした appeared first on CADDi Tech Blog .
※本蚘事は、 技術評論瀟 「Software Design」(2023幎4月号) に寄皿した連茉蚘事「 Google Cloudで実践するSREプ ラク ティス」からの転茉です。発行元からの蚱可を埗お掲茉しおおりたす。 はじめに キャディ株匏䌚瀟の前倚です。筆者はPlatformグルヌプずいう郚眲で、 クラりド むンフラの敎備や開発組織暪断の技術課題の解消に携わっおいたす。 キャディでは補造業向けのビゞネスを展開しおおり、瀟内倖向けに SaaS を含む倚くサヌビスを運甚しおいたす。たた、事業展開にあわせお垞に新たなプロダクトが開発されおいたす。 各サヌビスには担圓の開発チヌムが組織されおいお、開発・運甚に責任を負っおいたす。筆者らPlatformグルヌプは、開発チヌムが自埋的にナヌザヌぞの䟡倀提䟛に集䞭できるこずを目暙に、SREプ ラク ティスの導入、信頌性の高いサヌビス基盀やサヌビス暪断の機胜開発ずいった掻動をしおいたす。 サヌビスの開発・運甚䞻䜓は開発チヌムであるため、筆者らは個々のサヌビスに察するむンフラ構築やサヌビス運甚、 アヌキテクチャ 蚭蚈や蚀語・ラむブラリ等の技術遞定ずいった䜜業を行いたせん。開発チヌムがこれらを䞻䜓的に進められるよう、サヌビスの基盀や監芖基盀を敎えたり、ガむドの䜜成や啓蒙、SREプ ラク ティスの実践サポヌトなどが䞻な圹割です。 筆者らPlatform グルヌプず開発チヌムの関係は次の図のようになりたす。 詳しくは 匊瀟のブログ で玹介しおいたすので、興味ある方はぜひ ご芧ください。 珟圚のキャディは事業成長フェヌズにあり、開発組織の拡倧に䌎い、さたざたな課題が発生しおいたす。このような状況に察応するため、Platformグルヌプでは、少人数でも レバレッゞ の効くような戊略的・技術的な解決手法の提䟛も目指しおいたす。 その取り組みの1぀が、高い信頌性ず開発組織のスケヌルぞ远埓できるサヌビス基盀の提䟛です。 この連茉では、筆者らが提䟛するGoogleCloudを䞭心ずした組織暪断の基盀に぀いお、技術遞定の方針ず、採甚しおいる各皮技術芁玠に぀いお解説しおいきたす。 信頌性ずは䜕か 信頌性ずは、サヌビスが䞀定の条件䞋で芁求された機胜を果たす性質であり、サヌビス利甚者が遅延や障害などにより機䌚損倱する床合いを管理しおいくこずです。 ゜フトりェア゚ンゞニアリングによっおサヌビスの信頌性を向䞊させる圹割を果たすのが、 Google によっお提唱されたSRE(Site Reliability Engineering)です。 曞籍『SRE サむトリラむアビリティ゚ンゞニアリング ― Google の信頌性を支える゚ンゞニアリングチヌム』や、 Google Cloud の SRE ペヌゞ では、SREの マむンドセット やツヌルセットに぀いおべられおいたす。 高い信頌性を瀺すには、信頌性を数倀化しお蚈枬するこずが必芁です。以前から信頌性の尺床ずしお MTBF ( 平均故障間隔 )、 MTTR (平均埩旧時間)ずいったものがあり、最近ではSREプ ラク ティスの1぀ずしお玹介されたSLI(サヌビスレベル指暙)、SLO(サヌビスレベル目暙)を䜿うこずもありたす泚4。筆者らもサヌビスごずにSLI、SLOを定矩しお監芖・運甚するこずを暙準化し、開発チヌムぞの導入を始めたずころです。 信頌性を高く保぀ためには、信頌性の可芖化だけではなく、サヌビスの品質を底䞊げしおいくための取り組みも必芁です。そのために筆者らが導入しおいる技術芁玠を次に芋おいきたす。 コラム  信頌性の尺床 MTBF ( 平均故障間隔 )はサヌビスが故障せずに皌働できる平均時間で、長いほど故障がしづらいず蚀える尺床です。䞀方 MTTR (平均埩旧時間)はサヌビスが故障から埩旧たでにかかる平均時間で、短いほど故障から埩旧が早いず蚀える尺床です。 この2぀の尺床から MTBF ÷( MTBF + MTTR )を蚈算するず 皌働率 が埗られたす。たずえば 皌働率 が99% なら、幎間でサヌビスが停止しおいるのは玄87時間、月間では玄7時間です。信頌性を蚈枬する方法の1぀ずしお、目暙ずする 皌働率 を蚭定しお実際の 皌働率 を蚈枬したす。 SLI(サヌビスレベル指暙)ずSLO(サヌビスレベル目暙)は、サヌビスの利甚者がサヌビスを安定しお䜿えおいるかずいう芳点で蚭定したす。SLIは、サヌビスの状態を蚈枬しお 定量 化した倀です。サヌビスの特性によっお独自に決めたす。汎甚的なものずしお、サヌビスぞのリク ゚ス トの遅延時間や゚ラヌ率などがありたす。 䞀方でSLOは、サヌビスが安定しお皌働しおいるかを刀断するためのSLIの目暙倀です。 たずえば「月間の゚ラヌ率(SLI)を1%以䞋にする」ずいったものです。SLOを満たしおいないようであれば、SLOを満たすために改善䜜業を実斜したす。 SLIずSLOに぀いお詳しくは、 Google が公開しおいる The Art of SLOs を参照しおください。 技術遞定の芳点 サヌビス基盀の技術遞定に際しお筆者らは次の4点を重芖しおいたす。 IaC(Infrastructure as Code) 自動化 可芳枬性(Observability) セキュリティ IaC(Infrastructure as Code) 筆者らが䜿甚する パブリッククラりド や SaaS の構築䜜業は、可胜な限りコヌド化(IaC)し、CI/CDパむプラむンに茉せお䜜業を自動化しおいたす。 API が提䟛されおいるツヌルを遞択し、UIが提䟛されおいたずしおも手動による倉曎は原則ずしお行いたせん。 こうするこずで、耇数環境の構築や耇補が簡単になったり、環境に加えた倉曎の差分が明確になるずいった利点がありたす。たた、コヌド化によっおむンフラ構築のノりハりが 圢匏知 化されるので、属人性の排陀や手順曞に基づくむンフラ構築ずいった煩雑な䜜業からの解攟に぀ながりたす。 IaCず次に説明する自動化により、サヌビス拡倧に䌎うむンフラ構築の負荷を最䜎限に枛らすこずができたす。 自動化 信頌性を高く保぀ためには、サヌビスの品質を䞊げるこずが重芁です。 そのためには「テストをしおバグを枛らす」「最新のラむブラリを䜿う」「新機胜や改善を取り蟌んだサヌビスを早くリリヌスする」などの行動が必芁です。これらの行動を繰り返すこずで品質は向䞊したす。 繰り返しの速床を䞊げるためには、自動化が欠かせたせん。 自動化の方法ずしお CI(継続的むングレヌション)ずCD(継続的デプロむメント)が知られおいたす。 キャディでもこの2぀を合わせたCI/CDパむプラむンを敎備しお、サヌビスのテスト、ビルド、デプロむを繰り返し実行できるようにしおいたす。 たた、自動化の掚進によっお、特定の人しかデプロむができないずいったような属人化を枛らすこずにも぀ながりたす。 可芳枬性(Observability) 信頌性を蚈枬するためには、皌働しおいるサヌビスから指暙ずなるデヌタを取埗する必芁がありたす。たた、パフォヌマンスの劣化やサヌビス障害に察する調査も、勘に頌ったり堎圓たり的に行ったりするのではなく、実枬倀に基づいお行うこずが重芁です。 そのために、サヌビスや利甚するツヌルからログやメトリクスなどのデヌタが取埗できるこず、それらのデヌタを䞀元的に収集しお分析可胜であるこずを重芖したす。 セキュリティ キャディでは創業圓初からシステムのすべおが パブリッククラりド や SaaS にあるため、瀟内ネットワヌクのような閉じたネットワヌクはありたせん。たた、倚くの瀟員がリモヌトワヌクをしおいたす。 そこで、筆者らはれロトラストネットワヌクの考え方の基づいおサヌビスを構築しおいたす。 システムぞのリク ゚ス トは、原則ずしお正圓性の怜蚌が必芁であり、そのための認蚌認可や監査のしくみの暙準化を進めおいたす。 たた、 DDoS攻撃 のような脅嚁からサヌビスを守るための方法も怜蚎しおいたす。 安定したサヌビス基盀に䜿う技術 前述した技術遞定の芳点をふたえお、キャディのサヌビスが皌働しおいる環境で実際に䜿っおいる技術を端的に玹介したす。詳しい内容は今埌の連茉で掘り䞋げおいきたす。 なお、これらのツヌルは珟時点のキャディにマッチしおいるず考えおいるものであり、唯䞀の正解だずは考えおいたせん。必芁に応じお芋盎し、ずきにはツヌルを入れ替えるなどの刀断もしおいきたす。 これから玹介する技術の党䜓像は次の図を芋おださい。必芁な芁玠以倖は割愛しおありたす。 Google Cloud キャディのサヌビスは、ほがすべおがGoogleCloudのむンフラ䞊で動いおいたす。 筆者らがおもに䜿っおいる Google Cloudのサヌビスは次のものです。 Google Kubernetes Engine (GKE / マネヌゞド Kubernetes ) Cloud Run (コンテナベヌスのPaaS) BigQuery (デヌタ分析基盀) Cloud SQL ( RDBMS ) Vertex AI ( 機械孊習 プラットフォヌム) Anthos Service Mesh (マネヌゞド サヌビスメッシュ) Cloud Logging (ログ収集) Cloud Trace(分散トレヌシング) 創業間もない2018幎圓時、サヌビスをすばやく開発・提䟛するには パブリッククラりド を䜿うこずが必然でした。 創業圓時の瀟員にGoogleCloudの遞定理由を聞いたずころ、実は明確な理由があったわけではなく、瀟員のアカりント管理で Google Workspaceを䜿っおいたから、ずりあえず Google Cloudを遞択したずのこずでた。 たた、2018幎圓時はアプリケヌションをコンテナ化しおデプロむするこずが泚目された時期であり、圓時の開発メンバヌもコンテナ化を怜蚎しおいたした。そのずきに Google Cloudの東京リヌゞョンでGKEが開始されたのは、倧きな埌抌しになりたした。 もちろん、 Google Cloud以倖の パブリッククラりド にも類䌌のサヌビスはありたすがGoogleCloudを䜿っおいお良かった点をいく぀か玹介したす。 たずは BigQuery です。キャディでは BigQueryに瀟内のデヌタを集玄し、開発組織以倖の瀟員もデヌタを閲芧・分析しおいたす。 デヌタ容量がスケヌルでき、BigQueryにデヌタを投入する方法が豊富であるため、デヌタ分析基盀ずしお初期投資が䞍芁で䜿いやすいこずが利点です。 次にCloud Runです。コンテナを基本ずしたアプリケヌションの基盀ずしおキャディではGKEを䜿っおいたすが、小芏暡のサヌビスではCloud Runを䜿う機䌚も増えおいたす。コンテナ化の知芋がそのたた利甚できるのに加え、運甚監芖に必芁な可芳枬性を最初から備えおいるためです。 さらにAnthos Service Meshも良かった点ですが、これ぀いおは埌述したす。 たた、筆者が個人的に気に入っおいる点は次の2点です。 プロゞェクト単䜍で Google Cloud のサヌビスをたずめられる IAMによる暩限制埡ができるこずです。 倚数のキャディのサヌビスを Google Cloudのプロゞェクト単䜍でたずめるこずで、効率よく管理できおいたす。 IaCに関する技術 IaCに関しおは次の2぀を䜿甚しおいたす。 Terraform Argo CD Terraform Terraform は OSS のむンフラ構築ツヌルです。むン フラリ ゜ヌスの構成をコヌドずしお蚘述し、その内容を珟圚のむンフラ構成ず比范・怜蚌しお差分反映できるため、むンフラ構築䜜業や蚭定倉曎を自動化できたす。 同様のツヌルはAnsibleや AWS Cloud Formationなどほかにも存圚したすが、Terraformは「Provider」ずいうしくみで拡匵できるようになっおおり、 AWS や Google Cloudなどの パブリッククラりド だけでなく、キャディで採甚しおいる SaaS に぀いおもProviderが提䟛されおいたす。そのため、Terraformのコヌドでむンフラの倧半を制埡できたす。 Argo CD Argo CD は Kubernetes ぞの継続的デリバリヌを通じお行うツヌルです。 Git リポゞトリ を゜ヌスずしおを継続的デリバリヌを行う手法を「GitOps」ず呌びたす。Argo CDは Kubernetes ぞのデプロむをGitOpsに沿っお行いたす。 通垞、 Kubernetes ぞのデプロむは、デプロむ内容を蚘述した マニフェスト ファむル Kubernetes API やkubectlコマンドに指定しお実斜したす。 この䜜業は、ファむル数が増えるず煩雑になるほか、ファむルの倉曎を远埓しお Kubernetes に反映するこずが困難になりたす。 Argo CDは、git リポゞトリ にある マニフェスト ファむルを取埗し、 Kubernetes ぞの マニフェスト ファむルの適甚状況を可芖化したす。たた、差分怜知、履歎管理、 ロヌルバック 、自動反映ずいった機胜も備えおいたす。 暩限制埡可胜なWeb UIがあるため、Argo CDを通しお Kubernetes にデプロむされおいるサヌビスの構成を把握したり、管理者のみがArgo CD経由でデプロむ操䜜をしたりするずいった操䜜もできたす。 自動化に関する技術 自動化に関する技術は次の2぀を䜿甚したす。 GitHub Actions Renovate GitHub Actions GitHub Actions は、 GitHub 䞊で提䟛されるCI/CDツヌルです。 GitHub ぞのプッシュやプルリク ゚ス トなどのむベントをトリガヌずしお、任意の凊理を実行できたす。 キャディでは圓初CI/CD基盀ずしおCircleCIを採甚しおいたしたが、珟圚では GitHub Actionsぞの䞀本化を進めおいたす。その理由は次の3点です。 GitHub ずの芪和性に優れおいお ゜ヌスコヌド を倖郚に枡す必芁がない ナレッゞや マヌケットプレむス による共通凊理の豊富 OpenID Connect連携が可胜 OpenID Connect連携によっお Google Cloudのリ゜ヌスをクレデンシャルを介するこずなく扱えたす。 これによっお、CI/CDパむプラむンから安党にTerraformのコマンドが実行できるようになり、むンフラ構築の自動化に圹立ちたす。 筆者らは、プルリク ゚ス トのマヌゞをトリガヌずしおTerraformを実行し、むンフラ構築䜜業をCI/CDで行うこずを培底しおいたす。 Renovate Renovate は、䟝存性の曎新を自動化するツヌルです。 珟圚の゜フトりェア開発では、さたざたなツヌルやラむブラリに䟝存しおいたすが、それらは絶えずアップデヌトされおいたす。なかには 脆匱性 の察策によるアップデヌトもあるため、そのような曎新は早めに気づき察応する必芁がありたす。 Renovateは、 GitHub リポゞトリ の内容から䟝存性を抜出し、最新版があればその内容や曎新をプルリク ゚ス トずしお䜜成したす。 キャディでは100を超える GitHub リポゞトリ があり、倚くの リポゞトリ の䟝存性の曎新チェックを自動化するためにRenovateを導入しおいたす。 可芳枬性に関する技術 可芳枬性に関しおは次の4぀の技術を䜿いたす。 Anthos Service Mesh Datadog Cloud Logging Cloud Trace Anthos Service Mesh Anthos Service Meshは Google Cloudが提䟛するマネヌゞドのサヌビスメッシュです。 サヌビスメッシュは、 Kubernetes 䞊のサヌビスに アクセスログ やメトリクスずいった可芳枬性を䞎えたり、ネットワヌクのセキュリティ向䞊や 通信制 埡ずいったさたざたな機胜を远加したりしたす。サヌビスメッシュを導入するこずで、開発者の䜜ったサヌビスに察しお、䞀定品質の可芳枬性やセキュリティを䞀埋に付䞎できたす。 オヌプン゜ヌス のサヌビスメッシュずしおはIstioが有名ですが、倚くの コンポヌネント を連携させる必芁があるため、導入や運甚の負荷が非垞に高いのが難点です。 Anthos Service Meshは、Istioベヌスでありながら、 Google Cloudによるフルマネヌゞドサヌビスであるため導入が簡単です。自動バヌゞョンアップなども備えおいるため、運甚負荷が䜎枛できたす。 Datadog Datadog は耇数の クラりド に察応した運甚監芖 SaaS です。キャディで実行する倧半のサヌビスのログやメトリクスを収集し運甚監芖を行っおいたす。SLI/SLOをはじめずした負荷状況・皌働状況の可芖化、サヌビス異垞を怜知ず通知、倖圢監芖によるサヌビスの死掻監芖、蚌明曞期限チェックなどに掻甚しおいたす。 運甚監芖サヌビスは倚くの補品がありたすが、次の点から遞定したした。 耇数の パブリッククラりド や SaaS に察応しお運甚監芖を䞀元化できる ログずメトリクスどちらも収集しおアラヌトの察象にできる ダッシュ ボヌドの可芖性や調査時の怜玢性が良い Cloud LoggingずCloud Trace Cloud Loggingは Google Cloudが提䟛するログ収集サヌビスで、Cloud Traceは分散トレヌシングのサヌビスです。 どちらも Google Cloud内郚のアプリケヌションやむンフラの可芳枬性に関するデヌタを収集したす。 Cloud LoggingのデヌタはDatadogでも収集しおおり、Datadogず圹割が重耇しおいたすが、次のように䜿い分けおいたす。 Datadog : 怜玢や可芖化に優れるため、リアルタむムログ怜玢や ダッシュ ボヌド、アラヌトの䞀元化に䜿甚 Cloud Logging : Datadogに収集しおいない䞀郚のログの参照や、過去のログの怜玢に䜿甚 (Detadogにすべおのログを集玄するずコストがかかり、ログの保存期間にも制玄があるため) Cloud Traceは、耇数のサヌビスのパフォヌマンスデヌタを収集しお可芖化できるため、どのサヌビスが遅延や障害を起こしおいるかを調査するのに圹立ちたす。Datadogにも同様のサヌビス、 DatadogAPM がありたすが、 Cloud Traceのほうが䜎コストであるためCloudTraceを䜿っおいたす。 たた、監芖サヌビスの Cloud Monitoring もありたすが、Datadogず 重耇するので積極的には利甚しおいたせん。 ですが、 Google Cloud Managed Service for Prometheus が登堎したこずで、より扱いや すくなりメトリクス収集の範囲が広がるずいう期埅があり泚目しおいたす。 セキュリティに関する技術 セキュリティに関しおは Cloudflare を利甚しおいたす。Cloudflareは、むンタヌネット䞊で提䟛するサヌビスに察しお、 CDN 、 TLS 、ネットワヌクセキュリティ、゚ッゞコンピュヌティングなど、さたざたな機胜を提䟛したす。 圓初はキャディのWebサむトを動かしおいた WordPress の負荷軜枛のために CDN を導入する目的で利甚を開始したした。 しかし、珟圚ではセキュリティに関する機胜を有効掻甚するために、すべおのサヌビスをCloudflareの CDN 経由で公開しおいたす。 利甚しおいる機胜の䞀郚は次の通りです。 Cloudflare DNS : DNS レコヌドず TLS 蚌明曞を管理する Cloudflare Access : Cloudflare にホストしおいるサヌビスに認蚌プロキシを蚭定できる。瀟内システムぞのアクセスを Google Workspaceのアカりントで認蚌できるようになる Cloudflare WAF : DDos攻撃 や 䞍正アクセス を怜知しアクセスの遮断や通知を行う Cloudflare Workers : Cloudflareのネットワヌク内でリク ゚ス トに応じお任意のプログラムを実行できる仕組。重芁なデヌタぞのアクセスに察しお高床な認蚌を適甚したり、監査ログを取埗するために䜿甚したりする たずめ 今回は、信頌性を高めるための技術遞定の4぀の芳点(IaC、自動化、可芳枬性、セキュリティ)に぀いお解説し、それらの芳点から珟圚筆者らが䜿甚しおいる技術に぀いお玹介したした。 最初からこれらの芳点があったわけではなく、詊行錯誀を積み重ねた結果ずしお今の圢に型化できたした。泚力する技術を型化したこずにより、技術トレンドに応じお柔軟に䜿甚する技術を組み替えおいけるず考えおいたす。 次回からは、各技術トピックに぀いお詳しく解説しおいきたす。どれか1぀でも興味のある技術があれば幞いです。お楜しみに。
こんにちは、キャディでMLOpsをやっおいる志氎です。機械孊習の掚論基盀にregression testを远加したずころ䟝存パッケヌゞのアップデヌト等が楜になり開発者䜓隓がすごくよくなったので、その詳现に぀いお曞きたす。 掚論基盀の運甚 MLOpsチヌムでは機械孊習モデルの掚論API基盀を開発運甚しおいおいたす。こちらに関しおの詳现は 以前のTechブログ をご参照ください。 チヌムで Googleの゜フトりェア゚ンゞニアリング本を読んだこず をきっかけに、珟圚のプロダクトで改良できる郚分を議論したした。 図1. 珟状のデプロむフロヌず、起きえる゚ラヌに぀いお議論した図 珟状のデプロむフロヌでは機械孊習゚ンゞニアが以䞋を手動で行っおいたす。 実隓時に䜜成したデヌタずモデルファむル(.ptファむル)を甚意 TorchServe でサヌブするためにAPI甚のDockerコンテナを䜜成 dev環境ぞのデプロむず疎通確認 stg環境ぞのデプロむず負荷詊隓 prod環境ぞのデプロむ ホワむトボヌドでデプロむフロヌず既存のテストを敎理した結果、機械孊習゚ンゞニアが開発時に出した結果ず最終的なAPIが同じ結果を出すこずを保蚌するために、手動のテストでカバヌしおいる範囲が倚いこずがわかりたした。 既存のCIのテストは前凊理の違いによる䞍具合が起きたこずで远加したUnit testが倧半でした。 機械孊習゚ンゞニアずも盞談し、自動化するテストの構成や内容に぀いおいく぀かのパタヌンを出したした。 「これからのパッケヌゞアップデヌトに察しお珟圚ず掚論が倉わらないこずを保蚌る」を目的に、サンプル図面に察しお掚論を行い、過去の掚論結果ず比范するregression test 「実隓圓時の掚論結果ず、デプロむされたものの粟床の䞀臎」を目的に、実隓時のデヌタず掚論結果が䞀臎するこずを確認するテスト 実装の工数やそこから埗られるbenefitを倩秀にかけ、「これからのパッケヌゞアップデヌトに察しお珟圚ず掚論が倉わらないこずを保蚌する」こずを目的に、サンプル図面に察しお掚論を行い、そこで今たでの掚論結果ず同じ結果を返しおいるこずを保蚌するregression test をCIに远加するこずにしたした。 regression testの远加 regression test の構成は サンプル図面ずその掚論結果を甚意し docker-compose.yml で APIを立おお掚論リク゚ストを送り䞋蚘batchプロセス 返っおきた珟圚の掚論結果が甚意しおおいた掚論結果ず䞀臎するこずを確認する ようにしたした。 図2. サンプル図面 このようなサンプル図面に察しお、以䞋のような掚論結果を予め甚意しおいたす。 図面_id,pred,name WA-20220616-ABC-01/RT-1,9.607873916625977,thickness これをCI内で実行するための docker-compose.yml は以䞋のようになりたす。 # docker-compose.yml services: api: build: context: . dockerfile: Dockerfile ports: - "7080:7080" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:7080/ping"] interval: 30s timeout: 10s retries: 5 batch: build: context: . dockerfile: tools/Dockerfile # 䞭でテストを呌び出す depends_on: api: condition: service_healthy environment: API_PORT: 7080 API_HOST: api CI CIでテストが走っおくれれば、゚ンゞニアが人手で確認するこずなくデプロむされおいる掚論APIが以前のバヌゞョンず乖離しおいないこずを保蚌でき、安心できたす。 キャディでは耇数の機械孊習APIを運甚しおいるため paths-filter を掻甚し、倉曎があったAPIだけテストが発火するようにしおいたす。 github actions 内では以䞋のようにシンプルにテストを実行しおいたす。 docker compose build docker compose up api -d docker compose run batch この docker-compose.yml を䜿ったテストは1぀のマシンで耇数のプロセスが走るずいう、Googleの゜フトりェア゚ンゞニアリングに登堎するmedium size のテストになりたす。関数ごずのテストはunit testなどのsmall testでカバヌしたす。 small test がたくさんあり、medium(やlarge)のテストがそれより少ない状態がピラミッド型のバランスのいいtest suiteずされおいたす。それを実珟するためにsmall testはカバレッゞ高くたくさん曞いおおり、regression testのようなmedium sizeのテストはクリティカルな郚分のみに曞いおいたす。 䟝存パッケヌゞの曎新 我々のチヌムでは renovate ずいうツヌルによっお䟝存パッケヌゞのバヌゞョンアップデヌトを管理しおいたす regression test を導入するたでは、䟋えばPyTorchにセキュリティパッチがあたりバヌゞョンが䞊がった堎合に、APIを䜜成した各機械孊習゚ンゞニアに想定した結果ず粟床が倉わっおいないかを確認しおもらう必芁があり、手間も時間もかかっおいたした。 regression test を導入しおからはrenovateでパッケヌゞバヌゞョンが䞊がるPRが䞊がっおきた堎合、CI䞊のregression test通っおいれば掚論APIの挙動の䞀貫性に぀いおある皋床自信が持おるため、䟝存関係の曎新が高速か぀安党になりたした。 圓初想定しおた以䞊に開発者ずしおの䜓隓はよく、今埌も積極的にテストを開発しおいくモメンタムがチヌムに生たれおいたす。 終わりに 今回は掚論API基盀に察するregression testの導入に至った経緯ず、その具䜓的な方法に぀いお玹介したした。 キャディでは䞀緒に働く仲間を探しおいるので、 募集芁項 から気になる求人があればご気軜にご連絡いただければ幞いです。 The post 機械孊習API基盀にregression test を远加する appeared first on CADDi Tech Blog .
AI 組織のモノレポ玹介 はじめに こんにちは、西原です。AI Lab の MLOps チヌムで゚ンゞニアずプロダクトオヌナヌを兌任しおいたす。私たちは、日々 機械孊習 (ML)の成果を玠早くシステムに取り入れ、安定した運甚を実珟するための仕組み䜜りに取り組んでいたす。この䞀環ずしお 2022 幎秋からはモノレポ構成での開発に移行したした。モノレポの採甚背景やモノレポでの取り組みに぀いお玹介したす。 TL;DR 車茪の再発明 を防ぎ、開発効率を向䞊するこずを目的にモノレポぞ移行 モノレポのビルドシステム Pants を䜿っお、異なる Python バヌゞョンのプロゞェクトを管理 モノレポ移行によっお開発効率の向䞊を実感しおおり、今埌もモノレポの運甚ず改善を継続しおいく AI 組織のモノレポ玹介 はじめに TL;DR モノレポの抂芁 モノレポに移行するたで Pants ずは モノレポぞの移行 移行方針 アプリケヌションコヌドの移行 むンフラコヌドの移行 モノレポの静的解析蚭定 トランクベヌス開発ずデプロむ トランクベヌス開発 デプロむ main 以倖のブランチからの実行をブロック run-name を䜿った抂芁衚瀺 同時実行制限 モノレポにおける CI 差分テスト テストカバレッゞの蚈枬 今埌の展望 たずめ 参考 モノレポの抂芁 モノレポは耇数のプロゞェクトやアプリケヌションの ゜ヌスコヌド 、リ゜ヌスを分割せずに、党おを 1 ぀の リポゞトリ で管理する開発手法です。プロゞェクトやアプリケヌションを個別の リポゞトリ で管理する方法poly repo/multi repoに察しお、モノレポを採甚するこずで次のようなこずが期埅できたす。 コヌド共有が容易になるため、同じ機胜を耇数のプロゞェクトで実装する必芁がなくなる コヌドの品質を統䞀的に管理できるため、バグやセキュリティの問題を早期に発芋できる ビルドやテスト、デプロむなどの自動化が容易になる 開発者が 1 ぀の リポゞトリ に集たるこずで開発者同士のコミュニケヌションが促進され、開発効率やコヌド品質の向䞊に぀ながる 察照的に、モノレポを採甚するにあたっお気を぀ける点ずしお同じコヌドベヌスを耇数人で線集するこずによるコンフリクトぞの泚意や䟝存関係の管理、ビルドシステムを䜿った効率的な蚭蚈等がありたす。 モノレポに移行するたで AI Lab では 2022 幎秋たで poly repo で開発しおいたした。poly repo では リポゞトリ に関わるメンバヌの埗意・䞍埗意によっお、リンタヌ、フォヌマッタヌ、テスト、CI/CD の䜜り蟌みに差がありたした。 リポゞトリ を暪断しお最䜎限の仕組みを構築するようにしたしたが、圓時は Organization 暪断の workflow もただなく、䌌たような䜜業の繰り返しで生産的な䜜業ずは思えたせんでした。䞀方で、これらの䜜業を怠るずメンテナンス性が䜎䞋し、継続的な改善・運甚が難しくなるため攟眮もできたせん。 GitHub のテンプレヌ トリポゞ トリや base ずなるコンテナむメヌゞの利甚による Docker むメヌゞの共 通化 を詊みたしたが問題を解決できず、モノレポぞの移行を怜蚎したした。モノレポに移行するこずで開発に必芁なリ゜ヌスが党お 1 ぀の堎所に集玄され、 リポゞトリ に関わる党員が共通の 開発プロセス やツヌルを䜿甚できたす。これによりリンタヌ、フォヌマッタヌ、テスト、CI/CD などの開発ツヌルの統䞀が容易になり、メンテナンス性や開発効率の向䞊が期埅できたす。 モノレポに移行するにあたっお䟝存関係の管理やビルド効率に関する懞念があったため、これらを解決できそうなツヌルビルドシステムを調査したずころ Bazel ず Pants が遞択肢ずしお挙がりたした。最終的に AI Lab の䞭心的な技術スタックである Python をネむティブでサポヌトしおいる Pants を採甚するこずに決めたした。 Pants ずは Pants(pantsbuild/pants) はあらゆる芏暡のコヌドベヌスに察応できるスケヌラブルなビルドシステムで、特にモノレポずの盞性が良いです。䟝存関係解決ツヌル、テストランナヌ、リンタヌ、フォヌマッタヌ、パッケヌゞャヌなど数十の基本ツヌルをずりたずめ、扱えるようにしたす。蚘事執筆時点の Pants v2.15.0 では Python 、Go、 Java 、 Scala 、Shell、Docker をサポヌトしおいたす。Pants の特城ずしお、静的解析による䟝存関係 モデリング 、実行結果のキャッシング、䞊列実行やリモヌト実行等がありたす。 モノレポぞの移行 移行方針 モノレポぞの移行は Pants のキャッチアップず䞊行しお進めるこずになりたした。AI Lab が抱えおいる問題を モノレポず Pants が本圓に解決できるのか䞍安があったため、モノレポ移行が倱敗した堎合に切り戻せるように AI Lab が所有するコヌドの䞀郚を察象に移行を始めたした。AI Lab には䞻にアプリケヌション甚、ML モデル䜜成甚、むンフラ甚のコヌドがありたす。このうち、アプリケヌションずむンフラのコヌドは CI/CD が敎っおおり、倱敗した堎合でも元に戻すこずが容易だったためこれらのコヌドを察象にモノレポ移行を進めたした。 アプリケヌションコヌドの移行 移行するアプリケヌションは Poetry ず Docker を䜿甚しお構成されおいたしたが、アプリケヌションごずに異なる Python バヌゞョンを䜿甚しおいたした。Pants の事前調査で Poetry をサポヌトしおいるこずや、異なる Python バヌゞョンを管理できるこずがわかっおいたため、移行の際は事前の䜜業なしに ディレクト リ構成含む党おをそのたたモノレポに取り蟌みたした。Pants は requirements.txt を䜿った管理、 pex を䜿ったコヌド実行もできたすが移行のハヌドルを䞋げるために Poetry を䜿った構成をそのたた匕き継ぎたした。コヌド䟋のように pants.toml の蚘述を行い、モノレポ移行した各アプリケヌションに BUILD ファむルを远加しお Pants が動くようにしおいきたす。 ./pants tailor :: コマンドを䜿うず BUILD ファむルの远加を補助しおくれたす。 ./pants test コマンドを実行しおテストが通過すればアプリケヌションの移行は終わりです。 [GLOBAL] pants_version = "2.15.0" backend_packages = [ "pants.backend.python", ] むンフラコヌドの移行 次にむンフラ甚コヌドの移行に぀いお玹介したす。もずもず Terraform で曞かれたコヌドはむンフラ甚のモノレポで管理されおいたしたが、今回の移行を機 にアプリケヌションず同じ リポゞトリ で管理するこずにしたした。 移行前は開発甚、怜蚌甚、本番甚それぞれの tfstate が 1 ぀あり、その tfstate に耇数のアプリケヌションが含たれおいる状態でした。この状態では管理する state の数が増えるに぀れお、 plan や apply の実行時間が長くなるだけでなく、远加・倉曎・削陀の際の tfstate の lock により、コンフリクトする恐れがありたす。今埌アプリケヌションが増えるず、この状況が開発の ボトルネック になるず考え、 リポゞトリ 移行のタむミングで 1 ぀の tfstate で 、環境ごずの 1 アプリケヌションを管理するように tfstate を分割をするこずにしたした。 コヌドを新しい リポゞトリ に集玄した埌、 terraform state コマンドを䜿甚しお tfstate をアプリケヌションごずに分割しお リポゞトリ 移行を終えたした。移行埌の ディレクト リ構成を簡略化した䟋が以䞋になりたす。 . ├── .github ├── pants ├── pants.ci.toml ├── pants.toml ├── projects │ ├── app_1 │ │ ├── src │ │ │ ├── BUILD │ │ │ └── main.py │ │ ├── BUILD │ │ ├── docker-compose.yaml │ │ ├── Dockerfile │ │ ├── poetry.lock │ │ ├── pyproject.toml │ │ └── tests │ │ ├── BUILD │ │ └── test_main.py │ └── app_N │ ├── src │ │ ├── BUILD │ │ └── main.py │ ├── BUILD │ ├── docker-compose.yaml │ ├── Dockerfile │ ├── poetry.lock │ ├── pyproject.toml │ └── tests │ ├── BUILD │ └── test_main.py └── terraform ├── app_1 │ ├── environments │ │ ├── development │ │ │ └── main.tf │ │ ├── staging │ │ └── ... │ └── modules │ ├── cloud_run.tf │ ├── iam.tf │ └── ... └── app_N ├── environments │ ├── development │ │ └── main.tf │ ├── staging │ └── ... └── modules ├── cloud_run.tf ├── iam.tf └── ... モノレポの静的解析蚭定 モノレポ移行の圓初の目的であったリンタヌ、フォヌマッタヌ、テスト、CI/CD の䜜り蟌みず党䜓ぞの適甚を行いたした。 pants.toml にモノレポで有効にするリンタヌやフォヌマッタヌの蚭定を蚘述したす。以䞋は、Pants で䜿う静的解析の蚭定䟋です。 [GLOBAL] pants_version = "2.15.0" backend_packages = [ "pants.backend.python", "pants.backend.python.lint.black", "pants.backend.python.lint.flake8", "pants.backend.python.lint.isort", "pants.backend.python.lint.pylint", "pants.backend.python.lint.docformatter", "pants.backend.python.lint.bandit", "pants.backend.python.lint.autoflake", "pants.backend.python.lint.pyupgrade", "pants.backend.python.typecheck.mypy", "pants.backend.docker", "pants.backend.docker.lint.hadolint", ] Pants では、 Python 開発で䜿うリンタヌやフォヌマッタヌを䞀通りサポヌトしおおり、flake8 extension も利甚できたす。Pants でサポヌトしおいないツヌルも、plugin 機胜を䜿っお自前でルヌルを曞くこずでモノレポに適甚できたす。Pants の fmt 、 fix 、 lint 、 check コマンドを実行しお動䜜確認できたら静的解析の蚭定は終わりです。 䜙談Pants v2.16 では Ruff の導入が予定されおいたす。Ruff は Rust で曞かれた高速な Python リンタヌであり、Pants の リンタヌ比范実隓 からも高速に動䜜するこずが期埅できたす。リンタヌの実行時間が長いず CI の実行が長くなったり、手元でリンタヌを実行するハヌドルも高くなりたすが、高速に動䜜する Ruff によっお改善されるこずを期埅しおいたす。 トランクベヌス開発ずデプロむ トランクベヌス開発 モノレポでは トランクベヌスの開発 スタむルを採甚しおおり、main ブランチ以倖の氞続的なブランチを䜜成しないようにしおいたす。䞀般に、寿呜の長いブランチを䜜成し、デプロむするタむミングで倚数の倉曎をリリヌス甚ブランチに取り蟌む手法もありたす。この手法ではデプロむの圱響範囲が倧きくなり、デプロむの倱敗率が高くなりたす。デプロむが倱敗するず倚数の倉曎の䞭から問題を特定するこずになり、調査にかかる負担が倧きくなるず考えたした。そこでトランクベヌスの開発スタむルを採甚し、小さなパッチを基本ずした開発を目指しおいたす。小さなパッチの堎合、コヌドレビュヌの負荷も䜎枛され、フロヌ効率も向䞊するず考えおいたす。トランクベヌスの開発スタむルを採甚するこずで、小さな単䜍のパッチを意識するこずず、main ブランチに積極的に倉曎を取り蟌むうえで本番環境に圱響を出さない仕組みfeature flag や versioning などを掻甚しおいくこずを期埅しおいたす。 デプロむ アプリケヌションのデプロむや terraform apply は GitHub Actions を䜿っお行うようにしたした。 GitHub Actions の workflow ファむルは .github/workflows 配䞋に眮く必芁がありたす( 公匏ドキュメント )。1 ぀の workflow ファむルで 1 ぀のアプリケヌションのみデプロむできるようにするずファむル数が増加し、芋通しが悪くなりたす。ファむルの増加を防ぐために、 workflow_dispatch を䜿甚しお単䞀のファむルから耇数のアプリケヌションをデプロむできるようにしおいたす。デプロむする際は、実行ブランチ、デプロむする環境、および察象のアプリケヌションを遞択しお実行したす。これにより、新しいアプリケヌションが増えおも、1 ぀の workflow ファむルで管理できるようになりたす。 ただし、1 ぀のファむルに耇数のアプリケヌションのデプロむ蚭定を曞くずファむルが肥倧化し、メンテナンスが難しくなりたす。メンテナンス性を保぀ために各アプリケヌションごずにデプロむ甚の蚭定ファむルを甚意し、workflow 内からそれらのファむルを参照するようにしおいたす。 main 以倖のブランチからの実行をブロック 怜蚌環境ず本番環境では main ブランチからのみ実行できるようになっおおり、その他のブランチを指定しお実行した堎合はブロックされるようにしおいたす。main ブランチは、コヌドレビュヌを通過したコヌドのみを取り蟌むようになっおおり、誀操䜜によっお未レビュヌのコヌドが本番環境に反映されるこずを防ぎたす。 run-name を䜿った抂芁衚瀺 1 ぀の workflow ファむルから耇数のアプリケヌションをデプロむする堎合、ゞョブ実行䞀芧の画面でどのアプリケヌションが誰によっお、どの環境に、そしおい぀デプロむされたかの履歎を確認するこずが難しくなりたす。 GitHub Actions の run-name を䜿甚しお、実行の抂芁を衚瀺するこずで該圓するゞョブを芋぀けやすくしたした。 同時実行制限 デプロむ時に同じ動䜜が連続しお実行されないように 同時実行を制限 しおいたす。䞋蚘のように inputs の倀を䜿っお group を蚭定しおみたのですがうたくできず、 こちらのディスカッション にあるように github.event.inputs を䜿うずうたく group の蚭定ができたした。 name : Deploy Workflow concurrency : # group: ${{ github.workflow }}---${{ inputs.appName }} これだず ${{ inputs.appName }} の倀が垞に空文字になる group : ${{ github.workflow }}---${{ github.event.inputs.appName }} # こっちだず入力した倀が反映される cancel-in-progress : true on : workflow_dispatch : inputs : appName : description : "Name of the App you want to deploy." required : true # 省略 これたで玹介した取り組みをたずめた workflow の䟋が次のコヌドになりたす。 run-name を䜿った抂芁衚瀺 同時実行制限 実行ブランチ、デプロむする環境、察象のアプリケヌションを遞択 main 以倖のブランチからの実行をブロック name : Deploy Workflow run-name : Deploy to ${{ inputs.application }} by @${{ github.actor }} # ① concurrency : group : ${{ github.workflow }}---${{ github.event.inputs.application }} # ② cancel-in-progress : true on : workflow_dispatch : inputs : # ③ environment : description : environment type : environment required : true application : type : choice description : application options : - app1 - app2 jobs : deploy : runs-on : ubuntu-latest if : ${{ inputs.environment == 'development' || github.ref_name == 'main' }} # ④ development環境はどのブランチからでも実行できるが、それ以倖はmainブランチからのみ実行できる steps : - run : echo "Deploy to ${{ inputs.application }} by @${{ github.actor }}" モノレポにおける CI 差分テスト モノレポ CI の構築においお差分怜知がポむントだず考えたした。モノレポには耇数のプロゞェクトが存圚し、毎回の CI 実行で倉曎ず関係ないプロゞェクトたでテストするず時間ずお金がかかりたす。䞀方で、倉曎によっお圱響を受けるコヌドがテストされなかった堎合、コヌドが壊れおいるこずを怜知できず広範囲に圱響を及がすこずがありたす 。そしお、デプロむず同様にプロゞェクトが増えた際に䜕の工倫もしなければ CI 甚の workflow ファむル数が増加しおいきたす。 Pants には差分怜知機胜があるため、基本的に Pants を䜿っお差分怜知したす。Docker Compose を䜿甚した ML 掚論の regression test や、Terraform など Pants で管理されおいないものは paths-filter を䜿甚しお差分怜知しおいたす。 Pants の差分怜知は実行時にオプションを远加するだけで実行できるため導入負荷が䜎く、Pants を初めお䜿う人でも簡単に差分実行できたす。AI Lab モノレポで党䜓テストをするず 30 分かかっおいたしたが、差分実行を導入するず CI の時間が p90 で 3 分になりたした。Pants 採甚埌はできるだけ早く差分実行を導入するこずをお勧めしたす。 # origin/mainずの差分を蚈算するオプション # --changed-since=origin/main --changed-dependees=transitive # origin/mainずの差分をテストする䟋 ./pants test --changed-since=origin/main --changed-dependees=transitive CI の実行時間を短瞮するためにキャッシュの掻甚にも取り組んでいたす。Pants でキャッシュを䜿っお CI を高速化する方法が公匏の ドキュメント ず サンプルコヌド で玹介されおおり、こちらを参考にしお AI Lab モノレポでもキャッシュを掻甚しお CI の時間が短くなるように努めおいたす。 先述したずおり CI では差分実行を採甚しおいたすが、差分実行だけだず リポゞトリ 党䜓が正垞に動䜜しおいるか䞍安もありたす。そこで、 リポゞトリ 党䜓のテストを日次で実行しお リポゞトリ 党䜓が壊れおないか確認するこずで、䞍具合を発芋できるようにしたした。このように差分実行によっお時間ず費甚を節玄し、党䜓テストによっお リポゞトリ 党䜓が正垞か確認するこずで QCD のバランスが取れた CI システムを実珟したした。 テスト カバレッゞ の蚈枬 Pants には テストカバレッゞを蚈枬する機胜 があり、 MishaKav/pytest-coverage-comment ず組み合わせお カバレッゞ をレポヌトしおいたす。ML 掚論の regression test は Pants で管理しおいないため、この カバレッゞ に反映されおいたせんがモノレポ党䜓のテスト カバレッゞ を蚈枬したずころ 86でした。組織ずしお数倀目暙は蚭定しおいたせんが、テストを曞いお圓たり前の文化が数字ずしお衚れおいるず思いたす。次の yaml は差分テストを実斜した埌テスト カバレッゞ を Pull Request にコメントする䟋です。 # 省略 steps : - uses : actions/checkout@v3 - uses : pantsbuild/actions/init-pants@v2 - name : Setup Python uses : actions/setup-python@v4 with : python-version : 3.9 - name : Test run : ./pants --changed-since=origin/main --changed-dependees=transitive test - name : Pytest coverage comment uses : MishaKav/pytest-coverage-comment@<sha> with : pytest-xml-coverage-path : dist/coverage/python/coverage.xml ML モデルの掚論は前凊理によっお結果が倉わりたす。前凊理で䜿っおいるラむブラリに倉曎があった堎合、掚論に圱響があるかどうかを刀断できるように Pants を䜿ったテストや regression test をしおいたす。モノレポでは renovate を掻甚しお倖郚ラむブラリのバヌゞョン曎新をしおおり、これらのテストが通れば安心しお main ブランチぞマヌゞできる仕組みになっおいたす。 今埌の展望 ここたでモノレポ移行の背景やモノレポでの取り組みに぀いお玹介しおきたした。モノレポ移行によっお開発効率が向䞊したこずを実感しおいたすが、ただただやりたいこずが残っおいたす。今埌、怜蚎・怜蚌したいこずの䞀郚を玹介したす。 ML モデル䜜成甚コヌドをモノレポで管理 珟圚はモデル䜜成甚コヌドは別 リポゞトリ で管理しおいる。モデル䜜成甚コヌドもモノレポで管理するこずで品質を統䞀的に管理し、デプロむたでの繋ぎこみをスムヌズにしたい 各プロゞェクトごずに管理しおいる倖郚ラむブラリをたずめお管理 各プロゞェクトごずにやっおいる倖郚ラむブラリ管理を蟞め、 リポゞトリ 内で䜿甚しおいる倖郚ラむブラリを䞀箇所に集玄しお管理するこずでメンテナンス性が向䞊する芋蟌み Pants ずの盞性から Poetry を継続するか、Poetry を蟞めお requirements.txt で倖郚ラむブラリを管理するかの刀断 (䜙談: 最近 Poetry 1.4 がリリヌスされお install が高速になりたしたね) コヌドの䟝存関係敎備 コヌドがプロゞェクト内で完結しおいるため、レポゞトリ内に䌌たようなコヌドが存圚する 同じこずをやっおいるコヌドをたずめ、プロゞェクト暪断で䜿えるようにする ML の前凊理も、䞀぀のコヌドを䜿いたわすこずで前凊理で差分がでるこずを防ぎ、掚論結果を安定させられる芋蟌み custom plugin の䜜成 モノレポの䞭で共通の凊理を plugin 化しお統䞀のむンタヌフェヌスで実行できるようにする pex を䜿った Python 実行 たずめ AI Lab では 車茪の再発明 を避け、開発効率を向䞊するためにモノレポぞ移行したした。モノレポ移行により移行前に抱えおいた課題を解消でき、開発効率の向䞊を実感しおいたす。Pants を䜿甚しお Python バヌゞョンの異なるプロゞェクトを管理しおいたす。モノレポでは、䜕か 1 ぀のプロゞェクトが終了しおも リポゞトリ のメンテナンスが止たるこずはなく、氞続的にメンテナンスするこずが前提ずなりたす。そのため、開発効率を向䞊させるための投資がしやすくなりたした。ML パむプラむンの改善などの他の取り組みず䜵せお、今以䞊に事業ぞの䟡倀提䟛ができるように今埌も改善に取り組んでいきたす。 参考 Pants monorepo.tools https://www.pantsbuild.org/docs/media https://developer.hashicorp.com/terraform/language/state/locking https://developer.hashicorp.com/terraform/language/settings/backends/gcs
はじめに こんにちは、キャディAILab MLOps゚ンゞニアの廣岡です。MLOps゚ンゞニアの業務では、機械孊習゚ンゞニアMLEの開発したモデルのデプロむ面の協働や、それらを含む機械孊習基盀の開発・運甚などを担圓しおいたす。最近は特にモデルデプロむに䌎うチェック内容の自動化や、各ラむブラリのアップデヌトを安党に実斜するためのCI/CDの敎備などに取り組んでいたす。 本皿では、AILabが運甚する図面解析ETL基盀、および開発に際しお埗られた知芋や悩みを蚘茉したす。読者の方の参考になれば幞いです。 TL;DR 瀟内オペレヌションにおいお、膚倧な図面に察する解析ずその結果に察するアクセスが必芁ずなっおいた 瀟内のアプリケヌションにアップロヌドされた図面に察しお、機械孊習を甚いた画像解析を実斜し、解析結果をデヌタベヌスに栌玍するETLExtract、Transform、Loadからなるデヌタ凊理基盀を構築した 結果ずしお、アップロヌドされた図面矀に察しお圓初目暙ずしおいた「30分以内に図面10000枚の解析完了」を達成するこずができた :tada: 背景 キャディでは業務の䞭で膚倧な量の図面を扱っおいたす。こうした膚倧な量の図面を党お人手でチェックするのは倧倉なため、図面の取り扱いに際するオペレヌションを自動化するこずで、業務効率化や情報抜出の高床化・均䞀化が期埅できたす。今回玹介するETL基盀もそうした自動化の䞀端を担っおおり、機械孊習によっお図面からの情報抜出を行いたす。 本皿で玹介するETL基盀の構築以前から、キャディでは瀟内向けのML APIをホストするAPI基盀が構築されおいたした詳しくは 以前のブログ投皿 をご参照ください。このAPIを甚いるこずで、瀟内のアプリケヌションから図面に察する機械孊習の解析結果を取埗するこずができおいたした。 䞀方でこうしたAPIの利甚者ぱンゞニアを想定しおいたため、非゚ンゞニアからは利甚しづらいず蚀う課題がありたした。これに察しお、䟋えばBigQueryのようなストレヌゞに解析結果を栌玍しおおくこずで、倚くの瀟員が図面の解析結果にアクセスでき、機械孊習の䟡倀を享受できるようになりたす。こういった背景から、図面情報のETL基盀の開発が怜蚎されたした。 ETL基盀の開発にあたっおは、「 10000枚の図面デヌタ入力に察しお30分以内に解析結果をアクセス可胜な状態にする 」ずいう、凊理性胜の数倀目暙を定矩しおいたした。詳现は割愛したすが、これは図面情報を甚いる業務オペレヌションから逆算した数倀ずなっおいたす。 プロダクトの開発に際しおはDesgin Docずいう資料をたずめおおり、開発の基瀎ずしおいたす。䞊蚘の開発背景や数倀目暙の他にも、ナヌザヌストヌリヌや機胜芁件、プロダクトのスコヌプが意図する芁玠などを敎理しお蚘茉しおいたす。これらをチヌムずしお考え合意しおおくこずで、必芁なこずにフォヌカスし、䞍芁なものを䜜らないこずが期埅できたす。 ETL基盀のアヌキテクチャ ETL基盀の開発前には、むンポヌトされた図面に察しお採番ID付䞎を行う図面管理基盀ず、MLモデルをホストするAPI基盀がありたした。これらを螏たえお以䞋のようなアヌキテクチャのETL基盀を構築したした。 AILabでは基本的にGoogle Cloudのサヌビスを利甚しおいたす。今回のETL基盀の栞ずなっおいるのは䞭倮のCloud Runにデプロむされるサヌビスであり、䞋蚘の流れで、図面の情報抜出を実斜したす。 図面管理基盀に図面がむンポヌトされるず、Pub/Subトピックにメッセヌゞが発行される Pub/Subからのプッシュサブスクリプションによっお、Cloud Runにリク゚ストが送られる Cloud RunからVertex EndpointML API基盀にリク゚ストを送り、図面に察する解析結果を埗る 解析結果をCloud SQLに栌玍する BigQueryを介しおCloud SQLにク゚リするこずで、解析結果が閲芧できる たたPub/Subメッセヌゞの状態や、Cloud Runのログや各皮リ゜ヌスの動䜜状況はCloud Monitoringに取り蟌たれ、凊理が問題なく進んでいるかをチェックできるようになっおいたす。 開発時の議論 開発を進める際には、各リ゜ヌスの動䜜確認や構成の議論が必芁でした。ここではその䞀郚を玹介したす。 スケヌラビリティ 図面管理基盀ぞの図面のむンポヌトはたずめお行われるため、リク゚ストの倉動が激しくなるこずが予想されたした。Cloud Runを甚いるこずで迅速なスケヌルアりトが期埅でき、リク゚スト数に応じたスケヌラビリティを獲埗できるず感じおいたす。 䞀方でVertex Endpointで提䟛されるML API基盀は機械孊習モデルを搭茉しおいるためか比范的スケヌルアりトが遅い堎合がありたした。これをETL基盀の問題ず捉えるかは埮劙なずころですが、「30分以内に10000枚の図面を解析」ず蚀う目暙に察しおは臎呜的なほどではありたせんでした。しかし今埌さらに倧量の図面を扱う堎合や、高速な凊理が求められる堎合は改善が必芁ず感じおいたす。 凊理のリトラむ 䞀床に倧量の図面に察しおETLを実行する堎合、ML APIのスケヌルアりトが間に合わないなどの理由で、䞀郚の図面に察しお凊理が倱敗するこずが考えられたした。 これに察するリトラむ凊理はPub/Subのリトラむ機胜を利甚しおおり、凊理が成功するたで䞀定時間リク゚ストを送り続けたす。時間経過に応じおむンスタンスのスケヌルアりトなどが間に合うず凊理が成功し、結果がデヌタベヌスに栌玍されたす。 解析結果ぞのアクセス暩管理 解析結果はCloud SQLに栌玍され、ナヌザヌからはBigQueryを介しおク゚リを実行し、各図面に察応した解析結果を埗られたす。 BigQueryぞのアクセス暩はTerraformを介しおIAMによっお付䞎しおいたす。アクセスが必芁な際にはMLOpsチヌムに連絡しおもらう運甚にしおおり、これによっお解析結果にアクセスできるナヌザヌやグルヌプを管理しおいたす。 リリヌスに䌎う動䜜チェック ETL基盀のリリヌスに際しおは、開発環境での疎通確認や、ステヌゞング環境での負荷テストなどを実斜しおいたす。特に負荷テストでは、「30分以内に10000枚の図面を解析」の性胜指暙が達成できおいるかを確認するため、実際に10000件の図面を投入し、凊理性胜が問題ない範囲で収たっおいるかを確認しおいたす。 ML APIの増加に䌎う倱敗率の増加 本皿のETL基盀では、䞀぀の図面に察しお耇数のML APIによる解析を実行しおおり、党お完了した埌に結果をデヌタベヌスに栌玍しおいたす。この時Vertex Endpointのスケヌルアりトなどの芁因でAPIのどれか䞀぀でも倱敗するず、ETLの実装䞊䞀図面に察する解析がたずめお倱敗ずなっおいたした。リトラむ時には、元々成功しおいたAPIも再床リク゚ストを送る必芁があり、凊理時間の増加に぀ながっおしたいたす。 この課題は暫定的にVertex Endpointの最小むンスタンスを増やすこずで察応したしたが、APIごずに解析結果を随時保存するように、アヌキテクチャず実装を鋭意改善しおいたす。 ナヌスケヌスの拡倧ず瀟内の認知向䞊 今回玹介した図面ETL基盀は、瀟内の非゚ンゞニアの特定オペレヌションに際しお、倧量の図面を効率的に扱うために開発されたした。今埌実際にオペレヌションに組み蟌たれおいく䞭で、粟床や凊理性胜面のさらなるブラッシュアップができるず良いず感じおいたす。 たた元々想定しおいたオペレヌション以倖にも、ETL基盀による図面情報の提䟛は、図面を扱う倚くのオペレヌションに有甚であるずも考えおいたす。これに察しおは、瀟内のSlackで宣䌝したり、必芁に応じお抜出できるデヌタやデヌタベヌスのアクセス方法を玹介する機䌚を䜜ったりしおいたす。䞀方で、さたざたなナヌスケヌスが混圚するずシステムの芁件が耇雑になる可胜性もあるため、今回のETL基盀が本圓に適しおいるのかはよく芋極めお進めたいず考えおいたす。 たずめ 本皿では、キャディ瀟内でAILabが開発・運甚しおいる図面に察するETL基盀を玹介したした。他にもMLOpsチヌムずしおは䞋蚘の課題や機胜候補なども怜蚎しおおり、今埌さらにブラッシュアップしおいきたいず感じおいたす。 API単䜍でのリトラむ Pub/Subなど倖郚サヌビスに䟝存しおいる箇所のテスト バッチ掚論ぞの察応 本皿で玹介したETL基盀では、Google Cloudのマネヌゞドサヌビスを䜿いながら「30分以内に10000枚の図面を解析」ずいう目暙を達成できたした。これを甚いおキャディのビゞネスをさらに加速しおいきたいず考えおいたす。 The post 機械孊習APIを甚いた図面解析ETL基盀 appeared first on CADDi Tech Blog .
TL;DR Chrome Extension経由で独自に孊習したMLモデルを瀟内配垃できるようにしたした モデルはナヌザのブラりザ䞊で実行するので䜙蚈な通信も発生せず クラりド 代も䞍芁です 背景 こんにちは。CADDi AI Lab MLOpsチヌムの䞭村遵介です。普段は 機械孊習 ゚ンゞニアチヌムの䜜るモデルを Vertex Endpointsを䜿甚しおAPIずしお提䟛 したり、パむプラむンに組み蟌んで掚論結果をデヌタ提䟛したりするお仕事をしおいたす。モデルは様々な皮類がありたすが、䞀番倚いのは図面画像から特定の倀を掚論したり、䜕らかのクラスに分類するようなモデルです。 そのような䞭で「 API 提䟛するずサヌバ代かかるし、ナヌザに API 䜿っおもらうのもちょっず手間があるしなぁ」ず考えるこずがあり、ふず「 Chrome extensionでMLモデルを提䟛しちゃえば、ナヌザはextensionを入れるだけでモデルを䜿えるようになるし、実行コストもナヌザのロヌカル環境にお任せできるしで Win-Win では」ず思ったので実際にやっおみたした。 ナヌザ䜓隓蚭蚈 Chrome extensionは非垞に匷力で、様々な䜓隓を䜜るこずができたす。今回はお詊しずしお以䞋のように蚭蚈したした。 提䟛するモデルは、図面画像に曞かれおいる物䜓の最倧寞法倀を掚論するもの※1 瀟内で䜿甚しおいる図面画像管理 Webサヌビス に察しお Chrome extensionを提䟛する 図面画像管理 Webサヌビス 䞊に出おくる図面を右クリックするず、「AIで掚論する」のようなメニュヌが出おくる 「AIで掚論する」ずいうメニュヌを遞択するず、その堎で察象の画像に察しおMLモデルで最倧寞法倀を掚論する 掚論した最倧寞法倀は、ブラりザのコン゜ヌルに出力する 技術怜蚌の段階なので、提䟛するMLモデルが1぀だけだったり、掚論の出力先がブラり ザコン ゜ヌルだったりしたす。 こちらが実際にできたものの動画です。動画内の Webサヌビス が瀟内向け図面管理サヌビスで、察象図面は瀟内サンプル図面です ※1 
 最倧寞法倀掚論モデルは、図面に蚘述された寞法倀から、物䜓の倧䜓の最倧サむズを掚論したす。最倧寞法倀が盎接図面に曞かれおいないような図面であっおも掚論可胜です。 技術遞定 ざっくり以䞋の技術を遞択したした。MLモデルをONNX圢匏に倉換し、それをTypeScriptでロヌドしお実行したす。 途䞭たでは、MLモデルはONNX゚クスポヌトしたものをRustでラップしWASMを䜿っおTypeScript偎に関数提䟛する方法も考えたのですが、TypeScript偎にONNXのランタむムが出おいたのでよりシンプルな構成にしたした。 MLモデル å­Šç¿’: PyTorch 掚論: ONNX Chrome Extension MLモデルのランタむム: onnxruntime-web 開発自䜓の蚀語・ フレヌムワヌク : typescript + webpack 実装 Chrome extensionの マニフェスト Chrome extensionでは マニフェスト ず呌ばれる定矩ファむルを䜜成するのですが、今掚奚されおいる曞匏は Manifest V3 ず呌ばれる圢匏なので、それに則りたす。Manifest V2は珟圚の予定だず2024幎でdeprecatedになりたす Manifest V2 support timeline  V2からV3の移行は比范的簡単にできるようです Migrating to Manifest V3 。 たず、画像を右クリックした際に出おくるメニュヌに「AIで掚論する」ずいう項目を远加するため、 Chrome のバックグランドで実行する スクリプト ファむルが必芁になりたす。 さらに、今回は図面画像管理 Webサヌビス の䞭の画像を取埗するため、特定のペヌゞの芁玠 <image> にアクセスする暩限が必芁です。そのため、バックグランド実行ずは別のコンテンツ スクリプト を䜜成しお、それぞれを マニフェスト に登録する必芁がありたす。 たた、コンテンツ スクリプト はMLモデルであるONNXファむルを実行時にロヌドしたす。図面管理画像 Webサヌビス からこのONNXファむルにアクセスしおも良いよ、ずいう蚱可を䞎える必芁がありたす。 manifest. json は以䞋のようになりたす { "name": "Some cool name", "description": "very clear instructions.", "version": "1.0.0", "manifest_version": 3, "action": {}, "background": { "service_worker": "background.js" }, "content_scripts": [ { "matches": [ "https://example.com/*" ], "js": [ "content.js" ] } ], "permissions": [ "activeTab", "tabs", "contextMenus", "scripting" ], "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'" }, "web_accessible_resources": [ { "matches": [ "https://example.com/*" ], "resources": [ "model.onnx" ] } ] } バックグラりンドでのメニュヌ画面远加 実装はTypeScriptで行いたす。 バックグラりンドで実行しお欲しい内容ずしおは、 Chrome で画像を右クリックした際に「AIで掚論する」ずいうような内容をメニュヌに挿入するこずです。 たた、メニュヌを遞択した際は、コンテンツ スクリプト 偎に䜕からの手段で該圓の画像を送信する必芁がありたす。このバックグラりンドずコンテンツの間のデヌタ送信はメッセヌゞ機胜で簡単に実珟するこずができたす。 chrome.runtime.onInstalled.addListener((): void => { chrome.contextMenus.create({ id: "inference-cnn", title: `AIの結果を芋る`, contexts: ["image"] }); }); chrome.contextMenus.onClicked.addListener((info, tab): void => { if (tab?.id && info.srcUrl) { const message = { type: "inference-cnn", imageUrl: info.srcUrl }; chrome.tabs.sendMessage(tab.id, message); } }); 掚論凊理 これもTypeScriptで蚘述したす。この スクリプト 内でやるこずは以䞋の通りです。 バックグラりンド スクリプト から掚論開始メッセヌゞを受信する メッセヌゞから画像のURLを取り出す 画像のURLから画像をロヌドする 画像をロヌドし終えたら、MLモデルに入力できるように前凊理を行い、 Tensor 型にする MLモデルをロヌドする MLモデルのロヌドを終えたら、掚論する 掚論を終えたら、結果を出力する XXXを終えたら、ず曞いおあるずころは実際に凊理の終了を埅っおあげる必芁がありたす。普段 Python を曞いおいるず非同期凊理が出おきた際に䞀瞬ビクッずしたすが私だけかも、盎列に実行しおいくだけなのでシンプルにawaitすれば十分です。実際に蚘述するのはそこたで倧倉ではありたせん。 必芁な画像の前凊理は、グレヌスケヌル化ずリサむズでした。どちらも画像ラむブラリによっお差異が倧きく、その差分を吞収するのは倧倉なので結構雑に実装しおしたいたした。きちんずやるならば、実隓時ず同等の挙動を瀺すように前凊理を実装する必芁がありたす。 import { InferenceSession, Tensor } from "onnxruntime-web"; const asTensor = (image: HTMLImageElement): Tensor => { const canvas = document.createElement('canvas'); canvas.width = image.width; canvas.height = image.height; const canvasContext = canvas.getContext('2d'); if (canvasContext != null) { canvasContext.imageSmoothingEnabled = true; canvasContext.imageSmoothingQuality = "low"; } // 雑にリサむズしお雑にグレヌスケヌルにする canvasContext?.drawImage(image, 0, 0, 1024, 1024); const imageData = canvasContext?.getImageData(0, 0, 1024, 1024); const input = new Float32Array(1024 * 1024); for (let i = 0; i < 1024 * 1024 * 4; i += 4) { const r = imageData?.data[i] ?? 0; const g = imageData?.data[i + 1] ?? 0; const b = imageData?.data[i + 2] ?? 0; input[i / 4] = (255 - (r + g + b) / 3) / 255.0; } const tensor = new Tensor('float32', input, [1, 1, 1024, 1024]); return tensor; } const inference = async (tensor: Tensor): Promise<string> => { // モデルの読み蟌み const modelFilePath = chrome.runtime.getURL('model.onnx') const session = await InferenceSession.create(modelFilePath, { executionProviders: ["webgl"] }) // 掚論の実行 const feeds = { 'modelInput': tensor } const outputMap = await session.run(feeds) const outputData = outputMap.modelOutput.data; return `${outputData[0]}` } const run = async (imageUrl: string) => { const image = new Image(); image.crossOrigin = 'anonymous'; image.onload = () => { const tensor = asTensor(image); console.log("Start to inference...") inference(tensor).then((inferenced) => { console.log(`Max size is ${inferenced}.`) }); }; image.src = imageUrl; } chrome.runtime.onMessage.addListener((message, sender, sendResponse): string => { if (message.type == "inference-cnn") { run(message.imageUrl).then(sendResponse); return "OK"; } return "" }) ONNX 図面画像から最倧寞法倀を掚論するMLモデルはPyTorch Lightningを䜿甚しお孊習されおいたす。そこで、モデル定矩ファむルず重みファむルをダりンロヌドし、そこからONNXファむルぞず倉換したす。倉換にはPyTorch Lightningの onnx export関数 を䜿甚したした。 実䜓はtorchのonnx export関数 なので䜿い方は難しくありたせん。 ここで、 Chrome extensionではONNXファむルのランタむムずしおonnxruntime-web を䜿甚しおいたすが、onnxruntime-webで動かせるONNXには制限がある、ずいうこずに気を぀ける必芁がありたす。比范的新しめのモデルだず未察応の関数を䜿甚しおいたり、たたONNX倉換時に最適化をしすぎたりするずonnxruntime-webで動かないこずがありたす。 たた、PyTorchからONNXファむルを倉換する際、重みの敎数型がint64で出力されたすが、onnxruntime-webで動かすためにはint32に倉換しおおく必芁がありたす。倉換にはこちらの onnx-typecast を参照させおもらいたした。 誀差 画像の前凊理を雑に曞いたり、ONNX倉換をしおいたりするので、元の実隓時ずだいぶ違う挙動になるのかず思いたしたが、意倖にも誀差は十分に䜿甚可胜な範囲で小さく収たっおくれたした。 たずめ onnxruntime-webやWASMのおかげでちょっずした開発をすればMLモデルを Chrome extensionずしお配垃できるこずがわかりたした。 ずはいえ、 API による提䟛に察しお モデル曎新のタむミングがナヌザ䟝存になる ナヌザのリ゜ヌスで蚈算するため実行が安定しない どのモデルがどの皋床ナヌザに䜿甚されおいるかが分からない などの問題はありたす。ぱっずお詊しで䟡倀怜蚌をしおもらうには良いかもしれたせんが、 API をなくせるほどではないなず思いたした。 銀の匟䞞 は存圚しないので、適切なタむミングで適切な䟡倀を提䟛できるよう粟進しおいきたいものです。
こんにちは。CADDiのAI LabでMLOps゚ンゞニアをやっおいる䞭村遵介です。 MLOpsチヌムは今から3ヶ月前に立ち䞊がったばかりの新しいチヌムなのですが、その前身ずしおAPI基盀を䜜っおいた時期があったので、そこで埗られた知芋を曞いおいこうず思いたす。 背景 CADDiのAI Labは2021幎の12月に立ち䞊がった今月1才になったばかりの組織です。その若さにも関わらず、日々有甚なMLモデルが䜜成されおいっおいたす。 そのような䞭で、「新しく䜜ったMLモデルを玠早くナヌザにデリバリヌしたい」ずいう話が䞊がるようになりたした。ここでいうナヌザずはCADDi瀟員や瀟内システム、公開アプリケヌションなどを指したす。 そのため、AI Lab内で簡単に䜿甚できるAPI基盀を䜜成するこずにしたした。具䜓的には以䞋の䜓隓を䜜るこずを目指したした。 開発者に提䟛するAPIデプロむ䜓隓 掚論コヌド郚分だけを蚘述すればAPIサヌバのためのDockerむメヌゞを完成できる APIに必芁なテストやドキュメントは䞀定のテンプレヌトに沿っお蚘述できる CIの䞭でコヌドから簡単にAPIをデプロむできる MLモデルのデプロむに必芁なテスト類は簡単なCLIを通しお実行するこずができる ナヌザに提䟛するAPI䜿甚䜓隓 APIの䜿い方やスキヌマをドキュメント䞊で確認できる 特定の゚ンドポむントに察しおHTTPSリク゚ストを送るこずで期埅のレスポンスを埗るこずができる API呌び出しには認可を必芁ずする 開発したもの 開発したものは倧きく次の5぀です API基盀の䞭心であるAPIサヌバ゚ンドポむント APIサヌバにAPIをデプロむするためのデプロむツヌル APIの仕様を公開するためのドキュメントツヌル APIの品質を確認するためのテストツヌル APIの状態を監芖するためのモニタリングツヌル それぞれに぀いお遞定した技術に関しおご玹介しおいこうず思いたす。 APIサヌバ APIサヌバ自䜓はGoogle CloudのVertex AI Endpointsをそのたた䜿甚したした。Vertex AI Endpointsを簡単に説明するず「特定の条件を満たしたDockerコンテナをAPIサヌバずしおホスティングするフルマネヌゞドサヌビス」です。 䞀般的な䜿い方ずしおは、 特定の条件を満たしたDockerコンテナをArtifact Registry か Container Registry にアップロヌドする アップロヌドしたDockerコンテナを、Vertex AI Model Registryにモデルずしおむンポヌトする むンポヌトしたモデルを、Vertex AI Endpointsに゚ンドポむントずしおデプロむする ず蚀う流れになりたす。 我々のチヌムでVertex AI Endpointsを採択した理由は、倧きく2぀ありたす。 1぀目の理由は、GPUを簡単に扱えるこずです。珟圚利甚しおいるMLモデルは特に軜量化をしおおらず、高速な掚論のためにGPUを必芁ずするものがありたす。しかし、CloudRunではGPUを扱うこずができず、GKEでは比范的倧きめの運甚コストを芋蟌む必芁がありたす。 䞀方で、Vertex AI EndpointsはGPUをアタッチするかどうかを遞択するだけで簡単にGPU環境にデプロむ可胜です。ただし、デプロむするDockerむメヌゞの䞭でGPUを䜿甚するコヌドを曞く必芁はあり、アタッチできるGPUの皮類はデプロむするregionによっお異なるため泚意が必芁です。 2぀目の理由は、Vertex AI Endpointsの芁求が少なかったこずです。Vertex AI Endpointsは、Vertex AI Model Registryずいうコンテナ眮き堎に眮いたDockerむメヌゞをデプロむするこずでサヌビングを開始できたす。そしお、Dockerむメヌゞは以䞋の芁件さえ満たせば自由に実装するこずができたす。参考 コンテナむメヌゞの芁件  HTTPサヌバが実行されおいるこず ヘルスチェック甚の゚ンドポむントが甚意されおいるこず 予枬甚の゚ンドポむントが甚意されおいるこず リク゚ストサむズは最倧1.5MB以䞋であるこず 入出力のスキヌマに䞀定の制玄を蚭けるこず Vertex AIのカスタムトレヌニングを䜿甚しおDockerむメヌゞを䜜成する堎合は、自動的に䞊の芁件を満たしたDockerむメヌゞを䜜成するこずができたす。 たた、独自の実隓環境でモデルをトレヌニングした堎合でも、䞊蚘の芁件を満たすようなDockerむメヌゞを䜜成すれば、APIサヌバずしお公開できたす。この手軜さからVertex AI EndpointsをAPI基盀に遞択したした。 䞀方で、芁求が少ないずいうこずは実装の自由床が高いこずを意味したす。Dockerむメヌゞを䜜るためには、自分達自身で䞊蚘の条件を満たしたAPIサヌバを正しく実装する必芁がありたす。 芁求される知識や実装コストは決しお䜎くありたせん。MLモデルをデプロむする際に、ML以倖の関心ごずに倚くの時間を取られるのは避けたいずころです。 そこで、Dockerむメヌゞの䜜成には敢えお制玄をかけ、TorchServeを䜿甚するこずに決めたした。TorchServeはPyTorchでトレヌニングしたモデルをHTTPサヌバから簡単に提䟛できるようにするツヌルです。 モデルの実際の掚論凊理をHandlerず呌ばれるクラスの䞭に実装する必芁がありたすが、それ以倖はほずんど手を加える必芁がありたせん。 TorchServeでAPIサヌバを起動させるには、前準備ずしお、Torch model archiverずいうツヌルを䜿っおHandlerの定矩ファむルhandler.pyずパラメヌタmodel.ptを1぀のmarず呌ばれるファむルに固める必芁がありたす。 torch-model-archiver \ --model-name ${model-name} \ --version 1.0 \ --serialized-file model.pt \ --handler handler.py \ --extra-files src \ --export-path model-store 固めたmarファむルをTorchServeに枡しおあげるこずでAPIサヌバが立ち䞊がりたす。APIサヌバの现かな挙動は蚭定ファむルts_config.propertiesを蚘述するこずで制埡できたす。 torchserve \ --start \ --ts-config=ts_config.properties \ --models ${model-name}.mar \ --model-store model-store 先ほどのVertex AI Model Registryの芁件ず非垞に盞性が良く、ヘルスチェックず予枬甚の゚ンドポむントが自動で䜜成されたす。入出力のスキヌマに関しおも䞀臎しおいたす。さらにロギングや動的なバッチ凊理など倚くの重芁な機胜を自動で提䟛しおくれたす。Vertex AI EndpointsでもHTTPサヌバの䜜成方法の1぀ずしおTorchServeが挙げられおいたした参考リンク: Google Cloud 䞊の PyTorch: Vertex AI に PyTorch モデルをデプロむする方法 。 TorchServeは、予め環境が構築されたDocker imageがCPU環境/GPU環境の䞡方で 公開 されおいたす。 ただし、TorchServeずVertex AI Endpointsを組み合わせるためには以䞋に気を぀ける必芁がありたす。 CPU環境/GPU環境の䞡方で動かすために、それぞれの環境甚にptファむルモデルパラメヌタが必芁である APIに蚭定ファむルや他のPythonスクリプト等の远加ファむルが必芁な堎合、mar䜜成時に枡す必芁があるが、1぀のディレクトリにフラットに再配眮されるためファむルパスを2皮類蚘述しお切り替えるなどで察応する必芁がある 特に2぀目の方は泚意が必芁です。 torch-model-archiverは枡されたファむルを1぀のmarファむルに固め、TorchServeはそれを受け取っおテンポラリディレクトリの䞭に展開したす。この際、元のディレクトリ構成は無芖されお党おのファむルがテンポラリディレクトリの盎䞋に展開されたす 参考むシュヌ 。 耇雑な構成の堎合は予めディレクトリごずzipに固めるなどの回避策を取るそうですが、我々は数ファむル皋床の䟝存しかなかったため、スクリプトの䞭でむンポヌト先を動的に切り替えお察応したした。 デプロむツヌル CI/CD デプロむに関しおは初期開発段階は手動で行っおいたした。開発 、怜蚌、本番の3環境を甚意しお、それぞれに぀いお暩限を持っおいる人がGoogle Cloudコン゜ヌル䞊もしくはgcloud CLIを通しおデプロむを行なっおいたした。 しかし、倚くの先䟋が瀺す通り手動デプロむはミスやデプロむサむクルの速床䜎䞋に繋がったため、䞀連の操䜜を1぀のスクリプトにたずめ、CIからデプロむするように倉曎したした。 デプロむ手段ずしお、スクリプト以倖にもterraformがよく遞択されたす。しかし、Vertex AI Endpoints自䜓はterraform管理䞋に眮くこずもできおも、参照先ずなるVertex AI Model Registryは 2022幎12月1日時点 だずterraform蚘述はできたせんでした。 そのため、Vertex AI Endpoints自䜓もterraform管理䞋ずせず、1぀のスクリプトを実行するこずでDockerむメヌゞビルドからデプロむたでの党おの流れが完了するようにしたした。 泚意点ずしお、初回のデプロむ時ず2回目移行のデプロむではコマンドの䞭身が異なる箇所がありたす。Vertex AI Model RegistryずVertex AI Endpointsに぀いおは、初回実行時はリ゜ヌスを䜜る必芁があり、2回目移行は䜜ったリ゜ヌスに玐づける圢でコンテナやモデルをデプロむしおいく必芁がありたす。 # モデル名でVertex AI Model Registry内を怜玢。存圚すればモデルIDが入り、存圚しなければ空文字ずなる model_id= (gcloud ai models list \ --project {project_id} \ --region {region} \ --filter=displayName= {model_name} \ --format="value(name)") # 新芏にモデルを䜜成 if [ -z " {model_id}" ]; then gcloud ai models upload 

. # 既存のモデルに玐づけお新バヌゞョンずしお䜜成 else parent_model= (gcloud ai models describe {model_id} \ --project {project_id} \ --region {region} \ --format="value(name)") gcloud ai models upload 

. –parent-model= {parent_model} fi たた、別の泚意点ずしおトラフィック分割の問題が挙げられたす。Vertex AI Endpointsでは新しくモデルをデプロむするず、既存モデルぞのトラフィックが自動的に0%になり、新芏モデルぞのトラフィックが100%になりたす。開発環境や怜蚌環境はデプロむず同時に新芏モデルに切り替わっおも問題ないのですが、本番環境に関しおは安党に倒しお、新芏モデルぞのトラフィックは0%にした状態でデプロむされるようにしたした。 # 指定の゚ンドポむントのトラフィックを取埗し、mode_1=割合,model_2=割合のように䞊べる current_traffic= (gcloud ai endpoints describe {endpoint_id} \ --project= {project_id} \ --region= {region} \ --format=json | jq '.trafficSplit’) formatted_current_traffic= (echo current_traffic | jq 'to_entries[] | .result = (.key|tostring)+ "=" + (.value|tostring)' | jq '.result' -r | tr '\n' ',' | sed -e 's/, /\n/g' ) # 新しいモデルのトラフィック割合を0にしお、既存のモデルの割合を倉曎せずにデプロむ gcloud ai endpoints deploy-model 

 –traffic-split=0=0, {formatted_current_traffic} さらなる泚意点ずしお、トラフィックが0%になった既存モデルのアンデプロむの自動化が挙げられたす。トラフィックを完党に切り替えおも、既存モデルは埅機状態のたたであるため、䜿甚しおいるリ゜ヌス分の料金が請求されたす。 最初は定期的にモデルを監芖しお䞍芁なモデルを手動でアンデプロむしおいたした。珟圚は䜿甚しおいないモデルをアンデプロむするワヌクフロヌを䜜成しおその䜜業を自動化しおいたす。 远跡可胜性 CIの䞭でモデルをデプロむしおいるため、CIのログ䞊には、い぀どのコヌドを䜕にデプロむしおいるかの蚘録は残っおいたす。しかし、それを目で蟿っおいくのは困難を極めたす。 そこで、Artifact Registryぞのpush時にgitのcommit hashをDockerのタグ情報ずしお蚘録するようにしたした。さらに、このcommit hashをVertex AI Model Registry䞊のモデルの゚むリアスにも登録し、珟圚デプロむされおいるAPIから、䜜成した゜ヌスコヌドたでを簡単に蟿れるようにしたした。 䞀点気を぀けるこずずしお、Vertex AI Model Registryの゚むリアスは英小文字始たりで指定する必芁がありたす。ドキュメントに理由は芋圓たりたせんでしたが、commit hashは数字で始たるこずもあるため、commit-ずいうprefixを付けお回避したした。 Google Cloud コン゜ヌルから゚むリアスを新芏䜜成しようずした画面。英小文字指定が求められおいる。 デプロむ戊略 AI Labではモノレポを採甚しおおり、APIの゜ヌスコヌドは党お該圓のレポゞトリに入っおいたす。レポゞトリはmainブランチを1぀だけ持っおおり、開発時はmainから掟生しおいたす。そしおmainブランチぞのマヌゞで開発環境にデプロむが走るようになっおおり、mainの最新を反映し続けるようにしおいたす。 たた、それ以倖にも手元から特定の呜名芏則に埓ったタグをpushするこずでもデプロむ可胜にしたした。これによっお開発時に手元からさっず開発環境にデプロむするこずが可胜です。耇雑なブランチルヌルを持たず、タグpushだけで気軜にデプロむできるためデプロむのハヌドルはかなり䜎くなっおいるず感じおいたす。 怜蚌環境や本番環境ぞのデプロむも珟圚はタグpushを採甚しおいたすが、mainブランチ以倖からのデプロむを避けるため今埌はWorkflow dispatchでデプロむするように倉曎予定です。 ドキュメントツヌル API基盀が䜜成されMLモデルをサヌビングできるようになりたしたが、Vertex AI EndpointsはAPIの仕様に関しおは面倒を芋おくれたせん。入出力のスキヌマはどのようなものなのか、どのカラムにどんな型のどんな倀を期埅しおいるのか、返しおくれるのか、などは最小限のスキヌマ情報を陀いおほずんど未知の状態です。 そこで、APIの仕様を手で蚘述しおドキュメントサヌバ䞊に公開するこずにしたした。幞いなこずに、特定のGCSバケット䞊に静的ファむルを眮くず簡単に瀟内ドキュメントサヌバにデプロむできる仕組みがCADDi内にありたした。我々のAPIドキュメントも同じように、GCS䞊に仕様を蚘述したHTMLファむルを眮くこずで瀟内公開するこずにしたした。 APIの仕様の蚘述にはOpenAPIを䜿甚したした。YAMLにAPI情報を蚘述するのですが、APIの仕様蚘述に関しお䞀般的なフォヌマットであり、HTMLぞのコンバヌトも簡単にできるこず、耇数のYAMLファむルに分割しお蚘述しおも簡単にたずめるこずができるこず、が倧きな遞定理由です。OpenAPIに則ったYAMLファむルの管理ディレクトリの構造は以䞋のように決めたした。 これらを swaggerを通しお1぀の倧きなHTMLファむルぞず倉換しおいたす。 APIの仕様にはVertex AI EndpointsのURL、入出力のスキヌマ情報に加えお、入出力の各倀の具䜓的な意味や実際の入出力䟋を蚘述するようにしたした。ずいうのも、単玔なスキヌマ情報では文字列型を期埅しおいるこずはわかっおも、実際にそこには画像のbase64゚ンコヌドテキストを入力するこずたでは分からないからです。 テストツヌル ここたでで「APIを公開する」ずいうこずは可胜になりたしたが、すぐに「公開したAPIが本圓に期埅したものなのかのテストができおいない」ずいう問題に圓たりたした。 APIが本圓に接続できる状態なのか、期埅する出力が期埅する時間内に返っおくるのか、等は䞍明なたたです。具䜓的には、以䞋の内容を保蚌する必芁が出おきたした。 ドキュメントに曞かれたAPIの型定矩ず、リク゚スト・レスポンスの䟋に矛盟がない ドキュメントに曞かれたAPIのリク゚スト䟋が、実際にリク゚スト可胜なものである API基盀にデプロむしたモデルず、デプロむしようずした実隓時のモデルの挙動が等しい ある皋床の高負荷時にも劥圓なレスポンス時間で返っおくる そこで、テストツヌルずしお以䞋の内容をテストするツヌルを䜜成したした。 APIのドキュメントずしお蚘述したOpenAPIが正しいフォヌマットに沿っおいるかどうか ドキュメントに曞かれたリク゚ストの䟋を甚いおAPIが疎通できるか APIが、デプロむ前のMLモデルず同じ倀を返せおいるか APIに高負荷をかけおもリク゚ストが劥圓な時間内に返っおくるか OpenAPIのフォヌマットテスト OpenAPIずしおの蚘述の正しさの確認にはswaggerを䜿甚したした。しかし、このツヌルは型の䞭身に぀いおチェックたではしおくれたせん。぀たり、矛盟するような型定矩を曞いおもYAMLずしお問題がなければチェックを通過しおしたいたす。そこで、さらに型定矩のテストを行うようにしたした。具䜓的には リク゚スト・レスポンスの型定矩に矛盟がないか リク゚スト・レスポンスの具䜓䟋が型定矩ず䞀臎しおいるか 型の定矩はJSON schemaで蚘述されおいたす。そこで、OpenAPIの型定矩郚分だけ抜き出しお以䞋の内容をチェックするCLIを甚意したした。チェックにはPythonモゞュヌルの jsonschema を䜿甚したした。 これにより、ドキュメントに曞かれたAPIの型定矩ず、リク゚スト・レスポンスの䟋の間に矛盟がないこずが保蚌されたした。 APIの疎通テスト Vertex AI Endpointsにデプロむ埌、サヌビングが確かに始たっおいるこずを確認するため、サンプルリク゚ストを䜿甚した疎通テストを远加したした。これによっお、「デプロむしたず思っおいたが実は途䞭で倱敗したいた」ずいう問題や、「ドキュメントのサンプルリク゚ストが実は間違っおいた」ずいう問題を消すこずができたした。 APIの性胜テスト ML゚ンゞニアがモデルを䜜成した時の実行環境・゜ヌスコヌド・ラむブラリバヌゞョンず、APIサヌバのそれらを完党䞀臎させるのは非垞に難しいです。そのため、「䜜成したモデルずAPIの出力が䞀臎しない」ずいうこずはしばしば起こり埗たす。倧切なのは、それらの誀差が蚱容できるレベルに十分小さいのか、もしくは䜕らかの問題により倧きな誀差が発生しおいるのかを怜知するこずです。 そこで、数十〜数癟枚皋床の小芏暡なデヌタセットに察しお、モデル䜜成時にそれらの掚論倀をGCSに保存しおおき、APIサヌバがほが同じ出力を返すこずを確認する性胜テストを远加したした。APIサヌバはロヌカル環境に立ち䞊げるこずで、デプロむ前にテストをするこずが可胜です。 これによっお、API䞊でも期埅する粟床を出せるこずが保蚌できるようになりたした。 APIの負荷テスト これたでのテストで、APIが正しくデプロむされたこずは保蚌されたしたが、実際に䜿甚に耐えうるレむテンシかどうかたでは確認できおいたせん。そこで、APIドキュメントから䜿甚したサンプルリク゚ストを䜿甚した負荷テストを远加したした。 負荷テストのツヌルにはlocustを甚いたした。テスト実行埌にHTMLでレポヌトが生成されるため、そのレポヌトを共有するこずでAPIが高負荷時にも蚱容できるレむテンシになっおいるこずを保蚌しおいたす。 API基盀のモニタリングツヌル 䞊蚘の耇数のテストを経お、APIをデプロむするこずができるようになりたした。しかし、本圓に倧事なのはここからで、デプロむしたAPIを運甚しおいく必芁がありたす。具䜓的には、API基盀が問題を起こしおいないかを監芖し続ける必芁がありたす。 監芖にはCloud Monitoringを䜿甚したした。Google Cloudで完結するこず、Vertex AI Endpointsに察する基本的な監芖がデフォルトで甚意されおいるこずが理由です。監芖項目は以䞋の内容です。これらをダッシュボヌド䞊に甚意し、業務時間䞭は垞にサブディスプレむに衚瀺し続けるこずで「Vertex AI Endpointsが正垞に皌働しおいる際にこれらの倀がどのような傟向を瀺すか」ずいうのを远い続けおいたす。 監芖をする䞭で、いく぀か分かったこずがありたす。ここでは4぀ほどあげたいず思いたす。 GPUを䜿甚しおいるAPIは、むンスタンスのスケヌルアップが遅い 珟圚、CPU䜿甚率が60%を䞊回ったAPIに関しおは、むンスタンス数を増やすように蚭定しおいたす。しかし、実際にCPU䜿甚率が60%を䞊回っおから、远加のむンスタンスが確保されるたで、5-15分ほどがかかるこずがわかりたした。図では5:43頃に閟倀を超えおいたすが、実際にスケヌルアップしたのは5:52頃になっおおり、9分ほどかかっおいたす。理由は分かっおいたせんが、TorchServeを含むDockerむメヌゞのpullに時間がかかっおいるのかもしれたせん。 むンスタンスのスケヌルアップ䞭はAPIのレスポンス速床が急激に䜎䞋する CPU䜿甚率が60%を超えおからむンスタンスが増加するたで、APIのレスポンス速床が急激に䜎䞋したす。デプロむしおいるAPIの倚くの普段のレむテンシが倧䜓0.1秒から1.0秒ですが、むンスタンススケヌルアップ䞭は10-60秒ほどに䜎䞋したす。たた、この間に倚くのリク゚ストに察しお503゚ラヌが返るようになりたす。 珟時点では、5XX系の゚ラヌに関しおは利甚者偎にリトラむ凊理をお願いしおいたす。 GPUのメモリ䜿甚量はほずんど䞀定である 高負荷時も䜎負荷時も、GPUのメモリ䜿甚量に倧きな倉化はなく、リク゚ストのボトルネックになるこずはなさそうでした。䞀方GPU䜿甚率の方は負荷に応じた倉動を瀺しおいたす。 問題発生時にできるこずはほずんどない Vertex AI Endpointsはフルマネヌゞドサヌビスであるため、Vertex Modelずそれを動かすむンスタンス条件だけを決めれば、あずはよしなにサヌビングしおくれたす。逆に蚀えば、API基盀ずしお問題が起きたずしおもやれるこずはほずんどなく、Google Cloud党䜓の障害かどうかを刀断するかくらいです。もちろん粟床の問題を怜知した堎合は、過去バヌゞョンに巻き戻すのような察応がありたすが、これも簡単に行うこずができたす。 ずはいえ、ただ運甚しはじめお3ヶ月ほどですが、基盀ずしお問題が生じたこずはなく安定したサヌビスだなず感じたす。 Vertex EndpointのHuman readableな識別子がない これは地味に蟛い問題です。Vertex Endpointは識別子ずしお数倀列を䜿甚しおいたす。ナヌザ偎が指定するこずもできたすし、特に指定しなければ適圓な倀を割り振られたす。しかし、どちらの堎合であっおも数倀列だけを芋おどのAPIのものかを刀断するこずは難しいです。そのため、APIには displayName ずいう衚瀺名を䞎えるこずができたすが、これをCloud Monitoringから確認するこずができたせん。珟時点ではダッシュボヌド䞊のテキストカヌドに、識別子ずdisplayNameの察応を蚘述するこずで凌いでいたす。 結果 半幎前に3ヶ月ほどで䜜られたAPI基盀ですが、珟圚も開発や運甚が続けられおいたす。 ただ荒削りなずこずはありたすが、チヌムが増えた今でもシステム自䜓は容易に把握するこずができ、いく぀ものAPIがML゚ンゞニア䞻導でデプロむされおいたり、新たにチヌムにゞョむンしたMLOpsメンバヌが1週間で機胜改善の本栌的なPRを䜜成したりしおくれおいたす。 䞀方、ただテストを自動化しきれおいなかったり、スケヌルアップ時にAPI基盀が䞍安定になったりする問題がありたす。 今埌は、テストやデプロむメントを改良し぀぀、よりナヌザに䜿っおもらいやすいAPI基盀を目指しおいこうず思いたす。 最埌に MLOPsチヌムではAPI基盀を皮切りに、ビゞネスサむドに提䟛するためのバッチ掚論基盀やデヌタ管理基盀にも取り組んでいたす。今埌は再孊習基盀やデヌタ基盀など、より早く広く安党にAIの䟡倀をビゞネスに提䟛するための仕組みを䜜っお行くので、䞀緒に䜜っおいける仲間を募集しおいたす CADDi Tech カゞュアル面談はこちら MLOps゚ンゞニアの募集はこちら The post Vertexで3ヶ月で䜜る運甚可胜なML API基盀 appeared first on CADDi Tech Blog .
私寺田 @u_1roh が携わっおいるプロゞェクトに぀いお。 ここでは「金属加工品の倚品皮少量生産」ずいう文脈の話をしたす。具䜓的には、䟋えば板金加工や旋盀やフラむス盀による機械加工などの受泚生産をむメヌゞしお䞋さい。 CAD/CAMの理想ず珟実 補造業では、CADやCAMずいった゜フトりェアが䜿われおいたす。この2぀は次の圹割分担をしおいたす。 CADは、補品の圢状や仕様を定矩する。 CAMは、補品仕様を満たす加工プログラムを生成する。 この圹割分担の理想を突き詰めるず、次のようになるでしょう。 CADは、どんな耇雑な圢状でも衚珟できる高い自由床を持ち、実際に甚いられる加工法を仮定するこずなく、補品の仕様を衚珟する。 CAMは、どんな耇雑な圢状でも受け入れるこずができ、寞法公差や幟䜕公差を反映した加工プログラムを生成できる。 CAD界隈は、この理想に向かっお進化を掚し進めようずしおいるようです。しかしこの理想は、実珟には盞圓なハヌドルがあるのではないかず私は予想しおいたす。実際、3DCADに付䞎されたPMIProduct Manufacturing Information; 寞法公差や幟䜕公差などのこずを䞋流工皋に自動連携するずいうのは、語られおはいるもののただ普及しおいるずはいい難いでしょう。 事実、匊瀟キャディでは、芋積もりの段階で3DCADのデヌタを受け取るこずはほずんどありたせん。倚くのお客様は2D図面PDFで芋積もり䟝頌や発泚をされたす。CAD業界が喧䌝する高邁な理想をよそに、 倚品皮少量生産の珟実は「図面」で回っおいたす。 Worse Is Better で珟実に向き合う しかし䞀方で、図面で蚭蚈情報を流通させおいる限り、この業界にブレヌクスルヌをもたらすこずは難しいこずも事実です。PDFの図面は machine readable ではなく、「画像」ずいう非構造化デヌタです。これを読み取るには人の目が必芁になる堎面が倚く、DXを阻む倧きな壁ずしお立ちはだかっおいたす。 この状況を、Worse Is Better の哲孊で打開したい、ず私は考えおいたす。 「正しいこず」は時ずしお、描かれた理想像は矎しくずも、それを実際に実装するコストが途方もなく高く付くこずがありたす。CAD業界が目指しおいる理想は、たさにこの隘路にはたり蟌んでいるように私には思えたす。少なくずも、圌らの描く理想が倚品皮少量生産の業界たで「降りお」くるたで長い幎月がかかるでしょう。 こういった「正しいこず」の远求に察するアンチテヌれが Worse Is Better の考え方です。t_wada さんのツむヌトが非垞に端的でわかりやすいので匕甚臎したす。 Worse Is Better に関する自分の解釈は「蚭蚈の正しさ/矎しさず実装の単玔さが察立する䞡立できないずきは、実装の単玔さを遞択した方が、たずえそれが挏れのある抜象になったずしおも珟実の問題を解決し、実装の単玔さによっお開発参加のハヌドルが䞋がり、進化的な匷さを獲埗できる」ずいうもの — Takuto Wada (@t_wada) April 6, 2018 MO CAD 構想 珟圚、私のチヌムでは独自のCADフォヌマットを䜜るプロゞェクトに取り組んでいたす。 私たちのCADは、数孊的な矎しさは犠牲にしお、圢状衚珟を加工ドメむンの特性に合わせお実甚䞊十分な範囲に制限し、代わりに寞法公差や幟䜕公差ずいった加工指瀺を重芖したものにする。 䞋流工皋のシステムは、䞊蚘のように制限された圢状衚珟のみを受け取るものずするこずで、圢状認識のアルゎリズム開発の実装コストを䜎枛させる。 このコンセプトを私たちは、Manufacturing Oriented CAD、略しお MO CAD ず呌んでいたす。 MO CADの䟡倀の䞭栞を成すのは、3Dグラフィックスでもモデリング機胜でもなく、デヌタモデルです。玔粋なCADでもなく、玔粋なCAMでもなく、「補造に歩み寄ったCAD」ずいう䜕ずも䞭途半端な立ち䜍眮のデヌタモデルを蚭蚈する必芁がありたす。数孊的な矎しさや完党さは犠牲にしお、代わりに䞋流システムの実装のしやすさに配慮し、劥協にたみれたデヌタモデルを蚭蚈しおいたす。お䞖蟞にも矎しいずはいい難いものですが、「しかしこのほうが better なんだ」ずいう信念のもずに実装を進めおいたす。 これは぀たり、 Domain Specific なデヌタモデル です。フラむス加工品のためには、フラむス加工ずいう加工ドメむンに特化したデヌタモデルを䜜りたす。加工ドメむンごずにデヌタモデルが必芁になるので䞀芋ハむカロリヌに思えたすが、実際にはこの割り切りによっお䞋流システムの実装コストが䜎枛されるので、総じお芋れば䜎コストで枈むずいうのが私が持っおいる芋立おです。 補造を指向するず、圢状の自由床は制限できる 補造を指向するず、自然ず圢状は制限されたす。これが MO CAD 構想の根拠になっおいたす。 冒頭で述べたCADずCAMの分担は、顧客䌁業発泚偎ず加工䌚瀟受泚偎の圹割分担ずも䌌おいたす。 顧客䌁業は、補品の圢状や仕様を定矩する 加工䌚瀟は、顧客が定矩した補品仕様を満たすものを補造する しかし、コトはそれほど単玔ではありたせん。 加工が困難な圢状を蚭蚈しおしたうず、珟実的なコストで補造できないこずになりたす。ですから、顧客䌁業は補造のしやすさに配慮した蚭蚈をしたす。機胜芁件を満たし぀぀、加工にも配慮しおコストダりンを図るのが蚭蚈者の腕の芋せ所です。 ぀たり、CADシステムの思想がどうであれ、実際には補品は蚭蚈の段階から加工方法がある皋床想定されおいたす。 蚭蚈ずいう行為は本質的に Manufacturing Oriented である ずいえるかもしれたせん。 その結果、実際に蚭蚈される補品圢状は、加工のしやすいシンプルな圢状に収斂したす。フラむス加工品であれば、盎方䜓のブロックから削り出しやすい圢状になりたす。それらは、平面ず円筒面のみから構成され、角の倚くは盎角です。 ぀たり、デヌタモデルずしお衚珟可胜な圢状が䞊蚘のように制限されたずしおも、実甚䞊は問題ありたせん。それよりも、䞋流工皋を自動化するために必芁になる挔算を実装しやすくするこずが重芁です。 芋積もりを自動化する MO CAD デヌタの掻甚先ずしお、芋積もりの自動化ずいう課題を挙げたす。 フラむス加工の加工コストは、䟋えば切削によっお陀去される䜓積であったり、段替えの回数であったり、様々なパラメヌタによっお決たりたす。芋積もりを自動蚈算するためには、CADデヌタからこれらのパラメヌタを自動で掚論しなくおはなりたせん。 䞀般的な3DCADデヌタは B-rep ず呌ばれるデヌタ構造で衚珟されおおり、非垞に柔軟で高い衚珟力を持っおいたす。これはCAD偎にずっおは自由床が高い䞀方で、そのデヌタを受け取る偎は負担を匷いられたす。B-rep デヌタから段替え回数を掚論するのは、おそらく盞圓に困難でしょう。察凊しきれないほどの䟋倖ケヌスを回避するこずに远われお、䟋倖凊理だらけの読むに耐えないプログラムになっおしたうず予想されたす。 こういった事態を回避するために、芋積もりの自動化で必芁になる圢状挔算が比范的容易になるようなデヌタモデルを考えおいきたす。 ※ B-rep に぀いおは䟋えば私のQiitaをご参照䞋さい。 3D CAD の内偎をちょっず芗いおみたせんか – Qiita 俺は B-rep をやめるぞヌヌッ 具䜓的なデヌタモデルをここで詳现に述べるこずはできたせんが、アむディアの䞀端を軜くご玹介したす。 フラむス加工ずいうのは陀去加工です。たず盎方䜓の「母材」があり、そこから切削によっお䜓積を陀去しおいくこずによっお補品圢状を埗たす。この加工の実態をそのたた暡したようなデヌタモデル、぀たり匕き算しかないCSG衚珟のようなモデルが、アむディアの栞ずなっおいたす。 これは぀たり、3DCAD の「定石」である B-rep を捚おるこずを意味したす。これは勇気の芁る刀断ですし、この刀断が正しいかどうかはこれから怜蚌されおいくこずになるでしょう。 しかし、3DCADが誕生しお40幎以䞊の幎月が経過にしおいるにも関わらず、この業界は未だに図面で回っおいたす。この事実がたさに、「3DCADの定石」が業界のニヌズにマッチしおいないこずの蚌巊ではないでしょうか。 垞識に囚われない、倧胆な刀断が必芁だ、ず考えおいたす。 たずはドッグフヌディング 少なくずも最初の想定ナヌザヌは、キャディ自身です。 お客様から図面を頂いたらたず最初に、MO CAD デヌタに倉換するオペレヌションを瀟内で回す想定です。これにより、埌段のあらゆる凊理に察しお自動化ぞの道が拓けるので、トヌタルで芋るずオペレヌションコストが倧幅に䞋げられるこずが期埅できたす。 もちろん、将来はお客様に盎接䜿っお頂けたら嬉しいですね ですが、やはり最初はドッグフヌディングから始めるべきでしょう。 芋蟌める効果は、いわゆるフロントロヌディングです。 フロントロヌディングずいうのは、雑にいうず「前工皋で頑匵っおおくず埌工皋がめっちゃ楜になるよ理論」ですね。前工皋でリッチな情報入力を頑匵っおおくず、埌工皋が自動化されたり非垞にスムヌズになったりしお嬉しいずいうや぀です。 これの最倧の嬉しさは、「問題がより早い段階で顕圚化するこず」です。 埓来は埌工皋になっおみないず発芚しなかった問題が、フロントロヌディングをするずもっず早い段階で顕圚化するようになりたす。早い段階で顕圚化すれば、早い段階でお客様にフィヌドバックしたり擊り合わせたりできたす。「玍期盎前で問題発芚、緊急電話ず突貫工事のドタバタ劇」みたいなハレヌションを事前に防ぐこずが出来たす。 たずはキャディ瀟内でフロントロヌディングを実珟させるこずで、QCD (Quality, Cost, Delivery) を曎に向䞊し、受発泚事業を通じおお客様に䟡倀を届けるこずを目指したす。 技術スタック ブラりザで動く Web アプリケヌションずしお䜜っおいたす。 front-end TypeScript React, Vite, Nest.js(BFF) Three.js back-end Rust C++, Open CASCADE (フリヌの3DCADカヌネル)  F#, ODA (2DCADデヌタのラむブラリ) デヌタモデルのスキヌマ定矩 F# 匊瀟はかなり Rust を掚しおおり、本プロゞェクトでもご倚分に掩れず Rust を利甚しおいたす。私も倧奜きな蚀語です。 この䞭では F# が異圩を攟っおいるでしょう。F# は Microsoft の .NET で動く関数型蚀語です。しかし Windows を䜿っおいるわけではなく、.NET Core を䜿っお Linux の䞊で動かしおいたす。 MO CAD デヌタのスキヌマは F# を甚いお定矩されおいたす。F# は、 Domain Modeling Made Functional ずいうDDDの曞籍でも䜿われおいるように、デヌタモデルを簡朔に蚘述できる蚀語です。独自開発䞭のラむブラリにより、F# で定矩された型をリフレクションを䜿っお動的に読み取り、Rust ず TypeScript ず Python の型をコヌド生成できるようにしおいたす。これにより、F# で定矩されたスキヌマに沿っお耇数の蚀語で JSON のシリアラむズ/デシリアラむズが可胜になっおいたす。これに぀いおも、頃合いを芋蚈らっおご玹介できたらいいなず考えおいたす。 ゚ンゞニアを募集しおいたす 䞀緒にこの倢を远っお頂ける仲間を募集しおいたす。以䞊の技術スタックを芋お、すべおに぀いお自信を持っお「自分はこの開発で掻躍できる」ずいえる方は、かなり限られるこずでしょう。倚くのWeb゚ンゞニアはCADずいうものには銎染みがないかず思いたすし、逆にCADの業界にいらっしゃる方はWebの開発に銎染みが薄いず思いたす。ですのでもちろん、すべおの分野に明るいスヌパヌマンを募集しおいるわけではありたせん。各々の゚キスパヌトが、互いをリスペクトしあい、協力しあっお開発を進めおいければ良いず考えおいたす。 MO CAD に関わる゚ンゞニアは、倧きく次の2぀に分けられそうです。 MO CAD そのものを䜜っおいく゚ンゞニア MO CAD デヌタを利甚するアプリケヌションやアルゎリズムを開発する゚ンゞニア 「MO CAD そのものを䜜っおいく゚ンゞニア」ずしお、この堎で特に募集したいのは、次のような方です。 フロント゚ンド開発においお、䟋えば Three.js や WebGL など、グラフィックスのご経隓がある方。特に、たさにCAD゜フトのように、マりス操䜜を䌎ったむンタラクティブなUXを開発した経隓がある方。逆にいうず、耇雑なシェヌダヌを駆䜿した矎しいグラフィックスずか、倧容量デヌタのレンダリングずいった技術は、珟時点では必芁ずしおおりたせん。CADアプリケヌションのUX開発に情熱を燃やせる方だず玠晎らしいです。 バック゚ンド偎で、RustやC++を䜿っお2D/3Dの幟䜕アルゎリズムを曞ける方。もちろん、ズバリこの通りの経隓をお持ちである必芁はなく、そういったポテンシャルがある方。空間幟䜕を扱う基瀎的な数孊力や、グラフ構造を扱う基瀎的なアルゎリズムなどが必芁になりたす。 MO CAD や関連するデヌタモデルのスキヌマを蚭蚈できる方。加工ドメむンや䞋流工皋のオペレヌションずいったドメむン知識を積極的に獲埗し、補品の圢状衚珟ず䞋流工皋の凊理をバランスさせたドメむンモデリングをする必芁がありたす。 同時に、「MO CAD デヌタを利甚するアプリケヌションやアルゎリズムを開発する゚ンゞニア」も必芁になっおいきたす。次のような関連アプリケヌションが考えられたす。 MO CAD デヌタを管理するシステム。BOM や PDM/PLM ずいったドメむンに明るい方が加わっお頂けるず非垞に心匷いです。 MO CAD デヌタから加工プロセスを掚論するアルゎリズム。プロセスが掚論できれば、加工時間も予枬しやすくなり、原䟡蚈算にも぀ながっおいきたす。幟䜕的な蚈算凊理が曞けるず同時に、加工プロセスぞの理解が必芁になりたす。 難加工を自動怜出するアルゎリズム。加工が非垞に難しい箇所、あるいは䞍可胜な箇所があれば、これを自動で怜出するずいうものです。こういった難加工が受泚埌になっお芋぀かるず、倧きなトラブルに繋がりかねたせん。受泚前の早い段階で玠早く自動的に怜出できれば、お客様ず擊り合わせをしおトラブルを事前に防ぐこずが出来たす。この開発もやはり、幟䜕蚈算アルゎリズムのスキルず加工ぞの理解が必芁になるでしょう。 本プロゞェクトは、圓然ながら、決しお成功が玄束されたプロゞェクトではありたせん。ただただ倚くの䞍確実性が暪たわっおおり、それらを䞀぀䞀぀朰しおいく必芁がありたす。 そういった䞍確実性を含めお、倧きなチャレンゞに立ち向かうこずを楜しめる方のご応募をお埅ちしおおりたす 「たずはカゞュアルに話を聞いおみたい」ずいう方は、こちらより面談をお申し蟌み䞋さい。 – 応募フォヌム | JP-TECH-000.゚ンゞニア・デザむナヌカゞュアル面談 – キャディ株匏䌚瀟 遞考ぞの゚ントリヌは䞋蚘サむトよりお願いしたす。 応募フォヌム | Rust、F#を利甚したCAD開発を行うMO-CAD Engineer – キャディ株匏䌚瀟 応募フォヌム | WebGL、Typescriptを利甚したWebCAD開発を行うMO-CAD FrontEnd Engineer – キャディ株匏䌚瀟 The post Worse Is Better の粟神で Domain Specific なCADを䜜っおいる話 appeared first on CADDi Tech Blog .
はじめに こんにちは、Platformチヌムの小森です。 eBPF (extended Berkley Packet Filter) に぀いお、2022幎8月2日に開催された瀟内勉匷䌚で発衚したした。 eBPF はここ数幎で泚目が集たっおいる技術で、2021幎には eBPF Foundationが蚭立 され、Facebook、Google、Isovalent、Microsoft、Netflixなどの倧手IT䌁業が参画を進めおいたす。 筆者は抂芁皋床しか把握しおいなかったので、遅ればせながらキャッチアップのために情報収集したした。すでに倚くの情報が出回っおいるので新芏性は少ないですが、匕甚元を瀺し぀぀、短時間で理解できるようにたずめおみたした。 特に次のような方は、eBPFの抂芁を抌さえおおくず良いのではないかず思いたす。 Kubernetesに興味がある、たたは䜿っおいる Linuxの䞭でもネットワヌク呚りの基盀技術に興味がある 日々障害調査に远われおいる iptablesが倧奜き eBPF ずはなにか ざっくり抂芁 eBPF(extended Berkley Packet Filter)は、Linuxカヌネル内でのむベント発生時に動䜜する凊理を、安党・手軜に組み蟌むための仕組みで、珟圚ではLinuxカヌネルの機胜ずしお提䟛されおいたす。 eBPFは、カヌネル空間で動䜜する仮想マシンです。仮想マシンずいうず、VMWare、VirtualBox、KVMなど、ハヌドりェアを含めお゚ミュレヌトするハむパヌバむザを思い浮かべるかもしれたせんが、eBPFは専甚の呜什セットを持った仮想的なCPUのようなものです。小型のJavaVMのようなものがカヌネル内で動䜜するずむメヌゞすれば良さそうです。 JavaVMず同じように、eBPF専甚のバむトコヌドを枡すず、カヌネル内郚で怜蚌され、実際に動䜜するマシンコヌドにコンパむルされたのちに、カヌネルに組み蟌たれたす。 「Packet Filter」なのに「Virtual Machine」? なぜ、eBPFは「Packet Filter」ずいう名称なのに、実䜓は Virtual Machine なのでしょうか。理由は、その発展の歎史にありたす。 eBPFの歎史は意倖に叀く、玄30幎前の1992幎に、ロヌレンス・バヌクレヌ囜立研究所のSteven McCanneずVan Jacobsonが公開した論文「 The BSD Packet Filter: A New Architecture for User-level Packet Capture 」にさかのがりたす。 圓初はその名のずおり、パケットキャプチャやフィルタリングを効率化するための技術でした。 珟圚でも、障害調査などで特定の宛先やポヌトで通信されるパケットを取埗したいこずはよくありたす。 ネットワヌクでやりずりされるパケットをキャプチャしお必芁なものだけを抜出(フィルタ)するには、カヌネル空間で動䜜するネットワヌクドラむバはキャプチャしたパケットを、ナヌザヌ空間で動䜜するアプリケヌションに枡し、アプリケヌション偎でフィルタ凊理を行う必芁がありたした。 [出兞] : BPF Overview The BSD Packet Filter: A New Architecture for User-level Packet Capture このような䜜りでは、カヌネル空間ずナヌザヌ空間の切替が倚く発生し、効率がよくありたせん。 そこで、カヌネル空間で動䜜する仮想マシンを甚意し、その䞊でパケットをフィルタリングするアプリケヌションを実行できるようにすれば、倚くの凊理がカヌネル空間で完結するのでパフォヌマンスが向䞊するずいうのが基本的なアむデアです。 [出兞] : BPF Overview The BSD Packet Filter: A New Architecture for User-level Packet Capture BPFは圓初BSDに実装されたのち、 1997幎にLinuxカヌネル2.1.75に移怍されたした 。 BPFを利甚したパケットキャプチャずフィルタリング機胜はlibpcapずいうラむブラリずしお実装され、珟圚でもよく䜿われおいるパケットキャプチャツヌル、 tcpdump で利甚されおいたす。 そしおしばらく時がたち、2013幎、察象をネットワヌクパケットだけに限らず、より汎甚化した仕組みずしお「eBPF」が提案されたした。2014幎、eBPFがLinuxカヌネル3.17に組み蟌たれお拡匵が続き、今にいたりたす。 eBPFず察比しお、圓初のBPFを「cBPF (classic BPF)」ず呌ぶこずもありたす。なお、珟圚ではeBPFは 「 which is no longer an acronym for anything (䜕の略称でもない)」ずされおいたす。 [出兞] eBPF – 入門抂芁 ç·š – BPFの歎史 BPF – in-kernel virtual machine eBPFでなにができるか? カヌネルむベントのフック eBPFでは、ネットワヌクだけではなく、さたざたなカヌネル内のむベントをフックし、さたざたな凊理を実行するこずができたす。 フックできるカヌネルむベントは「 Program Types 」ずしお定矩されおいたす。 いく぀かの蚘事を参考にしお倧別するず、以䞋のようになりたす。Program Typeの説明は、原文のほうが分かりやすいので、翻蚳せずに出兞からそのたた匕甚したした。 ゜ケット操䜜 – パケットフィルタリングや、コネクション確立/タむムアりトなどの゜ケット属性倉曎、パケットのリダむレクトなどのむベント BPF_PROG_TYPE_SOCKET_FILTER : a network packet filter BPF_PROG_TYPE_SOCK_OPS : a program for setting socket parameters BPF_PROG_TYPE_SK_SKB : a network packet filter for forwarding packets between sockets トンネリング – ネットワヌクスタック内のパケットカプセル化フレヌムワヌクに関するむベント BPF_PROG_TYPE_LWT_* : a network packet filter for lightweight tunnels 垯域制埡 – 垯域制埡を実珟するためのむベント BPF_PROG_TYPE_SCHED_CLS : a network traffic-control classifier BPF_PROG_TYPE_SCHED_ACT : a network traffic-control action XDP(Xpress Data Path) – NICから受け取ったパケットデヌタを盎接操䜜するためのむベント BPF_PROG_TYPE_XDP : a network packet filter run from the device-driver receive path トレヌシング – カヌネル関数呌び出し、カヌネル関数内のむベント発生、パフォヌマンスカりンタなどのむベント BPF_PROG_TYPE_PERF_EVENT : determine whether a perf event handler should fire or not BPF_PROG_TYPE_KPROBE : determine whether a kprobe should fire or not BPF_PROG_TYPE_TRACEPOINT : determine whether a tracepoint should fire or not Cgroups – Cgroup(プロセスをグルヌプ化しおリ゜ヌス割り圓おを制埡する機構)におけるむベント BPF_PROG_TYPE_CGROUP_SKB : a network packet filter for control groups BPF_PROG_TYPE_CGROUP_SOCK : a network packet filter for control groups that is allowed to modify socket options BPF_PROG_CGROUP_DEVICE : determine if a device operation should be permitted or not 圓初のBPF(いわゆるcBPF)で実珟されおいたのは、ネットワヌク凊理の郚分だけでしたが、eBPFではさたざたなフックポむントが远加されおいるこずがわかりたす。 [出兞] A thorough introduction to eBPF (䞊蚘Program Typeの説明は、本蚘事より匕甚) BPF: A Tour of Program Types @IT – Berkeley Packet FilterBPF入門4 – LinuxのBPFで䜕ができるのか BPFの「プログラムタむプ」ずは eBPF - 入門抂芁 ç·š 「おいしくお぀よくなる」eBPFのはじめかた ナヌザヌランドアプリケヌションずのやりずり eBPFプログラムは、Program Typesでフックしたむベントによっお動䜜し、凊理結果をナヌザヌ空間で実行するアプリケヌションず受け枡しするこずができたす。 具䜓的には、 Maps ずいう機構が提䟛されおおり、ハッシュテヌブルや配列、LRU、リングバッファなどの各皮デヌタ構造が䜿えたす。 eBPFの䞻な甚途 eBPFが䜿われおいるプロダクトは、 こちら のサむトで玹介されおいたすが、いく぀かを簡単に玹介したす。 ネットワヌク制埡 Cilium コンテナ間通信に察しおパケット凊理に可芳枬性・セキュリティ・高床な通信制埡を付䞎する゜フトりェア。䞻にKubernetesでの利甚を想定 Katran Facebookが開発する、XDPを掻甚した高性胜L4ロヌドバランサ Pixie Kubernetesアプリケヌションから自動でテレメトリデヌタを取埗しおObservability(可芳枬性)を向䞊させるツヌル Cloudflare Magic Firewall プロダクトではないが、Cloudflare のDDoS察策機胜ずしお利甚されおいる セキュリティ Falco アプリケヌションの異垞な動䜜を怜出するアクティビティモニタ Tetragon 透過的にセキュリティや可芳枬性を適甚するためのツヌル トレヌシング bpftrace BPFをラップするDSL(Domain Specific Language)を提䟛する、汎甚的なトレヌシングツヌル このように、eBPFが提䟛するさたざたなフックポむントを掻甚したプロダクトが䜜られおいたす。 eBPFが泚目される背景 近幎eBPFが泚目されおいる背景には、Kubernetes(以䞋、k8s)の普及によるアプリケヌションのコンテナ化やマむクロサヌビス化が進んでいるこずがあるず思われたす。 k8sでコンテナ間の通信を実珟するには、Linuxに叀くからある netfilter/iptables が䜿われおいたす。 netfilter は、BPFず同様にカヌネル内のネットワヌクスタックのさたざたな堎所に、コヌルバック関数を远加できるしくみで、iptablesはそのフロント゚ンドツヌルです。 iptablesはnetfilterを䜿っおパケットのフィルタリングや転送を実珟し、Linuxによるファむアりォヌルやネットワヌクルヌタの実珟には、叀くからiptablesが利甚されおいたした。 さきほど述べたように、k8sにおけるコンテナ間通信にも、netfilter で実珟されおいたす。 出兞:Netfilter - Wikipedia しかし、k8sクラスタで管理されるコンテナが増えるにしたがっお、iptablesの問題点が浮き䞊がっおきたした。 iptablesは、ルヌル順番に凊理しおマッチしたパケットを凊理する仕組みなので、ルヌルの量(぀たり通信するコンテナの数)に比䟋しお遅くなっおしたいたす。 たずえば、eBPFの機胜の1぀であるXDP(Express Data Path)を掻甚するこずで、NICに近いずころでパケットを受け取り、独自に凊理しお高速なコンテナ間通信を実珟しようずしおいるのが、eBPFを掻甚したプロダクトでも特に泚目を集めおいる Cilium です。 (Ciliumに぀いおは、次回前倚さんが玹介予定です) たた、コンテナ環境を掻甚しおシステムのマむクロサヌビス化が進むず、安定運甚のために通信や各皮メトリクス、パフォヌマンスを把握する必芁性が高たり、可芳枬性やセキュリティもより重芁芖されたす。 このようなニヌズにも、冒頭で玹介したeBPFの倚圩なフックポむントず、カヌネル空間内で高速凊理できるずいう特性がマッチしおいるず思われたす。 [出兞] iptablesの埌に来るものは䜕か nftables - 赀垜゚ンゞニアブログ eBPFの仕組み アヌキテクチャず凊理フロヌ eBPFの抂芁や背景がわかったずころで、その仕組みをもう少し远っおみたしょう。 eBPF - 入門抂芁 ç·š – eBPFアヌキテクチャ抂芁 で説明されおいる図が分かりやすかったので、これを参考にアヌキテクチャの説明図を曞きたした。 この図に䜿っお、eBPFの倧たかな凊理フロヌを説明したす。 eBPFを扱うナヌザヌプログラムは、eBPF゜ヌスコヌドをコンパむルしおバむトコヌドに倉換する システムコヌルを䜿甚しおeBPFバむトコヌドをカヌネルぞロヌドする eBPF Verifierがバむトコヌドを怜蚌。問題なければコンパむラによっお機械語に倉換する ナヌザヌプログラムはeBPFプログラムを目的のむベントにアタッチする むベントが発生するず、ExecutorがeBPFプログラムを実行 凊理結果を map や ring buffer ぞ栌玍 ナヌザヌプログラムが結果を参照 カヌネルモゞュヌルずeBPFの違い もずもず、Linuxには「 カヌネルモゞュヌル 」ずいう仕組みがあり、カヌネルモゞュヌルを䜜成するこずでカヌネル空間で動く凊理を远加で組み蟌めるようになっおおり、これを䜿っおカヌネルの機胜を拡匵できるようになっおる。 カヌネルモゞュヌルの代衚䟋ずしおは、ファむルシステムや、デバむスドラむバなどハヌドりェアを制埡するプログラムが挙げられたす。 䞀方で、カヌネルモゞュヌルは自由床が高すぎるため、䜜りが悪いずカヌネルをクラッシュさせる危険性がありたす。たた、 modprobe コマンドで明瀺的にロヌド/アンロヌドする必芁がありたす。もずもずデバむスドラむバやファむルシステムなどの䜿い方が想定されおいたので、頻繁にロヌド/アンロヌドするこずは想定されおおらず、特定のアプリケヌション実行時だけカヌネルに凊理を組み蟌むずいった䜿い方がしにくいです。 eBPFでは、ナヌザヌ空間で動䜜するアプリケヌションからシステムコヌルを呌び出すこずで、カヌネル空間で動䜜するeBPFプログラムを動的に組み蟌むこずができたす。 たた、eBPFでは実行されるプログラムに䞀定の制玄を぀け、Verifierによる怜蚌をパスしなければカヌネルに組み蟌めないような仕組みずしお安党性を高めおいたす。 Linuxのリポゞトリに、怜蚌噚ののコヌド( verifier.c )がありたす。こちらのコメントを読むず、たずえば最初の段階では、次のようなチェックが行われるようです。 ( 参考1 , 参考2 ) 呜什数が䞀定数以䞋であるこず(倧きなプログラムはダメ) ルヌプがないこず 到達䞍可胜な呜什がないこず 境界の倖に出る䞍正なゞャンプがないこず 特に呜什数やルヌプなどの制玄は、カヌネル空間で実行される凊理であるこずから、厳しめになっおいるこずがわかりたす。 このような仕組みによっお、カヌネル空間での凊理を手軜か぀安党にアドオンできるようになったこずがeBPFずカヌネルモゞュヌルの違いです。eBPFによっおカヌネル空間での凊理を手軜に䜜成できるようになったため、埓来のようにカヌネル空間ずナヌザヌ空間の切替を頻繁に行う必芁もなくなり、パフォヌマンス向䞊にも寄䞎できるようになりたした。 [参考] Linux カヌネルハックを始める前に知っおおきたいこず - かヌねるさんずか 【図解】初心者向けナヌザヌ空間ずカヌネル空間,システムコヌル,MMU/メモリ保護,の仕組み | SEの道暙 eBPFプログラムの䜜り方 eBPFプログラムの実際の䜜り方を芋おみたしょう。 eBPFは最終的に専甚のバむトコヌドにしおカヌネルに枡す必芁がありたす。いきなりバむトコヌドを曞くわけにもいかない(曞くこずもできたすが)ので、珟実的にはなんらかのプログラミング蚀語を䜿うこずになりたす。 eBPFのプログラムはC蚀語で䜜成したす。 BCC(BPF Compiler Collection) ずいうツヌルチェヌンが公開されおいるので、これを䜿甚しおコンパむルしたす。 䞀方、eBPFプログラムをカヌネルに組み蟌み、eBPFの凊理結果を受け取っおナヌザヌに機胜を提䟛するアプリケヌション)を、eBPFのフロント゚ンドず呌びたす。 eBPFのフロント゚ンドは、BCCの公匏サポヌト範囲では、C++、Python、Luaで蚘述するこずができたす。(䞖の䞭のサンプルを芋枡すず、簡単なツヌルはPythonで曞かれおいるものが倚いようです) たた、BCC本䜓のサポヌト範囲ではありたせんが、RustやGoずいった新しい蚀語でもフロント゚ンドが䜜成できるようです。 たた、さきほど玹介したbpftraceは、独自の蚀語(DSL:Domain Specific Language)を提䟛しおおり、トレヌス甚途䞭心ではありたすが、C蚀語を知らなくおもeBPFずしお動䜜する凊理を䜜成するこずができたす。 bpftraceのGitHubリポゞトリ には、倚数のサンプルが公開されおいたす。 たずえば、次のようなワンラむナヌでプロセスが開くファむルを衚瀺させるこずができたす。 # Files opened by process bpftrace -e 'tracepoint:syscalls:sys_enter_open { printf("%s %s\n", comm, str(args->filename)); }' このように、コマンドラむンからアドホックにeBPF凊理を実行できるのがbpftraceの魅力です。 eBPFプログラムを䜜っおみる ここからは、実際にeBPFのプログラムを䜜成しお理解を深めたす。 環境の準備 今回は、 GCP の Computing Engine に CentOS8 の仮想マシンを䜜り、そこで詊したした。 先ほど玹介したbpftraceが暙準のパッケヌゞマネヌゞャで提䟛されおいるので、コマンド䞀発でむンストヌルできたす。 sudo dnf -y install bpftrace bpftraceをむンストヌルするず、bccも同時にむンストヌルされお䜿えるようになりたす。 Hello world BCCの公匏リポゞトリ にあるサンプルから、もっずも簡単なものを遞んで実行しおみたす。 hello_world.py を少しだけ分かりやすく曞き盎したものが、以䞋のコヌドです。 このサンプルは、Pythonをフロント゚ンドずしお曞かれおいたす。 from bcc import BPF bpf_src=""" int kprobe__sys_clone(void *ctx) { bpf_trace_printk("Hello, World!\\n"); return 0; } """ b = BPF(text=bpf_src) b.trace_print() 䞊のプログラムの最終行で BPF 関数に枡しおいるテキストのC蚀語郚分が、eBPFのコヌドです。 抜き出したものが以䞋のコヌドです。 int kprobe__sys_clone(void *ctx) { bpf_trace_printk("Hello, World!\\n"); return 0; } むベント名__関数名 ずいう呜名ルヌルで、カヌネル内の関数にフックできたす。(ここでは、 kprobe__sys_clone ) kprobe は、カヌネル内の関数呌び出し前のむベントであるこずを瀺したす。 sys_clone は、Linuxでプロセスがforkされるずきに呌び出されるシステムコヌルです。(この関数はカヌネルのバヌゞョンやCPUアヌキテクチャによっお異なるこずがあるので泚意 (※)) eBPFプログラムの3行目では、 bpf_trace_printk ずいう関数を呌び出しおいたす。これは䞻にeBPFのデバッグ甚途でカヌネル空間からナヌザヌ空間ぞ文字列を枡すためのものです。 Pythonで曞いたフロント゚ンドコヌドの11行目で BPF 関数を呌び出すこずにより、eBPFプログラムをコンパむルされ、カヌネルぞ組み蟌たれたす。 そしお、12行目の trace_print 関数で、 bpf_trace_printk が出力した文字列を受け取っお衚瀺しおいたす。 (このあたりの仕組みは eBPF の玹介 - Qiita で詳しく解説されおいたす) このサンプルを実行し、別のタヌミナルでコマンドを実行するず、そのたびに「Hello, World!」が衚瀺されたす。 実甚性はありたせんが、eBPFプログラムの曞き方ず組み蟌み方、結果の受け取りかたがわかりたした。 (※) 「この関数はカヌネルのバヌゞョンやCPUアヌキテクチャによっお異なるこずがあるので泚意」・・・BPFのプログラムは、その特性䞊CPUアヌキテクチャやカヌネルのバヌゞョンに䟝存しおしたいたす。このためBPFを実際に実行するマシン䞊でコンパむルするのが原則のようです。このあたりのポヌタビリティを高めるしくみずしお、 BPF CO-RECompile Once - Run Everywhere が考案されおおり、ある環境でコンパむルしたBPFコヌドを他の環境でも実行できるようにしおいたす。 もう少し耇雑なサンプル eBPF偎からフロント゚ンド偎に情報を枡す、もう少し実甚的なサンプルを動かしおみたす。 第690回 BCCでeBPFのコヌドを曞いおみる | gihyo.jp で玹介されおいるコヌドを写経したした。 eBPF郚分には、少しコメントを远加したした。 #!/usr/bin/python3 from bcc import BPF bpf_text=""" #include <linux/sched.h> /* Information passed from eBPF to frontend */ struct data_t { u32 pid; u32 ppid; char comm[TASK_COMM_LEN]; char fname[128]; }; /* Make ring buffer named `events` */ BPF_PERF_OUTPUT(events); int syscall__execve(struct pt_regs *ctx, const char __user *filename) { struct data_t data = {}; struct task_struct *task; /* Get PID */ data.pid = bpf_get_current_pid_tgid() >> 32; /* Get Parent PID */ task = (struct task_struct *)bpf_get_current_task(); data.ppid = task->real_parent->tgid; /* Get current task program name */ bpf_get_current_comm(&data.comm, sizeof(data.comm)); /* Get execve argument from user space */ bpf_probe_read_user(data.fname, sizeof(data.fname), (void *)filename); /* Stores data in a ring buffer */ events.perf_submit(ctx, &data, sizeof(struct data_t)); return 0; } """ b = BPF(text=bpf_text) b.attach_kprobe(event=b.get_syscall_fnname("execve"), fn_name="syscall__execve") print("PID PPID COMM FNAME") def print_event(cpu, data, size): # Get eBPF data from `event` ring buffer event = b["events"].event(data) print("{:<8} {:<8} {:16} {}".format(event.pid, event.ppid, event.comm.decode(), event.fname.decode())) b["events"].open_perf_buffer(print_event) while True: try: b.perf_buffer_poll() except KeyboardInterrupt: exit() 今床は、eBPF郚分が長くなっおいたすが、関数は syscall__execve の1぀です。 execve は、指定したファむルを実行する時に䜿甚するシステムコヌルです。システムコヌルの匕数をeBPFから取埗する堎合は、 syscall むベントでアタッチしたす。 execve システムコヌルが実行されるずきに、その匕数をeBPFで取埗しお data_t 構造䜓に栌玍し、リングバッファ経由でナヌザヌ空間で動くフロント゚ンドぞ送りたす。 フロント゚ンド偎では、リングバッファからデヌタを取埗しお敎圢しお出力しおいたす。 実行䟋は以䞋のようになりたす。プログラムを実行しお他のタヌミナルで ls や cat コマンドを実行するず、そのログが衚瀺されたすし、バックグラりンドで実行されおいるプログラムも怜知できおいたす。 その他のサンプル 最初に玹介した、BCCのリポゞトリには豊富なサンプルが公開されおおり、サンプルに留たらず、実甚性のあるツヌルも倚数公開されおいたす。倚くは数癟行皋床なので、勉匷がおら自分の甚途に改造しお䜿うこずもできそうです。 ツヌルずサンプルの䞀芧はこちらです。 https://github.com/iovisor/bcc#contents 最埌に、このなかから実甚性がありそうなものを、いく぀か遞んで玹介したす。 HTTPリク゚ストのダンプ https://github.com/iovisor/bcc/tree/master/examples/networking/http_filter TCP接続先の調査 tcpv4connect_example.txt tcpv4connect.py PID COMM SADDR DADDR DPORT 1479 telnet 127.0.0.1 127.0.0.1 23 1469 curl 10.201.219.236 54.245.105.25 80 1469 curl 10.201.219.236 54.67.101.145 80 tcplife 実行䞭に開始/終了されたTCPセッションを蚘録したす。 tcplife_example.txt tcplife.py PID COMM LADDR LPORT RADDR RPORT TX_KB RX_KB MS 22597 recordProg 127.0.0.1 46644 127.0.0.1 28527 0 0 0.23 3277 redis-serv 127.0.0.1 28527 127.0.0.1 46644 0 0 0.28 22598 curl 100.66.3.172 61620 52.205.89.26 80 0 1 91.79 22604 curl 100.66.3.172 44400 52.204.43.121 80 0 1 121.38 22624 recordProg 127.0.0.1 46648 127.0.0.1 28527 0 0 0.22 3277 redis-serv 127.0.0.1 28527 127.0.0.1 46648 0 0 0.27 22647 recordProg 127.0.0.1 46650 127.0.0.1 28527 0 0 0.21 3277 redis-serv 127.0.0.1 28527 127.0.0.1 46650 0 0 0.26 [...] dirtop ディレクトリ単䜍でのread/writeの蚘録。 dirtop_example.txt dirtop.py # ./dirtop.py -d '/hdfs/uuid/*/yarn' Tracing... Output every 1 secs. Hit Ctrl-C to end 14:28:12 loadavg: 25.00 22.85 21.22 31/2921 66450 READS WRITES R_Kb W_Kb PATH 1030 2852 8 147341 /hdfs/uuid/c11da291-28de-4a77-873e-44bb452d238b/yarn 3308 2459 10980 24893 /hdfs/uuid/bf829d08-1455-45b8-81fa-05c3303e8c45/yarn 2227 7165 6484 11157 /hdfs/uuid/76dc0b77-e2fd-4476-818f-2b5c3c452396/yarn 1985 9576 6431 6616 /hdfs/uuid/99c178d5-a209-4af2-8467-7382c7f03c1b/yarn 1986 398 6474 6486 /hdfs/uuid/7d512fe7-b20d-464c-a75a-dbf8b687ee1c/yarn 764 3685 5 7069 /hdfs/uuid/250b21c8-1714-45fe-8c08-d45d0271c6bd/yarn [...] filetop ファむル単䜍でのread/writeしたプロセスのトレヌス filetop_example.txt filetop.py # ./filetop -C Tracing... Output every 1 secs. Hit Ctrl-C to end 08:00:23 loadavg: 0.91 0.33 0.23 3/286 26635 PID COMM READS WRITES R_Kb W_Kb T FILE 26628 ld 161 186 643 152 R built-in.o 26634 cc1 1 0 200 0 R autoconf.h 26618 cc1 1 0 200 0 R autoconf.h 26634 cc1 12 0 192 0 R tracepoint.h 26584 cc1 2 0 143 0 R mm.h 26634 cc1 2 0 143 0 R mm.h 26631 make 34 0 136 0 R auto.conf [...] oomkill OOM killerによるプロセスkillむベントのトレヌス。 oomkill_example.txt oomkill.py # ./oomkill Tracing oom_kill_process()... Ctrl-C to end. 21:03:39 Triggered by PID 3297 ("ntpd"), OOM kill of PID 22516 ("perl"), 3850642 pages, loadavg: 0.99 0.39 0.30 3/282 22724 21:03:48 Triggered by PID 22517 ("perl"), OOM kill of PID 22517 ("perl"), 3850642 pages, loadavg: 0.99 0.41 0.30 2/282 22932 たずめ eBPF登堎の背景ず䜍眮づけを確認し、倧たかな仕組みずサンプルの実行を通しおeBPFプログラムの䜜り方を確認したした。 発展䞭の技術であるため、䜓系的な情報はただ少ない印象ですが、サンプルが豊富なので詊行錯誀しながら習埗しおいくこずもできそうです。 䞀方で䜿いこなすにはC蚀語やカヌネルのシステムコヌルなど、比范的䜎レむダヌの知識が必芁になるため、習埗には䞀定の時間がかかりそうです。(おそらく、bpftraceでこのハヌドルが䞋がりそうです) eBPFはなにに䜿えるか 党䜓ずしおは、採甚プロダクト䟋で挙げたように、特にネットワヌク呚りの基盀技術や、可芳枬性(Observability)の匷化が䞻な甚途になるず思いたした。 䞀般の゚ンゞニアがeBPFのコヌドをバリバリ曞けるようになる必芁は、あたり無さそうですが、eBPFの仕組みを理解しおおくず、これから登堎するeBPFを掻甚したプロダクトを䜿いこなす時に理解が進むず思いたす。 たた、bpftraceのようなツヌルを䜿いこなせるようになるず、障害調査に䜿える歊噚ずしお倧いに圹立ちそうです。 個人的には、システム運甚の堎面で監査やセキュリティチェックなど、プロダクト独自のニヌズが生じたずきに、eBPFを掻甚できるのではないかず期埅しおいたす。 ここたでお読みいただきありがずうございたした。 CADDiでは、珟圚積極的に採甚を行っおいたす。 たずはカゞュアルにお話を聞いおみたいずいう方は、ぜひ こちら より面談をお申し蟌みください。 たた、Tech Blogや勉匷䌚等のむベントに぀いおはSNSで随時発信しおおりたすので、 Twitter のフォロヌや、 connpass のメンバヌ登録をぜひよろしくお願いしたす。 参考サむト 蚘事䞭でも断片的に瀺したしたが、eBPFに぀いお䜓系的に玹介されおいお参考になったサむトを玹介したす。 IT連茉 > Berkeley Packet FilterBPF入門 å…š10回の連茉でかなりボリュヌムがありたすが、Web䞊の日本語での説明が䞀番よくたずたっおいたす。著者はbpftraceのコミッタでもありたす。 1 : パケットフィルタヌでトレヌシング Linuxで掻甚が進む「Berkeley Packet FilterBPF」ずは䜕か 2 : BPFのアヌキテクチャ、呜什セット、cBPFずeBPFの違い 3 : BPFプログラムの䜜成方法、BPFの怜蚌噚、JITコンパむル機胜 4 : LinuxのBPFで䜕ができるのか BPFの「プログラムタむプ」ずは 5 : BPFによるパケットトレヌス――C蚀語によるBPFプログラムの䜜り方、䜿い方 6 : BCCBPF Compiler CollectionによるBPFプログラムの䜜成 7 : Linux 5.5におけるBPFBerkeley Packet Filterの新機胜 8 : BPFを䜿ったLinuxにおけるトレヌシングの基瀎知識 9 : BPFによるトレヌシングが簡単にできる「bpftrace」の䜿い方 10 : 単なるデバッグ情報だけではない「BPF Type Format」BTFの䜿い道 「eBPF – 入門」 必芁な芁玠がコンパクトにたずたっおいたす。私もこの蚘事を取っ掛かりずしたした。 eBPF – 入門抂芁 ç·š eBPF – 仮想マシン ç·š eBPF – BCCチュヌトリアル ç·š eBPF – bpftraceチュヌトリアル ç·š eBPF – XDP抂芁 ç·š The post eBPFに3日で入門した話 appeared first on CADDi Tech Blog .
はじめに 前線 では、 setter メ゜ッドによる倀の蚭定や build メ゜ッドによる構造䜓の生成などの基本的な機胜を持った手続き的マクロを実装したした。埌線では以䞋の機胜を実装しおいきたす。 Optional な倀を構造䜓のフィヌルドずしお持おるようにする 以䞋の 2 ぀の方法で Vec 型のフィヌルドを曎新できるようにする ベクタを䞎えお䞀括で曎新する ベクタの芁玠を䞎えお 1 ぀ず぀フィヌルドに芁玠を远加する コンパむル゚ラヌが発生した際にわかりやすいメッセヌゞを衚瀺する builder マクロを䜜る続き 06-optional-field 目暙 構造䜓のフィヌルドずしお Optional な倀を持おるようにする Optional なフィヌルドには倀が入っおいなくおも build メ゜ッドで構造䜓を生成できる Optional でないフィヌルドは倀が入っおいないず build メ゜ッドで構造䜓を生成できない Optional なフィヌルドは Some でラップせずに䞭身の倀をそのたた䜿っお初期化できる 最埌の項目に぀いおですが、たずえば Command 構造䜓が以䞋のようになっおいる堎合、 pub struct Command { executable: String, args: Vec<String>, env: Vec<String>, current_dir: Option<String>, } current_dir は以䞋のように String を枡すだけでよいずいうこずです。 let command = Command::builder() .executable("cargo".to_owned()) .args(vec!["build".to_owned(), "--release".to_owned()]) .env(vec![]) .current_dir("..".to_owned()) .build() .unwrap(); 実装方針 目暙ずする機胜を実珟するために実装する必芁があるのは以䞋の項目です。たずはこれらの機胜を実装しおいきたしょう。 ガヌド節で Optional でない型のみ゚ラヌを出すようにする Option でラップされた型はアンラップしお CommandBuilder 構造䜓のフィヌルドで保持する 今は元の型が䜕であっおも Option でラップするようになっおいたす。そのため、Optional な型は Option<Option<_>> のようになりたす。このたただず扱いづらいので、Optional な型はいったんアンラップしお、すべおのフィヌルドの型が Option<_> になるようにしたす Optional な型の setter メ゜ッドはラップされた䞭身の型を匕数ずしお受け付けるようにする 実装 実装方針で説明した機胜を実装しおいきたす。 ガヌド節で Optional でない型のみ゚ラヌを出すようにする ガヌド節を生成する郚分の実装は以䞋のようになっおいたす。今はすべおのフィヌルドに察しお None であるかのチェックを生成しおいたす。これを Optional でない型のみガヌド節を生成するように倉曎したす。 let checks = idents.iter().map(|ident| { let err = format!("Required field '{}' is missing", ident.to_string()); quote! { if self.#ident.is_none() { return Err(#err.into()) } } }); Optional でないフィヌルドのみガヌド節を生成するためには、各フィヌルドの型を芋お Optional でないフィヌルドのみをフィルタすれば良さそうです。 types に各フィヌルドの型が栌玍されおいるので、以䞋のように filter を甚いお Optional でないフィヌルドの識別子に぀いおだけガヌド節を生成したす。 is_option は䞎えられた型が Optional であるかどうかを刀定する䜕らかの関数です。のちほど実装したす。 let checks = idents .iter() .zip(&types) .filter(|(_, ty)| !is_option(ty)) .map(|(ident, _)| { let err = format!("Required field '{}' is missing", ident.to_string()); quote! { if self.#ident.is_none() { return Err(#err.into()) } } }); Option でラップされた型はアンラップしお CommandBuilder 構造䜓のフィヌルドで保持する 今の Builder 構造䜓の定矩は以䞋のようになっおいたす。すべおのフィヌルドを Option でラップしおいたす。Optional なフィヌルドに぀いおは、あらかじめ Option でラップされた型を取り出しおおけば、あずの凊理は今たでず同じ内容になりたす。 #vis struct #builder_name { #(#idents: Option<#types>),* } 実際に実装しおいきたす。 Option の䞭身の型を取り出すなどの凊理が远加されるので、生成された Builder 構造䜓のフィヌルドを builder_fields にいったん保持しおあずで展開したしょう。 builder_fields の実装は以䞋のようになりたす。 unwrap_option 関数で Option にラップされおいる型を取り出しおいる以倖は今たでず同じです。 unwrap_option は is_option ず同じくのちほど実装したす。 let builder_fields = idents.iter().zip(&types).map(|(ident, ty)| { let t = unwrap_option(ty).unwrap_or(ty); quote! { #ident: Option<#t> } }); builder_fields は Builder 構造䜓の定矩を生成する郚分で展開したす。 #vis struct #builder_name { #(#builder_fields),* } Builder 構造䜓の定矩が倉わったため build 関数も倉える必芁がありたす。今は以䞋のように目的の構造䜓を生成する際にすべおのフィヌルドを unwrap しおいたすが、Optional なフィヌルドは unwrap する必芁がないのでそのたた返すようにしたしょう。 pub fn build(&mut self) -> Result<#ident, Box<dyn std::error::Error>> { #(#checks)* Ok(#ident { #(#idents: self.#idents.clone().unwrap()),* }) } Optional なフィヌルドかどうかを刀定する凊理が远加されるため、 builder_fields ず同様に別の倉数に栌玍しおのちほど展開したす。具䜓的な実装は以䞋の通りです。 is_option で Optional なフィヌルドかどうかを刀定しお、Optional であれば倀をそのたた䜿甚し、Optional でなければ unwrap しお埗られた倀を䜿甚したす。 let struct_fields = idents.iter().zip(&types).map(|(ident, ty)| { if is_option(ty) { quote! { #ident: self.#ident.clone() } } else { quote! { #ident: self.#ident.clone().unwrap() } } }); struct_fields は build 関数の䞭で展開したす。 pub fn build(&mut self) -> Result<#ident, Box<dyn std::error::Error>> { #(#checks)* Ok(#ident { #(#struct_fields),* }) } Optional な型の setter メ゜ッドはラップされた䞭身の型を匕数ずしお受け付けるようにする 今の setter の実装は以䞋のようになっおいたす。Optional なフィヌルドかどうかは関係なく、すべおのフィヌルドに぀いおそのフィヌルドの型をそのたた受け付けるようになっおいたす。 impl #builder_name { #(pub fn #idents(&mut self, #idents: #types) -> &mut Self { self.#idents = Some(#idents); self })* ... ぀たり Optional なフィヌルドに぀いおは以䞋のような setter が生成されたす。これでは Builder 構造䜓のフィヌルド定矩ず矛盟したす。そのため、Optional なフィヌルドの setter 関数は匕数ずしお Option の䞭身の型を受け取るようにしたす。 pub fn current_dir(&mut self, current_dir: Option<String>) -> &mut Self { self.current_dir = Some(current_dir); self } 具䜓的には以䞋のような setter を䜜っお展開するように曞き換えたす。Builder 構造䜓のフィヌルドず同様、 unwrap_option 関数を䜿っお Option でラップされた䞭身の型を取り出したす。 let setters = idents.iter().zip(&types).map(|(ident, ty)| { let t = unwrap_option(ty).unwrap_or(ty); quote! { pub fn #ident(&mut self, #ident: #t) -> &mut Self { self.#ident = Some(#ident); self } } }); ... impl #builder_name { #(#setters)* ... is_option ず unwrap_option の実装 is_option ず unwrap_option を実装しおいきたす。たずは is_option 関数から実装しおいきたしょう。 is_option 関数 is_option は Type 型を受け取っおそれが Option かどうかを刀定する関数なのでシグネチャは以䞋のようにすれば良さそうです。 fn is_option(ty: &Type) -> bool ty が Option かどうか刀定するのに䜿えそうな Type のメ゜ッドがあるか確認しおみたしょう。 syn クレヌトのドキュメント を確認したずころ Type は以䞋の enum のようです。 pub enum Type { Array(TypeArray), BareFn(TypeBareFn), Group(TypeGroup), ImplTrait(TypeImplTrait), Infer(TypeInfer), Macro(TypeMacro), Never(TypeNever), Paren(TypeParen), Path(TypePath), Ptr(TypePtr), Reference(TypeReference), Slice(TypeSlice), TraitObject(TypeTraitObject), Tuple(TypeTuple), Verbatim(TokenStream), // some variants omitted } Option がどのバリアントに分類されるかはただわかりたせんが、ずりあえずパタヌンマッチで凊理すれば良さそうです。 fn is_option(ty: &Type) -> bool { match ty { todo!() } } パタヌンマッチで分類できそうだずいうずころたで方針を立おられたしたが、 Option はどのバリアントに分類されるのでしょうか。ドキュメントを䞀芋しおもそれらしいものは芋圓たりたせんが、 Option は Type::Path(syn::TypePath) に分類されたす。 TypePath は std::iter::Iter のような Path をパヌスしお埗られる構造䜓です。 Type::Path にマッチしないバリアントに぀いおはこの時点で false を返しおしたっお問題ないでしょう。 fn is_option(ty: &Type) -> bool { match ty { Type::Path(path) => todo!(), _ => false } } TypePath には Option 以倖にも䞊述の std::iter::Iter のようなものも含たれたす。どのように Option かそれ以倖かを刀定すれば良いでしょうか。 先ほども説明したように、 TypePath は std::iter::Iter のようにコロン 2 ぀で分割されたセグメントの集合でした。぀たり、セグメントの集合の最埌の芁玠が Option であるかどうかを刀断すれば良さそうです。 では、どのようにしおセグメントの集合の最埌の芁玠を取埗すれば良いのでしょうか。 syn::TypePath は以䞋のような構造䜓で、 path に Path の情報を保持しおいたす。 syn::Path は以䞋のように segments にセグメントの集合を保持しおおり、 segments.last() で最埌の芁玠にアクセスできたす。 pub struct TypePath { pub qself: Option<QSelf>, pub path: Path } pub struct Path { pub leading_colon: Option<Colon2>, pub segments: Punctuated<PathSegment, Colon2>, } 䞊蚘を螏たえるず、珟時点での実装は以䞋のようになりたす。 fn is_option(ty: &Type) -> bool { match ty { Type::Path(path) => path.path.segments.last(), _ => false } } 最埌に、セグメントの最埌の芁玠の識別子が Option ず䞀臎するか比范する必芁がありたす。 segments.last() の戻り倀は PathSegment 型 であり、 ident フィヌルドから識別子にアクセスできたす。この識別子が Option かどうかを比范すれば良さそうです。 最終的な is_option 関数の実装は以䞋のようになりたす。 fn is_option(ty: &Type) -> bool { match ty { Type::Path(path) => match path.path.segments.last().unwrap() { Some(seg) => seg.ident == "Option", None => false }, _ => false } } unwrap_option 関数 次は unwrap_option 関数を実装しおきたす。 unwrap_option 関数は䞎えられた型が Option であれば Some(型) を返し、そうでなければ None を返す関数なので、シグネチャは以䞋のようにすれば良いでしょう。 fn unwrap_option(ty: &Type) -> Option<&Type> 匕数が Option でない堎合は None を返したす。匕数が Option かどうかは先ほど実装した is_option 関数が䜿えたす。 fn unwrap_option(ty: &Type) -> Option<&Type> { if !is_option(ty) { return None; } todo!() } 次に Option にラップされた䞭身の型を取り出す凊理を実装したす。䞭身の型はどうやっお取り出せば良いでしょうか。 is_option の実装ででおきた PathSegment のフィヌルドには ident 以倖にもう 1 ぀ arguments ずいうのがありたした。これが関係ありそうなので、 PathArguments の定矩を芋お芋たしょう。 pub enum PathArguments { None, AngleBracketed(AngleBracketedGenericArguments), Parenthesized(ParenthesizedGenericArguments), } PathArguments は enum のようです。バリアント名を眺めおみるず AngleBracketed ずいうものがありたす。これが関係ありそうです。実際、 ドキュメント の AngleBracketed の項目には以䞋のように蚘茉されおおり、このバリアントがゞェネリック匕数の情報を持っおいるこずがわかりたす。 AngleBracketed(AngleBracketedGenericArguments) The <'a, T> in std::slice::iter<'a, T> . syn::AngleBracketedGenericArgument 型の定矩を芋おみたしょう。 pub struct AngleBracketedGenericArguments { pub colon2_token: Option<Colon2>, pub lt_token: Lt, pub args: Punctuated<GenericArgument, Comma>, pub gt_token: Gt, } フィヌルド名を眺めおみるずゞェネリック匕数の型は args に入っおいそうです。 syn::Punctuated なので耇数の芁玠がありそうですが、 Option は匕数を 1 ぀しか取らないので first() で取埗すれば良いでしょう。 fn unwrap_option(ty: &Type) -> Option<&Type> { if !is_option(ty) { return None; } match ty { Type::Path(path) => path.path.segments.last().map(|seg| { match seg.arguments { PathArguments::AngleBracketed(ref args) => args.args.first(), _ => None } }), _ => None } } args.first() の戻り倀は Option<GenericArgument> です。 GenericArgument は以䞋のような enum で、型が含たれる堎合は GenericArgument::Type バリアントが䜿甚されるので、 Type バリアントにマッチさせれば良いでしょう。 pub enum GenericArgument { Lifetime(Lifetime), Type(Type), Binding(Binding), Constraint(Constraint), Const(Expr), } 䞊蚘を螏たえるず、 unwrap_option の最終的な実装は以䞋のようになりたす。 fn unwrap_option(ty: &Type) -> Option<&Type> { if !is_option(ty) { return None; } match ty { Type::Path(path) => path.path.segments.last().map(|seg| { match seg.arguments { PathArguments::AngleBracketed(ref args) => { args.args.first().and_then(|arg| match arg { &GenericArgument::Type(ref ty) => Some(ty), _ => None, }) } _ => None } }), _ => None } } Path の最埌の芁玠を持っおくる凊理はたずめられるので別の関数ずしお倖に切り出したす。これを甚いお is_option ず unwrap_option を曞き盎すず、最終的には以䞋のようになりたす。 fn get_last_path_segment(ty: &Type) -> Option<&PathSegment> { match ty { Type::Path(path) => path.path.segments.last(), _ => None, } } fn is_option(ty: &Type) -> bool { match get_last_path_segment(ty) { Some(seg) => seg.ident == "Option", _ => false, } } fn unwrap_option(ty: &Type) -> Option<&Type> { if !is_option(ty) { return None; } match get_last_path_segment(ty) { Some(seg) => match seg.arguments { PathArguments::AngleBracketed(ref args) => { args.args.first().and_then(|arg| match arg { &GenericArgument::Type(ref ty) => Some(ty), _ => None, }) } _ => None, }, None => None, } } 最終的な実装は以䞋のようになりたす。 use proc_macro::TokenStream; use quote::{format_ident, quote}; use syn::{ parse_macro_input, Data, DeriveInput, Fields, GenericArgument, Ident, PathArguments, PathSegment, Type, }; #[proc_macro_derive(Builder)] pub fn derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let ident = input.ident; let vis = input.vis; let builder_name = format_ident!("{}Builder", ident); let (idents, types): (Vec<Ident>, Vec<Type>) = match input.data { Data::Struct(data) => match data.fields { Fields::Named(fields) => fields .named .into_iter() .map(|field| { let ident = field.ident; let ty = field.ty; (ident.unwrap(), ty) }) .unzip(), _ => panic!("no unnamed fields are allowed"), }, _ => panic!("expects struct"), }; let builder_fields = idents.iter().zip(&types).map(|(ident, ty)| { let t = unwrap_option(ty).unwrap_or(ty); quote! { #ident: Option<#t> } }); let checks = idents .iter() .zip(&types) .filter(|(_, ty)| !is_option(ty)) .map(|(ident, _)| { let err = format!("Required field '{}' is missing", ident.to_string()); quote! { if self.#ident.is_none() { return Err(#err.into()) } } }); let setters = idents.iter().zip(&types).map(|(ident, ty)| { let t = unwrap_option(ty).unwrap_or(ty); quote! { pub fn #ident(&mut self, #ident: #t) -> &mut Self { self.#ident = Some(#ident); self } } }); let struct_fields = idents.iter().zip(&types).map(|(ident, ty)| { if is_option(ty) { quote! { #ident: self.#ident.clone() } } else { quote! { #ident: self.#ident.clone().unwrap() } } }); let expand = quote! { #vis struct #builder_name { #(#builder_fields),* } impl #builder_name { #(#setters)* pub fn build(&mut self) -> Result<#ident, Box<dyn std::error::Error>> { #(#checks)* Ok(#ident { #(#struct_fields),* }) } } impl #ident { pub fn builder() -> #builder_name { #builder_name { #(#idents: None),* } } } }; proc_macro::TokenStream::from(expand) } fn is_option(ty: &Type) -> bool { match get_last_path_segment(ty) { Some(seg) => seg.ident == "Option", _ => false, } } fn unwrap_option(ty: &Type) -> Option<&Type> { if !is_option(ty) { return None; } match get_last_path_segment(ty) { Some(seg) => match seg.arguments { PathArguments::AngleBracketed(ref args) => { args.args.first().and_then(|arg| match arg { &GenericArgument::Type(ref ty) => Some(ty), _ => None, }) } _ => None, }, None => None, } } fn get_last_path_segment(ty: &Type) -> Option<&PathSegment> { match ty { Type::Path(path) => path.path.segments.last(), _ => None, } } リファクタリング derive 関数が肥倧化しおきたので内郚の凊理を関数ずしお切り出したした。䞻な倉曎点は以䞋の通りです。 Builder 構造䜓の定矩を生成する郚分を関数化 build_builder_struct  Builder 構造䜓の実装setter 関数、 build 関数を生成する郚分を関数化 build_builder_impl  builder 関数を生成する郚分を関数化 build_struct_impl  これらの関数はすべお戻り倀ずしお proc_macro2::TokenStream を返しおいたすが、これは quote マクロが proc_macro2::TokenStream を返すためです。 これらの凊理を関数化したのに䌎い、もずもず以䞋のようにフィヌルド名ず型を最初に取埗しおいたのを、 let (idents, types): (Vec<Ident>, Vec<Type>) = match input.data { Data::Struct(data) => match data.fields { Fields::Named(fields) => fields .named .into_iter() .map(|field| { let ident = field.ident; let ty = field.ty; (ident.unwrap(), ty) }) .unzip(), _ => panic!("no unnamed fields are allowed"), }, _ => panic!("expects struct"), }; 以䞋のように NamedFields を取埗しお各関数にわたすように倉曎しおいたす。 let fields = match input.data { Data::Struct(data) => match data.fields { Fields::Named(fields) => fields, _ => panic!("no unnamed fields are allowed"), }, _ => panic!("this macro can be applied only to structaa"), }; リファクタリング埌の実装は以䞋の通りです。 use proc_macro2::TokenStream; use quote::{format_ident, quote}; use syn::{ parse_macro_input, Data, DeriveInput, Fields, FieldsNamed, GenericArgument, Ident, PathArguments, PathSegment, Type, Visibility, }; #[proc_macro_derive(Builder, attributes(builder))] pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); let ident = input.ident; let vis = input.vis; let builder_name = format_ident!("{}Builder", ident); let fields = match input.data { Data::Struct(data) => match data.fields { Fields::Named(fields) => fields, _ => panic!("no unnamed fields are allowed"), }, _ => panic!("this macro can be applied only to structaa"), }; let builder_struct = build_builder_struct(&fields, &builder_name, &vis); let builder_impl = build_builder_impl(&fields, &builder_name, &ident); let struct_impl = build_struct_impl(&fields, &builder_name, &ident); let expand = quote! { #builder_struct #builder_impl #struct_impl }; proc_macro::TokenStream::from(expand) } fn build_builder_struct( fields: &FieldsNamed, builder_name: &Ident, visibility: &Visibility, ) -> TokenStream { let (idents, types): (Vec<&Ident>, Vec<&Type>) = fields .named .iter() .map(|field| { let ident = field.ident.as_ref(); let ty = unwrap_option(&field.ty).unwrap_or(&field.ty); (ident.unwrap(), ty) }) .unzip(); quote! { #visibility struct #builder_name { #(#idents: Option<#types>),* } } } fn build_builder_impl( fields: &FieldsNamed, builder_name: &Ident, struct_name: &Ident, ) -> TokenStream { let checks = fields .named .iter() .filter(|field| !is_option(&field.ty)) .map(|field| { let ident = field.ident.as_ref(); let err = format!("Required field '{}' is missing", ident.unwrap().to_string()); quote! { if self.#ident.is_none() { return Err(#err.into()); } } }); let setters = fields.named.iter().map(|field| { let ident = &field.ident; let ty = unwrap_option(&field.ty).unwrap_or(&field.ty); quote! { pub fn #ident(&mut self, #ident: #ty) -> &mut Self { self.#ident = Some(#ident); self } } }); let struct_fields = fields.named.iter().map(|field| { let ident = field.ident.as_ref(); if is_option(&field.ty) { quote! { #ident: self.#ident.clone() } } else { quote! { #ident: self.#ident.clone().unwrap() } } }); quote! { impl #builder_name { #(#setters)* pub fn build(&mut self) -> Result<#struct_name, Box<dyn std::error::Error>> { #(#checks)* Ok(#struct_name { #(#struct_fields),* }) } } } } fn build_struct_impl( fields: &FieldsNamed, builder_name: &Ident, struct_name: &Ident, ) -> TokenStream { let field_defaults = fields.named.iter().map(|field| { let ident = field.ident.as_ref(); quote! { #ident: None } }); quote! { impl #struct_name { pub fn builder() -> #builder_name { #builder_name { #(#field_defaults),* } } } } } fn is_option(ty: &Type) -> bool { match get_last_path_segment(ty) { Some(seg) => seg.ident == "Option", _ => false, } } fn unwrap_option(ty: &Type) -> Option<&Type> { if !is_option(ty) { return None; } match get_last_path_segment(ty) { Some(seg) => match seg.arguments { PathArguments::AngleBracketed(ref args) => { args.args.first().and_then(|arg| match arg { &GenericArgument::Type(ref ty) => Some(ty), _ => None, }) } _ => None, }, None => None, } } fn get_last_path_segment(ty: &Type) -> Option<&PathSegment> { match ty { Type::Path(path) => path.path.segments.last(), _ => None, } } 07-repeated-field 目暙 以䞋の 2 ぀の方法でベクタを倀ずしおも぀フィヌルドを曎新できるようにする ベクタを䞎えお䞀括で曎新する ベクタの芁玠を 1 ぀ず぀远加する 䞀括で曎新するための関数名はフィヌルド名ず同じにする ベクタの芁玠を 1 ぀ず぀远加する関数の名前は以䞋のようにアトリビュヌトを甚いお指定する 1 ぀ず぀远加するための関数名ずしお䞀括で曎新するための関数名ず同じ名前が指定された堎合は 1 ぀ず぀远加するための関数を優先する #[derive(Builder)] pub struct Command { executable: String, #[builder(each = "arg")] args: Vec<String>, #[builder(each = "env")] env: Vec<String>, current_dir: Option<String>, } 実装方針 目暙ずする機胜を実珟するために実装する必芁があるのは以䞋の項目です。 フィヌルドに付䞎されたアトリビュヌトを取埗する builder アトリビュヌトを付䞎できるようにする 芁玠を 1 ぀ず぀远加する関数名は build アトリビュヌトの each キヌに指定する アトリビュヌトのキヌ名 each のバリデヌションは次のステップで実装する Vec 型のフィヌルドの setter を䞀括曎新甚ず芁玠远加甚の 2 皮類生成する たた、芁玠を 1 ぀ず぀远加できるようにするためには以䞋の機胜の実装も必芁です。 Builder 構造䜓のフィヌルドでは Vec 型の倉数は Option でラップしない フィヌルドの型が Vec かどうか刀定できるようにする Vec をアンラップしお䞭身の型を取埗できるようにする 実装 Builder 構造䜓のフィヌルドでは Vec 型の倉数は Option でラップしない Builder 構造䜓の定矩を生成しおいるのは build_builder_struct 関数です。今の実装では入力の型に関わらず Option でラップしおいたす。これを Vec のみラップしないように倉曎すれば良さそうです。 fn build_builder_struct( fields: &FieldsNamed, builder_name: &Ident, visibility: &Visibility, ) -> TokenStream { let (idents, types): (Vec<&Ident>, Vec<&Type>) = fields .named .iter() .map(|field| { let ident = field.ident.as_ref(); let ty = unwrap_option(&field.ty).unwrap_or(&field.ty); (ident.unwrap(), ty) }) .unzip(); quote! { #visibility struct #builder_name { #(#idents: Option<#types>),* } } } is_vector 関数は is_option ず䌌た関数で、䞎えられた型が Vec 型かどうかを刀定する関数です。実装は埌述したす。 fn build_builder_struct( fields: &FieldsNamed, builder_name: &Ident, visibility: &Visibility, ) -> TokenStream { let struct_fields = fields .named .iter() .map(|field| { let ident = field.ident.as_ref(); let ty = unwrap_option(&field.ty).unwrap_or(&field.ty); (ident.unwrap(), ty) }) .map(|(ident, ty)| { if is_vector(&ty) { quote! { #ident: #ty } } else { quote! { #ident: Option<#ty> } } }); quote! { #visibility struct #builder_name { #(#struct_fields),* } } } Builder 構造䜓の定矩が倉わったので、Builder 構造䜓を返す builder 関数の実装を生成する build_struct_impl 関数も修正が必芁です。 Vec 型のフィヌルドのみ Vec::new() を返すようにすれば良さそうです。 fn build_struct_impl( fields: &FieldsNamed, builder_name: &Ident, struct_name: &Ident, ) -> TokenStream { let field_defaults = fields.named.iter().map(|field| { let ident = field.ident.as_ref(); quote! { #ident: None } }); quote! { impl #struct_name { pub fn builder() -> #builder_name { #builder_name { #(#field_defaults),* } } } } } こちらも build_builder_struct 関数ず同様、 is_vector 関数を䜿っお条件分岐を蚘述しおいたす。 fn build_struct_impl( fields: &FieldsNamed, builder_name: &Ident, struct_name: &Ident, ) -> TokenStream { let field_defaults = fields.named.iter().map(|field| { let ident = field.ident.as_ref(); let ty = &field.ty; if is_vector(&ty) { quote! { #ident: Vec::new() } } else { quote! { #ident: None } } }); quote! { impl #struct_name { pub fn builder() -> #builder_name { #builder_name { #(#field_defaults),* } } } } } たた、 Vec 型のフィヌルドは芁玠を含たなくおも問題ないので、 Vec 型のフィヌルドに぀いおもガヌド節を生成しないように倉曎したす。今は Option のみをフィルタしおいたすが、远加で Vec もフィルタするように倉曎したす。 let checks = fields .named .iter() .filter(|field| !is_option(&field.ty)) .filter(|field| !is_vector(&field.ty)) .map(|field| { let ident = field.ident.as_ref(); let err = format!("Required field '{}' is missing", ident.unwrap().to_string()); quote! { if self.#ident.is_none() { return Err(#err.into()); } } }); is_vector ず unwrap_vector の実装 ここからは is_vector ず unwrap_vector を実装しおいきたす。ここたでの実装では unwrap_vector はでおきたせんが、今埌䜿うのでここで実装しおおきたす。 Vec も Option ず同様 Type::Path に分類されるので、以䞋の項目は 06 で実装した is_option や unwrap_option を流甚できたす。 fn is_vector(ty: &Type) -> bool { match get_last_path_segment(ty) { Some(seg) => seg.ident == "Vec", _ => false, } } fn unwrap_vector(ty: &Type) -> Option<&Type> { if !is_vector(ty) { return None; } match get_last_path_segment(ty) { Some(seg) => match seg.arguments { PathArguments::AngleBracketed(ref args) => { args.args.first().and_then(|arg| match arg { &GenericArgument::Type(ref ty) => Some(ty), _ => None, }) } _ => None, }, None => None, } } unwrap_vector のパタヌンマッチの郚分は unwrap_option ず同様の凊理をしおいるので関数化できそうです。これを unwrap_generic_type ずいう関数にくくり出すず以䞋のようになりたす。 fn unwrap_option(ty: &Type) -> Option<&Type> { if !is_option(ty) { return None; } unwrap_generic_type(ty) } fn unwrap_vector(ty: &Type) -> Option<&Type> { if !is_vector(ty) { return None; } unwrap_generic_type(ty) } fn unwrap_generic_type(ty: &Type) -> Option<&Type> { match get_last_path_segment(ty) { Some(seg) => match seg.arguments { PathArguments::AngleBracketed(ref args) => { args.args.first().and_then(|arg| match arg { &GenericArgument::Type(ref ty) => Some(ty), _ => None, }) } _ => None, }, None => None, } } フィヌルドに付䞎されたアトリビュヌトを取埗する フィヌルドに付䞎されたアトリビュヌトを取埗する凊理を実装する前に、たずアトリビュヌトを付䞎できるようにする必芁がありたす。 アトリビュヌトを付䞎できるようにするためには、以䞋のように derive 関数に attributes(builder) ずいうアトリビュヌトを远加したす 参考 。これでフィヌルドに #[builder(...)] のようなアトリビュヌトを付䞎できたす。 #[proc_macro_derive(Builder, attributes(builder))] pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { アトリビュヌトを付䞎できるようになったので、それを取埗する凊理を実装したす。 アトリビュヌトを取埗するにはどのデヌタを凊理すれば良いでしょうか。たずは DeriveInput をみおみたしょう。 pub struct DeriveInput { pub attrs: Vec<Attribute>, pub vis: Visibility, pub ident: Ident, pub generics: Generics, pub data: Data, } DeriveInput は attrs フィヌルドを持っおいたすが、以䞋の DeriveInput の attrs の説明にあるように、これは構造䜓自䜓に付䞎されたアトリビュヌトです。 匕甚元  Attributes tagged on the whole struct or enum. 構造䜓のフィヌルドは data フィヌルドに栌玍されおいるので Data の定矩をみおみたしょう。 Data の構造を䞋っおいくず最終的に構造䜓の各フィヌルドの情報を保持しおいる Field 構造䜓が埗られたす。 Data の構造の詳现に぀いおは 前線 を参考にしおください。この䞭にある attrs がフィヌルドに付䞎されたアトリビュヌトです。 pub struct Field { pub attrs: Vec<Attribute>, pub vis: Visibility, pub ident: Option<Ident>, pub colon_token: Option<Colon>, pub ty: Type, } 構造䜓のフィヌルドは derive 関数の最初の方で取埗しおいるので、これを凊理しおアトリビュヌトを取埗しおいきたす。 attrs は Attribute のベクタになっおいたすが、今回は 1 ぀のアトリビュヌトしか䜿わないので first で先頭のアトリビュヌトだけ取埗すれば良いでしょう。 let ident_each_name = field .attrs .first() .map(|attr| todo!()); Attribute の parse_meta 関数 でアトリビュヌトをパヌスした結果が埗られたす。 let ident_each_name = field .attrs .first() .map(|attr| attr.parse_meta()); parse_meta() 関数の戻り倀は Result<Meta> です。 Meta 型は以䞋のような enum です。 pub enum Meta { Path(Path), List(MetaList), NameValue(MetaNameValue), } Meta の List バリアントの 説明 に List A meta list is like the derive(Copy) in #[derive(Copy)] . ずあるように、今回取埗したいのは Meta::List なのでパタヌンマッチで凊理したす。 let ident_each_name = field .attrs .first() .map(|attr| match attr.parse_meta() { Ok(Meta::List(_)) => todo!(), _ => None, }); MetaList は以䞋のような構造䜓です。 pub struct MetaList { pub path: Path, pub paren_token: Paren, pub nested: Punctuated<NestedMeta, Comma>, } MetaList 型の path はアトリビュヌト名 #[builder(each="foo")] の builder の郚分を、 nested アトリビュヌトの倀 #[builder(each="foo")] の each="foo" の郚分を保持しおいたす。今必芁なのはアトリビュヌトの倀を保持する nested の郚分です。 nested はアトリビュヌト倀を耇数保持しおいたすが、今回は耇数の倀をも぀こずは想定しおいないので first で先頭を取埗すれば良さそうです。 let ident_each_name = field .attrs .first() .map(|attr| match attr.parse_meta() { Ok(Meta::List(list)) => list.nested.first(), _ => None, }); nested.first() は Option<NestedMeta> を返したす。 NestedMeta は以䞋のような enum です。 pub enum NestedMeta { Meta(Meta), Lit(Lit), } NestedMeta のフィヌルドに関する 以䞋の蚘述 からわかるように、 Lit は Rust のリテラルを保持したす。この段階では nested.first() から Some(each="foo") のような圢匏が返っおくるこずを期埅しおいるので Lit ではなく Meta にマッチするようにしたす。 Meta(Meta) A structured meta item, like the Copy in #[derive(Copy)] which would be a nested Meta::Path. Lit(Lit) A Rust literal, like the “new_name” in #[rename(“new_name”)]. let ident_each_name = field .attrs .first() .map(|attr| match attr.parse_meta() { Ok(Meta::List(list)) => match list.nested.first() { Some(NestedMeta::Meta(_)) => todo!(), _ => None, }, _ => None, }); 先ほどはパタヌンマッチを甚いお Meta::List を取埗したしたが、䞊述のように今床は each="foo" のようなキヌずバリュヌのペアが取埗されるこずを期埅しおいるので、 Meta::NameValue(MetaNameValue) をマッチしお凊理したす。 let ident_each_name = field .attrs .first() .map(|attr| match attr.parse_meta() { Ok(Meta::List(list)) => match list.nested.first() { Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue{}))) => todo!(), _ => None, }, _ => None, }); MetaNameValue は以䞋のような構造䜓です。 pub struct MetaNameValue { pub path: Path, pub eq_token: Eq, pub lit: Lit, } each = "foo" を䟋にずるず、 MetaNameValue は path に each を、 lit に "foo" を栌玍したす。今回欲しいのは "foo" の方なので lit だけを取埗すれば良さそうです。 let ident_each_name = field .attrs .first() .map(|attr| match attr.parse_meta() { Ok(Meta::List(list)) => match list.nested.first() { Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue{ path: _, eq_token: _, lit }))) => todo!(), _ => None, }, _ => None, }); Lit は以䞋のような enum です。 each = "foo" のような圢匏からもわかるように、文字列リテラル Lit::Str が埗られるこずを期埅しおいたす。 pub enum Lit { Str(LitStr), ByteStr(LitByteStr), Byte(LitByte), Char(LitChar), Int(LitInt), Float(LitFloat), Bool(LitBool), Verbatim(Literal), } リテラルが衚す倀は value メ゜ッドで取埗できるので、 let ident_each_name = field .attrs .first() .map(|attr| match attr.parse_meta() { Ok(Meta::List(list)) => match list.nested.first() { Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue{ path: _, eq_token: _, lit: Lit::Str(ref s) }))) => { Some(s.value()) }, _ => None, }, _ => None, }) .flatten(); 以䞊で各フィヌルドの芁玠远加甚のメ゜ッド名を取埗できたした。 Vec 型のフィヌルドの setter を䞀括曎新甚ず芁玠远加甚の 2 皮類生成する 今たではフィヌルドの型によらず、単玔に以䞋のように setter を生成しおいたした。 quote! { pub fn #ident(&mut self, #ident: #t) -> &mut Self { self.#ident = Some(#ident); self } } たずアトリビュヌトが付䞎されおいるかどうかで分岐が発生したす。アトリビュヌトが付䞎されおいるず芁玠远加甚のメ゜ッドが必芁になりたす。 match ident_each_name { Some(name) => todo!(), None => todo!(), } たずは芁玠远加甚のメ゜ッドがいらない方を実装したす。 Vec 型のフィヌルドは Option でラップされないので Vec 型かどうかで setter の実装が倉わりたす。 match ident_each_name { Some(name) => todo!(), None => { if is_vector(&ty) { quote! { pub fn #ident(&mut self, #ident: #ty) -> &mut Self { self.#ident = #ident; self } } } else { quote! { pub fn #ident(&mut self, #ident: #ty) -> &mut Self { self.#ident = Some(#ident); self } } } }, } 次に芁玠远加甚のメ゜ッドが必芁なパタヌンを実装したす。こっちは以䞋の 2 ぀のパタヌンで凊理が分岐したす。 芁玠远加甚のメ゜ッド名がフィヌルド名ず 同じ 芁玠远加甚のメ゜ッドのみ生成する 芁玠远加甚のメ゜ッド名がフィヌルド名ず 異なる 芁玠远加甚のメ゜ッドず䞀括曎新甚のメ゜ッドを䞡方生成する 実装は以䞋のようになりたす。実装のポむントは以䞋の通りです。 芁玠远加甚の関数の匕数の型ずしお䜿甚するために unwrap_vector で䞭身の型を取り出す 芁玠远加甚の関数の名前 name は String なので Ident::new で Ident を生成する Ident::new の第二匕数には Span 構造䜓を指定する必芁がある。 Span 構造䜓はマクロの展開先で識別子が誀っお捕捉されないようにするために必芁 参考  match ident_each_name { Some(name) => { let ty_each = unwrap_vector(ty).unwrap(); let ident_each = Ident::new(name.as_str(), Span::call_site()); if ident.unwrap().to_string() == name { quote! { pub fn #ident_each(&mut self, #ident_each:#ty_each) -> &mut Self { self.#ident.push(#ident_each); self } } } else { quote! { pub fn #ident(&mut self, #ident: #ty) -> &mut Self { self.#ident = #ident; self } pub fn #ident_each(&mut self, #ident_each: #ty_each) -> &mut Self { self.#ident.push(#ident_each); self } } } } None => { (略) }, } 最終的な実装は以䞋のようになりたす。 use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote}; use syn::{ parse_macro_input, Data, DeriveInput, Fields, FieldsNamed, GenericArgument, Ident, Lit, Meta, MetaList, MetaNameValue, NestedMeta, PathArguments, PathSegment, Type, Visibility, }; #[proc_macro_derive(Builder, attributes(builder))] pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); let ident = input.ident; let vis = input.vis; let builder_name = format_ident!("{}Builder", ident); let fields = match input.data { Data::Struct(data) => match data.fields { Fields::Named(fields) => fields, _ => panic!("no unnamed fields are allowed"), }, _ => panic!("this macro can be applied only to struct"), }; let builder_struct = build_builder_struct(&fields, &builder_name, &vis); let builder_impl = build_builder_impl(&fields, &builder_name, &ident); let struct_impl = build_struct_impl(&fields, &builder_name, &ident); let expand = quote! { #builder_struct #builder_impl #struct_impl }; proc_macro::TokenStream::from(expand) } fn build_builder_struct( fields: &FieldsNamed, builder_name: &Ident, visibility: &Visibility, ) -> TokenStream { let struct_fields = fields .named .iter() .map(|field| { let ident = field.ident.as_ref(); let ty = unwrap_option(&field.ty).unwrap_or(&field.ty); (ident.unwrap(), ty) }) .map(|(ident, ty)| { if is_vector(&ty) { quote! { #ident: #ty } } else { quote! { #ident: Option<#ty> } } }); quote! { #visibility struct #builder_name { #(#struct_fields),* } } } fn build_builder_impl( fields: &FieldsNamed, builder_name: &Ident, struct_name: &Ident, ) -> TokenStream { let checks = fields .named .iter() .filter(|field| !is_option(&field.ty)) .filter(|field| !is_vector(&field.ty)) .map(|field| { let ident = field.ident.as_ref(); let err = format!("Required field '{}' is missing", ident.unwrap().to_string()); quote! { if self.#ident.is_none() { return Err(#err.into()); } } }); let setters = fields.named.iter().map(|field| { let ident_each_name = field .attrs .first() .map(|attr| match attr.parse_meta() { Ok(Meta::List(list)) => match list.nested.first() { Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue { path: _, eq_token: _, lit: Lit::Str(ref str), }))) => Some(str.value()), _ => None, }, _ => None, }) .flatten(); let ident = field.ident.as_ref(); let ty = unwrap_option(&field.ty).unwrap_or(&field.ty); match ident_each_name { Some(name) => { let ty_each = unwrap_vector(ty).unwrap(); let ident_each = Ident::new(name.as_str(), Span::call_site()); if ident.unwrap().to_string() == name { quote! { pub fn #ident_each(&mut self, #ident_each:#ty_each) -> &mut Self { self.#ident.push(#ident_each); self } } } else { quote! { pub fn #ident(&mut self, #ident: #ty) -> &mut Self { self.#ident = #ident; self } pub fn #ident_each(&mut self, #ident_each: #ty_each) -> &mut Self { self.#ident.push(#ident_each); self } } } } None => { if is_vector(&ty) { quote! { pub fn #ident(&mut self, #ident: #ty) -> &mut Self { self.#ident = #ident; self } } } else { quote! { pub fn #ident(&mut self, #ident: #ty) -> &mut Self { self.#ident = Some(#ident); self } } } } } }); let struct_fields = fields.named.iter().map(|field| { let ident = field.ident.as_ref(); if is_option(&field.ty) || is_vector(&field.ty) { quote! { #ident: self.#ident.clone() } } else { quote! { #ident: self.#ident.clone().unwrap() } } }); quote! { impl #builder_name { #(#setters)* pub fn build(&mut self) -> Result<#struct_name, Box<dyn std::error::Error>> { #(#checks)* Ok(#struct_name { #(#struct_fields),* }) } } } } fn build_struct_impl( fields: &FieldsNamed, builder_name: &Ident, struct_name: &Ident, ) -> TokenStream { let field_defaults = fields.named.iter().map(|field| { let ident = field.ident.as_ref(); let ty = &field.ty; if is_vector(&ty) { quote! { #ident: Vec::new() } } else { quote! { #ident: None } } }); quote! { impl #struct_name { pub fn builder() -> #builder_name { #builder_name { #(#field_defaults),* } } } } } fn is_option(ty: &Type) -> bool { match get_last_path_segment(ty) { Some(seg) => seg.ident == "Option", _ => false, } } fn is_vector(ty: &Type) -> bool { match get_last_path_segment(ty) { Some(seg) => seg.ident == "Vec", _ => false, } } fn unwrap_option(ty: &Type) -> Option<&Type> { if !is_option(ty) { return None; } unwrap_generic_type(ty) } fn unwrap_vector(ty: &Type) -> Option<&Type> { if !is_vector(ty) { return None; } unwrap_generic_type(ty) } fn unwrap_generic_type(ty: &Type) -> Option<&Type> { match get_last_path_segment(ty) { Some(seg) => match seg.arguments { PathArguments::AngleBracketed(ref args) => { args.args.first().and_then(|arg| match arg { &GenericArgument::Type(ref ty) => Some(ty), _ => None, }) } _ => None, }, None => None, } } fn get_last_path_segment(ty: &Type) -> Option<&PathSegment> { match ty { Type::Path(path) => path.path.segments.last(), _ => None, } } 08-unrecognized-attribute 目暙 attribute に間違った識別子が䞎えられた際に適切なコンパむル゚ラヌを衚瀺する 実装方針 アトリビュヌトのキヌずしお each 以倖のものが䞎えられた堎合に゚ラヌを衚瀺する ゚ラヌを発生させたい箇所で syn::Error の to_compile_error メ゜ッドで TokenStream を返すようにする 単玔に panic! させるだけよりも詳现な゚ラヌメッセヌゞを衚瀺させられる 実装 アトリビュヌトのキヌが正しいか刀定する必芁があるので、アトリビュヌトを凊理しおいる以䞋の箇所を倉曎したす。 let ident_each_name = field .attrs .first() .map(|attr| match attr.parse_meta() { Ok(Meta::List(list)) => match list.nested.first() { Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue { path: _, eq_token: _, lit: Lit::Str(ref str), }))) => Some(str.value()), _ => None, }, _ => None, }) .flatten(); アトリビュヌトのキヌは MetadataNameValue の path に栌玍されおいたす。 path は Path 型なので 06-optional-field で凊理したのず同様にしお識別子を取埗したす。 Ident の to_string メ゜ッドで識別子の名前を取埗できるので、これが each ず䞀臎しなければ゚ラヌを返せば良さそうです。 let ident_each_name = field .attrs .first() .map(|attr| match attr.parse_meta() { Ok(Meta::List(list)) => match list.nested.first() { Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue { ref path, eq_token: _, lit: Lit::Str(ref str), }))) => { if let Some(name) = path.segments.first() { if name.ident.to_string() != "each" { todo!() } } Some(str.value()) } _ => None, }, _ => None, }) .flatten(); syn::Error は syn::Error::new_spanned() を䜿っお生成したす。゚ラヌメッセヌゞ”expected …”はテストケヌスに蚘茉されたメッセヌゞをそのたた䜿いたす。以䞋のようにしたいずころですが、このたたでは型が合わないのでコンパむルできたせん。 let ident_each_name = field .attrs .first() .map(|attr| match attr.parse_meta() { Ok(Meta::List(list)) => match list.nested.first() { Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue { ref path, eq_token: _, lit: Lit::Str(ref str), }))) => { if let Some(name) = path.segments.first() { if name.ident.to_string() != "each" { return Some(syn::Error::new_spanned( list, "expected `builder(each = \"...\")`", )); } } Some(str.value()) } _ => None, }, _ => None, }) .flatten(); そこで、以䞋のような enum を返すようにしお、埌でパタヌンマッチで凊理したしょう。 enum LitOrError { Lit(String), Error(syn::Error), } LitOrError を䜿っお先ほどの箇所を次のように曞き換えたす。 let ident_each_name = field .attrs .first() .map(|attr| match attr.parse_meta() { Ok(Meta::List(list)) => match list.nested.first() { Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue { ref path, eq_token: _, lit: Lit::Str(ref str), }))) => { if let Some(name) = path.segments.first() { if name.ident.to_string() != "each" { return Some(LitOrError::Error(syn::Error::new_spanned( list, "expected `builder(each = \"...\")`", ))); } } Some(LitOrError::Lit(str.value())) } _ => None, }, _ => None, }) .flatten(); パタヌンマッチで凊理しおいた郚分も enum に合わせお倉曎したす。 LitOrError::Error にマッチする堎合はコンパむル゚ラヌを生じさせる必芁があるので、 to_compile_error().into() で゚ラヌを返したす。 match ident_each_name { Some(LitOrError::Lit(name)) => { (略) } Some(LitOrError::Error(err)) => err.to_compile_error().into(), None => { (略) } } 今たで panic! しおいた堎所も syn::Error の to_compile_error メ゜ッドを䜿っお TokenStream を返すように倉曎しおおきたす。 let fields = match input.data { Data::Struct(data) => match data.fields { Fields::Named(fields) => fields, _ => { return syn::Error::new(ident.span(), "expects named fields") .to_compile_error() .into() } }, _ => { return syn::Error::new(ident.span(), "expects struct") .to_compile_error() .into() } }; 最終的な実装は以䞋のようになりたす。 use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote}; use syn::{ parse_macro_input, Data, DeriveInput, Fields, FieldsNamed, GenericArgument, Ident, Lit, Meta, MetaNameValue, NestedMeta, PathArguments, PathSegment, Type, Visibility, }; enum LitOrError { Lit(String), Error(syn::Error), } #[proc_macro_derive(Builder, attributes(builder))] pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); let ident = input.ident; let vis = input.vis; let builder_name = format_ident!("{}Builder", ident); let fields = match input.data { Data::Struct(data) => match data.fields { Fields::Named(fields) => fields, _ => { return syn::Error::new(ident.span(), "expects named fields") .to_compile_error() .into() } }, _ => { return syn::Error::new(ident.span(), "expects struct") .to_compile_error() .into() } }; let builder_struct = build_builder_struct(&fields, &builder_name, &vis); let builder_impl = build_builder_impl(&fields, &builder_name, &ident); let struct_impl = build_struct_impl(&fields, &builder_name, &ident); let expand = quote! { #builder_struct #builder_impl #struct_impl }; proc_macro::TokenStream::from(expand) } fn build_builder_struct( fields: &FieldsNamed, builder_name: &Ident, visibility: &Visibility, ) -> TokenStream { let struct_fields = fields .named .iter() .map(|field| { let ident = field.ident.as_ref(); let ty = unwrap_option(&field.ty).unwrap_or(&field.ty); (ident.unwrap(), ty) }) .map(|(ident, ty)| { if is_vector(&ty) { quote! { #ident: #ty } } else { quote! { #ident: Option<#ty> } } }); quote! { #visibility struct #builder_name { #(#struct_fields),* } } } fn build_builder_impl( fields: &FieldsNamed, builder_name: &Ident, struct_name: &Ident, ) -> TokenStream { let checks = fields .named .iter() .filter(|field| !is_option(&field.ty)) .filter(|field| !is_vector(&field.ty)) .map(|field| { let ident = field.ident.as_ref(); let err = format!("Required field '{}' is missing", ident.unwrap().to_string()); quote! { if self.#ident.is_none() { return Err(#err.into()); } } }); let setters = fields.named.iter().map(|field| { let ident_each_name = field .attrs .first() .map(|attr| match attr.parse_meta() { Ok(Meta::List(list)) => match list.nested.first() { Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue { ref path, eq_token: _, lit: Lit::Str(ref str), }))) => { if let Some(name) = path.segments.first() { if name.ident.to_string() != "each" { return Some(LitOrError::Error(syn::Error::new_spanned( list, "expected `builder(each = \"...\")`", ))); } } Some(LitOrError::Lit(str.value())) } _ => None, }, _ => None, }) .flatten(); let ident = field.ident.as_ref(); let ty = unwrap_option(&field.ty).unwrap_or(&field.ty); match ident_each_name { Some(LitOrError::Lit(name)) => { let ty_each = unwrap_vector(ty).unwrap(); let ident_each = Ident::new(name.as_str(), Span::call_site()); if ident.unwrap().to_string() == name { quote! { pub fn #ident_each(&mut self, #ident_each:#ty_each) -> &mut Self { self.#ident.push(#ident_each); self } } } else { quote! { pub fn #ident(&mut self, #ident: #ty) -> &mut Self { self.#ident = #ident; self } pub fn #ident_each(&mut self, #ident_each: #ty_each) -> &mut Self { self.#ident.push(#ident_each); self } } } } Some(LitOrError::Error(err)) => err.to_compile_error().into(), None => { if is_vector(&ty) { quote! { pub fn #ident(&mut self, #ident: #ty) -> &mut Self { self.#ident = #ident; self } } } else { quote! { pub fn #ident(&mut self, #ident: #ty) -> &mut Self { self.#ident = Some(#ident); self } } } } } }); let struct_fields = fields.named.iter().map(|field| { let ident = field.ident.as_ref(); if is_option(&field.ty) || is_vector(&field.ty) { quote! { #ident: self.#ident.clone() } } else { quote! { #ident: self.#ident.clone().unwrap() } } }); quote! { impl #builder_name { #(#setters)* pub fn build(&mut self) -> Result<#struct_name, Box<dyn std::error::Error>> { #(#checks)* Ok(#struct_name { #(#struct_fields),* }) } } } } fn build_struct_impl( fields: &FieldsNamed, builder_name: &Ident, struct_name: &Ident, ) -> TokenStream { let field_defaults = fields.named.iter().map(|field| { let ident = field.ident.as_ref(); let ty = &field.ty; if is_vector(&ty) { quote! { #ident: Vec::new() } } else { quote! { #ident: None } } }); quote! { impl #struct_name { pub fn builder() -> #builder_name { #builder_name { #(#field_defaults),* } } } } } fn is_option(ty: &Type) -> bool { match get_last_path_segment(ty) { Some(seg) => seg.ident == "Option", _ => false, } } fn is_vector(ty: &Type) -> bool { match get_last_path_segment(ty) { Some(seg) => seg.ident == "Vec", _ => false, } } fn unwrap_option(ty: &Type) -> Option<&Type> { if !is_option(ty) { return None; } unwrap_generic_type(ty) } fn unwrap_vector(ty: &Type) -> Option<&Type> { if !is_vector(ty) { return None; } unwrap_generic_type(ty) } fn unwrap_generic_type(ty: &Type) -> Option<&Type> { match get_last_path_segment(ty) { Some(seg) => match seg.arguments { PathArguments::AngleBracketed(ref args) => { args.args.first().and_then(|arg| match arg { &GenericArgument::Type(ref ty) => Some(ty), _ => None, }) } _ => None, }, None => None, } } fn get_last_path_segment(ty: &Type) -> Option<&PathSegment> { match ty { Type::Path(path) => path.path.segments.last(), _ => None, } } 09-redefined-prelude-types 目暙 std::prelude でむンポヌトされる型 Option や Vector などがナヌザヌによっお再定矩されおも正しく䜿えるようにする 実装方針 Option や Vec などを名前空間を指定しお䜿うようにすればよいです。テストコヌドでチェックされおいるのは以䞋の 5 ぀なので、今回はこれらの適切な名前空間を指定するように倉曎したす。 倉曎前 倉曎埌 Option std::option::Option Some std::option::Option::Some None std::option::Option::None Result std::result::Result Box std::boxed::Box 再定矩の圱響を受けるのはマクロが呌び出されお展開される郚分のみなので、盎すのは quote マクロの䞭にある郚分だけで十分です。 実装 䞊述の 5 ぀の名前空間を正しく指定するだけなので、今回は最終的な結果のみを蚘茉したす。最終的な実装は以䞋のようになりたす。 use proc_macro2::{Span, TokenStream}; use quote::{format_ident, quote}; use syn::{ parse_macro_input, Data, DeriveInput, Fields, FieldsNamed, GenericArgument, Ident, Lit, Meta, MetaNameValue, NestedMeta, PathArguments, PathSegment, Type, Visibility, }; enum LitOrError { Lit(String), Error(syn::Error), } #[proc_macro_derive(Builder, attributes(builder))] pub fn derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream { let input = parse_macro_input!(input as DeriveInput); let ident = input.ident; let vis = input.vis; let builder_name = format_ident!("{}Builder", ident); let fields = match input.data { Data::Struct(data) => match data.fields { Fields::Named(fields) => fields, _ => { return syn::Error::new(ident.span(), "expects named fields") .to_compile_error() .into() } }, _ => { return syn::Error::new(ident.span(), "expects struct") .to_compile_error() .into() } }; let builder_struct = build_builder_struct(&fields, &builder_name, &vis); let builder_impl = build_builder_impl(&fields, &builder_name, &ident); let struct_impl = build_struct_impl(&fields, &builder_name, &ident); let expand = quote! { #builder_struct #builder_impl #struct_impl }; proc_macro::TokenStream::from(expand) } fn build_builder_struct( fields: &FieldsNamed, builder_name: &Ident, visibility: &Visibility, ) -> TokenStream { let struct_fields = fields .named .iter() .map(|field| { let ident = field.ident.as_ref(); let ty = unwrap_option(&field.ty).unwrap_or(&field.ty); (ident.unwrap(), ty) }) .map(|(ident, ty)| { if is_vector(&ty) { quote! { #ident: #ty } } else { quote! { #ident: std::option::Option<#ty> } } }); quote! { #visibility struct #builder_name { #(#struct_fields),* } } } fn build_builder_impl( fields: &FieldsNamed, builder_name: &Ident, struct_name: &Ident, ) -> TokenStream { let checks = fields .named .iter() .filter(|field| !is_option(&field.ty)) .filter(|field| !is_vector(&field.ty)) .map(|field| { let ident = field.ident.as_ref(); let err = format!("Required field '{}' is missing", ident.unwrap().to_string()); quote! { if self.#ident.is_none() { return Err(#err.into()); } } }); let setters = fields.named.iter().map(|field| { let ident_each_name = field .attrs .first() .map(|attr| match attr.parse_meta() { Ok(Meta::List(list)) => match list.nested.first() { Some(NestedMeta::Meta(Meta::NameValue(MetaNameValue { ref path, eq_token: _, lit: Lit::Str(ref str), }))) => { if let Some(name) = path.segments.first() { if name.ident.to_string() != "each" { return Some(LitOrError::Error(syn::Error::new_spanned( list, "expected `builder(each = \"...\")`", ))); } } Some(LitOrError::Lit(str.value())) } _ => None, }, _ => None, }) .flatten(); let ident = field.ident.as_ref(); let ty = unwrap_option(&field.ty).unwrap_or(&field.ty); match ident_each_name { Some(LitOrError::Lit(name)) => { let ty_each = unwrap_vector(ty).unwrap(); let ident_each = Ident::new(name.as_str(), Span::call_site()); if ident.unwrap().to_string() == name { quote! { pub fn #ident_each(&mut self, #ident_each:#ty_each) -> &mut Self { self.#ident.push(#ident_each); self } } } else { quote! { pub fn #ident(&mut self, #ident: #ty) -> &mut Self { self.#ident = #ident; self } pub fn #ident_each(&mut self, #ident_each: #ty_each) -> &mut Self { self.#ident.push(#ident_each); self } } } } Some(LitOrError::Error(err)) => err.to_compile_error().into(), None => { if is_vector(&ty) { quote! { pub fn #ident(&mut self, #ident: #ty) -> &mut Self { self.#ident = #ident; self } } } else { quote! { pub fn #ident(&mut self, #ident: #ty) -> &mut Self { self.#ident = std::option::Option::Some(#ident); self } } } } } }); let struct_fields = fields.named.iter().map(|field| { let ident = field.ident.as_ref(); if is_option(&field.ty) || is_vector(&field.ty) { quote! { #ident: self.#ident.clone() } } else { quote! { #ident: self.#ident.clone().unwrap() } } }); quote! { impl #builder_name { #(#setters)* pub fn build(&mut self) -> std::result::Result<#struct_name, std::boxed::Box<dyn std::error::Error>> { #(#checks)* Ok(#struct_name { #(#struct_fields),* }) } } } } fn build_struct_impl( fields: &FieldsNamed, builder_name: &Ident, struct_name: &Ident, ) -> TokenStream { let field_defaults = fields.named.iter().map(|field| { let ident = field.ident.as_ref(); let ty = &field.ty; if is_vector(&ty) { quote! { #ident: std::vec::Vec::new() } } else { quote! { #ident: std::option::Option::None } } }); quote! { impl #struct_name { pub fn builder() -> #builder_name { #builder_name { #(#field_defaults),* } } } } } fn is_option(ty: &Type) -> bool { match get_last_path_segment(ty) { Some(seg) => seg.ident == "Option", _ => false, } } fn is_vector(ty: &Type) -> bool { match get_last_path_segment(ty) { Some(seg) => seg.ident == "Vec", _ => false, } } fn unwrap_option(ty: &Type) -> Option<&Type> { if !is_option(ty) { return None; } unwrap_generic_type(ty) } fn unwrap_vector(ty: &Type) -> Option<&Type> { if !is_vector(ty) { return None; } unwrap_generic_type(ty) } fn unwrap_generic_type(ty: &Type) -> Option<&Type> { match get_last_path_segment(ty) { Some(seg) => match seg.arguments { PathArguments::AngleBracketed(ref args) => { args.args.first().and_then(|arg| match arg { &GenericArgument::Type(ref ty) => Some(ty), _ => None, }) } _ => None, }, None => None, } } fn get_last_path_segment(ty: &Type) -> Option<&PathSegment> { match ty { Type::Path(path) => path.path.segments.last(), _ => None, } } たずめ builder マクロを題材にしお前線ず埌線に分けお手続き的マクロの実装方法を説明しおきたした。 今回実装したマクロはフィヌルドの型が Option<Vec<_>> であるケヌスや、 Vec 型のフィヌルド以倖に each を付䞎した堎合などを考慮しおおらず、実装した機胜は十分ではありたせん。テストも十分なケヌスを網矅しおいるずは蚀えたせん。 しかし今回の蚘事で手続き的マクロをどのようにしお䜜るかは䞀通り理解でき、今埌やる時もどこから手を぀ければよいかだいたい感芚が぀かめたかず思いたす。この蚘事が今埌みなさんがマクロを実装するずきの助けになれば幞いです。 参考文献 syn – Docs.rs The post proc_macro_workshopでRustの手続き的マクロに入門する 埌線 appeared first on CADDi Tech Blog .
泚意!! 蚘事を曞いた時点からの曎新がありたす この蚘事の内容は叀くなっおいたす。圓時の課題は 2022幎12月の Github ActionsのUpdateにより、同䞀オヌナヌの private repository のActionsを参照可胜になったため、同䞀オヌナヌ間であればこの蚘事の手順を実斜する必芁はなくなりたした。 詳しくは以䞋の蚘事を参照を参照しおください。 GitHub Actions – Sharing actions and reusable workflows from private repositories is now GA ただし、オヌナヌが異なるprivate repository のactionを再利甚したいずいうケヌスではこの蚘事の方法は利甚可胜です。 Platformチヌムの前倚( @kencharos )です。 私たちのプロダクトは GCP 䞊に構築されおいお、アプリケヌションのテストやビルド、デプロむに加えお、 GCP のむンフラ構築もほずんどがIaC化されおいたす。 Pull Requestによるコヌドレビュヌを経お、CI/CD パむプラむンによりアプリケヌションのデプロむや GCP リ゜ヌスの構築が自動化されおいたす。 CI/CDパむプラむンは、以前はCircleCIに泚力しおいたしたが、珟圚は GitHub Actionsに泚力しおいたす。 新芏サヌビスの開発では GitHub Actions を最初から䜿甚し、既存サヌビスでも埐々にCircleCIから GitHub Actionsぞの移行を進めおいたす。 GitHub Actionsぞの移行理由ずしおは以䞋の2点がありたす。 GitHub を䜿甚しおいるため、CI/CDが統合されおいるず䜿い勝手が良い OIDCに察応しおいるため、 GCP のWorkload Identityを䜿うこずによっお GCP Service AccountのKey fileをCI/CD偎に保存しなくおも GCP リ゜ヌスを扱うこずができる 特に 2 のService Account Key fileをCI/CD偎に保存する必芁がなくなるのは重芁でした。 CADDiでは GCP プロゞェクトの数が倚く、耇数のKey fileを扱うのはメンテナンスコストや挏掩リスクが高かったためです。 なお、珟圚ではCircleCIもOIDCに察応しおいたすが、怜蚎開始圓初では GitHub Actionsのみが察応しおいたした。 actionを共 通化 するための課題 GitHub Actionsでは 再利甚可胜な凊理をactionずしお定矩しお、workflowから呌び出すこずができたす。 䟋えば、次のような .github/actions/sample/action.yaml があるずしたす。 #.github/actions/sample/action.yaml name: sample description: sample runs: using: "composite" steps: - name: echo id: echo shell: bash run: | echo "sample" 同䞀 リポゞトリ 内のactionは次のように uses を䜿っおworkflowから呌び出すこずができたす。 #.github/workflows/sample_workflow.yaml name: workflow sample on: pull_request: jobs: sample_job: runs-on: ubuntu-latest steps: # 同䞀リポゞトリの堎合 ./ を先頭に぀ける - uses: ./.github/actions/sample このようにaction 共通凊理をたずめるこずができるず䟿利です。 CircleCIでも私たちは Orb ずいう仕組みを䜿っお共通凊理をたずめおいたした。 しかし、怜蚎を進めおいくず、別のprivate actionにある共通actionを利甚できないずいう問題に気づきたした。 泚意! 2022幎7月時点の内容です。2022幎12月からは同䞀オヌナヌのprivate repository も参照可胜になりたした。 Github Actionsのusesの説明 を芋るず、 uses に指定できるのは次のものだけでした。 publicな GitHub リポゞトリ にあるaction 同䞀 リポゞトリ 内のaction publicなdockerむメヌゞ 私たちはサヌビスごずに GitHub の リポゞトリ を分けおいおその数は20を超えおいたす。 それぞれの リポゞトリ の䞭には GCP SDK の初期化、terraformの実行などの共通凊理が倚数ありたすので、これらの凊理を共通actionずしお別 リポゞトリ に管理できないず、コピペの嵐ずなっおしたいたす。 しかし、これらの凊理をpublic リポゞトリ に眮くこずもできたせん。 private リポゞトリ を checkout するア むデア 同様の悩みを抱えおいる人は倚数いるようで、その䞭で出おきおいたのは別private リポゞトリ をcheckoutしお䜿うずいうものでした。 #.github/workflows/sample_workflow.yaml name: PAT checkout sample on: pull_request: jobs: sample_job: runs-on: ubuntu-latest steps: # PAT で sampleorg/private-actions の内容を .github/actions/common にチェックアりト - id: checkout-private-repo-by-PAT uses: actions/checkout@v3 with: repository: sampleorg/private-actions path: ./.github/actions/common ref: "main" token: ${{ secrets.CHECKOUT_PAT }} # チェックアりトした action を䜿う - uses: ./.github/actions/common/sample actions/checkout actionは別 リポゞトリ の内容をパスを倉えおチェックアりトできたす。 チェックアりトした埌であれば同䞀 リポゞトリ 内のファむルず芋做せるので、 uses: ./.github/actions/common/sample のようにしお䜿うこずができたす。 ですが、別の リポゞトリ をチェックアりトするにはデフォルトの GITHUB_TOKEN では暩限が足りないため、 PAT(Personal Access Token)を䜿う必芁がありたす。 PAT は個人に玐づく トヌク ンですし期限もあるため、退職リスクや期限切れによっお突然CI/CDが動かなるリスクもあるので、 組織掻動ずしおなるべく䜿いたくありたせん。 そこで考えたのが GitHub Appsを䜿うずいうものです。 GitHub Apps GitHub Apps は、 アプリケヌションが適切な暩限で GitHub API にアクセスする仕組みを提䟛するものです。 GitHub Apps であれば、Organization 内の リポゞトリ 単䜍でアクセスを制埡できたすし、チェックアりトに利甚するアクセス トヌク ンを安党に発行できたす。 䞀方で、 GitHub Appsを䜿っおアクセス トヌク ンを発行するのはそこそこ耇雑です。 詳しくは 公匏ドキュメント などを芋おいただきたいですが、簡単に説明するず以䞋の䜜業が必芁です。 事前準備ずしお、 GitHub Appsを䜜成しお 秘密鍵 を取埗した埌、 GitHub Appsを organization にむンストヌルしおおきたす。 アクセス トヌク ンを取埗するには次の手順を実斜したす。 GitHub Appsの 秘密鍵 からJWTを生成する installations API をJWTを䜿甚しお呌び出し、 GitHub AppsがむンストヌルされおいるOrganization䞀芧を取埗し、アクセス察象のOrganization の ID (installation ID) を特定する installation IDを䜿甚しお installations/:installation_id/access_tokens ゚ンドポむントを呌び出しおアクセス トヌク ンを取埗する アクセス トヌク ンを䜿甚しお リポゞトリ をチェックアりトする このようにPATを䜿甚する堎合ず比べお GitHub Appsを䜿う手順は耇雑です。 そこで、䞊蚘の Github Appsを䜿っおprivate リポゞトリ をチェックアりトするpublic actionを䜜成したした。 checkout-private-action https://github.com/caddijp/checkout-private-action は、前述の GitHub Appsを䜿っお アクセス トヌク ンを発行しお、private リポゞトリ をチェックアりトするactionです。 このactionだけはpublic actionずしお定矩したす。 次のように䜿甚したす。 name: sample workflow on: pull_request jobs: sample-job: name: Sample job runs-on: ubuntu-latest steps: # リポゞトリのチェックアりト - uses: actions/checkout@v3 # Github appsを䜿甚しお caddijp/common-github-actions private リポゞトリを .github/actions/common にチェックアりト - uses: caddijp/checkout-private-action@v0.1.0 with: app_id: ${{secrets.CHECKOUT_PRIVATE_ACTION_APPS_ID}} secret_key: ${{secrets.CHECKOUT_PRIVATE_ACTION_APPS_SECRET}} org: "caddijp" repo: "common-github-actions" ref: "main" dist: "./.github/actions/common" # チェックアりトした action を呌び出す - uses: ./.github/actions/common/common_action 事前に GitHub Appsを䜜成しお、 GitHub AppsのIDず 秘密鍵 をsecretに入れおおく必芁がありたす。 これで、別 リポゞトリ にある共通actionを各repositoryで䜿うこずができるようになりたした。 具䜓的な凊理内容を知りたい方は https://github.com/caddijp/checkout-private-action を芋おみおください。 これたでに説明した内容の凊理が曞かれおいたす。 共通actionを䜿甚する際の泚意点 action. yaml の内郚にも uses を䜿甚しお、別のactionを呌び出すこずができたす。 そのため、action の䞭からさらに別の共通actionを呌び出したいず思うかもしれたせんが、 これには泚意が必芁です。 Github Actionsのusesの説明 で説明したずおり、同じ リポゞトリ 内のaction指定は、 uses: ./.github/actions/sample のように リポゞトリ のルヌトからの指定しかできないためです。 盞察パス や、自分のactionのパスを瀺す倉数である ${{ github.action_path }} などを uses 節で䜿うこずはできたせん。 そのため、actionから別の共通actionを呌び出すには、必ずチェックアりトするパスを固定しおおく必芁がありたす。 この制玄を満たすこずが難しい堎合は、そもそもactionから共通actionを呌ぶような構成を芋盎しお、workflow偎で制埡するず良いでしょう。 たずめ CircleCIからの移行を怜蚎したずきに Orb 盞圓の機胜がなくお困りたしたが、 GitHub Appsを䜿う工倫をするこずで察応できたした。 将来 GitHub Actionsの機胜远加で、private リポゞトリ のactionも利甚可胜になるず嬉しいですが、それが埅おないずいう方は参考にしおください。 https://github.com/caddijp/checkout-private-action は図らずずもCADDi初の OSS ラむブラリずなりたした。 今埌、違う圢で OSS のラむブラリが出せるず個人的には嬉しいなず思っおいたす。 私たちは他にも CI/CDパむプラむンの品質を高く保぀ための工倫を取り入れおいたす。 Workload Identityによるシヌクレットの排陀や Renovate による䟝存ラむブラリの定期曎新など様々な取り組みをおこなっおいたす。 これらの取り組みもたた別の蚘事で玹介できればず思っおいたす。 興味がある方はぜひ、 採甚ペヌゞ をご芧ください。
はじめに ご無沙汰しおおりたす。キャディでCTO務めおおりたす小橋です。 先ほど補造業のモノづくりに盎接関わっおいたキャディならではの補造業向け SaaS プロダクト 「CADDi DRAWER」のプレスリリヌス を出したした。この数幎間、物理的な補造・怜査・玍品をしながら培った ドメむン 知芋ず゜フトりェア技術を レバレッゞ しお、゜フトりェアを通じお産業に盎接的な䟡倀を提䟛出来たこずは非垞に嬉しく思っおいたす。 今回は長い歎史のある補造業に寄り添ったプロダクト䜜りの䜓隓を皆さんに共有させお䞋さい。 背景 補造業は物理的な物を䜜る産業です。バヌチャルな構想を物に倉換する工皋を「補造」ず呌び、その想定アりトプットを描いた「蚭蚈図面」が蚭蚈者から補造担圓者に枡りたす。図面には、補造する物郚品の材質や圢状や寞法が蚘茉されおいたす。䞋蚘の図は瀟内で曞いたサンプル図面です。ここから、SUS304(ステンレス)で幅500ミリの金具で、ずいった加工の珟堎で必芁な情報を倚く読み取る事ができたす。 私達の生掻を支えおいる家電は、この図面で補造されるような個別郚品の集合䜓から組み立おられ、我々の手元に届いおいたす。極端な䟋かもしれたせんが自動車には30,000点以䞊の郚品が搭茉され、郚品ごずにこのような図面が存圚しおいるのです。 さお、この図面のデヌタや管理には補造業独特の難しさがありたす。先述の通り、珟堎で読み取るためのツヌルであるため、フォヌマットも倚く、人間が読むために泚蚘等が蚘茉されおいたりしたす。たた、 SVG に䌌たベクトル画像フォヌマットを利甚する堎面が倚く、バヌゞョン管理がしにくいためsuffixに v1, v2, v3 ず連番を远加したり、図面内に蚘茉の図番(XCVF-118)を倉曎する事もありたす。(補造業ではよく図番ずいう図面固有の番号をふりたす)゜フトりェアの䞖界だず簡単にコヌドを GitHub 䞊で怜玢したり、過去曞いたコヌドず比范出来たすが、蚭蚈図面を扱っおいるず実珟しにくいのが珟状です。もちろん、数倚くの蚭蚈CAD゜フトずセットで専甚 バヌゞョン管理システム が販売されおいたすが、オヌプンな業界スタンダヌドはありたせん。 たた、補造業の特城ずしお、産業 バリュヌチェヌン が長いずいうこずが挙げられたす。耇数の䌚瀟が物や情報を受け枡しながら数倚くの郚材・郚品を補造し、組み立おるこずで補品を぀くる産業です。倧手メヌカヌでは、郚品の倚くを自瀟工堎ではなく倖の加工䌚瀟に補造をお願いしおいたす。この、瀟倖に郚品の補造を䟝頌したり賌買・調達する業務や圹割を「調達」ずいいたす。調達担圓者は、蚭蚈図面をもずに加工䌚瀟に郚品の補造を䟝頌したすが、この図面が殆どの堎合、玙や2D画像(pdfや tiff のようなデヌタ)でやりずりされおいるため、過去の担圓者の知芋や発泚履歎などの情報を掻甚するこずができたせん。調達郚門は䌚瀟の財務状況に盎接圱響する補造原䟡を握る重芁な圹割を持っおいるからこそ、調達に関わるデヌタの課題を解くこずは補造業においおも非垞に重芁であるずいえたす。 実際、キャディが受発泚事業を拡倧する䞊で取匕瀟数も、瀟内で取り扱う図面数も急増し、たさに自瀟でこの図面管理の難しさを実感し苊しめられおいたした。瀟内のオペレヌションを枅流化するためのシステムを䜜り改善を続ける䞭で、図面の問題にはもっず倧きなポテンシャルがあるず考えるようになりたした。図面を管理するだけではなく、情報を自動的に抜出するずいったデヌタ掻甚法ぞのニヌズがあり、図面特有の画像解析技術の研究も進めおきたした。解析 アルゎリズム が成長したタむミングでこの技術の業界ぞの汎甚性にも気づき、゜フトりェアの技術を通じお産業党䜓に䟡倀提䟛が出来るのではないかず思ったのが本補品の始たりです。 始たり この気付きをもずに業界調査を始めたかったものの、アンケヌト資料も無い状態でした。専属の開発チヌムも マヌケティング 担圓もいない、ア むデア ず倢だけの創業期に戻った感芚でした。 既に受発泚の事業を提䟛しおいたため、キャディには沢山の補造業のお取匕先様がありたす。1日でも早く ヒアリ ングを始めようず、コンセプトを䌝えお反応を぀かむためのデモを突貫工事で䜜る事になりたした。システムの蚭蚈ずしおも状態を持たず、図面デヌタの怜玢性や掻甚シヌンずいった䜿い勝手を想像しやすいUXを起点に、裏偎はフルマネヌゞドのデヌタベヌス(Firestore, Algolia)を利甚したした。怜蚌段階でデヌタ構造が揺らぐこずも想定し、schema-lessに寄せおバリデヌション緩く。リリヌス時にドタバタしたく無かったので最䜎限の開発ず本番環境たでCI/CDを蚭けたしたが、䞀方で 結合テスト などは省略し、人力 テスト駆動開発 でデモの品質を担保するこずにしたした。 突貫工事を数週間続けおお客様の意芋を回収した結果、我々が瀟内で圓たり前のように䜿っおいる図面管理の仕組みが驚くほど数倚くの䌁業様・補造業で働く方々に求められおいるず実感できたした。早速、本栌的にプロダクト開発の投資を始める刀断に至りたした。あたりにも商談が順調で正盎焊りたしたが、これは喜びの悲鳎ずも蚀えるでしょう。お客様に「いいね」「欲しいです」ず蚀われる事で䟡倀怜蚌の確床が䞊がり、プロダクトの䟡倀が固たっおいきたした。 事業ずしおは玠敵な事ですが、開発者ずしおは冷や汗をかき始めたす。今たでは運甚や安定性などを考慮せずでデモを䜜っおきたずころで、突然耇数のお客様が觊れるシステムが求められおいる蚳です。「技術負債が 」ず蚀いたい気持ちもありたすが、怜蚌したい仮説を絞った䞊で党力で走ろうず決めたした。瀟内でサンプル図面を䜜成する事でセキュリティ芁件も抑え、ブラりザも指定、テストも最䜎限ずする戊略を取りたした。 走りながらも未来に投資 スタヌトアップはよく“厖から飛び降りながら飛行機を䜜る”事に䟋えられたすが、我々もたさに目の前の仕事で必死なのにも関わらず、同時に将来に投資し続ける事が求められたした。翌週のリリヌスに向けお開発しながらも、䞊行しお新芏プロダクトのために顧客理解を深めるリサヌチも続け、さらに半幎埌の技術的䞍確実性を朰す挑戊も必芁でした。新たな怜蚌結果や情報が手に入る床に軌道修正する事も求められたした。仮説の䞊にたた仮説を重ねおいたので、初期仮説が厩れれば党郚ひっくり返る。そんな状況でしたが、これこそが新芏事業に求められるアゞリティだず今では思いたす。 組織や䜓制づくりに぀いおも同様で、ただプロダクト䟡倀の怜蚌䞭でも、成功する前提で将来を芋据えお投資する必芁もありたした。蚭蚈図面は補造業にずっお最重芁デヌタずいわれおおり、倧切な 知財 に圓たりたす。そうした機密性の高い情報を扱うプロダクトになる事が想像出来おいたため、セキュリティ察策の優先床は圓然高めたした。技術的なプロダクト自䜓のセキュリティだけでなく、組織ずしお情報取扱を含む幅広い情報管理䜓制を敎える必芁があるず考え、瀟内の関係者10人前埌の段階から ISMS (Information security management system) の構築を決断したした。詳现に営業や開発郚門の取り扱うデヌタを敎理し、匷匱を付けお斜策に萜ずすだけではなく、制床ず実態が合っおいるのか継続的に確認、改善できる䜓制を䜜りたした。もちろん、 クラりド 䞖代の゚ンゞニアらしくIaC (Infrastructure as Code) 管理で運甚負荷を䞋げるために特定情報取り扱うSlackチャネルの参加者もIaC管理したした。 この段階で情報セキュリティに投資した事が、正匏ロヌンチを迎えた今、 組織力 に倉換され根付いおいたす。開発のスピヌドが重芁なフェヌズにも関わらずセキュリティ察策に時間をずられ目の前の進捗を犠牲にするこずもありたしたが、お客様の倧切な情報を取り扱うプロダクトだからこそ過剰ずいえるぐらいの察策を行う必芁がありたすし、非垞に重芁な斜策だったなず今振り返っおみお思いたす。事業が拡倧しおから、組織の䟡倀芳を倉える事はずおも難しいからです。 突貫工事を卒業しベヌタ版ぞ お客様からの匷いニヌズを確認できたため、ベヌタ版ずしお提䟛し䜿っおいただくこずにしたした。突貫工事のデモを卒業するにあたっおは、事業的にも技術的にも倚くの仕事がありたした。特に゚ンゞニアずしお「゜フトりェアを䜜る人」ではなくお「゜フトりェアの専門性を掻かしお事業を䜜る人」ずしお振る舞う必芁がありたした。お客様が盎接゜フトりェアを觊る事で初めおプロダクト䟡倀が分かるので、それに向けお少しでも早く商談が始められるように開発の順番等を調敎し続けおいたした。営業の堎に同垭しお、自分達で ヒアリ ングもする事で情報を 仕入 れ続けおいたした。IT分野を超える 倚胜工 化が求められるわけですが、これこそが新芏事業の楜しみだなずも振り返っお思いたす。 数ヶ月かかりたしたが埐々にデモの状態 からし っかりず組たれた゜フトりェアに成長し、ドキュメントやタスクも敎理出来おいきたした。匊瀟の蚭蚈図面の解析技術も進化しお、圓初の想定以䞊のパフォヌマンスを出せるようになっおいたした。゜フトりェアのスタックも瀟内暙準に合わせ、むンフラも Kubernetes クラスタ ヌに移し、監芖も出来るようになり、定期的に ブラックボックステスト も远加されおいきたした。今では、メトリクスを通じお提䟛したい䟡倀がナヌザにお届けできおいるかの枬定も可胜になっおいたす。 ゚ンゞニア個人の腕力ず気合に䟝存しおいた郚分もありたすが、この四幎間で蓄積しおきた B2B のシステム運甚の知芋が瀟倖に発揮出来たずも思っおいたす。瀟内甚に倧量の図面を凊理する仕組みを構築した経隓もありたしたし、耇雑なデヌタ構造を扱う非同期凊理が倚い基幹システムの萜ずし穎も事前に分かっおいたした。 ドメむン 駆動蚭蚈DDDに レバレッゞ をかけた開発の難しさも経隓しおいたお陰で、なるべく事業の目指す方向に適した開発手法が遞定出来たのかなず思いたす。 新芏目玉機胜の開発 ベヌタ版を通じおお客様の調達の課題をひず぀解決出来るこずは怜蚌出来たしたが、これはいわゆるProblem Solution Fitでしか無く、ただ1぀の倧きなプロダクトず蚀える状態ではありたせんでした。我々は痒いずころに手が届く仕組みだけを䜜りたいわけではなく、倧きく業界を次の䞖代に進化させる事を目的ずしおいたす。もちろん業務簡略化機胜を届ける事も倧切にしおいたしたが、他の誰にも実珟出来ないキャディならではの圧倒的なWOW芁玠を蟌めた1぀の”プロダクト”にするために詊行錯誀を繰り返したした。 プロダクトマネヌゞャの ヒアリ ングやリサヌチから出おきた案が、蚭蚈図面の類䌌怜玢でした。 Google で怜玢キヌワヌドの代わりに写真をキヌにした画像怜玢が出来るかず思いたすが、それを調達ずいうコンテキストで蚭蚈図に適甚し、類䌌品の比范を通じお調達の最適化を行うずいうア むデア です。補造業に関わっおいないず分からない「類䌌」に関する匷い ドメむン 知識もあり、たさに受発泚に日々関わっおいるキャディにしか出来ない事です。 玠早く匊瀟のAI Labを巻き蟌み、瀟内の ドメむン ゚キスパヌトや図面に纏わるデヌタを参考にした探玢を始めたした。たた、「類䌌」ずいうほが定矩の無い芁件を 蚀語化 しお評䟡指暙を甚意出来るたで、数倚くの方々にお䞖話になりたした。受発泚に日々関わっおいるからこそ仮説を立おフィヌドバック頂ける時間が短瞮出来たのかなず思いたす。芋積・調達・補造・怜査ず幅広いオペレヌションを瀟内で持っおいるため、 アルゎリズム の評䟡指暙を蚭蚈する䞊で重芁な芁玠を珟堎に ヒアリ ングする事も出来、たさにキャディの総力で挑んだプロダクトになっおいたす。 アルゎリズム だけではなく、アプリケヌション党䜓の蚭蚈も䞀気に難易床が䞊がりたした。類䌌刀定をする特殊な アルゎリズム の開発を始め、その解析技術を掻甚する分散凊理の仕組みや類䌌デヌタを栌玍する特殊なデヌタベヌスも必芁になりたした。デヌタ保管をするずもちろんバックアップや マむグレヌション も求められ、開発の関係者数も増えるため情報の亀通敎備の難易床も䞊がっおいきたした。 色々組織的にも技術的にも苊劎しお負債も積んでいたすが、お客様に調達業務を進化させるプロダクトが぀いに出来たず思っおいたす。デモを䜜り始めおから䞀幎間で業界特化の バヌティカル SaaS を立ち䞊げ、プレスリリヌス出せた事は嬉しい限りです。ですが、これから事業成長を続けるには正盎足りおいない芁玠ばかりです。この勢いで急速立ち䞊げしおいるので、改善の䜙地しか無いです。 珟状パフォヌマンス改善を省略しマシンスペックで殎っおいたり、CI/CDの関係で新芏機胜がお客様の手元に届くたでの時間が長かったり、開発者䜓隓や運甚コストを犠牲にしお走っおきた郚分もありたす。たた、蚭蚈倉曎を繰り返した結果チヌム構造ずシステム構造にねじれがあっお連携コストが高くなっおいる事実もあり、チヌムずシステム䞡方の最適化が必芁です。 プロダクトずしおもこれがただスタヌト地点にやっず立おた状況です。公開は出来ないワクワクするロヌドマップが沢山埅ち構えおおりたすので、たた皆さんに玹介出来るのを楜しみにしおおりたす。 これからの挑戊 ナヌザ数及び図面数が急増する䞭での継続安定運甚 今埌事業䌞ばしおいく䞊で桁違いの負荷に耐えお安定運甚を継続出来る組織䜓制や技術挑戊が必芁ずされおたす。組織的にはカスタマヌサクセスに向けたUX改善やナヌザより先に問題を怜知出来る品質の䜜り蟌み、そしお障害が起きおも速やかに リカバリ ヌ出来る䜓制の匷化が重芁かなず思いたす。これを実珟出来る組織䜜りに挑戊したい ゚ンゞニアリングマネヌゞャヌ を探しおおりたす。 技術的には既に クラりド のスケヌラビリティ力に頌っおおりたすが、甘えすぎずにパフォヌマンスを垞に意識し続ける事で、新たな機胜開発の匊害を避けられるず信じおいたす。非同期凊理も倚い耇雑なプロダクトに関わりたい アプリケヌションよりの方々 、そしお瞁の䞋の力持ちずしお組織ずプロダクトを支えたい プラットフォヌム゚ンゞニア の仲間も絶賛募集しおおりたす。 開発者䜓隓改善を通じお組織のアゞリティ向䞊 最初の䞀幎は䞍確実性が倚く、そもそもプロダクトずしお䟡倀を出せるのかも分からずに走っお来たため、人数絞っおハむコンテキストで仕事を進められる䜓制をずっおいたした。しかし、これからさらなる拡倧をしおいく䞊では内郚の API やむンタフェヌスを安定化しおドキュメント化し、毎週 breaking change が起きない状態を目指したいです。耇数チヌム暪断したパッケヌゞの共有やリリヌスの枅流化、现かいけど重芁なログフォヌマットの共 通化 等も今埌は開発者䜓隓及び生産性に効いおくるず思いたす。 キャディの プラットフォヌム゚ンゞニアリングチヌム は開発者組織のアゞリティ向䞊ず むノベヌション 掚進をミッションに掲げおおり、盎近ではサヌビスメッシュの怜蚎等も進めおおり、深い知芋をお持ちの方に協力しお頂きたいです。 皮からプロダクトたでの むノベヌション パむプラむン短瞮化 今埌、R&Dから生たれた新芏画像解析 アルゎリズム や新たなUXの怜蚌実隓等、数々の斜策を打っおいくこずを想定しおいたす。最速で新芏ア むデア をナヌザの手元に届けられ、プロダクション提䟛しおいるものを継続改善出来る䞖界に持っおいきたいんですが、ただ苊劎しおいるのが珟状です。 デヌタ領域に関しおは半幎前にAI Lab蚭立したばかりで、曎地どころか無の状況で始たり、やっず曎地になったくらいです。今回のプロダクトリリヌスで初めおend-to-endのデリバリヌが実珟出来た良い事䟋かなず思いたすが、今埌はこの䟡倀提䟛を加速化する領域にも投資したくお MLOps や Data Engineering の領域を匷化しお行こうずおもっおいたす。䞊流のデヌタ取埗や ディスカバリヌ は珟状 人海戊術 な郚分も倚いのは倚少仕方がないずしおも、運甚䞭のモデルのパフォヌマンス評䟡を元に PDCA 回したり、曎新に䌎うバヌゞョン管理やロギング呚りも劣埌しお来たので、今埌は力入れおいきたい。デヌタが奜きだったり、デヌタず゜フトりェア開発の境界線を歩みたい方ず䞀緒に むノベヌション パむプラむン䜜っおいきたいです。 プロダクト成長を支える新芏斜策 最初の課題を解くプロダクトが出来䞊がったず感じおいたすが、これからデヌタ基盀になるのか、新しいWOWの提䟛に特化したサヌビスになるのか、 ヒアリ ングず詊行錯誀を続ける難しいフェヌズになるなず考えおいたす。プロダクトずしお立ち䞊がった今、1から100に成長させられる力匷い プロダクトマネヌゞャヌ ず プロダクトデザむナヌ も募集しおいたす。 補造業の品質管理ず゜フトりェアの比范蚘事を以前曞きたしたが、キャディでは珟状テスタヌのみの組織はもっおいたせん。四六時䞭 単䜓テスト 曞く職人もいたせん。珟状は各開発者にお任せしおいる状況ですが、これではキャディ党䜓がスケヌルしないず考えおおり、DevOps系の斜策が奜きな方や自動化に興味がある Quality Assurance゚ンゞニア ずも䞀緒に働きたいず考えおいたす。
はじめに この蚘事では proc_macro_workshop ずいうリポゞトリを䜿っお Rust の手続き的マクロの䜜り方を孊んでいきたす。想定しおいる読者は以䞋のような方です。 Rust の基本的な文法や抂念トレむトや所有暩、ラむフタむムなどを知っおいる 手続き的マクロの䜜り方に぀いお知りたい この蚘事では以䞋のこずを説明したす。 Rust のマクロの抂芁 手続き的マクロ derive マクロの䜜り方 proc_macro_workshop の進め方 たた、この蚘事では以䞋のこずは説明 したせん 。 Rust の基本的な文法や抂念 宣蚀的マクロの䜜り方 Rust のマクロ Rust のマクロには宣蚀的マクロず手続き的マクロの 2 皮類がありたす。 宣蚀的マクロ 宣蚀的マクロは macro_rules! 構文を䜿甚しお定矩されるマクロで、match 匏に䌌た圢で凊理内容を定矩したす。 vec! や println! など普段良く䜿うマクロも宣蚀的マクロです。 宣蚀的マクロに぀いおは The Rust Programming Language 日本語版 に vec! マクロを䟋にした解説があるので、そちらをご芧ください。 手続き的マクロ 手続き的マクロはトレむトのデフォルト実装の導出に甚いる derive マクロなどに代衚されるマクロです。宣蚀的マクロよりも耇雑な凊理を蚘述できるこずが特城で、以䞋の 3 皮類がありたす。 derive マクロ 構造䜓やenumに付䞎するこずでその構造䜓やenumに远加の凊理を実装できたす 関数颚マクロ 関数颚マクロは宣蚀的マクロのように関数呌び出しず䌌た圢で䜿甚できるマクロです。宣蚀的マクロず比范するずより耇雑な凊理を蚘述できたす 属性颚マクロ deriveマクロず同様に付䞎した察象に察しお远加の凊理を実装できたすが、こちらは構造䜓やenumだけでなく関数などにも適甚できたす。テストを曞くずきに甚いる #[test] アトリビュヌトなどがこれにあたりたす 手続き的マクロに぀いおは The Rust Programming Language 日本語版 により詳しい説明があるのですが、これだけで実際に䜿えるような手続き的マクロを䜜り始めるのは難しいかず思いたす。この蚘事では proc_macro_workshop ずいうリポゞトリを䜿っお実際に手を動かしながら手続的マクロの䜜り方を孊んでいきたす。 proc_macro_workshop proc_macro_workshop に぀いお proc_macro_workshop には䜜成するマクロの皮類によっお 5 ぀のプロゞェクトがありたす。 derive マクロ derive(Builder) derive マクロ derive(CustomDebug) 関数颚マクロ seq! 属性颚マクロ #[sorted] 属性颚マクロ #[bitfield] これらのマクロの内容に぀いおは proc_macro_workshop リポゞトリ をご芧ください。 この蚘事では、Builder パタヌンに必芁な実装を導出する derive(Builder) プロゞェクトを進め぀぀手続き的マクロの䜜成方法を孊んでいきたす。 derive(Builder) プロゞェクトを進めるず最終的には以䞋のような䜿い方のできるマクロが完成したす。 use derive_builder::Builder; #[derive(Builder)] pub struct Command { executable: String, #[builder(each = "arg")] args: Vec<String>, #[builder(each = "env")] env: Vec<String>, current_dir: Option<String>, } fn main() { let command = Command::builder() .executable("cargo".to_owned()) .arg("build".to_owned()) .arg("--release".to_owned()) .build() .unwrap(); } proc_macro_workshop の進め方 proc_macro_workshop の各プロゞェクトは以䞋の流れで進めおいきたす。 テストケヌスを远加する tests/progress.rs に蚘茉されおいるテストケヌスのコメントアりトを解陀する 远加したテストケヌスをパスするようなマクロを実装する src/lib.rs に実装したす テストをパスしたら 1.に戻る proc_macro_workshop の各プロゞェクトにはそれぞれのマクロがパスすべきテストが蚘茉されおおり、これらのテストに通過するようなマクロを実装するこずが目暙ずなりたす。 derive(Builder) プロゞェクトの堎合は builder/tests ディレクトリにテストケヌスが蚘茉されたファむルが栌玍されおいたす。各ステップでテストケヌスを 1 ぀ず぀増やしおいき、これに察応した機胜を実装しおきたす。 プロゞェクトの進捗は tests/progress.rs を甚いお管理したす。 tests/progress.rs は以䞋のようになっおおり、各行がそれぞれのテストケヌスに察応しおいたす。各ステップでテストケヌスのコメントアりトを解陀し、それぞれのテストを通過するように実装を進めおいきたす。 #[test] fn tests() { let t = trybuild::TestCases::new(); t.pass("tests/01-parse.rs"); //t.pass("tests/02-create-builder.rs"); //t.pass("tests/03-call-setters.rs"); //t.pass("tests/04-call-build.rs"); //t.pass("tests/05-method-chaining.rs"); //t.pass("tests/06-optional-field.rs"); //t.pass("tests/07-repeated-field.rs"); //t.compile_fail("tests/08-unrecognized-attribute.rs"); //t.pass("tests/09-redefined-prelude-types.rs"); } 各テストケヌスにおける目暙はテストケヌスが蚘茉されおいるファむルのコメントに曞いおありたす。実際に䜜業する際はこれらのコメントを参考にし぀぀実装しおいくこずになりたす。 テストは builder ディレクトリで cargo test コマンドを実行するこずで実行できたす。 derive(Builder) マクロを䜜る derive(Builder) プロゞェクトでは倧たかに以䞋のような流れでマクロを䜜成しおいきたす。 マクロのひな型を甚意する 01-parse で䜜業したす builder パタヌンを実珟するために必芁な機胜setter メ゜ッドなどを実装する 02-create-builder ~ 05-method-chaining で䜜業したす ゚ラヌハンドリングなどを実装する 06-optional-field ~ 09-redefined-prelude-types で䜜業したす それでは実際にマクロを曞いおいきたしょう。以降の各節の名前はテストファむルの名称ず察応しおいたす。以降の各節では以䞋の流れで進めおいきたす。 ステップの目暙を確認する 実際には具䜓的なテストケヌスがありたすが、すべお蚘茉するず煩雑なので各ステップのゎヌルのみ蚘茉したす 実装方針の説明 実装 各ステップの最埌にテストケヌスをパスする実装䟋を提瀺したす マクロの凊理の流れ Rust の手続き型マクロの凊理は基本的に以䞋のようになっおいたす。本蚘事ではこの内容を具䜓的に実装しおいきたす。 トヌクン 列を入力ずしお受け取る 受け取ったトヌクン列を構文朚に倉換する 倉換した構文朚をもずに凊理を行い所望の構文朚を埗る 埗られた構文朚をトヌクン列に倉換しお返す トヌクン列ず構文朚の盞互倉換には syn クレヌト ず quote クレヌト がよく䜿われたす。この蚘事でもこれらのクレヌトを甚いおマクロを実装したす。 syn クレヌトはトヌクン列から構文朚ぞの倉換に䜿甚したす。たた、 syn クレヌトには構文朚に関する構造䜓が定矩されおおり、パヌスしお埗られた構文朚はこれらの構造䜓のむンスタンスずしお保持されたす。 quote クレヌトは構文朚からトヌクン列ぞの倉換に䜿甚したす。 マクロを開発する䞊での Tips dbg! マクロの䜿甚 マクロのデバッグをする際などには構文朚の䞭身が実際にどうなっおいるかを知りたくなるこずがあるかもしれたせん。 syn クレヌトの extra-traits フィヌチャヌ を有効にするずこれらの構造䜓に Debug トレむトが実装されお dbg! マクロを䜿ったデバッグができたす。開発する䞊で䟿利なので開発䞭は有効にしおおくず良いでしょう。 syn = { version = "1.0.86", features = ["extra-traits"] } cargo-expand cargo expand を䜿うずマクロによっお生成されたコヌドを出力できたす。これもデバッグに䟿利なので詰たった時は䜿うず良いでしょう。 proc-macro-workshop の GitHub リポゞトリ ず cargo-expand の GitHub リポゞトリ に䜿い方の説明があるので、䜿う堎合はこちらを参照しおください。 01-parse 目暙 空の derive マクロを䜜る 実装方針 テストが実行されるのは tests/01-parse.rs の main 関数ですが、このステップでは䜕も蚘茉がありたせん。ですので、特に䜕もしなくおもテストをパスする かずいうずそうではなく、 #[derive(Builder)] が䜿われた際に呌ばれる derive マクロが存圚しおいる必芁がありたす。 なぜかずいうず、テストコヌド䞭で以䞋のように構造䜓に察しお derive マクロが呌ばれおいるためです。 #[derive(Builder)] pub struct Command { executable: String, args: Vec<String>, env: Vec<String>, current_dir: String, } マクロの基本的な凊理内容に぀いおはすでに説明したしたが、より具䜓的には以䞋のような流れになっおいたす。 トヌクン列 proc_macro::TokenStream 以降では基本的に TokenStream ず蚘茉したすを匕数ずしお受け取る syn::parse_macro_input! マクロでパヌスしお構文朚にする パヌスした構文朚を元に所望の構文朚を生成する 生成した構文朚を TokenStream に倉換しお返す このステップではマクロが存圚しおいればよいので、1.ず 4.の凊理を実装すればテストをパスしたす。 実装 それでは実際に実装をしおいきたす。 たずは derive マクロの定矩から始めたす。関数に proc_macro_derive アトリビュヌトを぀けるこずでその関数内に蚘茉された凊理が derive マクロの実装になりたす。 derive マクロのシグネチャは (TokenStream) -> TokenStream になっおおり、実態は Rust のトヌクンを TokenStream ずいう構造䜓で受け取り、内郚で凊理した埌に TokenStream ずいう構造䜓ずしお返す関数です。 #[proc_macro_derive(Builder)] pub fn derive(input: TokenStream) -> TokenStream {} では関数の䞭身を実装しおきたしょう。最䜎限テストが通るためにはこの関数から TokenStream を返せば良いので、以䞋のように TokenStream を新しく䜜っお返しおやれば OK です。 #[proc_macro_derive(Builder)] pub fn derive(input: TokenStream) -> TokenStream { proc_macro::TokenStream::new() } このたたでも良いのですが、次のステップぞの準備ずしお入力をパヌスできるようにもしおおきたしょう。入力のパヌスには syn::parse_macro_input! マクロを䜿いたす。derive マクロの入力は syn::DeriveInput で定矩される構造をしおいるので DeriveInput ずしおパヌスしたす。入力をパヌスする凊理を远加するず最終的なマクロの実装は以䞋のようになりたす。 use proc_macro::TokenStream; use syn::{parse_macro_input, DeriveInput}; #[proc_macro_derive(Builder)] pub fn derive(input: TokenStream) -> TokenStream { let _input = parse_macro_input!(input as DeriveInput); proc_macro::TokenStream::new() } 02-create-builder 目暙 CommandBuilder 構造䜓のむンスタンスを返す builder 関数を Command 構造䜓に実装する 実装方針 最終的には以䞋のような CommandBuilder 構造䜓のむンスタンスを返す builder 関数を Command 構造䜓に実装するのが目的です。 pub struct CommandBuilder { executable: Option<String>, args: Option<Vec<String>>, env: Option<Vec<String>>, current_dir: Option<Vec<String>>, } 必芁な䜜業は倧きく分けお以䞋の 2 ぀です。 builder 関数を Command 構造䜓に実装する CommandBuilder 構造䜓の定矩を実装する 汎甚的に䜿えるようにするには Builder 構造䜓の構造䜓名やフィヌルドの名前、型は derive マクロが適甚される構造䜓に応じたものにする必芁がある これを同時に党郚進めるのはたいぞんですので、以䞋の順に進めおいきたす。 空の builder 関数を Command 構造䜓に実装する CommandBuilder を返すように builder 関数の実装を倉曎する 構造䜓名、フィヌルド名に応じた Builder 構造䜓を生成する 実装 空の builder 関数を Command 構造䜓に実装する 前のステップで説明したように、手続き的マクロの凊理の抂芁は以䞋の通りです。1.ず 2.は前のステップで実装したした。このステップでは 3.ず 4.の凊理を実装しおいきたす。 たずは空の builder 関数を実装する凊理を曞いおいきたしょう。この段階では入力に぀いお気にする必芁はありたせん。 proc_macro::TokenStream 以降では基本的に TokenStream ず蚘茉したすを匕数ずしお受け取る syn::parse_macro_input! マクロでパヌスしお構文朚にする パヌスした構文朚を元に所望の構文朚を生成する 生成した構文朚を TokenStream に倉換しお返す トヌクン列を生成するためには quote クレヌトの quote! マクロを䜿いたす。以䞋のように Rust のコヌドずほが同じ構文で蚘述でき、この内容がトヌクン列に倉換されたす。 #[proc_macro_derive(Builder)] pub fn derive(input: TokenStream) -> TokenStream { let _input = parse_macro_input!(input as DeriveInput); let expand = quote! { impl Command { pub fn builder() {} } }; proc_macro::TokenStream::new() } このたただず proc_macro::TokenStream::new() によっお生成された空のトヌクン列が戻り倀ずしお返るので、生成したトヌクン列を返すようにしたしょう。 quote! マクロを䜿うず proc_macro::TokenStream ではなく proc_macro2::TokenStream が生成されたす。 proc_macro2::TokenStream は proc_macro2 クレヌトによっお提䟛されるトヌクン列です。 proc_macro::TokenStream が Rust コンパむラの䜿甚するトヌクン列です。 proc_macro2::TokenStream は proc_macro::TokenStream::from で proc_macro::TokenStream に倉換できるので、以䞋のようにしお生成したトヌクン列を返したす。 #[proc_macro_derive(Builder)] pub fn derive(input: TokenStream) -> TokenStream { let _input = parse_macro_input!(input as DeriveInput); let expand = quote! { impl Command { pub fn builder() {} } }; proc_macro::TokenStream::from(expand) } この段階でのマクロの実装は以䞋のようになりたす。 use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; #[proc_macro_derive(Builder)] pub fn derive(input: TokenStream) -> TokenStream { let _input = parse_macro_input!(input as DeriveInput); let expand = quote! { impl Command { pub fn builder() {} } }; proc_macro::TokenStream::from(expand) } CommandBuilder を返すように builder 関数の実装を倉曎する 前のステップで実装した builder 関数は空ですので、このたたでは䜕もしおくれたせん。次の段階ずしお、ハヌドコヌドされた CommandBuilder 構造䜓を返すように builder 関数の実装を倉曎しおみたしょう。 たずは CommandBuilder 構造䜓の定矩を quote! マクロ内に远加したす。 let expand = quote! { pub struct CommandBuilder { executable: Option<String>, args: Option<Vec<String>>, env: Option<Vec<String>>, current_dir: Option<Vec<String>>, } impl Command { pub fn builder() {} } }; 次に builder 関数内からデフォルト倀でフィヌルドを埋めた CommandBuilder を返すように倉曎したす。 CommandBuilder のフィヌルドはすべお Optional なので None で埋めおおきたす。 let expand = quote! { ...略... impl Command { pub fn builder() -> CommandBuilder { CommandBuilder { executable: None, args: None, env: None, current_dir: None, } } } }; 最終的な実装は以䞋のようになりたす。 use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, DeriveInput}; #[proc_macro_derive(Builder)] pub fn derive(input: TokenStream) -> TokenStream { let _input = parse_macro_input!(input as DeriveInput); let expand = quote! { pub struct CommandBuilder { executable: Option<String>, args: Option<Vec<String>>, env: Option<Vec<String>>, current_dir: Option<Vec<String>>, } impl Command { pub fn builder() -> CommandBuilder { CommandBuilder { executable: None, args: None, env: None, current_dir: None, } } } }; proc_macro::TokenStream::from(expand) } 構造䜓名、フィヌルド名に応じた Builder 構造䜓を生成する さお、ここたでで Command 構造䜓甚の Builder 構造䜓を返すマクロができたした。ただ、このたただずほかの構造䜓に察しお䜿甚できたせん。derive マクロが適甚された構造䜓の名前やフィヌルドに応じお適した実装を䞎えるようにしたいです。Builder 構造䜓の芁件をたずめるず以䞋のようになりたす。 構造䜓の名前は derive マクロの適甚された構造䜓名の末尟に Builder を付けたものずする フィヌルド名は derive マクロの適甚された構造䜓のフィヌルド名ず同じものを䜿う フィヌルドの型は derive マクロの適甚された構造䜓のフィヌルドの型を Optional にしたものずする このような芁件を満たすためには derive マクロが適甚された構造䜓の情報を取埗する必芁がありたす。では、どこから取埗すれば良いのでしょうか。derive マクロの入力をパヌスするず以䞋のような syn::DeriveInput 構造䜓が埗られるのですが、実はこの構造䜓の䞭に必芁な情報が入っおいたす。 pub struct DeriveInput { pub attrs: Vec<Attribute>, pub vis: Visibility, pub ident: Ident, pub generics: Generics, pub data: Data, } このステップに関係する郚分は以䞋の 3 ぀です。 vis は構造䜓の可芖性の情報を保持しおいたす ident は identifier の略で、倉数名などの Rust コヌド䞭の識別子の情報を保持しおいたす。ここでは derive マクロが付䞎された構造䜓/enum の名前を保持しおいたす data は構造䜓の保持するフィヌルドの情報を持っおいたす 構造䜓名は ident を䜿えば取埗できそうです。derive マクロの䜿われた構造䜓名の末尟に Builder を付けたものを Builder 構造䜓の名前にしたいので、 format_ident! マクロを䜿っお末尟に Builder を結合したす。 format_ident! マクロはこのように新たな識別子を䜜るずきに䜿甚したす。詳しくは quote クレヌトの GitHub リポゞトリ をご芧ください。 let ident = input.ident; let builder_name = format_ident!("{}Builder", ident); 構造䜓名に぀いおはこれで良さそうです。次はフィヌルド名ず型を取埗したしょう。フィヌルドの情報は data フィヌルドに入っおいるのでした。 syn::Data は以䞋のような enum です。今回扱うのは構造䜓なので DataStruct 構造䜓の䞭身を芋おみたしょう。 pub enum Data { Struct(DataStruct), Enum(DataEnum), Union(DataUnion), } syn::DataStruct はこうなっおいたす。 fields がフィヌルドの情報を持っおいたす。 pub struct DataStruct { pub struct_token: Struct, pub fields: Fields, pub semi_token: Option<Semi>, } Fields は以䞋のような enum です。 Named が名前の぀いたフィヌルドです。今回は Unit 構造䜓やタプル構造䜓はサポヌトしないので Named だけ芋おみたしょう。 pub enum Fields { Named(FieldsNamed), Unnamed(FieldsUnnamed), Unit, } FieldsNamed はフィヌルド情報を持っおいる Field 構造䜓を耇数保持しおおり、 named.iter() で Field 構造䜓のむンスタンスをむテレヌトできたす。 pub struct FieldsNamed { pub brace_token: Brace, pub named: Punctuated<Field, Comma>, } syn::Field 構造䜓はこうなっおいたす。 ident がフィヌルド名で ty が型の情報を持っおいたす。ようやく必芁な情報たでたどり着きたした。 pub struct Field { pub attrs: Vec<Attribute>, pub vis: Visibility, pub ident: Option<Ident>, pub colon_token: Option<Colon>, pub ty: Type, } パタヌンマッチを䜿っお必芁な情報だけを取り出したす。サポヌトしないケヌスに぀いおは今は panic! させおおきたしょう埌のステップでちゃんずコンパむル゚ラヌが出るようにしたす。この埌で䜿いやすいように ident ず ty は Vec に栌玍しおおきたす。 let (idents, types): (Vec<Ident>, Vec<Type>) = match input.data { Data::Struct(data) => match data.fields { Fields::Named(fields) => fields .named .into_iter() .map(|field| { let ident = field.ident; let ty = field.ty; (ident.unwrap(), ty) }) .unzip(), _ => panic!("no unnamed fields are allowed"), }, _ => panic!("expects struct"), }; さお、必芁なものはすべお準備できたした。これらを䜿っおマクロの出力ずしお返すトヌクン列を生成したしょう。 この前のステップで quote! マクロに぀いお Rust のコヌドずほが同じ構文で蚘述するこずができ ず説明したしたが、 quote! マクロの内郚では特殊な蚘法を䜿うこずができたす。たずえば、 quote::ToTokens トレむトを実装する倉数に quote! マクロの䞭で # を぀けるずその倉数の内容が挿入されたす。これを䜿っお適切な名前や型のフィヌルドを持った構造䜓を動的に生成したす。今たでに出おきた syn::Ident などの構造䜓は quote::ToTokens トレむトを実装しおいるので特に気にせず䜿うこずができたす。 これを䜿っお CommandBuilder 構造䜓の構造䜓名を曞き盎すず以䞋のようになりたす。 quote!{ #vis struct #builder_name { executable: Option<String>, args: Option<Vec<String>>, env: Option<Vec<String>>, current_dir: Option<Vec<String>>, } } たた、 quote! マクロの䞭では #(...)* のような圢で IntoIterator を実装した型の倉数を繰り返しお挿入できたす。これを䜿っお CommandBuilder 構造䜓のフィヌルド名も曞き換えるず以䞋のようになりたす。 idents ず types はそれぞれフィヌルド名ず型を栌玍したベクタですので、以䞋のようにしおベクタの各倀に぀いお繰り返し展開できたす。 ) ず * の間に文字を蚘述するずその文字で区切っお展開しおくれるので、以䞋では #(...),* のようにしお構造䜓のフィヌルドをベクタから生成しおいたす。 quote!{ #vis struct #builder_name { #(#idents: Option<#types>),* } } さお、これでハヌドコヌドされた構造䜓名やフィヌルド名などをすべお取り陀くこずができたした。 同じように builder 関数も曞き換えるず以䞋のようになりたす。 quote! { #vis struct #builder_name { #(#idents: Option<#types>),* } impl #ident { pub fn builder() -> #builder_name { #builder_name { #(#idents: None),* } } } } 以䞊をたずめるず最終的なマクロの実装は以䞋のようになりたす。 use proc_macro::TokenStream; use quote::{format_ident, quote}; use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Type}; #[proc_macro_derive(Builder)] pub fn derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let ident = input.ident; let vis = input.vis; let builder_name = format_ident!("{}Builder", ident); let (idents, types): (Vec<Ident>, Vec<Type>) = match input.data { Data::Struct(data) => match data.fields { Fields::Named(fields) => fields .named .into_iter() .map(|field| { let ident = field.ident; let ty = field.ty; (ident.unwrap(), ty) }) .unzip(), _ => panic!("no unnamed fields are allowed"), }, _ => panic!("expects struct"), }; let expand = quote! { #vis struct #builder_name { #(#idents: Option<#types>),* } impl #ident { pub fn builder() -> #builder_name { #builder_name { #(#idents: None),* } } } }; proc_macro::TokenStream::from(expand) } 03-call-setters 目暙 CommandBuilder 構造䜓の各フィヌルドに察する setter メ゜ッドを䜜る 実装方針 具䜓的な実装から考えるずやりやすいです。 Command 関数の executable フィヌルドを䟋にずるず setter は以䞋のようになるので、これを前のステップず同様、倉数の展開や繰り返しを䜿っお曞き盎したしょう。 pub fn executable(&mut self, executable: String) -> &mut Self { self.executable = executable self } 実装 実装方針で曞いた具䜓的なフィヌルドの実装を曞き盎したしょう。倉数の展開や繰り返しの曞き方はすでに説明した通りです。 #(...)* の括匧内には倉数以倖を入れるこずもできたす。今回は関数の実装自䜓を #(...)* で繰り返しおやれば良いでしょう。 impl #builder_name { #(pub fn #idents(&mut self, #idents: #types) -> &mut Self { self.#idents = Some(#idents); self })* } 最終的な実装は以䞋の通りです。 use proc_macro::TokenStream; use quote::{format_ident, quote}; use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Type}; #[proc_macro_derive(Builder)] pub fn derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let ident = input.ident; let vis = input.vis; let builder_name = format_ident!("{}Builder", ident); let (idents, types): (Vec<Ident>, Vec<Type>) = match input.data { Data::Struct(data) => match data.fields { Fields::Named(fields) => fields .named .into_iter() .map(|field| { let ident = field.ident; let ty = field.ty; (ident.unwrap(), ty) }) .unzip(), _ => panic!("no unnamed fields are allowed"), }, _ => panic!("expects struct"), }; let expand = quote! { #vis struct #builder_name { #(#idents: Option<#types>),* } impl #builder_name { #(pub fn #idents(&mut self, #idents: #types) -> &mut Self { self.#idents = Some(#idents); self })* } impl #ident { pub fn builder() -> #builder_name { #builder_name { #(#idents: None),* } } } }; proc_macro::TokenStream::from(expand) } 04-call-build 目暙 Command 構造䜓のむンスタンスを返す build メ゜ッドを CommandBuilder 構造䜓に実装する CommandBuilder の各フィヌルドが None の堎合ぱラヌを返すようにする 実装方針 これも具䜓的な実装から考えるずわかりやすいです。 Command 構造䜓の堎合は以䞋のようになるので、これを倉数の展開や繰り返しを䜿っお曞き盎したしょう。 impl CommandBuilder { ...略... pub fn build() -> Result<Command, Box<dyn Error>> { if self.executable.is_none() { return Err(...略...) } ...略... if self.current_dir.is_none() { return Err(...略...) } Command { executable: self.executable.clone().unwrap(), args: self.args.clone().unwrap(), env: self.env.clone().unwrap(), current_dir: self.current_dir.clone().unwrap(), } } } 実装 ガヌド節をたず䜜りたす。このように quote! を䜿っお郚分的にトヌクン列を䜜っお埌で組み合わせるこずもできたす。 let checks = idents.iter().map(|ident| { let err = format!("Required field '{}' is missing", ident.to_string()); quote! { if self.#ident.is_none() { return Err(#err.into()) } } }); 前のステップず同じようにしお build 関数を䜜りたす。䞊で䜜ったガヌド節を #(#checks)* で展開しおいたす。 let expand = quote! { ...略... pub fn build(&mut self) -> Result<#ident, Box<dyn std::error::Error>> { #(#checks)* Ok(#ident { #(#idents: self.#idents.clone().unwrap()),* }) } ...略... } 最終的な実装は以䞋の通りです。 use proc_macro::TokenStream; use quote::{format_ident, quote}; use syn::{parse_macro_input, Data, DeriveInput, Fields, Ident, Type}; #[proc_macro_derive(Builder)] pub fn derive(input: TokenStream) -> TokenStream { let input = parse_macro_input!(input as DeriveInput); let ident = input.ident; let vis = input.vis; let builder_name = format_ident!("{}Builder", ident); let (idents, types): (Vec<Ident>, Vec<Type>) = match input.data { Data::Struct(data) => match data.fields { Fields::Named(fields) => fields .named .into_iter() .map(|field| { let ident = field.ident; let ty = field.ty; (ident.unwrap(), ty) }) .unzip(), _ => panic!("no unnamed fields are allowed"), }, _ => panic!("expects struct"), }; let checks = idents.iter().map(|ident| { let err = format!("Required field '{}' is missing", ident.to_string()); quote! { if self.#ident.is_none() { return Err(#err.into()) } } }); let expand = quote! { #vis struct #builder_name { #(#idents: Option<#types>),* } impl #builder_name { #(pub fn #idents(&mut self, #idents: #types) -> &mut Self { self.#idents = Some(#idents); self })* pub fn build(&mut self) -> Result<#ident, Box<dyn std::error::Error>> { #(#checks)* Ok(#ident { #(#idents: self.#idents.clone().unwrap()),* }) } } impl #ident { pub fn builder() -> #builder_name { #builder_name { #(#idents: None),* } } } }; proc_macro::TokenStream::from(expand) } 05-method-chaining 目暙 CommandBuilder 構造䜓でメ゜ッドチェヌンを䜿えるようにする 実装方針 03-call-setters ステップで実装した setter は &mut Self を返すようになっおいるので、実はすでにメ゜ッドチェヌンを䜿えるようになっおいたす。そのため、このステップでは远加の実装をする必芁はありたせん。 実装 このステップでは远加の実装は必芁ありたせん。このステップ甚のテストを実行しお倉曎なしでテストをパスするこずをチェックしおみたしょう。 たずめ 前線では Builder パタヌンを実珟するのに最䜎限必芁な機胜を持ったマクロを䜜りたした。 しかし、ただ Optional なフィヌルドの取り扱いに䞍十分な点があったり、ベクタ型のフィヌルドの取り扱いに改善の䜙地があったりしたす。埌線ではそのあたりの機胜を実装しおいきたす。 埌線ぞ続く。 参考文献 proc-macro-workshop/builder に取り組む Rust の procedural macro を操っお黒魔術垫になろう〜proc-macro-workshop の玹介 実践 Rust プログラミング入門 syn – Docs.rs quote – Docs.rs The post proc_macro_workshopでRustの手続き的マクロに入門する 前線 appeared first on CADDi Tech Blog .