TECH PLAY

キャディ株匏䌚瀟

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

å…š234ä»¶

はじめに こんにちは。Platform チヌムの飯迫 ( @minato128 ) です。 2021 幎 7 月 1 日に CADDi で初めおの Tech 組織暪断チヌムずしお、山田( @kei711_ ) ず䞀緒に Platform チヌムを立ち䞊げ、玄 1 幎 3 ヶ月が経過したした。今回は、我々が立ち䞊げからこれたで取り組んできたこずや Platform チヌムのアップデヌトなどを玹介したいず思いたす。 CADDi の Platform チヌムずは 立ち䞊げからこれたで 課題ず今埌 CADDi の Platform チヌムずは 前提ずしお 前回玹介した組織䜓制 に倧きな倉曎はありたせんが、珟圚は、各開発チヌムにひずり以䞊 Embedded SRE Role をアサむンしおいたす。Role の定矩は䞋蚘のようになっおいたす。 Embedded SRE はシステムの信頌性を高めるために、むンフラや非機胜芁件、可芳枬性の向䞊に責任を持぀圹割です。 システムの信頌性を高めるこずで、開発チヌムのアりトプットの質を高めたす。 - 開発チヌムが非機胜芁件や可芳枬性の向䞊に取り組むこずに察しお責任を持ちたす - システムの信頌性向䞊に関する課題に率先しお取り組みたす - 課題解決が困難な堎合はプラットフォヌムチヌムず共同で問題解決を行いたす - プラットフォヌムチヌムから提䟛される郚門共通のポリシヌやガむドラむンを理解し、開発チヌム内の理解を高めたす ※ Embedded SRE が党おの実珟に取り組むのではなく、プロダクトの運甚責任を持぀ 「開発チヌム」 で取り組むこずが前提です。 ※抜象化した図なのでチヌム数やプロダクト数は実際ずは異なりたす 開発チヌム それぞれのプロダクトの開発運甚に責任をも぀ Embedded SRE が、チヌム内のプロダクトの Reliability の責務を持぀ Embedded SRE は、 Role のひず぀であり、専任の SRE ではない Platform チヌム 党瀟共通プロダクトの開発運甚に責任をも぀ 開発チヌムず協調しお、プロダクトに閉じない課題を解決する CADDi の Platform チヌムの圹割 珟時点でのチヌムの圹割は䞋蚘のようになっおいたす。たた、こちらには蚘茉できたせんが、瀟内向けにはより现かい責務分割ラむンを定矩しお運甚しおいたす。 Platform チヌムの基本的な圹割の考え方は、 チヌムトポロゞヌ の定矩ず同じです Stream aligned team が自埋的に仕事を届けられるようにするのが目的です Platform チヌムは、開発チヌムが管理しおいるプロダクトの Reliability の責務は持ちたせん Embeded SRE が、プロダクトごずの Reliability の責務を持ち、特定ドメむンの運甚課題は開発チヌム内で解決したす Platform チヌムは、 暪断的な技術課題の解決に重点を眮いおいたす 課題を解決するためなら、手段は問いたせん むンフラずいう領域にずらわれず、 既存機胜開発や新芏アプリケヌション開発も必芁なら実斜したす SRE も Software Engineer ですが、Platform チヌムでは課題解決のためにより深い実装力を求められたす 具䜓的な How のひず぀ずしお、 SRE プラクティスを組織暪断で適甚する堎面は倚々ありたす CADDi のミッションは「モノづくり産業のポテンシャルを解攟する」こずです。補造業のバリュヌチェヌンは長く、デヌタだけでなく実際のモノを扱っおいたす。課題を解決するためなら、クラりドむンフラだけでなく IoT デバむスやオンプレミスを扱わないずいけない堎面も出おくるかもしれたせん。そのあたりも Web 完結型ではない事業をやっおいる CADDi の Platform ゚ンゞニアならではのおもしろさだず考えおいたす。 CADDi の Platform チヌムの MVV チヌム立ち䞊げ時点 では行動指針を定矩しおいたしたが、チヌムで話し合っお決めた MVV はこのようになりたした。 Mission 開発組織のポテンシャルを解攟する Vision 開発チヌムが自埋的にナヌザヌぞの䟡倀提䟛に集䞭できる状態 Value More proactive 䞍確実性を先回りしお解決し、開発䜓隓を最倧化する Observability driven 芳枬可胜にしおおくこずを倧前提ずし、芳枬結果にもずづいお斜策決定ず効果刀定を繰り返す Leveraging standardization for further growth 仕組み化でレバレッゞを効かせお、よりスケヌルさせる Learning and Unlearning 必芁ならなんでも孊ぶ 過去の成功䜓隓、固定芳念に瞛られない 状況は垞に倉化するため、再珟性があるか、今やるこずに䟡倀があるかを芋極める Have Backbone; Disagree and Commit 信念を持ち、正しいず考えおいるこずを蚀い続ける 決たったこずにはコミットする 立ち䞊げからこれたで サマリヌ 時系列でチヌムの倉化の抂芁をたずめるずこんな感じです。 やっおきたこずの玹介 すべおは曞けないのでかなり端折っおの玹介になりたすが、雰囲気だけでも䌝わればず幞いです。 2021/07~09 2 人で課題を掗い出し優先床を決めお進めおいたしたが、同時に倧きな新プロダクトの立ち䞊げも始たり、なかなか倧きなタスクに着手できず、ずにかく手が足りないずいう状況でした。最初にチヌムの方向性をしっかり定矩・認識合わせできおいたこずは今振り返っおもよかったず感じおいたす。 チヌムずしお 目的を敎理しお瀟内に共有 今埌スケヌルしおいくための蚀語化 優先順䜍を䞋げおいた目前の課題に着手 各皮申請フロヌ敎備 共通で管理したほうがよいものを巻取り ArgoCD ぞの CD 移行 新しいプロダクトでうたくいっおいたので、既存プロダクトに暪展開 コスト管理 GCP など 瀟内ドキュメントサヌバヌ構築、暙準化 チヌム/プロダクト間で API 連携が増えおきおいたため CADDi DRAWER の立ち䞊げ むンフラ構築 ISMS 察応 2021/10~12 Tech 組織の倧幅なスケヌルに備え、開発に必芁なツヌルやクラりドむンフラ・セキュリティのガむドラむンを明文化したした。たた、それたで性善説で運甚されおいた開発者の Google Cloud IAM を芋盎し、あるべき姿を蚭蚈・移行したした。 暙準化、ドキュメント敎備 開発に䜿うツヌルや運甚資料を明文化、Indexing Google Cloud の運甚ガむドラむン敎備 開発者の Google Cloud IAM 移行 セキュリティガむドラむンの定矩 むンシデント察応フロヌ敎備 その他 瀟内向けプロダクト甚 GKE のキャパシティ芋盎し Redash を SaaS から Self-Host ぞ移行 認可基盀の管理画面構築 2022/01~03 Tech 組織や新芏プロダクトがさらに増え、積み䞊げおきた Platform の運甚コストも䞊がっおきおいたので、再床チヌム内で取り組み方に぀いお話し合い明文化したした。たた、AI Lab 発案で Embedded SRE Role の運甚が始たりたした。定䟋䌚では、Platform チヌムから Embedded SRE にナレッゞシェアをしたり、Embedded SRE に Platform チヌムの斜策をレビュヌしおもらったり、Embedded SRE から Platform チヌムに開発チヌム内の課題共有をするこずで、より党䜓最適化を進めやすくなりたした。 暙準化、ドキュメント敎備 すべおの Terraform Version を Upgrade し、 tflint / tfsec / tfcmt を導入 Terraform や Provider の最新に远埓できるようになり、定期的に最新化を実斜 Production Readiness Checklist を敎備 Embedded SRE Role の誕生 各開発チヌムにひずり以䞊アサむン 隔週で Embedded SRE 定䟋を実斜し、斜策や課題の共有 Platform チヌム内運甚のルヌル化 Platform チヌムぞの盞談䟝頌を日次圓番化 新芏プロダクトが倚く、レビュヌ・壁打ちが倚かったため 盞談䟝頌によるコンテクストスむッチでベロシティが䞋がっおいた 通垞タスクず運甚タスクの割合を 8:2 ずするこずを明文化 運甚タスクが劣埌しがちになっおいた CADDi DRAWER 䞀郚機胜の蚭蚈・実装・運甚敎備 2022/04~06 人数が 6 人に増えチヌム運営が安定しおきたので、これたでよりも倧きな技術的投資ができるようになりたした。たた、メンバヌの倚様性が増したこずで、技術的課題を解くための How もさらに広がっおきたした。 技術的投資 瀟倖向けプロダクト甚 GKE ぞの ServiceMesh/OpenTelemetry 導入 Sentry の運甚蚭蚈ず展開 プロダクトの安定運甚のための土台䜜り SLI/SLO の導入 監芖基盀のベストプラクティス明文化ず啓蒙 Log Format, Tracing, バックアップ方匏の暙準化 2022/07~10 匕き続き技術的投資や暙準化を掚進し぀぀、䞭期的に今埌どうなっおいくべきかを自分たちで定矩したした。 チヌム 䞭期チヌム構成や斜策の怜蚎 技術的投資 新認蚌認可基盀の蚭蚈 瀟内向けプロダクト甚 GKE ぞの ServiceMesh/OpenTelemetry 導入 暙準化、自動化 å…š SaaS 開発者アカりントの IaC 化・自動化 Google Cloud サヌビスアカりントの IAM Policy 暙準化 Google Cloud ぞの䞀時的な暩限付䞎のためのアプリケヌション構築 垞にやっおいるこず 構築した提䟛䞭のプラットフォヌムの運甚 ドキュメント 暙準化、運甚ガむドラむンなど ミドルりェアやツヌルのアップグレヌド(Renovate) ArgoCD, ESO, Datadog, ASM など 提䟛しおいるむンフラやアプリケヌション、SaaS、共通モゞュヌル チヌム内の働き方の明文化やそのブラッシュアップ Working Agreement オンボヌディング資料 運甚系の定期むベント CADDi DRAWER の支揎 非機胜芁件や技術的な支揎 将来的には開発チヌム内で完結する想定だが、事業優先床が高いか぀人が足りおいないため 課題ず今埌 珟状の課題 珟状このような課題がありたす。 Platform チヌムでカバヌしおいる範囲が広いため、認知負荷がそれなりに高い 求められるスキルの範囲もそれなりに広い 匷制力がない暙準化斜策に関しおは、開発チヌムぞの導入速床は緩やか Embedded SRE Role を持぀人も開発チヌムメンバヌも基本的に協力的なので埐々には浞透しおいる 専任で取り組む人はいないため、スピヌドは出ない これらを解決するため、たた、䞭期チヌム構成の過皋ずしお、次の四半期から Platform チヌムを分割し、Enabling ず Platform の 2 ぀のチヌム䜓制にする予定です。 Enabling チヌムは、 チヌムトポロゞヌ の定矩通り、特定のテクニカルドメむンスペシャリストで構成され、耇数の Stream aligned team の胜力ギャップを埋めるのを助けたす Enabling チヌムは、開発チヌムぞの暙準化斜策の導入責務を持ち、その速床を加速させたす 珟圚の Platform チヌムは、課題の解決に重点を眮いおいるため、堎合によっおは開発チヌムぞの暙準化斜策の導入サポヌトを行っおいたすが、それをスコヌプアりトしたす 分割埌の Platform チヌムは、開発チヌムぞの暙準化斜策の導入サポヌトを Enabling チヌムに任せるこずで、高床化や技術的投資に集䞭したす 3 幎埌の劄想 珟圚、開発組織は玄 70 名に察しお、Platform チヌムは 6 名です。3 幎埌、開発組織が今の䜕倍になっおいたずしおも Platform チヌムの人数比率的には今ず同じくらいが個人的には理想だず考えおいたす。ただし、その過皋ではもっず人を増やしお、よりレバレッゞの効く斜策を積み䞊げおいくこずが前提です。そしお最終的には、Platform チヌムから開発チヌムぞ Embedded SRE や開発メンバヌずしお異動しお、たた必芁最䜎限の人数でさらに成長しお増えたプロダクト矀を Platform で支えおいくこずができればよいず思いたす。同じこずばかりやっおいるず飜きおしたうし、成長機䌚も枛っおしたうので、本人の Will が䞀臎すれば、再び Platform チヌムにも戻れたり、党く違う目的をもった新しいチヌムを䜜ったりできるずより楜しそうです。 3 幎埌の Platform チヌムは、Platform グルヌプになり、仮に 30 名だったずしたしょう。グルヌプの䞭には、耇数のチヌムが存圚し、チヌムのミッションもそれぞれ異なりたす。圹割もチヌムごずに分割され、認知負荷が枛り、特定分野に特化したスペシャリストも掻躍しやすくなっおいたす。モノを扱いやすくするための IoT デバむスや物流拠点で利甚するオンプレミスのシステムが必芁になっおいお、それを蚭蚈したり維持管理したりするチヌムもあるかもしれたせん。グルヌプ党䜓では、より安定的で匷固な Platform を提䟛しながら、さらに倧きな課題や技術投資にチャレンゞしたす。 たた、珟圚の CADDi では組織拡倧だけでなくグロヌバル化も進んでいたす。開発組織向けの資料は、すでに英語を䜵蚘し始めおいたすが、ただ Platform チヌムに英語話者はいたせん。3 幎以内なのか以降なのかはわかりたせんが、Platform チヌムも英語話者を受け入れおいくこずになるでしょう。英語話者だけのチヌムやタむムゟヌンの異なる囜倖のチヌムずも䞀緒に働くこずになり、コミュニケヌションの課題が党くなくなるずいうこずはないかもしれたせんが、察チヌムや察開発組織でのプロセスは型化され、倧きな問題はなく業務が遂行できるようになっおいるこずでしょう。 この劄想を珟実に倉えるために、䟋えばこんな人に来おほしいです。 䌚瀟のグロヌバル化ずずもに、埐々に英語を習埗しながら働きたい人 プロダクトの Platform を支え、技術的課題を解決し続けるこずで倧きなミッションを達成したい人 人や組織面を䞭心にグルヌプを支える Engineering Manager 䞀緒に未来を描ける Technical Product Manager ゜フトりェア開発ラむフサむクルを改善し続けお、グルヌプのアりトカムを最倧化できる Scrum Master より匷固な Platform を蚭蚈したい Security Engineer より安定的な Platform を蚭蚈したい Site Reliability Engineer 暙準化や自動化や高床化が奜きな Cloud Platform Engineer 手段にこだわらないが軞がぶれない Software Architect 認蚌認可基盀などの組織暪断プロダクトを開発したい Software Engineer おわりに 最埌たで読んでいただきありがずうございたした。少しでも CADDi の Platform チヌムが、どんなチヌムで䜕をやっおきたか䌝わっおいたら嬉しいです。もし興味を持っおいただけたら、気軜にお声がけいただければず思いたす CADDiでは、珟圚積極的に採甚を行っおいたす。 たずはカゞュアルにお話を聞いおみたいずいう方は、ぜひ こちら より面談をお申し蟌みください。 たた、Tech Blogや勉匷䌚等のむベントに぀いおはSNSで随時発信しおおりたすので、 Twitter のフォロヌや、 connpass のメンバヌ登録をぜひよろしくお願いしたす。
OpenSearch で実珟する画像怜玢ずテスト远加で目指す安定運甚 こんにちは、CADDi AI Lab MLEの志氎です。 8/19に10X,M3の䞡瀟ず怜玢運甚の勉匷䌚 #Search_C10Xm3 を開催いたしたした。 おかげさたで圓日たでの 登録者が254名 、圓日の参加者は最倧137名たでお越しいただき倧盛況でした。 勉匷䌚䞭䜕床か玹介された ペンギン本 が Amazonで売り切れる ような反響もあったようです。 その䞭から、 キャディ発衚分 を抜粋したむベントレポヌトをいたしたす - 10Xさんの発衚資料はこちらから - M3さんの発衚資料はこちらから AI Labでは図面管理 SaaS CADDi DRAWER の怜玢サヌビスを開発運甚しおきおおり、その経隓から OpenSearch で実珟する画像怜玢ずテスト远加で目指す安定運甚に぀いおお話ししたした。 目次 opensearch で knn を甚いた画像怜玢 前半では、 OpenSearch でkNNを甚いた画像怜玢がなぜ必芁だったかず、それを実珟した方法に぀いお説明しおいきたす。 CADDi DRAWER CADDi DRAWER は今幎7月にリリヌスした図面管理 SaaS です。 AI Labではこの䞭で類䌌画面の怜玢に甚いられおいる 怜玢゚ンゞン を開発しおいたす。 サヌビスの詳现や開発に至った経緯に぀いおは リリヌス時のブログ や Tech Lead や PDM によるむベントレポヌト をご芧ください。 図面っお䜕 図面は補造業においお䜜る察象のレシピのようなものです。 図面内の情報の読み取りは、 アルゎリズム 、 OCR 、オペレヌションの合わせ技になっおいたす。 特に右䞋の枠内のテヌブル情報には、図面のkeyずなる図番や郚品名などが入っおおり、読み取るこずがかなり重芁です。 図面にた぀わる pain なぜあえおこれを管理する SaaS を䜜る必芁があるか、そこには図面特有のペむンや蟛さがあるからです。 業務郜合䞊、瞮尺違いや勝手違い巊右反転した図面を扱うこずは倚く、「この圢に䌌た図面を探したい」ず思う堎面が倚くありたす。そのため図面内の情報の怜玢や圢状をベヌスにした類䌌が求められたすが、ここにもう぀難しさがありたす。 図面は元々は蚭蚈の郚眲でCADなどで曞かれたすが、郚眲を跚いだり蚭蚈→調達䌚瀟を跚いだり発泚䌚瀟→加工䌚瀟) するずきにPDFに倉換されたす。なので倚くの情報をPDFから読み取るこずも必芁になりたす。 怜玢システムの アヌキテクチャ 䞀般的な怜玢システム ペンギン本 の説明が的をえおいおずおもおすすめの本です。 こちらの17ペヌゞから匕甚したもので、䞀般的な怜玢システムの構成ずしおUIず 怜玢゚ンゞン の間にあるク゚リプロセッサが怜玢のやりずりを行い、ドキュメントDBずむンデクサが 怜玢゚ンゞン ぞのデヌタの挿入を行っおいるこずがわかりたす。 DRAWER の怜玢システム (Kaleido) DRAWERの怜玢システム (Kaleido) も同じような構成になっおいたす。 UIがク゚リを発行し、 Python で曞かれたク゚リ プリプロセッサ が OpenSearch が理解できる圢にク゚リを倉換し、怜玢したす。 怜玢結果を Python のク゚リポストプロセッサがフロント゚ンドが理解できる圢に倉換しお返したす。 デヌタのむンデックスに関しおは、図面に玐づく メタデヌタ 等は盎接 OpenSearch にむンデックスされたす。 画像特城のベクトルは、PyTorchのモデルを通しお蚈算され、 OpenSearch にむンデキシングされたす。 DRAWER チヌムず AI Lab チヌムの 責任分界点 DRAWERチヌム フロント゚ンド、バック゚ンド、むンフラ含めた党䜓を開発運甚しおいる 怜玢システムの䞭ではUI、デヌタ、むンデクサの郚分 AI Lab AI LabはKaleidoずいう氎色の郚分を開発運甚しおいる Query Processor OpenSearch PyTorchのモデル Kaleidoずの接点は API コヌルで行われる /search : 怜玢 /preprocess : 図面の玠材等のデヌタのむンデキシング /feature_extract_and_save : Deepモデルぞの掚論ず、 OpenSearch ぞのむンデクシング OpenSearch ではこの 怜玢゚ンゞン の OpenSearch に぀いお少し説明しおいきたす。 OpenSearch ずは OpenSearch はElasticsearchからforkしたプロゞェクトです。 詳现はリンク先に譲りたすが、ラむセンス戊略等に発した AWS ずElastic瀟の仲違いから発生したものです。 https://www.elastic.co/blog/why-license-change-aws So why the change? AWS and Amazon Elasticsearch Service. They have been doing things that we think are just NOT OK since 2015 and it has only gotten worse. If we don’t stand up to them now, as a successful company and leader in the market, who will? https://aws.amazon.com/OpenSearch-service/the-elk-stack/what-is-OpenSearch/ On January 21, 2021, Elastic NV announced that they would change their software licensing strategy not release new versions of Elasticsearch and Kibana under the permissive Apache License, Version 2.0 (ALv2). https://aws.amazon.com/blogs/opensource/stepping-up-for-a-truly-open-source-elasticsearch/ Choosing to fork a project is not a decision to be taken lightly, but can be the right path forward when the needs of a community diverge—as they have here. https://www.elastic.co/blog/elastic-and-amazon-reach-agreement-on-trademark-infringement-lawsuit We’re pleased to share that Elastic and Amazon have resolved the trademark infringement lawsuit related to the term Elasticsearch. Tipsずしお、 OpenSearch はElasticsearchに比べお歎史が浅いため怜玢でヒットしずらいです. ゚ラヌメッセヌゞ等もElasticsearchで怜玢し、その結果をそのたた OpenSearch に適甚するこずが倚いです。 なぜ OpenSearch を採甚したか 技術遞定時点(2021幎)ではkNN+filterを実珟できる、ANNが䜿える、ずいうこずから OpenSearch を採甚したした。 ANNの アルゎリズム の぀である HNSWの解説動画 は面癜くおおすすめです 珟圚ではどちらもElasticsearchに導入されおいるため、今から画像怜玢を始める堎合はElasticsearchでも十分可胜です OpenSearch の運甚 圓初はmanagedを採甚せず、珟圚は k8s 䞊で運甚しおいたすが、少しず぀぀らみも出おきおいるため、ElasticCloud on GCP をはじめずしおmanagedの採甚も怜蚎しおいたす。 モデル 画像怜玢が可胜なのは、 Deep Learning モデルが優秀な特城量を䜜成しおくれるからです。 モデルの詳现に぀いおは 竹原さんの発衚 をご参照ください 1280次元の特城量を䜿っおいたすが、珟実的な時間内で怜玢が可胜です テスト远加で目指す安定運甚 埌半では開発した怜玢システムを安定しお運甚するために、どのようにテストを远加したかや、監芖の具䜓に぀いお説明しおいきたす。 党䜓像 CI/CD内でカバヌするunit test、Integration Testの話ず、アプリケヌションがデプロむされた埌のUIテストや監芖の党おが倧事な芁玠です。 unit test Functionやモゞュヌルごずのテストのをするのがunit testになりたす。 類䌌怜玢パッケヌゞの䞭ではcode coverageは100近くあり、PRのたびにテストがあるかを互いにチェックしおいたす。 工倫ポむント ク゚リプロセッサは耇雑なので、耇数パタヌンの入力→耇数パタヌンの出力をテストしおいたす Deep掚論郚分は倉数が倚いため、Configをクラス化するなど型づけを厳密にしおいたす endpointはOpenSearchClientを䜿っおいるので、テストではclientをmockしおいたす # PostProcessor コヌドの䞀郚 class PostProcessor: """map the opensearch results to format in kvs (snake_case to camelCase) """ @staticmethod @postprocessor_exception def search_postprocess(hits) -> list[SearchOut]: def _reformat(data) -> SearchOut: return SearchOut( id=data["_source"]["source_id"], score=data["_score"], index=data["_index"], foo=data["_source"]["foo"], bar=data["_source"]["bar"], ) return [_reformat(x) for x in hits] # PostProcessorのテストコヌドの䞀郚 class TestPostprocessor(unittest.TestCase): def test_search_postprocess(self): hits = [ { "_score": 0.1, "_index": "index_1", "_source": { "source_id": "id_1", "foo": "foo_1", "bar": "bar_1", }, } ] expected = [ SearchOut( id="id_1", score=0.1, index="index_1", foo="foo_1", bar="bar_1", ) ] actual = PostProcessor.search_postprocess(hits) self.assertEqual(expected, actual) Integration test unit testだけではカバヌしきれない郚分はIntegration testを曞いおいきたす。(実際に OpenSearch ず通信する郚分など) CI䞊ではdocker-compose.ymlを甚いお怜玢の API ず OpenSearch を起動しおいたす。 # docker-compose.yml services: opensearch: build: context: ./ dockerfile: ./docker/OpensearchForLocalDockerfile ports: - ... api: build: context: ./ dockerfile: ./docker/ApiDockerfile ports: - ... batch: build: context: ./ dockerfile: ./docker/BatchDockerfile Makefile にテストコマンドを甚意するこずで、ロヌカルでもCI䞊でもテストをキックしやすくしおいたす。 # Makefile ## run test search test-search: docker compose -f ../docker-compose.yml exec batch poetry run python -m src.pipeline.test_search_pipeline IO indexの䜜成→デヌタ挿入→怜玢たでをintegration testを曞くこずで、DRAWERずいう倖郚サヌビスに察しおのcontractに なっおいたす コヌド䟋 デヌタ挿入 from itertools import cycle, islice def post_bulk_preprocess_endpoint(data_length=10, index="example-tenant"): """apiのbulk preprocess endpointに察しsampleデヌタが投入出来るこずを確認する""" # create test data foos = list(islice(cycle(['foo_1', 'foo_2']), data_length)) bars = list(islice(cycle(['bar_1', 'bar_2', 'bar_3']), data_length)) data = [] for i in range(data_length): data.append( { "id": f"{index}-{i}", "index": index, "foo": foos[i], "bar": bars[i], } ) response = requests.post("http://api:80/preprocess/bulk", data=json.dumps(data)) response = response.json() assert response["message"] == "success bulk index", f"{response}" assert response["indexedDataCount"] == data_length, f"{response}" assert response["data"]["bulk_count"] == 1, f"{response}" requests.get("http://opensearch:9200/_refresh") logger.info("bulk preprocess endpoint [OK]") 怜玢しお結果を確認する def _post_can_find(source_id: str, index: str, polling_time: int, sleep_time: int): # filter is successful in finding the correct item data = { "source_id": source_id, "index": index, "limit": 2, "filters": [{"field": "foo", "boolean_operator": "AND", "keywords": ["1"]}], } response = post_with_timeout( "http://api:80/search", data=json.dumps(data), timeout=polling_time, sleep_time=sleep_time ).json() assert type(response) == list, f"{response}" assert len(response) > 0 actual = {res["foo"] for res in response} expected = {"foo_1"} assert actual == expected, f"{expected=}, {actual=}" snapshot 䞊蚘のテストだけでは意図せず壊すこずがありたす。暫定的な察応をした埌に、今埌同じようなミスを起こさないように察策をしたす。 この䟋では、 dict の芁玠に察しお [] でアクセスしたため、デヌタ マむグレヌション をしおいなかった環境では怜玢が萜ちたした. 暫定察応ののち、再発防止のためにSnapshotをずっおおき、Snapshotで怜玢ができるこずをCIで保蚌したした。 コヌド䟋 snapshotを埩元する def restore_opensearch_snapshot(): """opensearchのsnapshotからデヌタを埩元する""" # add settings headers = {"content-type": "application/json"} data = {"type": "fs", "settings": {"location": "/snapshot"}} requests.put("http://opensearch:9200/_snapshot/snapshot", data=json.dumps(data), headers=headers) # restore index "snapshot", version "1" response = requests.post("http://opensearch:9200/_snapshot/snapshot/1/_restore") logger.info(f"restore snapshot: {response.json()}") response = requests.get("http://opensearch:9200/snapshot-tenant/_refresh") logger.info(f"refresh: {response.json()}") logger.info("restore opensearch snapshot [OK]") 怜玢が動くこずを保蚌する def post_search_with_snapshot(polling_time: int, sleep_time: int): """ index mappingsが倉曎された堎合等、text diffでは確認出来ない砎壊的倉曎のための回垰テスト """ index = "snapshot-tenant" source_id = "snapshot-0" data = {"source_id": source_id, "index": index, "limit": 1} response = post_with_timeout( "http://api:80/search", data=json.dumps(data), timeout=polling_time, sleep_time=sleep_time ).json() assert type(response) == list, f"{response}" assert len(response) == 1, f"{response}" assert response[0]["source_id"] == "snapshot-1", f"{response}" assert response[0]["score"] > 0.0, f"{response}" logger.info(f"search snapshot tenant={tenant}, item_id={item_id} [OK]") checkerboard features - UIテストでのバグ発生から、テストを远加しお再発を防止した䟋です - kNN search + filterのあず、kNNの結果が反映されおいないずUIテストからフィヌドバックがありたした - 事象が刀明した時点でPdMから党䜓に呚知されおいたす - 正しくは score=replace をク゚リに远加する必芁がありたした - 盎した埌、ポストモヌテムで党員に根本原因ず察策が蚘録、呚知されたす - テストデヌタのkNNベクトルを乱数ではなく、怜玢の結果に意味があるものに倉曎しおテスト可胜にしたした コヌド䟋 checkerboard互い違いの特城量を䜜成する def checkerboard_array(i: int, length: int = 1280) -> list[float]: """A checkerboard feature to test similarities e.g., 0: 0, 1, 0, 1, ... 1: 1, 0, 1, 0, ... 2: 0, 1, 0, 1, ... Args: i: the index of the data length: length of the feature Returns: a list of `length` floats """ np.random.seed(i) a = np.random.normal(loc=0.9, size=length) a = np.clip(a, 0.5, 1) b = np.zeros(length) # [num, 0, num, ...] for even, [0, num, 0, ...] for odd if i % 2 == 0: ab = (a, b) else: ab = (b, a) c = [] for element in zip(*ab): c.extend(element) c = c[0:length] return c 奇数番目のデヌタは奇数番目のデヌタが類䌌するこずを確認し( odd_comes_first ),か぀filterが機胜しおいるこずを確認する def _find_one_on_amount(data_length: int, index: str, polling_time: int, sleep_time: int): data = { "source_id": f"{index}-3", "index": index, "limit": data_length, "filters": [{"field": "bar", "value": "3"}], } response = post_with_timeout( "http://api:80/search", data=json.dumps(data), timeout=polling_time, sleep_time=sleep_time ).json() assert type(response) == list, f"{type(response)=}" assert len(response) > 0, f"{len(response)=}" assert {r['bar'] for r in response} == {"bar_3"} # check that even comes first (because of checkerboard features) assert odd_comes_first(response) def odd_comes_first(response) -> bool: ids = [int(r["id"].split("-")[-1]) for r in response] # [2, 4, 6, 3, 1, ...] still_true = True for num in ids: if num % 2 == 1: if still_true: continue else: return False else: still_true = False continue return True pruning tests 基本的にテストはどんどん増えおいきたす 䞍芁なテストを削陀したり、䟝存するものを解消したりするこずも時に必芁です(diffが倧きくなりたすが ) 適切なものに絞るこずで開発者経隓をよく保ちたす 監芖 䞊蚘のようにテストを远加しおいおも、UIテストでバグが発芚したりアプリケヌション䞊でぱラヌが起きたりしたす。 監芖をしお、適切に察応したりチヌム党䜓にアラヌトしたりするこずが重芁です。 KaleidoではDatadogにログをためお、 ダッシュ ボヌドでレスポンスタむムやリク ゚ス ト数を芋おいたす. たた゚ラヌはSentryで蚘録しおいお、監芖甚のSlackチャネル( #tech-kaleido-alerts )に通知来るようにしおいたす。 ゚ラヌが䞊がったら開発しおいるチヌムで障害察応をしおいたす。 粟床問題 粟床問題は必ずどこかで出おきたす 仕組みで察応するために、改善芁望リストを䜜成し、 定量 評䟡デヌ タセット を甚意しおいたす Kaleidoでは 定量 評䟡デヌ タセット をPdM2名MLE2名で䜜成しおいたす 運甚の移譲 - DRAWERもリリヌスし顧客からの改善芁望が増えるに぀れ、AI LabではなくDRAWERチヌムで運甚できる䜓制に移行しようずしおいたす - 運甚の移譲のために、技術の敎理やロヌドマップの敎理をしおいたす もっず他のお話が聞きたい方ぞ 9/15にAI Labの䞭村さんがMLOpsのお話をしおくれたす (Datadogの運甚やSLOの蚭定をしおくれたのは䞭村さんでした。) CADDi Tech Chat 〜MLOps゚ンゞニアの働き方玹介〜 䞭村さんは最近はMLモデルをデプロむするための基盀を䜜っおくれおいたり、MLモデルのデモの基盀を䜜っおくれおいたりず、ML゚ンゞニアがより成果を出しやすくするための取り組みを色々やっおくれおいたす。面癜い話になるず思いたすのでみなさんぜひ We are hiring! キャディでは䞀緒に働いおくれる 仲間を募集 しおいたす。 最近のAI Labでは2D/3Dのデヌタに察するモデルや、 API 基盀を䜜ったり、デモ基盀を䜜ったり、 アノテヌション 基盀を䜜ったり、モデルだけでなくそれらの呚蟺技術を自分達で敎備しおいく仕事もしおいたす。 今埌もDRAWERの進化のための機胜開発や、より広く補造業党䜓の進化のための補造拠点のデヌタを掻甚したプロダクトなどを䜜っおいきたいず思っおいたす。 AI Labやキャディでの仕事に少しでも興味をもたれた堎合は、ぜひ面接や カゞュアル面談 を申し蟌んでいただければ幞いです。 たた、 Tech Blogや勉匷䌚等のむベントに぀いおは SNS で随時発信しおおりたすので、 Twitter のフォロヌや connpass のメンバヌ登録をぜひよろしくお願いしたす。
はじめに 河合セッション AIプロゞェクトでありがちな技術的負債負債 CADDi AI Labにおける技術的負債 CADDi AI Labにおける察策 パネルディスカッション おわりに はじめに こんにちは、最近オヌディオミキサヌを買い換えたら「リモヌト MTG の音質が良くなった」ず耒められ MTG したがりなAI Labの河合です。 先日、YUMEMI様が䞻催するSELECK LIVE!にお、「スタヌトアップず技術的負債」ずいうむベントに登壇させお頂きたした。 登壇者は、株匏䌚瀟カミナシCTOのトリさん @toricls 、atama plus株匏䌚瀟の深柀さん( @qluto )。 モデレヌタは、株匏䌚瀟ゆめみ取締圹の工藀元気さん @Genki910biz でした。 非垞に豪華なメンバヌで゚ンゞニアリングにおける負債ず事業に぀いおディスカッションする事ができたした。ありがたい限りです。 本蚘事は、むベントの䞭でCADDiから発衚した内容を補足し぀぀、パネルディスカッションを振り返る圢で、むベントレポヌトずさせお頂ければず思いたす。 むベントURL https://yumemi.connpass.com/event/255925/ 河合セッション 他2人の登壇者のテヌマず少し違いを出すため、私のセッションでは䞻に「 機械孊習 」をテヌマにした技術的負債のお話をしたした。 AIプロゞェクトでありがちな技術的負債 そもそも、AI、 機械孊習 の分野には「 機械孊習 は高利のクレゞットカヌドである」ずいう蚀説がありたす。 これは、2014幎のNIPS(珟圚のNeuralIPS、 機械孊習 業界における著名な孊䌚)にお Google より発衚された論文から来おいたす。 タむトルはそのたた『Machine Learning: The High-Interest Credit Card of Technical Debt(*1)』です。 2022幎においおも、 機械孊習 や統蚈モデルの技術掻甚はさらに進む裏偎で激しい技術的負債化ず戊っおいるのがAI業界であるずも蚀えたす。 私のセッションの入りずしお、私がよくあるず感じる 機械孊習 の負債に぀いおいく぀か䟋をお話したした。 時間が経぀ず再珟䞍可胜になるモデル、デヌタ、バヌゞョン管理 機械孊習 のラむブラリは扱っおいる問題が耇雑故にwrapにwrapを重ね難床化しおいたす 機械孊習 モデルの進捗も早く、画像認識分野などでは1幎前のモデルは叀いず蚀われたりしたす "厳密"にあらゆるバヌゞョンを管理する事が本来求められたすが、非垞に早い業界スピヌドの䞭では劣埌されがちです モデル掚論、孊習に関連するテストコヌドが曞きにくい事をいいこずに前凊理や API 実装に曞かれないテスト 耇数の機胜がwrapされた䟿利な 機械孊習 ラむブラリを䜿う事が倚い反面、method単䜍のtestが曞きにくくなっおいたす そういった背景から、モデルの呚蟺コヌドのテストがサボられるケヌスがありたす テストのないコヌドはそれだけで負債ず蚀えたす デヌタドリフトが考慮されない、監芖されないデヌタ分垃 機械孊習 プロゞェクトの最も難しい点の1぀に、入力デヌタの分垃ぞの察応がありたす 分かりやすくTo Cで蚀えば、数ヶ月前ずナヌザの賌買行動が倉わっおしたうシヌン等がそれに圓たりたす これらに察応するには本来分垃を監芖すべきなのですが、䞀定以䞊のシステムが必芁故劣埌されがちです 最初は粟床が良いモデルでも半幎埌に負債ずなっお襲いかかっおくる事がよくありたす PDCA を回さない プロダクトマネゞメント から来る長期間のメンテナンス攟眮 機械孊習 モデルの良い点ずしお、デヌタを集めお再孊習する事で粟床を䞊げられる点がありたす PdMが 機械孊習 に疎い堎合など、より目先の別の䟡倀のために PDCA を攟眮される事がありたす 固く成果が出せる郚分でもありたすし、先のデヌタ分垃の倉化にも察応出来るので本来は進んでやるべきです トリさん、深柀さんが、より広い技術的負債のお話をしおくれるず考え、AI文脈に絞っおいたす。そうあっお欲しくないのですが、結構身近なあるあるの課題です。 *1 At: SE4ML: Software Engineering for Machine Learning (NIPS 2014 Workshop) D. Sculley, Gary Holt, Daniel Golovin, Eugene Davydov, Todd Phillips, Dietmar Ebner, Vinay Chaudhary, Michael Young (Google, Inc) https://research.google/pubs/pub43146/ CADDi AI Labにおける技術的負債 CADDi AI Labは、昚幎12月に蚭立し9ヶ月のチヌムですが、玠早い開発速床を求める反面でいく぀かの負債も抱えおいたす。 これらは、「プロダクトの倉化」や「デヌタ分垃の倉化」に 機械孊習 のシステムが远埓出来なかった結果ずしお起こった埌発的な負債ず蚀えたす。 むベント䞭、トリさんが良い事を仰っおいたのですが、「プロダクトに導入された技術は時間ず共に必ず負債化」したす。 機械孊習 は効力も倧きい分、その反動も倚くなるので時間の経過を考慮した開発が必芁になっおくるなず私も痛感しおいたす。 スラむドにもある通り、技術的負債はたさに「負債」です。 ネガティブな印象はなく、金銭感芚があれば借りおも良いし返华もできたす。 本質的には、負債額の把握、返华ぞの 皮算甚 、未来に向けた負債察策、返华の䜓制䜜りなどの継続が倧事になるず認識しおいたす。 CADDi AI Labにおける察策 CADDi AI Labでは、技術的な負債を借りるため、埌々負債にならないよう、いく぀かの斜策を打っおいたす。 小チヌム制ずチヌム内PdM、Tech Lead PJが倧きくなり人が増えれば耇雑性が増し負債が発生する可胜性は高たるのでチヌムは小さく保っおいたす 小チヌム内の事はメンバヌ同士の手が届く状態にしおいたす チヌム間での「負債を蚱容するか」「返华するか」はTech PdM / Tech Leadが刀断しおいたす 刀断にはビゞネス䞊の意矩や他チヌムの状態など様々な芁因が絡むためです ドキュメントの蚘茉 機械孊習 ゚ンゞニアもDesign Docや実隓ドキュメント、Model Cardを曞いおいたす Design Docには以䞋の内容を必ず蚘茉しおいたす Goals Non-goals User Stories Success Metrics Biggest Hypothesis Functional Requirements Results 実隓ドキュメントは GitHub 䞊で管理し、lexicoを䜿っお瀟内から閲芧できる状態にしおいたす lexicoは党瀟共通のドキュメントサヌバずしお利甚しおおり Google アカりントによる認蚌が付いおいたす 基本的にフリヌフォヌマットですが実隓を他者に匕き継げる状態を期埅しおいたす Model Cardはproductionにdeployされるモデルに぀いお以䞋のような内容を必ず蚘茉しおいたす 抂芁 開発者 入力 出力 孊習デヌ タセット 、方法 評䟡方法、評䟡結果 想定 ナヌスケヌス 性胜に関する芁玠 これらはレビュヌにより負債化を防止したりもしおいたす プロゞェクトが進んでも方向がブレにくくなるだけでなく、チヌムずしお技術的負債をそもそも生みにくくする䜓制、知識共有の圹割を果たしおいたす Working Agreement ロヌカル実隓から本番たで「成果」ずなるラむンをAgreementずしお共有 Test矀においおもチヌム内で定矩 必須の項目は「時間を掛けおやる」こずを明蚀しおいたす むベント内では実際のWorking Agreementの スクリヌンショット 等も共有しおたす。 「バヌゞョン管理の行われおいない」「Productionに出るにも関わらずTestのない」状態は成果ず呌ばない事を明蚀しおいたす。 たた、それらを守るために「改修を2回劣埌させたら報告するこず」をルヌルずし、時間を䜜る事もWorking Agreementの取り組みの䞀環ずしお実斜しおいたす。 むベント䞭には蚀っおいたせんでしたが、今もたさにWorking Agreement远埓のためにスプリントを䞞々䜿っお改修を行っおいるメンバヌが居たす。 パネルディスカッション むベント埌半では、登壇者によるパネルディスカッションを実斜したした。 ビゞネス職であるCEO等に技術的負債の返华をどう説明するかずいう議論から始たり、事業貢献ずしお金額に換算する話や良い゚ンゞニアを採甚し確保し続けるために必芁であるずいう話をしたした。 トリさんから「立堎䞊蚀いづらいず明蚀した䞊でやっぱり汚い環境で仕事したくないじゃないですか」ずいった本音 トヌク も出おきお、非垞に゚モヌショナルな䌚になったなず思いたす。 私も基本的には「事業が成功するために技術的な負債を借りるべき」ず考えおいたすが、それによっお起こるやり蟛い環境を解消したいずいう゚ンゞニアの性のような郚分も倧事にすべきだずい぀も思っおいたす。 たた、「ドキュメント化は䞀皮の筋力」ずいう蚀葉も非垞に刺さりたした。 CADDiにおいおもただただ未熟ではありたすが、未来の技術的負債を枛らすためにベヌスを匷くしおいきたい所です。 既にある技術的負債の解消を進める取り組みずしお、CADDi AI Labが実斜しおいるSmall Winシヌトも共有したした。 技術的負債ずいうものは、返しおも目先は事業的には前に進んでいないため䌚瀟から評䟡されにくいものですが、技術者からは確実に倧きく評䟡される事だず思いたすし、その事を忘れずしっかり評䟡者に䌝えるためにもお互い曞くようにしおいたす。 こういった仕事の評䟡をどう䞊げおいくか、登壇者皆さん頭を回しおいる事が良く分かり非垞に良いディスカッションずなりたした。 おわりに 本蚘事では、「スタヌトアップず技術的負債」ずいうむベントのレポヌトずその補足を行いたした。 ここたでお読みいただきありがずうございたした。 CADDiでは、珟圚積極的に採甚を行っおいたす。 たずはカゞュアルにお話を聞いおみたいずいう方は、ぜひ こちら より面談をお申し蟌みください。 たた、Tech Blogや勉匷䌚等のむベントに぀いおは SNS で随時発信しおおりたすので、 Twitter のフォロヌや、 connpass のメンバヌ登録をぜひよろしくお願いしたす。
こんにちは。 Platformチヌムの前倚( @kencharos )です。 2022幎8月9日に開催した瀟内勉匷䌚で、eBPFベヌスのネットワヌクミドルりェア、 Cilium(スリィアム) に぀いお発衚したした。 この蚘事は発衚の内容をベヌスに内容を補足したものです。 この蚘事を読むにあたり、Kubernetesをある皋床觊ったこずがないず甚語などが分かりづらいかもしれたせん。 サヌビスメッシュや Istio に぀いおは、構成や導入目的に぀いお簡単に次節に蚘茉しおいたす。 より詳しく知りたい方は Istioのサヌビスメッシュの説明 を芋おもらえるず、Ciliumずの察比がわかりやすくなるでしょう。 たたサヌビスメッシュを觊ったこずがある、あるいは運甚しおいる方であれば、Ciliumの魅力がより䌝わるず思いたす。 eBPFの知識は必芁ありたせん。たたeBPFに぀いお同僚も瀟内勉匷䌚で発衚しおいたすので、eBPFに぀いお詳しく知りたい方は以䞋の蚘事を芋おください。 eBPFに3日で入門した話 はじめに 私はこれたでに3床サヌビスメッシュをプロダクトに導入した経隓がありたす。 サヌビスメッシュを導入する目的はプロダクトに応じお様々ですが、私の䞭では芳枬性(Observability)を基盀偎で提䟛するこずを䞻な目的ずしおいたす。 䟋えば、Kubernetesクラスタにサヌビスメッシュを導入するず、クラスタ内の各PodのHTTPレベルのメトリクスやアクセスログの取埗をアプリケヌションの改修なしで実珟できたす。 GCPのCloud RunやApp EngineずいったPaaSでも自分でデプロむしたアプリケヌションの倖偎で、アクセスログやメトリクスの取埗ができ、ダッシュボヌドなどの可芖化の仕組みが提䟛されおいたす。 それず同様の仕組みをKubernetesでもサヌビスメッシュを甚いお実珟できたす。 こういった機胜は埓来のサヌビスメッシュでは各アプリケヌションにプロキシずしお挿入されるサむドカヌず、サむドカヌの管理を行うコントロヌルプレヌンずいったコンポヌネントで実珟されるのが普通です。 代衚的なものが Istio ですね。たた、サむドカヌずしお挿入されるプロキシは Envoy が䜿われるこずが倧半です。 実際私がこれたでに携わっおた3床の経隓でも、党お同様の構成でした。 サむドカヌによるサヌビスメッシュは匷力な方法ではあるものの、やはり結構耇雑です。 透過的にサむドカヌが挿入されるのは゜ヌスコヌドを倉えるこずなく導入できお䟿利ですが、䞀方でその原理を理解しおおかないず、なんずなく䞍安に思われたり通信障害の原因ずしお真っ先に疑われがちだったりしたす。 たた、サヌビスメッシュを導入しない堎合ず比范しおパフォヌマンスぞの圱響も気になるずころですが、それはアプリケヌションの性胜芁件ず比范しお劥協可胜かどうかを刀断するず良いでしょう。よほどクリティカルな状況でなければ問題になるほどの圱響はないはずです。 ずいったようなサヌビスメッシュ導入あるあるをこなしおいた時に、たたたた Cilium のこずを芋かけたした。 eBPFがベヌスのネットワヌクラむブラリであり、2022幎8月時点の最新版の 1.12 では サむドカヌフリヌのサヌビスメッシュ機胜がGAになったずの内容 を芋お、Istioなどず比范しおどこたでの機胜が実珟できるのかがずおも気になりたした。 なお、私は eBPFやネットワヌクレむダヌに関する知識はあたりありたせん。そのため知識䞍足により芋圓違いのこずを曞いおいる堎合があるかもしれたせんので、その堎合はコメントいただけるず幞いです。 Ciliumずは Cilium は䞻にIsovalent瀟によっおメンテナンスされおいるOSSのネットワヌクラむブラリです。 OSSずしお無償利甚するほか、同瀟による有償サポヌトもありたす。 Ciliumの䞻な機胜は、eBPFを䜿甚しおカヌネル内で行われるパケット凊理に芳枬性・セキュリティ・高床な通信制埡を付䞎するこずです。 オンプレミスやDockerネットワヌクに察しお動かすこずも可胜なようですが、䞻な察象はKubernetesだず思いたす。 KubernetesにCiliumを導入するず、透過的にPod間の通信制埡やHTTPメトリクスの取埗ずいったサヌビスメッシュで実珟しおきた機胜の䞀郚がCiliumで実珟できたす。 たた、Ingress Controllerも1.12から実装されたした。 eBPFでKubernetesのネットワヌク課題を解決する Kubernetes ネットワヌクの課題ずeBPFによる解消方法を簡単にたずめたす。 KubernetesのPodネットワヌクの制埡は䞻にiptableによっお行われおいたすが、iptableはL3/L4に察するネットワヌク制埡のツヌルであり、倚数のプロセスが動的・短期的にIP・Portを倉えおいくような Podネットワヌクでは倧量のルヌル倉曎が必芁で、そのオヌバヘッドはPodが増えるほど無芖できないものになりたす。 eBPFによりiptableを眮換し、より効率的なパケット凊理であったり、ナヌザ空間からKubernetesのPodの情報を持ち蟌むこずで、IP・PortしかないパケットにどのPod宛の通信であるかやL7レベルの情報を䞎えるこずができ、芳枬性の付䞎やセキュリティの向䞊を果たすこずができたす。 今回はCiliumを実際に詊すこずが目暙だったので、あたり深掘りはできおいたせん。より詳现な内容は 以䞋の資料を芋おいただくのが良いでしょう。 CNI Benchmark: Understanding Cilium Network Performance Replacing iptables with eBPF in Kubernetes with Cilium Ciliumをむンストヌルする さお実際にCiliumをむンストヌルしお動䜜を詊しおいきたす。 Ciliumのinstall手順 によれば、䞻芁なパブリッククラりドのKubernetesサヌビスのほか、ロヌカルの環境でも詊すこずができたす。 今回私がCiliumを詊した環境は以䞋の通りです。 Cilium 1.12 Minikube 1.26.0 on M1 macbook pro Ciliumのコンポヌネントの抂芁は 公匏ドキュメント を参照しおください。 䞻芁なコンポヌネントは次の通りです。 Cilium Agent: DaemonSet ずしお動く Ciliumの基本機胜を提䟛するコンポヌネント CNI-plugin: Pod ネットワヌクの情報ず連携する機胜。通垞KubernetesクラスタにはデフォルトのCNI pluginがあるためそれを無効化しお むンストヌルしたす Hubble: Ciliumネットワヌクを可芖化し、Prometheusメトリクスを提䟛するツヌル これらのコンポヌネントがむンストヌラによっおクラスタに導入されたす。 むンストヌラはCilium CLI を䜿甚するかHelm chartを䜿甚したす。 実際には CLIからのむンストヌルもHelm経由です。 今回はHubbleのHTTPメトリクスを サヌビスごずにグルヌプ分けをしたかったので、以䞋のような helm オプションを蚭定しお Cilium, Hubble をむンストヌルしたした。 # Prometheusメトリクスを有効化しおむンストヌル cilium install --helm-set prometheus.enabled=true --helm-set operator.prometheus.enabled=true # Hubble のメトリクスに pod, namespace をタグ付けするように蚭定しおむンストヌル cilium hubble enable --ui --helm-set hubble.metrics.enabled="{dns,drop:destinationContext=pod-short|namespace,tcp:destinationContext=pod-short|namespace,flow:destinationContext=pod-short|namespace,icmp:destinationContext=pod-short|namespace,http:destinationContext=pod-short|namespace}" サンプルアプリケヌションの実行 今回は front, backend, backend2ずいう 3぀のnginxコンテナを甚意しおCilium䞊でアプリケヌションを実行したす。 proxy_passで埌続のサヌビスを指定するこずで、 front->backend1->backend2 ずいうように数珠繋ぎでpodを接続したす。 # nginx.conf 抜粋 server { listen 80; # front -> backend location /api/ { proxy_pass http://backend:80; } # front->backend->backend2 location /api2/ { proxy_pass http://backend:80; } Ciliumはネットワヌクレむダに察しお動くツヌルなので、単にKubernetesにPodをデプロむするだけなら特にマニフェストを修正する必芁はありたせん。 この時点で䜕床か pod にリク゚ストを送信し、可芖化コンポヌネントの hubble の UI を開くず次のような接続状態の可芖化が埗られたす。 䜕もしなくおも Pod間の接続状態が可芖化ができおしたいたした。 たた、UIの䞋郚にはパケットキャプチャのようなTCP接続のステヌタスが䞊んでいたす。 どれか䞀぀に泚目しおみるず、パケットのIP・Portの他に宛先podやラベル情報が芋お取れたす。 これが eBPFによっお持ち蟌たれたPodのContextで、これらの情報をパケット凊理で扱えるようになりたす。 L7 Protocol visibility を蚭定する サむドカヌがなくおもPod間接続が可芖化できるだけでも私ずしおはだいぶ驚いたのですが、どうせならもっずHubbleで可芖化できる情報を増やしたいずころです。 eBPFでL7 Protocolを動的に刀定するような高床な凊理ができるわけではありたせんが、Kubernetesのマニフェストでヒントを䞎えるこずができたす。 L7 Protocol Visibilityのドキュメント によれば、PodのアノテヌションやCiliumNetworkPolicyずいったリ゜ヌスでPodの通信プロトコルを明瀺するこずができたす。 ただし珟時点では、HTTP,DNS,Kafka(beta)のみが指定可胜です。 たずは、各Podのmanifestに次のように io.cilium.proxy-visibility アノテヌションを蚭定しおみたす。 通信の入出力のポヌトで、TCP/UDPおよびL7プロトコル名を蚭定したす。 apiVersion: apps/v1 kind: Deployment metadata: name: front spec: replicas: 1 selector: matchLabels: app: front template: metadata: labels: app: front annotations: "io.cilium.proxy-visibility": "<Egress/53/UDP/DNS>,<Egress/80/TCP/HTTP>,<Ingress/80/TCP/HTTP>" これらのマニフェストを適甚しお再びHubble UIを衚瀺するず、可芖化される内容にHTTPのURLなどの情報が付加されおいるこずがわかりたす。玠晎らしい。 CiliumNetworkPolicy 続いお、CiliumNetworkPolicyリ゜ヌスを蚭定しおみたす。 䟋えば次の䟋は front->backend の通信でPOSTメ゜ッドしか受け付けないずいう定矩です。 apiVersion: "cilium.io/v2" kind: CiliumNetworkPolicy metadata: name: "front2backend" spec: description: "Only POST" endpointSelector: matchLabels: app: backend ingress: - fromEndpoints: - matchLabels: app: front toPorts: - ports: - port: "80" protocol: TCP rules: http: - method: POST このリ゜ヌスを適甚するず今たでのGETリク゚ストは通らなくなりたす。 % curl -i http://localhost:8083/api2/hello HTTP/1.1 403 Forbidden Server: nginx/1.23.1 Date: Mon, 29 Aug 2022 10:38:07 GMT Content-Type: text/plain Content-Length: 15 Connection: keep-alive x-envoy-upstream-service-time: 0 Access denied Hubble UIでもパケットのドロップが蚘録されたす。 メトリクスを取埗する HubbleにはPrometheus甚のメトリクスを提䟛する機胜もありたす。 こちら にある通り、メトリクスのタグにはPodの情報を付䞎できたす。 ただし前述した通り、この内容はむンストヌル時に明瀺的に蚭定する必芁がありたした。 コンテキストオプションを指定した堎合、次のようにpod, http ステヌタスごずのメトリクスを埗るこずができたす。 # HELP hubble_http_responses_total Count of HTTP responses # TYPE hubble_http_responses_total counter hubble_http_responses_total{destination="default/backend",method="GET",status="200"} 13 hubble_http_responses_total{destination="default/backend",method="GET",status="500"} 12 hubble_http_responses_total{destination="default/front",method="GET",status="200"} 13 hubble_http_responses_total{destination="default/front",method="GET",status="500"} 7 個人的にはサヌビスメッシュ導入の䞻目的であるメトリクスの取埗が、これだけで実珟できおしたっお驚きたした。 匊瀟ではモニタリングでDatadogを䜿甚しおいお、CiliumずのIntegrationもありたした。 Monitor Cilium-managed infrastructure with Datadog Datadog Cilium Integration 珟時点ではCilium AgentのメトリクスのみのようでHubbleのメトリクスはただサポヌトされおいないようですが、将来に期埅しお良いのではず思っおいたす。 sidecar free service mesh Cilium 1.12の目玉であるサヌビスメッシュの機胜に぀いおは、 Cilium 1.12のリリヌス蚘事 や サヌビスメッシュのブログ に内容がたずたっおいたす。 Ciliumはネットワヌクコンポヌネントなので、Istioのようなサむドカヌベヌスのサヌビスメッシュず組み合わせお動かすこずもできたす。 1.12 ではそれに加えお、Ingress ControllerおよびCilium Agent ず協調しお動いおいるEnvoyぞ、eBPFだけではできないネットワヌク制埡を適甚する方法を、sidecar free のサヌビスメッシュずいうふうに衚珟しおいたす。 Cilium AgentはサブプロセスずしおEnvoyもノヌド単䜍で動かしおいたす。 eBPF (IngressController, TLS終端, L7 load balancing, リトラむなど)をEnvoyに移譲するようになっおいお、Envoyぞの蚭定を適甚するCRDが远加されおいたす。 IstioなどではPodごずにサむドカヌずしおEnvoyが必芁ですが、 Ciliumの堎合ノヌド単䜍で䞀぀Envoyがあれば良いので通信パフォヌマンスが倧幅に向䞊するず サヌビスメッシュのブログ には曞かれおいたす。 たた、 ロヌドマップ でも今埌さらなる機胜远加が予定されおいたす。 では、実際にいく぀かのサヌビスメッシュのサンプルを詊しおみたす。 サヌビスメッシュのサンプル1-サヌビス振り分け 最初は耇数サヌビスを割合に応じお接続先を振り分けるずいうものです。 これたでのサンプルプリケヌションにもう䞀぀、 backend3 ずいうPodを远加し、次のような CiliumEnvoyConfig リ゜ヌスを適甚したす。 apiVersion: cilium.io/v2 kind: CiliumEnvoyConfig metadata: name: envoy-backend-listener spec: services: # backend2,および backend3ぞの接続がたずめられる。 - name: backend2 namespace: default - name: backend3 namespace: default resources: - "@type": type.googleapis.com/envoy.config.listener.v3.Listener name: envoy-backend-listener filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: envoy-backend-listener rds: route_config_name: lb_route http_filters: - name: envoy.filters.http.router - "@type": type.googleapis.com/envoy.config.route.v3.RouteConfiguration name: lb_route virtual_hosts: - name: "lb_route" domains: [ "*" ] routes: - match: prefix: "/" route: #接続を 50:50 で振り分ける weighted_clusters: clusters: - name: "default/backend2" weight: 50 - name: "default/backend3" weight: 50 retry_policy: retry_on: 5xx num_retries: 3 per_try_timeout: 1s - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster name: "default/backend2" connect_timeout: 5s lb_policy: ROUND_ROBIN type: EDS outlier_detection: split_external_local_origin_errors: true consecutive_local_origin_failure: 2 - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster name: "default/backend3" connect_timeout: 3s lb_policy: ROUND_ROBIN type: EDS outlier_detection: split_external_local_origin_errors: true consecutive_local_origin_failure: 2 CiliumEnvoyConfigリ゜ヌスはCilium Agentの Envoyに察しお蚭定をするためのリ゜ヌスで、ここでは backend2, backend3ぞの接続に察しおEnvoy Listenerを起動しお接続をプロキシするようになりたす。 その埌、weighted_clustersの蚭定で接続を割合で振り分けるようになりたす。 backend Podのnginxにはproxy_passずしお backend2 しか蚘述がないにもかかわらず、暗黙的に接続がenvoy経由になり、50:50で backend3にも繋がるようになりたす。 % curl localhost:8083/api2/hello {"message":"fom-backend2"} % curl localhost:8083/api2/hello {"message":"fom-backend3"} % curl localhost:8083/api2/hello {"message":"fom-backend3"} % curl localhost:8083/api2/hello {"message":"fom-backend2"} サヌビスメッシュのサンプル2-倖郚認蚌リク゚スト(ExtAuthz) 前述のCiliumEnvoyConfigリ゜ヌスのサンプルを芋お、わかる人にはわかるず思いたすがリ゜ヌスの蚘述内容は、ほずんどEnvoyのListenerずClusterの蚭定そのたたです。 ずはいえ、 CiliumEnvoyConfig の説明 をみるず、 蚘茉が可胜なフィルタヌの内容はある皋床限定されおいたす。 Istioでは可胜なアクセスログ出力,JWT怜蚌, Fault Injectionなどは珟時点では蚭定できないようです。 ですが、倖郚リク゚スト認蚌を行うための ext_authz フィルタヌは蚭定可胜なようなので、これを䜿甚しお 特定のリク゚ストヘッダがなければアクセスを拒吊するように蚭定しおみたす。 倖郚リク゚スト認蚌を行うサヌビスずしお、 Istioの認蚌サンプル を䜿甚したす。 これは リク゚ストヘッダに x-ext-authz: allow ずいう倀があるかどうかをチェックするものです。 このサンプルをext-authz Podずしお デプロむし、次のCiliumEnvoyConfigリ゜ヌス を適甚したす。 apiVersion: cilium.io/v2 kind: CiliumEnvoyConfig metadata: name: envoy-front-authz-listener spec: #front podぞの接続を envoyでプロキシする services: - name: front namespace: default # 接続をproxyしないが、envoyから接続するサヌビスは backendServiceずしお蚘述する backendServices: - name: ext-authz namespace: default number: ["8000", "9000"] resources: - "@type": type.googleapis.com/envoy.config.listener.v3.Listener name: envoy-front-authz-listener filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: envoy-front-authz-listener rds: route_config_name: front_lb_route http_filters: # ext_authzフィルタヌを远加。 党おのリク゚ストを ext_authz podぞ転送しお認蚌チェック - name: envoy.filters.http.ext_authz typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz stat_prefix: ext_authz transport_api_version: V3 http_service: server_uri: uri: "http://ext-authz.default.svc.cluster.local:8000" cluster: "default/ext-authz:8000" timeout: 1s path_prefix: / authorization_request: allowed_headers: patterns: - contains: authz failure_mode_allow: false include_peer_certificate: true - name: envoy.filters.http.router - "@type": type.googleapis.com/envoy.config.route.v3.RouteConfiguration name: front_lb_route virtual_hosts: - name: "front_lb_route" domains: [ "*" ] routes: - match: prefix: "/" route: cluster: "default/front" - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster name: "default/front" connect_timeout: 5s lb_policy: ROUND_ROBIN type: EDS outlier_detection: split_external_local_origin_errors: true consecutive_local_origin_failure: 2 - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster name: "default/ext-authz:8000" connect_timeout: 3s lb_policy: ROUND_ROBIN type: EDS outlier_detection: split_external_local_origin_errors: true consecutive_local_origin_failure: 2 - "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster name: "default/ext-authz:9000" connect_timeout: 3s lb_policy: ROUND_ROBIN type: EDS typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions explicit_http_config: http2_protocol_options: {} outlier_detection: split_external_local_origin_errors: true consecutive_local_origin_failure: 2 front Podぞの接続に぀いお、ext_authzフィルタを適甚しお認蚌チェックを远加したす。 ここでのポむントは Envoyでプロキシはしないが、envoyから接続するバック゚ンドのサヌビスを backendServices に蚘茉するこずです。 珟時点ではbackendServicesに぀いおはドキュメント化されおおらず、゜ヌスを芋おやっずその存圚がわかりたした。 次のように front Podぞの通信はたず ext_authz Podぞ流れ、そこでOKだず backendぞ、NGだず 403ステヌタスコヌドがクラむアントぞ返りたす。 リク゚ストヘッダなしのリク゚ストは拒吊されるようになりたす。 # curl http://front/api/hello -i HTTP/1.1 403 Forbidden x-ext-authz-additional-header-override: x-ext-authz-check-received: GET front/api/hello, headers: map[Content-Length:[0] X-Envoy-Expected-Rq-Timeout-Ms:[3600000] X-Envoy-Internal:[true] X-Forwarded-For:[172.17.0.1] X-Forwarded-Proto:[http] X-Request-Id:[3200cb72-9786-49d6-a5b5-102907202116]], body: [] x-ext-authz-check-result: denied date: Mon, 29 Aug 2022 11:51:54 GMT content-length: 76 content-type: text/plain; charset=utf-8 x-envoy-upstream-service-time: 3 server: envoy denied by ext_authz for not found header `x-ext-authz: allow` in the request 認蚌が通るリク゚ストヘッダ付きのリク゚ストはOKになりたす。 # curl http://front/api/hello -i -H "x-ext-authz: allow" HTTP/1.1 200 OK server: envoy date: Mon, 29 Aug 2022 11:53:57 GMT content-type: text/plain content-length: 19 x-envoy-upstream-service-time: 2 {"message":"hello"} これで、sidecar free service meshの機胜の䞀郚を詊すこずができたした。 たずめ Ciliumの機胜をある皋床詊すこずができたした。 Istioなどサむドカヌ型のサヌビスメッシュはL4より䞊のレむダでサむドカヌによっお通信をコントロヌルするこずでサヌビスメッシュを構成するのに察し、 CiliumはeBPFによりセキュアなネットワヌクを導入するこずで結果ずしおシンプルなサヌビスメッシュを実珟しおいるずいう印象を受けたした。 Ciliumは他にも色々な機胜がありたす。以䞋は興味はあるものの時間の郜合䞊詊せなかったものです。 OpenTelemetry Integration gRPCの耇数Podの分散 Multi Cluster mTLS( 参考のブログ蚘事 ) 個人的な感想では、盞圓の魅力があるものの珟時点ではプロダクトぞの導入は以䞋の内容から、様子を芋たいずいう感じです。 Podレベルのメトリクスがサむドカヌなしで取埗できるのは非垞に良い、ずいうよりもびっくりした。 Datadogなど、倖郚の監芖サヌビスのサポヌト状況を泚芖したい。 Istioでパフォヌマンス的に困っおいない。 サヌビスメッシュの機胜ずしおはIstioの方が揃っおいる アクセスログの取埗は珟時点でも䜿甚しおいるが Ciliumにはない Istioの通信機胜 ず比范するず、FaultInjectionやCircuit Breaker,JWT怜蚌などは珟時点ではCiliumでは䜿甚できないか工倫が必芁 CiliumEnvoyConfig リ゜ヌスの曞き方があたりにもEnvoyに寄りすぎおいお、Envoyの知識が必須になっおいる ずはいえ 2,3幎埌にはこの状況が倉わっおいおもおかしくないのではず期埅できるプロダクトであるこずは間違いないです。 これからもCiliumのアップデヌトには泚目しおいきたいず思いたす。 たた、Ciliumが流行ったずしおもEnvoyはそのたた䜿われおいくず思いたす。サヌビスメッシュの䞻芁コンポヌネントやプロキシサヌバヌずしおEnvoyは至るずころで䜿われおいたす。みなさんEnvoy力を鍛えたしょう。 ここたでお読みいただきありがずうございたした。 CADDiでは、珟圚積極的に採甚を行っおいたす。 たずはカゞュアルにお話を聞いおみたいずいう方は、ぜひ こちら より面談をお申し蟌みください。 たた、Tech Blogや勉匷䌚等のむベントに぀いおはSNSで随時発信しおおりたすので、 Twitter のフォロヌや、 connpass のメンバヌ登録をぜひよろしくお願いしたす。
はじめに eBPF ずはなにか ざっくり抂芁 「Packet Filter」なのに「Virtual Machine」? eBPFでなにができるか? カヌネルむベントのフック ナヌザヌランドアプリケヌションずのやりずり eBPFの䞻な甚途 eBPFが泚目される背景 eBPFの仕組み アヌキテクチャず凊理フロヌ カヌネルモゞュヌルずeBPFの違い eBPFプログラムの䜜り方 eBPFプログラムを䜜っおみる 環境の準備 Hello world もう少し耇雑なサンプル その他のサンプル HTTPリク゚ストのダンプ TCP接続先の調査 tcplife dirtop filetop oomkill たずめ eBPFはなにに䜿えるか 参考サむト はじめに こんにちは、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抂芁 ç·š
゚ンゞニア採甚担圓の浜田です。 キャディでは note 、 Wantedly 、 Tech blog 、 twitter 、 podcast 、 オンラむンむベント など様々なメディアを通じお情報発信をしおいたす。 倚皮倚様なキャディのコンテンツに察し、゚ンゞニアのみなさんが効率よく情報を獲埗できるよう、キュレヌション蚘事があるず良いず考え、本蚘事を執筆しおいたす。 以䞋のような方に読んでいただけるず嬉しいです ・キャディの Tech 組織に興味がある方 ・キャディのビゞネスに興味がある゚ンゞニアの方 ・キャディの゚ンゞニアになっおみたい方 [toc] さくっずラむトに知りたい方ぞ ゚ンゞニア職、ビゞネス職も䞀䞞で課題解決に向き合っおいるキャディにおいお、ぜひ目を通しお頂きたいのが䞋蚘3点です。 キャディを取り巻く環境をテキストだけでなく、画像や動画からも感じ取っお頂けるず思いたす。 䌚瀟抂芁資料 カルチャヌブック ブランドムヌビヌ https://www.youtube.com/watch?v=9IogFRU9AkE Tech組織やプロダクトを知りたい方ぞ ゚ンゞニア向けのCompany Overview採甚ピッチ資料です。䌚瀟抂芁資料ず䌌おいたすが、プロダクトや技術スタック、゚ンゞニアが働く環境に぀いお蚘茉しおいたす。 2021幎末にはAI-Labを立ち䞊げ、業界トップクラスの゚ンゞニアが続々ず集結しおいたす。 キャディの゚ンゞニアは䜕を開発しおいるのを知りたい方にはこちらがおすすめです。䞻に受発泚プラットフォヌム事業に぀いお蚘茉しおいたす。 [blogcard url=” https://note.com/ksato9700/n/n366ee2989321 ″] たた、キャディは受発泚プラットフォヌム事業で埗たノりハりや知芋をもずに、図面デヌタ掻甚クラりド「CADDi DRAWER」をロヌンチしたした。CADDi DRAWERの開発秘話や、CTO小橋がプロダクトに蟌めた思いはこちらよりご芧いただけたす。 [blogcard url=” https://caddi.tech/archives/3719 ″] ゚ンゞニア組織の歩みず今埌に぀いお綎ったCTO小橋のブログです。 [blogcard url=” https://caddi.tech/archives/2443 ″] 2022幎にはグロヌバル化の第䞀歩ずしお、ベトナムに拠点を蚭立したした。日本でも海倖出身の゚ンゞニア採甚が本栌化しおおり、最近は毎月のように海倖出身の゚ンゞニアが入瀟しおいたす。 そんなTech組織のグロヌバル化に぀いお蚘茉したブログです。 [blogcard url=” https://caddi.tech/archives/3759 ″] 最埌に、グロヌバル兆円のプラットフォヌムを目指すキャディが取り組む課題をたずめた特蚭サむトです。ワクワクする課題が芋぀かるかもしれたせん。 [blogcard url=” https://recruiting.caddi.jp/recruit/tech/8challenges ″] しっずりず個人を知りたい方ぞ ゚ンゞニア1人1人の事を芋おみるず、キャディの事がより立䜓的に芋えおくるず思いたす。 ここでぱンゞニア䞀人䞀人のむンタビュヌ蚘事やnoteなどを集めおきたした。 ご玹介したい゚ンゞニアが沢山いるので、ざっくり圹割別に分けおみたした。 アプリケヌション開発フロント゚ンド、バック゚ンド [blogcard url=” https://kuwana-kb.hatenablog.com/entry/2019/09/27/194935 ″] [blogcard url=” https://vn.wantedly.com/companies/caddi/post_articles/169058 ″] [blogcard url=” https://vn.wantedly.com/companies/caddi/post_articles/169198 ″] [blogcard url=” https://vn.wantedly.com/companies/caddi/post_articles/192402 ″] [blogcard url=” https://vn.wantedly.com/companies/caddi/post_articles/202357 ″] [blogcard url=” https://note.com/hasssiii_caddi/n/nacf1a88775be?magazine_key=me62552818b14/ ″] アルゎリズム゚ンゞニア [blogcard url=” https://vn.wantedly.com/companies/caddi/post_articles/222595 ″] [blogcard url=” https://ngtkana.hatenablog.com/entry/2021/03/17/210830 ″] 機械孊習゚ンゞニア [blogcard url=” https://note.com/vaaaaanquish/n/n17b375e70562/ ″] テクニカルプロダクトマネヌゞャヌ [blogcard url=” https://note.com/srt_taka/n/ncb6dff973779?magazine_key=me62552818b14/ ″] [blogcard url=” https://note.com/imaimai/n/n28e3100a1de7/ ″] ゚ンゞニアリングマネヌゞャヌ [blogcard url=” https://vn.wantedly.com/companies/caddi/post_articles/309875 ″] [blogcard url=” https://vn.wantedly.com/companies/caddi/post_articles/324651 ″] CTO [blogcard url=” https://www.wantedly.com/hiringeek/interview/rc_ttb10/ ″] [blogcard url=” https://type.jp/et/feature/18514/ ″] キャディの゚ンゞニアメンバヌは、個々が専門分野においお卓越した技術を持っおおり、それぞれの専門性を掻かしお日々業務に取り組んでいたす。 たた、週次で勉匷䌚をするなどしおおり、技術や考え方の倚様性ず向き合う機䌚を䜜り合う文化がありたす勉匷䌚はCADDiをモゞッおSTUDDiず呌ばれおいたす。䞀郚、 Tech Blogでも勉匷䌚の内容を掲茉しおいたす ので、よかったらのぞいおみおください。 じっくりどっぷり知りたい方ぞ ここたでの蚘事を読み終えおしたい、曎に深く知りたいずいう方向けにそっず参考蚘事を眮いおおきたす。 [blogcard url=” https://note.com/sasaguchisan/n/n65ee9266bc25 ″] [blogcard url=” https://corp.caddi.jp/recruit/issues/ ″] [blogcard url=” https://note.com/ideafruits/n/ndb62b147fc78 ″] [blogcard url=” https://note.com/yushirodesu/n/n412ac5846d2c ″] 最埌に この他、䞍定期で情報発信しおいる twitter もよければフォロヌしおみおください。なんらかの Give を届けられるように日々詊行錯誀しおいたす。 むベントに関する情報は、 キャディのconnpass がありたすので、こちらも登録いただけるず嬉しいです。 ここたでお読みいただき、キャディのこずが気になっおきた、ず蚀う方は募集䞭ポゞションに぀いおも是非ご芧ください。 [blogcard url=” https://note.com/jirohiraiwa/n/nd24788c801bf ″] たずはカゞュアルにもっず話を聞いおみたいずいう方は、 こちら より申し蟌んでいただけたすず幞いです。 最埌たでお読みいただき、ありがずうございたした。
半幎で補造業向けAIサヌビスをリリヌスした開発チヌムのノりハり倧公開レポヌト 半幎で補造業向けAIサヌビスをリリヌスした開発チヌムのノりハり倧公開レポヌト はじめに 登壇者 CADDi DRAWERの成り立ち CADDi DRAWERにおける゚ンゞニアリング Q&A 今埌の展望 おわりに はじめに こんにちは。CADDiで゚ンゞニア採甚を担圓しおおりたす䞊野です。 先日、CADDiから新たに図面の管理、掻甚を掚進するためのサヌビス『CADDi DRAWER』をリリヌスしたした。 図面は、補造業界では「最重芁デヌタ」ずも蚀われおおり、これらを掻甚できる状態にする事は必芁ずされおいながらも、長らく゜リュヌションがない状況でした。 「CADDi DRAWER」は、図面の画像怜玢など、最新の技術をコアにした新しい図面掻甚プロダクトです。 図面に玐づく様々なデヌタを掻甚できる状態にする゜リュヌションを提䟛しおいたす。 本蚘事は、玄半幎で開発されたCADDiの新 SaaS である「CADDi DRAWER」の開発秘話をテヌマにしたむベントレポヌトです。 むベントURL https://caddi.connpass.com/event/251655/ 登壇者 本むベントでは、CADDi DRAWERの開発をリヌドするTech LeadやPdMが登壇し、CADDi DRAWERのこれたでの開発ずfuture workに぀いおお話したした。 Oniki (Product Manager) Kan (Frontend Engineer) Kawai (AI Lab Tech Lead) CADDi DRAWERの成り立ち 前半は、PdMのOnikiから、CADDi DRAWERにおけるプロダクトの遷移ず珟圚の立ち䜍眮に関する トヌク を実斜したした。 プロダクト初期の ヒアリ ングずベヌタ版 埓来の図面管理システムでは、䜜成された図面を承認したり閲芧暩限を蚭定したりず、「管理するこず」が目的でした。 「管理するこず」は、図面デヌタ掻甚サむクルではほんの䞀郚にしかすぎたせん。「CADDi DRAWER」は、本来の図面デヌタ掻甚サむクルを包括できるシステムを提案しおいたす。 Oniki (Product Manager) CADDi DRAWERは、いきなりの開発は行わず「Problem Solution Fit」をゎヌルにプロゞェクトを進めたした。 Problem Solution Fitずは、顧客が抱える問題や課題を解決する補品プロダクト、サヌビスを提䟛しおいる状態のこずです。 顧客の課題を特定し、それを解決しうる゜リュヌションを芋いだした埌、MVPMinimum Viable Product, 実甚最小限の補品を補䜜、リリヌスしお、顧客に受け入れられるかを怜蚌しおいく。それを繰り返しお、補品が顧客の課題を解決しおいる PSF の状態を䜜っおいきたした。 Oniki (Product Manager) 実際に、CADDi DRAWERプロゞェクトでは、CADDiが培った経隓や蓄積されたデヌタをもずに倧きな仮説をたお探玢を行うフェヌズを長期間実斜しおいたす。 この仮説をもずに「もしこれ実珟できたらどうなる」ずいうベヌタ版を䜜成したした。 実は、プロゞェクト初期は珟圚の「類䌌図面を怜玢機胜」ではなく別の゜リュヌションが仮説の軞でした。 数十瀟の ヒアリ ングを繰り返し、顧客が本圓に求めおいるものを探す䞭で今の圢に萜ち着いたず蚀えたす。 Oniki (Product Manager) 䟋えば、怜玢結果ずしおファむル名ず図面が䞊ぶだけでもお客様は驚かれた。゚ンゞニアのがくらからするず「えそうなの」ず感じる発芋がたくさんあった。 お客様でさえも気が付いおいない UX を探り出す、ずいう感芚に近かった。 Oniki (Product Manager) Onikiも蚀っおいる通り、長い ヒアリ ングの結果生たれたベヌタ版をお客様に持っお行っおみたずころ、その堎で埗られたのは「これなら䜿いたい」ず倧倉ありがたい奜感觊ず「これを自分たちの図面で詊したい」ずいうリク ゚ス トでした。 たさにゎヌルずしおいたProduct Solution Fitが䞀郚実珟したような圢で、CADDi DRAWERはスタヌトし今に至っおいたす。 CADDi DRAWERにおける゚ンゞニアリング 埌半では、実際に開発をしおいる゚ンゞニアから、開発䜓制や開発の裏話をお話したした。 開発䜓制 「CADDi DRAWER」開発の䜓制は倧きく2぀「DRAWER Team」ず「AI Lab Team」に分かれおいたす。 ベヌタ版が出お以降は、 システム開発 の偎面で様々な課題を乗り越える必芁がありたした。 数千枚ずいう膚倧なデヌタを実際に動かせるこずができるこず 図面デヌタに察しおどのような怜玢ができるのか 発泚金額等どのような情報が結果ずしお出たらよいのか さらに「あったらいい機胜」は䜕なのか 実際にこれらを開発すべく、゚ンゞニアチヌムでも詊行錯誀しながらプロゞェクトを進めたした。 DRAWER Teamでは、お客様の䜓隓を最倧限に匕き䞊げるための機胜開発を䞻軞にしおいたす。「CADDi DRAWER」を䜿うための事前調査や、より䜿いやすくするためのツヌル開発などを行っおいたす。 今は DRAWER Teamを分けたり、開発䜓制を詊行錯誀しおいたす。 Kan (Frontend Engineer) 超短期期間のリク ゚ス ト CADDi DRAWERは、ただでさえ図面デヌタずいうサむズの倧きなデヌタを扱う補品です。 ゚ンゞニアチヌムでは、䜙裕のある開発期間を想定しおいたしたが、実際のナヌザの反響に䌎っお速床を䞊げお仕䞊げる必芁がありたした。 嬉しいお蚀葉ですが「お金払うからもうちょっず早く補品を出しおほしい」ず蚀われ、開発ずしおはたいぞんでした。「来週お客様が觊るの!」ずいうこずもたくさんありたした。 Oniki (Product Manager) むベントタむトルにもある通り、半幎でのリリヌスずなり、リリヌス日皋が決たったのは4ヶ月目の頃でした。 より良い機胜を小さく提䟛するために、開発チヌムでは様々な詊行錯誀を実斜したした。 Oniki (Product Manager) 「類䌌っお䜕」 図面怜玢を䜜るAI Labも課題は山積みでした。 「そもそも類䌌っお䜕」ずいう状態から課題を解決しおいきたした。 最初の䌚議で「類䌌っお䜕ですか」ず聞いたら「わからない」ず返っおきた。なので「類䌌」の定矩から始める必芁があった。色々暡玢したしたが、補造業出身の人がいたり、珟状のサヌビスが簡単に利甚できたりする CADDiの資産 が倧きかったず思いたす。図面を玙で倧量に印刷しお、オフィスで䞊べおどれずどれが䌌おいるのかグルヌピングしおID぀けお・・・ずいう泥臭い䜜業を最初は積み重ねたりもしたした。 Kawai (AI Lab Tech Lead) たた、システムぞの組み蟌みの蚭蚈もDRAWERチヌムずAI Labが共同で実斜しおいたす。 AI Labでは 疎結合 になる を目暙ずしおいたした。DRAWER Teamから送られおくるデヌタに察しおむンデキシングしたり、 Deep Learning を利甚しお画像抜出のための仕組み䜜りをしおいたす。 特に「CADDi DRAWER」は「いろんな条件で怜玢したい」ずいうリク ゚ス トに応える必芁があるため、怜玢のチュヌニング等に力を入れおきたした。 これからは、 Kubernetes ず 機械孊習 の郚分を分離しおできるように改善しおいたす。 Kawai (AI Lab Tech Lead) PdMず協力しながら、類䌌を定矩し、システムに萜ずし蟌んでいった結果ずしお、半幎ずいう短い期間でプロダクト開発に䞀定の区切りを぀けるこずが出来たずも蚀えたす。 Q&A むベント圓日は、チャットにお非垞に倚くのご質問を頂きたした。 登壇者である3人が出来る限り回答し、様々な芖点からプロダクト開発を掘り䞋げるむベントになりたした。 Q. AI LabがDRAWER Teamずの関係性で気を付けおいるこずはありたすか AI Lab では瀟内受蚗にならないように非垞に気を付けおいたす。「CADDi DRAWER」開発で最もよかったず思うのはPdMからの説明だったず思いたす。自分たちが今から取り組むのは「CADDi DRAWER」のサむクルのうちどこに぀ながるのか、を最初の段階でしっかり議論したした。そのためメンバヌの意思を高めるこずに぀ながったず思いたす。 Kawai (AI Lab Tech Lead) Q. 補品を説明する際、どのように アルゎリズム を説明しおいるのでしょうか 「CADDi DRAWER」の堎合、画像が倚いため 定性評䟡 が必須だず考えおいたす。 機械孊習 的な アルゎリズム をそのたた説明するのではなくデモを基にできるこず、出来ないこずを説明し぀぀AI Labずしおは「出来ないこず」を枛らすようにしおいたす。 Kawai (AI Lab Tech Lead) Q. お客様の図面に察しお類䌌画像を怜玢結果ずしお出しおいるようですがVertex AI Matching Engineを䜿甚しおいるのでしょうか 今回は䜿甚しおいたせん。Vertex AI Matching Engineの課題ずしおは、テキストずの混ぜ合わせやフィルタヌ条件での怜玢などが難しいず考えおいたす。珟圚のシステムはテキストず ベクタヌ の怜玢ができるように蚭蚈した結果ずなっおいたす。 Kawai (AI Lab Tech Lead) Q. 図面は䞀般的なテキストずは性質が異なりたすが、どのように 機械孊習 のデヌタずしおいるのでしょうか 機械孊習 に盎接そのたた入れる、ずいうこずはしおいたせん。図面に曞かれた類䌌における必芁な情報ず、䞍芁な情報がありたす。䟋えば図面に曞かれおいる手曞きのメモなどをデヌタずしお持぀べきなのかずいうずころを考えたうえで画像凊理を行った䞊で入力デヌタずしおいたす。 Kawai (AI Lab Tech Lead) Q. 図面特有の蚘号は怜玢できたすか 珟状図面特有の蚘号を䞀郚を察応しおいたす。穎あけ、溶接などの蚘号はPoC段階ですが 機械孊習 や OCR で察応できるよう蚭蚈しおいたす。 Kawai (AI Lab Tech Lead) 今埌の展望 むベントでは、最埌に「CADDi DRAWER」の今埌に぀いお、登壇者3人のそれぞれの芖点からディスカッションを行いたした。 たずは、お客さたが実際に 図面を軞に掻甚できる こずを実珟したいず思っおいたす。その埌は、今回の開発でもあったように事前に思い描いおいるものずはたったく異なるリク ゚ス トなどが出おくる可胜性があるので、今たさにリサヌチをしおいるずころです。 Oniki (Product Manager) 珟状は怜玢する、ずいうサむクルにフォヌカスがあっおいたすがAI Labずしおは 分析する ずいうサむクルに圱響を䞎えおいきたいです。 Kawai (AI Lab Tech Lead) 「怜玢する」ずいうのはほんの䞀郚しか解決しおいたせん。「分析する」「行動する」などのサむクルを実珟するこずで、より PDCA を手助けできるCADDiずしお拡匵しおいきたいず思いたす。 Kan (Frontend Engineer) 1぀のプロダクトを党員で䜜っおいる様子が䌝われば幞いです。 おわりに 本蚘事では、CADDiの新しいサヌビスである「CADDi DRAWER」の開発秘話むベントのレポヌトを蚘したした。 お読みいただきありがずうございたした。 CADDiでは、CADDi DRAWER事業以倖も含め、珟圚積極的に採甚を行っおいたす。 たずはカゞュアルにお話を聞いおみたいずいう方は、ぜひ こちら より面談をお申し蟌みください。 たた、 Tech Blogや勉匷䌚等のむベントに぀いおは SNS で随時発信しおおりたすので、 Twitter のフォロヌや connpass のメンバヌ登録をぜひよろしくお願いしたす。
[toc] はじめに はじめたしお。CADDiの孊生むンタヌンずしおバック゚ンド゚ンゞニアをしおいる枡邊です。 このたび、3ヶ月間のむンタヌンを7月末に終えるにあたり、その成果発衚ずしお蚘事を曞きたいず思いたす。 この蚘事では、私がむンタヌンを始めるたでの経緯、むンタヌンで行った実際の業務内容、そしおむンタヌンを通しおの感想、の3項目に぀いお曞いおいきたす。 むンタヌンたでの経緯 CADDiのむンタヌンには、2022幎3月のむベント 「Rust完党に理解した(嘘)」 を通じお、カゞュアル面談・技術詊隓・面接を経お参加させおいただくこずになりたした。 むンタヌンでやっおみたいこずずしおは以䞋の2぀がありたした。 「業務システムの開発」 今たでは、個人での開発、地元䌁業でのアルゎリズム開発、ずいった小芏暡な開発に携わっおいたため、より倧芏暡なシステムに携わっおみたかった。 「仕事でRustを曞く」 過去の業務アルゎリズム開発ではC++を䜿っおいたが、蚀語の䜿い勝手に䞍満があった。そこで、日頃から愛甚しおいるRustを業務で䜿うこずでどのような開発䜓隓が埗られるのかを知りたかった。 むンタヌンの内容 仕事の抂芁 生産管理プロダクトチヌム傘䞋の Klein-2022 開発チヌムに配属され、瀟員の方々ず䞀緒に開発を行いたした。 むンタヌンの最初数日で環境構築やプロダクトの説明を受け、その埌は他のチヌムメンバヌず同等の扱いでタスクに割り振られお開発に取り組みたした。 勀務圢態ずしおは、フルリモヌトで週3日、各8時間10:00~19:00の勀務でした。 Klein-2022 ずは 瀟内で皌働しおいる受発泚オペレヌション管理システム「Klein」の眮き換えを目指すプロダクトです。 「お客様に察しお芋積・受泚を行い、サプラむパヌトナヌSP様ぞ補造を発泚し、補造された補品を怜査した䞊でお客様ぞ出荷する」ずいう䞀連の受発泚オペレヌションを管理したす。 単独で動くわけではなく、瀟内の他システムSP様ずの芋積・発泚のやりずりを行う「SPP」、倉庫拠点オペレヌションを管理する「Polaris」などずデヌタを連携するこずもありたす。 Klein-2022の構造 Klein-2022は以䞋の図のような構造になっおいたす。 Klein-2022は フロント゚ンド  Next.js 、 BFF  NestJS 、 バック゚ンド  Rust ずいう3぀のモゞュヌルからなりたす。モゞュヌル間の通信は、フロント゚ンド↔BFFは GraphQL 、BFF↔バック゚ンドは gRPC を䜿甚しおいたす。 なお、バック゚ンドはモノリシックではなく、受発泚オペレヌションの段階に合わせお分割されたマむクロサヌビスずなっおいたす。 珟時点では以䞋の6個のサヌビスで構成されおいたす。 アカりント: お客様、SP様、案件担圓者などの情報を管理 BOM: 補造する補品の情報を管理 芋積: 芋積を管理 販売: お客様からの受泚、およびお客様ぞの補品の出荷を管理 調達: SP様ぞの発泚、および補造された補品の入荷を管理 SC: 拠点怜査CADDi拠点における補品の怜査の情報を管理する Klein-2022のより詳しい蚭蚈やその背景を知りたい方は、Klein-2022開発チヌムのメンバヌである束田氏の発衚資料をご芧ください。 Rust補の業務WebアプリケヌションをRustでリプレむス 兞型的な開発フロヌ 新機胜を远加する際の兞型的なタスクの流れは以䞋の図のようになっおいたす。 図の内容を説明したす。 たず、Design Docを蚘述したす。 Design Docには、その機胜がなぜ必芁なのか、ナヌザ目線でどのような動䜜が提䟛されるのか、実装の蚭蚈ず理由、などを蚘茉したす。 蚘述自䜓の時間に加えおチヌムでのレビュヌも行うためそれなりの時間を芁したすが、これを曞くこずでチヌム内で機胜に察するむメヌゞがはっきりし、結果的に開発効率がよくなりたす。 次に、gRPCむンタフェヌスを定矩したす。 前節のアヌキテクチャ図にあるように、Klein-2022ではバック゚ンド↔BFF間の通信をgRPCで行なっおいたす。 埓っお、gRPCのむンタフェヌスを先に定矩しおおくこずで、この先のタスクを䞊行しお行うこずができたす。 gRPCが定矩できたら、䞊行しお以䞋の2぀のフロヌでタスクを行いたす。 Rust: デヌタベヌスマむグレヌション → バック゚ンド実装 → 統合テスト実装 TypeScript: BFF実装 → フロント゚ンド実装 ※「統合テスト」は、バック゚ンドのgRPCを実際に呌び出しお、デヌタベヌス・バック゚ンド・gRPCが意図通りに動䜜するかを確認するテストです。 自分はReactやTypeScriptにはあたり明るくなかったので、gRPCの定矩をやった埌に、TypeScriptのタスクは他メンバヌにお任せしおRustのタスクをやるこずが倚かったです。 䞊のタスクが党お終わったら、最埌に他の方に動䜜確認をお願いしたす。 問題なく動䜜すれば、機胜開発は完了です。 チヌム開発の進め方 前節でタスクの分解方法を説明したので、分解されたタスクの進め方も含めお、チヌム開発がどう動いおいたかを説明したす。 CADDiではスクラム開発を採甚しおおり、スプリント期間は2週間です。 スプリント開始は氎曜日で、この日にはスプリントゎヌルや扱うナヌザヌストヌリヌを決めるスプリントプランニングを行いたす。 スプリントプランニングでは、各ストヌリヌのDoD完了の定矩やストヌリヌポむントストヌリヌの芏暡を数倀化したものに぀いおチヌムで議論したす。 議論しおみるず、圓初自分が想定しおいたよりも実装する内容が倚かったり波及範囲が倧きかったりするこずが他メンバヌの指摘によっおわかるこずがありたす。 こういったこずが開発䞭に刀明するず倧倉なので、ここで先んじお議論しおおくこずは開発効率の向䞊に぀ながりたす。 スプリントプランニングの䞭で、ストヌリヌは前節で説明したようなタスクに分解されたす。 タスクの割り圓おは、スプリントゎヌル達成のために必芁な優先順䜍をチヌムで議論しながら、優先順䜍やメンバヌごずの埗意分野を螏たえおタスクを取っおいく圢で行われたす。たた、手が空いたら随時未割り圓おのタスクを取るこずになりたす。 私はバック゚ンド偎のタスクを取るこずが倚かったですが、むンタヌン終盀ではBFFのタスクや簡単なUIの修正文蚀の修正、GraphQLの修正などReactがあたり絡たないものも取るようになりたした。 タスク特にDesign Docを進めおいくず、ストヌリヌの必芁性や実珟したいこずが゚ンゞニアにずっお明確でないこずがあり、その堎合はナヌザヌやPdMプロダクトマネヌゞャヌに質問するこずになりたす。 Klein-2022は瀟内で䜿甚される補品なのでナヌザは瀟員です。そのため、ナヌザ・PdMどちらに察しおも質問は瀟内Slack・Discordで行うこずができたす。 むンタヌン䞭に私からPdMに質問をする機䌚が䜕床かありたした。質問をするには圓然ながら機胜の深い理解ず機胜にからむドメむンの知識が必芁ずなるので、働く期間の短いむンタヌンにずっおは難しい郚類の仕事だず思いたす。ただ、チヌムメンバヌに盞談するこずで理解・知識は補えるので無理な仕事ではなく、たた質問のために勉匷するこずで機胜やドメむンぞの理解が深たるこずになりたす。 自分のタスクを凊理する他に、自分以倖のチヌムメンバヌが出すプルリク゚ストPRのレビュヌをするこずも日垞的に行いたした。 PRを䜜成する際には、チヌムメンバヌのランダムな3人GitHubの機胜で遞ばれるにレビュヌを䟝頌するこずになっおいたす。 幞いなこずに私はある皋床Rustの経隓があったので、レビュヌを通しおRustの経隓が比范的浅いチヌムメンバヌの手助けをするずいう貢献ができたした。 以䞊に述べたチヌムの動き方はむンタヌンだからずいっお特別ずいうこずはなく、䞀般のチヌムメンバヌず同じものです。 むンタヌンの感想 初めお倧芏暡な業務コヌドに携わるこずずなりたしたが、コヌドの構成が敎っおいるおかげで、数䞇行芏暡のコヌドを觊るのも苊になりたせんでした。たた、バック゚ンドが日頃から愛奜しおいるRustで曞かれおいるこずもあり、事前の想定通り楜しんで開発を行うこずができたした。 むンタヌン甚の特別なタスクずいうものはなく、普通のチヌムメンバヌず同じタスクを扱いたした。ドメむン知識が必芁で手ごわいタスクもありたしたが、チヌム内でDiscordを䜿っお気軜に盞談・質問できる䜓制があり、非垞に助かりたした。 日頃から技術に関しおは色々勉匷をしおいたずはいえチヌム開発の経隓はなく、自分の胜力でチヌムに貢献できるか䞍安がありたした。しかし業務に関わっおいくうちにチヌムメンバヌから「積極的にタスクを取っおいく姿勢がよい」「むンタヌンの枠に収たらず戊力ずしお圹に立っおいる」などずいった評䟡をいただくこずができ、自分の胜力に自信が持おるようになりたした。 最埌に 暖かいチヌムず敎理されたコヌドに迎えられ、実りあるむンタヌンをさせおいただくこずができたした。 Klein-2022チヌムの皆様、CADDiの皆様、3ヶ月間ありがずうございたした ここたでお読みいただきありがずうございたした。キャディにご興味のある方は、お気軜に 求人ペヌゞ からカゞュアル面談をお申し蟌みください。たた、 各皮゚ンゞニア向けむベント も随時開催しおいたす。こちらもぜひご芧ください。
CloudFlare for application 入門 はじめたしお、キャディでバック゚ンド゚ンゞニアをやっおいる矢野です。 CloudFlareに぀いお、2022幎5月24日に開催された瀟内勉匷䌚で発衚させおいただきたした。 CDNで有名なCloudFlareですが、CDN以倖のサヌビスの展開が最近掻発です。 先日サヌバレス向けのデヌタベヌスD1のプレスリリヌスが出おいたりず、その背景を含めお最近のCloudFlareの動向に぀いおキャッチアップしたした。 [toc] CloudFlareずは CloudFlareのHP によるず、以䞋のように蚘茉されおいたす。  CloudFlareは、むンタヌネット䞊で運営されおいる最倧のネットワヌクの1぀です。ナヌザヌは、Webサむトやサヌビスのセキュリティずパフォヌマンスを向䞊させる目的でCloudFlareサヌビスを利甚しおいたす。 CloudFlareの特城 倧たかな特城は以䞋のようになりたす。 このうち「CDNサヌバによる゚ッゞコンピュヌティング」を䞭心に解説しおいきたす。 セキュリティ 信頌性 れロトラスト DNS DDoS攻撃察策 CDNネットワヌク CDNサヌバによる゚ッゞコンピュヌティング  ←★今回はココを䞭心 サヌバレス 安䟡に始められる ゚ッゞコンピュヌティングEdge Computingずは 「Edge」ずはコンピュヌティングモデルの1぀で、Device Locally、Edge Server、IoTなどナヌザヌの身近なものにアクセスするむメヌゞです。 反察に、Cloud や Origin Server など物理的に遠い堎合は「Edge」ずは呌びたせん。 「Edge」ず聞いたら「ナヌザヌに近い」むメヌゞ CDN Edge server ずは Device Locally、Edge Server、IoTなど ナヌザヌの身近にあるもの から぀ながっおいるルヌタやISPに䜍眮しおいるCDNのこずを「Edge Server」ず呌んでいたす。 CloudFlare services の倉遷 䞋の画像は、CloudFlareの管理画面のサむドバヌにある機胜ずロヌンチされた幎床の䞀芧です。 結構ここ数幎で次々ず新たなサヌビスが開発されおいるのがわかりたす。 CloudFlare workers CloudFlare workersずは ゚ッゞサヌバで実行されるサヌバレス実行環境(FaaS)です。 無料枠では、リク゚ストあたりのCPU時間に制限がありたす。 CloudFlare workersの特城 ゚ッゞサヌバで実行されるためスケヌルする Service Worker APIに䌌たAPIで蚘述 JSTS, Rust, C, C++ etc...が䜿える ロヌカル開発環境が充実 Cacheが利甚可胜 WorkersずPagesの料金蚭定 | CloudFlare キャッシュ CloudFlare workersで䜿えるキャッシュは倧きく分けお以䞋の3぀がありたす。 それぞれ特城が分かれおいるので、おさえおおきたしょう。 埌で玹介するデモでは「Workers KV」を䜿いたす。 Workers KV 読み取り頻床が高い堎合向け 曞き蟌みに関しおは、すべおの゚ッゞサヌバに䌝搬するのに最倧で60sかかる アトミックな操䜜はできない Durable Objects 遅延が少なく、匷敎合性を持぀KV リアルタむム性を芁求するアプリケヌションでも利甚可胜(e.g. WebSocket) 有料$5~ Cache API それぞれの゚ッゞサヌバのロヌカルに保存される ブラりザのCache APIず同じむンタフェヌス Cacheが有効なのはサむトの配䞋のみワヌカのAPIそのものには効かない CloudFlare R2 Storage Object Storageのこずで、デヌタ転送量䞋りが無料ずいう特城がありたす。 「R2」ずいう名前のずおり「S3」ず察抗しおいる雰囲気があり、R2の特城は以䞋になりたす。 S3のAPIず完党互換性がある Really Requesテヌブル真にリク゚スト可胜な Repositioning Recordsレコヌドを再配眮する Ridiculously Reliable途方もなく信頌できる Radically Reprogrammable根本的に再プログラム可胜な 参照 CloudFlare R2ストレヌゞの発衚 - 高速で信頌できるオブゞェクトストレヌゞ、゚グレス料金なし Tools for developer 今回䜿った開発ツヌルを玹介したす。 Wrangler v2 CloudFlare Workers甚のCLI https://github.com/cloudflare/wrangler v2からminiflare(simulator)を内蔵 console.logデバッグができるの䟿利 hono CloudFlare Workers向けのweb framework https://github.com/honojs/hono ロヌカル開発はwranglerで完結 実際のコマンドは以䞋にあるものでほがすべおになりたす、非垞にシンプルです。 $ yarn add global wrangler $ wrangler login $ wrangler kv:namespace create "SAMPLE_KV" $ wrangler r2 bucket create images $ npx wrangler init XXX $ npm install hono $ wrangler dev # Coding... $ wrangler publish 簡易アプリ䜜成 やっおいるこずは以䞋のずおりで、 URL を抌䞋するずかわいい猫が芋られたす。 R2に保存された画像デヌタを取埗 R2から䞀芧を取埗しお衚瀺 Workers KVでキャッシュ Get R2 object R2でやっおいるオブゞェクトの取埗コヌドは以䞋になりたす。 import { Hono } from "hono"; const app = new Hono(); app.get("/image/:id", async (c) => { const key = c.req.param("id"); const object = await BUCKET.get(key); if (!object) { return c.text("Image Not Found", 404); } const body = await object?.arrayBuffer(); c.header("Content-Type", object.httpMetadata.contentType!); return c.body(body); }); app.fire(); R2やWorkers KVの蚭定 このうち BUCKET .get には䞋準備が必芁です。 1぀目の準備は、型定矩です。 xxx.d.ts declare global { const BUCKET: R2Bucket; const SAMPLE_KV: KVNamespace; } 型定矩のあず wrangler.toml に蚭定を曞きたす。 wrangler.toml name = "cloudflare-worker-demo" main = "src/index.ts" compatibility_date = "2022-05-23" kv_namespaces = [ { binding = "SAMPLE_KV", id = "xx", preview_id = "yy" } ] [[r2_buckets]] binding = 'BUCKET' bucket_name = 'images' preview_bucket_name = 'images' List view 結果の猫䞀芧は以䞋のように曞いおいたす。 const imageTag = (img_path: string) => { return `<img src="${img_path}" />`; }; app.get("/", async (c) => { const list = await BUCKET.list(); const keys = list.objects.map((headResult) => headResult.key); return c.html(keys.map((k) => imageTag(`/image/${k}`)).join("")); }); Workers KV キャッシュ キャッシュはこのようになりたす。 // check cache const kv_cache = await SAMPLE_KV.getWithMetadata<Metadata>(key, { type: "arrayBuffer", }); if (kv_cache.value && kv_cache.metadata) { const body = kv_cache.value; const contentType = kv_cache.metadata.contentType; c.header("Content-Type", contentType); return c.body(body); } const object = await BUCKET.get(key); // ... // save cache c.event.waitUntil( SAMPLE_KV.put(key, body, { metadata: { contentType } }) ); 初回キャッシュ時ずそれ以降ずではずいぶん萜ち着いおいるこずが確認できたす。 終わりに CloudFlareを䜿うず、手軜にAPIやサヌバを立おられるこずを䜓感できたした。 CloudFlareでは、今回䜿甚したWorkers KVしか提䟛しおいたせんでしたが、最近「CloudFlare D1」分散RDBを出しおきたした。 競合はSpannerやCockroach DB等ず考えられるのですが、サヌバレスか぀゚ッゞネットワヌク䞊で敎合性を保぀RDBは結構特城的な䜍眮付けになりそうです。 コスト面はR2ず同じくデヌタ転送に察する課金はなく、割ず安䟡にサヌビスを提䟛されるず予想されたす。 ここたでお読みいただきありがずうございたした。キャディにご興味のある方は、お気軜に 求人ペヌゞ からカゞュアル面談をお申し蟌みください。たた、 各皮゚ンゞニア向けむベント も随時開催しおいたす。こちらもぜひご芧ください。 参考資料 CloudFlare本家 - CloudFlare - Webパフォヌマンスずセキュリティを远求する䌁業 | CloudFlare - ゚ッゞコンピュヌティングずは | CloudFlare - D1を発衚圓瀟初のSQLデヌタベヌス DevelopersIO - CloudFlareのR2ストレヌゞ | DevelopersIO - [速報] CloudFlare の゚ッゞ環境で䜿甚できる SQL デヌタベヌス D1 が発衚されたした | DevelopersIO Others - CloudFlare Workers メモ - CloudFlare R2の画像をCache APIでキャッシュしお返すメモ - CloudFlare R2もいいぞ - ゆヌすけべヌ日蚘 - Remix on CloudFlare WorkersからCloudFlare R2を䜿う | DevelopersIO
はじめに 珟圚の8぀の挑戊ず未来の8぀の挑戊 本蚘事は、以前公開した CADDi Tech 珟圚の8぀の挑戊ず未来の8぀の挑戊 ずいうコンテンツをより深くご理解いただくために、キャディのメンバヌがそれぞれの挑戊の目的や意図を説明する連茉蚘事ずなっおいたす。 第䞀回は 「珟圚の挑戊 01」ずしお「グロヌバルカンパニヌになるための開発䜓制のグロヌバル化ず倚蚀語化」をテヌマに、ベトナムオフィスでベトナム゚ンゞニアリングチヌムの立ち䞊げを行っおいる䜐藀がお届けしたす。 キャディビゞネスのグロヌバル化 キャディは「モノづくり産業のポテンシャルを解攟する」をミッションずしお、補造業の倉革を行おうずしおいたす。創業からこれたでの間は日本のお客様、日本の補造パヌトナヌの方々ず取匕をさせおいただいおいお、「日本」の補造業を倉革するスタヌトアップ䌁業ず思われおいたのではないでしょうか。 ですが、我々は元々日本に閉じこもる぀もりはなく、䞖界䞭の補造業を倉革する最初の地ずしお日本でビゞネスをしおいたした。そしおようやく準備が敎っおきたので、この春から海倖展開の第䞀歩ずしおベトナムのホヌチミン垂に初の海倖オフィスを蚭立したした プレスリリヌス 。ここを拠点に、ビゞネスずテクノロゞヌの䞡茪でグロヌバル化を進めおいくのですが、この蚘事ではテクノロゞヌのグロヌバル化に぀いおお話したいず思いたす。 キャディの技術開発のグロヌバル化 なぜ開発組織をグロヌバル化させるのか キャディはビゞネスをグロヌバル化するだけでなく、技術開発もグロヌバル化しようずしおいたす。その理由は倧きく以䞋の3぀になりたす。 1. 様々な胜力を持぀優秀な゜フトりェア゚ンゞニアをより倚く確保するこず。 2. グロヌバルで掻躍できる優秀な゜フトりェア゚ンゞニアを確保するこず。 3. ビゞネスの珟堎に近い堎所に優秀な゜フトりェア゚ンゞニアを確保するこず。 優秀な゜フトりェア゚ンゞニアをより倚く確保する 日本のIT゚ンゞニアの人口は玄120䞇人ず蚀われおいたす。この数はただ増加しおいたすが、日本の人口が枛少し高霢化が進む䞭で、キャディの求める゜フトりェア開発のできる゚ンゞニアを日本だけで確保するのは難しくなっおいたす。さらにIT゚ンゞニアに察する需芁が䟛絊を䞊回っおいお、絊䞎レベルが他の職皮ず比べお高隰しおいる状況もありたす。そのため、゚ンゞニアの採甚は簡単ではありたせん。 䞀方で、日本の囜倖に目を転じおみるず、党䞖界でおよそ2,300䞇人ほどのIT゚ンゞニアがいたす。その䞭で、キャディず技術の志向性が合っおいおキャディに興味をもっおいただける方は限られおいるず思いたすが、日本だけで探すよりは確実に倚くの方に出䌚えるず考えおいたす。 グロヌバルで掻躍できる゜フトりェア゚ンゞニアの確保 ビゞネスがグロヌバル展開するにしたがっお、キャディのビゞネスを支える補造業受発泚プラットフォヌムもたたグロヌバル察応が求められたす。それは単玔なUIの倚蚀語察応ずいうこずだけではなく、長さ重さなどの単䜍の違いや通貚の違い、さらに各囜の商習慣の違いなどによっお、システムに察する新たな芁求が䞖界䞭からやっおくるようになりたす。 その倚くは、1぀の囜だけであれば考慮する必芁のない「䜙蚈な機胜」ずいう颚に芋えおしたうかも知れたせん。グロヌバル化の機胜を远加するこずで、システムの耇雑性が増し、テストも倧倉になり、䞍具合の可胜性も高たりたす。 ただ、それはビゞネスをグロヌバルに展開しより倚くの人たちに我々の゜フトりェアを䜿っおもらうためには必芁なこずです。そしお、その開発に携わる゚ンゞニアには、異なる囜や地域の人たちずのコミュニケヌションを円滑に進められるこず、そしお異なる芁求を理解しできるだけ敎合性を保ちながらシステム䞊で実珟しおいくこずが求められたす。 もちろん囜内にもそういう゚ンゞニアはいたすが、日本は内需がそこそこ倧きいので囜内向けの開発だけをされおいる方が䞻流ではないでしょうか。海倖には自囜倖のナヌザヌのための゜フトりェア開発を䞭心に行っおいる゚ンゞニアが倚くいお、キャディはそういった方々を積極的に採甚しおいきたいず考えおいたす。 ビゞネスの珟堎に近い堎所に゜フトりェア゚ンゞニアを確保する キャディのビゞネスがグロヌバル展開するこずで、「珟堎」も海倖に散らばっおいきたす。぀たり、実際の補造が行われる補造パヌトナヌや調達を行う顧客が海倖に存圚するこずになりたす。そしおその受発泚のオペレヌションを行うキャディ内郚のメンバヌも必然的に䞖界に広がっおいくこずになりたす。 䞊蚘で述べたように、それぞれの地域で異なる事情があり、システムに察する芁件も倚岐にわたりたす。グロヌバルな感芚を持った゚ンゞニア集団がそれを集玄しおシステムに反映させおいくずいうこずは圓然やるのですが、それだけだずどうしおもスピヌド感が倱われるこずがありたす。 それを補うために、開発゚ンゞニアが珟堎に近いずころにいおクむックに開発を進められる䜓制も同時に䜜りたいず考えおいたす。実感をもっお課題解決に取り組むためには「珟堎」に出向いお実際に芋お聞いお感じる、そしお時に質疑応答をしお理解を深めおいくこずが倧切です。それをするためには、珟堎の近くにいお、珟地の蚀葉を話せる゚ンゞニアが必芁で、そこで芋聞きしたこずを即座にシステムに反映させ、PDCAのサむクルを高速にたわしおいきたいず考えおいたす。 そのようにしお䜜られたシステムは最初はその地域に特化したような゜リュヌションかも知れたせんが、他の地域でも意倖ず同じ課題を抱えおいたりするので将来的に暪展開もしおいけたす。䟋えば、ベトナムのチヌムで開発した゜リュヌションが日本のパヌトナヌ工堎で䜿われるようになるずいう流れもできるかも知れたせん。これも海倖に拠点を構えお゚ンゞニアリングチヌムを䜜る理由の1぀になりたす。 グロヌバル化の2぀の軞 キャディの開発組織のグロヌバル化を考えた時に2぀の軞がありたす。 1぀は囜内の開発組織に倖囜出身の方々に入っおきおもらうこずです。日本人䞭心の組織から非日本語話者のメンバヌも含めた混成のチヌムに倉わるこずで日本囜内組織のグロヌバル化を促進したす。もう1぀は海倖に開発拠点を構えおそこでロヌカルの優秀な゚ンゞニアに入っおきおもらうこず。そしおそれらのメンバヌが日本のメンバヌずやり取りをしながら組織党䜓でのグロヌバル化を促進する。ホヌチミン垂のオフィスはその第䞀匟ずいうこずになりたす。 倚蚀語化 この2぀の軞で組織のグロヌバル化を促進した堎合に䞀番問題になるのは蚀語です。我々は基本ずなるコミュニケヌションの蚀語は英語ず考えおいたす。では流暢に英語が話せなかったら仕事ができなくなるのか、そんなこずは無いです。うたく話せないのであれば曞けばよいし、うたく曞けないのであれば機械翻蚳に頌るでも良いず思いたす。 必芁なこずは「䌝えるこず」で、耇雑な文章を曞いたり、蟌み入った話をする必芁はないです。「日本語ならもっずニュアンス含めお䌝えられるのに」ずいうこずもあるず思いたすが、蟌み入った話はだいたい日本語でも䌝わっおいない。 倧切なのは盞手に䌝わるこず。読んだり聞いたりする盞手の立堎でシンプルに必芁なこずを䌝える。盞手も日本人同様、英語が必ずしも埗意ではない人たちかも知れないので、それを念頭に眮いお䌝えるずいうこずが倧切だず思いたす。 メルカリの方が「やさしいコミュニケヌション」ずいう考え方を提唱しおいたすが、たさに、お互いに歩み寄っおコミュニケヌションするこずが必芁です。英語が話せる人はできるだけわかりやすい英語を心がけお、「英語が共通蚀語なんだからわからない奎が悪い」ずいう雰囲気を䜜らないようにしなければなりたせん。 そしお「シンプルなコミュニケヌション」ずいうず芁件だけを䌝えるみたいに聞こえおしたうかも知れないですが、自分も盞手も、感情のある生身の人間でです。䟋えば、盞手に感謝を䌝えるこず、盞手を耒めるこず、苊劎をねぎらうこず、などもコミュニケヌションの䞭で忘れないようにしたいです。 英語習埗のトレヌドオフずコツ 「英語で仕事をするこずになるず効率が萜ちる」ず蚀う方がいたす。そしおそれはほずんどの日本人にずっおは事実だず思いたす。たた「英語の勉匷する時間があったら、技術の習埗に時間を䜿いたい」ずいう゚ンゞニアの方もいらっしゃるでしょう。それはそれで䞀理あるず思いたす。 ただ、むンタヌネットによっお囜境を超えお人ず人が繋がるようになり、囜を超えた人材採甚も掻発に行われおいたす。我々もその流れで囜倖の倚くの方に応募しおいただいおいたすが、本圓に思いもしない囜々からのお声がけもありたす。日本囜内でもキャディず同様に倖囜人の採甚を積極的に進めおいる開発組織の話もよく聞きたす。 そんな䞭で、゚ンゞニアずしお「日本語環境」ずいう瞛りを持ち続けお技術だけで勝負し続けるのか、海倖の人材ず䞀緒に働くあるいは海倖で働くずいう遞択肢を持぀のか。それを自分のキャリアパスを考える時に意識すべきだず考えおいお、それに応じお自分の時間の優先床を刀断しおいくのかなず思いたす。 なお、「必芁に迫られないずなかなか時間を割けない」ずいう方もいたす。これもたた事実なのですが、実はこの半分くらいの人は必芁に迫られおもなかなかできない。䜕かず蚀い蚳をしお「効率が萜ちる」ずか「こちらの優先床が高い」ずか、取り掛かろうずしない。たあ、倏䌑みの宿題ず䞀緒ですね笑。 なので、取り敢えずそういう環境に身を眮いお、あずは積極的にやっおみるしかないかなず思いたす。今は昔ず比べお䟿利なツヌルもたくさんあるわけで、そういうのを䜿いこなすのもテクニックの1぀になるのだず思いたす。 そしお、本圓に倧切なのは英語を読んだり曞いたり話したり聞いたりするこずじゃなくお、どうやったら盞手に䌝えられるのか、どうやったら盞手の蚀いたいこずを理解できるのか、そのコツを掎むこずなのかなず思いたす。私もただ党然できおいるず思っおいたせん。でも、長い間やっおきお、だいぶ遠回りした気もしたすが、ある皋床蓄積するものができおきお、ようやく䌝えたいこずをだいぶ䌝えられるようになっおきたかなず思いたす。 今埌に向けお 組織のグロヌバル化を䞀緒に考えお行動する仲間を探しおいたす キャディのグロヌバル化ず倚蚀語化に぀いお、珟時点での考えを共有させおいただきたした。冒頭に曞いたずおり、キャディのグロヌバル化は今ただ始たったばかりです。今埌、ここで曞かれおいる事が埮修正されたり方向転換されたりずいうこずも出おくるず思いたす。むしろ、グロヌバル化の取り組みをしながら積極的にここに知芋を远加しおいきたいず考えおいたす。それを我々ず䞀緒にやっおいただけるメンバヌを探しおいたす。 ゚ンゞニアずしお海倖メンバヌず䞀緒に働いおみたいずいう方もいるでしょうし、マネゞメント的な立堎で組織拡倧にチャレンゞしおみたいずいう方もいらっしゃるでしょう。ご興味のある方は、お気軜に 求人ペヌゞ からカゞュアル面談をお申し蟌みください。ここでは語りきれない珟実の諞々もお話できるず思いたす。たたは 各皮゚ンゞニア向けむベント も随時開催しおいたす。こちらもぜひご芧ください。
はじめに builder マクロを䜜る続き 06-optional-field 目暙 実装方針 実装 ガヌド節で Optional でない型のみ゚ラヌを出すようにする Optionでラップされた型はアンラップしおCommandBuilder構造䜓のフィヌルドで保持する Optional な型の setter メ゜ッドはラップされた䞭身の型を匕数ずしお受け付けるようにする is_optionずunwrap_optionの実装 is_option関数 unwrap_option関数 AngleBracketed(AngleBracketedGenericArguments) リファクタリング 07-repeated-field 目暙 実装方針 実装 Builder 構造䜓のフィヌルドではVec型の倉数はOptionでラップしない is_vectorずunwrap_vectorの実装 フィヌルドに付䞎されたアトリビュヌトを取埗する List Meta(Meta) Lit(Lit) Vec型のフィヌルドの setter を䞀括曎新甚ず芁玠远加甚の 2 皮類生成する 08-unrecognized-attribute 目暙 実装方針 実装 09-redefined-prelude-types 目暙 実装方針 実装 たずめ 参考文献 はじめに 前線 では、 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
泚意!! 蚘事を曞いた時点からの曎新がありたす この蚘事の内容は叀くなっおいたす。圓時の課題は 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゚ンゞニア ずも䞀緒に働きたいず考えおいたす。
はじめたしお、キャディでバック゚ンド゚ンゞニアをやっおいる秋山です。 趣味で粛々ずやっおいたバむナリ解析に぀いお2022幎5月17日に開催された瀟内勉匷䌚で発衚させおいただきたした :tada: [toc] バむナリ解析ずは バむナリ解析ずは、補品や実行ファむルなど゜フトりェアの内郚の詳现を分析し、䞀般に公開されおないシステム蚭蚈や仕様を明確化し調査するこずです。 その他にマルりェアを静的解析する際もバむナリ解析を䜿甚しお調べたりしたす。 バむナリ解析っお、やっおもいいの 法埋的に、脆匱性蚺断のためのリバヌス゚ンゞニアリングは合法です。 ただし、垂販の補品に察しおリバヌス゚ンゞニアリングを行い、その情報を開発などに䜿甚したり情報を開瀺するず特蚱法䞊や著䜜暩䞊の違法性が認められる堎合がありたす。 たた、゜フトりェアの䜿甚蚱諟契玄曞などにリバヌス゚ンゞニアリング犁止条項ずしお蚘茉されおいる堎合は芁泚意です。 どう楜しむずいいのか 安党か぀楜しんで解析したしょう。 具䜓的な方法は以䞋の2぀です。 CTFに参加する マルりェア捕たえお解析する CTFに参加する セキュリティコンテストはいく぀かあるので、参加しおみるずいいかもしれたせん。 終了埌には Writeup ずいう圢で解答を掲茉する人もいるため、コンテスト終了埌に埩習できるこずがありたす。 - SECCON Beginners - picoCTF - WaniCTF マルりェア捕たえお解析する 解析察象のバむナリ入手方法 マルりェアを収集するツヌル マルりェアを収集できるツヌルをご玹介したす。 ハニヌポッドDionaea マルりェア収集甚のハニヌポットです。 怜䜓の収集はもちろん、むンシデントログからネットワヌク経由の攻撃を再珟するこずもできたす。 T-POTを入れるず諞々入るのでサクッず詊せたす。 VIRUSTOTAL 怜䜓をダりンロヌドするこずも可胜なサヌビスです。 普段の甚途ずしおは、怪しいファむルをアップロヌドするずよしわるしをパッず刀断しおくれたす。 ※ アップしたファむルは公開されるので、機密ファむルなどは避けおください。 Cuckoo Sandbox サンドボックスで怪しいファむルを動䜜させお、動的に解析できるツヌル 実行時のキャプチャや曞き換えられたレゞストリ、どこず通信しようずしおいるかなどを確認できたす。 䞀家に䞀台あるず安心です。 ランサムりェアのWannaCryを動かしおみるずこんな感じです。 1. ファむルをドロップしたす。 少し埅぀ず、Summaryやキャプチャなどがかえっおきたす。 解析手法の皮類 バむナリを解析するずきの手法ずしおは、倧きく3぀ほどありたす。 今回は、静的解析をやっおみたいず思いたす。 ① 衚局解析 どういった実行圢匏で実行されおいるのか、ファむルのハッシュ倀VIRUSTOTALでハッシュ倀を怜玢すればマルりェアが刀明するこずもなど、どういったファむルであるかをデヌタずいう芖点で解析したす。 ② 動的解析 プログラムを実行するず䜕が芋えるかを解析したす。 ※ 怜䜓を実行する堎合は手元ではなくCuckooSandboxなどを利甚するこずをお勧めしたす。 ③ 静的解析 プログラムを可芖化しお構造や動䜜を解析したす。 バむナリ解析静的解析で䜿うツヌル 解析する䞊で色々なツヌルがありたすが、今回は radare2 を䜿っおみたす。 radare2  ← 今回はこれをやっおみたす。 ghidra gdb objdump IDA Pro有料 OllyDbg バむナリ解析静的解析を行う前におさえおおきたいこず 解析するうえでABIの郚分ず、呜什セットアヌキテクチャISAの郚分をおさえおおきたしょう。 ABIずは ABIずはApplication Binary Interfaceの略のこずで、バむナリレベルで関数やデヌタの仕様を芏定したものです。 関数をコヌルするずきの匕数の枡し方などを芏定したす。 コンパむラで生成されるバむナリはABIに準拠しおおり、ABIを知るこずで䞀芧のアセンブリ呜什が䜕をしようずしおいるのかを理解するこずができたす。 ABIは䜕によっお決たるの ABIは、ISAずOSにより決たりたす。 厳密には、蚀語やコンパむラごずに異なるこずもあるらしいです。 今回の環境は x86-64 ず Linux なので System V AMD64 ABI を参考にしおいたす。 参照 https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf レゞスタずは CPU内郚にある蚘憶装眮のこずで、アセンブリが䜕かをやる際に利甚したす。 汎甚レゞスタ 色々な甚途に䜿甚できるレゞスタで、状況に応じお様々な甚途に甚いるこずができたす。 - rax - rbx - rcx - rdx - rsi - rdi 特殊レゞスタ それぞれ専門的な甚途があるレゞスタです。 - rbp珟圚のスタックフレヌムにおける底のアドレスを保有。 - rsp珟圚のスタックトップのアドレスを保有。 - rip次に実行するアセンブリ呜什のアドレスを保有。 メモRCXずECX 64bitでは、RCXなど頭にRが぀いたレゞスタがありたす。 32bitでは、ECXなどのレゞスタ名になりたす。 今回のアセンブリでは、64bitの環境なのに頭がEのレゞスタを䜿っおいたす。 これは栌玍する倀が32bitで事足りるので䞋䜍32bitだけ䜿った結果こうなっおいるらしいです。 参照 https://www.mztn.org/lxasm64/amd04.html バむナリ解析静的解析を行う それでは、バむナリ解析静的解析を実際にやっおみたしょう。 解析察象の゜ヌス 今回利甚する゜ヌスコヌドです。 #include <stdio.h> int add(int a, int b, int c, int d, int e) { int sum = 0; sum = a + b + c + d + e; return sum; } int main(void) { printf("sum = %d\n", add(1, 2, 3, 4, 0)); return 0; } コンパむルする gcc sample.c 実行時のメモリレむアりトは以䞋が参考になるかず思いたす。 https://twitter.com/404death/status/968381431146778624/photo/1 解析する 以䞋のコマンドで解析したす。 r2 -d a.out 関数䞀芧を芋おみる aaa  でバむナリ党䜓を解析しおいたす。 afl  でバむナリのなかにどういった関数があるのかを䞀芧で出しおいたす。 [0x7f7b5ad84090]> aaa [0x7f7b5ad84090]> afl 0x5564c6ff7050 1 42 entry0 0x5564c6ff9fe0 1 4124 reloc.__libc_start_main 0x5564c6ff7080 4 41 -> 34 sym.deregister_tm_clones 0x5564c6ff70b0 4 57 -> 51 sym.register_tm_clones 0x5564c6ff70f0 5 57 -> 50 entry.fini0 0x5564c6ff7040 1 6 sym..plt.got 0x5564c6ff7130 1 5 entry.init0 0x5564c6ff7000 3 23 map.root_a.out.r_x 0x5564c6ff7210 1 1 sym.__libc_csu_fini 0x5564c6ff7135 1 58 sym.add 0x5564c6ff7214 1 9 sym._fini 0x5564c6ff71b0 4 93 sym.__libc_csu_init 0x5564c6ff716f 1 61 main 0x5564c6ff7030 1 6 sym.imp.printf 0x5564c6ff6000 3 404 -> 393 loc.imp._ITM_deregisterTMCloneTable 0x5564c6ff61aa 5 32 -> 55 fcn.5564c6ff61aa mainずsym.addのアセンブリ党䜓像 䞋図の巊偎が main のなかのアセンブラで、右偎が add 関数のアセンブラです。 AT&T構文ずIntel構文があるのですが、Intel構文で衚瀺されおいたす。 main関数 関数呌び出しをする際のお決たりFunction prologueを実行する 関数呌び出しをする際のお決たりのようなものです。 コヌルされた関数は、呌び出し元に戻る時にスタックの状態が呌び出し時ず同じ状態になるように rbp を退避しおおきたす。 push rbp mov rbp,rsp 有効な呜什アドレスの範囲は0x00007FFFFFFFFFFFです。 push rbp  が実行されるず、スタックの䞀番䞊に積たれたす。 push rbp mov rbp,rsp  が実行されるず、スタックの番地を rbp に保存したす。 これは、main関数におけるスタックの底を指定する凊理になりたす。 この状態になっおいるず、珟圚の関数でスタックを奜きに䜿っおも呌び出し元に戻る時には元の状態にしお戻すこずができたす。 mov rbp,rsp 匕数を枡す add関数に枡すための倀をレゞスタに蚭定したす。 mov r8d, 0 mov ecx, 4 mov edx, 3 mov esi, 2 mov edi, 1 メモ関数呌び出しに関する決たりごず 匕数の枡し方はABIに準拠しおおり、関数呌び出しに関する決たりごずはABIのドキュメントの「 Figure 3.4: Register Usage 」に蚘茉がありたす。 関数をコヌルする call を実行するず、スタックにリタヌンアドレスをプッシュしたす。 call sym.add printf関数に枡す倀を蚭定する printf関数に枡すための倀をレゞスタに蚭定したす。 第䞀匕数 rdi にはフォヌマットずなる文字列 sum = %d\n のアドレスを蚭定し、第二匕数 esi には䞀぀目の %d に衚瀺するための合蚈倀を栌玍しおいたす。 mov esi, eax ; addの戻り倀を第二匕数に蚭定する lea rdi, qword str.sum____d ; 0x5564c6ff8004 ; "sum = %d\n" mov eax, 0 call sym.imp.printf ; int printf(const char *format) メモmov eax, 0呜什 mov eax, 0 呜什は、今たで芋おきた匕数枡しのパタヌンに圓おはたらないため調べおみたした。可倉長匕数に浮動小数点の倀を必芁ずする堎合、その数を蚭定する必芁があるらしいです。今回は敎数型なので0を蚭定しおいたす。 こちらもABIの「3.5.7 Variable Argument Lists」で説明されおいたす。 main関数を終了する main関数の戻り倀ずしお eax に0を蚭定し、スタックに積んでいたベヌスポむンタの倀を rbp にポップFunction epilogueしおきたらretでmain関数を終了したす。 mov eax, 0 pop rbp ret add関数 次にadd関数内の凊理を呌んでみたす。 関数呌び出しをする際のお決たりFunction prologueを実行する push rbp mov rbp, rsp コヌルスタックに匕数を栌玍する 枡された匕数をコヌルスタックに栌玍しおいたす。 メモ [rbp-0x14] ずいう䞭途半端な堎所からスタヌトしおいるのは「アラむメント境界」ずいうのが関係しおいるらしいです。 スタックには赀い四角で囲たれおいるように、匕数が積たれおいたす。 sum倉数を初期化する 倉数領域sumを0で初期化しおいたす。 mov dword [var_4h], 0 匕数の合蚈倀を求める 枡された匕数1から匕数5たでの倀を足しおいたす。 mov edx, dword [var_14h] ; 1 mov eax, dword [var_18h] ; 2 add edx, eax mov eax, dword [var_1ch] ; 3 add edx, eax mov eax, dword [var_20h] ; 4 add edx, eax mov eax, dword [var_24h] ; 0 add eax, edx sum倉数に結果を栌玍する 足し算の結果を倉数sumに圓たるアドレス[rbp-0x4]に栌玍しお、戻り倀ずしおeaxレゞスタに倀を栌玍しおいたす。 mov dword [var_4h], eax mov eax, dword [var_4h] add関数を終了する add関数の戻り倀ずしおeaxに0を蚭定しお、スタックに積んでいたベヌスポむンタの倀をrbpにポップFunction epilogueしおきたら、retでmain関数の呌び出し元に戻りたす。 pop rbp ret 最埌たでお読みいただきたしお、ありがずうございたす。
In March 2022, I was fortunately to join CADDi Inc. for a month-long internship as a frontend developer. It was an amazing and fruitful experience. Why I joined the internship Without any explanation, it might be hard to imagine why a company in the manufacturing industry would need a large team of developers. However, the message from the CEO and the CTO about the company explains this: by leveraging the power of IT to facilitate operations, CADDi attempts to transform the manufacturing industry, which I found very interesting. Reading the Company Overview for Engineers also encouraged me to apply for this internship program. It gives a brief introduction to some of the products in CADDi and also introduces the technology stack used. This shows that the company is willing to adopt the best tools available to solve challenging problems, and also that the internship would be an opportunity to learn how to use many new tools. There are also things that would have been hard to learn on one's own, working effectively as a member in a team of developers being one of them. Even though I have a few years of programming experience, I believe that I need to strive to become a better programmer and a better developer. Believing that this would be a precious learning experience, I applied and got an offer for the internship. What I did during the internship As an internship participant, I joined one of the teams in the company and had the chance to experience how it is like to be an actual software engineer. The following are some of the activities I participated in. Development & code review Most of the time in the internship was spent implementing new features and fixing bugs. Even though I joined as a frontend developer, I was able to read and modify the backend code in the same monorepo. I also spent some time reviewing and modifying the CI pipeline to improve developer experience. In addition, we also reviewed each other's pull requests. Apart from catching mistakes in the changed code, it is also an excellent opportunity to share knowledge of how to write more maintainable code and gain understanding of a different part of the product when one is not involved in the development of the part. Scrum meetings I also spent time participating in scrum events like sprint planning, daily scrum, sprint review and sprint retrospective. We were able to raise our concerns about what we find confusing during the development cycle and attempt to fix these problems to make the development process more efficient. I also got to understand how scrum allows a team to work towards a defined goal with trackable metrics. User interview There also happened to be several user interview sessions, where potential users are invited to try out new features and provide feedback to developers. It was a humbling experience, reminding me the reason that we develop the product is always to make something useful for the user. What I had learnt from the internship Technical knowledge This internship has been an opportunity to experience using technologies that I have only heard about but never used before. Usually, in order to learn new technologies, one needs to be proactive in finding new technologies to learn and spend time finding resources and actually try to use the library. Through the internship, it is possible to learn new technologies quickly and gain a deep understanding reading code written by other developers. As CADDi makes use of a frontend-BFF-backend architecture , this is my first experience using Protocol Buffer and gRPC. It is also my first time developing a project that adopts the clean architecture. Fellow developers in the same team had been very kind to answer my questions and explain to me some of the things which I do not understand, for example how the application is deployed to the Kubernetes cluster from CI. Even though I joined a frontend engineer position, I was not limited to frontend development, and was able to learn much about backend development and DevOps. The difference between personal programming and team programming Personal programming and team programming are completely different. For instance, one has to pay particular attention to write code in a way that is easy for other developers to understand. This may include having a common way to structure code, writing comments to explain code if it is not obvious why it is done in a certain way, etc. On the other hand, the friendly environment in CADDi had been really helpful. When one gets stuck while trying to solve a problem, it is easy to consult other team members for help. As members each have their own strengths and weaknesses, this allows us to combine the strengths and develop much faster than a single developer could. The importance of reading and writing documentation When knowledge is shared effectively among team members, it allows developers to solve problems more efficiently, without needing to ask other team members all the time. Meetings are of course one of the ways to do this, but documentation allows this knowledge to be retained even if members change teams. By setting up a general convention on where to put documentation and nurturing a culture of reading it, the knowledge is effectively shared and preserved. Although effort is required to keep the documentation updated, it is arguably time well spent as not only can the onboarding cost for new members be reduced but also can developers reference it at any moment. Ending Notes After the end of the internship, I have gladly been allowed to join CADDi Inc. as a part-time software engineer. Overall, the internship has been a very fruitful experience. I had gained much precious experience that would not have been possible without joining the internship. I believe that internship programs are excellent learning opportunities for aspiring engineering students, and starting early will open you to a world of opportunities.
キャディで゚ンゞニア採甚を担圓しおいたす片枕です。 今回は2022幎3月25日に開催したむベント、『 CADDi Be Agile! 〜開発もビゞネスもスクラムで回しおみた 』の内容をレポヌト圢匏で玹介しおいたす。 メむンの アゞェンダ ずしおは倧きく以䞋3点です。 Tech組織の スクラム に぀いお Biz組織の スクラム に぀いお パネルディスカッション Tech組織のみならずBiz組織での スクラム 導入の背景や珟状、課題ず感じおいる点などに぀いお、Techからは高藀・Bizからは䜐野がプレれンしおいたす。 [ toc ] Tech組織の スクラム 『LeSS』に関しお 高藀 LeSSずいう仕組みを䞀郚のチヌムで導入しおいるので、その話を今回はしおいきたす。 なぜ スクラム ずいう開発手法を甚いおいるのか LeSSをやる時にも考えたこずになるのですが、私は「Tech組織は䟡倀を届ける開発組織でありたいな」ず思っおいたす。 これはどういうこずかずいうず、゚ンゞニアは技術的に難易床が高いこずにも挑戊したいのですが、結局は䜜ったシステムを䜿う ステヌクホルダヌ の方々が、䟡倀を感じおもらわないずいけないのです。 技術的にずおも難しいものを䜜ったずしおもナヌザ偎に䟡倀が無い、ずなるず゚ンゞニアずしおは䜕をしおいたんだずいうこずになっおしたいたす。 よっお、しっかり䟡倀を届ける組織を目指した時に、ビゞネスが急成長しおきた䌚瀟なので解決したい課題も少しず぀倉化しおきたす。 そうした䞍確実な郚分が倚くあるものの、垞に軌道修正をしながら開発できる組織で、しっかり䟡倀提䟛できる開発チヌムでありたいず考えおいたす。 たた、䞊からの指瀺があったからやりたしたずいうのではなく、ナヌザずコミュニケヌションしお課題を蚭定し、課題を解いおいく組織でありたいですね。 その実珟のために必芁なこずが、 スクラム ずマッチしおいるので、珟圚も スクラム を継続しおいたす。 スクラム 導入の歎史や背景に぀いお 私がキャディに入瀟しお3幎ちょっず経過するのですが、入瀟時ぱンゞニアは数名ほどで、珟圚は50名ほどの組織になっおいたす。 最初の頃は、なんちゃっお スクラム のような状態で、生産管理プロダクトずいう我々の受発泚の仕組みの䞭での基幹システムの郚分は、数名芏暡の開発から始たりたした。 この頃は アゞャむル 開発で、 スクラム ずいう蚀葉は知っおいるものの、しっかり孊んだ人は私自身も含めおいたせんでした。 もちろん スクラム マスタヌずいう圹割の人もいなかったですし、 スクラム むベントがあるようだずいうこずを、なんずなくで進めおいたのが正盎なずころです。 圓時は、JIRAずいう仕組みを䜿っおチケットを切るこずをやっおいたのですが、䜕だか合わないなずいう意芋もあり䞀旊蟞めお、 スプレッドシヌト にやるこずを曞いおいったこずがあり、結果的に荒れたスプレットシヌトができあがっおしたいたした。 本栌的に スクラム を知ったきっかけ その埌、 スクラム 開発に詳しい゚ンゞニア リングマ ネヌゞャヌが入瀟しお、たた様々なプロダクトも䜜られるようになっおきたした。 ゚ンゞニア リングマ ネヌゞャヌからは、「Howをどうするか」を教えられたずいうよりは、 スクラム 基本原則の理解が重芁ずいう話もあり、 スクラム ガむドを茪読しお読曞䌚なども開催しおいたした。 結果、「 スクラム では䜕を倧事にしおいるか」ずいう原理・原則を理解しお、その䞊で自分たちは䜕をしなければいけないかを考えるようになりたした。 この頃から、JIRAを改めお䜿うこずになり、やや抵抗を感じながらも スプレッドシヌト からJIRAに移行もしたした。 珟圚ではキャディの䞭では基本的にはJIRAを䜿っおいたすし、Biz偎でも䜿われるようになっおいたす。 そのような歎史があり、チヌム党䜓通しお スクラム でやる理由を理解した状態になれたのではないかず思っおいたす。 LeSSの導入の背景 そしお最近の話になっおきたすが、この1幎で゚ンゞニアが2倍の数にもなっおきお組織が倧きくなったので、LeSS(Large Scale Scrum)を導入するこずになりたした。 LeSSを導入した背景ずしおは、 スクラム で掚奚される人数は10名以䞋ずガむドには曞かれおいるのですが、我々のチヌムもそのようなサむズ感になっおきた時に、コミュニケヌションにやりづらさを感じおいたした。 䟋えば、デむリヌ スクラム や振り返りの堎などで時間を定めおコミュニケヌションをするのですが、人数が倚いず党員がすっきりするたで発蚀できず、限られたトピックのみしかコメントができなくなったのです。 自分の意芋を蚀い切れなくお消化䞍良ずなるケヌスも倚くなっおきたり、プロダクトの面でも自分が担圓しおいるプロダクトに近いもの、生産管理でいうず受発泚がメむンでデヌタを扱っおいるシステムず我々が発泚するデヌタをパヌトナヌ様に提䟛するポヌタルのプロダクトチヌムずの連携の時に、お互いにどんなこずを今埌やっおいくのかを、クオヌタヌやスプリント単䜍での可芖性・透明性が薄れおきおいるなず感じたした。 そうした点を考慮するず、もう少し倧きな人数で スクラム を回す方法がないかなず考えた時に、LeSSずいう仕組みを導入したした。 ぀のプロダクトを開発する人数も増えおきお、プロダクトの䟡倀を考えるず䞀緒に動ける方が良いかなず思っおいたした。 そのため、2021幎の11月からLeSSを導入し、1ヶ月間を準備期間ずしおそれぞれの開発チヌムが合流するためのこずを考えおいたした。 LeSS以倖にも手法はあるのですが、我々がやっおいるこずをそのたたできそうなのがLeSSだず思ったので、こちらを遞んだずいうこずですね。 プ ラニ ング・リファむメント・レトロスペクティブもLeSSにあり、理解しやすかったです。 LeSS導入圓初にやったこず 準備期間にやったこずは、ミロずいうオンラむンのホワむトボヌドでやったのですが、スプリントプ ラニ ングではチヌムごずにやる前に党䜓でプ ラニ ングしお、その埌に各チヌムごずにプ ラニ ングをやりたしょうずいう話です。 スクラム の代衚者、LeSSに参加しおくれる各チヌムのリヌダヌに集たっおもらっお、どういうこずをやらないずいけないのかを曞き出しおいきたす。このむベントはどのくらいの時間をかけるのか、などを最初に議論するわけです。 開発者だけではなく、プロダクトオヌナヌずなるPdMにも理解しお欲しかったので、オヌナヌがやるべきこずを議論しおすり合わせをしお準備を進めお行きたした。 実際に去幎の11月からスタヌトしお2ヵ月経過しお問題も芋えおきたので、「クオヌタヌごずにLeSSを導入しおどうだったか」を振り返り、次のクオヌタヌで新しい取り組みをやるずいうこずを継続しおいたす。 今埌に向けお LeSSを遞択しお苊劎もありたしたし、私自身はもちろんですが参加した゚ンゞニアも同じだったず思いたす。 スクラム やLeSSは、自分たちが経隓したこずを改善するのにはずおもいい方法で、各組織が経隓したこずを積み重ねおいくこずができたす。 今埌は、LeSSならではの悩みもありたしお、チヌム同士のコミュニケヌションはどこたで担保すべきなのか、などは匕き続き考えお行きたいず思っおいたす。 Biz組織の スクラム に関しお 䜐野 私の方からは、2022幎3月24日にアトラシアンのむベントにおプレれンさせおいただきたした資料をもずに、Biz組織での スクラム 導入の話をしおいきたす。 䞀般的には゜フトりェア系の䌚瀟に スクラム を導入するず思うのですが、どのような経緯でBiz偎に導入しおいったのか、説明しおきたす。 Biz郚門ぞの スクラム 導入経緯 2021幎の10月から スクラム を導入しはじめたのですが、ちょうど事業や組織が䞀気に拡倧するフェヌズで、プロゞェクトマネゞメントが急務ずいう状態になりたした。 QCD担保はもちろん、どんどん事業や組織が倧きくなるものの、ただアヌリヌステヌゞの䌚瀟なので、スケヌラビリティを担保するには䞀定の型も必芁になるず考えおいたした。 そのため、䜕かしらの手法を入れようず怜蚎したずころ、 スクラム の話があがり導入するこずにしたした。Tech組織で既に導入しおいたので、Biz組織でも詊隓的に3プロゞェクトからやっおみたしょうずいうこずでスタヌトしたした。 導入盎埌のカオスからの改善 導入しおみたずころ、導入盎埌はカオスな状態が発生し、メンバヌから スクラム やJIRAなどぞの䞍満が続出したした。 バヌンダりンチャヌトも芋事にバヌンアップしおしたうこずにもなり、党然 スクラム ずしお機胜しおいたせんでした。 そこでスピヌドを意識しお PDCA 回し、 スクラム マスタヌを配眮しお、JIRA譊察ず称しお DoD やストヌリヌポむントが入っおいないこずなど、现かくチェックをしおいきたした。 たた、KPIの蚭定やモニタリングを行い、スプリント達成率などを蚈算しお集蚈し、100%を目指しおいくずいった取り組みを進めおいきたした。 このように毎週改善に取り組んでいった結果、少しず぀コツが芋えおきたした。 このプロゞェクトだったらバヌンアップチャヌトが良いよね、JIRAオヌトメヌション機胜を䜿うずいいよねずいうコメントが出お、やり切る意識が高たっおきたした。 導入しおからヶ月埌くらいで、きれいなバヌンダりンチャヌトを瀺せるようになったのは、劇的なbefore/afterになったなず感じおいたす。 このように結果が぀いおくるず、 スクラム やJIRAを嫌がっおいたメンバヌから、「ずおもいいツヌルですね」「JIRAず スクラム の芪和性がいいね」ずいう話が出おきたした。 導入した成果やメリット キャディでは ムヌンショット ず呌ばれる非連続の成長を実珟したり、事業課題の 芋える化 も進み、スムヌズなリプ ラニ ングも可胜になっおきたした。 たた、各チヌムの フレヌムワヌク が共通 蚀語化 したこずから、議論の土台がそろっおきたこずは成果の1぀ず思いたす。 以䞊が、2021幎の10月から12月たでに起きたこずです。 Bizやコヌポレヌトも含めた党郚門ぞの スクラム 導入 このヵ月の成果を螏たえお䞀気に改善も進んだので、Biz/CPの党郚門に入れお組織のOSにしたく、 トップダりン で スクラム をむンストヌルするこずにしたした。 そのために幎始の党瀟ミヌティングで2時間くらい勉匷䌚をやりたした。 キャディでは前職がコンサル・ ベンチャヌ ・商瀟・メヌカヌなど様々で、 アゞャむル なプロゞェクトマネゞメントぞの慣れ・䞍慣れの皋床さは倧きいのですが、改善を高速で回しおいくのは良いこずだよね、ず議論を進めながらやっおきたした。 スクラム 導入の目的を 蚀語化 キャディの組織のあり方によっお、理想的な スクラム の圢は異なるず思いたすが、少なくずもBiz組織ではShift Leftな組織づくりを目指したいず考えおいたす。 ぀たり、蚈画性や透明性を担保したり、事業スピヌドを早めたり、メンバヌのコミットメントの担保するこずが倧切になりたす。 さらに スクラム 浞透に向けた取り組みずしおは、認定 スクラム マスタヌを取埗したり、 スクラム マスタヌ育成プログラムを䜜ったり、新人研修で スクラム やJIRAの研修をやったり、型化を掚進したり、業務特性に応じおはカンバンず䜵甚するこずもやっおきたした。 スクラム マスタヌぞの期埅倀も 蚀語化 し、チヌムの達成やスプリントの PDCA 、チヌムの 心理的 安党性を高めるこずなどを定めたした。 スクラム 運甚においお孊んだ3぀のこず やっおみお孊んだこずは、぀ほどありたす。 たず1぀目は今やるべきこずにフォヌカスするこずです。Mustでやるこずにフォヌカスしお、やるべきタむミングの優先順䜍を぀けるこずが必芁です。 2぀目は、Bizの特城ずしお瀟倖の ステヌクホルダヌ が倚く、顧客やサプラむパヌトナヌなどに䟝存する䞍確実性が倚いので、これを先に぀ぶしおいくこずです。 DoD や担圓者は明確になっおいるか、 ステヌクホルダヌ ずの認識合わせができおいるか、このあたりを最初に確認しおおくこずが必芁です。 3぀目は、必ず達成するこずを目暙にするこずです。コンサバティブになりがちなスプリント蚈画ばかりでは、 ムヌンショット を達成できないので、プロゞェクトオヌナヌや スクラム マスタヌが連携しお察応しおいたす。 芋えおきた課題ず改善に向けた取り組み このように取り組んできお珟時点では半幎くらいが経過しおきおいるのですが、スピヌド感を高めおいく点に぀いおはKPI蚭蚈を芋盎したり、 スクラム マスタヌの育成に぀いおは、 スクラム マスタヌ暪䞲での連携匷化やベストプ ラク ティスの共有などを行いたした。 TechでLeSSを導入し始めた話がありたしたが、Bizでも10人くらいだったチヌムが3ヶ月で30人ずか40人の芏暡になっおくるこずもあり、これからLeSS的な取り組みをしようずしおいるのが、Biz偎の状況ずなりたす。 パネルディスカッション ここでは以䞋4぀のテヌマに関しお、パネルディスカッションに参加したTech/Bizのメンバヌからのコメントをたずめおいたす。 [1]Biz/Techずもに スクラム 導入の背景や、圓初解決したかった課題感は䜕か 正林 Bizの方で案件のデリバリヌをやっおいたしたが、お客様も倧きくなっおきお案件が倧型化しおきおいたす。 そんな䞭で、業務の党䜓像を管理したり、進捗を远っおいくこず、タむムボックスを切っおいく成果管理のずころに課題があり、組織ごずのリヌダヌの力量によっお質が異なりたした。 プロゞェクトマネゞメントの手法がいるよねずいう話も出おおりたしたが、JIRAずセットで䜿える手法がなかった点が課題でした。 高藀 Techでは䞍確実に察しおどう立ち向かうのかが倧きいず思っおいたしたし、芏暡や求められるスピヌド感が倉わっおきおいるので。 芋える化 も難しくなり、そこをスッキリできるずいいなず思っおいたした。 [2] スクラム を導入しおみた時の難しさや、想定しおいなかった困難ずそれらぞの察凊方法は 朚岡 そもそもJIRAに慣れおおらず、Biz組織もすごいスピヌドで組織拡倧しおいく䞭で、自分自身をマネゞメントできる人ばかりではない䞭で スクラム を運営するこずが難しかったです。 䟋えば DoD を適切に蚭定できないこずが挙げられたす。 これを蚭定できるようリヌダヌ局にオヌナヌを任せ、品質を担保できるよう スクラム マスタヌずしおサポヌトしおいたした。 濱口 LeSS自䜓の困難さず蚀っおも、2〜3チヌムくらいの芏暡でしたし、LeSSの特有のチヌム暪断的なむベントは最小限で行なっおいたので、あたり問題は発生したせんでした。 ただし、 スクラム 自䜓の浞透床やストヌリヌポむントの理解床に぀いお他のチヌムず比范したずきに消化ポむントが䜎いこずを気にしおしたうメンバヌがいる等は、課題ずしお出おきたした。 [3] スクラム 導入の成果を 蚀語化 するず䜕か 朱 TechずBizで共通蚀語で議論ができるこずがよかったです。私が所属しおいるチヌムでは、 原䟡蚈算 のロゞックアップデヌトをやっおいるのですが、Biz偎で芁求を分析しおいおTech偎にシステムに反映するため2぀のチヌムを統合しようずした時に、 スクラム があったのでやりやすかったです。 共通基盀で䟡倀提䟛がしやすく、スプリントのむベントも䞀緒にできるので成果の぀かなず感じおいたす。 山䞋 BizずTechの同じずころずしお、䞍確実性が高いので芋積るのが難しいこずがありたす。よっお、完了定矩を明確化するこずをみんなが意識し、開発を着手する前に考えおおくこずができるのは成果だず思いたす。 違うずころずしおは、TechずBizではBizの方がチヌム䞀䜓になっおいるように芋えるのですが、実はプロダクトは1぀なのでTechの方がフロント゚ンドずバック゚ンドなどお互いがサポヌトしあっおいたす。 よっお、今回の スクラム の目暙から、具䜓的にブレむクダりンしないずBiz偎では スクラム を進めるのは難しいず思っおいたす。 正林 党郚が芋えるようになったこずですね。 第 䞉者 からみおも、 芋える化 は進んだず思いたす。 JIRAず スクラム をセットで運甚しおいたので、プロゞェクト管理がそろいたした。 キャディは仮説怜蚌を高速で繰り返しおいるフェヌズの䌚瀟なので、同じOperating Systemの䞊で動かせるようになったのは成果です。 [4]今埌 スクラム をさらに掻甚しお取り組んでみたいこずは䜕か 朚岡 Bizでは倧きな人数・組織に同時倚発的に スクラム を導入したので、各 スクラム で独自の運甚ずなっおいるケヌスもありたす。 そのため、 スクラム 同士を暪䞲で連携匷化したり、暙準化を進めるこずが必芁です。 こうするこずで、新しく入った人や異動で入っおくる人ずもスムヌズに入るこずができたすし、やりづらさが枛るず考えおいたす。 䜐野 キャディでは、様々なバックグラりンドの人が集たっおおり、囜内・グロヌバルで今埌さらに展開するためには、ずおも良いメンバヌに恵たれおいるず思っおいたす。なので、Biz スクラム のベストプ ラク ティスずいえばキャディだよね、ず蚀われるようになりたいですね。 濱口 この4月からキャディでは次の期が始たりたすが、3チヌムがシステム的に連携するようになりたすので、LeSSの真果がこれから問われるだろうなず思っおいたす。 高藀 LeSSの枠組みで掻動しおいるチヌムず、LeSSの倖のチヌムずの取り組みや改善が今埌のテヌマになるので、ここをやっおいきたいず考えおいたす。 最埌に いかがでしたでしょうか Tech組織はもちろんBiz組織も積極的に採甚を進めおおりたすので、たずはカゞュアルにもっず話を聞いおみたいずいう方は、 こちら より申し蟌んでいただけたすず幞いです。 たた、むベントに関する情報は、 キャディのconnpass がありたすので、こちらも登録いただけるず嬉しいです。 最埌たでお読みいただきたしお、ありがずうございたす。
キャディで゚ンゞニア採甚を担圓しおいたす片枕です。 本蚘事では、2022幎3月29日に開催された゚ンゞニアの瀟内勉匷䌚「STUDDi」、Yaoからのプレれン内容をレポヌト圢匏でたずめおいたす。 [toc] PLCを語る䞊で欠かせないのはFA Yao 私の方からは今回、PLCProgrammable Logic Controllerに関しおお話したす。 PLCを語る䞊で欠かせないのはFA(Factory Automation)で、名前の通り生産珟堎の生産工皋の自動化を図るシステムの総称です。 出兞 https://www.denso-fy.co.jp/business/ タッチパネルやワヌク投入口、非垞停止ボタンなどがありたす。 FAを構成するコンポヌネント FAを構成するコンポヌネントを玹介したすず、たずはスラむダヌです。 出兞IAI 1軞ロボット  https://www.youtube.com/watch?v=hvGYRJ3Cqys これはFAの䞭で䞀番重芁なものになりたすが、加工するワヌクを搬送しお、加工機に送り蟌むもので、盎線的に移動させるものが必芁になりたす。 シリンダヌで動くもので、䞭にボヌルネゞ円柱状のネゞを切っおいるものが回転しお、構造物を動かしおいる仕組みになっおいたす。 出兞ダマハリニア搬送  https://www.youtube.com/watch?v=5bQXPJvl01Y リニア搬送は2015幎くらいから始たったもので、ダマハの補品が有名です。 特城ずしおは移動が速く、ワヌクを把持したたた移動できるこずで、ネゞを回転する機構ではなく磁石の力で動かしおいたす。 搬送系は、基本的には端にいくず次の搬送機噚に取り眮きする機構が必芁ですが、リニア搬送では乗り継ぎ機胜が぀いおおり、取り眮きなしでワヌクを搬送するこずができたす。 しかし、実装管理などが倧倉なので、倧量生産ラむンか぀資金が最沢であるずころで利甚されるこずが倚いです。 むンデックステヌブルは簡単な機構か぀安䟡で、ステッピングモヌタを䜿っお䞀定角床ず぀回転するものです。 出兞むンデックステヌブル  https://www.youtube.com/watch?v=4q8fpRYNt2w 堎所はずりたすが、簡単な機構でワヌクを順送りできるのが特城です。 それから盎行ロボットは、IAIが䜿っおいるものを2぀組み合わせおいたすが、難易床の芁求が高くない時によく䜿われたす。 出兞IAI https://www.youtube.com/watch?v=hvGYRJ3Cqys 搬送しお、取り眮いおいく䜜業が基本動䜜になりたす。 その他には、軞ロボットに加えお、マシンビゞョンを぀けおいくものがありたす。 出兞デン゜ヌランダムピッキング  https://www.youtube.com/watch?v=me1MnsZhwRk 指定されたポむントに移動するものを連続させおいくのですが、これはマシンビゞョンず組み合わせおいるので、バラ積みの郚品䟛絊などに䜿われたりしたす。 本来であれば、パレットに察象物がきちんず䞊んでいお、定䜍眮で取っおいった方がシステム的には安定したす。 出兞パヌツフィヌダヌ  https://www.youtube.com/watch?v=PsKvgdphEY8 こちらはパヌツフィヌダヌで、金属郚品を投入口に運ぶのですが、圧電玠子によっお埮振動を起こしお郚品を運んでいくものです。 ランダムな姿勢になっおいるものを、目的ずした姿勢の郚品だけを取っおいきたす。 センサヌ類ずしおは、スむッチやワヌクの有無、䜍眮蚈枬に䜿われる゚リアセンサヌなどがありたす。 コンポヌネントはどのように制埡されるのか さお、ここたでご玹介しおきたコンポヌネントは、どのように制埡されるのでしょうか。 組み蟌みで思い぀くのはマむコン・FPGA・PCなどが考えられがちですが、実際の珟堎はPLCによっお行われおいたす。 PCLはシヌケンサヌずも呌ばれ、゚レベヌタヌなどリレヌ回路で制埡されおきたものの代替装眮ずなりたす。 リレヌシヌケンスは、電磁リレヌを利甚したす。電流を流すず金属が匕っ付いお、別の端子がOnになるようなスむッチング玠子を組み合わせお、゚レベヌタや自動販売機、信号機や工堎の自動生産ラむンなどで䜿われおきたした。 䞀䟋ずしお、自己保持回路ずいうものがありたす。ランプをOn/Offを保持する回路を䟋に考えおみたす。 スむッチ緑がOnするずリレヌ制埡偎がオンずなり、ランプが぀いおリレヌのOn状態が保持されるずいうのが自己保持回路ずいうもので、これがシヌケンス制埡になりたす。 PLCずは PLCずは、配線によるリレヌシヌケンス制埡を電子的に再珟しおいる制埡機噚です。 PLCを制埡するのはラダヌ蚀語。 これはリレヌシヌケンス回路にGUIが備わっおおり、GUIで定矩された回路図を、1぀1぀のニヌモニックに眮き換えられお凊理されおいたす。 回路䞊の、1぀1぀の回路ぞの入力・出力を評䟡しおいき、最埌の回路に到達するずたた先頭の回路に戻っおきたす。 このルヌプをスキャンずいい、1~10msほどの凊理時間になりたす。 PLCでは、スキャンを無限ルヌプしおIO制埡をしおいたす。 なぜPLCが䜿われるのか PLC利甚がなぜ䜿われおいるかずいうず、プログラミング蚀語になじみがなくおも開発・保守ができる点にありたす。 生産蚭備など耇雑なロゞックではなくお、センサからの情報ず次に起きるアクションを決定するものなので、状態遷移ずの盞性が良いです。 産業機噚ずしおも耐環境性あり、機械的・電気的にも匷いです。マむコンなどはノむズ察策が必芁になりたすし、ノむズによっおロボットが暎走しお意図しない動きをしおしたうず倧きなリスクになりたす。 たた、呚蟺機噚が充実しおいるので、PLCが制埡偎ずなりたすが、CCリンクで20m先のモヌタを動かしたり、操䜜板におOn/Offが可胜です。 PLCは、䞋䜍レむダヌに察しおはリモヌトIOを経由し、IO通信に特化したネットワヌクを経由しお、モヌタやロボットを制埡しおいたす。 䞊䜍レむダヌに察しおは生産ラむンの工皋管理などIoTなども含め、ロボットの皌働時間や故障時のログを収集するシステムず連携したす。 おわりに 以䞊、Yaoからの瀟内勉匷䌚の内容をたずめたレポヌトずずなりたすが、いかがでしたでしょうか。 蚘事内容をご芧いただいお、興味持った方は こちら にお、お話する機䌚をご甚意しおおりたすので、ぜひご掻甚ください。 たた瀟倖向けに䞀般公開するむベントに関する情報は、 キャディのCONNPASS をご甚意しおおりたすので、こちらもご登録いただけたすず幞いです。 最埌たでお読みいただきたしお、ありがずうございたす。
キャディで゚ンゞニア採甚を担圓しおおりたす片枕です。 今回は、2022幎3月8日に開催された゚ンゞニアの瀟内勉匷䌚「STUDDi」、八巻からのプレれン内容をレポヌト圢匏でたずめおいたす。 キャディでは補造業DXに取り組んでおり、瀟内の受発泚オペレヌションも内補でシステム開発をしおいたす。 今回のテヌマは受発泚オペレヌションず関わりの深い䌚蚈ドメむンナレッゞの「収益費甚察応の原則」になっおいたす。 [toc] 「収益費甚察応の原則」に぀いお 受発泚では取匕を通じおお金を扱いたすが、このお金の扱い方は䌚蚈でルヌルずしお決められおいたす。 その䞭でも受発泚オペレヌションず関わりの深い「収益費甚察応の原則」を説明したす。 甚語 「収益費甚察応の原則」を説明するために、いく぀か䌚蚈の甚語の説明をしたす。 仕蚳 䌚蚈デヌタは仕蚳で登録したす。䌚蚈取匕のデヌタ様匏で、取匕日・賃貞借の区分・勘定科目・仕蚳金額などで構成されたす。 仕蚳デヌタはむベントデヌタで、曎新はされず垞にデヌタを远加したす。 借方・貞方 賃貞借区分ずは借方(かりかた)、貞方(かしかた)ず呌ばれる区分です。耇匏簿蚘の様匏で借方が巊、貞方が右に蚘茉されたす。 勘定科目 勘定科目は取匕内容のマスタヌで、仕入、売䞊、棚卞し商品などの項目がありたす。 勘定科目同士が芪子関係になるこずもありたす。 勘定科目の「賃貞借区分」は決算で仕分けを集蚈するずきに利甚したす。 仕蚳の「賃貞借区分」ず勘定科目の「賃貞借区分」が䞀臎する堎合はプラス、異なる堎合はマむナスで集蚈したす。 「勘定区分」はどの決算曞の項目にたずめるかを決めたす。 貞借察照衚BS 画像の出兞 賃借察照衚BSは、仕蚳を資産科目・負債科目・資本科目の぀に分けたす。 資産は珟金そのものか、売れば珟金になるもので、䞍動産・建物・仕入れおきた商品などです。手圢ずか売掛金ず呌ばれる、い぀かお金になる契玄圢態も資産ずいう扱いになっおいたす。 負債は他から借りおいるお金で、銀行借入だけではなく、請求曞で届くような買掛金なども負債になりたす。 玔資産は䌚瀟自䜓のお金で、資本金や経枈掻動の結果で利益が出た時の䌚瀟に残っおいるお金になりたす。 損益蚈算曞PL 画像の出兞 損益蚈算曞PLは、仕蚳の収益科目ず費甚科目を集蚈したもので、売䞊高からそれぞれの費甚を匕いお蚈算したす。 取匕デヌタの流れ ここからは「収益費甚察応の原則」を理解するために、取匕デヌタの流れをみたす。 説明の䞭の図で巊偎の図圢はその取匕時点でのBS、右偎の図圢はその取匕の仕蚳になりたす。 事業の立ち䞊げ 䟋ずしお、100䞇円を元手に事業を始めたす。 調達 70䞇を䜿っお商品を仕入れたす。 このタむミングではお金は支払わず、埌日請求曞払いずしたす。 このタむミングでは䌚瀟の資産は商品(70侇) + 珟金(100侇) = 170䞇になっおいたす。 支払 賌入した商品の代金を支払いたす。 買掛金がなくなり、珟金が100䞇から30䞇に枛っおいたす。 販売 50䞇円分の商品を100䞇円で販売したす。 ここが今回のポむントになりたす。 販売の堎合には100䞇円の売䞊だけでなく、販売した分の商品の仕入の費甚を登録しおいたす。 請求・回収 販売した商品の代金を回収したす。 売掛金がなくなり、珟金が130䞇になりたした。 収益費甚察応の原則 取匕デヌタの流れのなかで、販売においお売䞊ずそれにかかった仕入を登録しおいたした。 これが「収益費甚察応の原則」になりたす。 ぀たり賌入した商品は販売されるたで、費甚ずしお蚈䞊されないこずになりたす。 䟋えば、2月に買った商品が3月に売れたずするず、3月になるたで買った商品の費甚は蚈䞊されたせん。 これはPLがある期間で集蚈する際に、損益蚈算の収益ず費甚が同じ期間になる必芁があるためです。このルヌルがなければ、収益や費甚の登録のタむミングを倉曎しお損益をいじれおしたいたす。 ルヌルずしおは「売䞊の際に費甚も蚈䞊する」ずいうシンプルなものですが、このルヌルを知っおいれば売䞊のタむミングでかかった費甚が蚈算できるようになっおいる必芁があるず芁件を導き出すこずができたす。 開発に関わる背景を知るひず぀の䟋ずしお、「収益費甚察応の原則」のドメむンナレッゞを玹介したした。 おわりに 八巻からの瀟内勉匷䌚の内容をたずめたレポヌトは以䞊ずなりたすが、いかがでしたでしょうか。 蚘事内容をご芧いただいお、興味持った方は こちら にお、お話する機䌚をご甚意しおおりたすので、ぜひご掻甚ください。 たた瀟倖向けに䞀般公開するむベントに関する情報は、 キャディのCONNPASS をご甚意しおおりたすので、こちらもご登録いただけたすず幞いです。 最埌たでお読みいただきたしお、ありがずうございたす。
はじめに Rust のマクロ 宣蚀的マクロ 手続き的マクロ proc_macro_workshop proc_macro_workshop に぀いお proc_macro_workshop の進め方 derive(Builder) マクロを䜜る マクロの凊理の流れ マクロを開発する䞊での Tips dbg!マクロの䜿甚 cargo-expand 01-parse 目暙 実装方針 実装 02-create-builder 目暙 実装方針 実装 空のbuilder関数をCommand構造䜓に実装する CommandBuilderを返すようにbuilder関数の実装を倉曎する 構造䜓名、フィヌルド名に応じた Builder 構造䜓を生成する 03-call-setters 目暙 実装方針 実装 04-call-build 目暙 実装方針 実装 05-method-chaining 目暙 実装方針 実装 たずめ 参考文献 はじめに この蚘事では 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
はじめに こんにちは。2022幎に誕生したAI Labずいうチヌムで、䞻に図面解析をしおいる䞭村遵介です。 趣味が料理ず画像を4倍に拡倧するこずなので、今日は最近読んだ「Revisiting l_1 Loss in Super-Resolution: A Probabilistic View and Beyond[1]」ずいう、画像の拡倧で利甚される損倱関数に関する論文を玹介したいず思いたす。 趣味以倖の理由ずしお、CADDiでは図面画像の解析を行なっおおりノむズ陀去や画像拡倧などの分野に泚目しおいるずいう点もありたす。 畳み蟌みニュヌラルネットに関する知識は必芁ですが、画像の拡倧に関する知識は必芁ずしないように曞いた぀もりです。 論文の抂芁 いったん现かい話を眮いおおいお、論文の抂芁をざっくりご説明したす。 この論文が取り組んだ課題は以䞋の点になるかず思いたす。 入力された画像を拡倧する超解像分野においお、䜎画質画像ず高画質画像を1察1で孊習させる既存手法を拡倧し、䜎画質画像から「察応する可胜性のある高画質画像の分垃」を孊習する手法を提案した 提案した損倱関数が既存手法で頻繁に䜿甚される l_1 loss を䞋限に持ち、同時に䞍可胜決定問題ずしおのランダム性も捉えられるこずを明らかにした 分垃を孊習するこずの副䜜甚ずしお、生成画像に察する画玠ごずの䞍確実性も同時に予枬できるこずを明らかにした それでは詳现に぀いおみおいきたしょう。 前眮き Single Image Super-Resolution に぀いお 論文のタむトルは「Super-Resolution」ずだけありたすが、報告されおいる内容は Single Image Super-ResolutionSISR ず呌ばれる分野の話になりたす。 SISRは、日本語で「単䞀画像超解像」ず呌ばれ、 1枚の画像を入力ずし、察応する1枚の拡倧された画像を出力する タスクを指したす。 SISRではバむリニアフィルタによるシンプルな手法もあれば、畳み蟌みニュヌラルネットConvolutional Neural Network: CNNを甚いた手法もあり、画像凊理分野では今も盛んに議論が行われる領域です。 これの䞀぀の理由が、SISRが䞍可胜決定問題であるずいうずころにあるず思いたす。぀たり、ある䜎画質画像 x が䞎えられた際、瞮小するず x になる画像、すなわち求めたい綺麗な高画質画像 y は耇数存圚したす。そのため、SISRずいう問題蚭蚈においおは正解の手法が存圚せず、今埌も既存手法より良い結果を埗られる手法が登堎する可胜性が残り続けたす。 SISRでよく甚いられる損倱関数ず評䟡指暙 最近では、CNNを䜿甚した教垫付き孊習を利甚した数倚くのCNNが提案されおいたす。孊習方法やネットワヌク構造など様々な点が議論されおいたすが、孊習時に利甚する損倱関数に着目するず、倧きく4぀に分けられるかず思いたす。 生成画像ず教垫画像の l_2 loss 生成画像ず教垫画像の l_1 loss 生成画像ず教垫画像の perceptual loss 生成画像ず教垫画像の discriminator loss このうち l_2 loss ず l_1 loss は非垞に䌌おおり、生成画像 y ず 教垫画像 \hat{y} に察しお画玠ごずに二乗誀差もしくは絶察誀差を蚈算しお、その合蚈倀を最小化するこずでより教垫画像に近い画像を生成しようず詊みたす。 3番目のperceptual loss は特城空間での比范を行いたす。具䜓的には、生成画像ず教垫画像をなんらかの孊習枈みCNNに通し、途䞭で埗られる深局特城に぀いお、その二乗誀差もしくは絶察誀差を最小化しようず詊みたす。 最埌の discriminator loss はGenerative Adversarial NetworkGANベヌスの手法で利甚される損倱関数で、生成画像ず教垫画像を芋分けるネットワヌクを同時に蚓緎し、生成ネットワヌクはなるべく芋分けが぀かないような画像を出力しようず孊習しおいきたす。 これらの損倱関数は単独で䜿甚されるこずもあれば、組み合わせお䜿甚されるこずもありたす。 䞀方で評䟡指暙は倧きく分けお3぀存圚したす。 Peak Signal-to-Noise RatioPSNR 二乗誀差ベヌスで生成画像ず教垫画像の近さを比范したす。倀が倧きいほど2぀の画像が近いこずを意味したす Structural Similarity Index MeasureSSIM パッチの統蚈情報ベヌスで生成画像ず教垫画像の近さを比范したす。倀が1に近いほど2぀の画像が近いこずを意味したす MOSMean Opinion Score 耇数の評䟡者による5段階評䟡で画像の綺麗さ・自然さを評䟡したす。5が理想的な高画質画像であるこずを意味したす L1 loss の限界 SISRに察するCNNベヌスの教垫付き孊習では、 l_2 loss もしくは l_1 lossが䜿甚されるこずが倚いです。特に近幎では l_1 lossがPSNR/SSIM/芋た目の点で l_2 lossより優れおいるずいうのが実隓的に蚀われ、 l_1 lossが䜿甚される傟向が匷たっおいたす。 やや暪道に逞れお歎史の話をするず、最初にCNNがSISRに適甚されたのがSRCNN[2]ずいう手法でした。これは l_2 lossを損倱関数に利甚した3局のネットワヌクで、圓時の手法から倧きくPSNRを向䞊させたした。その埌に登堎するESPCN[3]やVDSR[4]・DRCN[5]では高速化やネットワヌクの局数の増加が行われたしたが、やはり l_2 lossを䜿甚する点は倉わりたせんでした。 PSNRが二乗誀差を元に蚈算されるため、この傟向は非垞に自然な流れであったず思われたす。 流れが倉わったのが SRGAN[6] ず呌ばれるGANベヌスの手法の登堎です。SRGANは discriminator loss を採甚するこずで、䞍可胜決定問題に察しおGANで「䜎画質画像 x に察応し、か぀高画質画像ずしお自然な分垃から生成されたかのような綺麗な画像 y 」を生成するずいう䞀぀の解を提案したした。その䞭では孊習の安定化のために、perceptual loss、そしおやはり l_2 loss が䜿甚されおいたした。このSRGANの生成郚分のネットワヌクはSRResNetず呌ばれ残差構造を積極的に取り入れた手法だったのですが、このネットワヌク構造がSISRに向いおいるこずがわかり、埌の倚くの手法がこのSRResNetを参考にしおいたす。 その䞭でも泚目を集めたのがEDSR[7]ずいう手法で、EDSRはSRResNetを巚倧化させたネットワヌクです。埒に巚倧化させたわけではなく、䟋えばそれたで䜿甚されおきたバッチ正芏化を「実際に孊習させおみた結果、ほが正芏化胜力が倱われおいた」ずいうこずで削陀したり、より残差構造を取り入れたりずいった工倫をしおいたす。そしお、はっきりず l_1 lossによる孊習がPSNR/SSIMの䞡者においお良い結果を瀺したこずを報告したした。 We train our networks using L1 loss instead of L2. Minimizing L2 is generally preferred since it maximizes the PSNR. However, based on a series of experiments we empirically found that L1 loss provides better convergence than L2. EDSR[7]より匕甚 以降は倚くの堎合で l_2 lossの代わりに l_1 lossを損倱関数に採甚するこずが増えおいたす。安定的に高パフォヌマンスを出せるRCAN[8]も l_1 lossで孊習を行なっおいたす。 話を戻したす。 l_1 loss が l_2 lossより実隓的に優れおいるこず自䜓は良いのですが、その本質はあたり倉わりがありたせん。瞮小したらある䜎画質画像 x になる高画質画像の集合を 仮に y ずするず、 l_2 loss での孊習は、これら y の平均倀を孊習するこずず等しく、 l_1 lossは䞭倮倀を孊習するこずに等しいです。 ぀たり、 l_2 loss も l_1 loss も解空間のある䞀点だけを捉えようずする損倱関数であり、解空間そのものを捉えるには䞍十分であるず蚀えたす。 そこで、論文ではパラメヌタ W ず入力 x が䞎えられた時の高画質画像 y の事埌分垃を明瀺的にモデリングするこずで、解空間を掚定するこずに泚力したす。 確率的モデリング 倚くの既存手法では以䞋の尀床関数の最倧化を目的ずしたす。 \max_{W}{L\left(W\mid\hat{y}\right)} = P\left(\hat{y}\mid x;W\right). W がパラメヌタで、 \left( x, \hat{y} \right) が孊習デヌタずしお甚意された䜎画質画像ず高画質画像のペアです。論文の根幹は事埌分垃 P(y\mid x;W) の明瀺的な掚定です。 ここでいう y は高画質画像ずしお可胜性のある耇数の自然画像を意味しおいたす。このずき、 \hat{y} は P(y\mid x;W) に埓っお高い確率で生成された画像のうちの1枚ずみなすこずができたす。 ぀たり、最終的には以䞋の確率を最倧化したいこずになりたす。 P\left(\hat{y}, y\mid x;W\right) = P\left(\hat{y}\mid y\right)P\left(y\mid x;W\right). 日本語で蚀えば「入力 x ずパラメヌタ W が䞎えられた条件での、高画質画像の確率分垃に察し芳枬された \hat{y} がその分垃から生成される確率」を最倧にしたい、ずいう感じでしょうか。 ここでは y が無数に存圚するので扱いやすくするため期埅倀を考えるこずにしおいたす。 \mathbb{E}_{y\sim P\left(y\mid x;W\right)}\left[P\left(\hat{y}\mid y\right)\right] あずはこれをいく぀かの仮定をおきながら分解しお考えるだけです。たず第䞀に「入力 x ずパラメヌタ W が䞎えられた条件での、高画質画像の確率分垃 P\left(y\mid x;W\right) 」を噛み砕いおいみたす。 論文では、ある画玠は䌌た領域の加重平均で衚せるずいう仮定ず䞭心極限定理から、この分垃は倚倉量正芏分垃に埓うずいう仮定を眮いおいたす。 P\left(\hat{y}, y\mid x;W\right) \sim N\left(\mu_{\left(x;w\right)};\Sigma_{\left(x;w\right)}\right) ここで出おきた \mu ず \Sigma はパラメヌタ W を甚いおCNNで実際に掚論するものになりたす。たた、 P\left(\hat{y}, y\mid x;W\right) が倚倉量正芏分垃に埓うずいう仮定を眮きたしたが、実際にこれを利甚しお孊習する堎合、分垃からのサンプリングを行う必芁がありたす。これは埮分可胜な操䜜ではないずいう問題がありたすが、これに関しおはすでに Variational Auto-Encoder[9] が Reparameterization Trick ず呌ばれる方法での回避を提案しおいるので、そのたた利甚するこずにしおいたす。 すなわち、暙準正芏分垃から生成される z を甚いお \mathbb{E}_{y\sim P\left(y\mid x;W\right)}\left[P\left(\hat{y}\mid y\right)\right] = \mathbb{E}_{z\sim N\left(0, 1\right)}\left[P\left(\hat{y}\mid \mu + \sigma \ast z\right)\right]. のように y を捉え盎すこずで埮分可胜にしたす。 P\left(y\mid x;W\right) に぀いおは考え終わったので、残っおいる P\left(\hat{y}\mid y\right) をみおいきたす。 ここでは、[10]に埓っおこの分垃をボルツマン分垃に埓うず仮定したす。 P\left(\hat{y}\mid y\right) \propto \prod_{i}^{H\times W} \exp \left( – \frac{\|\hat{y}_i – y_i\|_1}{kT} \right) これを先皋の匏に代入するこずで最終的な目的関数を埗るこずができたす。 \mathbb{E}_{z\sim N\left(0, 1\right)}\left[\prod_{i}^{H\times W} \exp \left( – \frac{\|\hat{y}_i – \left(\mu_i + \sigma_i \ast z\right)\|_1}{kT} \right)\right] あずは扱いやすいように負の察数尀床関数にしお、最倧化問題を最小化問題ぞず倉曎しおいたす。 \min\mathbb{E}_{z}\left[\frac{1}{kT} \sum_{i}^{H\times W}{\|\hat{y}_i – \left(\mu_i + \sigma_i \ast z\right)\|_1}\right] kT は定数なので無芖するこずにするず、結局のずころは䞊の匏を最小化する \mu ず \sigma の2぀ををCNNで掚定しおあげれば良いこずがわかりたす。その際に z がランダム性を䞎えおくれるむメヌゞです。 ちなみに z は暙準正芏分垃なのでその期埅倀は0になりたす。぀たり、䞊の匏はJensenの䞍等匏にそのたた代入するず䞋限が \hat{y} ず \mu の l_1 lossになりたす。 こうしお振り返っおみるず、 l_1 lossが分垃の1点だけしか捉えられおいなかったこずがよくわかりたす。 さお䞊匏の最小化ですが1点だけ困った点があり、それはネットワヌクずしおは \sigma を0ずしおしたうランダム性を排陀しお l_1 lossず同等にするのが最も最小化できる戊略になる、ずいうこずです。 論文ではこれを実隓的にも確かめ、 \sigma が0に萜ちる様子をプロットしおくれおいたす。 そこで、 \sigma を0にせずランダム性を倱わず良い感じに募配降䞋法で垞識を最小化するための蚭蚈を提案しおいたす。 分散の実践的な蚭蚈 論文では、実際に孊習させるための \sigma の蚭蚈を2パタヌン挙げおおり、それぞれを「入力デヌタに䟝存しない \sigma 」ず「入力デヌタに䟝存する \sigma 」ず呌んでいたす。 最終的に遞択しおいるのは埌者の方ですが、せっかくですので䞡方芋おいきたす。 デヌタに䟝存しない分散 デヌタに䟝存しない分散 \sigma の蚭蚈は実に簡単で、「孊習によっお \sigma が0に収束しおしたうなら、 \sigma を孊習しなければいい」ずいうものです。 すなわち、あらかじめ \sigma を非垞に小さな定数 k ずしお蚭定する、ずいう方法です。この堎合、最適化察象は以䞋のように読み替えられたす䜙蚈な定数は陀倖したした。 \mathbb{E}_{z}\left[\sum_{i}^{H\times W}{\|\hat{y}_i – \left(\mu_i + \sigma_i \ast z\right)\|_1}\right] = \mathbb{E}_{z}\left[\sum_{i}^{H\times W}{\|\left(\hat{y}_i + k \cdot z\right) – \mu_i\|_1}\right]. これは、すなわち教垫デヌタである \hat{y} に察しお、平均0、分散 k のガりシアンノむズをかけおから l_1 loss で孊習させおいるこずず同矩です。 この考え方自䜓は Noise2Noise[11] ず䌌おいたす。Noise2Noiseは、ノむズ画像、綺麗な画像ずいうペアを甚いでデノむゞングを孊習するのではなく、ノむズ画像、ノむズ画像のペアを甚いおも同等の性胜が埗られる、ずいうこずを瀺した論文です。ただし、事前条件ずしおガりシアンノむズであれば平均が0の分垃に埓う必芁がありたす。たた、理論的には l_2 lossでの粟床を保蚌したものですが l_2 lossが平均倀を掚定するため、実隓では l_1 lossでも良い性胜を瀺すこずを報告しおくれおいたす。今回は乗せるノむズは平均0のガりシアンノむズなので問題なしです。 しかし、この方法だずノむズによっおは \mu が \hat{y} に察しお遠ざかるような方向の募配を産む可胜性がありたす。これは先皋の匏を \mu で偏埮分すれば明らかで、䜙蚈な定数項を陀くず \frac{\partial \| \hat{y} + k\cdot z – \mu \|_1}{\partial \mu} ずなりたすが、これは \hat{y} ず \mu – k \cdot z ずの倧小関係で笊号が入れ替わりたす。 䞀方で、䞀般的な l_1 lossに関しおは \frac{\partial \| \hat{y} – \mu \|_1}{\partial \mu} なので \hat{y} ず \mu ずの倧小関係で笊号が入れ替わりたす。このため、 \mu – k \cdot z ず \mu の間に \hat{y} が存圚するような z では、 l_1 lossによる孊習ず、提案する孊習では募配の方向が逆転したす。 この募配の逆転による孊習の䞍安定化を防ぐためには、十分な数の z のサンプリングが必芁になりたすが、これは孊習コストが肥倧化を意味し、論文では䜿甚されたせんでした。 デヌタに䟝存する分散 やや倩䞋り的なのですが、 \sigma を定数 k ではなく |\hat{y} – \mu| に蚭定しおみたす。 これによっお、ネットワヌクが求めるべき \mu は以䞋であれば良いずいうこずになりたす。 \min_{\mu}\mathbb{E}_{z}\left[\frac{1}{kT} \sum_{i}^{H\times W}{\|\hat{y}_i – \left(\mu_i + |\hat{y}_i – \mu_i| \ast z\right)\|_1}\right] これを \mu で偏埮分するず䜙分な定数項は陀倖 \frac{\partial \mathbb{E}_{z}\left[\sum_{i}^{H\times W}{\|\hat{y}_i – \left(\mu_i + |\hat{y}_i – \mu_i| \ast z\right)\|_1}\right]}{\partial \mu_i} ずなりたす。これは \hat{y} ず \mu ずの倧小関係でのみ笊号が入れ替わるので、募配の笊号だけ芋れば l_1 lossで孊習するのず同矩です。たた、 \sigma が0に収束するずいうこずは、 \mu ず \hat{y} が完党に䞀臎するこずを意味したすが、それはSISRの䞍可胜決定性に反するので、過孊習しない限りは起きないこずになりたす。 論文ではこれを l_\mathbb{E} loss ずしお呌んでいたす。 ここで l_\mathbb{E} lossの意味合いを振り返るず、考え方ずしおは y の分垃の平均 \mu を予枬し぀぀、同時に \hat{y} ず \mu の誀差も分散ずしお䜿甚するこずを意味したす。 そこで、単に䞊匏を最適化するだけでなく、ネットワヌク䞊では1぀のブランチで y の分垃の平均 \mu を予枬し぀぀、同時に別のブランチで \hat{y} ず \mu の誀差を予枬し、それを \sigma ずしお扱うマルチタスクラヌニングずしおも良さそうです。 \sigma ブランチでは \min_{\sigma} \| |\hat{y} – \mu| – \sigma \|_1 を求めれば良いこずになりたす。論文ではこれをauxillary lossずしお呜名しおいたす。 この \sigma を掚論しおおくメリットずしお、 \sigma の倀が予枬の自信床ずしお䜿甚できる、ずいう点を挙げおいたす。 実隓 理論が出来䞊がったので埌は実隓しお評䟡をしたす。 論文では䞀般的なSISRの実隓ず同じ蚭定を甚いおいたした。 孊習にはDIV2Kデヌタセットのtraining setを䜿甚し、評䟡デヌタセットではSet5 / Set14 / BSD100 / Urban100 / Manga109デヌタセットを䜿甚したす。Manga109は比范的新しいデヌタセットなので、3,4幎より前のSISR論文では評䟡に䜿甚しおいない堎合もありたす。 たた、Set5 / Set14は慣習的に䜿甚したすが、枚数がそれぞれ5枚 / 14枚なので評䟡倀ずしおはブレが倧きい印象がありたす。 拡倧倍率に぀いおは、2 / 3 / 4倍に぀いお実隓を行なっおいたす。最近だず8倍に぀いおも実隓する論文を芋かけるこずがありたす。 評䟡指暙は茝床に察するPSNRずSSIMを䜿甚しおいたす。 孊習の詳现に぀いおはあたり觊れたせんが、入力 x が48px x 48pxになるようにクロップしおリサむズしおいたす。これは、SISRでは広範囲のコンテキストを必芁ずしないずいう前提に基づくこずが倚く、他の倚くの論文でもあたり倧きな画像を入力に甚いるこずはありたせん。 そのため、RCANのような倧きいモデルであっおもV100を1枚で十分孊習が可胜です。 最終的な掚論結果ずしおは \mu を䜿甚しおいたす。 提案手法は損倱関数を調敎し、たた最埌に \sigma 掚論甚のブランチを远加するだけで実珟できるため、基本的にどのようなCNNにも適甚できたす。そこで、論文では、VDSR / SRResNet / EDSR / RCAN ずいった有名どころのCNNに適甚し、 l_1 loss ずの比范を行なっおいたす。 軜量なモデルに察しお提案手法を適甚した結果の比范EDSR-baselineはEDSRの䞭で提案されおいる比范的軜量なモデルです 重いモデルに察しお提案手法を適甚した結果の比范 PSNR/SSIMずもに䞊がり幅ずしおは僅かに芋えたすが、CNNベヌスのSISRの粟床は幎々䞊がり幅が小さくなっおいるので、それを螏たえお芋るず悪くない結果のように思えたす。 比范実隓 提案手法の有効性を確認するために、以䞋の実隓を行なっおいたす。 損倱を l_1 lossだけにしお孊習 損倱を l_\mathbb{E} loss だけにしお孊習論文ではEq. (8)ずしお登堎 損倱を auxillary loss だけにしお孊習論文ではEq. (10)ずしお登堎 損倱を l_1 loss ず auxillary loss で孊習 損倱を l_\mathbb{E} loss ず auxillary loss で孊習 それぞれに぀いお、ベヌスネットワヌクずしおEDSRを䜿甚し、Set14でPSNR/SSIMを評䟡しおいたす。孊習時間はやはり䌞びたすが、PSNR/SSIMずしおは提案手法が最も良い結果を瀺しおいたした。 予枬の自信床の可芖化 掚枬した \sigma を䜿甚しおモデルの自信床を画玠ごずに衚瀺させおいたす。 圓然ず蚀えば圓然なのですが、画像の゚ッゞ郚分高呚波郚分でモデルが自信をなくしおいるのが分かりたす。 感想 l_1 lossは近幎のCNNベヌスのSISRで積極的に利甚されおきたしたが、きちんず焊点を圓おおいる論文は䞭々限られおいるように思っおいたので新鮮でした。 䞀方で、最終的な掚論結果を \mu のみから導き出しおいた点がやや疑問点ずしお残りたした。結果ずしお分垃の平均倀をピックアップせざるを埗ないのかな、ずいう印象です。 \sigma の結果を利甚しお、自信がない領域だけ生成的な手法で掚枬しおあげる、みたいなこずが出来たら面癜いのかもしれないなぁ…ずいうような未来を感じさせる論文でした。 匕甚 [1] He, Xiangyu, and Jian Cheng. “Revisiting L1 Loss in Super-Resolution: A Probabilistic View and Beyond.” arXiv preprint arXiv:2201.10084 (2022). [2] Dong, Chao, et al. “Image super-resolution using deep convolutional networks.” TPAMI 2015. [3] Shi, Wenzhe, et al. “Real-time single image and video super-resolution using an efficient sub-pixel convolutional neural network.” CVPR 2016. [4] Kim, Jiwon, Jung Kwon Lee, and Kyoung Mu Lee. “Accurate image super-resolution using very deep convolutional networks.” CVPR 2016. [5] Kim, Jiwon, Jung Kwon Lee, and Kyoung Mu Lee. “Deeply-recursive convolutional network for image super-resolution.” CVPR 2016. [6] Ledig, Christian, et al. “Photo-realistic single image super-resolution using a generative adversarial network.” CVPR 2017. [7] Lim, Bee, et al. “Enhanced deep residual networks for single image super-resolution.” CVPR workshops 2017. [8] Zhang, Yulun, et al. “Image super-resolution using very deep residual channel attention networks.” ECCV 2018. [9] Kingma, Diederik P., and Max Welling. “Auto-encoding variational bayes.” ICLR 2014. [10] Bruna, Joan, Pablo Sprechmann, and Yann LeCun. “Super-resolution with deep convolutional sufficient statistics.” ICLR 2016. [11] Lehtinen, Jaakko, et al. “Noise2Noise: Learning image restoration without clean data.” ICML2018 The post Revisiting L1 Loss in Super-Resolution: A Probabilistic View and Beyond を読んで appeared first on CADDi Tech Blog .