CADDi Tech Blog 2024-03-19T09:50:20+09:00 caddi-tech Hatena::Blog hatenablog://blog/6801883189080957472 会計システムのアーキテクチャとモデリング ~会計というドメインを Rust で表現している話~ hatenablog://entry/6801883189089819742 2024-03-19T09:50:20+09:00 2024-03-21T21:49:17+09:00 はじめに こんにちは。 バックエンドエンジニアの松本です。今回は、会計システムの開発を通じて、 CADDi におけるプロダクト開発の様子を紹介します。 2024年3月現在、CADDiでは2つのサービスを提供しています。1つは図面データ活用クラウド「CADDi Drawer」で、もう1つは加工品製造サービス「CADDi Manufacturing」です。 今回、後者の加工品製造サービス「CADDi Manufacturing」向けに、 会計システムを構築しました。これは、生産管理システムや拠点管理システムから取得した各種情報を基にして、会計仕訳データを生成し、経理部門に公開する役割を持ちます。 … <h1 id="はじめに">はじめに</h1> <p>こんにちは。 バックエンドエンジニアの松本です。今回は、会計システムの開発を通じて、 CADDi におけるプロダクト開発の様子を紹介します。</p> <p>2024年3月現在、CADDiでは2つのサービスを提供しています。1つは図面データ活用クラウド「CADDi Drawer」で、もう1つは加工品製造サービス「CADDi Manufacturing」です。</p> <p>今回、後者の加工品製造サービス「CADDi Manufacturing」向けに、 会計システムを構築しました。これは、生産管理システムや拠点管理システムから取得した各種情報を基にして、会計仕訳データを生成し、経理部門に公開する役割を持ちます。</p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#会計システムのアーキテクチャとその狙い">会計システムのアーキテクチャとその狙い</a><ul> <li><a href="#計算処理を少しずつ進める">計算処理を少しずつ進める</a></li> <li><a href="#会計数値の妥当性をダッシュボードに表示する">会計数値の妥当性をダッシュボードに表示する</a></li> </ul> </li> <li><a href="#会計システムのモデリングと最初の開発">会計システムのモデリングと最初の開発</a><ul> <li><a href="#仕訳の流れを整理してドメインモデルデータベースモデルを作る">仕訳の流れを整理して、ドメインモデル、データベースモデルを作る</a></li> <li><a href="#ユーザーの言葉で話す">ユーザーの言葉で話す</a></li> <li><a href="#最初の開発をどの機能にするか検討する">最初の開発をどの機能にするか検討する</a></li> </ul> </li> <li><a href="#会計というドメインを-Rust-で表現する">会計というドメインを Rust で表現する</a><ul> <li><a href="#New-Type-Pattern-と-Phantom-Type-Pattern">New Type Pattern と Phantom Type Pattern</a></li> <li><a href="#会計台帳を-Rust-で表現する">会計台帳を Rust で表現する</a></li> <li><a href="#State-Machine-を型で表現する">State Machine を型で表現する</a></li> </ul> </li> <li><a href="#おわりに">おわりに</a></li> </ul> <h1 id="会計システムのアーキテクチャとその狙い">会計システムのアーキテクチャとその狙い</h1> <p>「CADDi Manufacturing」は、以下の特徴があり、会計システムとしての難しさはここにあります。</p> <ul> <li>多品種小ロットの取引のため、1つ1つの取引ごとの数量が少なく取引数が多い</li> <li>多くの顧客、多くのサプライパートナーと取引を行うため、サプライチェーンが複雑</li> </ul> <h2 id="計算処理を少しずつ進める">計算処理を少しずつ進める</h2> <p>システムは生産管理システムや拠点管理システムがデプロイされているKubernetesクラスタ上にCronJobとしてデプロイされています。</p> <p>CronJobの処理が始まると、対象月の入出荷などのイベントを上流システムのBigQueryから抽出します。そのイベントを会計データに変換し、アプリケーションのCloudSQLに永続化します。最後に、その月の会計データとして経理部門が参照するBigQueryに転送します。</p> <pre class="code mermaid" data-lang="mermaid" data-unlink>flowchart LR 上流システムのBigQuery -- イベント --&gt; CronJob CronJob -- 会計データ --&gt; CloudSQL CloudSQL -- 会計データ --&gt; 経理部門のBigQuery</pre> <p>会計システムは月に一度、「締め」を行い計算結果を確定し、BigQueryのデータをバランスシートなどを生成するシステムに登録します。月末になり、全てのイベントが上流システムで登録されないと、その月の会計データは確定しません。しかし、後続の会計プロセスが存在するために、「締め」は翌月上旬の数日間のうちに実施する必要があります。</p> <p>実際にはユーザの入力不備やシステムの不具合が発生することも考えられますから、かなりタイトなスケジュールで原因を特定し修正する必要があります。そこで、もっと早期にこれらの問題を発見できないかと考え、CronJobを毎日実行するようにして、対象月の初日から実行した日の前日までの会計計算を行う仕組みとしました。</p> <pre class="code mermaid" data-lang="mermaid" data-unlink>gantt dateFormat MM-DD axisFormat %d tickInterval 1month section 3月2日の処理 3月1日まで計算 :2014-03-01, 1d section 3月3日の処理 3月2日まで計算 :2014-03-01, 2d section 3月4日の処理 3月3日まで計算 :2014-03-01, 3d section 3月5日の処理 3月4日まで計算 :2014-03-01, 4d</pre> <p>この仕組みにより、月末を待つことなく、毎日少しずつ増えるイベントを対象に実際の処理を実行し、チェックを行うことができるようになりました。結果として、「締め」を余裕を持って行うことができるようになっています。</p> <blockquote><p><a href="https://www.amazon.co.jp/%E9%81%94%E4%BA%BA%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%83%BC-%E2%80%95%E7%86%9F%E9%81%94%E3%81%AB%E5%90%91%E3%81%91%E3%81%9F%E3%81%82%E3%81%AA%E3%81%9F%E3%81%AE%E6%97%85%E2%80%95-%E7%AC%AC2%E7%89%88-David-Thomas-ebook/dp/B08T9BXSVD">達人プログラマー第二版</a> Tip 42 「少しずつ進めること―――常に」</p></blockquote> <h2 id="会計数値の妥当性をダッシュボードに表示する">会計数値の妥当性をダッシュボードに表示する</h2> <p>「CADDi Manufacturing」では毎月大量の取引を行っており、人間による妥当性チェックには限界があります。できるだけ自動的に検証することはできないかと考えて、検証機能をデザインしました。</p> <p>検証機能の1つを紹介しますと、一定期間中の製品の入庫と出庫のイベントによって変動した在庫数量の合計と、その期間の開始と終了の間の在庫数の差分が一致しているかをチェックしています。</p> <pre class="code mermaid" data-lang="mermaid" data-unlink>flowchart LR 1a[入庫: 2個] --&gt; 1b[&#34;イベントの合計: (2 - 1 = 1) 個&#34;] 1c[出庫: 1個] --&gt; 1b 1d[開始時点の在庫数: 1個] --&gt; 1f[&#34;在庫数の差分: (2 - 1 = 1) 個&#34;] 1e[終了時点の在庫数: 2個] --&gt; 1f 1b --&gt; 1g[1 == 1: OK] 1f --&gt; 1g 2a[入庫: 2個] --&gt; 2b[&#34;イベントの合計: (2 - 2 = 0) 個&#34;] 2c[出庫: 2個] --&gt; 2b 2d[開始時点の在庫数: 1個] --&gt; 2f[&#34;在庫数の差分: (2 - 1 = 1) 個&#34;] 2e[終了時点の在庫数: 2個] --&gt; 2f 2b --&gt; 2g[0 != 1: NG] 2f --&gt; 2g</pre> <p>この検証機能により、次の項目を検証することが可能になりました。</p> <ul> <li>上流システムが、ヌケモレやダブりなく、入庫、出庫イベントを送信しているか?</li> <li>会計システムが、間違いなく入庫、出庫イベントを会計データに変換しているか?</li> </ul> <p>この検証結果は <a href="https://www.datadoghq.com/">Datadog</a> 上にダッシュボード化されていて、一目で異常が発生したかどうか、異常の発生した割合がどれくらいかが分かる仕組みとなっています。</p> <p><figure class="figure-image figure-image-fotolife" title="Datadog Dashboard"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240319/20240319095022.png" width="800" height="435" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Datadog Dashboard</figcaption></figure></p> <h1 id="会計システムのモデリングと最初の開発">会計システムのモデリングと最初の開発</h1> <p>開発初期は以下の流れで設計を進めました。</p> <ol> <li>仕訳<a href="#f-9d95098c" id="fn-9d95098c" name="fn-9d95098c" title="企業のお金の流れを記録するもの">*1</a>の流れを整理する</li> <li>ドメインモデルとデータベースモデルを作る</li> <li>最初の開発をどの機能にするか決める</li> </ol> <h2 id="仕訳の流れを整理してドメインモデルデータベースモデルを作る">仕訳の流れを整理して、ドメインモデル、データベースモデルを作る</h2> <p>まず、以下の様な図で仕訳の流れを整理しました。</p> <pre class="code mermaid" data-lang="mermaid" data-unlink>flowchart LR k[買掛金] -- 入荷 --&gt; s[仕掛品] s -- 製品完成 --&gt; p[製品] p -- 原価計上 --&gt; 売上原価</pre> <p>イベントによって、どのように仕訳の勘定科目が移り変わって行くのかを図示しています。例えば1つ目の矢印では、「入荷」というイベントによって、「買掛金」という勘定科目の金額が増えるととともに、「仕掛品」という勘定科目の金額が増えることを示しています。</p> <p>この図を用いて、生産管理システムで発生する入荷や製品完成などのイベントによって、どのような仕訳が生まれるのかを経理部門と認識を合わせます。 Miro上に描かれたラフなポンチ絵を使っておおまかに擦り合わせていきます。</p> <p>そして、以下のようなドメインモデルとデータベースモデルを初期に作成し、経理部門にレビューしてもらいながら進めていたのですが、ここで違和感を感じ始めます。</p> <p><figure class="figure-image figure-image-fotolife" title="データベースモデル"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240319/20240319095025.png" width="750" height="527" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>データベースモデル</figcaption></figure></p> <p><figure class="figure-image figure-image-fotolife" title="ドメインモデル"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240319/20240319095028.png" width="767" height="487" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>ドメインモデル</figcaption></figure></p> <h2 id="ユーザーの言葉で話す">ユーザーの言葉で話す</h2> <p>レビュー会では目立った指摘を受けることなく設計が進んでいました。手戻りが少ないのは嬉しいですが、正しいものがきちんと設計できているのか、不安視する声もエンジニアからは上がってきます。</p> <p>そんなある日、とあるレビュー会で処理の内容を説明するために、仕訳の表を用いて説明をしたときのことです。経理部門からはいつもよりも多くの発言を頂き、とても有意義なディスカッションが実施できたのを記憶しています。</p> <p><figure class="figure-image figure-image-fotolife" title="仕訳の表"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240319/20240319095031.png" width="800" height="172" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>仕訳の表</figcaption></figure></p> <p>考えてみれば、ドメインモデルやデータモデルはエンジニアの言語です。仕訳の表は経理部門の言語です。経理部門の言語でエンジニアが会話したことにより、経理部門の理解が進んだ結果、有意義なディスカッションが発生したのだと考えています。</p> <p>ドメインエキスパートの日々の仕事内容にまで踏み込んで会話して初めて、良いプロダクトができる、ということを実感したエピソードでした。</p> <blockquote><p><a href="https://www.amazon.co.jp/%E9%81%94%E4%BA%BA%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%83%BC-%E2%80%95%E7%86%9F%E9%81%94%E3%81%AB%E5%90%91%E3%81%91%E3%81%9F%E3%81%82%E3%81%AA%E3%81%9F%E3%81%AE%E6%97%85%E2%80%95-%E7%AC%AC2%E7%89%88-David-Thomas-ebook/dp/B08T9BXSVD">達人プログラマー第二版</a> Tip 78 「ユーザーとともに働き、ユーザーのように考える」</p></blockquote> <h2 id="最初の開発をどの機能にするか検討する">最初の開発をどの機能にするか検討する</h2> <p>設計は進めていたものの、開発すべき仕訳の種類は多種多様で、どこから手をつければ良いか全く検討がついていませんでした。ただ、チームでは次に該当する機能を開発してリリースしよう、と話をしていました。</p> <ul> <li>仕訳はごく一部にしぼる</li> <li>システムアーキテクチャ全体を串刺す</li> <li>一部でもビジネスに貢献できる</li> </ul> <p>最終的に、製品仕訳についてイベントを収集して検証する機能を開発することに決定しました。</p> <ul> <li>製品仕訳に関わるイベントの収集</li> <li>製品仕訳に関わる仕訳の生成と保存</li> <li>製品仕訳と在庫数の検証</li> </ul> <p>製品仕訳について、システムアーキテクチャ全体を串刺して開発することにより、アーキテクチャに起因するリスクを早期に洗い出す狙いです。この機能はうまく完成し、その後は取り扱う仕訳の種類を増やしていくことで開発を進めることができました。これは、「曳光弾」と呼ばれる開発手法です。</p> <blockquote><p><a href="https://www.amazon.co.jp/%E9%81%94%E4%BA%BA%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%83%BC-%E2%80%95%E7%86%9F%E9%81%94%E3%81%AB%E5%90%91%E3%81%91%E3%81%9F%E3%81%82%E3%81%AA%E3%81%9F%E3%81%AE%E6%97%85%E2%80%95-%E7%AC%AC2%E7%89%88-David-Thomas-ebook/dp/B08T9BXSVD">達人プログラマー第二版</a> Tip 20 「目標を見つけるには曳光弾を使うこと」</p></blockquote> <h1 id="会計というドメインを-Rust-で表現する">会計というドメインを Rust で表現する</h1> <h2 id="New-Type-Pattern-と-Phantom-Type-Pattern">New Type Pattern と Phantom Type Pattern</h2> <p>金額や数値、IDなどの単純な項目は基本的に <a href="https://doc.rust-jp.rs/rust-by-example-ja/generics/new_types.html">"New Type Pattern"</a> を使用しています。"New Type Pattern"を使用することで、在庫数を金額に代入してしまうような、単純な代入のミスによる不具合の発生を防ぐことができます。</p> <p>同種の値は同じようなロジックを持つ事が多いですから、 "Phantom Type Pattern" の利用も積極的に行います。 "Phantom Type Pattern" については以下の記事を参照ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcaddi.tech%2Farchives%2F1373" title="DDDのパターンをRustで表現する ~ Value Object編 ~ - CADDi Tech Blog" class="embed-card embed-blogcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 190px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://caddi.tech/archives/1373">caddi.tech</a></cite></p> <p>下の例をご覧ください。加工後の数である<code>ProcessedQuantity</code>と在庫数である<code>InventoryQuantity</code>を別の型として表現しています。さらに、"Phantom Type Pattern"を使用して<code>i32</code>との相互変換処理は共通のものを定義しています。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">use</span> <span class="synPreProc">std</span><span class="synSpecial">::</span><span class="synPreProc">marker</span><span class="synSpecial">::</span>PhantomData; <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">TaggedQuantity</span><span class="synStatement">&lt;</span>T: <span class="synPreProc">quantity_type</span><span class="synSpecial">::</span>QuantityType<span class="synStatement">&gt;</span> { value: <span class="synType">i32</span>, quantity_type: PhantomData<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span>, } <span class="synStatement">pub</span> <span class="synStatement">type</span> <span class="synIdentifier">InventoryQuantity</span> <span class="synStatement">=</span> TaggedQuantity<span class="synStatement">&lt;</span><span class="synPreProc">quantity_type</span><span class="synSpecial">::</span>Inventory<span class="synStatement">&gt;</span>; <span class="synStatement">pub</span> <span class="synStatement">type</span> <span class="synIdentifier">ProcessedQuantity</span> <span class="synStatement">=</span> TaggedQuantity<span class="synStatement">&lt;</span><span class="synPreProc">quantity_type</span><span class="synSpecial">::</span>Processed<span class="synStatement">&gt;</span>; <span class="synStatement">pub</span> <span class="synStatement">mod</span> <span class="synIdentifier">quantity_type</span> { <span class="synStatement">use</span> <span class="synPreProc">std</span><span class="synSpecial">::</span><span class="synPreProc">fmt</span><span class="synSpecial">::</span>Debug; <span class="synComment">// Trait 制約をつけるための trait</span> <span class="synStatement">pub</span> <span class="synStatement">trait</span> <span class="synIdentifier">QuantityType</span>: <span class="synType">Eq</span> <span class="synStatement">+</span> <span class="synType">PartialEq</span> <span class="synStatement">+</span> Debug {} <span class="synComment">// PhantomData の型パラメータに渡すための抽象的な型</span> <span class="synPreProc">#[derive(</span><span class="synType">Debug</span><span class="synPreProc">, </span><span class="synType">Eq</span><span class="synPreProc">, </span><span class="synType">PartialEq</span><span class="synPreProc">)]</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">Inventory</span>; <span class="synStatement">impl</span> QuantityType <span class="synStatement">for</span> Inventory {} <span class="synComment">// PhantomData の型パラメータに渡すための抽象的な型</span> <span class="synPreProc">#[derive(</span><span class="synType">Debug</span><span class="synPreProc">, </span><span class="synType">Eq</span><span class="synPreProc">, </span><span class="synType">PartialEq</span><span class="synPreProc">)]</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">Processed</span>; <span class="synStatement">impl</span> QuantityType <span class="synStatement">for</span> Processed {} } <span class="synStatement">impl&lt;</span>T: <span class="synPreProc">quantity_type</span><span class="synSpecial">::</span>QuantityType<span class="synStatement">&gt;</span> TaggedQuantity<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> { <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">signum</span>(<span class="synType">&amp;</span><span class="synConstant">self</span>) <span class="synStatement">-&gt;</span> <span class="synType">i32</span> { <span class="synConstant">self</span>.value.<span class="synIdentifier">signum</span>() } } <span class="synStatement">impl&lt;</span>T: <span class="synPreProc">quantity_type</span><span class="synSpecial">::</span>QuantityType<span class="synStatement">&gt;</span> <span class="synType">From</span><span class="synStatement">&lt;</span><span class="synType">i32</span><span class="synStatement">&gt;</span> <span class="synStatement">for</span> TaggedQuantity<span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> { <span class="synStatement">fn</span> <span class="synIdentifier">from</span>(value: <span class="synType">i32</span>) <span class="synStatement">-&gt;</span> <span class="synType">Self</span> { <span class="synType">Self</span> { value, quantity_type: <span class="synIdentifier">PhantomData</span><span class="synSpecial">::</span><span class="synStatement">&lt;</span>T<span class="synStatement">&gt;</span> {}, } } } </pre> <h2 id="会計台帳を-Rust-で表現する">会計台帳を Rust で表現する</h2> <p>会計台帳を表現する会計仕訳のコードサンプルは以下です。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synComment">// 一定期間の台帳全体</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">AccountingJournal</span> { id: JournalId, transactions: <span class="synType">Vec</span><span class="synStatement">&lt;</span>AccountingTransaction<span class="synStatement">&gt;</span>, } <span class="synComment">// 台帳の1行</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">AccountingTransaction</span> { id: AccountingTransactionId, accounting_date: AccountingDate, occurred_at: EventDateTime, entries: AccountingEntrySet, } <span class="synStatement">pub</span> <span class="synStatement">enum</span> <span class="synIdentifier">AccountingEntrySet</span> { <span class="synComment">// 製品完成というイベントに対応するレコード</span> <span class="synIdentifier">ProductComplete</span>( AccountingInventoryEntry, AccountingWorkInProcessProductEntry, ), <span class="synComment">// ・・・ 各種イベントごとの定義が続く</span> } <span class="synComment">// 台帳の1行を構成する要素で、勘定科目「製品」の金額を示す</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">AccountingInventoryEntry</span> { id: EntryId, amount: TotalAmount, quantity: InventoryQuantity, } <span class="synComment">// 台帳の1行を構成する要素で、勘定科目「仕掛品」の金額を示す</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">AccountingWorkInProcessProductEntry</span> { id: EntryId, amount: TotalAmount, quantity: InventoryQuantity, } </pre> <p>台帳全体を表す<code>AccountingJournal</code>、台帳の一行を表す<code>AccountingTransaction</code>、1つの金額と勘定科目をセットにした<code>Accounting**Entry</code>などの要素を用いて台帳という概念を表現しています。</p> <p>最終形に至るまで何度もこのドメインの設計は見直しを行っています。最初はチームに会計知識が少ないところからスタートしましたが、開発を経るごとに知識が高まり、以前に書かれたコードの見直しが必要になったためです。</p> <p>ドメイン知識をRustのような言語で厳密にコード化すると、コンパイラに指摘された箇所からドメインへの理解が曖昧な点が分かることがあります。そのような気づきからドメイン知識をアップデートしてコードを改善し、ドメインへの理解を深めていく活動はとても楽しいものです。</p> <blockquote><p><a href="https://www.amazon.co.jp/%E9%81%94%E4%BA%BA%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9E%E3%83%BC-%E2%80%95%E7%86%9F%E9%81%94%E3%81%AB%E5%90%91%E3%81%91%E3%81%9F%E3%81%82%E3%81%AA%E3%81%9F%E3%81%AE%E6%97%85%E2%80%95-%E7%AC%AC2%E7%89%88-David-Thomas-ebook/dp/B08T9BXSVD">達人プログラマー第二版</a> Tip 65 「早めにリファクタリングすること、そしてこまめにリファクタリングすること」</p></blockquote> <h2 id="State-Machine-を型で表現する">State Machine を型で表現する</h2> <p>もう1つコード例を紹介しましょう。</p> <p>バッチ処理は以下の流れで実行されます。</p> <ol> <li>初期化</li> <li>イベントから仕訳(Journal)を生成する</li> <li>検証してReportを生成する</li> </ol> <p>以下は、1回のバッチ処理の進捗状況を示すクラスです。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synComment">// 初期化後の状態</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">CreationSetInitialized</span> { id: JournalCreationSetId, target_month: YearMonth, } <span class="synStatement">impl</span> CreationSetInitialized { <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">create_journal</span>(<span class="synConstant">self</span>, journal_id: JournalId) <span class="synStatement">-&gt;</span> CreationSetJournalCreated { CreationSetInventoryCreated { id: <span class="synConstant">self</span>.id, target_month: <span class="synConstant">self</span>.target_month, journal_id, } } } <span class="synComment">// 仕訳(Journal)生成後の状態</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">CreationSetJournalCreated</span> { id: JournalCreationSetId, target_month: YearMonth, journal_id: JournalId, } <span class="synStatement">impl</span> CreationSetJournalCreated { <span class="synStatement">pub</span> <span class="synStatement">fn</span> <span class="synIdentifier">create_report</span>( <span class="synConstant">self</span>, report_id: ReportId, ) <span class="synStatement">-&gt;</span> CreationSetReportCreated { CreationSetReportCreated { id: <span class="synConstant">self</span>.id, journal_id: <span class="synConstant">self</span>.journal_id, report_id, } } } <span class="synComment">// Report生成後の状態</span> <span class="synStatement">pub</span> <span class="synStatement">struct</span> <span class="synIdentifier">CreationSetReportCreated</span> { id: JournalCreationSetId, target_month: YearMonth, journal_id: JournalId, report_id: ReportId, } </pre> <p>状態ごとに別々の型を定義しています。処理が進むに従って情報が追加されるので、フィールドが増えていくようにしています<a href="#f-8eb07bd3" id="fn-8eb07bd3" name="fn-8eb07bd3" title="Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#という本を参考にしました">*2</a>。このような実装にすることで、以下のメリットがあります。</p> <ul> <li>状態ごとに型が定義できるので可読性が高くなる</li> <li><code>Option</code>を排除して分岐を少なく記述できる</li> </ul> <h1 id="おわりに">おわりに</h1> <p>今回は、会計システムのアーキテクチャと設計の進め方、Rustの実装サンプルを紹介しました。</p> <p>会計システムでは、モノづくり産業のほんの一部である会計という世界をシステムに落とし込む難しさ、面白さに向き合うことができました。CADDiでは、「リアルな世界をシステムに落とし込む難しさ×面白さ」に向き合う開発エンジニアを募集しています。</p> <p><a href="https://recruit.caddi.tech/">エンジニア向け採用情報</a></p> <div class="footnote"> <p class="footnote"><a href="#fn-9d95098c" id="f-9d95098c" name="f-9d95098c" class="footnote-number">*1</a><span class="footnote-delimiter">:</span><span class="footnote-text">企業のお金の流れを記録するもの</span></p> <p class="footnote"><a href="#fn-8eb07bd3" id="f-8eb07bd3" name="f-8eb07bd3" class="footnote-number">*2</a><span class="footnote-delimiter">:</span><span class="footnote-text"><a href="https://www.amazon.co.jp/-/en/Scott-Wlaschin/dp/1680502549">Domain Modeling Made Functional: Tackle Software Complexity with Domain-Driven Design and F#</a>という本を参考にしました</span></p> </div> youmts RustでWeb APIを作る際のエラーハンドリング hatenablog://entry/6801883189088460252 2024-03-06T18:41:43+09:00 2024-03-06T21:28:05+09:00 TL;DR エラーハンドリングを行う目的 エラーハンドリングが適切に行われているとどう嬉しいか 1. エラーの発生原因が分かる 2. レスポンスステータスを型安全に出し分けることが可能になる どうエラーハンドリングを行うのか 実装方法 エラー型の定義で気を付けるべきポイント なぜanyhowを利用しないのか エラーハンドリングを行う上で持っている課題感 Drawer Growth グループ バックエンドエンジニアの中野です。今回は、私が所属するチームで gRPC API を開発する際に実践している Rust でのエラーハンドリングについて紹介していきます。 TL;DR エラーの発生原因がわかる… <ul class="table-of-contents"> <li><a href="#TLDR">TL;DR</a></li> <li><a href="#エラーハンドリングを行う目的">エラーハンドリングを行う目的</a></li> <li><a href="#エラーハンドリングが適切に行われているとどう嬉しいか">エラーハンドリングが適切に行われているとどう嬉しいか</a><ul> <li><a href="#1-エラーの発生原因が分かる">1. エラーの発生原因が分かる</a></li> <li><a href="#2-レスポンスステータスを型安全に出し分けることが可能になる">2. レスポンスステータスを型安全に出し分けることが可能になる</a></li> </ul> </li> <li><a href="#どうエラーハンドリングを行うのか">どうエラーハンドリングを行うのか</a><ul> <li><a href="#実装方法">実装方法</a></li> <li><a href="#エラー型の定義で気を付けるべきポイント">エラー型の定義で気を付けるべきポイント</a></li> </ul> </li> <li><a href="#なぜanyhowを利用しないのか">なぜanyhowを利用しないのか</a></li> <li><a href="#エラーハンドリングを行う上で持っている課題感">エラーハンドリングを行う上で持っている課題感</a></li> </ul> <p>Drawer Growth グループ バックエンドエンジニアの中野です。今回は、私が所属するチームで gRPC API を開発する際に実践している Rust でのエラーハンドリングについて紹介していきます。</p> <h1 id="TLDR">TL;DR</h1> <ul> <li>エラーの発生原因がわかるようにエラー型を定義することが大切。</li> <li>anyhow は使わずに自前のエラー型を定義して利用する。</li> </ul> <h1 id="エラーハンドリングを行う目的">エラーハンドリングを行う目的</h1> <p>そもそもなぜエラーハンドリングを行う必要があるのでしょうか。私が所属するチームでは、以下目的を達成するためにエラーハンドリングを行っています。</p> <ul> <li>発生したエラーに関する情報をログに含めて、調査しやすくするため。</li> <li>API の利用者に適切なレスポンスを返すため。</li> </ul> <h1 id="エラーハンドリングが適切に行われているとどう嬉しいか">エラーハンドリングが適切に行われているとどう嬉しいか</h1> <p>エラーハンドリングが適切に行われている場合、我々は以下のような出力をログに埋め込むことができるようになります。</p> <pre class="code" data-lang="" data-unlink>DrawingServiceError * DrawingPageUseCaseError * DrawingPageError * Failed to cast 0 to PageNumber. PageNumber should be greater than 0. * out of range integral type conversion attempted</pre> <p>実行可能なコードは<a href="https://play.rust-lang.org/?version=stable&amp;mode=debug&amp;edition=2021&amp;gist=b77dc9833088e3125b4a8a0b70af5b98">こちら</a>。 エラーハンドリングを適切に行なった場合に嬉しいポイントは2つあります。</p> <h2 id="1-エラーの発生原因が分かる">1. エラーの発生原因が分かる</h2> <p>調査の際に「該当エラーはどの経路を通ってなぜ発生したのか」がログからすぐにわからないと辛いです。例えばどの API が呼び出されて発生したエラーなのか、コードベースにおけるどのレイヤーで発生したエラーなのか、といった情報がログを見るだけでわかると調査がスムーズに進みます。 エラー発生元のメッセージを見てみます。</p> <pre class="code" data-lang="" data-unlink>* Failed to cast 0 to PageNumber. PageNumber should be greater than 0. * out of range integral type conversion attempted</pre> <p>この出力がポイントで、このメッセージを見るだけで、「PageNumber は 0 より大きい数字である必要があるが、0 を PageNumber にキャストしようとして失敗した」という情報を得ることができます。 これがもし、以下のようなメッセージだと、ログを見るだけでは何が問題だったのかわからなくなり、エラーハンドリングを行なう旨みが半減してしまいます。</p> <pre class="code" data-lang="" data-unlink>// 悪い例1 // `std::num::TryFromIntError`に定義されたメッセージだけが出力される * out of range integral type conversion attempted // 悪い例2 // 0を何にキャストしようとして失敗したのか」がわからないメッセージが出力される * Failed to cast 0 * out of range integral type conversion attempted</pre> <p>これらの例を見るとわかるように、エラーメッセージを定義する際にはアプリケーションにおける文脈をエラーメッセージに残すことが大切です。</p> <p>(補足)stack trace を出力することでもエラーの発生経路はわかります。しかし、以下の技術的な理由から私たちのチームでは stack trace を出力することをやめました。</p> <ul> <li>チーム開発としてどこでエラーログを出力するのかポリシーを決めるのが難しかった。</li> <li>stack trace を出すにはエラーが発生した箇所でログを出力する必要があるが、ログ出力するコードを書くのを忘れる可能性がある。</li> </ul> <p>そのため、stack trace が本来持っている役割の一部をエラーメッセージに持たせる設計としています。</p> <h2 id="2-レスポンスステータスを型安全に出し分けることが可能になる">2. レスポンスステータスを型安全に出し分けることが可能になる</h2> <p>何かエラーが発生し、API 呼び出しが失敗した場合には、発生したエラーによって <a href="https://grpc.io/docs/guides/error/#error-status-codes">ステータスコード</a> を出し分ける必要があります。エラーを型で表現していると、このステータスコードの出し分けを型安全に行うことができて嬉しいです。 後ほど具体的に紹介する方法を使うと、エラー型を新しく定義するたびにエラーをステータスコードに変換する箇所でコンパイルエラーが発生し、エンジニアにステータスコードの定義を強制させることができます。そのためステータスコードへの変換漏れや意図しないステータスコードに変換されてしまう可能性をなくすことが可能です。これによって、問題に気づくのがランタイムからコンパイル時にシフトレフトでき、エラーを定義するする手間を考えてもトータルの開発スピードを向上させることができると考えています。</p> <h1 id="どうエラーハンドリングを行うのか">どうエラーハンドリングを行うのか</h1> <h2 id="実装方法">実装方法</h2> <p>必要なエラー型を Enum で定義していきます。この際、 実装を簡略化するために<a href="https://github.com/dtolnay/thiserror">thiserror</a>という crate を用いています。<code>thiserror::Error</code>を derive すると、自分で実装しなくてもコンパイル時に <code>std::error::Error</code> trait が実装され楽をできます。 以下は<code>DrawingUseCase</code>で利用するためのエラー型の例です。 <code>#[from]</code> attribute をつけると <code>From</code> trait が実装されるので、<code>?</code>演算子を使ってエラーハンドリングしていくことが可能になります。 このようなエラー型を我々のチームでは基本 trait 毎に定義するようしています。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">use</span> <span class="synPreProc">thiserror</span><span class="synSpecial">::</span>Error; <span class="synPreProc">#[derive(</span><span class="synType">Debug</span><span class="synPreProc">, Error)]</span> <span class="synStatement">pub</span> <span class="synStatement">enum</span> <span class="synIdentifier">DrawingUseCaseError</span> { <span class="synPreProc">#[error(</span><span class="synConstant">&quot;DrawingRepository&quot;</span><span class="synPreProc">)]</span> <span class="synIdentifier">DrawingRepository</span>(<span class="synPreProc">#[from]</span> DrawingRepositoryError), <span class="synPreProc">#[error(</span><span class="synConstant">&quot;CompanyRepository&quot;</span><span class="synPreProc">)]</span> <span class="synIdentifier">CompanyRepository</span>(<span class="synPreProc">#[from]</span> CompanyRepositoryError), } <span class="synComment">// DrawingUseCase trait</span> <span class="synStatement">pub</span> <span class="synStatement">trait</span> <span class="synIdentifier">DrawingUseCase</span> { async <span class="synStatement">fn</span> <span class="synIdentifier">create_drawing</span>( <span class="synType">&amp;</span><span class="synConstant">self</span>, command: CreateDrawingCommand, ) <span class="synStatement">-&gt;</span> <span class="synType">Result</span><span class="synStatement">&lt;</span>Drawing, DrawingUseCaseError<span class="synStatement">&gt;</span>; } <span class="synComment">// create_drawingの実装例</span> <span class="synStatement">fn</span> <span class="synIdentifier">create_drawing</span>( <span class="synType">&amp;</span><span class="synConstant">self</span>, command: CreateDrawingCommand, ) <span class="synStatement">-&gt;</span> <span class="synType">Result</span><span class="synStatement">&lt;</span>Drawing, DrawingUseCaseError<span class="synStatement">&gt;</span> { <span class="synComment">// この関数は Result&lt;Company, CompanyRepositoryError&gt;を返り値に持つ</span> <span class="synComment">// CompanyRepositoryErrorに対して#[from] attributeがつけられているので、</span> <span class="synComment">// ?演算子でDrawingUseCaseErrorへの変換が可能になっている。</span> <span class="synStatement">let</span> company <span class="synStatement">=</span> <span class="synIdentifier">get_company_by_name</span>(command.<span class="synIdentifier">company_name</span>())<span class="synSpecial">?</span>; ... drawing } </pre> <p>エラーを伝播させていくと、最終的には API のエンドポイントとなる箇所において自分たちで定義したエラー型を<code>tonic::Status</code>に変換する必要があります。このマイクロサービスの実装では、gRPC サービスを実装する際のデファクトである<a href="https://github.com/hyperium/tonic">tonic</a>という crate を利用しています。別の crate を利用している場合には、<code>tonic::Status</code>の箇所を適宜ステータスコードを表す別の型に読み替えてください。 この変換処理を行うために、定義したエラー型を<code>tonic::Status</code>に変換する<code>ToErrorStatus</code> trait と<code>ErrorHandler</code>struct を定義します。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">trait</span> <span class="synIdentifier">ToErrorStatus</span> { <span class="synStatement">fn</span> <span class="synIdentifier">build</span>(<span class="synConstant">self</span>, error_message: <span class="synType">String</span>) <span class="synStatement">-&gt;</span> <span class="synPreProc">tonic</span><span class="synSpecial">::</span>Status; } <span class="synStatement">struct</span> <span class="synIdentifier">ErrorHandler</span><span class="synStatement">&lt;</span><span class="synSpecial">'a</span>, Error: <span class="synPreProc">std</span><span class="synSpecial">::</span><span class="synPreProc">error</span><span class="synSpecial">::</span>Error<span class="synStatement">&gt;</span>(<span class="synType">&amp;</span><span class="synSpecial">'a</span> Error); </pre> <p>そして <code>ToErrorStatus</code> trait をこれまで定義してきたエラー型に対してそれぞれ実装していきます。ここでは<code>DrawingUseCaseError</code>に対する<code>ToErrorStatus</code>の実装だけを例に出していますが、同様にその他のエラー型に対しても<code>ToErrorStatus</code>を実装していく必要があります。例えば、<code>ErrorHandler(repository_error).build(error_message)</code>が呼び出されているので <code>RepositoryError</code>型にも<code>ToErrorStatus</code>を実装する必要があります。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">impl</span> ToErrorStatus <span class="synStatement">for</span> ErrorHandler<span class="synStatement">&lt;</span><span class="synSpecial">'_</span>, DrawingUseCaseError<span class="synStatement">&gt;</span> { <span class="synStatement">fn</span> <span class="synIdentifier">build</span>(<span class="synConstant">self</span>, error_message: <span class="synType">String</span>) <span class="synStatement">-&gt;</span> Status { <span class="synStatement">use</span> <span class="synPreProc">DrawingUseCaseError</span><span class="synSpecial">::</span><span class="synType">*</span>; <span class="synStatement">match</span> <span class="synConstant">self</span>.<span class="synConstant">0</span> { <span class="synIdentifier">DrawingRepository</span>(<span class="synPreProc">DrawingRepositoryError</span><span class="synSpecial">::</span><span class="synIdentifier">Repository</span>(repository_error)) <span class="synStatement">=&gt;</span> { <span class="synIdentifier">ErrorHandler</span>(repository_error).<span class="synIdentifier">build</span>(error_message) } <span class="synIdentifier">DrawingRepository</span>(<span class="synPreProc">DrawingRepositoryError</span><span class="synSpecial">::</span><span class="synIdentifier">ParseDrawingId</span>(_)) <span class="synStatement">=&gt;</span> { <span class="synPreProc">Status</span><span class="synSpecial">::</span><span class="synIdentifier">with_error_details</span>(<span class="synPreProc">Code</span><span class="synSpecial">::</span>Internal, error_message, <span class="synPreProc">ErrorDetails</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>()) } <span class="synIdentifier">CompanyRepository</span>(<span class="synPreProc">CompanyRepositoryError</span><span class="synSpecial">::</span><span class="synIdentifier">Repository</span>(repository_error)) <span class="synStatement">=&gt;</span> { <span class="synIdentifier">ErrorHandler</span>(repository_error).<span class="synIdentifier">build</span>(error_message) } } } } </pre> <p>最後に、以下のような関数を定義して、gRPC API のエンドポイントとなる<code>Result&lt;tonic::Response&lt;HogeResponse&gt;, tonic::Status&gt;</code>を返り値とする関数内で呼び出せるようにします。あとはエンドポイントとなる関数内でエラーハンドリングを行う際に、必要に応じて<code>to_error_status</code>関数を呼び出せば OK です。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synStatement">pub</span><span class="synSpecial">(</span><span class="synStatement">crate</span><span class="synSpecial">)</span> <span class="synStatement">fn</span> <span class="synIdentifier">to_error_status</span>(error: <span class="synStatement">impl</span> <span class="synType">Into</span><span class="synStatement">&lt;</span>GrpcServiceError<span class="synStatement">&gt;</span>) <span class="synStatement">-&gt;</span> Status { <span class="synStatement">use</span> <span class="synPreProc">GrpcServiceError</span><span class="synSpecial">::</span><span class="synType">*</span>; <span class="synStatement">let</span> error: GrpcServiceError <span class="synStatement">=</span> error.<span class="synIdentifier">into</span>(); <span class="synStatement">let</span> error_message <span class="synStatement">=</span> error.<span class="synIdentifier">to_traverse_error_message</span>(); <span class="synStatement">let</span> <span class="synType">mut</span> status <span class="synStatement">=</span> { <span class="synStatement">let</span> error_message <span class="synStatement">=</span> error_message.<span class="synIdentifier">clone</span>(); <span class="synStatement">match</span> <span class="synType">&amp;</span>error { <span class="synIdentifier">DrawingService</span>(service_error) <span class="synStatement">=&gt;</span> { <span class="synIdentifier">ErrorHandler</span>(service_error).<span class="synIdentifier">build</span>(error_message) } <span class="synIdentifier">CompanyService</span>(service_error) <span class="synStatement">=&gt;</span> { <span class="synIdentifier">ErrorHandler</span>(service_error).<span class="synIdentifier">build</span>(error_message) } } }; status.<span class="synIdentifier">set_source</span>(<span class="synPreProc">Arc</span><span class="synSpecial">::</span><span class="synIdentifier">new</span>(error)); <span class="synComment">// 任意の方法でログを出力する</span> <span class="synComment">// println!(&quot;{error_message}&quot;);</span> status } <span class="synPreProc">#[derive(</span><span class="synType">Debug</span><span class="synPreProc">, Error)]</span> <span class="synStatement">pub</span><span class="synSpecial">(</span><span class="synStatement">crate</span><span class="synSpecial">)</span> <span class="synStatement">enum</span> <span class="synIdentifier">GrpcServiceError</span> { <span class="synPreProc">#[error(</span><span class="synConstant">&quot;DrawingService&quot;</span><span class="synPreProc">)]</span> <span class="synIdentifier">DrawingService</span>(<span class="synPreProc">#[from]</span> DrawingServiceError), <span class="synPreProc">#[error(</span><span class="synConstant">&quot;CompanyService&quot;</span><span class="synPreProc">)]</span> <span class="synIdentifier">CompanyService</span>(<span class="synPreProc">#[from]</span> CompanyServiceError), } </pre> <p>このコードで利用している<code>to_traversal_error_message</code>メソッドはエラーの source を辿って全てを 1 つの String にまとめるための関数です。以下のエラーメッセージは<code>to_traverse_error_message</code>メソッドを利用して出力した例です。</p> <pre class="code" data-lang="" data-unlink>DrawingServiceError * DrawingPageUseCaseError * DrawingPageError * Failed to cast 0 to PageNumber. PageNumber should be greater than 0. * out of range integral type conversion attempted</pre> <p><code>to_traverse_error_message</code>メソッドを利用しない場合、 <code>DrawingServiceError</code>だけが出力され、<code>DrawingServiceError</code>の source を辿ったそれ以下の出力はなくなります。<code>to_traverse_error_message</code>メソッドの実装は<a href="https://play.rust-lang.org/?version=stable&amp;mode=debug&amp;edition=2021&amp;gist=b77dc9833088e3125b4a8a0b70af5b98">こちら</a>にあるので、気になる方は確認してみてください。 このメソッドと同様のことは<a href="https://github.com/dtolnay/anyhow">anyhow</a>の debug 出力でも可能ですが、以下の理由から自前で関数を実装しています。</p> <ul> <li>anyhow で wrap するのが面倒だった。</li> <li>anyhow の他の機能は不要で debug 出力だけが欲しかった。</li> <li>anyhow を使いたくなかったので、間違えて利用することが無いように crate から依存を外したかった。</li> </ul> <h2 id="エラー型の定義で気を付けるべきポイント">エラー型の定義で気を付けるべきポイント</h2> <p>気を付けるべきポイントとして「エラー型を共通化しないこと」が挙げられます。我々のチームの場合、Infra 層で利用している crate である<a href="https://github.com/SeaQL/sea-orm">sea-orm</a>が返すエラーをマッピングしている<code>RepositoryError</code>型以外は基本的に共通化せず個別で定義するようにしています。そのため、例えば以下のように、2 つの別の UseCase のエラーの variants の中身がほぼ同じになることもあり得ます。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synComment">// 良い例</span> <span class="synStatement">use</span> <span class="synPreProc">thiserror</span><span class="synSpecial">::</span>Error; <span class="synPreProc">#[derive(</span><span class="synType">Debug</span><span class="synPreProc">, Error)]</span> <span class="synStatement">pub</span> <span class="synStatement">enum</span> <span class="synIdentifier">DrawingUseCaseError</span> { <span class="synPreProc">#[error(</span><span class="synConstant">&quot;DrawingRepository&quot;</span><span class="synPreProc">)]</span> <span class="synIdentifier">DrawingRepository</span>(<span class="synPreProc">#[from]</span> DrawingRepositoryError), <span class="synPreProc">#[error(</span><span class="synConstant">&quot;CompanyRepository&quot;</span><span class="synPreProc">)]</span> <span class="synIdentifier">CompanyRepository</span>(<span class="synPreProc">#[from]</span> CompanyRepositoryError), } <span class="synPreProc">#[derive(</span><span class="synType">Debug</span><span class="synPreProc">, Error)]</span> <span class="synStatement">pub</span> <span class="synStatement">enum</span> <span class="synIdentifier">SalesUseCaseError</span> { <span class="synPreProc">#[error(</span><span class="synConstant">&quot;DrawingRepository&quot;</span><span class="synPreProc">)]</span> <span class="synIdentifier">DrawingRepository</span>(<span class="synPreProc">#[from]</span> DrawingRepositoryError), <span class="synPreProc">#[error(</span><span class="synConstant">&quot;SalesRepository&quot;</span><span class="synPreProc">)]</span> <span class="synIdentifier">SalesRepository</span>(<span class="synPreProc">#[from]</span> SalesRepositoryError), } </pre> <p>エラー型の variants に重複が増えてくると、つい「<code>DrawingUseCaseError</code>と<code>SalesUseCaseError</code>を統合して<code>UseCaseError</code>にしてしまおう」という誘惑に駆られるのですが、それはあまりいいアイデアではありません。</p> <pre class="code lang-rust" data-lang="rust" data-unlink><span class="synComment">// よくない例</span> <span class="synStatement">use</span> <span class="synPreProc">thiserror</span><span class="synSpecial">::</span>Error; <span class="synPreProc">#[derive(</span><span class="synType">Debug</span><span class="synPreProc">, Error)]</span> <span class="synStatement">pub</span> <span class="synStatement">enum</span> <span class="synIdentifier">UseCaseError</span> { <span class="synPreProc">#[error(</span><span class="synConstant">&quot;DrawingRepository&quot;</span><span class="synPreProc">)]</span> <span class="synIdentifier">DrawingRepository</span>(<span class="synPreProc">#[from]</span> DrawingRepositoryError), <span class="synPreProc">#[error(</span><span class="synConstant">&quot;CompanyRepository&quot;</span><span class="synPreProc">)]</span> <span class="synIdentifier">CompanyRepository</span>(<span class="synPreProc">#[from]</span> CompanyRepositoryError), <span class="synPreProc">#[error(</span><span class="synConstant">&quot;SalesRepository&quot;</span><span class="synPreProc">)]</span> <span class="synIdentifier">SalesRepository</span>(<span class="synPreProc">#[from]</span> SalesRepositoryError), } </pre> <p>なぜなら、そうしてしまうと前述したエラーハンドリングを適切に行なった場合の嬉しいポイントである「何が原因でエラーが発生したのかが分かる」が失われてしまうからです。<code>UseCaseError</code>以外も全て共通化してしまった場合、ログの出力は以下のようになり、「0 を PageNumber にキャストしようとして発生したエラーである」ことしかわからなくなってしまいます。</p> <pre class="code" data-lang="" data-unlink>ServiceError * UseCaseError * DomainError * Failed to cast 0 to PageNumber. PageNumber should be greater than 0. * out of range integral type conversion attempted</pre> <h1 id="なぜanyhowを利用しないのか">なぜanyhowを利用しないのか</h1> <p>Rust でエラーハンドリングを行う際によく名前が挙がる crate として anyhow がありますが、上記説明の通り我々は anyhow を利用していません。 anyhow の利用例はリポジトリのサンプルを見るとよくわかります。anyhow を利用すると、関数の返り値を<code>anyhow::Result&lt;i32&gt;</code>のように定義することで関数内では<code>?</code>演算子を使うだけでよくなり、自分でエラー型を定義する手間が省けます。一時的に利用するだけのスクリプトを書く際や利用者が限られる開発者用ツールを作る際など、エラー型を厳密に定義する必要がなく、anyhow を使うと楽に実装できるケースも多々あります。しかし、我々のケースのようにクライアントから利用される Web API を作る場合にはステータスコードの出し分けが必要であるはずですし、運用のために適切なログも必要になるはずです。この場合多少面倒でも、自分自身でエラー型を定義した方が型安全でデバッグに有益なコードを書くことができ、トータルの生産性は高くなると考えています。</p> <p>実は以前、弊社で別のアプリケーションにおける Web API を作る際に anyhow を用いて実装したことがあったのですが、とても辛い結果になったという過去があります。具体的な辛いポイントとしては以下の要素などが挙げられます。</p> <ul> <li>context を引き回すために常に <code>with_context</code>をつけて回らなければいけない。</li> <li>ログを見てもエラーの発生箇所を示すだけで原因がわからない。</li> <li>ステータスコードの出し分けがエラーメッセージの文字列に頼るしかない。</li> </ul> <h1 id="エラーハンドリングを行う上で持っている課題感">エラーハンドリングを行う上で持っている課題感</h1> <p>ステータスコードの出し分けをする箇所の実装がごちゃつくことを現状の課題感として持っています。コードベースの成長に伴い UseCase や Infra、Domain 層の種類も増え、<code>ToErrorStatus</code>を実装するコードがどんどん肥大化して見通しが悪くなってきます。型で守ることができているとはいえ、ここはもう少し上手くやる方法がないか頭を悩ませているところです。</p> shogo-nakano-desu 大量のSeedデータの管理基盤としてAirtableを活用したら開発体験が素晴らしかった話 hatenablog://entry/6801883189088292875 2024-03-06T09:03:40+09:00 2024-03-06T09:04:08+09:00 はじめに こんにちは。CADDiでバックエンドエンジニアとして働いている中山です。 今日は、プロダクト開発において大量Seedデータの管理基盤としてAirtableを使ったら開発体験が素晴らしかったのでご紹介しようと思います。 ※ 以下の内容はAirtableの契約プランによって機能が異なること、執筆時にはできないが今後機能が追加されてできるようになっている可能性があることはご了承ください。 はじめに 背景 Airtableとは Airtableでできること UI上で操作が完結し、データの追加/編集がサクサクできる 表計算ソフトでおなじみの便利機能がたくさんある Web APIでCRUD操作が… <h1 id="はじめに">はじめに</h1> <p>こんにちは。CADDiでバックエンドエンジニアとして働いている中山です。 今日は、プロダクト開発において大量Seedデータの管理基盤としてAirtableを使ったら開発体験が素晴らしかったのでご紹介しようと思います。</p> <p>※ 以下の内容はAirtableの契約プランによって機能が異なること、執筆時にはできないが今後機能が追加されてできるようになっている可能性があることはご了承ください。</p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#背景">背景</a></li> <li><a href="#Airtableとは">Airtableとは</a></li> <li><a href="#Airtableでできること">Airtableでできること</a><ul> <li><a href="#UI上で操作が完結しデータの追加編集がサクサクできる">UI上で操作が完結し、データの追加/編集がサクサクできる</a></li> <li><a href="#表計算ソフトでおなじみの便利機能がたくさんある">表計算ソフトでおなじみの便利機能がたくさんある</a></li> <li><a href="#Web-APIでCRUD操作ができる">Web APIでCRUD操作ができる</a></li> <li><a href="#IDの生成をAirtableにお任せできる">IDの生成をAirtableにお任せできる</a></li> <li><a href="#RDBのようにテーブル間にリレーションを作成できる">RDBのようにテーブル間にリレーションを作成できる</a></li> <li><a href="#Airtable-Automation--Airtable-Scripting">Airtable Automation &amp; Airtable Scripting</a></li> <li><a href="#細かく権限管理ができる">細かく権限管理ができる</a></li> </ul> </li> <li><a href="#Airtableでできないこと">Airtableでできないこと</a><ul> <li><a href="#データベース間で同期できるテーブル数に上限がある">データベース間で同期できるテーブル数に上限がある。</a></li> <li><a href="#RDBのようなカスケード削除の機能がない">RDBのようなカスケード削除の機能がない</a></li> </ul> </li> <li><a href="#実際に使ってみて">実際に使ってみて</a></li> <li><a href="#おわりに">おわりに</a></li> </ul> <h1 id="背景">背景</h1> <p>私が開発に携わっているプロダクトではDBのテーブル数が80程度あり、そのうち約半数のテーブルにSeedデータ(※1)を投入する必要があります (このプロダクトの詳細については割愛させてください、それだけで記事になってしまいます)。開発当初はコード上でデータを定義していましたが、以下の課題がありました。</p> <ol> <li>データの量が多く開発工数が膨らむ <ul> <li>データ実装だけで1スプリント終わってしまうなんてことも、、、</li> </ul> </li> <li> 実装ミスが多発。レビューでも気づかれずに不具合に <ul> <li>データ間のリレーションや実装漏れなど</li> </ul> </li> <li>いい感じの変数名を考えるのが面倒</li> <li>少しのデータ変更を反映するだけでもリリースサイクルに合わせないといけない</li> </ol> <p>これらの課題を解決するために、我々のチームは<strong>SeedデータをAirtableで管理する</strong>ことを決めました。 <Br /></p> <p>※1: ユーザーがシステムを使うために最初にDBに入れておく必要があるデータ <Br /> (例: フォームで使う選択肢)</p> <h1 id="Airtableとは">Airtableとは</h1> <p>Airtableは表計算ソフトとデータベースの機能を併せ持つ、Airtable社が提供しているクラウドベースのデータベースツールです。 <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fairtable.com%2F" title="The platform to build next‒gen apps | Airtable" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://airtable.com/">airtable.com</a></cite> UIは以下のようになっていて、RDBでいうところのUser(左上のタブ)がテーブルを、行がレコードを、列がカラムを表しています。以下はサンプルでUserとCompanyテーブルを実装しています。</p> <p><figure class="figure-image figure-image-fotolife" title="AirtableのUI"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240306/20240306090347.png" width="800" height="137" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>AirtableのUI</figcaption></figure></p> <h1 id="Airtableでできること">Airtableでできること</h1> <h2 id="UI上で操作が完結しデータの追加編集がサクサクできる">UI上で操作が完結し、データの追加/編集がサクサクできる</h2> <p>AirtableはGUIベースで操作でき、コードで実装するよりも格段に早くデータを作成することができます。また、一般的な表計算ソフトと同じような感覚で使えるため、エンジニア以外でも簡単に操作することができます。</p> <p>またコードだといい感じの変数名を考える必要があり面倒(似たような名前の表現を迷ったり、やたらと長い変数名になってしまったり)でしたが、Airtableであれば不要です。</p> <h2 id="表計算ソフトでおなじみの便利機能がたくさんある">表計算ソフトでおなじみの便利機能がたくさんある</h2> <p>AirtableではSortやFilter、GroupBy、Lookupといった表計算ソフトでおなじみの機能が使えることでデータの視認性が格段に向上します。</p> <p>また、カラム毎にデータ型の入力制限(テキスト、 数値、 選択肢、 .etc)や、関数による値の自動生成、テーブルやカラムに説明文の記載といった便利な機能がたくさんあります。以下の例だと、age列には数値以外入力できないようにし、Name列には関数でLastNameとFirstNameを結合させる、みたいなことができます。</p> <p><figure class="figure-image figure-image-fotolife" title="カラムのカスタマイズ例"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240306/20240306090402.png" width="800" height="308" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>カラムのカスタマイズ例</figcaption></figure></p> <p>これらの機能を使いこなすことによって、ミスの予防や早期発見に繋がり安全に早く開発できるようになりました。</p> <h2 id="Web-APIでCRUD操作ができる">Web APIでCRUD操作ができる</h2> <p>AirtableにはWeb APIが用意されており、基本的なCRUD操作が可能で、JavaScript(TypeScript)やRuby、 .NET、 Python等で書くことができます。</p> <p>このAPI経由でAirtableからDBへデータを投入しています。</p> <p><a href="https://airtable.com/developers/web/api/introduction">Airtable Web API</a></p> <h2 id="IDの生成をAirtableにお任せできる">IDの生成をAirtableにお任せできる</h2> <p>AirtableのレコードにはデフォルトでRecord IDが付与されます(わかりやすいようにテーブルに表示させています)。 このIDもAPIで取得できるため、そのままDBのIDとして使うことができます。</p> <p><figure class="figure-image figure-image-fotolife" title="Record ID"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240306/20240306090356.png" width="800" height="240" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Record ID</figcaption></figure></p> <h2 id="RDBのようにテーブル間にリレーションを作成できる">RDBのようにテーブル間にリレーションを作成できる</h2> <p>AirtableにはLinkedRecordというデータ種別があり、以下だとUserとCompany列がそれにあたります。</p> <p><a href="https://support.airtable.com/docs/linking-records-in-airtable">Linking Records in Airtable | Airtable Support</a></p> <p>UI上では”A株式会社”や”山田太郎”のような値が表示されていますが、セルに格納されている値はRecord IDです。 APIで取得できるのもRecord IDなので、そのままRDBの外部キー制約を満たす形で投入できます。(これが表計算ソフトとデータベースツールの機能を併せ持っていることの良さです)</p> <p>このLinkedRecordのリレーションは1:1、1:n、n:m全てに対応しており、設定で制限をかけることもできます。</p> <p><figure class="figure-image figure-image-fotolife" title="Linked Record"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240306/20240306090353.png" width="800" height="136" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240306/20240306090350.png" width="800" height="123" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Linked Record</figcaption></figure></p> <h2 id="Airtable-Automation--Airtable-Scripting">Airtable Automation &amp; Airtable Scripting</h2> <p>Airtable Automationsという機能があり、簡単なWorkflowを組むことができます。</p> <p><a href="https://www.airtable.com/platform/automations">Airtable Automations - Get More Work Done | Airtable</a></p> <p>例えば、以下ではUserテーブルにレコードが作成されたら特定のSlackチャネルにメッセージを通知する、みたいなことができます。(もちろんもっと色々できます)</p> <p><figure class="figure-image figure-image-fotolife" title="Airtable Automationsの例"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240306/20240306090359.png" width="800" height="245" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span><figcaption>Airtable Automationsの例</figcaption></figure></p> <p>また、Airtable Scriptingを使えば、JavaScriptで書いたスクリプトをWorkflowに組み込むこともできます。</p> <p><a href="https://airtable.com/developers/scripting">Airtable Scripting</a></p> <p>我々のチームではデータの入力漏れがないかを定期的にチェックするスクリプトを実装してミスを早期発見できる仕組みを自動化していました。</p> <h2 id="細かく権限管理ができる">細かく権限管理ができる</h2> <p>Airtableでは、ユーザー毎に細かく権限管理ができます。 例えば、操作に慣れているエンジニアのみレコードの削除が可能、であったり管理者以外はテーブルの定義(カラムのデータ型やFilter条件など)を変更できないようにするなど、用途に合わせて自由度高く権限を設定することができます。 <Br /> これによって操作に不慣れなメンバーの操作ミスによって環境が壊れてしまった、などのリスクを減らすことができます。もし環境が壊れてしまった場合でもバックアップされているのでSnapshotによって過去の状態に戻すことも可能です。</p> <p>今回紹介した機能はごく一部で、Airtableにはまだまだ便利な機能があるので興味ある方は公式ドキュメントを御覧ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fairtable.com%2F" title="The platform to build next‒gen apps | Airtable" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://airtable.com/">airtable.com</a></cite></p> <h1 id="Airtableでできないこと">Airtableでできないこと</h1> <p>Airtableを活用することで様々なメリットがあることを紹介しましたが、実現できなかったこともあります。</p> <h2 id="データベース間で同期できるテーブル数に上限がある">データベース間で同期できるテーブル数に上限がある。</h2> <p>通常、開発環境毎にAirtableを用意して運用すると思いますが、最上位プランでもAirtable間で同期できるテーブル数に上限があり全てのテーブルを同期できませんでした。</p> <p><a href="https://support.airtable.com/docs/getting-started-with-airtable-sync">Getting started with Airtable sync | Airtable Support</a></p> <p>手動によるデータ同期は手間やミス予防の観点で許容できなかったため、全環境に対して共通のAirtable1つだけで運用しています。 不安はありましたが、データを反映する際はdevelopやstaging環境で、データ反映後に動作確認したうえで本番環境に反映させるため、半年以上運用してトラブルになったことはほとんどありません。</p> <h2 id="RDBのようなカスケード削除の機能がない">RDBのようなカスケード削除の機能がない</h2> <p>先述した通りLinkedRecordという仕組みでデータ間にリレーションを作成することができますが、RDBのカスケード削除のような依存関係のあるデータを一括で削除する仕組みがありません。(ネットには要望の声が多数あり、将来的には実装されるかもしれません)</p> <p>手動で関連するデータを削除してまわる運用ではミスを防げないため、我々のチームではデータを削除したい場合はフラグを付けてFilterでデータ投入対象から弾くという工夫をして運用しています。</p> <p>上記のようにできないことはあるものの、今のところ運用の工夫でカバーできています。</p> <h1 id="実際に使ってみて">実際に使ってみて</h1> <p>結論としてAirtableをSeedデータの管理基盤にしたことは良い判断だったと思います。</p> <p>一番良かったことはデータの更新サイクルを素早く回せるようになったことだと思います。 リリースサイクルとは別にAirtableを変更してデータ投入のジョブを実行するだけで反映できるため、ちょっとした文言の変更やフォームの選択肢を一つ追加してほしい、といった要望に対して素早く対応できるようになりました。</p> <p>加えてデータの実装速度が向上し、かつミスも減少したことで開発効率が劇的に改善し、機能開発など本質的な開発に多くの時間を使えるようになったことも大きなメリットです。</p> <h1 id="おわりに">おわりに</h1> <p>本稿では、Seedデータの管理基盤としてAirtableを活用することの利点やできないこと、それに対する運用上の工夫を紹介しました。もし大量のSeedデータを取り扱うことになったとき、Airtableを使う方法があるということを選択肢の一つとして検討してもらえれば幸いです。</p> <p>CADDiでは現在、私たちと一緒に開発を推進してくださるメンバーを募集しています。 以下に採用情報を掲載しますので、興味のある方はぜひご連絡お待ちしています!</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Frecruit.caddi.tech%2F" title="CADDi 採用情報 for Engineer/Designer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://recruit.caddi.tech/">recruit.caddi.tech</a></cite></p> nakayamaryo モノレポの Python バージョンを 3.9 から 3.11 に上げる hatenablog://entry/6801883189088362917 2024-03-05T16:31:06+09:00 2024-03-05T16:31:07+09:00 はじめに AI Team MLOps Engineer の西原です。前回は kubeflow pipeline(kfp)のローカル環境での実行について Tech Blog を書きました。kfp は 2024 年に入ってからローカル環境の実行以外にも嬉しいアップデートがあったのでそれに少し絡めて今回の取り組みを紹介しようと思います。 今回の取り組みは、モノレポで使っている Python の最低バージョンを 3.9 から 3.11 に上げるというものです。なぜ、バージョンを上げたのか、上げる際の障壁とその対応を紹介しようと思います。 はじめに なぜ Python バージョンを上げたのか パッケージ… <h1 id="はじめに">はじめに</h1> <p>AI Team MLOps Engineer の西原です。<a href="https://caddi.tech/archives/4850">前回</a>は kubeflow pipeline(kfp)のローカル環境での実行について Tech Blog を書きました。kfp は 2024 年に入ってからローカル環境の実行以外にも嬉しいアップデートがあったのでそれに少し絡めて今回の取り組みを紹介しようと思います。</p> <p>今回の取り組みは、モノレポで使っている Python の最低バージョンを 3.9 から 3.11 に上げるというものです。なぜ、バージョンを上げたのか、上げる際の障壁とその対応を紹介しようと思います。</p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#なぜ-Python-バージョンを上げたのか">なぜ Python バージョンを上げたのか</a><ul> <li><a href="#パッケージ更新を頻繁にする理由">パッケージ更新を頻繁にする理由</a></li> <li><a href="#パッケージの更新ができなくなった">パッケージの更新ができなくなった</a></li> <li><a href="#torchserve-と各ソフトウェアのバージョン">torchserve と各ソフトウェアのバージョン</a></li> </ul> </li> <li><a href="#Python-のバージョンをどこまで上げるか">Python のバージョンをどこまで上げるか</a></li> <li><a href="#torchserve-のコンテナイメージを自分たちで構築する">torchserve のコンテナイメージを自分たちで構築する</a><ul> <li><a href="#torchserve-メトリクスのエラー対応">torchserve メトリクスのエラー対応</a></li> </ul> </li> <li><a href="#pyupgrade-で-Python-コードをアップデートする">pyupgrade で Python コードをアップデートする</a></li> <li><a href="#まとめ">まとめ</a></li> <li><a href="#参考">参考</a></li> </ul> <h1 id="なぜ-Python-バージョンを上げたのか">なぜ Python バージョンを上げたのか</h1> <h2 id="パッケージ更新を頻繁にする理由">パッケージ更新を頻繁にする理由</h2> <p>Python のバージョンを上げるにあたって、依存するパッケージの更新の話が関係してきます。そのため、パッケージの更新について先に触れます。</p> <p>以前、モノレポに置いて依存するパッケージは高頻度でアップデートしているということを <a href="https://caddi.tech/archives/4362">Tech Blog</a> で紹介しました。その Tech Blog のおさらいになりますが、新しいバージョンはバグ修正や新規機能追加が含まれていることが多いので、バージョン更新することでバグを回避したり、新しい機能が使えるようになります。また、頻繁にアップデートすることで一回あたりの変更量が少なくなるため、何か問題があった時も少ない変更量から原因を特定することになり、デバッグも簡単になります。こういった背景から、私たちのモノレポでは依存するパッケージのバージョンを頻繁に上げています。</p> <h2 id="パッケージの更新ができなくなった">パッケージの更新ができなくなった</h2> <p>上記の通り、モノレポでは依存するパッケージのバージョンを頻繁に上げています。しかし、パッケージの更新ができないものが出てきました。更新できないパッケージというのは torchserve です。我々 AI Team では ML WebAPI のフレームワークとして <a href="https://pytorch.org/serve/">torchserve</a> を利用しています。torchserve 公式が提供しているコンテナイメージをベースイメージとして WebAPI を構築しています。この公式が提供しているコンテナイメージの環境が、私達に合わなくなりました。具体的には、torchserve <code>v0.9.0</code> から CUDA 12 系のコンテナイメージになり、これを使って Vertex AI Endpoints にデプロイすると、CUDA の認識ができずエラーになります。そのため、Vertex AI Endpoints を使うには CUDA 11 系の<code>v0.8</code>のコンテナイメージに依存することになりパッケージの更新ができなくなりました。とはいえ torchserve には信頼性に関する issue がずっと立っていたりと、これからのアップデートに期待してる部分があるため torchserve 自体のアップデートを諦めるわけにはいきません。そのため、公式が提供するコンテナイメージを使うのを辞め、自分たちに適したコンテナイメージを構築することにしました。</p> <h2 id="torchserve-と各ソフトウェアのバージョン">torchserve と各ソフトウェアのバージョン</h2> <p>自分達に適した torchserve のコンテナイメージを構築することでコントールできるようになったものが次になります。</p> <ul> <li>CUDA のバージョン</li> <li>torch と torchserve のバージョン</li> <li>Python のバージョン</li> </ul> <p>前置きが長くなりましたが、ここで Python バージョンの話になります。torchserve 公式が提供するコンテナイメージの Python バージョンは 3.9 でした。この公式のコンテナイメージへの依存を止めることで、Python のバージョンをコントロールできます。v3.10 以降の Python では型表現の強化や match 構文の追加、パフォーマンス改善などが含まれており、よりよい Python コードを書くことができるようになります。加えて Python には <a href="https://devguide.python.org/versions/">EOL</a> があり、古いバージョンのサポートが終わります。3.9 の EOL はまだ先ですが、直前になって慌ててバージョンアップするよりも、早めにバージョンアップしておいた方が良いと考え、Python のバージョンを上げることにしました。</p> <p>余談:CUDA のバージョンをコントールするのは、torchserve と Vertex AI Endpoints の相性が悪いというのも理由の一つですが、他にも理由があります。それは、<a href="https://pytorch.org/serve/">GitHub Actions の GPU runner</a> に向けた対応です。GitHub Actions で GPU を使えるようになるという話があり、現在はクローズドベータとして提供されているようです。このサービスが一般提供された時の利用に備えて CUDA のバージョンをコントロールすることで、導入負荷を下げることも目的の一つです。</p> <h1 id="Python-のバージョンをどこまで上げるか">Python のバージョンをどこまで上げるか</h1> <p>torchserve のコンテナイメージを自分たちで構築することで Python のバージョンを自由に選べるようになります。タイトルにもあるように Python 3.9 から 3.11 に上げたのですが、なぜ 3.11 なのかの意思決定の過程を紹介します。このタスクは 2024 年の新年最初のタスクとして取り組みました。私たちは Kubeflow Pipelines(kfp)を使って ML パイプライン開発をしていますが、2024 年に入った時点で kfp は protobuf 3 系に依存していました。モノレポで使っている 3rd party package の中には Python 3.11 以上から protobuf 4 系を求めるものがあるため、protobuf の依存の関係で 1 月はじめの時点では Python 3.10 までしか上げることができませんでした。そこで一度 Python 3.10 に上げたのですが、その数日後に kfp が protobuf 4 系に対応したため Python を 3.11 まで上げることができました。stable release として Python 3.12 が出ていますが、開発で使っているパッケージが 3.11 までしか対応していないものがあるため 3.11 までの更新にとどめました。</p> <h1 id="torchserve-のコンテナイメージを自分たちで構築する">torchserve のコンテナイメージを自分たちで構築する</h1> <p>先述の通り、torchserve のコンテナイメージを自分たちで管理します。この章ではコンテナイメージの管理について紹介します。まず、torchserve 公式の <a href="https://github.com/pytorch/serve/blob/master/docker/Dockerfile">Dockerfile</a> を見ると build 時の引数として Python、 CUDA のバージョンを指定できることがわかります。公式の Dockerfile を参考に、build 時に各ソフトウェアのバージョンを指定することで自分たちに適したコンテナイメージを構築することができます。</p> <p>Pants を使ってコンテナビルドの設定を記述すると次のようになります。</p> <pre class="code lang-python" data-lang="python" data-unlink>docker_image( name=<span class="synConstant">&quot;caddi-torchserve&quot;</span>, source=<span class="synConstant">&quot;Dockerfile&quot;</span>, extra_build_args=[ <span class="synConstant">&quot;PYTHON_VERSION=3.11&quot;</span>, <span class="synConstant">&quot;CUDA_VERSION=cu118&quot;</span>, ], ... ) </pre> <p>Pants を使わずともコンテナイメージをビルドして registry に push できますが、Pants で管理することでベースイメージを使った build/push が効率的に行えます。Pants には差分実行機能があり、あるコンテナイメージに変更があった場合に、それに依存するコンテナイメージを検知して一括で build/push できます。今回構築した torchserve のベースイメージに依存しているコンテナイメージは二桁に及び、今後も増えると予想しています。今後、開発規模が大きくなったとしても Pants を使ってコンテナイメージを管理することで一括で build/push できるため、運用負荷を下げることができそうです。</p> <h2 id="torchserve-メトリクスのエラー対応">torchserve メトリクスのエラー対応</h2> <p>Vertex AI Endpoints では torchserve 公式の CUDA 12 系ではうまく動きませんでしたが、今回構築した CUDA 11 系のコンテナイメージで動作することが確認できました。しかし、動作確認をする中で GPU のメトリクスを計測するプログラムでエラーが発生するようになりました。推論処理自体は問題なく動作しているため、このエラーを無視して利用することもできます。しかし、エラーによって狼少年アラートが発砲すると良くないため、エラーを解消することにしました。</p> <p>原因を調査すると<code>pynvml</code>というパッケージでエラーが発生していました。<a href="https://pypi.org/project/pynvml/">pynvml</a> の v11.5 では CUDA 11 との相性が悪いため、v11.4 にダウングレードすることでエラーを解消することができました。</p> <h1 id="pyupgrade-で-Python-コードをアップデートする">pyupgrade で Python コードをアップデートする</h1> <p>Python のバージョンを上げるにあたり、コードの構文も新しいものにアップデートしました。これまで使っていた Python 3.9 から 3.11 までの間で新しい構文が追加されています。例えば、<code>match</code>構文や<code>|</code>を使った型表現です。これらの新しい構文によって古い構文を置き換えることができます。機械的に置き換えできるものもあり、<code>pyupgrade</code>というツールを使って Python コードをアップデートしました。<a href="https://github.com/asottile/pyupgrade">pyupgrade</a> は Pants 公式がサポートしているため、特別な設定をすることなく使うことができます。</p> <p><a href="https://github.com/astral-sh/ruff">Ruff</a> でも pyupgrade のように Python の古い構文を新しい構文に置き換える機能があります。Ruff も Pants 公式がサポートしているため、簡単に利用できます。</p> <h1 id="まとめ">まとめ</h1> <p>モノレポで使っている Python のバージョンを 3.9 から 3.11 に更新する取り組みについて紹介しました。AI Team では開発で使っているソフトウェアのバージョンを更新できる仕組み作りをしています。Python のパッケージにおいても kfp や torch、pydantic などがここ一年でメジャーアップデートをしています。チームでこれらのアップデートに追従し、更新可能な古い機能で新規開発をしなくていいように努めています。また、Python に限らず Terraform や GitHub Actions などのツールについても同様の取り組みをしています。 古いバージョンのソフトウェアに依存していて生産性を低下させているが、更新が難しいというのを技術負債あるあるとしてよく耳にします。このような状況を回避するために、短いサイクルでソフトウェアを更新できる仕組みと体制が大事だと考え、チームで取り組んでいます。</p> <h1 id="参考">参考</h1> <ul> <li><a href="https://www.pantsbuild.org/2.19/docs/docker#dependency-inference-support">pants docker</a></li> </ul> nishikoh 第10回:Cloudflareの紹介と運用のポイント hatenablog://entry/6801883189086229637 2024-02-16T14:00:00+09:00 2024-03-02T09:35:41+09:00 ※本記事は、技術評論社「Software Design」(2024年1月号)に寄稿した連載記事「Google Cloudを軸に実践するSREプラクティス」からの転載1です。発行元からの許可を得て掲載しております。 はじめに 前回はDatadogによるクラウド横断のモニタリング基盤について解説しました。 今回はCloudflareとは何か、なぜ使っているのか、各サービスとポイント、キャディでの活用例を紹介します。 ▼図1 CADDiスタックにおける今回の位置付け Cloudflare とは 本記事では、Cloudflare社が提供しているプラットフォーム全体を「Cloudflare」とします。 … <p>※本記事は、技術評論社<a href="https://gihyo.jp/magazine/SD/archive/2024/202401">「Software Design」(2024年1月号)</a>に寄稿した連載記事「Google Cloudを軸に実践するSREプラクティス」からの転載<sup id="fnref:1"><a href="#fn:1" rel="footnote">1</a></sup>です。発行元からの許可を得て掲載しております。</p> <h2 id="はじめに">はじめに</h2> <p>前回はDatadogによるクラウド横断のモニタリング基盤について解説しました。 今回は<a href="https://www.cloudflare.com/">Cloudflare</a>とは何か、なぜ使っているのか、各サービスとポイント、キャディでの活用例を紹介します。</p> <p>▼図1 CADDiスタックにおける今回の位置付け <span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240226/20240226094527.png" width="903" height="448" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <h2 id="Cloudflare-とは">Cloudflare とは</h2> <p>本記事では、Cloudflare社が提供しているプラットフォーム全体を「Cloudflare」とします。 Cloudflareは、ひと昔前までは数あるシンプルな<a href="https://www.cloudflare.com/ja-jp/learning/cdn/what-is-a-cdn/">CDN(Contents Delivery Network)</a>サービスの1つでした。CDNとは、コンテンツの配信を最適化するためのネットワークです。コンテンツキャッシュを利用して、エンドユーザーにより早く効率的にコンテンツを配信できます。 近年、CDN事業者が提供するサービスは、単なるCachingやLoad Balancingだけではなくなってきています。Edge Cloud/Edge ComputingやSecurity領域など、Webアプリケーションのネットワーク上の“Edge”であることを活かしたさまざまなサービスを展開しています。その中でもCloudflareは、筆者の知る限り、最も先進的で幅広いスコープのサービスを提供するプラットフォームです。 執筆時点では、Cloudflare社は提供しているプラットフォーム全体を<a href="https://www.cloudflare.com/ja-jp/connectivity-cloud/">コネクティビティクラウド</a>と呼んでいます。また、それを構成する要素として次の3つのサービスがあります(図2)。</p> <ul> <li>アプリケーションとインフラストラクチャサービス</li> <li>開発者サービス</li> <li><a href="https://www.cloudflare.com/ja-jp/learning/access-management/what-is-sase/">SASE(Secure Access Service Edge)</a>と<a href="https://www.cloudflare.com/ja-jp/learning/access-management/security-service-edge-sse/">SSE(Security Service Edge)</a>のサービス</li> </ul> <p>▼図2 Edge ServerとOrigin Server <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208193343.png" alt="" /></p> <h2 id="なぜCloudflareか">なぜCloudflareか</h2> <p>キャディでは2019年にコーポレートサイト(Origin Server)の負荷を下げるためのCDNとしてCloudflareを使い始めました。理由は単純で、非常に安い費用で利用開始できたからです。 費用が安いことはCloudflareの大きな魅力の1つで、無償から使えるさまざまなサービスを提供しているため、個人開発での利用にもお勧めです。きっかけこそCDNとしての費用が理由ではありましたが、現在はCDN以外のさまざまなサービスを利用しています。 筆者が過去に所属していた企業では、<a href="https://www.akamai.com/">Akamai</a>や<a href="https://www.fastly.com/">Fastly</a>を利用していました。とくにFastlyは、Developer Friendlyで、柔軟かつ積極的なキャッシュ戦略をとれるのが魅力です。それが大量のコンテンツを配信するB2Cのサービスにはマッチしていて重宝していました。 一方、キャディはB2Bのサービスを提供しており、要件的にそれほど高度なキャッシュ戦略を必要としていません。そのため今となっては「先進的で幅広いサービスを提供しているプラットフォームで、それが事業にマッチしているから」という理由にとらえなおしています。</p> <h2 id="各サービスの紹介とポイント">各サービスの紹介とポイント</h2> <p>誌面の都合上、Cloudflareのサービスを網羅的に紹介することは難しいので、いくつかピックアップしてポイントを解説します。</p> <h3 id="Cloudflare-DNS">Cloudflare DNS</h3> <p><a href="https://developers.cloudflare.com/dns/">Cloudflare DNS</a>は一部機能を除いて無償で使えるDNSサービスです。 Cloudflare DNSを使ってDNS Recordを管理すると、図3のように設定画面上にProxy statusが表示され、このフラグをON(Proxied)にすることでEdge Serverへ向き先が変わります。nslookupすると、Proxy statusの状態によって返ってくる値が違うことを確認できます。DNSの設定は<a href="https://github.com/octodns/octodns">octoDNS</a>やTerraformでIaCしておくことをお勧めします。 また、Business Plan以上であれば、<a href="https://developers.cloudflare.com/dns/zone-setups/partial-setup/">任意のDNSプロバイダでCNAMEを指定</a>してEdge Serverへプロキシできます。つまり、Cloudflare DNSの利用は、Cloudflare CDNを使うための必須条件はでありません。</p> <p>▼図3 DNS Recordの画面 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208193335.png" alt="" /></p> <h3 id="Origin-Serverの保護">Origin Serverの保護</h3> <p>前述のとおり、Cloudflare DNSのProxy statusを変更するだけで、リクエストを簡単にEdge Server経由に切り替えられます。とはいえ、より安全かつ効率的に運用していくための準備が必要です。その準備の1つとして「Origin Serverの保護」について触れておきます。 前提として、Edge Server側でさまざまな最適化をするためにEdge Serverを経由させたいので、Origin Serverへの直接アクセスされるとその最適化が効かなくなってしまいます。仮にOrigin Serverの場所がユーザーや攻撃者に漏れてしまっても、そのリスクを最小化するために「<a href="https://developers.cloudflare.com/learning-paths/get-started/security/secure-origin/">Origin Serverの保護</a>」が必要です。具体的には、Origin Server側がEdge Serverを経由していないリクエストをブロックしたり、Cloudflareへの専用接続を作ったりします。方法には次のようなものがありますが、やり方によってセキュリティレベルや実装難易度、費用が変わってくるので、提供するプロダクトの要件しだいでどれを選択するか決めましょう。</p> <ul> <li>アプリケーションレイヤ <ul> <li>Cloudflare Tunnel (HTTP / WebSockets)</li> <li>HTTP Header Validation</li> <li>JSON Web Tokens (JWT) Validation</li> </ul> </li> <li>トランスポートレイヤ <ul> <li>Authenticated Origin Pulls</li> <li>Cloudflare Tunnel (SSH / RDP)</li> </ul> </li> <li>ネットワークレイヤ <ul> <li>Allowlist Cloudflare IP addresses</li> <li>Cloudflare Network Interconnect</li> <li>Cloudflare Aegis</li> </ul> </li> </ul> <p>「Cloudflare Tunnel」は、暗号化されたCloudflare専用の接続を構築することで、Origin Serverへの入口を非公開にできます。「HTTP Header Validation」は、Edge Serverで任意のヘッダを付与し、Origin Serverでそのヘッダがあるもののみ受け入れます。「JWT Validation」は、Cloudflare Access(後述)で付与された正しいJWTかどうかを検証して受け入れます。「Allowlist Cloudflare IP addresses」は、アクセス元が公開されているCloudflareのIPだったときのみ受け入れます。 手軽にやりたい場合は、「HTTP Header Validation」や「JWT Validation」がお勧めです。 Web Application Frameworkのミドルウェアで検証してもよいですが、Backend Applicationより前のレイヤで検証して関心事を分離するのもよいでしょう。GKE の場合、Service Mesh(Envoy)で検証できます。CloudRunの場合でも<a href="https://cloud.google.com/blog/ja/products/serverless/cloud-run-now-supports-multi-container-deployments">サイドカーが実装できるようになった</a>ので、同様にEnvoyでの検証ができます。<a href="https://cloud.google.com/armor">Google Cloud Armor</a>を使う場合、「Allowlist Cloudflare IP addresses」が選択肢の1つになるでしょう。しかし、Cloudflare WAF(後述)を使う場合、WAFが重複してしまうことや、最新のCloudflareのIPに追従する機能の費用が高いことに注意が必要です。 そのほかの準備や詳細は、<a href="https://developers.cloudflare.com/learning-paths/get-started/security/">公式のGettingStarted</a>を参照してください。</p> <h3 id="証明書">証明書</h3> <p><a href="https://developers.cloudflare.com/ssl/edge-certificates/">Edge Serverの証明書</a>は、デフォルトではCloudflareが発行したManagedな証明書を利用します。もちろんすでに別途発行済みの証明書をアップロードして使うこともできます。 また、「クライアントとEdge ServerとOriginServer間」の通信を常に暗号化しておくため、<a href="https://developers.cloudflare.com/ssl/origin-configuration/ssl-modes/">encryption modes(暗号化モード)</a>をFull以上にして、Origin Serverの証明書を設定しましょう。暗号化モードがFull未満の場合、「クライアントとEdge Server間」「Edge ServerとOriginServer間」のどちらか、または両方が暗号化なしで接続されます。 CloudflareにはOrigin CA(CertificateAuthority)証明書の発行機能があるので、それを利用するのがお勧めです。Kubernetesを使っている場合、cert-managerとOrigin CAIssuerを使って<a href="https://blog.cloudflare.com/automated-origin-ca-for-kubernetes/">証明書の発行を自動化</a>できます。</p> <h3 id="Cloudflare-Cache">Cloudflare Cache</h3> <p><a href="https://developers.cloudflare.com/cache/">Cloudflare Cache</a>は、Cloudflare CDNのコア機能です。コンテンツをEdge Server側でキャッシュすることにより、Origin Serverの負荷を下げつつ、エンドユーザーにより早くコンテンツを配信します。 基本となるデフォルトのキャッシュ動作は次のとおりです(もちろんカスタマイズにより、ほかのルールや設定にオーバーライドされる可能性があります)。</p> <p>キャッシュされないケース:</p> <ul> <li>ache-Control ヘ ッ ダ に「private」「no-store」「no-cache」「max-age=0」が設定されているとき</li> <li>Set-Cookieヘッダが存在するとき</li> </ul> <p>キャッシュされるケース:</p> <ul> <li>Cache-Controlヘッダがpublicに設定され、max-ageが0より大きいとき</li> <li>Expiresヘッダが未来の日付に設定されているとき</li> </ul> <p>デフォルトでは、HTMLはキャッシュされず定義されているファイルの拡張子をもとにキャッシュされます。たとえばJS/CSS/JPG/SVG/CSV/ICOなどが対象です。 実際のレスポンスがキャッシュされたものかどうかを確認するには、<code>CF-Cache-Status</code>ヘッダを参照します。ヘッダの値が<code>HIT</code>であればEdge Serverにキャッシュされたコンテンツであり、<code>MISS</code>であればOrigin Serverから返されたコンテンツです。 Edge Serverのキャッシュを削除(パージ)したいときは、Cloudflare DashboardやWeb APIのどちらからでも実行可能です。ただし、Enterprise Plan以外はURL単位でしかパージできません。たとえば、Webアプリケーションのデプロイ時に特定領域のキャッシュをまとめてパージできると便利なのですが、Enterprise Plan以外はそのオプションを利用できません。 また、キャッシュパージ以外にも契約しているプランごとに表1のような制限があります。</p> <p>▼表1 プラン別の制限内容</p> <table> <thead> <tr> <th> </th> <th> Free </th> <th> Pro </th> <th> Business </th> <th> Enterprise </th> </tr> </thead> <tbody> <tr> <td> HTTP POST Requestサイズ上限 </td> <td> 100MB </td> <td> 100MB </td> <td> 200MB </td> <td> デフォルト500MB(変更可能) </td> </tr> <tr> <td> キャッシュ可能なファイルサイズ上限 </td> <td> 512MB </td> <td> 512MB </td> <td> 512MB </td> <td> デフォルト5GB(変更可能) </td> </tr> <tr> <td> キャッシュパージの単位 </td> <td> URL </td> <td> URL </td> <td> URL </td> <td> URL,Hostname,Tag,Prefix </td> </tr> </tbody> </table> <h3 id="Web-Application-Security">Web Application Security</h3> <p>Cloudflare CDN を利用すると、自動的に<a href="https://developers.cloudflare.com/waf/">Cloudflare WAF</a>や <a href="https://developers.cloudflare.com/ddos-protection/">Cloudflare DDoSProtection</a>が有効になり、セキュリティを強化できます。どちらも無償から利用可能で、プランをアップグレードするとより高度な機能が利用できます。WebアプリケーションやOriginServerを守るため、これらを使うだけでもCloudflare CDNを導入する価値が十分あります。 ビジネス用途の場合、Cloudflare WAFの<a href="https://developers.cloudflare.com/waf/managed-rules/reference/owasp-core-ruleset/">OWASP Core Ruleset</a>を有効にしておきましょう。これは、<a href="https://owasp.org/about/">OWASPFoundation</a>というセキュリティ向上に取り組むコミュニティが定義しているWAF用の攻撃検出ルールセットです。 プロダクトの運用開始後にWAFを設定するとき、ユーザー影響が心配であれば、適用範囲絞ったり、OWASP Anomaly Score Thresholdを低めに設定したりできます。 また、<a href="https://developers.cloudflare.com/ruleset-engine/rules-language/actions/">ルールを検出したときのアクション</a>を設定しておくことができ、次の中から選択できます。Enterprise Planを契約している場合は、ルールを厳しめにしてからアクションをLogに設定してしばらく様子を見るのもありでしょう。</p> <ul> <li>Block: アクセスをブロックする</li> <li>Log: Cloudflare Logに書き込むだけ(Enterprise Planが必要)</li> <li>JS(JavaScript) Challenge: ボットやスパム対策。リクエスト元がブラウザかどうかを判定する</li> <li>Interactive Challenge: 人間が何らかの操作をすることで突破できる</li> <li>Managed Challenge: リクエストに応じてほかのチャレンジを自動選択する</li> </ul> <h3 id="Cloudflare-Access">Cloudflare Access</h3> <p><a href="https://developers.cloudflare.com/cloudflare-one/applications/configure-apps/">Cloudflare Access</a>は、<a href="https://developers.cloudflare.com/cloudflare-one/">Cloudflare ZeroTrust</a>を構成するサービスの一部です。ZeroTrust(ゼロトラスト)とは、従来のネットワークによる境界防御ではなく、情報資産に対するアクセスを信頼せずに必ず検証することにより防御するという考え方です。 Cloudflare Accessを利用すると、WebアプリケーションやSaaSに簡単に認証認可を付与できます。Public InternetからアクセスできないPrivate Network Applicationにも適用できます。また、Synthetic Monitoring(外形監視)やシステム間API連携などのために、保護されたアプリケーションにアクセスするためのサービストークン発行機能もあります。 キャディでは、開発用のSaaSやツール、社内向けWebアプリケーションへのアクセスのために活用しています。認証プロバイダ(IdP)にはGoogle Workspaceを使い、Google GroupsとWebアプリケーションをひも付けることで認可を制御しています。たとえば、特定グループに属する社員だけが当該アプリケーションにアクセスできるといった制御です。一時的に業務委託の方へ社内向けアプリケーションを公開したいときは、One-time PIN login(OTP)によるアクセスを許可するなど柔軟な設定ができます。OTPとは、認証ページでE-mailを入力し、送られてきたパスワードを入力して認証するログイン方法です。概要図は図4のとおりで、表2のようなアクセスポリシーで制御します。 ちなみに、Google Cloudには<a href="https://cloud.google.com/iap">Identity-AwareProxy</a>(以降、IAP)というゼロトラストのアクセスモデルを実装できるサービスがあります。 要件によってはIAPを利用することで同等の認証認可を付与できますが、汎用性・柔軟性・メンテナンス性などの観点からCloudflare Accessをメインで使っています。たとえば、グローバルIPを持たないBastion Serverなど、Google Cloud内で完結させたほうがよいものはIAPを使って制御しています。</p> <p>▼図4 Cloudflare Accessの概要図 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208183503.png" alt="" /></p> <p>▼表2 アクセスポリシー</p> <table> <thead> <tr> <th> Application </th> <th> Policy </th> </tr> </thead> <tbody> <tr> <td> Development Tool <br/> Staging Environment<br/> Sentry </td> <td> Allow Developer Group </td> </tr> <tr> <td> Internal Application </td> <td> Allow Internal App Group <br/> Allow outsourcing@example.com </td> </tr> </tbody> </table> <h3 id="Cloudflare-Gateway">Cloudflare Gateway</h3> <p>Cloudflare Gatewayは、<a href="https://www.cloudflare.com/ja-jp/learning/access-management/what-is-a-secure-web-gateway/">Secure WebGateway</a>の実装の1つであり、Cloudflare Accessと同様にCloudflare Zero Trustを構成するサービスの一部です。Secure Web Gatewayとは、安全な外部通信をするために、URLフィルタリングやマルウェア検出/ブロック、アクセス制御などを行うゲートウェイ(プロキシ)のことです。 図5がCloudflare Gatewayを使用したときの概要図です。まず、エンドユーザーの端末とCloudflare Gateway間において暗号化された専用接続を構築します。具体的には、<a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-devices/warp/">WARP</a>というクライアントをインストールします。 WARPを有効にすると、Webの通信がすべてCloudflare Gatewayにプロキシされます。 Cloudflare Gateway側では、DNS、HTTP、Networkのそれぞれのレイヤで<a href="https://developers.cloudflare.com/cloudflare-one/policies/gateway/order-of-enforcement/">フィルタリングルール(ポリシー)が適用</a>されます。企業の管理者は、ポリシーを設定しておくことで、各レイヤごとにマルウェアやフィッシングをブロックしたり、特定のグループのみに特定のアプリケーションへのアクセス権を与えたりできます。 また、アンチウイルススキャンの設定を有効にしておけば、ダウンロードしようとしているファイルにもマルウェア検知とブロックを実行できます。</p> <p>▼図5 Cloudflare Gatewayの概要図 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208193351.png" alt="" /></p> <h3 id="Cloudflare-Rules">Cloudflare Rules</h3> <p><a href="https://developers.cloudflare.com/rules/">Cloudflare Rules</a>を使うと、Edge Server側にて任意の条件でリクエストやレスポンスを変更できます。一例として、図6のようにリダイレクトルールのAPIを利用し、プロダクトごとに計画メンテナンス用の画面に切り替えができるように、共通の開発者向けアプリケーションを構築しています。 また、執筆時点でGAになっていませんが、<a href="https://blog.cloudflare.com/ja-jp/cloudflare-snippets-alpha-ja-jp/">Cloudflare Snippets</a>によりJavaScript でルールを書けるようになるため、さらに柔軟な変更が可能になりそうです。</p> <p>▼図6 計画メンテナンス管理の概要図 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208193400.png" alt="" /></p> <h3 id="実行順序">実行順序</h3> <p>これまで紹介したとおり、CloudflareではEdge Server側でさまざまな最適化ができます。 しかし、利用する機能が増えてきたときには注意が必要です。どの機能がどの順序で処理されるかを意識しておかないと、設定したときに想定外の挙動になりかねないためです。Cloudflareのダッシュボードから各機能の設定画面でTraffic Sequenceを確認できます(図7)。初めて利用する機能の設定をするときには、既存の設定との副作用や考慮漏れがないかをチェックしましょう。 執筆時点ではまだBetaの機能ではありますが、<a href="https://blog.cloudflare.com/traffic-transparency-unleashing-cloudflare-trace/">Cloudflare Trace</a>を利用すると便利です。 Cloudflareのダッシュボード上でリクエストをカスタマイズして実行し、そのリクエストにどの設定が適用されるかシミュレートできます。</p> <p>▼図7 実行順序</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208193408.png" alt="" /></p> <h3 id="Developer-Platform">Developer Platform</h3> <p>CloudflareにはWebアプリケーションを構築するためのコンポーネントが一通りそろっています。多くのサービスが無償から利用できます。<a href="https://developers.cloudflare.com/workers/">Cloudflare Workers</a>は<a href="https://blog.cloudflare.com/ja-jp/cloudflare-workers-serverless-week-ja-jp/">エッジコンピューティング</a>の実装の1つであり、FaaS/Serverlessでもあります。次のような特徴があり、使い勝手が良いため、キャディの一部プロダクトでも利用しています。</p> <ul> <li>非常に安価</li> <li>高いスケーラビリティ</li> <li>開発者体験がよい</li> <li>0ms Cold Start(0ミリ秒コールドスタート)</li> </ul> <p>少しだけしくみにも触れておきます。 Workers基盤上では、V8 EngineのIsolateが利用されており、リクエストごとに軽量かつ独立した環境でユーザーコードが実行されます。 さらに、<a href="https://blog.cloudflare.com/eliminating-cold-starts-with-cloudflare-workers/">TLSハンドシェイク中にWarmupを終わらせる</a>ことで、0ミリ秒コールドスタートを実現しています。これらの発明が従来のFaaSのコールドスタート問題を解決したおかげで、用途が格段に広がったのだと思います。 開発者体験の面では、デフォルトではWorkers専用ドメイン(workers.dev)で動くため独自ドメイン追加することなく始められますし、専用のCLIツールがシンプルで使いやすいです。CI/CDは<a href="https://github.com/cloudflare/wrangler-action">公式のGitHub Action</a>で簡単にセットアップできます。また、<a href="https://github.com/honojs/hono">Hono</a>というWebフレームワークを使うとより開発が楽になるのでお勧めです。 そのほかのサービスも一部概要を紹介します(すべてのサービスとその詳細は<a href="https://developers.cloudflare.com/products/?product-group=Developer+platform">こちら</a>を参照してください)。次のようなサービスをCloudflare Workersと組み合わせることで、さらに多様なアプリケーションが構築できるようになります。Edge Server上で処理を完結させられれば、より早くレスポンスを返すことができ、ユーザー体験の向上につながるでしょう。</p> <ul> <li>Cloudflare Pages: Gitと統合されたJAMstackプラットフォーム</li> <li>Cloudflare D1: SQLiteベースのデータベース</li> <li>Cloudflare R2: S3互換でエグレス料金無料のオブジェクトストレージ</li> <li>Cloudflare Workers KV: key-valueデータストレージ</li> <li>Cloudflare Queues: メッセージキュー</li> </ul> <p>運用上の課題としては、まだIAM(Identity and Access Management)に相当する機能がないため、最小の権限でのワークロード実行制御ができません。たとえば、「特定のCloudflare WorkersからはCloudflare R2の読み取りだけしかできない」といった制御です。一方「特定のCloudflare Workersから特定のCloudflare R2のすべての操作」はBindingによる紐づけを前提としており、制御できるようになっています。 ユーザーアカウントに関しては、RBAC機能を利用してある程度権限が管理できますが、リリース環境ごとに権限を分離するなどの制御はできません。とはいえ、IaCを前提としたGitリポジトリ側での統制により、ある程度担保できるでしょう。また、管理コストは上がりますが、本番環境とその他の環境をアカウント(テナント)やドメイン単位で分離することも有効な手段です。</p> <h3 id="Cloudflare-Registrar">Cloudflare Registrar</h3> <p><a href="https://developers.cloudflare.com/registrar/">Cloudflare Registrar</a>は、Cloudflareが運営するレジストラです。ほかのレジストラと比較して特別な機能があるわけではないですが、レジストリやICANNの請求される費用(つまり原価)のみで利用できるのが魅力です。すでにCloudflareを使っていれば、集約による管理コストの低減が期待できるでしょう。<a href="https://www.itmedia.co.jp/pcuser/articles/2306/16/news083.html">Google Domainsの売却の発表</a>に伴い、その移行先としても注目を集めています。Cloudflare DNSと同様にCloudflare CDNを使うための必須条件ではありません。</p> <h3 id="Cloudflare-Waiting-Room">Cloudflare Waiting Room</h3> <p>Cloudflare Waiting Roomは仮想待合室サービスです。想定したキャパシティを超えた急激なトラフィックの上昇からWebアプリケーションを守り、一定の可用性を維持できます。<a href="https://classmethod.jp/news/202106-cloudflare/">数百の自治体の新型コロナワクチン予約サイトで導入</a>されていました。 利用するための前提条件は、Cloudflare CDNが設定済みでCookieが有効になっていることです。あとは対象のホストやパス、閾値などを設定するだけでWaiting Roomを適用できます。トラフィックが設定された閾値を超えたときユーザーは待合室に誘導されます。待合室入ったユーザーには待合室専用のページが表示され、20秒ごとに推定待ち時間が更新されます。その後、デフォルトではFirst In First Out(FIFO)でOrigin Serverへ到達できるようになります。</p> <h3 id="IaC">IaC</h3> <p>キャディでは、Cloudflareに関してもともとIaC管理できていませんでしたが、履歴管理や変更容易性などの観点から徐々にIaC化を進めています。とくにCloudflare DNSやCloudflare Accessは、変更頻度が比較的高く、変更履歴や監査ログも重要ですので、早めにIaC化して良かったと感じています。まだIaC化はこれからという方は、<a href="https://developers.cloudflare.com/terraform/advanced-topics/best-practices/">公式のTerraformベストプラクティス</a>を参考にして進めるとよいでしょう。</p> <h3 id="Enterprise-Plan">Enterprise Plan</h3> <p>低コストでさまざまなサービスを利用できるのは、Cloudflareの大きな魅力です。しかし、組織としてさらに統制を効かせやすくしながらプロダクトの運用レベルを上げるためには、<a href="https://www.cloudflare.com/ja-jp/plans/">Enterprise Plan</a>への移行が必要になります。一例ですが次の機能はEnterprise Planでしか利用できません。</p> <ul> <li>Cloudflare DashboardのSSO対応</li> <li>ホスト単位のキャッシュパージ</li> <li>Request Log(Access Log)のExport</li> <li>Audit LogのExport</li> </ul> <p>また、Enterprise Planの顧客専用のチームがサポートチケットに対応してくれるため、回答の質やスピードが向上します。<sup id="fnref:2"><a href="#fn:2" rel="footnote">2</a></sup> ちなみに、RBAC機能は以前はEnterprise Plan限定でしたが、<a href="https://blog.cloudflare.com/rbac-for-everyone/">2023年にほかのプランにも解放</a>されました。</p> <h2 id="連載のおわりに">連載のおわりに</h2> <p>今回が本連載の最終回となります。2021年7月に<a href="https://caddi.tech/archives/4088">2名で立ち上げたPlatform Team</a>ですが、2年数ヵ月経過した現在、メンバーが大幅に増えPlatform Groupになりました。本連載では、筆者らがその間に取り組んできたことの一部を紹介してきました。 振り返ってみると、筆者らが本連載の企画を始めたとき、まだPlatform Engineeringという言葉はそれほど認知されていませんでした。そのため、議論の結果「Google Cloudで実践するSREプラクティス」というタイトルや内容に落ち着いたと記憶しています。ところが、<a href="https://www.gartner.com/en/articles/gartner-top-10-strategic-technology-trends-for-2024">Gartnerのテクノロジートレンド</a>に登場したり、<a href="https://platformengineering.connpass.com/">Platform Engineering Meetup</a>が盛り上がっていたりと、現在はだいぶ認知が進んできたようです。今後も機会があれば、筆者らの取り組みをなんらかの形で共有したいと思います。 本連載が、みなさんにとって価値のあるDevOps、SREを実現するためのヒントになっていたら幸いです。最後までお読みいただきありがとうございました。</p> <div class="footnotes"> <hr/> <ol> <li id="fn:1"> 一部内容に誤りがあったため、訂正してあります。<a href="#fnref:1" rev="footnote">&#8617;</a></li> <li id="fn:2"> キャディでは、Cloudflare Accessの不具合を報告し、修正してもらったことがあります。<a href="#fnref:2" rev="footnote">&#8617;</a></li> </ol> </div> minato128 Kubeflow Pipelines の local 実行で開発効率を上げる hatenablog://entry/6801883189081010167 2024-02-01T23:41:18+09:00 2024-03-05T10:53:40+09:00 はじめに AI Team MLOps エンジニアの西原です。2024 年 1 月にローカル環境で Kubeflow Pipelines を実行するドキュメントが公式から公開されました。今回はそのドキュメントを参考にローカル環境で Kubeflow Pipelines を実行する方法を紹介します。 はじめに Kubeflow Pipelines とは kfp を使った開発の課題 kfp を手元の開発環境で実行する ローカル環境でコンポーネント実行 アーティファクトを出力 任意のコンテナイメージを使ったコンポーネント GPU を使ったコンポーネント pipeline 実行 pipeline とは何… <h1 id="はじめに">はじめに</h1> <p>AI Team MLOps エンジニアの西原です。2024 年 1 月にローカル環境で Kubeflow Pipelines を実行するドキュメントが公式から<a class="wp-editor-md-post-content-link" href="https://www.kubeflow.org/docs/components/pipelines/v2/local-execution/">公開</a>されました。今回はそのドキュメントを参考にローカル環境で Kubeflow Pipelines を実行する方法を紹介します。</p> <ul class="table-of-contents"> <li><a href="#はじめに">はじめに</a></li> <li><a href="#Kubeflow-Pipelines-とは">Kubeflow Pipelines とは</a></li> <li><a href="#kfp-を使った開発の課題">kfp を使った開発の課題</a></li> <li><a href="#kfp-を手元の開発環境で実行する">kfp を手元の開発環境で実行する</a><ul> <li><a href="#ローカル環境でコンポーネント実行">ローカル環境でコンポーネント実行</a></li> <li><a href="#アーティファクトを出力">アーティファクトを出力</a></li> <li><a href="#任意のコンテナイメージを使ったコンポーネント">任意のコンテナイメージを使ったコンポーネント</a></li> <li><a href="#GPU-を使ったコンポーネント">GPU を使ったコンポーネント</a></li> </ul> </li> <li><a href="#pipeline-実行">pipeline 実行</a><ul> <li><a href="#pipeline-とは何か">pipeline とは何か?</a></li> <li><a href="#pipeline-実行-1">pipeline 実行</a></li> </ul> </li> <li><a href="#まとめ">まとめ</a></li> <li><a href="#参考">参考</a></li> </ul> <h1 id="Kubeflow-Pipelines-とは">Kubeflow Pipelines とは</h1> <p>今回取り扱う Kubeflow Pipelines とは何か?公式のドキュメントを引用します。</p> <blockquote> Kubeflow Pipelines(kfp)は、コンテナイメージを使ってポータブルでスケーラブルな機械学習(ML)ワークフローを構築し、デプロイするためのプラットフォームです。 </blockquote> <p>CADDi AI Team では Google Cloud のマネージドなプラットフォームである Vertex AI Pipelines を使って機械学習パイプライン開発をしています。この裏で kfp が動いており、開発時に kfp の Python SDK を使ってパイプラインを定義しています。</p> <h1 id="kfp-を使った開発の課題">kfp を使った開発の課題</h1> <p>機械学習用のコンテナイメージは比較的大きく、私たちのチームでは 1 つあたり 10~20GB になることが多いです。イメージサイズが大きくなる要因は GPU 環境でプログラムを動かすために必要なソフトウェアを setup するためです。これらの大きなコンテナイメージを push してリモートのパイプライン上で動作確認すると、Node の起動やコンテナの push と pull による待ち時間が長くなります。私たちのチームでは一番最初のコンポーネントが実行されるまでに 20 分弱かかることもありました。こういった状況では試行錯誤の回数が下がり開発効率が悪くなるため、コンテナイメージを 不必要に push せずにローカル環境で動作確認したいという話がありました。</p> <p>この課題を解決するために、kfp の Python SDK を使ってローカル環境でパイプラインを実行する方法を調査し、検証したので紹介します。</p> <h1 id="kfp-を手元の開発環境で実行する">kfp を手元の開発環境で実行する</h1> <h2 id="ローカル環境でコンポーネント実行">ローカル環境でコンポーネント実行</h2> <p>サンプルコードを使ってローカル環境でコンポーネント実行する方法を紹介します。シンプルな足し算の例が次のコードになります。<code>local.init</code>がない状態だと実行できずエラーでプログラムが終わりますが、これを記述することでローカル環境で実行できます。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">from</span> kfp <span class="synPreProc">import</span> local <span class="synPreProc">from</span> kfp <span class="synPreProc">import</span> dsl <span class="synComment"># 関数定義の後に実行しても良い</span> <span class="synComment"># 実行にはdockerが必要</span> local.init(runner=local.DockerRunner()) <span class="synPreProc">@</span><span class="synIdentifier">dsl.component</span> <span class="synStatement">def</span> <span class="synIdentifier">add</span>(a: <span class="synIdentifier">int</span>, b: <span class="synIdentifier">int</span>) -&amp;gt; <span class="synIdentifier">int</span>: <span class="synStatement">return</span> a + b task = add(a=<span class="synConstant">1</span>, b=<span class="synConstant">2</span>) <span class="synStatement">assert</span> task.output == <span class="synConstant">3</span> </pre> <p>このプログラムを実行するとログから入力と出力が確認でき、問題なく動作していることがわかります。</p> <pre class="code text" data-lang="text" data-unlink>...省略 { &#34;inputs&#34;: { &#34;parameterValues&#34;: { &#34;a&#34;: 1, &#34;b&#34;: 2 } }, &#34;outputs&#34;: { &#34;parameters&#34;: { &#34;Output&#34;: { &#34;outputFile&#34;: &#34;~/&amp;lt;PATH&amp;gt;/local_outputs/add-2024-01-15-18-45-51-383673/add/Output&#34; } }, &#34;outputFile&#34;: &#34;~/&amp;lt;PATH&amp;gt;/local_outputs/add-2024-01-15-18-45-51-383673/add/executor_output.json&#34; } } [KFP Executor 2024-01-15 18:45:55,665 INFO]: Wrote executor output file to ~/&amp;lt;PATH&amp;gt;/local_outputs/add-2024-01-15-18-45-51-383673/add/executor_output.json. 18:45:55.877 - INFO - Task &#39;add&#39; finished with status SUCCESS 18:45:55.878 - INFO - Task &#39;add&#39; outputs: Output: 3</pre> <h2 id="アーティファクトを出力">アーティファクトを出力</h2> <p>kfp にはアーティファクトというものがあります。詳しい説明はここでは省略しますが、パイプラインと紐づくもので、データセットやモデルなどがそれになります。kfp でアーティファクトの扱いはコアな部分になるため、サンプルコードで動作を確認します。足し算の結果をアーティファクトとしてファイル出力する例を次に示します。<code>with</code>句でファイルを開いて、書き込みと読み込みをするプログラムです。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">from</span> kfp <span class="synPreProc">import</span> local <span class="synPreProc">from</span> kfp <span class="synPreProc">import</span> dsl <span class="synPreProc">from</span> kfp.dsl <span class="synPreProc">import</span> Output, Artifact <span class="synPreProc">import</span> json local.init(runner=local.DockerRunner()) <span class="synPreProc">@</span><span class="synIdentifier">dsl.component</span> <span class="synStatement">def</span> <span class="synIdentifier">add</span>(a: <span class="synIdentifier">int</span>, b: <span class="synIdentifier">int</span>, out_artifact: Output[Artifact]): <span class="synPreProc">import</span> json result = json.dumps(a + b) <span class="synStatement">with</span> <span class="synIdentifier">open</span>(out_artifact.path, <span class="synConstant">'w'</span>) <span class="synStatement">as</span> f: f.write(result) out_artifact.metadata[<span class="synConstant">'operation'</span>] = <span class="synConstant">'addition'</span> task = add(a=<span class="synConstant">1</span>, b=<span class="synConstant">2</span>) <span class="synStatement">with</span> <span class="synIdentifier">open</span>(task.outputs[<span class="synConstant">'out_artifact'</span>].path) <span class="synStatement">as</span> f: contents = f.read() <span class="synStatement">assert</span> json.loads(contents) == <span class="synConstant">3</span> <span class="synStatement">assert</span> task.outputs[<span class="synConstant">'out_artifact'</span>].metadata[<span class="synConstant">'operation'</span>] == <span class="synConstant">'addition'</span> </pre> <p>実行した際のログからアーティファクトの出力先が確認できます。</p> <pre class="code text" data-lang="text" data-unlink>.. [KFP Executor 2024-01-15 20:38:32,771 INFO]: Wrote executor output file to ~/&amp;lt;PATH&amp;gt;/local_outputs/add-2024-01-15-20-38-28-731045/add/executor_output.json. __import__(pkg_name) 20:38:32.975 - INFO - Task &#39;add&#39; finished with status SUCCESS 20:38:32.975 - INFO - Task &#39;add&#39; outputs: out_artifact: Artifact( name=&#39;out_artifact&#39;, uri=&#39;~/&amp;lt;PATH&amp;gt;/local_outputs/add-2024-01-15-20-38-28-731045/add/out_artifact&#39;, metadata={&#39;operation&#39;: &#39;addition&#39;} )</pre> <p>出力先のファイルを確認すると、json 形式で 3 が書き込まれていることが確認できます。</p> <h2 id="任意のコンテナイメージを使ったコンポーネント">任意のコンテナイメージを使ったコンポーネント</h2> <p>ここまで Python の関数としてコンポーネントを実行してきましたが、<code>dsl.ContainerSpec</code>を使うと任意のコンテナイメージをコンポーネントとして実行できます。<code>Hello World</code>の文字列をファイルに書き込む例が次になります。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">from</span> kfp <span class="synPreProc">import</span> dsl, local local.init(runner=local.DockerRunner()) <span class="synPreProc">@</span><span class="synIdentifier">dsl.container_component</span> <span class="synStatement">def</span> <span class="synIdentifier">say_hello</span>(name: <span class="synIdentifier">str</span>, greeting: dsl.OutputPath(<span class="synIdentifier">str</span>)): <span class="synConstant">&quot;&quot;&quot;Log a greeting and return it as an output.&quot;&quot;&quot;</span> <span class="synStatement">return</span> dsl.ContainerSpec( image=<span class="synConstant">&quot;alpine&quot;</span>, command=[ <span class="synConstant">&quot;sh&quot;</span>, <span class="synConstant">&quot;-c&quot;</span>, <span class="synConstant">&quot;&quot;&quot;RESPONSE=&quot;Hello, $0!&quot;</span><span class="synSpecial">\</span> <span class="synConstant"> &amp;amp;&amp;amp; echo $RESPONSE</span><span class="synSpecial">\</span> <span class="synConstant"> &amp;amp;&amp;amp; mkdir -p $(dirname $1)</span><span class="synSpecial">\</span> <span class="synConstant"> &amp;amp;&amp;amp; echo $RESPONSE &amp;gt; $1</span> <span class="synConstant"> &quot;&quot;&quot;</span>, ], args=[name, greeting], ) task = say_hello(name=<span class="synConstant">&quot;World&quot;</span>) <span class="synIdentifier">print</span>(task.outputs) </pre> <p>上記のプログラムを実行すると次のようなログが出力され、Hello World という文字列が見えます。実際に出力されたファイルを確認すると、Hello World という文字列が書き込まれていることが確認できます。</p> <pre class="code text" data-lang="text" data-unlink> Found image &#39;alpine:latest&#39; Hello, World! 06:39:37.953 - INFO - Task &#39;say-hello&#39; finished with status SUCCESS 06:39:37.953 - INFO - Task &#39;say-hello&#39; outputs: greeting: &#39;Hello, World! &#39; {&#39;greeting&#39;: &#39;Hello, World!\n&#39;}</pre> <h2 id="GPU-を使ったコンポーネント">GPU を使ったコンポーネント</h2> <p>機械学習では GPU を使って学習や推論を行うことがあります。先述した通り、GPU を使ってプログラム実行するには依存するソフトウェアが増え、コンテナイメージのサイズが大きくなります。大きなコンテナイメージを使ってリモート環境で動作確認すると待機時間が長くなります。GPU を使ったコンポーネントがローカル環境で実行できると不必要にリモートのパイプライン上で動作確認することがなくなり、待機時間を減らすことができます。これにより開発効率が大きく改善できるため、今回のローカル環境の検証の核となる部分です。</p> <p>結論として、今回紹介している kfp local で GPU を使ったコンポーネントをローカル環境で実行できます。GPU を使ったサンプルのプログラムが次になります。次のプログラムは、CUDA(GPU)がない環境で実行すると失敗しますが、CUDA がある環境では成功するようになっています。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">from</span> kfp <span class="synPreProc">import</span> dsl, local local.init(runner=local.DockerRunner()) <span class="synPreProc">@</span><span class="synIdentifier">dsl.container_component</span> <span class="synStatement">def</span> <span class="synIdentifier">gpu_processing</span>(): <span class="synStatement">return</span> dsl.ContainerSpec( image=<span class="synConstant">&quot;gcr.io/google_containers/cuda-vector-add:v0.1&quot;</span>, ) task = gpu_processing() <span class="synIdentifier">print</span>(task.outputs) </pre> <p>上記のコンポーネントを実行した結果が次になります。ログから GPU を使ったコンポーネントが問題なく実行できていることが確認できます。</p> <pre class="code text" data-lang="text" data-unlink>16:21:16.599 - INFO - Executing task &#39;gpu-processing&#39; 16:21:16.600 - INFO - Streamed logs: Found image &#39;gcr.io/google_containers/cuda-vector-add:v0.1&#39; [Vector addition of 50000 elements] Copy input data from the host memory to the CUDA device CUDA kernel launch with 196 blocks of 256 threads Copy output data from the CUDA device to the host memory Test PASSED Done 16:21:18.040 - INFO - Task &#39;gpu-processing&#39; finished with status SUCCESS 16:21:18.040 - INFO - Task &#39;gpu-processing&#39; has no outputs</pre> <h1 id="pipeline-実行">pipeline 実行</h1> <p>これまでコンポーネントの実行について紹介してきましたが、パイプライン実行についても紹介します。</p> <h2 id="pipeline-とは何か">pipeline とは何か?</h2> <p>kfp におけるパイプラインとは何か?公式のドキュメントを引用します。</p> <blockquote> パイプラインとは、1 つまたは複数のコンポーネントを組み合わせて計算有向 非循環グラフ(DAG)を形成するワークフローの定義です。実行時、各コンポーネント実行は 1 つのコンテナ実行に対応し、コンテナは ML のアーティファクトを作成します。パイプラインは制御フローを含むことがあります。 </blockquote> <h2 id="pipeline-実行-1">pipeline 実行</h2> <p>ローカル環境でのパイプライン実行を実際にやってみます。対象の関数に<code>@dsl.pipeline</code>をつけることでパイプラインとして定義できます。下記はコンポーネントを組み合わせて三平方の定理を計算するパイプラインの例です。</p> <pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">from</span> kfp <span class="synPreProc">import</span> dsl, local local.init(runner=local.DockerRunner()) <span class="synPreProc">@</span><span class="synIdentifier">dsl.component</span> <span class="synStatement">def</span> <span class="synIdentifier">square</span>(x: <span class="synIdentifier">float</span>) -&amp;gt; <span class="synIdentifier">float</span>: <span class="synStatement">return</span> x ** <span class="synConstant">2</span> <span class="synPreProc">@</span><span class="synIdentifier">dsl.component</span> <span class="synStatement">def</span> <span class="synIdentifier">add</span>(x: <span class="synIdentifier">float</span>, y: <span class="synIdentifier">float</span>) -&amp;gt; <span class="synIdentifier">float</span>: <span class="synStatement">return</span> x + y <span class="synPreProc">@</span><span class="synIdentifier">dsl.component</span> <span class="synStatement">def</span> <span class="synIdentifier">square_root</span>(x: <span class="synIdentifier">float</span>) -&amp;gt; <span class="synIdentifier">float</span>: <span class="synStatement">return</span> x ** <span class="synConstant">.5</span> <span class="synPreProc">@</span><span class="synIdentifier">dsl.pipeline</span> <span class="synStatement">def</span> <span class="synIdentifier">pythagorean</span>(a: <span class="synIdentifier">float</span>, b: <span class="synIdentifier">float</span>) -&amp;gt; <span class="synIdentifier">float</span>: a_sq_task = square(x=a) b_sq_task = square(x=b) sum_task = add(x=a_sq_task.output, y=b_sq_task.output) <span class="synStatement">return</span> square_root(x=sum_task.output).output result = pythagorean(a=<span class="synConstant">3.0</span>, b=<span class="synConstant">4.0</span>) <span class="synIdentifier">print</span>(result) </pre> <p>これを実行すると次のようなログが出力され、ローカル環境だとパイプラインの実行はサポートされていないことが分かります。 (追記:v2.7.0でパイプライン実行がサポートされました。)</p> <pre class="code text" data-lang="text" data-unlink>... raise NotImplementedError( NotImplementedError: Local pipeline execution is not currently supported.</pre> <p>ローカル環境でパイプライン実行はサポートされてませんが、コンポーネントの実行はサポートされているのでコンポーネントを組み合わせてパイプラインっぽく実行することはできます。具体的にどうするのかというと、サンプルコードの<code>@dsl.pipeline</code>を消して実行するだけです。</p> <p>パイプライン関数のデコレータを消して実行した結果が次になります。3<sup>2</sup> + 4<sup>2</sup> の平方根は 5 なので正しく動いていることが確認できます。</p> <pre class="code text" data-lang="text" data-unlink>... 06:59:08.912 - INFO - Task &#39;square-root&#39; finished with status SUCCESS 06:59:08.912 - INFO - Task &#39;square-root&#39; outputs: Output: 5.0</pre> <h1 id="まとめ">まとめ</h1> <p>ここまで kfp のローカル環境での実行について紹介しました。 機械学習では GPU を使ったプログラムを実行することもありますが、その場合はコンテナイメージのサイズが大きくなります。大きなコンテナイメージを使ってリモート環境で検証すると、待機時間が長くなります。今回紹介したローカル環境での実行によって、リモート環境以外で動作確認できるようになり、不必要な待機時間を減らすことができます。今回紹介した kfp local によって開発業務の待機時間を減らせるため、うまく取り入れることで開発効率の改善が期待できます。</p> <h1 id="参考">参考</h1> <ul> <li><a class="wp-editor-md-post-content-link" href="https://www.kubeflow.org/docs/components/pipelines/v2/local-execution/">Kubeflow Pipelines のローカル実行</a></li> <li><a class="wp-editor-md-post-content-link" href="https://www.kubeflow.org/docs/components/pipelines/v2/components/">kfp components</a></li> <li><a class="wp-editor-md-post-content-link" href="https://www.kubeflow.org/docs/components/pipelines/v2/pipelines/">kfp pipeline</a></li> </ul> caddi-tech 第9回: Datadogによるクラウド横断のモニタリング基盤 hatenablog://entry/6801883189081010186 2024-01-30T08:55:31+09:00 2024-02-26T18:06:54+09:00 ※本記事は、技術評論社「Software Design」(2023年12月号)に寄稿した連載記事「Google Cloudで実践するSREプラクティス」からの転載です。発行元からの許可を得て掲載しております。 はじめに 前回は、Google Cloudが提供するAnthos Service Meshを導入して、GKEで動くアプリケーションに可観測性やセキュリティなどの機能を追加する方法について紹介しました。今回はDatadog1を利用したモニタリング基盤について、Datadogの採用理由や基本機能、キャディでの活用事例を紹介します(図1)。 ▼図1 CADDiスタックにおける今回の位置付け D… <p>※本記事は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%BB%BD%D1%C9%BE%CF%C0%BC%D2">技術評論社</a><a class="wp-editor-md-post-content-link" href="https://gihyo.jp/magazine/SD/archive/2023/202312">「Software Design」(2023年12月号)</a>に寄稿した連載記事「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudで実践するSREプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティス」からの転載です。発行元からの許可を得て掲載しております。</p> <h2 id="はじめに">はじめに</h2> <p><a class="wp-editor-md-post-content-link" href="https://caddi.tech/archives/4653?preview_id=4653&amp;preview_nonce=a222984494&amp;post_format=standard&amp;_thumbnail_id=-1&amp;preview=true">前回</a>は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudが提供するAnthos Service Meshを導入して、GKEで動くアプリケーションに可観測性やセキュリティなどの機能を追加する方法について紹介しました。今回はDatadog<sup id="fnref-4756-1"><a href="#fn-4756-1" class="footnote-ref" role="doc-noteref">1</a></sup>を利用したモニタリング基盤について、Datadogの採用理由や基本機能、キャディでの活用事例を紹介します(図1)。</p> <p>▼図1 CADDiスタックにおける今回の位置付け <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208183512.png" alt="" /></p> <h2 id="Datadogとは">Datadogとは</h2> <p>Datadogは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>ベースの運用監視<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>です。おもに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>プロバイダ(<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a>、Azure、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudなど)やオンプレミス環境でのアプリケーションとインフラスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>チャの監視をサポートし、システムの状況をリアルタイムで追跡・可視化する機能を提供しています。 また、インフラスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>チャモニタリング、ログ管理などの用途でも利用でき、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>、NGINX、<a class="keyword" href="https://d.hatena.ne.jp/keyword/MySQL">MySQL</a>などさまざまなサービスやアプケーションをサポートしているのもDatadogの特徴です。</p> <h2 id="なぜDatadogなのか">なぜDatadogなのか</h2> <p>筆者らがDatadogを導入した2020年頃以前、キャディでは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudが提供するCloudMonitoringを利用していました。キャディでは複数の<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudのプロジェクトでさまざまなプロダクトを運用しています。それぞれの状態を確認するためには、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudの管理コンソール上で対象プロジェクトに移動する必要があり、操作が繁雑でした。このことから、モニタリングを一元化したいという要望が出てきました。 また、キャディはスタートアップ企業であるためプロダクト開発に注力する必要があり、独自の監視基盤を構築・運用する人的リソースを割くことができません。そのため、フルマネージドな監視基盤であるDatadogを採用することにしました。 Datadogはさまざまな特徴を備えますが、とくに筆者らの要件に合致したのは次のような点でした。</p> <ul> <li>さまざまな対象からログやメトリクスを収集して一元管理できる</li> <li>WebUI上で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボードが簡単に作成できる</li> <li>クエリ定義による柔軟なアラート設定ができる</li> <li>Terraformが対応しており、設定をIaC化できる</li> </ul> <p>これらの特性から、自社で運用する多くのプロダクトの状態をプロダクト軸・時間軸の両方で分析・監視でき、状況把握や障害対応の効率が大きく向上しました。</p> <h2 id="Google-Cloud連携のしくみ"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud連携のしくみ</h2> <p>Datadogが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスからログやメトリクスなどを収集するしくみを、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudとの連携を例に紹介します(図2)。</p> <p>▼図2 Datadogと<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudの連携方法 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208193317.png" alt="" /></p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudとの連携では、Datadog向けに用意したサービスアカウントを通じて認証します。Datadogはこのサービスアカウントを利用して<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudの<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>をコールすることで、多くの情報を収集します。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を通じて取得する主要な情報は各種メトリクスです。メトリクスにはCPU使用率、メモリ使用量、ネットワーク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D5%A5%A3%A5%C3%A5%AF">トラフィック</a>などをはじめとするシステムやアプリケーションの状態、パフォーマンス、使用状況などに関する数値データがあります。これらのメトリクスは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> CloudのCloud Monitoringと呼ばれるモニタリングサービスの<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>から取得します。 Pod などの GKE 上のリソースは、CloudMonitoringでメトリクス収集できないため、GKEにDatadog Agentをインストールし、DataDog AgentがPodの状態を収集し、Datadogへメトリクスを送信します。 CloudRunやGKE上のコンテナなどをはじめとする各種ログは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudの標準サービスであるCloud Loggingに集められます。CloudLoggingにはログ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EB%A1%BC%A5%BF%A1%BC">ルーター</a>という機能があり、ここでログのフィルタリングと転送ができます。 ログ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EB%A1%BC%A5%BF%A1%BC">ルーター</a>の転送先としてCloud Pub/Sub(<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudのキューイングサービス)を指定し、さらにPub/SubにDatadogのログ転送<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を設定することでDatadogにログが転送されます。 メトリクスやログ収集の詳しい設定方法はDatadogのドキュメント<sup id="fnref-4756-2"><a href="#fn-4756-2" class="footnote-ref" role="doc-noteref">2</a></sup>にわかりやすく説明されています。これに従えば、それほど難しくはないでしょう。</p> <h3 id="Resource-Managerを活用した一括設定">Resource Managerを活用した一括設定</h3> <p>前述のように、Datadogと<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudの連携作業はそれほど難しくありません。しかし、冒頭でも紹介したように、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud上に多くのプロジェクトを抱える組織では、これらの設定がトイルになってしまいます。また、設定漏れや誤りといった作業ミスも発生します。 キャディではこれらの課題を解決するために、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> CloudのResource Managerを活用しています。Resource Manager は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudアカウント内のリソースの整理、階層化する機能で、筆者らはアクセスコン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>やコスト管理の向上に役立てています。 Resource Managerでは「組織」と「フォルダ」という単位でプロジェクトを管理できます。「組織」は<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudアカウント全体を管理するトッ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%EC%A5%D9">プレベ</a>ルのエンティティです。組織には複数のプロジェクトやフォルダを含められます。 「フォルダ」は組織内でプロジェクトを管理するための階層的なエンティティです。フォルダ配下のプロジェクトに関して、アクセスコン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>ポリシーや管理ポリシーを一元的に設定できます。 キャディでは、特定フォルダ配下のプロジェクトを自動検出するしくみを取り入れてDatadog導入の運用負荷を下げています。図3のようなフォルダ構成では、Enabling DD Integrationフォルダで連携設定するようにしており、このフォルダ配下に作成したプロジェクトではメトリクスが収集されるようにしています。一方、Disabling DD Integrationフォルダ配下のプロジェクトは対象外になります。</p> <p>▼図3 プロジェクトの自動検知を考慮したResource Managerの構成 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208193323.png" alt="" /></p> <h2 id="ログをDatadogで管理する">ログをDatadogで管理する</h2> <p>ログをDatadogに集約することで、分析、視覚化、アラートなどの機能が提供されます。これによって、システムやアプリケーションの監視が可能になり、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D6%A5%EB%A5%B7%A5%E5%A1%BC%A5%C6%A5%A3%A5%F3%A5%B0">トラブルシューティング</a>もしやすくなります。キャディではアプリケーションログ、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%BB%A5%B9%A5%ED%A5%B0">アクセスログ</a>、監査ログなどさまざまなログDatadogに集めており、分析や監視に役立てています。</p> <h3 id="Datadog-logsをメインで利用する理由">Datadog logsをメインで利用する理由</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> CloudにはCloud Loggingという機能があり、こちらでもログ管理ができます。しかし、キャディでは次の理由でおもにDatadog logsを利用しています。</p> <ul> <li>ログ、メトリクス合わせて普段運用で見るべき場所をDatadogだけに統一できる</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud上のプロジェクトが増えても横断的にログを調べられる</li> <li>使いやすい<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%AF%A5%B9%A5%D7%A5%ED%A1%BC%A5%E9">エクスプローラ</a>によって、高度なフィルタや加工ができる</li> <li>ログの属性をインデックスする(ファセット化<sup id="fnref-4756-3"><a href="#fn-4756-3" class="footnote-ref" role="doc-noteref">3</a></sup>)ことにより、条件によってはCloudLoggingより検索が速い</li> <li>ログの内容から、HTTPステータスの統計をメトリクスで可視化したり、処理時間に対してアラートの設定ができる</li> </ul> <p>ただ、すべてのログをDatadogで管理しているわけではありません。Datadogでは、ログの保持および、取り込み時・復元時に料金がかかります。このため、利用頻度が高いアプリケーションや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%BB%A5%B9%A5%ED%A5%B0">アクセスログ</a>などをDatadogで利用し、そのほかのログにはCloud Loggingを利用するといった使い分けをしています。</p> <h3 id="運用方針">運用方針</h3> <p>キャディではいくつかの方針を定めてDatadogでログを運用しています。利用するアプリケーションによってログのフォーマットがさまざまであり、そのままDatadogに送るだけでは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D6%A5%EB%A5%B7%A5%E5%A1%BC%A5%C6%A5%A3%A5%F3%A5%B0">トラブルシューティング</a>や監視の有効活用にはなりません。また、コストにも注意をはらう必要があります。</p> <h4 id="ログの標準属性を決める">ログの標準属性を決める</h4> <p>キャディでは、Datadog側でログを解析してもらうために、<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>形式で出力することを推奨しています。解析されたログは、各属性に割り振られ、ログのフィルタリングに利用できます。よく利用する属性(ユーザーIDやリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トIDなど)を統一させて標準化することで、直感的に検索が行えるようになります。また、ログの一覧画面(図4)で項目の追加や削除ができたり、各項目でのソートができたりして、分析や調査時にとても役立ちます。</p> <p>▼図4 ログ一覧のイメージ <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208193329.png" alt="" /></p> <p>ただ、アプリケーションの仕様でフォーマット化しづらいケースなどもあります。その場合は、Datadogのパース機能<sup id="fnref-4756-4"><a href="#fn-4756-4" class="footnote-ref" role="doc-noteref">4</a></sup>を使ってDatadog内で構造化できます。</p> <p>たとえば、リスト2のような非構造化ログがDatadogに送られてくるとします。</p> <p>▼リスト2 非構造化ログの例</p> <pre class="code" data-lang="" data-unlink>[2023-10-10 02:20:48][PID:158][INFO] method=GET path=/ping status=200 content_type=text/html;</pre> <p>これに対してリスト3のようなパース規則を設定します。</p> <p>▼リスト3 パース規則の設定</p> <pre class="code" data-lang="" data-unlink>SampleRule \[%{date(&#34;yyyy-MM-dd HH:mm:ss&#34;):date}\]\[PID\:%{integer:pid}\]\[%{word:level}\]\s+%{data::keyvalue(&#34;=&#34;,&#34;;\\[\\]/&#34;)}</pre> <p>結果、リスト4のような<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>形式に解釈され、ログインデックスに保存されます。</p> <p>▼リスト4 ログインデックスに保存される<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a></p> <pre class="code lang-json" data-lang="json" data-unlink><span class="synSpecial">{</span> &quot;<span class="synStatement">date</span>&quot;: <span class="synConstant">1696904448000</span>, &quot;<span class="synStatement">path</span>&quot;: &quot;<span class="synConstant">/ping</span>&quot;, &quot;<span class="synStatement">method</span>&quot;: &quot;<span class="synConstant">GET</span>&quot;, &quot;<span class="synStatement">content_type</span>&quot;: &quot;<span class="synConstant">text/html;</span>&quot;, &quot;<span class="synStatement">level</span>&quot;: &quot;<span class="synConstant">INFO</span>&quot;, &quot;<span class="synStatement">pid</span>&quot;: <span class="synConstant">158</span>, &quot;<span class="synStatement">status</span>&quot;: <span class="synConstant">200</span> <span class="synSpecial">}</span> </pre> <p>パース規則の定義は、Grokと呼ばれるパターンマッチ構文を使って、ログ解析ルールを作る作業です。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%B5%B5%AC%C9%BD%B8%BD">正規表現</a>に似た部分もあるので、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%B5%B5%AC%C9%BD%B8%BD">正規表現</a>を知っていればドキュメントを見ながら規則を書けると思います。 また、DatadogのUI上で実際のログをサンプルとしてパース規則を作成でき、作成したパース規則の動作確認もしやすくなっています。 NGINXや<a class="keyword" href="https://d.hatena.ne.jp/keyword/PostgreSQL">PostgreSQL</a>など、代表的なアプリケーションのログのパース規則もプリセットで用意されているので、これらを参考にするのも良いでしょう。 なお、推奨レベルではありますが、日時のフォーマットや必須項目(サービス名、リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トIDなど)を定義しており、ログフォーマットの標準化に努めています。</p> <h4 id="必要なものだけインデックスする">必要なものだけインデックスする</h4> <p>Datadog logsでは、インデックス<sup id="fnref-4756-5"><a href="#fn-4756-5" class="footnote-ref" role="doc-noteref">5</a></sup>という箱にログを格納することでLog <a class="keyword" href="https://d.hatena.ne.jp/keyword/Explorer">Explorer</a>からの検索が可能になります。どのインデックスにどんなログを入れるかフィルタを書くことができるので、アプリケーション側で選別して送信する必要がありません。Datadog側でフィルタすることで、より柔軟なログの運用ができます。一方で、インデックスするログの量が増えるほどコストがかかるので、なるべく不要なログは除外しておくようにしています。</p> <h4 id="ログをアーカイブする">ログを<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AB%A5%A4%A5%D6">アーカイブ</a>する</h4> <p>Datadogの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AB%A5%A4%A5%D6">アーカイブ</a><sup id="fnref-4756-6"><a href="#fn-4756-6" class="footnote-ref" role="doc-noteref">6</a></sup>は、収集したログを長期保存するためにログを<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>ストレージ(<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Strageなど)へ転送する機能です。インデックスの保存期間を過ぎてしまったログを再度確認したくなった際に、リハイドレート<sup id="fnref-4756-7"><a href="#fn-4756-7" class="footnote-ref" role="doc-noteref">7</a></sup>使って<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>ストレージに保存されている<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AB%A5%A4%A5%D6">アーカイブ</a>から復元できます。</p> <h2 id="ダッシュボードを作成する"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボードを作成する</h2> <p>Datadogの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボードは、複数のメトリクスやログから得られる情報を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A6%A5%A3%A5%B8%A5%A7%A5%C3%A5%C8">ウィジェット</a><sup id="fnref-4756-8"><a href="#fn-4756-8" class="footnote-ref" role="doc-noteref">8</a></sup>と呼ばれるブロックで配置します。グラフ、テーブル、ヒートマップなどさまざまな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A6%A5%A3%A5%B8%A5%A7%A5%C3%A5%C8">ウィジェット</a>が提供されており、自身のニーズに合わせてカスタマイズできます。 筆者らは次の3つを念頭に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボードを作成しています。</p> <ul> <li>プロダクトの現在の状態を一目で把握できる</li> <li>異常検知後の原因分析が速やかにできる</li> <li>将来のための傾向分析ができる</li> </ul> <p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボードを切り替えながら監視や調査をするのは困難なため、1つのプロダクトに1つの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボードを作成することを推奨しています。キャディでは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Grani">Grani</a>社の事例<sup id="fnref-4756-9"><a href="#fn-4756-9" class="footnote-ref" role="doc-noteref">9</a></sup>を参考にして表1の3つのレイヤを定義しています。レイヤごとにその役割を解説していきます。</p> <p>▼表1 キャディで定義している<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボードの3つのレイヤ</p> <table> <thead> <tr> <th>レイヤー</th> <th>概要</th> <th>閲覧頻度</th> <th>詳細度</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>Overview</td> <td>常時</td> <td>低</td> </tr> <tr> <td>2</td> <td>重要指標の詳細</td> <td>障害時、最適化時</td> <td>中-高</td> </tr> <tr> <td>3</td> <td>リソースやアプリケーションの詳細</td> <td>障害時、最適化時</td> <td>中-高</td> </tr> </tbody> </table> <h3 id="Overview">Overview</h3> <p>プロダクトが正常に稼働できているかを一目で把握するためのレイヤです。ファーストビューに配置し、Query <a class="keyword" href="https://d.hatena.ne.jp/keyword/Value">Value</a>と呼ばれる<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A6%A5%A3%A5%B8%A5%A7%A5%C3%A5%C8">ウィジェット</a>を使用して現在の値を表示します。また、値に応じて背景色が変わるようになっており、正常時は緑、警告時は黄色、異常時は赤に切り替わります(図5)。</p> <p>▼図5 Overview正常時のイメージ <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208193311.png" alt="" /></p> <p>また、プロダクト固有のメトリクスも含めると、開発者以外のメンバーも状況が把握しやすくなります。ショッピングサイトを例に固有のメトリクスの詳細を挙げてみます。</p> <ul> <li>商品検索の成功率</li> <li>ログイン成功率</li> <li>決済の成功率</li> <li>商品レビューの投稿成功率</li> </ul> <p>このような指標は、障害が発生したときに、プロダクトにどのような影響が及んでいるか早期発見ができ、プロダクトマネージャーなどのシステムの詳細を把握していないメンバーとの連携もしやすくなります。このような情報は、基本メトリクスとして用意されていないので、カスタムメトリクス<sup id="fnref-4756-10"><a href="#fn-4756-10" class="footnote-ref" role="doc-noteref">10</a></sup>としてアプリケーション側から送ります。また、ログからメトリクスへの変換もできます。 SLOの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A6%A5%A3%A5%B8%A5%A7%A5%C3%A5%C8">ウィジェット</a>を利用して、パフォーマンスや信頼性を可視化するのもよいでしょう。</p> <h3 id="重要指標の詳細">重要指標の詳細</h3> <p>このレイヤには、アプリケーションやビジネスとして重要なメトリクスや、可用性や性能面で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DC%A5%C8%A5%EB%A5%CD%A5%C3%A5%AF">ボトルネック</a>になりやすいメトリクスをグラフでまとめておきます。重要指標の関連グラフを横断的に参照できるようにしておくことで、特定時刻に何が起こったかを分析しやすくなります。障害が起きたとき、問題の切り分けが迅速にでき、より早く復旧できるようになります。 たとえば、次のようなメトリクスが考えられます(図6は「ユーザーの同時接続数」「アプリケーションエラー数」を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボードにグラフ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A6%A5%A3%A5%B8%A5%A7%A5%C3%A5%C8">ウィジェット</a>で可視化したときのイメージです。</p> <p>▼図6 アプリケーションのメトリクスイメージ <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208193303.png" alt="" /></p> <ul> <li>ユーザーの同時接続数</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>数</li> <li>アプリケーションエラー数</li> <li>メッセージキューの状態</li> <li>データベース(DB)やWorkloadの負荷</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B2%BE%C1%DB%A5%DE%A5%B7%A5%F3">仮想マシン</a>(<a class="keyword" href="https://d.hatena.ne.jp/keyword/VM">VM</a>)/コンテナの再起動イベント</li> <li>DBのコネクション数</li> <li>平均レスポンスタイム</li> <li>定期実行ジョブの成功・失敗</li> </ul> <h3 id="リソースやアプリケーションの詳細">リソースやアプリケーションの詳細</h3> <p>このレイヤには、重要指標ではないがプロダクトに関連するすべてのメトリクス(ApplicationやDBやWorkloadなど)をグラフでまとめておきます。「重要指標の詳細」レイヤで障害の特定ができないときや将来のための傾向・キャパシティ分析のために利用します。</p> <h2 id="アラート運用">アラート運用</h2> <p>ソフトウェアは複雑で、運用中にはさまざまな問題が発生します。とくに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>ネイティブな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>では、さまざまな要因で障害が発生します。ログやメトリクスを監視して問題が発生したとき、即座に通知するようアラート設定することで、迅速に対処し、システムのダウンタイムや障害の影響を最小限に抑えられます。 アラートというと、おもにリソース枯渇の検知というイメージが強いかもしれません。しかし、そのほかにもセキュリティやパフォーマンスの観点でアラートを設定すると、プロダクトの信頼性向上につなげられます。Datadogのアラート機能(Monitors)を運用するにあたり、筆者らが注意しているポイントを紹介します。</p> <h3 id="アラート設定基準">アラート設定基準</h3> <p>キャディではdevelopment、staging、productionの全環境でアラートを設定しています。本番環境以外でも作成しておくと、アラート自体の動作検証にもなります。通知先はSlackにしており、環境に応じてチャンネルを分けています。 また、4つのレベルでSeverity(重大度)を定義(表2)し、アラートの緊急度が一目でわかるようにしています。</p> <p>▼表2 キャディが定義する重大度の4つのレベル</p> <table> <thead> <tr> <th>レベル</th> <th>重大度</th> </tr> </thead> <tbody> <tr> <td>A</td> <td>なるべくはやく対応する</td> </tr> <tr> <td>B</td> <td>4時間以内に対応する</td> </tr> <tr> <td>C</td> <td>24時間以内に対応する</td> </tr> <tr> <td>D</td> <td>一週間以内に対応する</td> </tr> </tbody> </table> <h3 id="Runbookの整備">Runbookの整備</h3> <p>Runbookとは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D6%A5%EB%A5%B7%A5%E5%A1%BC%A5%C6%A5%A3%A5%F3%A5%B0">トラブルシューティング</a>の手順や関連情報などをまとめた文書のことで、筆者らはそのURLをアラート本文に記載しています(図7)。Runbookはシステムの正常運用を維持するのに必要な情報を提供し、運用担当者が問題を迅速に特定し、解決するのに役立ちます。</p> <p>▼図7 Slackに通知されるアラートのイメージ <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208193256.png" alt="" /></p> <p>一般的な記載内容は次のとおりです。</p> <ul> <li>システム概要:システムの構成、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>、技術スタックなどの概要情報</li> <li>運用手順:システムの起動、停止、バックアップ、復元などの基本的な運用手順を詳細に示す</li> <li>対応方法:システムの問題を特定し、解決するための具体的な手法を提供する。エラーコードに対する対処法や調査に使えるコマンドの実行方法が記載されている</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>カレーション手順:複雑な問題や深刻な障害が発生した場合、適切なサポートまたは管理チームへの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>カレーション手順を示す</li> <li>緊急対応手順:システムに深刻な障害が発生した場合の緊急対応手順を示し、迅速な復旧を目指す</li> </ul> <p>キャディではDevOpsを実践しています。運用専門のチームはおらず、各開発チームがアラート対応をしています。アラート対応は属人的になりがちで、それゆえに一部のメンバーに負荷が偏りがちです。筆者らは日替わりでアラート対応の当番を決め、アラート発生時はRunbookを参照することで対応しやすくなるよう、運用の改善に取り組んでいます。</p> <h3 id="セキュリティへのアラート活用">セキュリティへのアラート活用</h3> <p>キャディでは、一歩進めてセキュリティ観点でもアラートを活用しています。本連載の第2~3回(本誌2023年5~6月号)でも紹介したように、キャディでは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> CloudのリソースをTerraformで管理しており、IAM Policyもその対象の1つです。IaC化したにもかかわらず、誰かがIAM Policyを手動で変更してしまったり、意図しない変更があった場合、とくに本番環境では<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BB%A5%AD%A5%E5%A5%EA%A5%C6%A5%A3%A5%A4%A5%F3%A5%B7%A5%C7%A5%F3%A5%C8">セキュリティインシデント</a>につながることもあります。これをただちに検知できるようにアラートを設定しています。 IAMの変更は<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudの監査ログで確認できます。監査ログをDatadogに流したうえで、リスト5のようなqueryを設定することでアラートを通知できます。 Terraformによる正規の手順でIAM Policyが変更された場合もアラート発報しますが、めったに変更するものではないため、その都度それが意図した変更なのかどうかをアラート担当者が確認する運用としています。</p> <p>▼リスト5 alert-queryのサンプル</p> <pre class="code" data-lang="" data-unlink>logs(&#34;@evt.name:SetIamPolicy project_id:*-production -@usr.id:(*iam.gserviceaccount.com* OR service-agent-manager@system.gserviceaccount.com)&#34;).index(&#34;*&#34;).rollup(&#34;count&#34;).by(&#34;@usr.id&#34;).last(&#34;5m&#34;) &gt; 0</pre> <p>Datadogを導入していない場合、このような検知は<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudのプロジェクト単位で実現しなければならないでしょう。Datadogを導入するとログが集約されるので、1つのアラート設定で複数のプロジェクトを監視できます。</p> <h2 id="まとめ">まとめ</h2> <p>今回はDatadogを利用したモニタリング基盤の構築や運用について紹介しました。要点は次のとおりです。</p> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>を使って運用負荷を下げ、リッチなモニタリング環境を利用する</li> <li>メトリクスやログを一元化することで、複数の<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Projectに対応した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボードやアラートが作成できる</li> <li>Runbookを作成し、誰でも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%D6%A5%EB%A5%B7%A5%E5%A1%BC%A5%C6%A5%A3%A5%F3%A5%B0">トラブルシューティング</a>できるような運用体制を目指す</li> </ul> <p>本稿がみなさんのシステム運用のヒントになれば幸いです。次回はCloudflareを用いた<a class="keyword" href="https://d.hatena.ne.jp/keyword/CDN">CDN</a>やゼロトラストセキュリティについて紹介します。</p> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn-4756-1" role="doc-endnote"> <a href="https://www.datadoghq.com/ ">https://www.datadoghq.com/ </a><a href="#fnref-4756-1" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4756-2" role="doc-endnote"> <a href="https://docs.datadoghq.com/ja/integrations/google_cloud_platform/ ">https://docs.datadoghq.com/ja/integrations/google_cloud_platform/ </a><a href="#fnref-4756-2" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4756-3" role="doc-endnote"> <a href="https://docs.datadoghq.com/logs/explorer/facets/ ">https://docs.datadoghq.com/logs/explorer/facets/ </a><a href="#fnref-4756-3" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4756-4" role="doc-endnote"> <a href="https://docs.datadoghq.com/ja/logs/log_configuration/parsing/ ">https://docs.datadoghq.com/ja/logs/log_configuration/parsing/ </a><a href="#fnref-4756-4" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4756-5" role="doc-endnote"> <a href="https://docs.datadoghq.com/ja/logs/log_configuration/indexes/ ">https://docs.datadoghq.com/ja/logs/log_configuration/indexes/ </a><a href="#fnref-4756-5" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4756-6" role="doc-endnote"> <a href="https://docs.datadoghq.com/ja/logs/log_configuration/archives/ ">https://docs.datadoghq.com/ja/logs/log_configuration/archives/ </a><a href="#fnref-4756-6" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4756-7" role="doc-endnote"> <a href="https://docs.datadoghq.com/ja/logs/log_configuration/rehydrating/ ">https://docs.datadoghq.com/ja/logs/log_configuration/rehydrating/ </a><a href="#fnref-4756-7" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4756-8" role="doc-endnote"> <a href="https://docs.datadoghq.com/ja/dashboards/widgets/ ">https://docs.datadoghq.com/ja/dashboards/widgets/ </a><a href="#fnref-4756-8" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4756-9" role="doc-endnote"> <a href="https://engineering.grani.jp/entry/2017/05/29/173141 ">https://engineering.grani.jp/entry/2017/05/29/173141 </a><a href="#fnref-4756-9" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4756-10" role="doc-endnote"> <a href="https://docs.datadoghq.com/ja/metrics/custom_metrics/ ">https://docs.datadoghq.com/ja/metrics/custom_metrics/ </a><a href="#fnref-4756-10" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> </ol> </div> caddi-tech 第8回: Anthos Service Mesh 入門 hatenablog://entry/6801883189081010210 2023-12-21T09:00:54+09:00 2024-02-08T18:34:11+09:00 ※本記事は、技術評論社「Software Design」(2023年11月号)に寄稿した連載記事「Google Cloudで実践するSREプラクティス」からの転載です。発行元からの許可を得て掲載しております。 はじめに 前回はArgo CDによるKubernetesへの継続的デリバリについて紹介しました。今回は、Google Cloudが提供するAnthos Service Meshを導入して、GKEで動くアプリケーションに可観測性やセキュリティなどの機能を追加する方法を紹介します(図1)。また、本記事に関するサンプルコードについてはGitHub1を参照してください。 ▼図1 CADDiスタッ… <p>※本記事は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%BB%BD%D1%C9%BE%CF%C0%BC%D2">技術評論社</a><a class="wp-editor-md-post-content-link" href="https://gihyo.jp/magazine/SD/archive/2023/202311">「Software Design」(2023年11月号)</a>に寄稿した連載記事「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudで実践するSREプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティス」からの転載です。発行元からの許可を得て掲載しております。</p> <h2 id="はじめに">はじめに</h2> <p><a class="wp-editor-md-post-content-link" href="https://caddi.tech/?p=4626&amp;preview=true">前回</a>はArgo CDによる<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>への継続的デリバリについて紹介しました。今回は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudが提供するAnthos Service Meshを導入して、GKEで動くアプリケーションに可観測性やセキュリティなどの機能を追加する方法を紹介します(図1)。また、本記事に関するサンプルコードについては<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a><sup id="fnref-4653-1"><a href="#fn-4653-1" class="footnote-ref" role="doc-noteref">1</a></sup>を参照してください。</p> <p>▼図1 CADDiスタックにおける今回の位置付け <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208183350.png" alt="" /></p> <h2 id="Anthos-Service-Meshとは">Anthos Service Meshとは</h2> <p>Anthos Service Mesh<sup id="fnref-4653-2"><a href="#fn-4653-2" class="footnote-ref" role="doc-noteref">2</a></sup>(以降、ASM)とは、サービスメッシュの<a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a>製品であるIstio<sup id="fnref-4653-3"><a href="#fn-4653-3" class="footnote-ref" role="doc-noteref">3</a></sup>をベースに機能を追加したフルマネージドのサービスメッシュのサービスです。GKEにアドオンとしてインストールし、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudコンソールと連携されるほか、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudの技術サポートを受けることもできます。 ASMはAnthos<sup id="fnref-4653-4"><a href="#fn-4653-4" class="footnote-ref" role="doc-noteref">4</a></sup>というサービスの一部でもあります。Anthosはマルチ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>やオンプレミス環境にGKEを中核とした<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudのサービスを構築し一元管理するためのサービスです。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> CloudのGKEでのみサービスメッシュを使用したいのであれば、Anthos全体ではなくASMを単体で使用したほうが低コストで済みます<sup id="fnref-4653-5"><a href="#fn-4653-5" class="footnote-ref" role="doc-noteref">5</a></sup>。キャディでもASMのみを使用しています。ASMを使用する際に誤ってAnthosの課金を有効にしないように注意してください。</p> <h2 id="サービスメッシュとIstio">サービスメッシュとIstio</h2> <p>サービスメッシュは分散システムで動く複数のサービス間の通信を制御するためのインフラです。 信頼性の高いサービス間通信には、適切なリトライや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%A4%A5%E0%A5%A2%A5%A6%A5%C8">タイムアウト</a>の設定、ログやメトリクス、分散トレーシングなどの可観測性の向上、<a class="keyword" href="https://d.hatena.ne.jp/keyword/TLS">TLS</a>通信などのセキュリティ向上など、さまざまな処理が必要です。これらの要素を各サービスへ個別実装するのは開発<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>が増えるほか、設定変更のたびにサービスを再デプロイするといった運用負荷も増えます。サービスメッシュはこれらをインフラとして提供することで、サービスに対して高信頼で設定変更が容易な通信機能を透過的に提供します。 ASMのベースになっているIstioは代表的なサービスメッシュの製品であり、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>」と「コン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>プレーン」という2つの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を使用します。これらの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>配置は図2を見てください。</p> <p>▼図2 サービスメッシュの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a> <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208183357.png" alt="" /></p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>は各サービスのPodに通信プロキシとして挿入されます。その後、Podへの通信とPodから出ていく通信の両方が<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>経由となります。すべての通信が<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>経由となるので、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%BF%AE%C0%A9">通信制</a>御やログ、メトリクスの出力が<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>に集約できます。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>への通信設定を行うのがコントールプレーンです。Istio用の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>ファイルを使うことでその設定内容をカスタマイズできます。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>はenvoy<sup id="fnref-4653-6"><a href="#fn-4653-6" class="footnote-ref" role="doc-noteref">6</a></sup>という通信プロキシを使用します。envoyは自身の設定を<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>経由で更新する機能を有しているため、Podを再起動することなく設定を変更できます。 Istioは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>の挿入を透過的にできることが特徴です。つまり、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>の挿入はPodの起動時に自動で行われます。また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>導入のためにアプリケーションコードの変更や再起動は不要で、既存環境に対して低コストでサービスメッシュを導入できます。</p> <h3 id="サービスメッシュを導入する理由">サービスメッシュを導入する理由</h3> <p>Istioの導入は難しくありません。一方で、導入後にIstioを最新に保っていくためには、コン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>プレーンとすべてのPodの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>の更新が必要なため、簡単なことではありません。Istioは多機能であり、明確な目的を持たずに導入すると運用コストがあとから負債となるでしょう。 キャディでの導入目的は「可観測性の向上」です。キャディでは、多数のサービスが単一のGKE<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>上で稼働しています。サービスの運用監視を効率的に行うには、各サービスが同じフォーマットでログやメトリクスを出力することが望ましいです。多数のサービスにこれらの処理を手作業で実装することは困難ですが、サービスメッシュの導入によって容易に実現できます。</p> <p>図3はASMの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボードのキャプチャです。ASMを導入することで、GKE内のPodの通信グラフやリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト統計が可視化されるほか、HTTPメトリクスも取得できるため、リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トエラーに基づく監視アラートも設定できます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%BB%A5%B9%A5%ED%A5%B0">アクセスログ</a>の出力については後述します。</p> <p>また、キャディではサービスメッシュとOpenTelemetry<sup id="fnref-4653-7"><a href="#fn-4653-7" class="footnote-ref" role="doc-noteref">7</a></sup>を組み合わせ、Cloud Traceによる分散トレーシングの収集も行っています(図4)。これら可観測性の向上も、最小限のアプリケーションコードの変更で実現しています。また、筆者の過去の経験では、非常に高いセキュリティを求めるシステムで、全サービス間通信の暗号化を要求されたことがあります。サービスメッシュを使えば<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>でmTLS通信を強制できるため、セキュリティ向上の手段としてもサービスメッシュは有効です。 サービスメッシュが何をできるかを知るには、Istioのドキュメント<sup id="fnref-4653-8"><a href="#fn-4653-8" class="footnote-ref" role="doc-noteref">8</a></sup>を見たり、実際に動かして試してみたりするのが良いでしょう。</p> <p>▼図3 ASM<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボード <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208183343.png" alt="" /></p> <p>▼図4 分散トレーシング <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208183404.png" alt="" /></p> <h3 id="コラム-Ambient-Mesh">コラム: Ambient Mesh</h3> <p>まだ安定版にはなっていませんが、IstioではAmbient Meshという<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>を用いないサービスメッシュが開発中です<sup id="fnref-4653-9"><a href="#fn-4653-9" class="footnote-ref" role="doc-noteref">9</a></sup>。Ambient Meshでは、各ノードに配置するセキュアな通信用のエージェント(ztunnel)と、L7レイヤの通信処理を集中して行うenvoy Podを使用して、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>と同等の機能を実現するようです。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>が不要になることで、リソース利用効率の向上や<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>の簡素化につながることが期待できます。とくに、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>更新時のPod再起動が不要になることは、大きな利点です。 ASMでAmbient Meshが提供されるかはまだわかりませんが、ぜひとも利用したい機能です。</p> <h3 id="コラム-分散トレーシングOpenTelemetry">コラム: 分散トレーシング OpenTelemetry</h3> <p>Istioでは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>での分散トレーシングをサポートしていますが、複数のサービス間通信を一連のトレーシングとしてまとめることはできません。Context Propagation<sup id="fnref-4653-10"><a href="#fn-4653-10" class="footnote-ref" role="doc-noteref">10</a></sup>、という通信元から通信先へトレーシングに関する情報を引き継ぐ処理が必要で、現時点ではアプリケーションコードでの対応が必須となっています。 分散トレーシングのライブラリはOpenTelemetry<sup id="fnref-4653-11"><a href="#fn-4653-11" class="footnote-ref" role="doc-noteref">11</a></sup>として標準化が進められていて、本記事のサンプルコードでも使用しています。また、対応する言語やカスタマイズ性は限られますが、OpenTelemetry Operator<sup id="fnref-4653-12"><a href="#fn-4653-12" class="footnote-ref" role="doc-noteref">12</a></sup>を使えば、アプリケーションの対応が不要で<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>でのデプロイ時にOpenTelemetryを自動で組み込むことも可能です。</p> <h2 id="ASMを試す">ASMを試す</h2> <p>それでは、実際にASMをインストールし、サービスメッシュの機能を試していきます。</p> <h3 id="ASMの種類とインストール">ASMの種類とインストール</h3> <p>ASMには次の2種類のオプションがあります。</p> <p>①マネージドAnthos Service Mesh(以降、マネージドASM) ②<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>内コン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>プレーン</p> <p>両者の違いはコン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>プレーンの管理方法です。</p> <p>①はコン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>プレーンがGKE<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>の外にある<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudのマネージドサービスから提供されます。コン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>プレーンの運用やアップデートは自動で行われますが、バージョンの選択や使用できる機能に制限があります。 ②はGKE<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>内に自身でコントールプレーンをインストールするものです。Istioを自前でインストールして運用する形式に近く、運用やアップデートは自分で行う必要がありますが、細かくカスタマイズできます。 詳細は公式ドキュメント<sup id="fnref-4653-13"><a href="#fn-4653-13" class="footnote-ref" role="doc-noteref">13</a></sup>を参照してください。 筆者としては、マネージドASMを選択することをお勧めします。最大の理由は「マネージドASMを使うと、コン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>プレーンの運用が大幅に簡単になるため」です。 通常、Istioのアップデートにはコン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>プレーンと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>両方の更新が必要です。さらに、安全なアップデートのためには複数バージョンのコントールプレーンをインストールして段階的にアップデートする<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AB%A5%CA%A5%EA%A5%A2">カナリア</a>アップデートが必要です。 一方、マネージドASMではコントールプレーンのアップデートが自動で行われます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>にも、コントールプレーンの変更を検知して自動再起動するマネージドデータプレーン<sup id="fnref-4653-14"><a href="#fn-4653-14" class="footnote-ref" role="doc-noteref">14</a></sup>というしくみが提供されます。これによって、コン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>プレーンと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>が自動で安全に更新されるのが、大きなメリットです。 キャディでは、マネージドASMを使用していて、これまでに大きなトラブルなくコン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>プレーンが更新され続けています。 ASMのインストール方法は、Istioが提供する方法ではなく<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudから提供されているものを使用します。マネージドASMでは、「デフォルト設定のASMをfleet <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>でインストールする方法<sup id="fnref-4653-15"><a href="#fn-4653-15" class="footnote-ref" role="doc-noteref">15</a></sup>」か「asmcliで細かくカスタマイズする方法<sup id="fnref-4653-16"><a href="#fn-4653-16" class="footnote-ref" role="doc-noteref">16</a></sup>」を選択します。詳細はこれらのドキュメントとサンプルコードのインストール<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を参照ください。</p> <h3 id="サービスメッシュ全体設定">サービスメッシュ全体設定</h3> <p>マネージドASMでは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%BB%A5%B9%A5%ED%A5%B0">アクセスログ</a>や分散トレーシングなどのサービスメッシュ全般に関する設定を ConfigMapで行います。このConfigMapは、istio-systemネームスペースにistio-asm-managedという名前で作成します。 たとえば、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%BB%A5%B9%A5%ED%A5%B0">アクセスログ</a>と分散トレーシングを有効化する場合の例はリスト1のとおりです。</p> <p>▼リスト1 manifests/3_controlplane_config.<a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a></p> <pre><code class="line-numbers">apiVersion: v1 kind: ConfigMap metadata: name: istio-asm-managed namespace: istio-system data: mesh: |- # アクセスログの出力と形式 accessLogFile: /dev/stdout accessLogEncoding: JSON accessLogFormat: (..割愛) # デフォルトで使用する分散トレーシングの機能 defaultConfig: tracing: stackdriver: {} </code></pre> <p>ConfigMapの名称の後半のasm-managedは、マネージドASMのバージョンを表すリリースチャネル<sup id="fnref-4653-17"><a href="#fn-4653-17" class="footnote-ref" role="doc-noteref">17</a></sup>の値です。マネージドASMは更新頻度が異なる3種類のリリースチャネルがあります。asm-managedは最新バージョンから数世代前の安定稼働を確認したバージョンのIstioをベースとした使いやすいバージョンになっています。 dataに記述する内容はマネージドASMのドキュメント<sup id="fnref-4653-18"><a href="#fn-4653-18" class="footnote-ref" role="doc-noteref">18</a></sup>を参照してください。また、ドキュメントにない設定もIstioのMeshConfig<sup id="fnref-4653-19"><a href="#fn-4653-19" class="footnote-ref" role="doc-noteref">19</a></sup>を参考に独自に設定できます。 キャディでは、分散トレーシングのサンプリングレートを変更するなど、マネージドASMのドキュメントに記載のない機能も検証し使用しています。</p> <h3 id="Ingress-Gateway"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Ingress">Ingress</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Gateway">Gateway</a></h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Ingress">Ingress</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Gateway">Gateway</a>はサービスメッシュへの通信の入り口となるサービスで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>と同じくenvoyを使用しています。ロードバランサとの接続先となるサービスで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/Ingress">Ingress</a>リソースの代わりとなるものです。Istioが提供する<a class="keyword" href="https://d.hatena.ne.jp/keyword/Gateway">Gateway</a><sup id="fnref-4653-20"><a href="#fn-4653-20" class="footnote-ref" role="doc-noteref">20</a></sup>リソース、Virtual Service<sup id="fnref-4653-21"><a href="#fn-4653-21" class="footnote-ref" role="doc-noteref">21</a></sup>リソースを記述して、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>やパスに応じたサービスのルーティング、CORSやリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トヘッダ加工、<a class="keyword" href="https://d.hatena.ne.jp/keyword/TLS">TLS</a>終端といった処理ができます。ASMではインストール用の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>が提供されていますので、必要に応じてインストールします。公式ドキュメント<sup id="fnref-4653-22"><a href="#fn-4653-22" class="footnote-ref" role="doc-noteref">22</a></sup>かサンプルコードを参照してください。</p> <h3 id="Istioのマニフェスト">Istioの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a></h3> <p>コン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>プレーンを通じて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>の設定を変更するには、Istioが提供する<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>を作成して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>に適用します。数が多いので、よく使用する機能を中心に取り上げます。 最も利用頻度が高いのは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%BF%AE%C0%A9">通信制</a>御に関する設定でしょう。たとえば、リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トルーティング、リトライ、サーキットブレーカなどです。Istioの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%BF%AE%C0%A9">通信制</a>御に関するガイド<sup id="fnref-4653-23"><a href="#fn-4653-23" class="footnote-ref" role="doc-noteref">23</a></sup>に設定例がまとまっていますので参照してください。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Gateway">Gateway</a>、VirtualService DestinationRule、ServiceEntryといったリソースを使用して通信をカスタマイズできます。 次に、キャディでは認証認可に関する設定をよく使います。サービスメッシュ上のPod間はmTLSで通信するので、各Podはサービスアカウントに基づくクライアント証明書を通信に付与します。クライアント証明書は通信元Podの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BF%C8%B8%B5%CA%DD%BE%DA">身元保証</a>に使用できるため、特定のPodからのみ通信を受け付けるといった制御ができます。 設定例はサンプルコードを参照してください。 そのほかにも、HTTPリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを検証して、特定ヘッダや、認証<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンがなければ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>でアクセスを拒否するという振る舞いも実現できます。これについては、Istioの認証認可に関するガイド<sup id="fnref-4653-24"><a href="#fn-4653-24" class="footnote-ref" role="doc-noteref">24</a></sup>を参照してください。 AuthorizationPolicy、RequestAuthenticationなどのリソースを使用してリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トの検証ができます。</p> <h3 id="アプリケーションをASMに対応する">アプリケーションをASMに対応する</h3> <p>アプリケーションをサービスメッシュに組み込むには、アプリケーションの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>にも一部修正が必要です。 今回のサンプルコードでは、app というnamespaceに、frontおよびbackendという2つのPodをデプロイします。frontサービスはリスト2のように、backnedサービスの<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を呼び出したあとに、その結果を加工してレスポンスを返します。</p> <p>▼リスト2 samples/front/index.js(一部抜粋)</p> <pre><code class="language-Javascript line-numbers">const BACKEND_SERVICE_URL = "http://backend:3100" app.get('/hello', (req, res) =&gt; { axios.get(`${BACKEND_SERVICE_URL}/api`).then(resp =&gt; {F res.send({result: resp.data.answer * 2}) })) </code></pre> <p>app namespaceにデプロイするすべてのPodに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>を挿入するようにするには、リスト3のとおり、app namespaceにistio.io/revラベルを追加します。ラベルの値は前述したリリースチャンネルの値です。このラベルが付与されたnamespaceに Podをデプロイすると、Podの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>に対して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>のimageやサービスメッシュの設定を組み込むための各種設定が自動的に追加されます。</p> <p>▼リスト3 manifests/1_namespace.<a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a></p> <pre><code class="line-numbers">apiVersion: v1 kind: Namespace metadata: name: app labels: istio.io/rev: asm-managed </code></pre> <p>また、ServiceリソースにnameまたはappProtocolフィールドを追加し、サービスの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%BF%AE%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">通信プロトコル</a>を明示します(リスト4)。</p> <p>▼リスト4 manifests/5_front.<a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a>(一部抜粋)</p> <pre><code class="line-numbers">apiVersion: v1 kind: Service metadata: name: front spec: type: ClusterIP ports: - port: 3000 name: http-web # appProtocol: http protocol: TCP selector: app: front </code></pre> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">プロトコル</a>を明示することにより、メトリクスの強化が行われるほか、gRPCを使用している場合はクライアントサイド負荷分散ができるといった利点があります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">プロトコル</a>の種類や規則については、Istioのドキュメント<sup id="fnref-4653-25"><a href="#fn-4653-25" class="footnote-ref" role="doc-noteref">25</a></sup>を参照してください。ここまでの内容を設定してアプリケーションをデプロイすると、アプリケーションPodは図5のように、istio-proxyというコンテナが追加された状態で動いていることがわかります。</p> <p>▼図5 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>コンテナの挿入を確認する</p> <pre><code class="line-numbers"># Pod 一覧 $kubectl get pod -n app # pod内部コンテナを表示 $kubectl get pod backend-nnnn -n app -o jsonpath="{.spec.containers[*].name}" # istio-proxyと backend2つのコンテナがある。 istio-proxy backend </code></pre> <p>istio-proxyが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>です。このとき、Podへの通信はすべて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>経由となっています。前述のfrontサービスのサンプルコードでは、<a href="http://backend:3100">http://backend:3100</a>のようにKubnetesのサービス名でほかのPodへ通信をしています。サービス名を使用した通信は<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>ではよく使用しますが、これは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>を導入したあとでもそのまま使用できます。そのため、アプリケーションコードはASMを導入しても変更する必要はありません。マネージドデータプレーンによりPodは定期的に再起動されることを考慮しておく必要があります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>の起動オプションに <code>EXIT_ON_ZERO_ACTIVE_CONNECTIONS</code>というフラグを有効化すると、Pod終了時にPodへの接続がなくなることを待ってから終了するように指示できます。詳しい設定例はサンプルコードのDeploymentリソースを参照してください。</p> <h3 id="Google-Cloudとの統合"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudとの統合</h3> <p>デプロイしたアプリケーションにリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを送って稼働確認を行うと、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>経由でログやメトリクスが出力されます。これらの情報は<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudの次の機能で利用できます。</p> <ul> <li><strong>Anthos Service Mesh<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボード</strong> : <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>を導入したPodの可視化やリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>ト統計を確認できる</li> <li><strong>Cloud Logging</strong> : <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%BB%A5%B9%A5%ED%A5%B0">アクセスログ</a>が収集される</li> <li><strong>Cloud Monitoring</strong> : <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>のメトリクスを参照、監視できる</li> <li><strong>Cloud Trace</strong> : 分散トレーシングを設定した場合のみ、トレーシングの確認ができる</li> </ul> <p>これで、GKEのPod単位での運用監視が<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudの標準ツールでできるようになりました。ASMの導入を通して、GKE上のサービスの可観測性を向上できたことがわかると思います。</p> <h2 id="今回のまとめ">今回のまとめ</h2> <p>サービスメッシュの導入は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>を使うことから、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>の複雑性やパフォーマンスへの影響を心配されることがあります。 確かに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>は複雑ですが、開発者から見れば、アプリケーションコードの変更なくデプロイできるように配慮されています。一方でASMの運用者は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>を理解し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>の実装であるenvoyを理解しておくと、運用がしやすくなるでしょう。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>インフラとアプリケーションの間にあるサービスメッシュは、通信エラーなどが起きたとき悪者にされがちです。筆者の経験から言えば、通信エラーの原因は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>かアプリケーションのどちらかに適切な通信設定をされていないことが大半であり、サービスメッシュによって強化されたログやメトリクスでエラーを検知できるようになったというだけでした。 通信エラーの調査をする際には、envoyの知識があると役に立ちます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%BB%A5%B9%A5%ED%A5%B0">アクセスログ</a>やメトリクスにあるResponse Flags<sup id="fnref-4653-26"><a href="#fn-4653-26" class="footnote-ref" role="doc-noteref">26</a></sup>という値を見ると、通信断が起きたとき、どちら側からどのような理由で切断されたのかがわかります。 パフォーマンスの影響については、筆者はこれまでに3回、プロダクトにサービスメッシュを導入した経験がありますが、サービスメッシュによってパフォーマンスが極端に落ちたということはありません。結局はそのサービスの性能指標を満たせるかどうかを実際に計測してみるのが大事です。 また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>によってもたらされる可観測性やセキュリティの機能を、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>なしで各サービスに実装するとなったら、その開発<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>は膨大なものとなるでしょう。よって筆者は、サービスメッシュの導入コストは「各サービスに横断で必要となる機能を実装するコストの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%EC%A1%BC%A5%C9%A5%AA%A5%D5">トレードオフ</a>」と考えています。</p> <p>◆ ◆ ◆</p> <p>今回は、サービスメッシュおよび、ASMのインストールとサンプル実行までを紹介しました。 キャディではGKEの可観測性向上を目的にサービスメッシュを導入し、その後も認証認可やセキュリティ強化のために利用する機能を増やしていく予定です。導入目的をはっきりしないといけないと述べましたが、それを明確にするためにも一度ASMを試してみて、みなさんが運用しているサービスの運用向上に役に立つ部分がないかを検証してみると良いと思います。 次回は、モニタリング基盤について紹介する予定です。お楽しみに。</p> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn-4653-1" role="doc-endnote"> <a href="https://github.com/caddijp/sd-asm-example/ ">https://github.com/caddijp/sd-asm-example/ </a><a href="#fnref-4653-1" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-2" role="doc-endnote"> <a href="https://cloud.google.com/anthos/service-mesh ">https://cloud.google.com/anthos/service-mesh </a><a href="#fnref-4653-2" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-3" role="doc-endnote"> <a href="https://istio.io/ ">https://istio.io/ </a><a href="#fnref-4653-3" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-4" role="doc-endnote"> <a href="https://cloud.google.com/anthos ">https://cloud.google.com/anthos </a><a href="#fnref-4653-4" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-5" role="doc-endnote"> AnthosとASMの料金比較<a href="https://cloud.google.com/anthos/pricing">https://cloud.google.com/anthos/pricing</a> <a href="https://cloud.google.com/service-mesh/pricing ">https://cloud.google.com/service-mesh/pricing </a><a href="#fnref-4653-5" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-6" role="doc-endnote"> <a href="https://www.envoyproxy.io/ ">https://www.envoyproxy.io/ </a><a href="#fnref-4653-6" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-7" role="doc-endnote"> <a href="https://opentelemetry.io/ ">https://opentelemetry.io/ </a><a href="#fnref-4653-7" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-8" role="doc-endnote"> <a href="https://istio.io/latest/docs/ ">https://istio.io/latest/docs/ </a><a href="#fnref-4653-8" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-9" role="doc-endnote"> <a href="https://istio.io/latest/blog/2022/introducing-ambient-mesh/ ">https://istio.io/latest/blog/2022/introducing-ambient-mesh/ </a><a href="#fnref-4653-9" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-10" role="doc-endnote"> <a href="https://istio.io/latest/docs/tasks/observability/distributed-tracing/overview/ ">https://istio.io/latest/docs/tasks/observability/distributed-tracing/overview/ </a><a href="#fnref-4653-10" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-11" role="doc-endnote"> <a href="https://opentelemetry.io/ ">https://opentelemetry.io/ </a><a href="#fnref-4653-11" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-12" role="doc-endnote"> <a href="https://opentelemetry.io/docs/kubernetes/operator/ ">https://opentelemetry.io/docs/kubernetes/operator/ </a><a href="#fnref-4653-12" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-13" role="doc-endnote"> <a href="https://cloud.google.com/service-mesh/docs/managed/supported-features-mcp,">https://cloud.google.com/service-mesh/docs/managed/supported-features-mcp,</a> <a href="https://cloud.google.com/service-mesh/docs/supported-features ">https://cloud.google.com/service-mesh/docs/supported-features </a><a href="#fnref-4653-13" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-14" role="doc-endnote"> <a href="https://cloud.google.com/service-mesh/docs/managed/provision-managed-anthos-service-mesh#managed-data-plane ">https://cloud.google.com/service-mesh/docs/managed/provision-managed-anthos-service-mesh#managed-data-plane </a><a href="#fnref-4653-14" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-15" role="doc-endnote"> <a href="https://cloud.google.com/service-mesh/docs/managed/provision-managed-anthos-service-mesh ">https://cloud.google.com/service-mesh/docs/managed/provision-managed-anthos-service-mesh </a><a href="#fnref-4653-15" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-16" role="doc-endnote"> <a href="https://cloud.google.com/service-mesh/docs/managed/provision-managed-anthos-service-mesh ">https://cloud.google.com/service-mesh/docs/managed/provision-managed-anthos-service-mesh </a><a href="#fnref-4653-16" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-17" role="doc-endnote"> <a href="https://cloud.google.com/service-mesh/docs/managed/select-a-release-channel ">https://cloud.google.com/service-mesh/docs/managed/select-a-release-channel </a><a href="#fnref-4653-17" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-18" role="doc-endnote"> <a href="https://cloud.google.com/service-mesh/docs/managed/enable-managed-anthos-service-mesh-optional- ">https://cloud.google.com/service-mesh/docs/managed/enable-managed-anthos-service-mesh-optional- </a><a href="#fnref-4653-18" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-19" role="doc-endnote"> <a href="https://istio.io/latest/docs/reference/config/istio.mesh.v1alpha1/#MeshConfig ">https://istio.io/latest/docs/reference/config/istio.mesh.v1alpha1/#MeshConfig </a><a href="#fnref-4653-19" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-20" role="doc-endnote"> <a href="https://istio.io/latest/docs/reference/config/networking/gateway/ ">https://istio.io/latest/docs/reference/config/networking/gateway/ </a><a href="#fnref-4653-20" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-21" role="doc-endnote"> <a href="https://istio.io/latest/docs/reference/config/networking/virtual-service/ ">https://istio.io/latest/docs/reference/config/networking/virtual-service/ </a><a href="#fnref-4653-21" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-22" role="doc-endnote"> <a href="https://cloud.google.com/service-mesh/docs/gateways ">https://cloud.google.com/service-mesh/docs/gateways </a><a href="#fnref-4653-22" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-23" role="doc-endnote"> <a href="https://istio.io/latest/docs/tasks/traffic-management/ ">https://istio.io/latest/docs/tasks/traffic-management/ </a><a href="#fnref-4653-23" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-24" role="doc-endnote"> <a href="https://istio.io/latest/docs/tasks/security/ ">https://istio.io/latest/docs/tasks/security/ </a><a href="#fnref-4653-24" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-25" role="doc-endnote"> <a href="https://istio.io/latest/docs/ops/configuration/traffic-">https://istio.io/latest/docs/ops/configuration/traffic-</a> management/protocol-selection/&#160;<a href="#fnref-4653-25" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4653-26" role="doc-endnote"> <a href="https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage ">https://www.envoyproxy.io/docs/envoy/latest/configuration/observability/access_log/usage </a><a href="#fnref-4653-26" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> </ol> </div> caddi-tech SaaSのSREチームを立ち上げました hatenablog://entry/6801883189081010198 2023-12-19T10:59:37+09:00 2024-03-27T07:52:08+09:00 本投稿はSRE Advent Calendar 2023の19日目の記事になります。 こんにちは。SREチームの矢野(@yymm)です。 今年の4月からCADDi DRAWERのサービス信頼性向上のためSREチームが活動を始めています。チーム立ち上げから3Q経過して方向性も見えてきたため改めて立ち上がりから今までのことを紹介します。 CADDi DRAWERについて 図面データ活用クラウドのCADDi DRAWERは、2022年6月に正式にローンチされました。 ローンチから1年半経過し取り扱うデータ数のオーダーやユーザー数が拡大してきており、機能面の拡充はもちろんのことですが、非機能要件の重要… <p>本投稿は<a class="wp-editor-md-post-content-link" href="https://qiita.com/advent-calendar/2023/sre">SRE Advent Calendar 2023</a>の19日目の記事になります。</p> <p>こんにちは。SREチームの矢野(<a class="wp-editor-md-post-content-link" href="https://github.com/yymm">@yymm</a>)です。</p> <p>今年の4月からCADDi DRAWERのサービス信頼性向上のためSREチームが活動を始めています。チーム立ち上げから3Q経過して方向性も見えてきたため改めて立ち上がりから今までのことを紹介します。</p> <h1 id="CADDi-DRAWERについて">CADDi DRAWERについて</h1> <p><img src="https://assets.caddi.com/tech-blog/wp-content/uploads/2023/12/スクリーンショット-2023-12-20-17.01.32-1024x316.png" alt="CADDi DRAWER" /></p> <p><a class="wp-editor-md-post-content-link" href="https://caddi.com/drawer/">図面データ活用クラウドのCADDi DRAWER</a>は、2022年6月に正式にローンチされました。</p> <p>ローンチから1年半経過し取り扱うデータ数のオーダーやユーザー数が拡大してきており、機能面の拡充はもちろんのことですが、非機能要件の重要性も高まってきています。</p> <p>私は2021年12月のDRAWER立ち上げ期から開発に関わるSWEだったのですが、私も含め当時関わっていたメンバーがアプリケーション開発メインだったこともありインフラ周りや非機能要件周りに課題があるのを感じていました。</p> <p>課題があるなら解決やったるぞということでSREへの社内ジョブチェンジを打診してチームの立ち上げをしました。1エンジニアのチャレンジを認めてもらえたことに感謝をしているのと、会社としての懐の広さを感じています。</p> <h1 id="SREチーム立ち上げから今までの道のり">SREチーム立ち上げから今までの道のり</h1> <p><img src="https://assets.caddi.com/tech-blog/wp-content/uploads/2023/12/無題-2023-01-16-1516-5.png" alt="SREチーム立ち上げタイムライン" /></p> <p>ローンチから9ヶ月してCADDi DRAWER開発チームの拡大に伴い、Team Topologyをベースにしたチーム構成が採用されました。SREチームは、横断的な活動をするEnablingチームのサブチームとして位置づけられています。</p> <p>キャディでは3ヶ月を1クォーター(Q)として、その都度目標を設定しています。立ち上げから3Q経過したので、それぞれのQでの活動を振り返っていきます。</p> <h2 id="立ち上げ初期2023-04-SRE未経験からのスタート">立ち上げ初期(2023-04~): SRE未経験からのスタート</h2> <p>SREチームの初期メンバーは2人で実は2人ともSRE未経験、当時MLOps領域を担当していたメンバーと共に始まりました。モチベーションはあるもののどうしようか...というところからのスタートでした。</p> <p>とはいえ目先の課題はあったので、Engineering Managerと一緒に課題を整理して目標を設定しました。 経験もなくSREチームとしても人数少ないですが、 Platformチームに協力を仰ぎ適宜支援いただきながら 以下のようなトピックに取り組みました。</p> <ul> <li>インフラコストの削減と可視化</li> <li>セキュリティ向上のためのIAMやサービスアカウント周り整備</li> <li>IaCの整備</li> <li>GKEの保守運用(クラスタアップグレードなど)</li> </ul> <p>もう一つ大事なことは、我々はそもそもSREとは何かを学ぶ必要があったので、輪読会を開催してSRE本やSRE Workbookを読みSREへの理解を深めていきました。</p> <ul> <li><a class="wp-editor-md-post-content-link" href="https://www.oreilly.co.jp/books/9784873117911/">SRE サイトリライアビリティエンジニアリング―Googleの信頼性を支えるエンジニアリングチーム</a></li> <li><a class="wp-editor-md-post-content-link" href="https://www.oreilly.co.jp/books/9784873119137/">サイトリライアビリティワークブック―SREの実践方法</a></li> </ul> <p>安心安定の定番書籍SRE本です、SRE未経験の私にとってはSREの役割や考え方を学ぶにはとても良い本でした。今でも作業の傍らにおいて読み返したりすることも多くバイブルと化しています。</p> <h2 id="自律的な目標設定2023-07-プロダクトロードマップとの連携">自律的な目標設定(2023-07~): プロダクトロードマップとの連携</h2> <p>前の四半期ではSREとは何かをよく分からず取り組みましたが、今後はSREらしいチーム目標を立てて自律的に活動していきたいと考えていました。</p> <p>自分もSREという言葉を知っていましたが具体的なSite Reliability Engneeringというエンジニアリングアプローチを知らない方も一定いると思い、そもそもSREについての組織全体の理解度を上げることも大事と思い、Slackの雑談チャンネルなどでの布教活動もしていました。</p> <p><img src="https://assets.caddi.com/tech-blog/wp-content/uploads/2023/12/スクリーンショット-2023-12-21-11.28.19.png" alt="SREのSLI/SLO Tips" /></p> <p>これからSREチームの取り組みを効果的に発揮していくためには、開発チームに閉じないでPdMなどのプロダクトの意思決定ができるステークホルダーと一緒に取り組むのが大事と思い、そのレイヤーの方々を巻き込んでの目標設定に取り組みました。</p> <p>今まで取り組んできたトピックでもPdMやTPM(Technical Product Manager)などのプロダクトサイドとの接点があったことや前述の布教活動が功を奏してSREの認知が広まっていたこともありスムーズに議論は進み、最終的にプロダクトロードマップの1項目としてReliabilityを設定することになりました。</p> <p><img src="https://assets.caddi.com/tech-blog/wp-content/uploads/2023/12/スクリーンショット-2023-12-21-11.17.34.png" alt="What is SRE" /></p> <p>これはプロダクトロードマップの説明スライドの1枚なのですが、大枠は<a class="wp-editor-md-post-content-link" href="https://www.googlecloudcommunity.com/gc/Community-Blogs/Site-Reliability-Engineering-SRE-Fundamentals/ba-p/472590#toc-hId-826625717">Googleが提唱しているSREのベストプラクティス</a>に則る形で策定しています。</p> <p>プロダクトサイドとの連携は今後も重要になってくるため、このアプローチを活かしてSREチームはSREらしく活動することができ、その活動がビジネスの目標と連携していき組織的な成長を促進していけるように取り組んで行こうと思っています。</p> <h2 id="本格活動開始2023-10-Enablingチーム拡大でSREにフォーカスできるように">本格活動開始(2023-10~): Enablingチーム拡大でSREにフォーカスできるように</h2> <p>順調に方向性も定まりやっていくぞという雰囲気なのですが1つ問題がありました。</p> <p>横断的なチームという特性と、私が落ちがちなボールを見つけると拾う習性があったため、一般的にはSREチームでは扱わないようなトピックも扱うようになっておりSREのフォーカスに集中しきれないという課題がありました。</p> <p>2023-10のタイミングでちょうど全社的な組織変更がありEnablingチームメンバーの拡充され、以下のようなチーム体制への変化がありました。</p> <p><img src="https://assets.caddi.com/tech-blog/wp-content/uploads/2023/12/スクリーンショット-2023-12-20-10.54.35-1024x574.png" alt="開発組織" /></p> <p><a class="wp-editor-md-post-content-link" href="https://speakerdeck.com/caddi_eng/caddi-the-letter-from-cto-to-all-the-engineer-applicants?slide=39">エンジニア向け会社紹介資料</a>から抜粋</p> <p>紫色のEnablingチームは元々SRE/QA/アーキテクチャチームで構成されていたのですが、データマネジメントチームが組成され各チームの人員も増えました。</p> <p>この組織変更と人事異動が功を奏して関わっていたSRE以外のトピックを、適切なチームに責務とタスクを委譲することができました。担当の余力が少なく、SREで巻き取っていたデータマネジメント関連のタスクを委譲できたのは個人的にインパクトが大きかったです。</p> <p>Enablingチームの人数が増えたことでチームに活気も生まれ、連携強化のためにサブチーム全員で1つのスクラムを回してみたり、スクラムのトレーニングが実施されるなどチームの強度が上がってきているのを感じています。</p> <p>実際の活動についてはPdMとTPMと協力して定めたプロダクトロードマップに沿って進めており、以下のような内容に取り組んでいます。</p> <ul> <li>Metrics &amp; Monitoring <ul> <li>PdM, TPMと相談してクリティカルユーザージャーニーの再設定、SLIの計測に向けた基盤構築</li> </ul></li> <li>Emergency Response <ul> <li>インシデント対応フロー、On-call体制の整備</li> </ul></li> <li>Capacity Planning <ul> <li>キャパシティ評価の基盤構築</li> </ul></li> <li>Change Management <ul> <li>QAチームとArchチームでアーキテクチャやリリースフローの整理</li> <li>PlatformチームでFour Keys測定の基盤構築</li> </ul></li> </ul> <p>プロダクトロードマップの大部分はSREチームがOnwerとして取り組んでいますが、Change Managementの部分は状況的にSREチーム以外のチームに協力してもらいつつ連携を取りながら進めています。横断チーム間で連携しつつプロダクトロードマップにアラインした活動を行うことができており、今後も適宜コラボレーションモードを切り替えながら効果的に施策を進めていきたいと思っています。</p> <h1 id="今後の展望">今後の展望</h1> <p>定めたロードマップを推進して、信頼性の高いサービス提供ができるように組織全体のSRE力を上げていきます。今回の記事では具体的な取り組み内容に関しては言及できてないので、今後はSREチームの成果も発信していきたいと思っています。</p> <p>SREというロールは初めてまだ1年満たないので馬力不足なところもありますが、一人前のSREを名のれるようにどんどん成長していく所存です。</p> <p>昨今はSREに関連する書籍も結構増えてきているので、輪読会も継続していきます。輪読会はSREチーム以外の方も自由参加でわいわいやっています。</p> <p><img src="https://assets.caddi.com/tech-blog/wp-content/uploads/2023/12/スクリーンショット-2023-12-21-11.30.55.png" alt="輪読会の様子" /></p> <p>実はPlatformチームからインフラに強いメンバーのジョインもあり現在3人チームになりました。とはいえまだまだやらないといけないことがたくさんあり、SREポジションを絶賛募集中です。</p> <p>製造業のポテンシャルを解放する成長中のSaaSを支えるSREに興味のある方は、カジュアル面談も行っていますのでぜひお気軽にお声掛けください。最後まで読んでいただきありがとうございました。</p> <ul> <li><a class="wp-editor-md-post-content-link" href="https://open.talentio.com/r/1/c/caddi-jp-recruit/pages/83887">SRE(Site Reliability Engineer)採用情報・応募ページ</a></li> <li><a class="wp-editor-md-post-content-link" href="https://recruit.caddi.tech/casualmeeting?pvs=25">Tech向けカジュアル面談申し込みページ</a></li> </ul> caddi-tech 第7回: Argo CDによるKubernetesへの継続的デリバリ hatenablog://entry/6801883189081010231 2023-11-21T14:00:27+09:00 2024-02-26T17:09:08+09:00 ※本記事は、技術評論社「Software Design」(2023年10月号)に寄稿した連載記事「Google Cloudで実践するSREプラクティス」からの転載です。発行元からの許可を得て掲載しております。 はじめに 前回はRenovateによる依存関係の更新について解説しました。今回はArgo CD1を利用した、Kubernetesへの継続的デリバリ(Continuous Delivery、CD)について紹介します。Argo CDとは何か、なぜ使うのか、基本的な機能やキャディでどのように活用しているかを紹介します(図1)。 ▼図1 CADDiスタックにおける今回の位置付け Argo CDと… <p>※本記事は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%BB%BD%D1%C9%BE%CF%C0%BC%D2">技術評論社</a><a class="wp-editor-md-post-content-link" href="https://gihyo.jp/magazine/SD/archive/2023/202310">「Software Design」(2023年10月号)</a>に寄稿した連載記事「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudで実践するSREプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティス」からの転載です。発行元からの許可を得て掲載しております。</p> <h2 id="はじめに">はじめに</h2> <p><a class="wp-editor-md-post-content-link" href="https://caddi.tech/archives/4585">前回</a>はRenovateによる依存関係の更新について解説しました。今回はArgo CD<sup id="fnref-4626-1"><a href="#fn-4626-1" class="footnote-ref" role="doc-noteref">1</a></sup>を利用した、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>への継続的デリバリ(Continuous Delivery、CD)について紹介します。Argo CDとは何か、なぜ使うのか、基本的な機能やキャディでどのように活用しているかを紹介します(図1)。</p> <p>▼図1 CADDiスタックにおける今回の位置付け <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208183329.png" alt="" /></p> <h2 id="Argo-CDとは">Argo CDとは</h2> <p>Argo CDは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>への継続的デリバリを行うツールです。Git<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>をソースとして継続的デリバリを行う手法をGitOpsと呼びます<sup id="fnref-4626-2"><a href="#fn-4626-2" class="footnote-ref" role="doc-noteref">2</a></sup>。Argo CDは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>へのデプロイをGitOpsに沿って行います。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>へのデプロイは、デプロイ内容を記述した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>ファイルを、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a> APIやkubectlコマンドに指定して実施します。 この作業は、ファイル数が増えると煩雑になるほか、ファイルの変更を追従して<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>に反映することが困難になります。 Argo CDはGit<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にある<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>ファイルを取得し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>への<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>ファイルの適用状況を可視化します。また、差分検知や履歴管理、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>、自動反映といった機能も備えています。権限制御可能なWeb UI があるため、Argo CDを通して<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>にデプロイされているサービスの構成を把握する、管理者のみがArgo CD経由でデプロイ操作をするといった操作もできます。</p> <h2 id="なぜArgo-CDか">なぜArgo CDか</h2> <p>Argo CDは豊富な機能を提供していますが、その中でも筆者らがArgo CDを採用している最大の理由は、リッチなWeb UIがあるからです。たかがUIされどUIです。百聞は一見にしかずですので、まだ触ったことがなければぜひ公式のデモ環境<sup id="fnref-4626-3"><a href="#fn-4626-3" class="footnote-ref" role="doc-noteref">3</a></sup>を体験してみてください。 DevOps実現のため、開発者がkubectlコマンド使いこなすことはすばらしいことです。しかし、チーム内すべての開発者がそれを習得する必要はないと考えています。Web UIでは、簡単にデプロイしたりリソースの状態を参照したりできます。それによって開発者が、プロダクト(サービス)の本質的な価値向上のためにより多くの時間を使えるようになります。 また、キャディのArgo CD導入以前(2020年ごろ)のCDは、Push型GitOps<sup id="fnref-4626-4"><a href="#fn-4626-4" class="footnote-ref" role="doc-noteref">4</a></sup>を採用しており、セキュリティやデプロイ単位の柔軟性・属人性といった面で次のような課題がありました。これらの課題の解消にもArgo CDは役立っています。</p> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a> Engine(GKE)のPrivate Cluster<sup id="fnref-4626-5"><a href="#fn-4626-5" class="footnote-ref" role="doc-noteref">5</a></sup>に対して、デプロイごとに CD Server側のIPを承認済みネットワーク<sup id="fnref-4626-6"><a href="#fn-4626-6" class="footnote-ref" role="doc-noteref">6</a></sup>に追加する必要がある</li> <li>CD Server側で、機密情報をDecryptして<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>をデプロイする必要がある</li> <li>特定のプロダクトの単位でデプロイができない(一括で複数のプロダクトリソースをClusterに対してすべてまとめてデプロイしていた)</li> <li>デプロイ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を作り込んであり、作成者以外が簡単に変更できない</li> </ul> <h2 id="デプロイの流れ">デプロイの流れ</h2> <p>図2は、Argo CDによるデプロイの流れを抽象化したものです。Git<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の変更を起点として、Argo CDがその変更を検知し、次の流れでデプロイを実行します。</p> <p>①Argo CDがPollingによりGit<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>から<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>を取得、差分検知する <br> ②Argo CDが指定された差分をデプロイする <br> ③開発者がWeb UI上でデプロイ結果を確認する <br></p> <p>また、これは自動同期の設定を有効にしている場合の例です。自動同期の設定を無効にしておくと、①と②のステップの間で、開発者がWebUI上で差分を確認しながら手動で同期処理をトリガーできます。</p> <p>▼図2 Argo CDによるデプロイの流れ</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208183307.png" alt="image" width="600" height="auto"></p> <h2 id="Argo-CD-のProjectとApplication">Argo CD のProjectとApplication</h2> <p>Argo CD を構成する重要な要素として、ProjectとApplicationがあります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>のCustom Resource Definition では、「AppProject」と「Application」という名前でそれぞれ定義されています。 図3は、ProjectとApplicationの構成例と簡単なデプロイの関係性を表したものです。</p> <p>▼図3 ProjectとApplication <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208183336.png" alt="image" width="450" height="auto"></p> <p>Applicationの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>には、デプロイ対象<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>群(以降、<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>)の場所を定義します(リスト1)。 このApplicationがArgo CDによるデプロイの最小単位となります。 より具体的には、次のような情報を指定します。</p> <ul> <li>デプロイ先のClusterやNamespace</li> <li>所属するProject</li> <li>デプロイ対象<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>群の場所やRevision</li> </ul> <p>Git<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>とCluster間で差分が発生したときの同期ポリシーデプロイ対象<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>群の指定は、デフォルトでは次のものに対応しています。</p> <ul> <li>Kustomize</li> <li>Helm chart</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/YAML">YAML</a>/<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>/Jsonnetの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ</li> </ul> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>を別途入れることによって、そのほかのConfig管理ツールの利用も可能です。 また、Applicationは必ず1つのProjectにひも付きます。デフォルトでは、default Projectが用意されており、指定が可能となっていますが、特別な事情がない限り個別にProjectを作成することをお勧めします。</p> <p>▼リスト1 application-example.<a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a></p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">apiVersion</span><span class="synSpecial">:</span> argoproj.io/v1alpha1 <span class="synIdentifier">kind</span><span class="synSpecial">:</span> Application <span class="synIdentifier">metadata</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> example <span class="synIdentifier">namespace</span><span class="synSpecial">:</span> argocd <span class="synIdentifier">spec</span><span class="synSpecial">:</span> <span class="synIdentifier">destination</span><span class="synSpecial">:</span> <span class="synIdentifier">namespace</span><span class="synSpecial">:</span> example-namespace <span class="synIdentifier">server</span><span class="synSpecial">:</span> https://kubernetes.default.svc <span class="synIdentifier">project</span><span class="synSpecial">:</span> example-project <span class="synIdentifier">source</span><span class="synSpecial">:</span> <span class="synIdentifier">path</span><span class="synSpecial">:</span> applications/example/overlays/dev <span class="synIdentifier">repoURL</span><span class="synSpecial">:</span> https://github.com/caddijp/example-cluster-config.git <span class="synIdentifier">targetRevision</span><span class="synSpecial">:</span> main <span class="synIdentifier">syncPolicy</span><span class="synSpecial">:</span> <span class="synIdentifier">automated</span><span class="synSpecial">:</span> <span class="synSpecial">{}</span> </pre> <p>Projectは、Applicationを束ねるオブジェクトです(リスト2)。この<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>に、一定の制限を定義することで統制を効かせやすくなります。具体的には次のようなものです。</p> <ul> <li>デプロイできるGit<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の制限</li> <li>デプロイ先のClusterやNamespaceの制限</li> <li>デプロイできる<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>リソースの種類の制限</li> <li>RBACで利用するProjectにひも付くロールの定義</li> </ul> <p>RBAC(後述)の設定で、特定のProjectをそのオーナーとなる開発チームへ割り当てることで、誰が何を管理しているかを明確にしつつ、必要最小限の権限を付与できます。</p> <p>▼リスト2 project-example.<a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a></p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">apiVersion</span><span class="synSpecial">:</span> argoproj.io/v1alpha1 <span class="synIdentifier">kind</span><span class="synSpecial">:</span> AppProject <span class="synIdentifier">metadata</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> example-project <span class="synIdentifier">namespace</span><span class="synSpecial">:</span> argocd <span class="synIdentifier">finalizers</span><span class="synSpecial">:</span> <span class="synStatement">- </span>resources-finalizer.argocd.argoproj.io <span class="synIdentifier">spec</span><span class="synSpecial">:</span> <span class="synIdentifier">description</span><span class="synSpecial">:</span> Admin Project <span class="synIdentifier">sourceRepos</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synConstant">'*'</span> <span class="synIdentifier">destinations</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">namespace</span><span class="synSpecial">:</span> example-namespace <span class="synIdentifier">server</span><span class="synSpecial">:</span> https://kubernetes.default.svc <span class="synIdentifier">clusterResourceWhitelist</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">group</span><span class="synSpecial">:</span> <span class="synConstant">'*'</span> <span class="synIdentifier">kind</span><span class="synSpecial">:</span> <span class="synConstant">'*'</span> <span class="synIdentifier">roles</span><span class="synSpecial">:</span> <span class="synSpecial">[]</span> </pre> <h2 id="キャディで利用している構成">キャディで利用している構成</h2> <p>図4は、キャディで利用している構成の概要図です。開発者を起点としたデプロイの流れは次のようになります。</p> <p>①:開発者が<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>のPull requestをマージもしくはRelease Tagを作成する<br> ②:<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsの指定されたWorkflowが起動する<br> ③:Imageを作成しArtifact Registryにプッシュする<br> ④:<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の対象のImage Tagを書き換える<br> ⑤:Argo CDが Polling によりGit<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>から<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>を取得、差分検知する<br> ⑥:Argo CDが指定された差分をデプロイする<br> ⑦:Argo CDが指定されたSlack Channelに同期状態の変更を通知する<br></p> <p>図4では表現できていないところを含め、詳細を解説していきます。</p> <p>▼図4 キャディで利用している構成 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208183322.png" alt="image" width="600" height="auto"></p> <h3 id="Cluster構成">Cluster構成</h3> <p>筆者らは、マルチテナント方式<sup id="fnref-4626-7"><a href="#fn-4626-7" class="footnote-ref" role="doc-noteref">7</a></sup>でArgo CDを構築し、同じCluster上で複数のプロダクト(サービス)を運用しています。環境はCluster単位で分離し、Development/Staging/Productionの3つです。 また、Argo CDは仕様上1つのArgo CD環境で複数のClusterを管理できますが、筆者らClusterごとにArgo CDを構築するようにしています。おもな理由は3つです。</p> <p>1つめは「単一障害点(SPOF)になるのを避ける」ためです。仮に1つのArgo CD環境ですべてのClusterを管理している場合、そのArgo CD環境が動かなくなったときにすべてのデリバリが止まってしまうリスクがあります。ClusterごとにArgo CDを構築しておくことで、依存関係のない独立したClusterとなり、そのリスクを最小化できます。</p> <p>2つ目は「アップグレードがしやすい」からです。アップグレードの重要性は前回の連載で触れているため省略します。Argo CDは開発が活発で、リリースサイクルが早いです。仮に、アップグレード時に移行ミスがあった場合、デリバリが 止まってしまうリスクがあります。 Development環境のClusterからアップグレードを進め、適用後一定期間様子を見るなど、リスクを最小化するためのアップグレード戦略を立てやすくなります。</p> <p>3つ目は「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を外部に公開する必要がなくなる」からです。前述のとおり独立したClusterとなるため、外部に<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を公開する必要がなく、Clusterをより安全に運用できます。</p> <h3 id="リポジトリ構成"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>構成</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>は次のような構成となっています。</p> <ul> <li>Clusterで管理する<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>を集約した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>が1つ</li> <li>アプリケーションごとの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>が複数</li> </ul> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>はアプリケーション側の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>でも管理できます。しかし筆者らは、それぞれの責務やライフサイクルが異なるため、Argo CDを採用する前から意図的に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を分離しています。ポイントは、公式ドキュメントのベストプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティス<sup id="fnref-4626-8"><a href="#fn-4626-8" class="footnote-ref" role="doc-noteref">8</a></sup>に記載されています。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>とアプリケーションコードの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を分離する利点は次のとおりです。</p> <ul> <li>それぞれのライフサイクルに依存しない<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B7%D1%C2%B3%C5%AA%A5%A4%A5%F3%A5%C6%A5%B0%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">継続的インテグレーション</a>やデリバリを構築できる</li> <li>変更履歴(監査ログ)をきれいに保てる</li> <li>それぞれの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>でアクセス権や変更権限を分離できる</li> </ul> <p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を分離しない場合は次のような課題が残ります。</p> <ul> <li>アプリケーションコードの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>が複数あるとき、どこに<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>を配置するべきかを考える必要がある</li> <li>自動化のトリガーとなる変更対象が何かを判定する必要があり、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B7%D1%C2%B3%C5%AA%A5%A4%A5%F3%A5%C6%A5%B0%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">継続的インテグレーション</a>のパイプライン構築が複雑化する</li> </ul> <h3 id="ブランチ戦略">ブランチ戦略</h3> <p>図5はブランチ戦略を簡単に表現した図です。 アプリケーションコードの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>と<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>、どちらもmainブランチのみを利用しています。<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>上では、通常の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>の変更はPull requestを作成する運用になっています。アプリケーションのデプロイパイプラインではImage Tagのみを<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsで自動的に書き換えています。</p> <p>▼図5 ブランチ戦略 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208183314.png" alt="" /></p> <h4 id="Development環境への反映">Development環境への反映</h4> <p>Development環境へ反映の流れは次のようになります。</p> <p>①アプリケーションコードの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>でPull requestをマージする<br> ②<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsでテスト、Imageの作成後、<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>のWorkflowをトリガーする<br> ③<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>のWorkflowでDevelopment環境用の<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>のImage Tagを書き換える<br></p> <p>Image Tagの書き換えは<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsのrepository_dispatch<sup id="fnref-4626-9"><a href="#fn-4626-9" class="footnote-ref" role="doc-noteref">9</a></sup>を利用して<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>側で実行しています。アプリケーションコードの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>側のWorkflowで書き換えると、コンフリクトが発生したり、余計な権限を持たせたりしないといけないからです。</p> <h4 id="StagingProduction環境への反映">Staging/Production環境への反映</h4> <p>Staging/Production環境へ反映の流れは次のようになります。基本的な流れはDevelopment環境の場合と同様ですが、起点と2環境ぶん同時にImage Tag書き換えをするところが異なります。</p> <p>①アプリケーションコードの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>でRelease Tagを作成する<br> ②<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsでテスト、Imageの作成後、<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>のWorkflowをトリガーする<br> ③<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>のWorkflowでStaging/Production環境用の<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>のImage Tagを書き換える<br></p> <p>Production環境だけArgo CDの自動同期設定をOFFにしており、Staging環境での動作確認後、開発チームごとに任意のタイミングでWebUI上からデプロイや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>をする運用となっています。 同じCommit HashでImageがすでに作成済みのときは、Image作成処理をSkipすることでリードタイムを短縮する工夫をしています。Development 環境で検証済みの ImageをStaging/Production環境でも使うことは、アプリケーションコードの同一性担保にも役立ちます。</p> <h3 id="Argo-CDの設定管理">Argo CDの設定管理</h3> <p>Argo CDは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>へデプロイするリソースを宣言的に管理します。開発者が追加する<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>だけでなく、Argo CD本体やその設定も宣言的に管理<sup id="fnref-4626-10"><a href="#fn-4626-10" class="footnote-ref" role="doc-noteref">10</a></sup>できます。 Argo CDをClusterへインストール後、Argo CDのProjectやApplicationをWeb UIから追加できますが、筆者らはそれらの設定もコード化しています。Argo CDの本体や設定をコード化するおもな理由は、次のようなことを実現するためです。</p> <ul> <li>再現性</li> <li>再利用性</li> <li>属人性の排除</li> <li>静的解析による統制</li> </ul> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>では、Kustomizeを利用し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構成は下記のようになっています。</p> <p>▼リスト3 <a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構成</p> <pre class="code" data-lang="" data-unlink>applications/ ├── product1/ │ ├── base/ │ │ ├── ui/ │ │ │ └── ... │ │ ├── bff/ │ │ │ ├── deployment.yaml │ │ │ ├── secret.yaml │ │ │ ├── service.yaml │ │ │ └── config.yaml │ │ └── kustomization.yaml │ └── overlays/ │ ├── dev │ │ └── ... │ │ └── kustomization.yaml │ ├── stg │ └── prod ├── product2 └── ... argocd/ ├── base/ │ ├── argocd-cm.yaml │ ├── argocd-notifications-cm.yaml │ ├── ... │ └── kustomization.yaml └── overlays/ ├── dev/ │ ├── pj-admin/ │ │ ├── app-argocd.yaml │ │ ├── ... │ │ ├── helm-eso.yaml │ │ ├── helm-eso.values.yaml │ │ └── project.yaml │ ├── pj-sample1/ │ │ ├── app-product1.yaml │ │ └── app-product2.yaml │ ├── ... │ ├── argocd-rbac-cm.yaml │ └── kustomization.yaml ├── stg └── prod</pre> <p>applications<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リでは、Argo CD Applicationから指定する<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>を管理します。ここでは、プロダクト(サービス)ごと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを作成し、デプロイしたい<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>群の最小単位をまとめています。この<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>群の最小単位が、どのArgo CD Application/Project や Namespaceに所属するかは関心事として切り離されているため、意図的にフラットな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構成としています。</p> <p>argocd<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リでは、Argo CDの本体や設定を管理します。初回インストールは、KustomizeでArgo CDのリモートリソース指定し<sup id="fnref-4626-11"><a href="#fn-4626-11" class="footnote-ref" role="doc-noteref">11</a></sup><a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>を作成し、kubectlコマンドで反映します。その<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>自体をapp-argocd.<a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a>(リスト4)で定義した1つのArgo CD Applicationとして、インストールされたArgo CDで管理します。</p> <p>▼リスト4 app-argocd.<a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a></p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">apiVersion</span><span class="synSpecial">:</span> argoproj.io/v1alpha1 <span class="synIdentifier">kind</span><span class="synSpecial">:</span> Application <span class="synIdentifier">metadata</span><span class="synSpecial">:</span> <span class="synIdentifier">name</span><span class="synSpecial">:</span> app-argocd <span class="synIdentifier">namespace</span><span class="synSpecial">:</span> argocd <span class="synIdentifier">spec</span><span class="synSpecial">:</span> <span class="synIdentifier">destination</span><span class="synSpecial">:</span> <span class="synIdentifier">namespace</span><span class="synSpecial">:</span> argocd <span class="synIdentifier">server</span><span class="synSpecial">:</span> https://kubernetes.default.svc <span class="synIdentifier">project</span><span class="synSpecial">:</span> pj-admin <span class="synIdentifier">source</span><span class="synSpecial">:</span> <span class="synIdentifier">path</span><span class="synSpecial">:</span> argocd/overlays/dev <span class="synIdentifier">repoURL</span><span class="synSpecial">:</span> https://github.com/caddijp/example-cluster-config.git <span class="synIdentifier">targetRevision</span><span class="synSpecial">:</span> main <span class="synIdentifier">syncPolicy</span><span class="synSpecial">:</span> </pre> <h3 id="RBAC">RBAC</h3> <p>Argo CDの認証にはさまざまな方法がとれますが、筆者らキャディでは<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>認証を使用しています。<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>アカウントにひも付いている<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Team<sup id="fnref-4626-12"><a href="#fn-4626-12" class="footnote-ref" role="doc-noteref">12</a></sup>とArgo CDのRole<sup id="fnref-4626-13"><a href="#fn-4626-13" class="footnote-ref" role="doc-noteref">13</a></sup>をひも付けて権限を管理しています。 リソースとアクションを組み合わせることで、要件に合わせて柔軟に権限を定義し、ユーザーグループ(<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Team)へのひも付けができます。Argo CD Applicationに対して個別に権限付与するより、Argo CD Project単位で権限付与たほうが圧倒的に楽ですので、基本的に開発チーム単位でArgo CD Projectを定義するのがお勧めです。 しかし、キャディはスタートアップという特性上、事業や開発チームの変更頻度が高く、その運用だと開発チームの実態と Argo CD Projectがすぐに一致しなくなります。そのため、執筆時点では、プロダクト(サービス)や類似プロダクト群ごとにArgo CD Projectを作成するケースが多くなっています。</p> <h3 id="Secret管理">Secret管理</h3> <p>GKE内で機密情報(Secret)を安全かつ簡単に管理するために、External Secrets<sup id="fnref-4626-14"><a href="#fn-4626-14" class="footnote-ref" role="doc-noteref">14</a></sup>を利用しています。機密情報の実体は<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> CloudのSecret Manager<sup id="fnref-4626-15"><a href="#fn-4626-15" class="footnote-ref" role="doc-noteref">15</a></sup>で管理していますExternal Secretsを利用することで、各<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>リソースからは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a> Secretを通して透過的に機密情報にアクセスできます。 また、Workload Identity<sup id="fnref-4626-16"><a href="#fn-4626-16" class="footnote-ref" role="doc-noteref">16</a></sup>を利用し、External Secretsの<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>サービスアカウントと<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudのサービスアカウントをひも付けることができます。これによって、Secret Managerを参照するための鍵情報(サービスアカウントキー)をGKE内に持たせず運用できています。 ちなみに、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudのサービスアカウントのベストプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティス<sup id="fnref-4626-17"><a href="#fn-4626-17" class="footnote-ref" role="doc-noteref">17</a></sup>を参考にして、External Secret以外のリソースも基本的にサービスアカウントを分離しWorkload Identityを利用しています。<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudサービスアカウントの鍵情報を管理する必要がなくなることにより、両方のサービスアカウントの分離作業が楽になります。それは、サービスアカウントの権限を最小化し、トレーサビリティを向上させることも楽になるということです。</p> <h3 id="Slackへの通知">Slackへの通知</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>の同期状態をSlackへ通知<sup id="fnref-4626-18"><a href="#fn-4626-18" class="footnote-ref" role="doc-noteref">18</a></sup>させて、継続的デリバリの状態を把握できるようにしています。Argo CDのv2.3からArgo CD Notificationsが内包<sup id="fnref-4626-19"><a href="#fn-4626-19" class="footnote-ref" role="doc-noteref">19</a></sup>されるようになり、より簡単に通知の設定ができます。通知先は、Argo CD ProjectやArgo CD Applicationのannotationsでイベントごとに定義します。</p> <h2 id="おわりに">おわりに</h2> <p>今回はArgo CDの概要とキャディでの採用理由、また基本的な機能や継続的デリバリの構築事例を紹介しました。キャディでは、2021年の初めからArgo CDへ移行し、今ではプロダクト(サービス)を構築、運用していくための欠かせないツールになっています。筆者自身、執筆していく中で、Argo CDがさまざまな運用の課題を解決してくれるすばらしいツールだとあらためて感じました。 来月はサービスメッシュについて紹介する予定です。お楽しみに。</p> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn-4626-1" role="doc-endnote"> <a href="https://github.com/argoproj/argo-cd ">https://github.com/argoproj/argo-cd </a><a href="#fnref-4626-1" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-2" role="doc-endnote"> <a href="https://www.weave.works/technologies/gitops/ ">https://www.weave.works/technologies/gitops/ </a><a href="#fnref-4626-2" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-3" role="doc-endnote"> <a href="https://cd.apps.argoproj.io/ ">https://cd.apps.argoproj.io/ </a><a href="#fnref-4626-3" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-4" role="doc-endnote"> <a href="https://caddi.tech/archives/2041 ">https://caddi.tech/archives/2041 </a><a href="#fnref-4626-4" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-5" role="doc-endnote"> <a href="https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters ">https://cloud.google.com/kubernetes-engine/docs/how-to/private-clusters </a><a href="#fnref-4626-5" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-6" role="doc-endnote"> <a href="https://cloud.google.com/kubernetes-engine/docs/how-to/authorized-networks ">https://cloud.google.com/kubernetes-engine/docs/how-to/authorized-networks </a><a href="#fnref-4626-6" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-7" role="doc-endnote"> <a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/installation/#multi-tenant ">https://argo-cd.readthedocs.io/en/stable/operator-manual/installation/#multi-tenant </a><a href="#fnref-4626-7" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-8" role="doc-endnote"> <a href="https://argocd.readthedocs.io/en/stable/user-guide/best_practices/ ">https://argocd.readthedocs.io/en/stable/user-guide/best_practices/ </a><a href="#fnref-4626-8" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-9" role="doc-endnote"> <a href="https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#repository_dispatch ">https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#repository_dispatch </a><a href="#fnref-4626-9" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-10" role="doc-endnote"> <a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/declarative-setup/ ">https://argo-cd.readthedocs.io/en/stable/operator-manual/declarative-setup/ </a><a href="#fnref-4626-10" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-11" role="doc-endnote"> <a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/installation/#kustomize ">https://argo-cd.readthedocs.io/en/stable/operator-manual/installation/#kustomize </a><a href="#fnref-4626-11" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-12" role="doc-endnote"> <a href="https://docs.github.com/en/organizations/organizing-members-into-teams/about-teams ">https://docs.github.com/en/organizations/organizing-members-into-teams/about-teams </a><a href="#fnref-4626-12" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-13" role="doc-endnote"> <a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/ ">https://argo-cd.readthedocs.io/en/stable/operator-manual/rbac/ </a><a href="#fnref-4626-13" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-14" role="doc-endnote"> <a href="https://github.com/external-secrets/external-secrets ">https://github.com/external-secrets/external-secrets </a><a href="#fnref-4626-14" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-15" role="doc-endnote"> <a href="https://cloud.google.com/secret-manager ">https://cloud.google.com/secret-manager </a><a href="#fnref-4626-15" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-16" role="doc-endnote"> <a href="https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity?hl=ja ">https://cloud.google.com/kubernetes-engine/docs/concepts/workload-identity?hl=ja </a><a href="#fnref-4626-16" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-17" role="doc-endnote"> <a href="https://cloud.google.com/iam/docs/best-practices-service-accounts?hl=ja#using_service_accounts ">https://cloud.google.com/iam/docs/best-practices-service-accounts?hl=ja#using_service_accounts </a><a href="#fnref-4626-17" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-18" role="doc-endnote"> <a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/services/slack/ ">https://argo-cd.readthedocs.io/en/stable/operator-manual/notifications/services/slack/ </a><a href="#fnref-4626-18" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4626-19" role="doc-endnote"> <a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/upgrading/2.2-2.3/ ">https://argo-cd.readthedocs.io/en/stable/operator-manual/upgrading/2.2-2.3/ </a><a href="#fnref-4626-19" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> </ol> </div> caddi-tech 第6回: Renovateによる依存関係の更新 hatenablog://entry/6801883189087242214 2023-10-16T09:00:53+09:00 2024-03-01T09:51:41+09:00 ※本記事は、技術評論社「Software Design」(2023年9月号)に寄稿した連載記事「Google Cloudで実践するSREプラクティス」からの転載です。発行元からの許可を得て掲載しております。 はじめに 前回はTerraformとGitHub Actionsで実践するインフラCI/CDについて解説しました。 今回はRenovate1を利用した、ツールやライブラリの依存関係更新について紹介します(図1)。 なぜ依存関係を更新する必要がある必要があるかという背景から、Renovateのしくみの解説と利用方法、更新の運用を手軽に行うためにキャディで取り組んでいることを紹介します。 ▼図… <p>※本記事は、技術評論社<a class="wp-editor-md-post-content-link" href="https://gihyo.jp/magazine/SD/archive/2023/202309">「Software Design」(2023年9月号)</a>に寄稿した連載記事「Google Cloudで実践するSREプラクティス」からの転載です。発行元からの許可を得て掲載しております。</p> <h2 id="はじめに">はじめに</h2> <p><a class="wp-editor-md-post-content-link" href="https://caddi.tech/?p=4427&amp;preview=true">前回</a>はTerraformとGitHub Actionsで実践するインフラCI/CDについて解説しました。 今回はRenovate<sup id="fnref-4585-1"><a href="#fn-4585-1" class="footnote-ref" role="doc-noteref">1</a></sup>を利用した、ツールやライブラリの依存関係更新について紹介します(図1)。 なぜ依存関係を更新する必要がある必要があるかという背景から、Renovateのしくみの解説と利用方法、更新の運用を手軽に行うためにキャディで取り組んでいることを紹介します。</p> <p>▼図1 CADDiスタックにおける今回の位置付け <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173607.png" alt="" /> <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173602.png" alt="" /></p> <h2 id="なぜ依存関係を更新するのか">なぜ依存関係を更新するのか</h2> <p>現代のアプリケーション開発において、私たちエンジニアはさまざまなツールやライブラリの利用を通して、先人の知恵を借り、効率的な開発を進めています。また、前回までで紹介したTerraformやGitHub ActionsなどのインフラCI/CDの領域でも、なんらかの再利用のしくみを活用することで効率化しています。</p> <p>しかし、ツールやライブラリは絶えずアップデートされています。機能の追加やバグの修正、脆弱性への対策など、その理由はさまざまです。その中でも筆者らが依存関係の更新を重視する理由は、セキュリティと対応コストの2点です。</p> <p>セキュリティ観点では、ツールやライブラリの脆弱性やバグ修正の更新をいち早く検知・対応することが欠かせません。キャディでは、自社事業の基幹システムをフルクラウドで構築・運用しており、これらの放置は安定した価値提供を損ねることにつながるからです。</p> <p>対応コスト観点では、頻繁な対応によって、バージョン間の差分が小さいうちに更新できることを重視しています。そのため、1回あたりの更新対応のコストを下げることが可能です。また、CHANGELOGにも常に目を通すことになるため、副次的に情報のキャッチアップにもつながります。</p> <h2 id="なぜRenovateを使うか">なぜRenovateを使うか</h2> <p>依存関係の更新をサポートしてくれる主要なツールとしては、RenovateのほかにGitHubで標準提供されているDependabot<sup id="fnref-4585-2"><a href="#fn-4585-2" class="footnote-ref" role="doc-noteref">2</a></sup>があります。 キャディでは、2020年にRenovateを採用するまでは、Dependabotを一部で利用している程度でした。 本連載で紹介しているように、筆者の所属するPlatformグループでは、TerraformやGitHubActions を用いた IaC や CI/CDの高度化に取り組んでいます。これらにより依存するものが増えているため、前節のとおり依存関係の更新は必要です。一方で、事業の拡大を支えるための、本質的な価値提供にも集中する必要があります。 このような背景に対するトイル削減の一環で、高いカスタマイズ性を持つRenovateに魅力を感じ、利用を拡大しました。とくに、のちほど紹介するauto merge、Pull request(PR)のグループ化、正規表現を利用しながら更新ルールをカスタマイズできる点が効果的だったととらえています。 また、Dependabotは利用をやめているわけではありません。一部の開発チームではセキュリティアラートを活用するなどして、Renovateと共存しています。 Renovateでもセキュリティアラートを通知する設定はありますが、それぞれのツールでも得手不得手もあるため、開発者が最もメンテナンスしやすい方法を選択していく必要があると考えています。</p> <h2 id="Renovateのしくみ">Renovateのしくみ</h2> <p>ここからは、Renovateのしくみと設定方法について簡単に解説します(図2)。さらに理解を深めたい方は公式ドキュメント<sup id="fnref-4585-3"><a href="#fn-4585-3" class="footnote-ref" role="doc-noteref">3</a></sup>を参照してください。</p> <p>Renovateは依存関係を一元管理し、新しいバージョンがリリースされたときに自動的にファイルを更新します。そしてGitHubやGitLabなどのサポートされているプラットフォームにて、RenovateによってPRやMerge requestが作成されます。 まず、Renovateは依存関係の現状を把握するために各バージョンを確認します。JavaScriptの場合は package.json、Terraform の場合はterraform blockのprovider定義など、各言語やツールに応じたファイルから取得します。 次に、そのバージョンが最新であるかどうかをRenovateのルールに従って判定し、最新でない場合はバージョンを更新するPRを作成します。</p> <p>▼図2 Renovateのしくみ <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173607.png" alt="" /></p> <h2 id="Renovateの設定">Renovateの設定</h2> <p>Renovateの挙動を理解するために欠かせない概念として、設定ファイルとマネージャーがあります。</p> <h4 id="設定ファイル">設定ファイル</h4> <p>Renovateは設定ファイルや環境変数によって挙動をカスタマイズできます。どんな依存関係にあるものをどんな頻度で更新するか、レビュアーを指定するか、PRのラベルを指定するかなど、さまざまな設定が可能です。設定ファイルは、<code>renovate.json</code> <code>.github/renovate.json</code> <code>.renovaterc</code>として配置できたり、コメントが記載できるようにも拡張されたjson5形式<sup id="fnref-4585-4"><a href="#fn-4585-4" class="footnote-ref" role="doc-noteref">4</a></sup>でも記述できます。 リスト1の設定例をもとに、簡単に紹介します。より詳細を理解したい方はドキュメント<sup id="fnref-4585-5"><a href="#fn-4585-5" class="footnote-ref" role="doc-noteref">5</a></sup>を参照ください。</p> <p>▼リスト1 renovate.json</p> <pre><code class="language-json line-numbers">{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", // ① "extends": [ // ② "config:base", // ⑤ ":label(renovate)", // ⑥ ":timezone(Asia/Tokyo)", // ⑦ ], "schedule": ["after 1am and before 9am every weekday"], // ③ "reviewers": ["team:reviewer-team", "kei711"], // ④ } </code></pre> <p>$schema(①)はJSON Schemaの指定です。この値により、エディタによっては設定名が補完されるようになります。extends(②)は設定値のプリセットを指定します。schedule(③)はcron形式で実行スケジュールを指定します。リスト1の例では、平日の午前1時から午前9時の間に実行されます。reviewers(④)はレビュアーを指定します。GitHubやGitLabなどの挙動に合わせて、グループや個人を指定できます。 また、リスト1の例ではRenovateで用意されているデフォルトプリセットの一部を指定しているため、こちらも紹介します。 config:base(⑤)はRenovateのデフォルト設定で、設定値はRenovate自体に組み込まれています<sup id="fnref-4585-6"><a href="#fn-4585-6" class="footnote-ref" role="doc-noteref">6</a></sup>。:label(⑥)の設定により、作成されるPRに特定のラベルを指定します。ここでは、renovateというラベルを設定します。:timezone(⑦)の設定により、scheduleで指定された実行スケジュールのタイムゾーンを指定します。なお、デフォルトプリセットの詳細はドキュメント<sup id="fnref-4585-7"><a href="#fn-4585-7" class="footnote-ref" role="doc-noteref">7</a></sup>を参照してください。 このように、設定ファイルによりRenovate自体の挙動を柔軟にカスタマイズできます。</p> <h4 id="マネージャー">マネージャー</h4> <p>Renovateのマネージャーとは、各言語やツールに応じた処理が定義されたモジュールのことを指します。このマネージャーを通して、Renovateが依存関係の解析や更新をします。たとえば、JavaScriptであればnpm、Terraformであればterraform や terraform-version などのマネージャーがあります。 Renovateは初期設定でも多くのマネージャーを利用する設定となっています。詳細はドキュメント<sup id="fnref-4585-8"><a href="#fn-4585-8" class="footnote-ref" role="doc-noteref">8</a></sup>を参照してください。各マネージャーもドキュメントにて紹介されています。 また、未設定だと利用されないマネージャーもあります。たとえば、Argo CDはファイル構成が利用者に委ねられており正確な検知が難しいため、リスト2のように明示が必要です。</p> <p>▼リスト2 Argo CD向け設定の抜粋</p> <pre><code class="language-json line-numbers"> ... "argocd": { "fileMatch": [ "argocd/.+\\.ya?ml$", "applications/.+\\.ya?ml$" ] }, </code></pre> <p>Argo CDは、本連載第1回(本誌2023年4月号)で紹介した、KubernetesマニフェストをGitOpsで管理するためのツールです。本連載でも以降の回で詳しく紹介する予定です。 最初から用意されているマネージャーのほかにも、正規表現を利用して振る舞いを定義できる、regex manager<sup id="fnref-4585-9"><a href="#fn-4585-9" class="footnote-ref" role="doc-noteref">9</a></sup>が存在します。 こちらの詳細はキャディで利用している実例とともに、のちほど紹介します。</p> <h2 id="Renovateの組み込み方法">Renovateの組み込み方法</h2> <p>ここからは、実際の使用方法を説明します。 大きく分けて「GitHub Appの利用」「ローカルで実行」「GitHub Actionsなどの環境で実行」の3パターンがありますが、ここでは最も手軽なGitHub Appによる方法を紹介します。 RenovateはMend社により、無償のGitHub Appとしても提供されています。ソースコードの管理にGitHubを利用している場合には、GitHubのMarketplace<sup id="fnref-4585-10"><a href="#fn-4585-10" class="footnote-ref" role="doc-noteref">10</a></sup>からGitHub Appを導入して利用することで、手軽にRenovateを利用できます。 MarketplaceからGitHub Appをインストールし、依存関係を自動更新させたいリポジトリを選択します。そうすると、Renovateの更新対象として選択したリポジトリにて、Renovateの設定ファイルであるrenovate.jsonを作成するPRが自動作成されます。設定ファイルをリポジトリに配置することで、Renovateの設定が完了し、自動で依存関係の更新が行われるようになります。 GitHub Appの各リポジトリにおけるGitHub Appの動作状況は、ポータルサイト<sup id="fnref-4585-11"><a href="#fn-4585-11" class="footnote-ref" role="doc-noteref">11</a></sup>から確認できます。GitHub Organizationを選択すると、Installed Repositoriesとして、Renovateをインストールしたリポジトリの一覧と、それぞれのインストール日や最終実行時刻が表示されます。 次に、リポジトリの行をクリックするとRecentJobsのページが表示され、リポジトリ単位の実行状況が表示されます。 さらにJobごとの行をクリックすると、Renovateが実行された際のログを、ログレベルや詳細情報の表示切り替えをしながら確認できます。もしRenovateの設定変更がうまくPRに反映されていない場合は、このログから状況を確認できます。</p> <h2 id="応用的な使い方">応用的な使い方</h2> <p>ここからは、更新の運用を手軽に行うために取り組んでいることをピックアップして紹介します。</p> <h3 id="共通設定の定義と利用">共通設定の定義と利用</h3> <p>Renovateの設定はrenovate.jsonに記述しますが、リポジトリそれぞれで定義すると管理コストが非常に高くなります。実際、キャディでは管理するリポジトリが多く、設定の共通化で管理コストを下げています。 共通設定の共有方法は複数ありますが、今回は手軽なGitHubで公開する方法を紹介します。 ほかの共有方法や、GitHubで公開する方法の詳細はドキュメント<sup id="fnref-4585-12"><a href="#fn-4585-12" class="footnote-ref" role="doc-noteref">12</a></sup>を参照してください。 まず、renovateの設定ファイルを共有するためのリポジトリを作成します。キャディでは、renovate-configという名前で作成しています。 次に、後述するプリセット名を省略した場合のため、default.jsonにrenovateの設定を記述します。また、言語や開発チームごとに共通設定を用意したい場合はpreset name.jsonというような命名をします。たとえば、go.jsonやteam-platform.json5のような形です。 このように共通設定を用意したら、利用したいリポジトリのrenovate.jsonにて、リスト3のように記述します。GITHUB_ORGはcaddijpのような組織名やkei711のようなアカウント名に書き換えてください。</p> <p>▼リスト3 共通設定の利用例</p> <pre><code class="language-json line-numbers">{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "github&gt;GITHUB_ORG/renovate-config", // ① "github&gt;GITHUB_ORG/renovate-config:go", // ② "github&gt;GITHUB_ORG/renovate-config:team-platform.json5", // ③ ] } </code></pre> <p>リスト3の設定ファイルでは、次のように共通設定を参照します。 ①リポジトリ上のdefault.jsonを利用 ②プリセット名を指定し、go.jsonを利用 ③platform.json5を利用。JSON5形式の場合はプリセット名に拡張子を含める必要がある また、Gitのタグやファイルパスも指定できます。詳細な例はドキュメント<sup id="fnref-4585-13"><a href="#fn-4585-13" class="footnote-ref" role="doc-noteref">13</a></sup>のGitHubの項目を参照してください。 なお、プライベートリポジトリでGitHub AppのRenovateを利用する場合には、renovate-configリポジトリもプライベートリポジトリにし、GitHub Appの導入も必要となる点に注意してください。</p> <h5 id="Column-Renovate-の-GitHub-Appを利用する際の注意点">[Column] Renovate の GitHub Appを利用する際の注意点</h5> <p>GitHub Appは手軽に使える反面、注意すべき点が2点あります。 1点目は、セキュリティ観点です。GitHub Appを導入するということは、アプリケーションを作成する場合のサプライチェーン攻撃の攻撃面が増えることを意味します。RenovateのGitHub Appをインストールすると、自分たちが管理するソースコードへのアクセス権を与えることになります。そのため、このアクセス権が奪われた場合や、GitHub Appに不正なコードが含まれている場合、それが自分たちのソースコードにも影響を及ぼす可能性があることを理解する必要があります。 2点目は、作業コストの観点です。全リポジトリを対象にRenovateを導入するオプションがありますが、リポジトリに導入したぶん、Renovateにより依存関係の更新PRが作成されます。作成されるPRが多過ぎるとメンテナンスを行いにくくなります。そのため、前述のように導入するリポジトリを選択することをお勧めします。また、セキュリティリスクにおいても、リスクを取れるリポジトリと取れないリポジトリもあるため、適切に選択をする必要があります。</p> <h3 id="設定ファイルの検証">設定ファイルの検証</h3> <p>Renovateには設定ファイルの構文が正常かどうかを確認するコマンドが用意されています。 RENOVATE_CONFIG_FILE=renovate.jsonnpx renovate-config-validator のように実行することで確認できます。共通設定を管理するリポジトリのCIに設定しておくと安心して編集できるでしょう。</p> <h3 id="GitHubでオートマージを利用する">GitHubでオートマージを利用する</h3> <p>Renovateのautomergeを活用する場合は、設定ファイルの変更と、GitHubやGitLabなどにて設定しているマージ条件が満たされている必要があります。</p> <p>・Allow auto-mergeが有効になっている ・ Branch Protection Rulesで設定されたマージ条件が満たされている</p> <p>たとえば、CODEOWNERSのレビューを必須にしている場合には、更新対象のファイルをCODEOWNERSから除外するか、ルールをバイパスする設定を追加する必要があります。また、Branch Protection RulesでPRのapproveを必須としている場合には、RenovateのPRを自動approveしてくれる GitHub App である「renovate-approve」を導入します。approveの数などの必要に応じて「renovate-approve2」のGitHub Appsも導入してください。 リスト4は、セマンティックバージョニングされたツールで、minorpatchの更新をオートマージする例です。設定ファイルでは、packageRulesブロックで依存関係ごとに上書きできます。matchPackageNamesでは対象を指定することで、特定の依存関係のみオートマージできます。また、必要に応じてignoreTestsでテストの実行を無視できます。</p> <p>▼リスト4 オートマージの設定例</p> <pre><code class="language-json line-numbers">{ "platformAutomerge": true, "packageRules": [ { "automerge": true, "matchUpdateTypes": ["minor", "patch"], "matchPackageNames": [ "kubernetes-sigs/kustomize", "mikefarah/yq" ], "ignoreTests": true } ] } </code></pre> <h3 id="グループ化により依存関係をまとめて更新する">グループ化により、依存関係をまとめて更新する</h3> <p>同じ用途のバージョンは、一度に更新したいことが多いかと思います。キャディでは前回までの連載で紹介しているとおり、TerraformでGoogle Cloudの設定をIaC化しています。 Google Cloudの設定をするためのTerraformproviderには、googleとgoogle-betaの2種類があります。早く技術検証したい場合にはgoogle-beta providerを利用することがあります。 筆者らはこれらのproviderをまとめて更新するためのリスト5の設定を利用しています。</p> <p>▼リスト5 PRをグループ化する設定例</p> <pre><code class="language-json line-numbers">{ "packageRules": [ { "matchManagers": ["terraform"], "matchPackageNames": ["google", "google-beta"], "groupName": "Google Terraform providers" } ] } </code></pre> <p>matchManagersとmatchPackageNamesで対象となる依存関係を指定します。そしてgroupNameを指定することで、1つのPRの中で一度にバージョンが更新されるようになります。</p> <h3 id="regexManagersによる独自の更新ルール定義">regexManagersによる独自の更新ルール定義</h3> <p>筆者らは、TerraformによるGoogle Cloudの設定処理を共通化するため、GitHub ActionsのComposite Actionを作成しています。このとき、terraform_versionを渡す必要がありますが、このバージョンの指定方法ではRenovateが更新してくれません(リスト6)。</p> <p>▼リスト6 標準では更新対象外となる独自定義</p> <pre><code class="language-yaml line-numbers">... - name: Setup Terraform and Auth Google Cloud uses: caddijp/gh-actions/terraform/setup_terraform@v0.21.0 with: workload_identity_provider: ${{ vars.GCP_WI_PROVIDER }} service_account: ${{ vars.GCP_WI_SERVICE_ACCOUNT }} working_directory: ./terraform terraform_version: 1.4.6 </code></pre> <p>そこで登場するのがregex managerです。対象ファイルと正規表現をもとに更新すべき対象を絞り込みし、依存するバージョンの公開先を指定することで一緒に更新してくれるようになります。 リスト7の設定は、キャディで実際に利用している共通定義の一部を抜粋したものです。</p> <p>▼リスト7 独自の更新ルールを指示する設定</p> <pre><code class="language-json line-numbers">{ "regexManagers": [ { "fileMatch": [ // ① "^\\.github/workflows/.*\\.ya?ml$", "^\\.circleci/config\\.ya?ml$" ], "matchStrings": [ // ② "terraform_version: +['\"]?(?&lt;currentValue&gt;[^'\" \\n]+?)['\"]?\\n" // ③ ], "depNameTemplate": "hashicorp/terraform", // ④ "datasourceTemplate": "github-releases", // ⑤ "extractVersionTemplate": "^v(?&lt;version&gt;.*)$" // ⑥ } ] } </code></pre> <p>まず、Renovateの更新対象となるファイルを①fileMatchで指定します。キャディではGitHubActionsのほか、CircleCIも利用しているので、両方を指定しています。次の②matchStringsでは正規表現を指定し、fileMatchで指定したファイルの中からマッチするものを探します。③<currentValue>がRenovate中で特殊に扱われているキャプチャグループ名です。terraform_version: 1.4.6という表記のほかにもterraform_version: '1.4.6'のような表記、terraform_version: 1.4.6 # comment のような表記のブレも吸収できるようにしています。</p> <p>次の④⑤⑥は、データソースの設定です。④depNameTemplateと⑤datasourceTemplate により、TerraformのGitHub Release<sup id="fnref-4585-14"><a href="#fn-4585-14" class="footnote-ref" role="doc-noteref">14</a></sup>の情報をもとに最新バージョンを取得します。最後の⑥extractVersionTemplateは、バージョンの表現を指定しています。Terraformのバージョンはv1.4.6のように先頭がvから始まるタグの命名ルールです。ですが、キャディではworkflow中のバージョンではvを除いているため、記述方法に合わせるように先頭のvを除外しています。ほかにも特殊なキャプチャグループ名がありますので、興味がある方はregex managerのドキュメント<sup id="fnref-4585-15"><a href="#fn-4585-15" class="footnote-ref" role="doc-noteref">15</a></sup>を参照してください。</p> <h2 id="Renovateの更新運用の工夫">Renovateの更新運用の工夫</h2> <p>Renovateが自動的にPRを作成してくれるとはいえ、リポジトリ数が増えると差分を確認しながらマージするだけでも一苦労です。依存関係の更新が形骸化しないように、Platformグループ設立から2年間試行錯誤してきました。ここからは筆者らが現在行っている運用の一部を紹介します。</p> <h3 id="依存関係更新の運用">依存関係更新の運用</h3> <p>Renovateの更新PRが溜まってしまうことを防ぎつつも、依存関係を更新していくには、習慣化するのが一番です。そこで筆者らは毎週1回30分カレンダーにRenovate用の予定を登録しました。この時間内は必ず依存関係を更新するルールにしています。 また、作業開始前にRenovateによるPRがあるリポジトリのURLをSlackに通知しています。このしくみを作ることにより、対象PRを探しに行く手間をなくすようにしました。図3のようにリポジトリ単位でリポジトリのURLを投稿されるため、それぞれにリアクションができるようになります。筆者らは作業開始するリポジトリに対して「やります」のリアクションをしながら分担して作業を進めています。</p> <p>▼図3 Slackを利用した運用 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173613.png" alt="" /></p> <h4 id="CIのみで利用するツールのpatchminorは極力automergeする">CIのみで利用するツールのpatch、minorは極力automergeする</h4> <p>CIのみで利用するテスト、Lint、静的解析に関連するツールを自動更新しても大きく壊れることがなかったため、極力automergeを利用するようにしています。 ただし、CDでも利用しているツールは自動でデプロイされると影響が大きく困るため、automergeの対象から外しています。</p> <h4 id="Renovate経由で作られたPRの通知を削減">Renovate経由で作られたPRの通知を削減</h4> <p>筆者らはSlackのGitHub Appを経由して、担当するリポジトリのPRを定期的にSlackに通知し、PRマージまでのリードタイムを短くする取り組みをしています。この通知設定にて、renovateのラベルが付いているPRを除外することで、Renovateによる通知疲れを低減させています。 Platformグループは横断組織であることから、認知負荷が高くなりがちなため、日ごろから通知を減らす努力をしています。</p> <h2 id="おわりに">おわりに</h2> <p>今回は依存関係の更新が必要な背景、Renovateの解説、キャディでの取り組みについて紹介しました。連載の流れから、TerraformとRenovateの組み合わせを中心に紹介をしてきましたが、今回紹介したものはアプリケーション開発でも同様に使えるものばかりです。みなさんの開発においても、依存関係の更新が楽になることを願っています。来月はArgo CDを利用したKubernetesのCDについて、キャディの事例をまじえながら紹介する予定です。</p> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn-4585-1" role="doc-endnote"> <a href="https://www.mend.io/renovate/ ">https://www.mend.io/renovate/ </a><a href="#fnref-4585-1" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-2" role="doc-endnote"> <a href="https://docs.github.com/ja/code-security/dependabot ">https://docs.github.com/ja/code-security/dependabot </a><a href="#fnref-4585-2" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-3" role="doc-endnote"> <a href="https://docs.renovatebot.com/ ">https://docs.renovatebot.com/ </a><a href="#fnref-4585-3" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-4" role="doc-endnote"> <a href="https://json5.org/ ">https://json5.org/ </a><a href="#fnref-4585-4" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-5" role="doc-endnote"> <a href="https://docs.renovatebot.com/configuration-options/ ">https://docs.renovatebot.com/configuration-options/ </a><a href="#fnref-4585-5" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-6" role="doc-endnote"> <a href="https://github.com/renovatebot/renovate/blob/35.141.3/lib/config/presets/internal/config.ts ">https://github.com/renovatebot/renovate/blob/35.141.3/lib/config/presets/internal/config.ts </a><a href="#fnref-4585-6" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-7" role="doc-endnote"> <a href="https://docs.renovatebot.com/presets-config/ ">https://docs.renovatebot.com/presets-config/ </a><a href="#fnref-4585-7" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-8" role="doc-endnote"> <a href="https://docs.renovatebot.com/modules/manager/ ">https://docs.renovatebot.com/modules/manager/ </a><a href="#fnref-4585-8" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-9" role="doc-endnote"> <a href="https://docs.renovatebot.com/modules/manager/regex/ ">https://docs.renovatebot.com/modules/manager/regex/ </a><a href="#fnref-4585-9" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-10" role="doc-endnote"> <a href="https://github.com/marketplace/renovate ">https://github.com/marketplace/renovate </a><a href="#fnref-4585-10" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-11" role="doc-endnote"> <a href="https://developer.mend.io/ ">https://developer.mend.io/ </a><a href="#fnref-4585-11" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-12" role="doc-endnote"> <a href="https://docs.renovatebot.com/config-presets/ ">https://docs.renovatebot.com/config-presets/ </a><a href="#fnref-4585-12" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-13" role="doc-endnote"> <a href="https://docs.renovatebot.com/config-presets/#github ">https://docs.renovatebot.com/config-presets/#github </a><a href="#fnref-4585-13" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-14" role="doc-endnote"> <a href="https://github.com/hashicorp/terraform/releases ">https://github.com/hashicorp/terraform/releases </a><a href="#fnref-4585-14" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4585-15" role="doc-endnote"> <a href="https://docs.renovatebot.com/modules/manager/regex/ ">https://docs.renovatebot.com/modules/manager/regex/ </a><a href="#fnref-4585-15" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> </ol> </div> kei711 SRE NEXT 2023 に参加しました hatenablog://entry/6801883189081010253 2023-10-12T18:00:16+09:00 2024-03-05T01:34:20+09:00 こんにちは。DRAWER SRE(Site Reliability Engineer) の廣岡です。最近は DRAWER サービスを運営する上での SLI/SLO 、エラーバジェットポリシーの策定や、モニタリングの整備などを進めています。 DRAWER SRE チームでは、リライアビリティの推進事例やプラクティスへの理解を深めるため、SRE NEXT 2023 というイベントに参加・聴講しました。本ブログはこの参加レポートになります。少しでもイベントの雰囲気を感じていただけると幸いです。 SRE NEXT とは SRE NEXT は、SRE などのサービスの信頼性構築やその維持・改善に関心を持… <p>こんにちは。DRAWER SRE(Site Reliability Engineer) の廣岡です。最近は DRAWER サービスを運営する上での SLI/SLO 、エラーバジェットポリシーの策定や、モニタリングの整備などを進めています。</p> <p>DRAWER SRE チームでは、リライアビリティの推進事例やプラクティスへの理解を深めるため、SRE NEXT 2023 というイベントに参加・聴講しました。本ブログはこの参加レポートになります。少しでもイベントの雰囲気を感じていただけると幸いです。</p> <h1 id="SRE-NEXT-とは">SRE NEXT とは</h1> <p>SRE NEXT は、SRE などのサービスの信頼性構築やその維持・改善に関心を持つエンジニア向けに開催されているカンファレンスです。2020年から開催されており、今回が3回目の開催とのことでした。</p> <p>スピーカーやスポンサーもさまざまな業種、規模の企業が務めており、信頼性改善に対する熱量の高さが窺えました。セッション内容は動画で公開されており、非常にありがたいです。</p> <ul> <li><a class="wp-editor-md-post-content-link" href="https://sre-next.dev/2023/">SRE NEXT 2023</a></li> <li><a class="wp-editor-md-post-content-link" href="https://www.youtube.com/@srenext6778">SRE NEXT 2023 - YouTube</a></li> </ul> <h2 id="CADDi-DRAWER-SRE-とイベント参加の経緯">CADDi DRAWER SRE とイベント参加の経緯</h2> <p>キャディが提供する図面活用 SaaS である DRAWER では、サービスの成長とともにユーザーの期待するサービスレベルを維持することの重要性が増しています。DRAWER SRE チームはこうしたサービスと事業状況に対応するため、日々信頼性改善のためのキャッチアップと社内適用に取り組んでいます。</p> <p>SRE に関するプラクティスは、例えばオライリーの「Site Reliability Engineering」など、さまざまな書籍や資料を通じて公開されています。一方でそれらのプラクティスを実際に組織やサービスに適用する際には、サービスの性質やチームの規模などに応じたチューニングが必要になります。今回は実際のさまざまな企業の信頼性構築、改善事例紹介を聴くことで、より現実的かつ実践的なノウハウや障壁について理解が得られると考え、SRE NEXT に参加しました。</p> <h2 id="会場の様子">会場の様子</h2> <p>SRE NEXT 2023 はオフライン・オンラインのハイブリッド開催であり、オフライン会場は九段テラスとなっていました。スピーカーセッションは3つのトラックがそれぞれのホールで並行して開催されていました。</p> <ul> <li><a class="wp-editor-md-post-content-link" href="https://sre-next.dev/2023/schedule/">Schedule | SRE NEXT 2023</a></li> </ul> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208183256.jpg" alt="" /></p> <p>オフラインでの参加者はかなり多く、イベントの熱量の高さが窺えました。また運営の方々は丁寧かつスムーズに会場案内やイベント進行を実施してくださっており、快適に参加することができました。</p> <p>スピーカーセッションの他には、スポンサーブースやアンケートなどのイベントブースもありました。スポンサーブースでは、スポンサー企業のメンバーの方々と間近でお話することができ、各社が提供するサービスの詳細や、信頼性改善に関してより密なディスカッションができたと感じます。</p> <p>アンケートボードには、組織における SRE のタイプや、SLO の導入度合い、SRE 関連書籍の読書具合などのアンケートが掲示されており、個人的に非常に興味深く感じました。</p> <p><img src="https://assets.caddi.com/tech-blog/wp-content/uploads/2023/10/アンケートボード.jpeg" alt="" /> <a class="wp-editor-md-post-content-link" href="https://twitter.com/srenext/status/1707640080008155292">SRE NEXT 公式アカウントの投稿</a>より引用</p> <p>実は DRAWER SRE は厳密には Enabling SRE という形で DRAWER サービスのエンジニアリングに関わっています。このアンケートを通じて、SRE とサービスの関わり方を俯瞰することができました。</p> <p>SRE 関連書籍の読書具合も興味深いと感じました。代表的な書籍であるオライリーの「サイトリライアビリティエンジニアリング」や「入門 監視」はやはり広く読まれており、業界におけるバイブル的位置づけであることがわかります。次いで「サイトリライアビリティワークブック」もかなり読まれていることがわかります。「サイトリライアビリティエンジニアリング」が Google が提供する SRE の基本原則やプラクティスを掲載しているのに対して、「サイトリライアビリティワークブック」では Google 以外も含めたより実践的なプラクティスや事例が紹介されています。DRAWER SRE チームでもちょうど先日「サイトリライアビリティワークブック」の輪読会を終えたところであり、実践的な理解を深める上で非常に有意義だったと感じました。</p> <p>「セキュアで信頼性のあるシステム構築」は、私はこのイベントで初めて存在を知りました。信頼性とセキュリティの関係性を解説した本は貴重に思います。また「カオスエンジニアリング」に関しては、DRAWER で実践するにはまだ先かもしれませんが、SRE の代表的な役割の一つであるキャパシティプランニングの一貫として確かに大事だと感じます。</p> <p>総じてスピーカーセッション、スポンサーブース、イベントブースどれも発見があり、非常に楽しむことができました。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173628.jpg" alt="" /></p> <p>会場では広島のワキヤコーヒーさんがコーヒーを提供してくださっていました。とても美味しく、長いイベントでしたが集中して参加することができました!</p> <h1 id="イベントへの感想">イベントへの感想</h1> <p>個別のスピーカーセッションに対する感想は割愛しますが、オブザーバビリティやインシデント対応、SLO の浸透などといった SRE の役割について実践的な事例を聞くことができました。また Generative AI の活用例などといったリサーチレベルの内容まであり、非常に面白く聴講できたと感じます。</p> <p>イベント開始と終了時のキーノートセッションでは、経営の柱の一つとしての信頼性の重要性や、成長していくサービスにおいてどのように信頼性目標を実現していくかなどが話されていました。どちらもキーノートにふさわしく、信頼性に関わる多くの人に刺さる内容だったと感じます。</p> <h2 id="参加メンバーコメント廣岡">参加メンバーコメント(廣岡)</h2> <p>全体の感想として、参加者のレベルがとても高いと感じました。特にスピーカーセッションでは、「このプラクティスは〇〇の本に書かれてて、」と言うような話が多くあり、基本的な知識やプラクティスを抑えることの重要性を感じました。私は SRE として動き始めたのが比較的最近のため、引き続きイベントブースで紹介されていたような書籍のキャッチアップ&amp;実践を進めていこうと感じました。</p> <p>また、どのスピーカーセッションでも周囲のチームやステークホルダーとうまく連携しながら信頼性活動の取り組みを進めているように感じました。これは信頼性改善の取り組みが潜在的にサービスおよびユーザーの広い範囲に影響を与えるからであり、どの組織も開発チームやプロダクトマネージャーを巻き込むことで、効果的に信頼性改善を推進しているのだと考えています。CADDi DRAWER でもここ最近でプロダクトにおける信頼性の重要性が認識され始めており、このまま強度高く活動を進めていきたいと思います。</p> <h2 id="参加メンバーコメント矢野">参加メンバーコメント(矢野)</h2> <p>まず初めのスピーカーセッションがとても印象的でした。SREはプロダクト開発する組織にとって必要な機能であるという認識を新たにすることができて、今進もうとしている方向性の自信となりました。</p> <p>私自身はSWEとしての経験がメインで、SREとしての経験を積んでいるのはここ最近の話なので、各社のリアルなSREの事例を聞くことができてどのセッションも非常に興味深く面白かったです。2000年初期に生まれたSREというエンジニアリングのプラクティスが今まさに各社で活発に実践されていることを肌で感じることができて、CADDiのSaaSでも信頼性高く提供し続けられるようにやっていこというモチベーションが高まるとても良い一日でした。</p> <h1 id="終わりに">終わりに</h1> <p>SRE など、サービスの信頼性改善に関心のある方向けのイベントである、SRE NEXT 2023 に参加させていただきました。</p> <p>スピーカーセッションやイベントブースなど、どれもためになる情報やお話が多く、非常に参考になりました。また信頼性改善という共通の目標に向かって邁進している方々のお話を聞くことで、DRAWER SRE としても大いに今後のモチベーションに繋がるところがあり、参加して良かったと感じています。今回は聴講側の参加でしたが、次回は是非スピーカー側としてもプラクティスをお話しできるように頑張りたいと思います。</p> <p>また運営の皆様の配慮により、快適に聴講や各イベントを楽しむことができました。改めてお礼を申し上げます。</p> <p>本イベントで得られた知識や洞察をもとに、キャディでは引き続き製造業の変革に貢献するようなプロダクト開発を進めていきます。ご興味のある方は是非お気軽にご連絡いただけると幸いです。</p> <ul> <li><a class="wp-editor-md-post-content-link" href="https://togetter.com/li/2232488">SRE NEXT 2023 全体 まとめ - Togetter</a></li> <li><a class="wp-editor-md-post-content-link" href="https://www.youtube.com/@srenext6778">SRE NEXT 2023 - YouTube</a></li> <li><a class="wp-editor-md-post-content-link" href="https://open.talentio.com/r/1/c/caddi-jp-recruit/pages/83887">JP-TECH19.SRE(Site Reliability Engineer) / キャディ株式会社</a></li> <li><a class="wp-editor-md-post-content-link" href="https://recruit.caddi.tech/">CADDi Engineering</a></li> </ul> caddi-tech チームワークショップで採用面接を見直した話 hatenablog://entry/6801883189081010276 2023-10-03T17:12:18+09:00 2024-02-08T18:33:05+09:00 こんにちは。CADDi DRAWERでMLOpsチームのチームリードをしている中村遵介です。 チームリードは技術に関して多方面の意思決定を行ってチームの成果に貢献するテックリードと異なり、チームのメンバーや組織に関する意思決定を行ってチームの成長に貢献します。貢献したいです。頑張ります。 最近では、機械学習メンバー/MLOpsメンバーの採用を積極的に行っています。チームメンバーも採用に対してもっと関わっていきたい、と普段から活動してくれています。 私たちのチームでは採用に半構造化面接を用いています。どういう観点でどんな質問をするのか、を予め決めています。 しかし、メンバーの期待している人物像に… <p>こんにちは。CADDi DRAWERでMLOpsチームのチームリードをしている中村遵介です。 チームリードは技術に関して多方面の意思決定を行ってチームの成果に貢献するテッ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%EA%A1%BC%A5%C9">クリード</a>と異なり、チームのメンバーや組織に関する意思決定を行ってチームの成長に貢献します。貢献したいです。頑張ります。</p> <p>最近では、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>メンバー/MLOpsメンバーの採用を積極的に行っています。チームメンバーも採用に対してもっと関わっていきたい、と普段から活動してくれています。 私たちのチームでは採用に半構造化面接を用いています。どういう観点でどんな質問をするのか、を予め決めています。</p> <p>しかし、メンバーの期待している人物像に関して聞いてみると、この質問内容に対して人物像が少しずつ乖離し始めているのはいないか、ということが気になりました。また、チーム全体で顔を合わせて議論すると「xxな人に来てほしい」という何となくのイメージは共有されているのですが、詳細を一人一人に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D2%A5%A2%A5%EA">ヒアリ</a>ングすると微妙に想定している内容が異なることに気づきました。当然ですね。</p> <p>そこで、チームメンバーで「我々はどういう仲間と働きたいのか」を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>した後に、構造化面接の内容を見直すワークショップを開催しました。</p> <h2 id="ワークショップの準備">ワークショップの準備</h2> <h3 id="Values-Card">Values Card</h3> <p>Values Cardとは、Wevoxさんの出している自己理解とチームの相互理解を深める取り組みです(<a class="wp-editor-md-post-content-link" href="https://wevox.io/valuescard/" title="https://wevox.io/valuescard/">https://wevox.io/valuescard/</a>)。 過去に部署の相互理解目的で利用したことがあり非常に良い体験だったため取り入れることにしました。 ただし、今回は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C1%A1%BC%A5%DF%A5%F3%A5%B0">チーミング</a>目的ではなく「どんな新しい仲間に来てほしいか?」という価値観を共有し<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>し合うために使用したいと思いました。 よってカードの内容はより私たちの目的に限定したものにするために自作することにしました。</p> <h3 id="カードの生成">カードの生成</h3> <p>まずはバリューが記載された多様なカードを用意する必要があります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>/MLOpsの新しい仲間に望む要素を1つ1つ思い浮かべて大量に用意する...なかなかすぐに出来ることではありません。自分だけでやると偏りも生じます。 そうです。ChatGPTです。これなら100点の答えを出すことは難しいですが、60点の答えを一瞬で大量に用意することができます。 以下がChatGPTに送ったプロンプトです。</p> <pre><code class="language-text line-numbers">あなたはエンジニアの採用の最高責任者をやっています。 いま、あなたはスタートアップの機械学習/MLOpsエンジニアを採用しようとしています。そこで、今のチームにはどんな人がマッチするのかを調べるために、下記のワークを開催することにしました。 * カードが大量にあり、それぞれに「高度なエンジニアリングスキルを持っている」「他のチームメンバーへの質問を躊躇わない」(*注: 実際にここに書いた例は異なります)など、エンジニアとしてのスキルや指向といった採用観点での様々な要素が1枚につき1つ書かれている * プレーヤーは最初に5枚のカードが伏せた状態で配られる * プレーヤーは自分のターンになると山札もしくは川から1枚カードを引く * プレーヤーは5枚のカードと、引いた1枚のカードのうち、新しい仲間に求めるものとして大事だと思う要素を5つ手元に残し、1枚を川に捨てる * プレーヤーはターンを終了し、次の人がターンを開始する * 山札がなくなるまでこれを繰り返す これにより、メンバーがどういう仲間を探しているのかをシャープに掴もうと考えています。山札がN(*Nは十分大きな数)枚ほど必要なので、カードの中身を考えてみてください </code></pre> <p>これに対して、ChatGPTは「ユニークで面白い」と言った上でN個の要素を出してくれました。いい時代です。 しかし、いまいちピンと来ない内容も入っています。そのまま使用するには粗すぎる印象です。</p> <h3 id="カードの精製">カードの精製</h3> <p>LLMに限らず、AIで100点の納得感を出せる回答を用意するには、やはり最後にはエキスパートによる修正を加える必要があります。 そこで、HR(Human Resource)で一緒にエンジニアの採用をしている<a class="wp-editor-md-post-content-link" href="https://twitter.com/hamadiii15">はまDさん</a>にお願いして、一緒にチェックをしてもらうことにしました。</p> <pre><code class="language-text line-numbers">はまDさん「まずはそれぞれの項目を分類して整理すると良いです」 タイピングが得意なわたし「任せてください。『10個くらいにカテゴリーで分けられますか?』」 ChatGPT「もちろん」 </code></pre> <p>分類してもらった結果、「技術力」「学習・成長志向」など、確かに納得できるカテゴリーが作られました。「あ、このカテゴリーはもっと詳しく聞きたいんだよな」とか「この要素とこの要素はほとんど同じ内容だな」というのが分かりはじめます。</p> <p>さらに、はまDさんから「このカテゴリーについては、HRではさらにこういう分類をすることがあります」などプロの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>知識を教えてもらいました。それにより要素がさらに磨かれていきました。もしかするとこのタイミングで自分のバイアスが入ってしまったかもしれません。ただ、ある程度の数を用意できたのでその点についてはカバーされているだろうと思います。</p> <p>最後に「ワークショップの最後にただお互いの5つの要素を見せ合うだけでなく、それを文章にして説明することでより具体的な相互理解が深まる」というアド<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>も貰えたので、ワークショップに組み込むことにしました。</p> <h3 id="カードの準備">カードの準備</h3> <p>さて、最後は実際にカードを用意すればおしまいです。オフラインで顔を合わせて行いたかったので、100均で売ってるメッセージカードに油性ペンで書くことにしました。 一つだけポイントとして、裏面から内容が透けてしまわないように少し厚みのあるカードをお勧めします。</p> <h2 id="実際のワークショップ">実際のワークショップ</h2> <p>実際のワークショップは4人で行いました。手元に残せるのは5つだけ、になるとどうしても「うーんこの要素も...この要素も重要だと思う...どれも捨てられない...」という状態になりますが当然です。今回カードに書いた要素は全て Better to have な要素です。あった方が良いに決まっていますが、全てを兼ね備えるのはほとんど無理な話です。「5つ」という制約を加えると自分の中で深く比較することになり、本当に譲れないものだけを残せます。</p> <p>結果として4人×5枚で20の要素が残っていました。「あ、意外とこの要素はそこまで求められていないんだな」とか「やっぱりみんなこの要素は欠かせないと思っているんだね」がメンバー間でかなり具体化されたように感じます。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208183302.jpg" alt="残ったカード: それぞれについてどういう意図を持って残したのかを紹介し合う" title="最後まで残った20枚のカード" /></p> <p>最後に、それらの要素に対して既存の構造化面接の内容を見直してみると「この要素は見極められていないんじゃないか?」ということが見えてきます。新しく質問を追加することにしました。 もちろん「あなたはこの要素を大事に思いますか?」という質問をしてもあまり効果はないでしょう。大抵の要素は大事です。 みんなでホワイトボードに様々な質問を列挙していくことで、この観点を見るためにこの質問を追加しよう、というのを全員で共通認識として持つことができました。</p> <p>今後も定期的に自分たちの認識を見直していきたいと思います。</p> <h2 id="おわりに">おわりに</h2> <p>私たちと一緒に開発を推進してくださるメンバーを募集しています。興味のある方、是非お気軽にご連絡ください!</p> caddi-tech Cloud Data FusionをIaCで構築し、データパイプラインのマイグレーションを行いました hatenablog://entry/6801883189081010314 2023-09-20T11:27:56+09:00 2024-02-27T18:26:22+09:00 はじめまして。CADDiでバックエンドエンジニアとして働いている中野です。 この記事では、Cloud Data Fusionを利用して作成したデータパイプラインについてご紹介します。 TL;DR SalesforceとBigQuery間のデータ連携にHeroku Connectをこれまで利用していたのですが、Cloud Data Fusionに乗り換えることでダウンタイムなしで約1/8までコストダウンができました。 モチベーション 弊社では、Salesforceに溜まったデータをBigQueryに連携し、営業などのBizサイドの組織も含めアクセスできる状態にしております。これまでは連携に He… <p>はじめまして。CADDiでバックエンドエンジニアとして働いている中野です。</p> <p>この記事では、Cloud Data Fusionを利用して作成したデータパイプラインについてご紹介します。</p> <h2 id="TLDR">TL;DR</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>とBigQuery間のデータ連携にHeroku Connectをこれまで利用していたのですが、Cloud Data Fusionに乗り換えることでダウンタイムなしで約1/8までコストダウンができました。</p> <h2 id="モチベーション">モチベーション</h2> <p>弊社では、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>に溜まったデータをBigQueryに連携し、営業などのBizサイドの組織も含めアクセスできる状態にしております。これまでは連携に <a class="wp-editor-md-post-content-link" href="https://jp.heroku.com/connect">Heroku Connect</a>及び<a class="wp-editor-md-post-content-link" href="https://jp.heroku.com/postgres">Heroku Postgres</a>と <a class="wp-editor-md-post-content-link" href="https://www.stitchdata.com/">Stitch</a>というCloud Data Pipelineを用いていました。</p> <p>しかし、Heroku Connect及びHeroku Postgresの利用料が高額でコストダウンしたいというモチベーションがありました。</p> <p>乗り換え先として、Embulkなどの<a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a>を利用して自分たちで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DB%A5%B9%A5%C6%A5%A3%A5%F3%A5%B0">ホスティング</a>を行う方法なども検討に上がりましたが、なるべくメンテナンスコストをかけたくないことから、要件を全て満たせそう且つフルマネージドなCloud Data Fusionを使うことに決定しました。</p> <h2 id="Cloud-Data-Fusionについて">Cloud Data Fusionについて</h2> <blockquote> Cloud Data Fusion は、データ パイプラインを迅速に構築し管理するための、フルマネージドかつ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a> ネイティブな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%F3%A5%BF%A1%BC%A5%D7%A5%E9%A5%A4%A5%BA">エンタープライズ</a> データ統合サービスです。Cloud Data Fusion は、データ パイプラインを迅速に構築し管理するための、フルマネージドかつ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a> ネイティブな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%F3%A5%BF%A1%BC%A5%D7%A5%E9%A5%A4%A5%BA">エンタープライズ</a> データ統合サービスです。 </blockquote> <p>引用:<a href="https://cloud.google.com/data-fusion/docs/concepts/overview?hl=ja">https://cloud.google.com/data-fusion/docs/concepts/overview?hl=ja</a></p> <p>UIからの操作も直感的に可能で、シンプルなパイプラインであればエンジニア以外でも簡単にデプロイすることができます。</p> <h2 id="構成">構成</h2> <p>今回我々がやりたかったことは、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>にあるデータをBigQueryに連携する」ということです。それを実現するために、Cloud Data Fusionのデプロイは以下の構成で行いました。</p> <p>▽図1:システム構成図 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173555.png" alt="System Architecture Diagram 1" /></p> <p>しかし、一度デプロイした後には不要になるリソースがいくつかあります。そのためデプロイが完了し、Dataproc<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>ーをCloud Data Fusionがプロビジョニング可能な状態になった後には、定期実行のスケジュールを設定し不要なリソースを削除した上で、以下の構成で運用しています。</p> <p>Dataprocは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%C3%A5%C1%BD%E8%CD%FD">バッチ処理</a>などを行うためのマネージドサービスです。Dataprocが実際に<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>と通信してデータを取得し、BigQueryにデータを貯める役割を担っています。Dataprocの詳細は最後に参考文献として載せています。</p> <p>▽図2:リソース削除後システム構成図 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173540.png" alt="System Architecture Diagram 2" /></p> <h2 id="マイグレーションプラン"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%A4%A5%B0%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">マイグレーション</a>プラン</h2> <p>弊社では様々な部署がBigQueryに蓄積されたデータを元に業務を行っているため、できる限りダウンタイムを作らずに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%A4%A5%B0%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">マイグレーション</a>を行う必要がありました。そのため以下方針で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%A4%A5%B0%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">マイグレーション</a>を行い、ダウンタイムを発生させずに作業を完了させることができました。(前提として、BigQueryの利用者はこれまで<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>のデータが連携されていた dataset sf_heroku_connect にある各テーブルを直接参照せず、dataset sf にあるViewを経由してデータにアクセスしておりました。)</p> <ol> <li>Cloud Data Fusionのリソースを作成し、dataset <code>sf_cloud_data_fusion</code>の各テーブルに<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>から取得したデータを格納する。</p></li> <li><p>dataset <code>sf</code>のデータソースを dataset <code>sf_heroku_connect</code>の各テーブルから、 dataset <code>sf_cloud_data_fusion</code>の各テーブルに置き換える。</p></li> <li><p>しばらく稼働させ、問題が発生しないか確認する。</p></li> <li><p>dataset <code>sf_heroku_connect</code>を削除する。</p></li> </ol> <h2 id="実装詳細">実装詳細</h2> <p><p>以下リソースの定義を行いました。</p> <p>実際には、module化して管理しておりますが、ここではブログ用に基本的にresourceとして定義しています。また、BigQueryのリソースも実際には別プロジェクト内に配置してあるのですが、ここでは簡易化のために同一プロジェクト内に配置しております。</p> <p><code>FILL_YOUR_XXX</code>と記載がある箇所はご自身で適切なIPレンジに置き換えてください。</p> <h3 id="全体設定">全体設定</h3> <pre><code class="line-numbers">provider "google" { project = "sample-project" region = "asia-northeast1" zone = "asia-northeast1-c" } data "google_client_config" "current" {} provider "cdap" { host = "${module.wait_healthy.service_endpoint}/api" token = data.google_client_config.current.access_token } terraform { required_providers { google = { source = "hashicorp/google" version = "4.78.0" } google-beta = { source = "hashicorp/google-beta" version = "4.73.2" } cdap = { source = "GoogleCloudPlatform/cdap" version = "~&gt; 0.10" } } required_version = "&gt;= 1.1" } </code></pre> <h3 id="Cloud-Data-Fusion関連リソース">Cloud Data Fusion関連リソース</h3> <pre><code class="line-numbers"># Service Account resource "google_service_account" "sa_for_data_fusion" { project = "sample-project" account_id = "data-fusion-instance-sa" display_name = "For cloud data fusion" } resource "google_project_iam_member" "sa_for_data_fusion_role_bindings" { project = "sample-project" for_each = toset([ "roles/storage.admin", "roles/datafusion.runner", "roles/dataproc.worker", "roles/bigquery.jobUser", ]) role = each.key member = "serviceAccount:${google_service_account.sa_for_data_fusion.email}" } locals { data_fusion_service_account = "service-${data.google_project.data_fusion_project.number}@gcp-sa-datafusion.iam.gserviceaccount.com" } resource "google_service_account_iam_binding" "google_managed_sa_role_bindings" { service_account_id = "projects/sample-project/serviceAccounts/${google_service_account.sa_for_data_fusion.email}" role = "roles/iam.serviceAccountUser" members = [ "serviceAccount:${local.data_fusion_service_account}", ] } # Data Fusion resource "google_data_fusion_instance" "create_instance" { name = "data-fusion-instance-name" description = "data-fusion-instance-description" region = "asia-northeast1" type = "DEVELOPER" enable_stackdriver_logging = true enable_stackdriver_monitoring = true private_instance = true dataproc_service_account = google_service_account.sa_for_data_fusion.email network_config { network = "sample-private-network" ip_allocation = "FILL_YOUR_IP_RANGE_OF_DATAFUSION_INSTANCE" } version = "6.9.1" } # Source is from # https://cdfhub-asia-northeast1.storage.googleapis.com/hub/packages/plugin-salesforce/1.6.0/salesforce-plugins-1.6.0.json # https://cdfhub-asia-northeast1.storage.googleapis.com/hub/packages/plugin-salesforce/1.6.0/salesforce-plugins-1.6.0.jar resource "cdap_local_artifact" "salesforce-plugins" { name = "salesforce-plugins" version = "1.6.0" json_config_path = "path/to/file/salesforce-plugins-1.6.0.json" jar_binary_path = "path/to/file/salesforce-plugins-1.6.0.jar" depends_on = [google_data_fusion_instance.create_instance] } data "google_project" "data_fusion_project" { project_id = "sample-project" } resource "cdap_application" "sf-bq-sync-account" { name = "sf-bq-sync-account" spec = file("path/to/file/sf-bq-sync-account-cdap-data-pipeline.json") depends_on = [google_data_fusion_instance.create_instance, cdap_local_artifact.salesforce-plugins] } resource "cdap_application" "sf-bq-sync-user" { name = "sf-bq-sync-user" spec = file("path/to/file/sf-bq-sync-user-cdap-data-pipeline.json") depends_on = [google_data_fusion_instance.create_instance, cdap_local_artifact.salesforce-plugins] } # https://github.com/terraform-google-modules/terraform-google-data-fusion/tree/master/modules/wait_healthy module "wait_healthy" { source = "terraform-google-modules/data-fusion/google//modules/wait_healthy" version = "~&gt; 0.1" service_endpoint = google_data_fusion_instance.create_instance.service_endpoint access_token = data.google_client_config.current.access_token } </code></pre> <h3 id="ネットワーク関連リソース">ネットワーク関連リソース</h3> <pre><code class="line-numbers"># Gateway VM resource "google_service_account" "sa_for_gateway_vm" { project = "sample-project" account_id = "gateway-vm-instance-sa" display_name = "For cloud data fusion gateway" } resource "google_compute_instance" "sample_gateway_vm" { name = "sample-gateway-vm" machine_type = "e2-micro" zone = "asia-northeast1-b" tags = ["allow-http-for-data-fusion", "allow-https-for-data-fusion"] can_ip_forward = true boot_disk { initialize_params { image = "debian-cloud/debian-11" } } network_interface { network = google_compute_network.sample_private_network.self_link subnetwork = google_compute_subnetwork.sample_subnetwork.self_link } metadata_startup_script = "#! /bin/bash \n echo 1 &gt; /proc/sys/net/ipv4/ip_forward \n iptables -t nat -A POSTROUTING -s FILL_YOUR_IP_RANGE_OF_DATAFUSION_INSTANCE -j MASQUERADE \n echo net.ipv4.ip_forward=1 &gt; /etc/sysctl.d/11-gce-network-security.conf \n iptables-save" service_account { email = google_service_account.sa_for_gateway_vm.email scopes = ["cloud-platform"] } shielded_instance_config { enable_integrity_monitoring = true enable_vtpm = true } metadata = { block-project-ssh-keys = true } } # VPC resource "google_compute_network" "sample_private_network" { project = "sample-project" name = "sample-private-network" auto_create_subnetworks = "false" delete_default_routes_on_create = "false" routing_mode = "REGIONAL" } resource "google_compute_subnetwork" "sample_subnetwork" { project = "sample-project" region = "asia-northeast1" name = "sample-subnetwork" ip_cidr_range = "FILL_YOUR_IP_CIDR_RANGE" network = google_compute_network.sample_private_network.self_link private_ip_google_access = "true" } resource "google_compute_network_peering" "sample_peering" { name = "sample-peering" network = google_compute_network.sample_private_network.self_link peer_network = "https://www.googleapis.com/compute/v1/projects/${google_data_fusion_instance.create_instance.tenant_project_id}/global/networks/${google_data_fusion_instance.create_instance.region}-${google_data_fusion_instance.create_instance.name}" export_custom_routes = true } # NAT resource "google_compute_router" "router" { name = "sample-router" project = "sample-project" region = "asia-northeast1" network = google_compute_network.sample_private_network.self_link bgp { advertise_mode = "CUSTOM" advertised_groups = ["ALL_SUBNETS"] asn = "64512" } } resource "google_compute_address" "address" { name = "nat-ip" project = "sample-project" region = google_compute_router.router.region } resource "google_compute_router_nat" "cluster_router_nat" { name = "sample-router-nat" project = "sample-project" region = google_compute_router.router.region router = google_compute_router.router.name nat_ip_allocate_option = "MANUAL_ONLY" nat_ips = [google_compute_address.address.self_link] source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES" log_config { enable = true filter = "ERRORS_ONLY" } } # Firewall rule resource "google_compute_firewall" "gateway_vm_for_data_fusion_allow_http_fw" { project = "sample-project" name = "gateway-vm-for-data-fusion-allow-http" network = "sample-private-network" allow { ports = ["80"] protocol = "tcp" } direction = "INGRESS" disabled = "false" priority = "1000" source_ranges = ["FILL_YOUR_IP_RANGE_OF_DATAFUSION_INSTANCE"] target_tags = ["allow-http-for-data-fusion"] } resource "google_compute_firewall" "gateway_vm_for_data_fusion_allow_https_fw" { project = "sample-project" name = "gateway-vm-for-data-fusion-allow-https" network = "sample-private-network" allow { ports = ["443"] protocol = "tcp" } direction = "INGRESS" disabled = "false" priority = "1000" source_ranges = ["FILL_YOUR_IP_RANGE_OF_DATAFUSION_INSTANCE"] target_tags = ["allow-https-for-data-fusion"] } # Route resource "google_compute_route" "sf_bq_sync_route" { name = "sample-route" dest_range = "0.0.0.0/0" network = google_compute_network.sample_private_network.self_link next_hop_instance = google_compute_instance.sample_gateway_vm.self_link priority = 1001 } </code></pre> <h3 id="Secret-Manager関連リソース">Secret Manager関連リソース</h3> <p>FILL_YOUR_CIPHERTEXT と記載がある箇所は <a class="wp-editor-md-post-content-link" href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/kms_secret" title="google_kms_secret">google_kms_secret</a> に従って、Cloud <a class="keyword" href="https://d.hatena.ne.jp/keyword/SDK">SDK</a>を用いて暗号化したsecretを入れます。</p> <p><a class="wp-editor-md-post-content-link" href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/kms_secret#example-usage" title="google_kms_secretの例">google_kms_secretの例</a> だと、<code>my-secret-password</code>にpasswordなどのsecretを入れ、outputとして出てきた<code>CiQAaCd+xX4SsOXziF10a8JYq4spf~~~</code>を<code>FILL_YOUR_CIPHERTEXT</code>に登録します。</p> <pre><code class="line-numbers">$ echo -n my-secret-password | gcloud kms encrypt \ &gt; --project my-project \ &gt; --location us-central1 \ &gt; --keyring my-key-ring \ &gt; --key my-crypto-key \ &gt; --plaintext-file - \ &gt; --ciphertext-file - \ &gt; | base64 CiQAqD+xX4SXOSziF4a8JYvq4spfAuWhhYSNul33H85HnVtNQW4SOgDu2UZ46dQCRFl5MF6ekabviN8xq+F+2035ZJ85B+xTYXqNf4mZs0RJitnWWuXlYQh6axnnJYu3kDU= </code></pre> <p>(引用:<a class="wp-editor-md-post-content-link" href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/data-sources/kms_secret#example-usage" title="google_kms_secret">google_kms_secret</a> )</p> <pre><code class="line-numbers"># secret manager resource "google_secret_manager_secret" "salesforce_username" { project = "sample-project" secret_id = "salesforce-consumer-secret" replication { automatic = true } } resource "google_secret_manager_secret" "salesforce_password" { project = "sample-project" secret_id = "salesforce-consumer-key" replication { automatic = true } } resource "google_secret_manager_secret" "salesforce_consumer_secret" { project = "sample-project" secret_id = "salesforce-consumer-secret" replication { automatic = true } } resource "google_secret_manager_secret" "salesforce_consumer_key" { project = "sample-project" secret_id = "salesforce-consumer-key" replication { automatic = true } } data "google_secret_manager_secret_version" "salesforce_username" { project = "sample-project" secret = google_secret_manager_secret.salesforce_username.id } data "google_secret_manager_secret_version" "salesforce_password" { project = "sample-project" secret = google_secret_manager_secret.salesforce_password.id } data "google_secret_manager_secret_version" "salesforce_consumer_secret" { project = "sample-project" secret = google_secret_manager_secret.salesforce_consumer_secret.id } data "google_secret_manager_secret_version" "salesforce_consumer_key" { project = "sample-project" secret = google_secret_manager_secret.salesforce_consumer_key.id } data "google_kms_secret" "salesforce_username" { crypto_key = var.crypto_key ciphertext = var.salesforce_username } data "google_kms_secret" "salesforce_password" { crypto_key = var.crypto_key ciphertext = var.salesforce_password } data "google_kms_secret" "salesforce_consumer_secret" { crypto_key = var.crypto_key ciphertext = var.salesforce_consumer_secret } data "google_kms_secret" "salesforce_consumer_key" { crypto_key = var.crypto_key ciphertext = var.salesforce_consumer_key } variable "crypto_key" { type = string default = "sample-project/global/sample/terraform" } variable "salesforce_password" { type = string sensitive = true default = "FILL_YOUR_CIPHERTEXT" } variable "salesforce_username" { type = string sensitive = true default = "FILL_YOUR_CIPHERTEXT" } variable "salesforce_consumer_key" { type = string sensitive = true default = "FILL_YOUR_CIPHERTEXT" } variable "salesforce_consumer_secret" { type = string sensitive = true default = "FILL_YOUR_CIPHERTEXT" } </code></pre> <h3 id="BigQuery関連リソース">BigQuery関連リソース</h3> <pre><code class="line-numbers"># BigQuery resource "google_bigquery_dataset" "sf_cloud_data_fusion" { project = "sample-project" dataset_id = "sf_cloud_data_fusion" location = "asia-northeast1" } resource "google_bigquery_dataset_iam_member" "sf_cloud_data_fusion_owner" { project = "sample-project" dataset_id = google_bigquery_dataset.sf_cloud_data_fusion.dataset_id role = "roles/bigquery.dataOwner" member = "user:john_doe@caddi.jp" } resource "google_bigquery_dataset_iam_member" "data_fusion_editor" { project = "sample-project" dataset_id = google_bigquery_dataset.sf_cloud_data_fusion.dataset_id role = "roles/bigquery.dataEditor" member = "serviceAccount:${google_service_account.sa_for_data_fusion.email}" } </code></pre> <h2 id="説明">説明</h2> <p>いくつかCloud Data Fusionを定義する上でのポイントをかいつまんで説明します。</p> <h3 id="Service-Account">Service Account</h3> <p>図2をみるとわかる通り、Pipelineの実行時にはCloud Data Fusionは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>に接続しておらず、Dataprocが<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>に接続して必要なデータの取得を行なっています。</p> <p>Cloud Data Fusionのリソース定義を行う際にDataprocで利用するService Accountは宣言することができるのですが、Cloud Data Fusion自体が利用するService Accountは宣言することができません。</p> <pre><code class="line-numbers">resource "google_data_fusion_instance" "create_instance" { name = "data-fusion-instance-name" description = "data-fusion-instance-description" region = "asia-northeast1" type = "DEVELOPER" enable_stackdriver_logging = true enable_stackdriver_monitoring = true private_instance = true dataproc_service_account = google_service_account.sa_for_data_fusion.email network_config { network = "sample-private-network" ip_allocation = "FILL_YOUR_IP_RANGE_OF_DATAFUSION_INSTANCE" } version = "6.9.1" } </code></pre> <p>Cloud Data Fusion自体が利用するService AccountはCloud Data Fusion <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を有効化した際に作成される、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Managed Service Accountになるので、Cloud Data Fusion自体が行う操作に対して追加で権限を付与する必要がある場合には、この<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Managed Service Accountに対して権限を付与してやる必要があります。(<a class="wp-editor-md-post-content-link" href="https://cloud.google.com/data-fusion/docs/concepts/service-accounts?hl=ja#cloud-data-fusion-service-account" title="参考:Cloud Data Fusion でのサービス アカウント">参考:Cloud Data Fusion でのサービス アカウント</a> )</p> <p>例えば、Pipeline作成時に別プロジェクトにあるBigQueryテーブルを確認しに行くためには、自身で定義したSerivce Accountではなく、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Managed Service Accountに対して必要なロールを付与する必要があります。</p> <h3 id="プライベートインスタンスからインターネット上のリソースへの接続">プライベート<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>からインターネット上のリソースへの接続</h3> <p>Cloud Data Fusionの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を作成した後に、パイプラインの作成が行われるのですが、その際に<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>(インターネット上に存在するデータソース)に接続し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>上の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>情報を取得する必要があります。<a class="wp-editor-md-post-content-link" href="https://cloud.google.com/data-fusion/docs/how-to/connect-to-public-source?hl=ja" title="プライベートインスタンスからパブリックソースへの接続">プライベートインスタンスからパブリックソースへの接続</a> のドキュメントを読むと、プライベート<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>からインターネット上に存在するデータソースに接続するためには、Network Peeringを設定し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Gateway">Gateway</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/VM">VM</a>や<a class="keyword" href="https://d.hatena.ne.jp/keyword/Firewall">Firewall</a> Ruleなども設定し、Cloud Data Fusion<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が外部に接続することができる状態を作る必要があることがわかります。</p> <p>しかし、一度パイプラインを作成した後、Dataprocのプロビジョニングを行う際には既にCloud Data Fusion<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>情報など必要な情報を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CA%DD%CD%AD">保有</a>しているため、再度インターネット上に存在するデータソースに接続する必要がありません。そのためパイプラインの編集を頻繁には行わない場合などには、パイプラインのデプロイ後、Network Peering, <a class="keyword" href="https://d.hatena.ne.jp/keyword/Gateway">Gateway</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/VM">VM</a>, <a class="keyword" href="https://d.hatena.ne.jp/keyword/Firewall">Firewall</a> Rule, Routeなど、インターネット上に存在するデータソースにCloud Data Fusionプライベート<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が接続するために必要なリソースは削除することが可能です。</p> <p>ただしこれらのリソースの削除にはメリットデメリットが存在するので、用途に応じて削除するかどうかの判断が必要です。</p> <h4 id="メリット">メリット</h4> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/VPC">VPC</a>構成の複雑さを抑えて、ネットワークに問題が生じた際の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>が容易になる。</li> <li>リソース削除により定常コストを削減できる。</li> </ul> <h4 id="デメリット">デメリット</h4> <ul> <li>外部サービス(弊社の例では<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>)の最新<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>を取得できなくなる。取得するためには再度これらのリソースを構築し直す必要がある。</li> </ul> <p>弊社の場合、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>の更新頻度が低い且つIaCでリソースを管理しており再構築が容易に可能という状況だったため、これらのリソースを削除するという選択を行いました。</p> <h3 id="Cloud-Data-Fusion-インスタンスとパイプラインの作成タイミング">Cloud Data Fusion <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>とパイプラインの作成タイミング</h3> <p>Cloud Data Fusion<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の作成には30分ほど時間がかかります。パイプラインの作成はCloud Data Fusion<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が存在してはじめて可能になるため、Cloud Data Fusion<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の作成が完了するまでパイプライン作成は待つ必要があります。そこで、<a class="wp-editor-md-post-content-link" href="https://github.com/terraform-google-modules/terraform-google-data-fusion/tree/master/modules/wait_healthy#data-fusion-wait-healthy" title="wait_healty module">wait_healty module</a>を利用することでCloud Data Fusion<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>の作成を待ってパイプラインの作成に移ることが可能になります。</p> <h3 id="パイプラインの定義方法">パイプラインの定義方法</h3> <p>パイプラインを定義する際に <code>path/to/file/sf-bq-sync-user-cdap-data-pipeline.json</code>で参照しているファイルは、以下のような<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>ファイルを参照しています。</p> <pre><code class="line-numbers">resource "cdap_application" "sf-bq-sync-user" { name = "sf-bq-sync-user" spec = templatefile("sf-bq-sync-user-cdap-data-pipeline.json", { consumer_key = data.google_kms_secret.salesforce_consumer_key.plaintext, consumer_secret = data.google_kms_secret.salesforce_consumer_secret.plaintext, username = data.google_kms_secret.salesforce_username.plaintext, password = data.google_kms_secret.salesforce_password.plaintext }) depends_on = [google_data_fusion_instance.create_instance, cdap_local_artifact.salesforce-plugins] } sf-bq-sync-user-cdap-data-pipeline.json { "name": "sf-bq-sync-user", "description": "Data Pipeline Application", "artifact": { "name": "cdap-data-pipeline", "version": "6.9.1", "scope": "SYSTEM" }, "config": { "resources": { "memoryMB": 2048, "virtualCores": 1 }, "driverResources": { "memoryMB": 2048, "virtualCores": 1 }, "connections": [ { "from": "Salesforce", "to": "BigQuery" } ], "comments": [], "postActions": [], "properties": {}, "processTimingEnabled": true, "stageLoggingEnabled": false, "stages": [ { "name": "Salesforce", "plugin": { "name": "Salesforce", "type": "batchsource", "label": "Salesforce", "artifact": { "name": "salesforce-plugins", "version": "1.6.0", "scope": "USER" }, "properties": { "referenceName": "user", "useConnection": "false", "username": "${username}", "password": "${password}", "consumerKey": "${consumer_key}, "consumerSecret": "${consumer_secret}", "loginUrl": "https://login.salesforce.com/services/oauth2/token", "connectTimeout": "30000", "query": "select\nlastname,\nid,\nname,\ndivision\nfrom user", "operation": "query", "enablePKChunk": "false", "schema": "{\"name\":\"etlSchemaBody\",\"type\":\"record\",\"fields\":[{\"name\":\"lastname\",\"type\":[\"string\",\"null\"]},{\"name\":\"id\",\"type\":[\"string\",\"null\"]},{\"name\":\"name\",\"type\":[\"string\",\"null\"]},{\"name\":\"division\",\"type\":[\"string\",\"null\"]}]}" } }, "outputSchema": "{\"name\":\"etlSchemaBody\",\"type\":\"record\",\"fields\":[{\"name\":\"lastname\",\"type\":[\"string\",\"null\"]},{\"name\":\"id\",\"type\":[\"string\",\"null\"]},{\"name\":\"name\",\"type\":[\"string\",\"null\"]},{\"name\":\"division\",\"type\":[\"string\",\"null\"]}]}", "id": "Salesforce" }, { "name": "BigQuery", "plugin": { "name": "BigQueryTable", "type": "batchsink", "label": "BigQuery", "artifact": { "name": "google-cloud", "version": "0.22.1", "scope": "SYSTEM" }, "properties": { "useConnection": "false", "project": "sample-project", "datasetProject": "sample-project", "serviceAccountType": "filePath", "serviceFilePath": "auto-detect", "dataset": "sf_cloud_data_fusion", "table": "user", "operation": "upsert", "relationTableKey": "id", "allowSchemaRelaxation": "false", "location": "asia-northeast1", "createPartitionedTable": "false", "partitioningType": "NONE", "schema": "{\"name\":\"etlSchemaBody\",\"type\":\"record\",\"fields\":[{\"name\":\"lastname\",\"type\":[\"string\",\"null\"]},{\"name\":\"id\",\"type\":[\"string\",\"null\"]},{\"name\":\"name\",\"type\":[\"string\",\"null\"]},{\"name\":\"division\",\"type\":[\"string\",\"null\"]}]}" } }, "outputSchema": "{\"name\":\"etlSchemaBody\",\"type\":\"record\",\"fields\":[{\"name\":\"lastname\",\"type\":[\"string\",\"null\"]},{\"name\":\"id\",\"type\":[\"string\",\"null\"]},{\"name\":\"name\",\"type\":[\"string\",\"null\"]},{\"name\":\"division\",\"type\":[\"string\",\"null\"]}]}", "inputSchema": [ { "name": "Salesforce", "schema": "{\"name\":\"etlSchemaBody\",\"type\":\"record\",\"fields\":[{\"name\":\"lastname\",\"type\":[\"string\",\"null\"]},{\"name\":\"id\",\"type\":[\"string\",\"null\"]},{\"name\":\"name\",\"type\":[\"string\",\"null\"]},{\"name\":\"division\",\"type\":[\"string\",\"null\"]}]}" } ], "id": "BigQuery" } ], "schedule": "0 */2 * * *", "engine": "spark", "numOfRecordsPreview": 100, "rangeRecordsPreview": { "min": 1, "max": "5000" }, "description": "Data Pipeline Application", "maxConcurrentRuns": 1 }, "version": "de67b401-29e2-11ee-9d6b-7ad3ba276e43" } </code></pre> <p>この<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>ファイルを1から手で書くのは骨が折れますが、Cloud Data FusionではUIから定義したパイプラインの設定をパイプラインのページからExportし、利用することが可能です。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173547.png" alt="Pipe line" /></p> <p>そのため、1番最初はUIからパイプラインの定義を行い、exportした<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>ファイルを雛形として利用し、必要に応じて編集しながら使うのが効率的かと思います。その際に、secretの扱いを気をつける必要があります。</p> <p>設定をexportすると、<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>ファイルの中に以下passwordやconsumerSecretなどの情報が直接入ってきます。これらを<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>などにPushしてしまうとまずいため、templatefile function を利用して、Secret Managerなどから取得したsecretに置き換えてやる必要があります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>ファイル内で <code>"password": "${password}",</code>と書いて変数を埋め込み、パイプラインの定義を行う際に以下のように<a class="wp-editor-md-post-content-link" href="https://developer.hashicorp.com/terraform/language/functions/templatefile" title="templatefile function">templatefile function</a>を利用してsecretに置き換えます。</p> <pre><code class="line-numbers">resource "cdap_application" "sf-bq-sync-user" { name = "sf-bq-sync-user" spec = templatefile("sf-bq-sync-user-cdap-data-pipeline.json", { consumer_key = data.google_kms_secret.salesforce_consumer_key.plaintext, consumer_secret = data.google_kms_secret.salesforce_consumer_secret.plaintext, username = data.google_kms_secret.salesforce_username.plaintext, password = data.google_kms_secret.salesforce_password.plaintext }) depends_on = [google_data_fusion_instance.create_instance, cdap_local_artifact.salesforce-plugins] } </code></pre> <h3 id="スキーマの更新"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>の更新</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AD%A1%BC%A5%DE">スキーマ</a>の更新の際には<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>ファイルを編集する必要があります。変更内容が多い場合でも、置換をうまく使えば作業自体はそこまで大変ではないので、Heroku ConnectでUIから管理していた時よりも個人的には作業が楽になったように感じます。また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>ファイルもGit管理下に置かれるので、変更前後のDiffが見られる安心感もメリットに感じています。</p> <h3 id="Salesforce側の設定"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>側の設定</h3> <p>Cloud Data Fusionを利用して<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>のデータを取得するためには、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>側の設定も必要になります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>の設定はClassmethodさんの記事「<a class="wp-editor-md-post-content-link" href="https://dev.classmethod.jp/articles/cloud-data-fusion_salesforce/" title="Cloud Data FusionでSalesforceのデータをBigQueryに取り込んでみる">Cloud Data FusionでSalesforceのデータをBigQueryに取り込んでみる</a>」を参考にさせていただきました。</p> <h2 id="困っている点">困っている点</h2> <h3 id="パイプラインの定期実行スケジュールのトリガー方法">パイプラインの定期実行スケジュールのトリガー方法</h3> <p>パイプラインのデプロイまではIaCで自動化することができたのですが、パイプラインの定期実行スケジュールをデプロイと同時に開始することができず、スケジュールの開始だけはUIから操作する必要があります。UIから定期実行を開始したのちに、<a class="keyword" href="https://d.hatena.ne.jp/keyword/google">google</a>_data_fusion_instance に対してterraform import&amp;terramform plan を実行しても差分が出ず、また、pipelineを作成しているcdap_applicationは terraform importをサポートしておらず、定期実行のスケジュールを開始する方法は見つけられておりません。</p> <h2 id="おわりに">おわりに</h2> <p>お決まりですが採用についてです。リアルな世界に向き合い複雑な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>を取り扱うことに興味がある方、検証を回しつつ、スケールするための基盤作りに興味がある方を募集しています。カジュアル面談もやっていますのでぜひお気軽にご連絡ください。</p> <p>エンジニア向け採用サイト <a class="wp-editor-md-post-content-link" href="https://recruit.caddi.tech/" title="https://recruit.caddi.tech/">https://recruit.caddi.tech/</a> 求人一覧 <a class="wp-editor-md-post-content-link" href="https://open.talentio.com/r/1/c/caddi-jp-recruit/homes/4139" title="https://open.talentio.com/r/1/c/caddi-jp-recruit/homes/4139">https://open.talentio.com/r/1/c/caddi-jp-recruit/homes/4139</a></p> <p>参考文献</p> <p><a class="wp-editor-md-post-content-link" href="https://cloud.google.com/data-fusion/docs/concepts/overview?hl=ja" title="Cloud Data Fusion の概要">Cloud Data Fusion の概要</a></p> <p><a class="wp-editor-md-post-content-link" href="https://cloud.google.com/data-fusion/docs/concepts/architecture?hl=ja" title="アーキテクチャとコンポーネント">アーキテクチャとコンポーネント</a></p> <p><a class="wp-editor-md-post-content-link" href="https://cloud.google.com/data-fusion/docs/how-to/create-instance?hl=ja" title="Cloud Data Fusion インスタンスを作成する">Cloud Data Fusion インスタンスを作成する</a></p> <p><a class="wp-editor-md-post-content-link" href="https://cloud.google.com/data-fusion/docs/how-to/create-private-ip?hl=ja" title="プライベート インスタンスを作成する">プライベート インスタンスを作成する</a></p> <p><a class="wp-editor-md-post-content-link" href="https://cloud.google.com/data-fusion/docs/how-to/connect-to-public-source?hl=ja" title="プライベート インスタンスからパブリック ソースへの接続">プライベート インスタンスからパブリック ソースへの接続</a></p> <p><a class="wp-editor-md-post-content-link" href="https://cloud.google.com/data-fusion/docs/concepts/service-accounts?hl=ja" title="Cloud Data Fusion サービス アカウント">Cloud Data Fusion サービス アカウント</a></p> <p><a class="wp-editor-md-post-content-link" href="https://cloud.google.com/dataproc/docs/concepts/overview?hl=ja" title="Dataproc とは">Dataproc とは</a></p> <p><a class="wp-editor-md-post-content-link" href="https://cloud.google.com/secret-manager/docs/overview?hl=ja" title="Secret Manager のコンセプトの概要">Secret Manager のコンセプトの概要</a></p> <p><a class="wp-editor-md-post-content-link" href="https://github.com/terraform-google-modules/terraform-google-data-fusion/tree/master/modules/wait_healthy#data-fusion-wait-healthy" title="Data Fusion Wait Healthy">Data Fusion Wait Healthy</a></p> <p><a class="wp-editor-md-post-content-link" href="https://dev.classmethod.jp/articles/cloud-data-fusion_salesforce/" title="Cloud Data FusionでSalesforceのデータをBigQueryに取り込んでみる">Cloud Data FusionでSalesforceのデータをBigQueryに取り込んでみる</a></p> caddi-tech 第5回:TerraformとGitHub Actionsで構築するインフラCD hatenablog://entry/6801883189081010356 2023-09-12T18:00:22+09:00 2024-02-08T17:35:27+09:00 ※本記事は、技術評論社「Software Design」(2023年8月号)に寄稿した連載記事「Google Cloudで実践するSREプラクティス」からの転載です。発行元からの許可を得て掲載しております。 はじめに 前回はTerraformとGitHub Actionsで実践するインフラCI/CDのCI部分について解説しました。今回はその続きとなるCD部分、デプロイについて扱います。また、運用をよりスケールさせるために検討すべき観点やキャディでの事例についても紹介します。 terraform applyの実行 前回はPull request(PR)に対してterraform planを実行し… <p>※本記事は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%BB%BD%D1%C9%BE%CF%C0%BC%D2">技術評論社</a><a class="wp-editor-md-post-content-link" href="https://gihyo.jp/magazine/SD/archive/2023/202308">「Software Design」(2023年8月号)</a>に寄稿した連載記事「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudで実践するSREプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティス」からの転載です。発行元からの許可を得て掲載しております。</p> <h2 id="はじめに">はじめに</h2> <p><a class="wp-editor-md-post-content-link" href="https://caddi.tech/?p=4385&amp;preview=true">前回</a>はTerraformと<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsで実践するインフラCI/CDのCI部分について解説しました。今回はその続きとなるCD部分、デプロイについて扱います。また、運用をよりスケールさせるために検討すべき観点やキャディでの事例についても紹介します。</p> <h2 id="terraform-applyの実行">terraform applyの実行</h2> <p>前回はPull request(PR)に対して<code>terraform plan</code>を実行し、どのようなリソース変更が予定されているのかチェックしました。今回は、PRがマージされたら<code>terraform apply</code>を実行し、リソース変更が適用されるようなパイプラインを構築してみましょう。 リスト1はmainブランチへのプッシュをトリガーに<code>terraform apply</code>を実行し、apply結果をPRコメントとして投稿するワークフロー定義です。サンプルを実行するとCompute<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>が作成されるので、費用を抑えたい方はサービスアカウントなど無料で作成できる別のリソースに置き換えてください。</p> <p>▼リスト1 .<a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a>/workflows/terraform_ci.yml</p> <pre><code class="line-numbers">name: Terraform Apply on: push: # ① branches: - main jobs: terraform_apply: runs-on: ubuntu-latest permissions: contents: read id-token: write pull-requests: write steps: - uses: actions/checkout@v3 - uses: hashicorp/setup-terraform@v2 - uses: google-github-actions/auth@v1 with: workload_identity_provider: projects/(…略…)/providers/github-provider service_account: my-github-actions@(…略…).com - run: terraform init working-directory: ./terraform/environments/dev - id: apply run: terraform apply -no-color -auto-approve working-directory: ./terraform/environments/dev continue-on-error: true - uses: actions/github-script@v6 # ② with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const output = `Terraform Apply: \`${{ steps.apply.outcome }}\` &lt;details&gt;&lt;summary&gt;Show apply&lt;/summary&gt; \`\`\` ${{ steps.apply.outputs.stdout }} \`\`\` &lt;/details&gt;` const { data } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ owner: context.repo.owner, repo: context.repo.repo, commit_sha: context.sha }); const pr_number = data?.[0]?.number; if (pr_number) { github.rest.issues.createComment({ issue_number: pr_number, owner: context.repo.owner, repo: context.repo.repo, body: output }) } </code></pre> <p>①でPRがマージされ、mainブランチに取り込まれたプッシュイベントをトリガーにワークフローが実行されます。 ②でこのワークフローはmainブランチ上で実行されますが、マージ元のPRにapply結果をコメントで投稿しています。何をしようとして(plan)、結果どうなったか(apply)が1つのPRにまとまり証跡の見通しが良くなります(図1)。</p> <p>▼図1 apply結果のコメント投稿 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173524.png" alt="" /></p> <h2 id="複数環境対応">複数環境対応</h2> <p>プロダクトを運用するうえで、開発用(dev)・商用(prod)など目的ごとに環境を分離することは非常に重要です。環境を分離することでセキュリティリスクを軽減したり、厳格な権限管理ができたりします。また、開発中に誤って商用環境を操作してしまうといった人的ミスの予防にもつながります。 環境分離の境界は<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudプロジェクトや<a class="keyword" href="https://d.hatena.ne.jp/keyword/VPC">VPC</a>ネットワークなどさまざまですが、キャディでは環境ごとにプロジェクトを分離しています。1つのプロダクトを1つのIaC<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>で複数環境へデプロイする方法について、キャディでの事例をもとに解説します。環境ごとの差分をどのようにTerraformで扱うか、各環境へのデプロイをどのように<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsで実現するのか、それぞれ見ていきましょう。</p> <h3 id="tfファイルの構成">tfファイルの構成</h3> <p>環境ごとにワーキング<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを作成し、ステートを分離する手法がプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティス<sup id="fnref-4427-1"><a href="#fn-4427-1" class="footnote-ref" role="doc-noteref">1</a></sup>として知られています。前回の例をもとにしてprod環境用のリソース定義を作成すると、リスト2のようになります。</p> <p>▼リスト2 terraform/environments/prod/main.tf</p> <pre><code class="line-numbers">terraform { backend "gcs" { bucket = "my-tfstate-prod-&lt;&lt;SUFFIX&gt;&gt;" # ① } } provider "google" { project = "&lt;&lt;プロジェクトID&gt;&gt;" region = "asia-northeast1" zone = "asia-northeast1-b" } module "vm" { # ② source = "../../modules/vm" name = "my-vm-prod" } </code></pre> <p>①で、tfstateを保管するバックエンドの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%B1%A5%C3%A5%C8">バケット</a>についても、プロジェクトごとに分離すると構成がシンプルになります。一方で、tfstateを1つの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%B1%A5%C3%A5%C8">バケット</a>に集約して厳格に集中管理したいというケースも考えられますので、自分たちに合った方法を選択しましょう。 ②で、ワーキング<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リをただ分割してしまうと、環境ごとに似たような内容のコードが増え冗長になります。共<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%B2%BD">通化</a>や再利用が可能なリソース定義はモジュール化し、各環境からはモジュールとして利用することで、コードの記述量が減りメンテナンスしやすくなります。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>名やマシンタイプなど、環境ごとに異なるパラメータはモジュールの変数として定義し、外部から変更可能な余地を与えます。</p> <h3 id="ワークフロー定義">ワークフロー定義</h3> <p>prod 環境へ<code>terraform apply</code> を実行するワークフローはリスト3のようになります。dev環境向けのワークフローから変更がない箇所は省略しています。</p> <p>▼リスト3 .<a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a>/workflows/terraform_apply_prod.yml</p> <pre><code class="line-numbers">name: Terraform Apply Prod on: push: branches: - production # ① jobs: terraform_apply: # 略 steps: # 略 - uses: google-github-actions/auth@v1 with: # ② workload_identity_provider: projects/(…略…)/providers/github-provider service_account: my-github-actions@(…略…).com - run: terraform init working-directory: ./terraform/environments/prod # ③ - id: apply run: terraform apply -no-color -auto-approve working-directory: ./terraform/environments/prod continue-on-error: true - uses: actions/github-script@v6 # 略 </code></pre> <p>①は適用するタイミングをdev環境とずらすために、ワークフローのトリガーはproductionブランチへのマージとしています。ブランチ戦略の詳細については後述します。 ②はdev環境とprod環境でプロジェクトが異なる場合、prod環境用の値に置き換えます。 Workload Identity連携、サービスアカウントの作成については前回を参照してください。 ③はterraformを実行する際にはprod用のワーキング<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを指定します。 これでmainブランチをマージするとdev環境向けの変更が、productionブランチをマージするとprod環境向けの変更がそれぞれ適用されるようになりました。 なお、<code>terraform plan</code>を実行する<code>terraform_ci.yml</code>についても修正内容は同じです。 ここまでのファイル構成は図2のとおりです。</p> <p>▼図2 プロジェクトIaC<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の構成</p> <pre><code class="line-numbers">. ├── .github │ └── workflows │ ├── terraform_apply.yml │ ├── terraform_apply_prod.yml │ ├── terraform_ci.yml │ └── terraform_ci_prod.yml └── terraform ├── environments │ ├── dev │ │ └── main.tf │ └── prod │ └── main.tf └── modules └── vm └── main.tf </code></pre> <h3 id="リポジトリのブランチ戦略"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>のブランチ戦略</h3> <p>プロダクト運用では、開発環境で検証したあとに商用など後続環境へのデプロイが行われます。ワーキング<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ分離により各環境を管理している場合、mainブランチ1本だけではデプロイサイクルの管理が難しくなります。 デプロイサイクルをずらすために、人間が環境ごとにPRを作成し、個別に適用するという手間が発生してしまいます。また、全環境から参照されているモジュールを変更した場合、dev環境だけ先に適用するといったタイミングの調整難度はより高くなります。 この解決策の1つとして、環境ごとにブランチを分離し、ワークフローの実行タイミングをずらす方法があります。mainブランチが変更されたらdev環境へデプロイし、productionブランチが変更されたらprod環境へデプロイするという具合です。具体的な運用サイクルは図3のようになります。</p> <p>▼図3 ブランチ戦略 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173508.png" alt="" /></p> <p>この運用では、修正した環境(<code>terraform/environments/*</code>)に関わらず、mainブランチに対してPRを作成します。 PRをトリガーに <code>terraform plan</code> が実行され、マージするとdev環境に対して<code>terraform apply</code>が実行されます。 このとき、prod環境への適用はまだ行われていません。 dev環境で動作確認を行い問題ないことを確認したら、productionブランチに対してmainブランチの変更を含んだPRを作成します。単純な場合には「base: production, compare: main」としてPRを作成します。ここでもPRをトリガーに<code>terraform plan</code>が実行され、マージすると今度はprod環境に対して<code>terraform apply</code>が実行されます(図4)。</p> <p>▼図4 prod環境へ適用するPR<img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173513.png" alt="" /></p> <h2 id="Column-Terraform-Workspace">[Column] Terraform Workspace</h2> <p>複数環境を管理する別の手段として、Terraform Workspace<sup id="fnref-4427-2"><a href="#fn-4427-2" class="footnote-ref" role="doc-noteref">2</a></sup>があります。 Workspaceは、ステートを管理する1つのバックエンド上で複数の独立したステートを保持できる機能です。本稿で解説したワーキング<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ分割の方法と比べて、コードの記述量は少なくなります。 しかし、Workspaceは次の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>を想定した機能となっています。</p> <p>・バックエンドや認証方法が変わらない環境での利用 ・環境を複製し、一時的な検証用途としての利用</p> <p>環境ごとにtfstate用の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%B1%A5%C3%A5%C8">バケット</a>を分離している場合や、リソース定義に違いのある場合には、ワーキング<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リによる分離のほうが管理は容易です。「開発環境は費用を抑えるためにデータベースは1<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>だけだが、商用環境ではリードレプリカ用の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を追加で構築する」といった環境差分にも容易に対応できます。 一方でコードの記述量は増えてしまうので、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a><sup id="fnref-4427-3"><a href="#fn-4427-3" class="footnote-ref" role="doc-noteref">3</a></sup>を確認し、自分たちに合った管理方法を選択しましょう。</p> <h2 id="ワークフローの統合">ワークフローの統合</h2> <p>これまでは、簡単のためにCI/CDのワークフローを個別の環境ごとに作成してきました。ここではワークフロー<code>terraform_apply.yml</code> を例に、よりDRYに記述する方法について解説します。 <code>terraform_apply.yml, terraform_apply_prod.yml</code> を1つのワークフローに統合してみましょう。</p> <p>環境ごとに変わる値は次のとおりです。</p> <p>・トリガーとなるベースブランチ ・<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud認証用のパラメータ ・Terraformのワーキング<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ</p> <p>これらのうち、<code>google-github-actions/auth</code>の入力パラメータ、Terraformのワーキング<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リについては、ベースブランチ名によって値を切り替えられれば良さそうです。 また今まで<code>workload_identity_provider</code>, <code>service_account</code>はハードコードしていましたが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsシークレットも活用してみましょう。<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsシークレットは、機密性の高いデータを管理するための機能です。 ワークフローからは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>として参照できます。 図5のように環境名の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%EC%A5%D5%A5%A3%A5%C3%A5%AF%A5%B9">プレフィックス</a>を付け、Workload Identityプロバイダとサービスアカウントをシークレットに登録します。</p> <p>▼図5 Actionsシークレット <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173457.png" alt="" /></p> <p>環境差分を吸収した、統合後のワークフローはリスト4のようになります。</p> <p>▼リスト4 .<a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a>/workflows/terraform_apply.yml</p> <pre><code class="line-numbers">name: Terraform Apply on: push: # ① branches: - main - production jobs: terraform_apply: # 略 steps: - uses: actions/checkout@v3 - uses: hashicorp/setup-terraform@v2 - id: get_env # ② shell: bash run: | case ${{ github.ref_name }} in production ) echo 'env=prod' &gt;&gt; $GITHUB_OUTPUT echo 'upper_case_env=PROD' &gt;&gt; $GITHUB_OUTPUT ;; * ) echo 'env=dev' &gt;&gt; $GITHUB_OUTPUT echo 'upper_case_env=DEV' &gt;&gt; $GITHUB_OUTPUT ;; esac - uses: google-github-actions/auth@v1 with: # ③ workload_identity_provider: ${{ secrets[format('{0}_GCP_WI_PROVIDER', steps.get_env.outputs.upper_case_env)] }} service_account: ${{ secrets[format('{0}_GCP_WI_SERVICE_ACCOUNT', steps.get_env.outputs.upper_case_env)] }} - run: terraform init working-directory: ./terraform/environments/${{ steps.get_env.outputs.env }} # ④ - id: apply run: terraform apply -no-color -auto-approve working-directory: ./terraform/environments/${{ steps.get_env.outputs.env }} continue-on-error: true </code></pre> <p>①はmainまたはproductionブランチへのプッシュイベントをトリガーに、このワークフローが実行されます。 ②はブランチ名を参照し、対応する環境名をステップの出力パラメータとして設定します。 ③は環境名プレフィクスを追加した文字列(<code>DEV_GCP_WI_PROVIDER</code>)を作成し、シークレット(<code>secrets.DEV_GCP_WI_PROVIDER</code>)を参照します。 ④では環境名に対応したワーキング<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを指定します。 これで、CDのワークフローファイルを1つに統合できました。今後ステージング環境など環境が追加された場合にも数行の修正で対応できます。CIのワークフローを修正する際には<code>{{ github.ref_name }}</code>を<code>${{ github.base_ref }}</code>に置き換えてください。 今回は<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsシークレットを取り扱いましたが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>の契約プランによってはEnvironmentsのシークレット機能が利用できます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>のEnvironmentsはデプロイ先の環境ごとに、ブランチ保護ルールやシークレット、変数を管理できます。これによりmainブランチでは<code>GCP_WI_PROVIDER=AAA</code> 、productionブランチでは <code>GCP_WI_PROVIDER=BBB</code> といった値の切り替えが容易に実現できます。</p> <h2 id="Column-Matrix-strategyの活用">[Column] Matrix strategyの活用</h2> <p>今回紹介したブランチ戦略では、各環境に対応するブランチへのPRやプッシュをトリガーにCI/CDが実行されます。この場合、prod環境に対するCI (<code>terraform plan</code>) を実行するには、一度mainブランチへマージしなければなりません。 しかし、より早く間違いを検知するために、mainブランチへのPR上でdev/prod両環境に対してCIを実行したくなります。この課題はMatrix strategy<sup id="fnref-4427-4"><a href="#fn-4427-4" class="footnote-ref" role="doc-noteref">4</a></sup>を活用することで解決できます。 Matrix strategyは、dev/prodなどのバリエーションを変数で定義し、その値ごとにジョブを複数実行できる機能です。たとえば、前回紹介した<code>terraform_ci.yml</code>ではリストAのように修正します。</p> <p>▼リストA .<a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a>/workflows/terraform_ci.yml</p> <pre><code class="line-numbers">jobs: terraform_ci: # 略 strategy: # ① matrix: environment: [main, production] steps: # 略 - id: get_env shell: bash run: | case ${{ matrix.environment }} in # ② production ) # 略 </code></pre> <p>①で並列実行のための変数を定義します。ここでは各環境に対応するベースブランチ名を与えています。 ②でブランチ名から環境名を解決する <code>get_env</code> ステップ内で、 <code>${{ github.base_ref }}</code> の代わりに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%C8%A5%EA%A5%C3%A5%AF%A5%B9">マトリックス</a>の値 <code>${{ matrix.environment }}</code> を与えます。 これで、mainブランチに対してPRが作成された際に、dev/prod各環境に対する<code>terraform plan</code>を確認できます。</p> <h2 id="組織アカウントの横断管理">組織アカウントの横断管理</h2> <p>企業として<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudを利用している場合、組織リソース配下で複数プロダクト、プロジェクトを管理することになります。しかし、管理下の全プロジェクトに対して前回の事前準備で触れた作業を実施するのは非常に手間がかかり、運用がスケールしません。 この課題に関するキャディでの取り組み事例を簡単に紹介します。 キャディでは組織リソースに対してもIaCを行い、プロジェクトの横断的な構成管理やガバナンス強化を実施しています。IaC用の<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>は責務ごとに分離していますが、おもに2種の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>から構成されます。 一つは本稿で解説してきた、プロダクトにひも付くIaC<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>です(以下product-repo)。 product-repoはプロダクトごとに作成され、対応する<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudプロジェクトに関連するリソースを管理します。 そしてもう一つは組織全体を管理するIaC<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>です(以下org-repo)。org-repoでは横断的に設定したい項目や、フォルダに対するIAM設定を管理しています。 具体的には、org-repoで次のようなリソースを管理しています。</p> <p>・tfstate用のStorage<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%B1%A5%C3%A5%C8">バケット</a>作成 ・<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> ActionsでTerraformを実行するためのセットアップ作業 ・Workload Identityプール、プロバイダ作成 ・サービスアカウント作成 ・ product-repoに<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsシークレット登録<sup id="fnref-4427-5"><a href="#fn-4427-5" class="footnote-ref" role="doc-noteref">5</a></sup> ・組織ポリシーの管理 ・セキュリティ基盤向けのLogging転送設定</p> <p>新規プロダクトが作成された際には、product-repoとプロジェクトの対応関係をorg-repoに追加します。org-repo上のワークフローによって、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> ActionsでTerraformを実行するための各種セットアップが行われます。セットアップを自動化することで、product-repoのCI/CD環境をすばやく開発者へ提供できます(図6)。 このような取り組みを通してキャディでは運用のスケーラビリティ向上を目指しています。</p> <p>▼図6 横断管理の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a> <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173519.png" alt="" /></p> <h2 id="おわりに">おわりに</h2> <p>前回から2回にわたりTerraformと<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsを組み合わせたIaCのCI/CDパイプラインについて紹介しました。手作業によるミスをなくしつつ、安全にすばやくリリースするためにIaCとCI/CDは欠かせない要素です。本稿がみなさんの運用負荷を下げるヒントになれば幸いです。 次回はRenovateを用いたライブラリの自動更新について紹介します。</p> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn-4427-1" role="doc-endnote"> <a href="https://cloud.google.com/docs/terraform/best-practices-for-terraform ">https://cloud.google.com/docs/terraform/best-practices-for-terraform </a><a href="#fnref-4427-1" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4427-2" role="doc-endnote"> <a href="https://developer.hashicorp.com/terraform/language/state/workspaces ">https://developer.hashicorp.com/terraform/language/state/workspaces </a><a href="#fnref-4427-2" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4427-3" role="doc-endnote"> <a href="https://developer.hashicorp.com/terraform/cli/workspaces ">https://developer.hashicorp.com/terraform/cli/workspaces </a><a href="#fnref-4427-3" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4427-4" role="doc-endnote"> <a href="https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs ">https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs </a><a href="#fnref-4427-4" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4427-5" role="doc-endnote"> <a href="https://github.com/integrations/terraform-provider-github ">https://github.com/integrations/terraform-provider-github </a><a href="#fnref-4427-5" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> </ol> </div> caddi-tech CADDiで4年間働いたエンジニアがプロダクトの変遷について話してみる hatenablog://entry/6801883189081010332 2023-09-08T14:29:47+09:00 2024-02-08T17:36:22+09:00 こんにちは、キャディでエンジニアやエンジニアリングマネージャをやっている高藤です。 久しぶりのTechブログへの投稿です。 今回はタイトルにあるようにCADDiで4年働く上で起きた事をエンジニア視点でまとめつつ、これから何をしようとしているのか私自身の思いを込めて書いてみようかなと思います。 4年前のあのころ 私は2019年2月に入社し、4年強の期間CADDiで働いてきました。私が当時CADDiに興味を持ったのは、単純に面白い経歴のCEOとCTOがなぜ日本で起業したのかという興味と私自身が成長できる環境で働きたいという2点でした。 正直な話、CADDiが対象とする、製造業ドメインへの興味は全く… <p>こんにちは、キャディでエンジニアやエンジニア<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%F3%A5%B0%A5%DE">リングマ</a>ネージャをやっている高藤です。 久しぶりのTechブログへの投稿です。</p> <p>今回はタイトルにあるようにCADDiで4年働く上で起きた事をエンジニア視点でまとめつつ、これから何をしようとしているのか私自身の思いを込めて書いてみようかなと思います。</p> <h2 id="4年前のあのころ">4年前のあのころ</h2> <p>私は2019年2月に入社し、4年強の期間CADDiで働いてきました。私が当時CADDiに興味を持ったのは、単純に面白い経歴のCEOとCTOがなぜ日本で起業したのかという興味と私自身が成長できる環境で働きたいという2点でした。</p> <p>正直な話、CADDiが対象とする、製造業<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>への興味は全くありませんでした。</p> <p>当時のCADDiではWebから<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CE%AE%C6%FE">流入</a>するたくさんの顧客に対して見積を自動で行い、見積頂いた顧客からくる製造依頼に対して製品を納品していました。これらの業務は当時フォーカスをしていた3D CADからの自動見積を行う技術や頂いた図面から見積に必要な入力を人力で抜き出し、社内の見積ロジックを使ってコストを算出するオペレーションを通じて実現していました。</p> <p>まだまだ、複雑なオペレーションを人力で行っている状況も多く、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CE%AE%C6%FE">流入</a>する顧客数の増加に伴い、複雑なオペレーションを支えるための仕組みが社内で必要とされているタイミングでもありました。</p> <p>当時私達が採用していた技術をまとめると以下のものがありました。</p> <ul> <li>3D CADを解析して見積に必要な入力値を解析する<a class="keyword" href="https://d.hatena.ne.jp/keyword/C%2B%2B">C++</a>で記述された<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a></li> <li>入力値から製造コストを算出する<a class="keyword" href="https://d.hatena.ne.jp/keyword/C%2B%2B">C++</a>で記述されたロジック</li> <li>製造コストを算出するための<a class="keyword" href="https://d.hatena.ne.jp/keyword/Excel">Excel</a>で記述された計算モデル</li> <li>3D CADをアップロードし見積/製造依頼ができるWebアプリケーション</li> </ul> <h2 id="受発注管理の課題から生まれたKleinというプロダクト">受発注管理の課題から生まれたKleinというプロダクト</h2> <p>当時の受発注の仕組は社内で用意された<a class="keyword" href="https://d.hatena.ne.jp/keyword/Salesforce">Salesforce</a>を軸にオペレーションを構築していました。しかし事業のスケールを見据え、より大量の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%E9%A5%F3%A5%B6%A5%AF%A5%B7%A5%E7%A5%F3">トランザクション</a>に耐えるオペレーションを実現するため、2つの大きなプロダクトを開発する決断をしました。</p> <p>1つは顧客からの見積リードタイムを減らすため見積ロジックを担うQuipuと呼ばれるプロダクト。もう1つは受発注管理を行うKleinというプロダクトです。私は後者のKleinというプロダクトの開発に携わってきました。どんなプロダクトなのかは弊社白井の<a class="wp-editor-md-post-content-link" href="https://caddi.tech/archives/2211">記事</a>を見てもらったほうがわかりやすいかと思います。</p> <p>また技術面としても開発言語をRustにするなどいくつかの大きな決定を行いました。当時Rustを選択した経緯などは<a class="wp-editor-md-post-content-link" href="https://caddi.tech/archives/2331">別の記事</a>でまとめてありますが、今ふりかえると自分でもよく決断したなと思ったりもします。Kleinの開発を通して私達が学んだことは大きく2点あると思います。</p> <h2 id="ドメイン駆動設計"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>駆動設計</h2> <p>Kleinの開発を通じて最も学んだことは「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>駆動設計」を採用した事です。当時の開発チームはプロダクトマネージャーの白井も含め製造業出身のメンバーがおらず、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>ナレッジが全くない状態からのスタートでした。</p> <p>私達は「どのような業務プロセスにしたいのか」「どのような課題を解きたいのか」を明らかにしないと前にも進めない状態でした。またCADDiの成長に伴いプロダクトに求められることも変わると予測していたため、プロダクトの中核部分である<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>はその時に見えている範囲で正しく設計したいという判断を行いました。</p> <p>一言に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>駆動設計と聞くと、エンジニア視点ではどのように実装するのかという部分に目が行きがちですが、その本質は開発対象となる<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>についてどれだけ理解している状態で開発できるかが重要です。そこで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>エキスパートとチームで徹底的に議論を行い、プロダクトの設計にあたりました。</p> <p>具体的には毎日昼食時にはチームと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>エキスパートである業務担当者を招き、徹底的に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D2%A5%A2%A5%EA">ヒアリ</a>ングとホワイトボード上での<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E2%A5%C7%A5%EA%A5%F3%A5%B0">モデリング</a>繰り返しました。これらのプロセスを通じて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A5%D3%A5%AD%A5%BF%A5%B9">ユビキタス</a>言語の構築したことで、エンジニア自身が現場で起きていることをクリアに理解でき、同じ言葉を使って議論できるようになったことが大きな学びでした。</p> <h2 id="新しい技術の採用">新しい技術の採用</h2> <p>前述したRust以外にも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%BF%AE%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">通信プロトコル</a>としてのgRPC/GraphQLの採用を行いました。これらの新しい技術の採用過程には、解きたい課題に使う技術が合致しているかという問いだけでなく、新しい技術を使ってみたいという感情的な理由もあったかなとも思っています。それらの決断に対して「だめだったら考え直せばいいじゃん」「学べば良いでしょ」という開発組織の空気感があったことも支えになったと思っています。初めて利用する技術については我々の無知故にハマった数々の落とし穴など数え切れないような失敗も経験しましたが、ここで得た知見は現在でも資産になっています。</p> <h2 id="おやCADDiのようすが">おや、CADDiのようすが</h2> <p>前述のKleinの開発と前後してCADDiが大きく方針を変えたのもこの頃でした。前述したとおり、当時のCADDiはWebからの受注を主としており、事業の成長としてもより多くの顧客からWebを通じて取引が発生する事を計画をしておりました。</p> <p>しかしいくつかの理由からこの方針を変更することになりました。一言でまとめると以下のようなことだったかと思います。</p> <p>「顧客の要求が顧客毎に異なりCADDiとして標準化した要求としてまとめることが難しかった」 これは品質などに対する要求が顧客毎に異なること、そのような品質を業界や用途向けに統一的に定義し、それを顧客に提示し受け入れてもらうことがが難しかったということになります。当たり前ではありますが、1スタートアップの小さい会社が標準化された仕様を作り上げたとしてもそれを受け入れてもらう交渉力はなかったと思います。</p> <p>そこで、CADDiは顧客を徹底的に絞る決断をしました。今までは顧客が持つ装置の一部分の部品に対する調達依頼を受けていましたが、この方針転換により、装置一式などより大きい単位で依頼を受けることになりました。この変更によりプロダクト開発側としても様々な点で考慮が必要になりました。</p> <h2 id="プロダクトに求められる要件の変化">プロダクトに求められる要件の変化</h2> <p>当初、顧客との取引においてCADDiが調達を行う製品数は20製品ほどが多く、プロダクトの設計においても20製品程度の納品を行う案件を大量に処理するオペレーションを想定していました。しかしこの方針転換の実施後、1回の取引で1,000製品を超える取引が発生するようになりました。</p> <p>Kleinなどオペレーションを担うプロダクトは小さい案件を大量に処理することから、大きな案件を問題なく完遂するための大量の製品に対する操作が必要になるなど、当初想定していた機能ではオペレーションを支えきれないことがわかりました。</p> <p>これらの問題を解決するためにプロダクトに対して一括で処理を行う機能の提供を行ったりなど、大きな案件を処理する上で必要な機能の追加や大量の処理を行った際のパフォーマンスを改善することを行ってきました。</p> <h2 id="より深いドメインナレッジの獲得">より深い<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>ナレッジの獲得</h2> <p>大きな意思決定ではありましたが、より顧客にフォーカスし特定の業界における産業装置に対する知見を獲得することができました。また大きな調達プロジェクトにおいて、どんな事が発生するのかなど数え切れないほど学びを得ることができました。</p> <p>これらの新しい<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>ナレッジは単純に既存プロダクトの改修だけには留まらず、新しいプロダクトの種にもなったと思っています。</p> <h2 id="CADDiにおける生産管理プロダクト">CADDiにおける生産管理プロダクト</h2> <p>前述の大きな転換を行いながらも、CADDiは大きく成長してきました。私が入社した4年前を考えると比較にできないくらい大きい案件の調達を行っています。また、当初のCADDiでは基本的には受注生産を行ったオペレーションを行っていましたが、案件の規模が大きくなるにつれ、見込み生産を行い在庫を持つような取引も発生しています。</p> <p>私が当初開発を行ったKleinでは受注生産モデルとして設計していたこともあり、こういった事業の成長に対してモデル自体を刷新する必要もでてきました。こちらは既存のKleinというプロダクトを改修する判断ではなく、根本からモデルを刷新するためプロダクトのリプレイスを実施しました。</p> <p>顧客との取引を重ねることで製造業における図面管理の難しさに気づきました。現在CADDiが提供している<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>プロダクト「CADDi DRAWER」の原型となる図面管理プロダクトの開発も行い図面の世代管理や図面を介したコミュニケーションの改善を行ったりしています。</p> <p>また、単純に受発注だけの管理だけでなく、製造を引き受けていただく加工会社様とのコミュニケーションを円滑にするためのプロダクトや倉庫での在庫管理や倉庫内オペレーションを支援するためのプロダクトを開発してきました。このように事業拡大と共に必要な課題を様々なプロダクトを開発・運用することで解決してきました。</p> <p>これらの開発を通じて得たナレッジはエンジニアだけでなくCADDiの資産になっています。他方でCADDiの生産管理プロセスにおいてCADDi独自のナレッジや解決させるためのHowになっている部分と多くの企業と同様のアプローチで課題解決を行っている部分が出てきていることも事実です。</p> <h2 id="サプライチェーンのデータを資産化する"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%D7%A5%E9%A5%A4%A5%C1%A5%A7%A1%BC%A5%F3">サプライチェーン</a>のデータを資産化する</h2> <p>ここまで、今のCADDiのプロダクトの開発とその開発や課題を解決することで得たナレッジについての話をしてきました。私は現在「CADDi DRAWER」というCADDi初の<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>プロダクトの開発を行っています。CADDi DRAWERについては以下の2つの記事をみてもらったほうが良いかなと思います。</p> <ul> <li><a class="wp-editor-md-post-content-link" href="https://note.com/yosukeshirai/n/nc55301dddc49">CADDi DRAWERで何をしたいのか?/創業6年目からの新しい挑戦</a></li> <li><a class="wp-editor-md-post-content-link" href="https://note.com/yosukeshirai/n/n060824de8982">プロダクトが何かを変える瞬間に立ち会うこと</a></li> </ul> <p>この新しいプロダクトはCADDiで私達が経験した課題やその解決方法から生み出されたプロダクトです。現在は主に「図面」という製造業における重要なデータを取り扱っています。しかし製造業においては、図面以外にも様々なプロセスから情報が発生しています。それらはデータとしては存在しているのですが、資産として扱える状態ではないと考えています。</p> <p>今後CADDi DRAWERには製造の各プロセスに関するより多くの情報をが蓄積され、それら情報から課題発見や意思決定を促すプラットフォーム基盤になると考えています。これらは私達が受発注プラットフォーム事業を行ってきたからこそ実現できることだと思っています。もちろん解くべき課題も多く、技術面だけでなく大きくなってきたCADDiの開発組織がより生産的に活動できるようにするにはどうしたら良いのかなど多くのことに取り組まないといけない状況です。</p> <h2 id="さてなぜ私は働いているのだろうか">さて、なぜ私は働いているのだろうか?</h2> <p>製造業という未知の領域にCADDiのエンジニアとして飛び込んで、4年と少し働いてきました。製造業における課題を少しづつではありますが見てきたつもりです。最後になぜCADDiにいるのかをまとめて終わりにしようと思います。</p> <p>記事の冒頭で記載したとおり、当初、製造業という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>自体への興味はありませんでした。4年間濃い経験を過ごすことができた結果、製造業という産業自体の課題をエンジニアとして解決してみたいと思うようになりました。</p> <p>私が今後CADDiを通してやりたいのは「製造プロセスの中で標準的な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">プロトコル</a>を定める」ことです。CADDiへ飛び込む前は製造において図面さえあれば顧客が望むものは製造できると考えていました。その意味で図面は1つの標準<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">プロトコル</a>だと捉えていました。ですが製造業の中で働く中で、図面だけでは顧客が望んでいるものを納品できないという現実が見えてきました。</p> <p>顧客が望むものを納品するためには図面を元にした要求事項の確認が必要であったり、なかには図面で表現できていないことや過去の商習慣から生まれた<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B0%C5%CC%DB%C3%CE">暗黙知</a>など、取引において図面以外のコンテキストが必要になります。このような標準でない<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%C8%A5%B3%A5%EB">プロトコル</a>をCADDiが取引に参加することで標準化を促したり定義できると考えています。これは今まで行ってきた受発注プラットフォームや「CADDi DRAWER」どちらを通じても実現できるだろうと考えています。</p> <p>こうした産業への<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%D1%A5%AF">インパク</a>トを起こせる仕事というのもなかなか無いと思っています。このようなチャレンジをできるCADDiだからこそ面白いと改めて実感しています。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173618.png" alt="" /></p> <p>(なお、この4年間は、本当にきついと感じることは何度もありました。ですが良い仲間に出会えお互いを支えられる関係でここまで仕事ができました。本当に感謝しています。)</p> <p>本記事を読んで、CADDiのミッションやプロダクト組織に少しでも興味を持ってくださった方。お気軽にカジュアル面談でお話ししませんか。 <a class="wp-editor-md-post-content-link" href="https://open.talentio.com/r/1/c/caddi-jp-recruit/pages/78398">https://open.talentio.com/r/1/c/caddi-jp-recruit/pages/78398</a></p> <p>プロダクトマネージャ、ソフトウェアエンジニア、セキュリティエンジニア等、様々なポジションを募集しています。 <a class="wp-editor-md-post-content-link" href="https://open.talentio.com/r/1/c/caddi-jp-recruit/homes/4139?group_ids=8633">https://open.talentio.com/r/1/c/caddi-jp-recruit/homes/4139?group_ids=8633</a></p> <p>ご連絡お待ちしています。</p> caddi-tech Pantsモノレポの改善~テスト時間短縮・依存の集約管理・pex~ hatenablog://entry/6801883189081010388 2023-09-05T03:54:56+09:00 2024-02-26T17:18:25+09:00 MLOps Team Tech Lead の西原です。以前のTech Blogで Pants を使った Python モノレポ移行への取り組みについて紹介しました。日々の業務で得た知見を Python コミュニティに共有できるといいなと思い、PyCon APAC 2023に「Pants ではじめる Python モノレポ」というタイトルで CfP を提出し採択されました。この記事では、PyCon APAC 発表に向けての整理も兼ねて、Pants を使ったモノレポの管理・運用を効率的に行うための取り組みを一部紹介します。 TL;DR CI の待ち時間を短縮する リモートキャッシュの活用 テストの… <p>MLOps Team Tech Lead の西原です。以前の<a class="wp-editor-md-post-content-link" href="https://caddi.tech/archives/4187">Tech Blog</a>で Pants を使った <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> モノレポ移行への取り組みについて紹介しました。日々の業務で得た知見を <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> コミュニティに共有できるといいなと思い、<a class="wp-editor-md-post-content-link" href="https://2023-apac.pycon.jp/">PyCon APAC 2023</a>に「Pants ではじめる <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> モノレポ」というタイトルで CfP を提出し採択されました。この記事では、PyCon APAC 発表に向けての整理も兼ねて、Pants を使ったモノレポの管理・運用を効率的に行うための取り組みを一部紹介します。</p> <ul class="table-of-contents"> <li><a href="#TLDR">TL;DR</a></li> <li><a href="#CI-の待ち時間を短縮する">CI の待ち時間を短縮する</a><ul> <li><a href="#リモートキャッシュの活用">リモートキャッシュの活用</a></li> <li><a href="#テストの分散実行による効率化">テストの分散実行による効率化</a></li> </ul> </li> <li><a href="#モノレポ内の依存を集約管理">モノレポ内の依存を集約管理</a><ul> <li><a href="#依存管理を集約する背景">依存管理を集約する背景</a></li> <li><a href="#依存関係の更新">依存関係の更新</a><ul> <li><a href="#poetry-up">poetry up</a></li> <li><a href="#pip-compile">pip-compile</a></li> </ul> </li> <li><a href="#依存関係の集約">依存関係の集約</a></li> </ul> </li> <li><a href="#pex-による-Python-環境のパッケージング">pex による Python 環境のパッケージング</a><ul> <li><a href="#pex-とは">pex とは</a></li> <li><a href="#Pants-による-pex-の構築">Pants による pex の構築</a></li> <li><a href="#pex-を用いたコンテナイメージの構築">pex を用いたコンテナイメージの構築</a></li> </ul> </li> <li><a href="#プロジェクトの依存関係を制御するPants-の-visibility-機能の活用">プロジェクトの依存関係を制御する:Pants の visibility 機能の活用</a></li> <li><a href="#まとめ">まとめ</a></li> <li><a href="#link">link</a></li> </ul> <h1 id="TLDR">TL;DR</h1> <ul> <li>リモートキャッシュでテスト時間を 1/12 に短縮</li> <li>テストの分割実行で CI の待ち時間短縮</li> <li>依存の集約管理でビルドの堅牢性向上</li> <li>pex を使った <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> コードのパッケージング <ul> <li>シンプルなビルドプロセス</li> <li>コンテナイメージの軽量化</li> </ul></li> <li>依存禁止ルールを設け、意図しない依存関係形成の防止</li> </ul> <h1 id="CI-の待ち時間を短縮する">CI の待ち時間を短縮する</h1> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>のサイズが大きくなると、依存が増え CI の待ち時間が長くなります。CI の待ち時間が長くなれば開発業務の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DC%A5%C8%A5%EB%A5%CD%A5%C3%A5%AF">ボトルネック</a>になり、開発スピードが低下します。この状況は開発体験を損ないますが、リモートキャッシュの活用やテストの分割実行をすることで CI の待ち時間を短縮できます。</p> <h2 id="リモートキャッシュの活用">リモートキャッシュの活用</h2> <p>Pants は Remote Execution <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>(REAPI)による リモートキャッシュを<a class="wp-editor-md-post-content-link" href="https://www.pantsbuild.org/docs/remote-caching">サポート</a>しています。これにより、個々の開発マシンのローカルキャッシュだけでなく、異なる開発マシン間でキャッシュを共有できます。リモートキャッシュを参照することで一度実行済みのコードやテストの結果を再利用できるため、CI の待ち時間を短縮できます。REAPI をサポートした self-hosted の <a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a> やマネージドサービスがいくつかありますが、私たちのモノレポでは<a class="wp-editor-md-post-content-link" href="https://github.com/buchgr/bazel-remote">bazel-remote-cache</a>を使って検証を進めています。Bazel Remote Cache は、ローカル ディスク、S3、GCS、Azure Blob ストレージ をサポートしています。REAPI 用のサーバを建てる必要がなく、Docker コンテナ 1 つだけで動作するため簡単に導入できます。Pants の公式<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>には、S3 を用いて リモートキャッシュを有効にする<a class="wp-editor-md-post-content-link" href="https://github.com/pantsbuild/pants/blob/e0ef5b83820ce5d269516a3ce5c04f5fc354a9ab/build-support/bin/generate_github_workflows.py#L201-L250">例</a>が存在します。これを参考に、GCS 用の setup を行い、検証を進めました。</p> <p>リモートキャッシュを有効にするためには、<code>pants.toml</code> または<code>.pants.rc</code> ファイルに以下のような設定を追加します。</p> <pre class="code toml" data-lang="toml" data-unlink>[GLOBAL] remote_cache_read = true remote_cache_write = true remote_store_address = &#34;grpc://localhost:9092&#34;</pre> <p>GCS を使った リモートキャッシュ setup の例が次になります。リモートキャッシュを格納する GCS の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%B1%A5%C3%A5%C8">バケット</a>を事前に作成し、作成済みの <a class="keyword" href="https://d.hatena.ne.jp/keyword/bucket">bucket</a> を<code>--gcs_proxy.bucket</code>オプションで指定します。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synStatement">mkdir</span> <span class="synSpecial">-p</span> ~/bazel-remote docker run <span class="synSpecial">-u</span> 1000:1000 <span class="synSpecial">-v</span> ~/bazel-remote:/data <span class="synSpecial">-p</span> 9092:9092 buchgr/bazel-remote-cache <span class="synSpecial">-d</span> <span class="synSpecial">--max_size</span> <span class="synConstant">10</span> <span class="synSpecial">--gcs_proxy.bucket=foo_bar_remote_cache_example_bucket</span> <span class="synSpecial">--gcs_proxy.use_default_credentials=true</span> </pre> <p>上記の setup を行った状態で、2 万行のコードに対してテストを実行し、キャッシュの有無で処理時間に差が出るかを確認しました。結果として、キャッシュがない状態で<code>12分50秒</code>かかっていたテストが、リモートキャッシュに full hit すると<code>59秒</code>で終了することが確認できました。</p> <p>私たちのモノレポでは pre-commit hook などを活用し、コードが <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> に push される前に個々の開発環境でテストが実行されるように努めています。これらのテストは CI でも実行されているので、ほとんどの場合で同じテストが 2 度実行されることになります。リモートキャッシュを使うと開発環境でのテスト結果を CI での実行時に参照できるので、CI の待ち時間を大幅に短縮できます。</p> <h2 id="テストの分散実行による効率化">テストの分散実行による効率化</h2> <p>Pants では、テストを複数の shard(分割単位)に分けて実行できます。CI 環境でこの機能を活用すると複数のマシンで分散してテストを実行でき、CI の待ち時間を短縮できます。以下に、テストを 2 つの shard に分割して実行するコード例を示します。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>pants <span class="synStatement">test</span> <span class="synSpecial">--shard=0/2</span> :: pants <span class="synStatement">test</span> <span class="synSpecial">--shard=1/2</span> :: </pre> <p>Pants の公式<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>では、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions を使用して shard に分割したテストを複数のマシンで実行しています。<a class="wp-editor-md-post-content-link" href="https://github.com/pantsbuild/pants/actions/runs/5645067367">こちらの例</a>では、10 台のマシンでテストを並列実行し、1 つのジョブが 10 分程度で完了しています。</p> <p>リモートキャッシュの実験でも使った 2 万行のコードに対して<code>pants test --shard=1/10</code>を実行してみました。キャッシュヒットがない状態でも 58 秒でテストを終了することが確認できました。開発規模が拡大し、依存するテストが増えた場合でも shard 数を増やして複数のマシンで並列分散することで CI の待ち時間を短縮できます。</p> <h1 id="モノレポ内の依存を集約管理">モノレポ内の依存を集約管理</h1> <h2 id="依存管理を集約する背景">依存管理を集約する背景</h2> <p>以前の<a class="wp-editor-md-post-content-link" href="https://caddi.tech/archives/4187">Tech Blog</a>では、私たちが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>内の各プロジェクトごとで依存ライブラリを管理していることを紹介しました。しかし、そのアプローチを続けるうえで 2 つの問題がありました。これらの問題に対処するために、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>内の依存管理を集約することにしました。</p> <p>最初の問題は Diamond Dependencies です。これは下の図のように<code>liba</code>と<code>libb</code>が<code>libbase</code>に依存している場合、<code>liba</code>が使う<code>libbase</code>のバージョンと<code>libb</code>が使う<code>libbase</code>のバージョンが異なると、ビルドに失敗する場合があるというものです。私たちが使ってるライブラリの例だと<code>PyTorch</code>や<code>pydantic</code>、<code>Kubeflow pipeline</code>、<code>pandas</code>などで最近メジャーアップデートがありました。モノレポではコードの参照が容易にできますが、それぞれのコードで依存しているライブラリのバージョンが異なるとビルドに失敗することがあります。任意のライブラリに対して 1 つのバージョンを使用することでビルドの堅牢性を高め、失敗することを防ぐことができます。これは書籍『Software Engineering at <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a>』の<a class="wp-editor-md-post-content-link" href="https://abseil.io/resources/swe-book/html/ch18.html">Build Systems and Build Philosophy</a>の章で"One-Version Rule"として紹介されています。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240206/20240206163116.png" alt="Diamond Dependencies" /> 図 : Diamond Dependencies の例。引用:<a class="wp-editor-md-post-content-link" href="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240206/20240206163116.png">Diamond Dependencies</a></p> <p>2 つ目の問題は、依存関係のバージョン更新にかかる<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>が増えてきたことです。モノレポを運用する上で、次の理由から依存関係の更新をなるべく高頻度で行うようにしています。</p> <ul> <li>開発が落ち着いてるから何もいじらないという選択肢もあるが、そのまま塩漬けになり久しぶりに触ってみた時に動かない可能性がある <ul> <li>頻繁に更新することで CI やビルドが動くのである程度動作確認できる</li> <li>依存してるライブラリが更新されると互換性がなくて動かないということが起こり得るが、どのバージョンまでなら動くのかを把握するために高頻度に更新したい</li> <li>更新サイクルが長く、1 回の更新で多くを変更して問題が起きた場合に何が原因なのか問題の特定に時間がかかる</li> </ul></li> <li>セキュリティ的にハイリスクな問題は放置できない</li> <li>通常新しいバージョンは新機能の追加やバグ修正がされてるので私たちにとってプラス</li> </ul> <p>これまで、各プロジェクトごとに poetry を使って依存関係を管理していました。poetry を使っている場合は、<code>poetry lock</code>コマンドを実行すると依存が壊れない範囲で最新バージョンを poetry.lock ファイルに記述してくれます。しかし、私たちの環境では poetry.lock ファイルを使って依存管理をしないため別の方法で依存関係の管理をする必要があります(後述)。poetry で管理しているライブラリバージョンをまとめて更新するために<a class="wp-editor-md-post-content-link" href="https://github.com/MousaZeidBaker/poetry-plugin-up">poetry up</a>の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>を使っていました。poetry には<code>poetry update &lt;lib&gt;</code>コマンドで個別のライブラリを更新する機能がありますが poetry up を使うことでライブラリ個別にではなく、一括で依存関係を更新できます。依存関係の更新を行う際は、都度プロジェクトで使う <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> バージョンに切り替えて<code>poetry up</code>コマンドを実行して依存関係の更新を行っていました。Diamond Dependencies の問題になりそうなところは個別にバージョンを揃えていました。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>で管理するプロジェクトが増えてくると、それぞれのプロジェクトで依存しているライブラリのバージョンを揃えつつ更新する作業が大変になってきました。</p> <p>これらの問題を解決し、依存管理の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>を下げるために<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>内の依存管理を集約することにしました。</p> <h2 id="依存関係の更新">依存関係の更新</h2> <p>依存関係を集約する上で考慮するした点は、集約した依存関係のバージョンをどう更新していくかです。Pants で <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> ライブラリの依存関係を管理するにあたって、次の<a class="wp-editor-md-post-content-link" href="https://www.pantsbuild.org/docs/python-third-party-dependencies">4 つの形式</a>がサポートされています。</p> <ul> <li><code>requirements.txt</code>ファイル</li> <li><code>poetry</code>形式の<code>pyproject.toml</code>ファイル</li> <li><code>PEP621</code>形式の<code>pyproject.toml</code>ファイル</li> <li><code>pipenv</code>形式の<code>Pipfile.lock</code>ファイル</li> </ul> <p>Pants にも lock ファイル生成の機能があり、上記の形式に従って管理すれば、依存が壊れない範囲の最新バージョンで依存関係の管理をしてくれます。しかし、この lock ファイルは人が読んで理解しやすい形式ではありません。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%D0%A5%C3%A5%B0">デバッグ</a>時など、使用しているバージョンを特定する場面を考えると人から見ても分かりやすい形式で管理するのが望ましいと考えました。</p> <p>そこで、人からも読める形式であり、"One-Version Rule"を実現できるように<code>poetry up</code>を使う方法と<code>pip-compile</code>を使う 2 つの方法を検討しました。</p> <h3 id="poetry-up">poetry up</h3> <p>poetry up は先にも紹介した通り、poetry 管理の依存関係を一括で更新するための<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>です。poetry up を使って更新する手順は次のように考えました。</p> <ol> <li>モノレポで使うライブラリをバージョン指定せずに pyproject.toml に記述。バージョンを指定しない理由は、新規追加した際と<code>poetry up</code>を実行した際に既存のライブラリとバージョンが競合することがあるため。依存が多いモノレポでは競合が起きやすく、都度解決するのは大変。</li> <li><code>poetry up --no-install</code>を実行して poetry.lock を更新。私たちのモノレポでは 3 桁の <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%C9%A5%D1%A1%BC%A5%C6%A5%A3">サードパーティ</a>ライブラリに依存しており、毎回インストールを行うと時間がかかるため、<code>--no-install</code>をつけることで依存関係の更新だけ行い、意図しないインストールは行わない。</li> <li>pyproject.toml にはバージョンを記述していないため、<code>poetry export --without-hashes -f requirements.txt --output requirements.txt</code>を実行してバージョンが記載された requirements.txt を生成</li> <li>requirements.txt を Pants の依存関係に追加</li> </ol> <h3 id="pip-compile">pip-compile</h3> <p><code>pip-compile</code>は requirements.txt 形式、または PEP621 形式の pyproject.toml の依存関係を更新して requirements.txt に出力するツールです。<code>pip-compile</code>を使って更新する手順は次のように考えました。</p> <ol> <li>バージョンを指定せずに requirements.in にライブラリを記述。バージョンを指定しない理由は、poetry up の時と同様に新規追加時のバージョンの競合を避けるため。</li> <li><code>pip-compile --output-file requirements.txt requirements.in</code>を実行してライブラリのバージョンが記載された requirements.txt を生成</li> <li>requirements.txt を Pants の依存関係に追加</li> </ol> <p>これら 2 つの方法を検討した結果、依存関係のライブラリバージョンを管理するだけであれば<code>pip-compile</code>を使う方がシンプルにできそうだったためこちらを採用しました。</p> <p>requirements.in にバージョンを指定せずにライブラリを記述することで、pip-compile 時に依存関係が衝突しない範囲で最新のバージョンに更新されます。一部、バージョンが更新されるとテストや静的解析が失敗する場合においては、明示的にバージョンを指定して固定するようにしています。</p> <p>余談:依存関係を集約した当初だと <a class="wp-editor-md-post-content-link" href="https://rye-up.com/https://rye-up.com/">rye</a> がまだリリースされてませんでしたが、この記事を書きながら rye を使う方法も考えてみました。大まかな手順は poetry の時と同様になりますが、<code>--no-install</code>のようなオプションをつけなくてもデフォルトの挙動として依存関係をインストールしないのが良い点だと思います。rye の裏側で pip-compile を使っており、<code>rye lock</code>コマンドを使うと、他の手法と同様に requirements.txt 形式でバージョンが記載されたファイルが出力できます。このファイルを Pants の依存に加えれば依存の集約管理ができそうです。ただ、rye で pip-tools を消す<a class="wp-editor-md-post-content-link" href="https://github.com/mitsuhiko/rye/discussions/418">動き</a>があるので rye の機能を直接使わずに、pip-compile などを間に入れるのが良いのではないかと思います。私たちのモノレポではすでに requirements.in と pip-compile でうまくいってるため<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>内で rye を使う場面がありませんが <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> 環境構築を pyenv から rye に切り替えたりと他の場面で活用しています。</p> <h2 id="依存関係の集約">依存関係の集約</h2> <p>依存関係を集約した後の更新方法が決まったので、次は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>内の依存関係を集約していきます。<code>pants peek --filter-target-type=python_requirement ::</code>コマンドを実行すると<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>内の <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> ライブラリの依存関係を確認できます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/json">json</a> で出力されるので、次のように jq コマンドを使って整形し、<code>requirements.in</code>に記述します。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>pants peek <span class="synSpecial">--filter-target-type=python_requirement</span> :: | jq <span class="synStatement">'</span><span class="synConstant">[ .[].requirements ] | flatten | unique | .[]</span><span class="synStatement">'</span> <span class="synSpecial">-r</span> &amp;gt; requirements.in </pre> <p>集約した requirements.in にライブラリのバージョンが記載されていますが、先にも記載した通り新規追加時に依存のコンフリクトを避けるためにバージョンを削除します。その後、上記の pip-compile の手順を実行して依存関係の集約は完了です。</p> <p>依存管理を集約する前は手作業も多く、依存関係の更新作業に数時間かかることもありました。集約後はコマンド 1 つ実行すると数分で依存関係の更新ができ、Diamond Dependencies の問題を解決するための"One-Version Rule"も実現できました。今後は原則として<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>内で使われているライブラリは集約管理していきます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>全体で使ってるバージョンと異なるバージョンを使う必要がある場合は、これまでのように個別で管理もできるのでそのように対応する予定です。</p> <h1 id="pex-による-Python-環境のパッケージング">pex による <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> 環境のパッケージング</h1> <h2 id="pex-とは">pex とは</h2> <p>Pants は、<a class="wp-editor-md-post-content-link" href="https://pex.readthedocs.io/en/latest/whatispex.html">pex</a> 形式の <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> 仮想環境を構築できます。pex ファイルは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%C9%A5%D1%A1%BC%A5%C6%A5%A3">サードパーティ</a>のパッケージを含めた <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> 仮想環境を zip 形式でパッケージングした実行ファイルです。これにより、必要なライブラリや依存関係を含んだ環境を 1 つのファイルにまとめることが可能となります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%D7%A5%EA%A5%BF">インタープリタ</a>が存在する環境であれば、pex ファイルを実行することで、それぞれ独立した仮想環境上でコードが実行されます。また、pex の仮想環境内のパッケージのみを使用するか、システムの <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> 環境に存在するパッケージも利用するかは pex の設定で選ぶことができます。</p> <p>以下に pex の使用例を示します。まず、pex コマンドを実行する際に必要なパッケージを指定します。すると、指定したパッケージとその依存関係を含む pex ファイルが生成されます。生成された pex ファイルを実行すると、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%BF%A1%BC%A5%D7%A5%EA%A5%BF">インタープリタ</a>が起動し、ファイルに含まれるパッケージを使ってコードを実行できます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ pip install pex <span class="synComment"># pex をインストール</span> $ pex pydantic pip <span class="synSpecial">-o</span> demo.pex <span class="synComment"># pydantic と pip を含む pex ファイルを作成</span> $ ./demo.pex <span class="synSpecial">-m</span> pip list <span class="synComment"># pex ファイル内のパッケージを確認</span> Package Version ----------------- <span class="synSpecial">-------</span> annotated-types <span class="synConstant">0</span>.<span class="synConstant">5</span>.<span class="synConstant">0</span> pip <span class="synConstant">23</span>.<span class="synConstant">2</span>.<span class="synConstant">1</span> pydantic <span class="synConstant">2</span>.<span class="synConstant">3</span>.<span class="synConstant">0</span> pydantic_core <span class="synConstant">2</span>.<span class="synConstant">6</span>.<span class="synConstant">3</span> typing_extensions <span class="synConstant">4</span>.<span class="synConstant">7</span>.<span class="synConstant">1</span> $ ./demo.pex <span class="synComment"># pex ファイル内の Python インタプリタを実行</span> Python <span class="synConstant">3</span>.<span class="synConstant">11</span>.<span class="synConstant">3</span> <span class="synPreProc">(</span><span class="synSpecial">main, Apr </span><span class="synConstant">7</span><span class="synSpecial"> </span><span class="synConstant">2023</span><span class="synSpecial">, 20:13:31</span><span class="synPreProc">)</span> <span class="synStatement">[</span>Clang <span class="synConstant">14</span>.<span class="synConstant">0</span>.<span class="synConstant">0</span> (clang-1400.<span class="synConstant">0</span>.<span class="synConstant">29</span>.<span class="synConstant">202</span>)<span class="synStatement">]</span> on darwin Type <span class="synStatement">&quot;</span><span class="synConstant">help</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">copyright</span><span class="synStatement">&quot;</span>, <span class="synStatement">&quot;</span><span class="synConstant">credits</span><span class="synStatement">&quot;</span> or <span class="synStatement">&quot;</span><span class="synConstant">license</span><span class="synStatement">&quot;</span> <span class="synStatement">for</span> more information. <span class="synPreProc">(</span>InteractiveConsole<span class="synPreProc">)</span> <span class="synStatement">&amp;</span>gt<span class="synStatement">;&amp;</span>gt<span class="synStatement">;&amp;</span>gt<span class="synStatement">;</span> import pydantic <span class="synStatement">&amp;</span>gt<span class="synStatement">;&amp;</span>gt<span class="synStatement">;&amp;</span>gt<span class="synStatement">;</span> pydantic.VERSION <span class="synStatement">'</span><span class="synConstant">2.3.0</span><span class="synStatement">'</span> $ unzip <span class="synStatement">-l</span> ./demo.pex <span class="synComment"># pex ファイルの中身を確認</span> <span class="synComment"># 省略</span> </pre> <h2 id="Pants-による-pex-の構築">Pants による pex の構築</h2> <p>Pants を使って pex ファイルを構築すると、Pants が必要な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%C9%A5%D1%A1%BC%A5%C6%A5%A3">サードパーティ</a>のパッケージと自作のコードの依存関係を自動的に検知し pex ファイルを構築してくれます。どの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%C9%A5%D1%A1%BC%A5%C6%A5%A3">サードパーティ</a>パッケージと自作のコードを含めるかを、ほとんどの場合において明示的に指定する必要がありません。特定の場合には明示的な指定が必要になるかもしれませんが、基本的に Pants が最適な依存関係を推測してくれるので、開発者はシンプルで効率的なビルドプロセスを実現できます。</p> <p>Pants を使って pex を構築する例が次になります。main.py を実行すると、pydantic のバージョンが表示されます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># dir構成</span> . ├── BUILD ├── lib │ ├── BUILD │ └── foo.py ├── main.py └── pants.toml </pre> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment"># lib/foo.py</span> <span class="synPreProc">import</span> pydantic <span class="synStatement">def</span> <span class="synIdentifier">get_pydantic_version</span>(): <span class="synStatement">return</span> pydantic.VERSION </pre> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment"># main.py</span> <span class="synPreProc">from</span> lib.foo <span class="synPreProc">import</span> get_pydantic_version <span class="synStatement">if</span> __name__ == <span class="synConstant">'__main__'</span>: <span class="synIdentifier">print</span>(f<span class="synConstant">&quot;pydantic {get_pydantic_version()}&quot;</span>) </pre> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment"># lib/BUILD</span> python_requirement( name=<span class="synConstant">&quot;pydantic&quot;</span>, requirements=[<span class="synConstant">&quot;pydantic&quot;</span>] ) python_sources() </pre> <pre class="code lang-python" data-lang="python" data-unlink><span class="synComment"># BUILD</span> python_sources() pex_binary( name=<span class="synConstant">&quot;pex-demo&quot;</span>, entry_point=<span class="synConstant">&quot;main.py&quot;</span>, ) </pre> <pre class="code toml" data-lang="toml" data-unlink># pants.toml [GLOBAL] pants_version = &#34;2.17.0&#34; backend_packages = [ &#34;pants.backend.python&#34;, ]</pre> <pre class="code lang-sh" data-lang="sh" data-unlink>$ pants run main.py <span class="synComment"># pex ファイルを作成せずに実行</span> pydantic <span class="synConstant">2</span>.<span class="synConstant">3</span>.<span class="synConstant">0</span> $ pants package :pex-demo <span class="synComment"># pex ファイルを作成</span> 05:07:12.<span class="synConstant">53</span> <span class="synStatement">[</span>INFO<span class="synStatement">]</span> Completed: Building pex-demo.pex with <span class="synConstant">1</span> requirement: pydantic 05:07:12.<span class="synConstant">53</span> <span class="synStatement">[</span>INFO<span class="synStatement">]</span> Wrote dist/pex-demo.pex $ ./dist/pex-demo.pex <span class="synComment"># pex ファイルの実行</span> pydantic <span class="synConstant">2</span>.<span class="synConstant">3</span>.<span class="synConstant">0</span> </pre> <p>このように Pants を使って pex ファイルを構築することで、Pants が依存関係を自動的に検知して pex ファイルにパッケージングしてくれます。パッケージングの際に開発者は自作のコードと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%C9%A5%D1%A1%BC%A5%C6%A5%A3">サードパーティ</a>のパッケージの依存を意識する必要がありません。上記の例では、main.py から lib/foo.py をインポートしています。lib/foo.py からインポートされている pydantic が自動的に検知され、これらが pex ファイルに含まれています。</p> <h2 id="pex-を用いたコンテナイメージの構築">pex を用いたコンテナイメージの構築</h2> <p>pex を使うとポータビリティが高まり、コンテナイメージの構築もシンプルになります。pex を使わない場合、コードを参照するために Dockerfile 内で各ファイルや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを都度 COPY する必要があります。依存するファイルが増えるほど、Dockerfile の記述が大変になります。以下にその例を示します。</p> <pre class="code docker" data-lang="docker" data-unlink>FROM python:3.11-slim COPY requirements.txt . RUN pip install pydantic==2.3.0 COPY lib/foo.py lib/foo.py # 依存するファイルが増えるほど COPY の記述が大変になる COPY main.py main.py CMD [&#34;python&#34;, &#34;main.py&#34;]</pre> <p>一方、pex ファイルを使用する場合はそのファイルを COPY するだけでアプリケーションを実行できます。これにより、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>内の様々な場所から関連するコードを集める作業が不要となり、Dockerfile の記述をシンプルにできます。</p> <pre class="code docker" data-lang="docker" data-unlink>FROM python:3.11-slim COPY pex-demo.pex pex-demo.pex CMD [&#34;./pex-demo.pex&#34;]</pre> <p>pex をコンテナ環境で使用することで、ポータビリティを向上させるだけでなく、コンテナイメージのサイズの削減がしやすくなります。イメージサイズを小さくすることで、コールドスタートの時間を短縮したり、スケールアウトの速度を改善できます。コンテナイメージを構築する際にイメージサイズを小さくする tips は様々あります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> では、依存ライブラリのインストールのキャッシュを無効化・削除したり、multi-stage build で venv を COPY するなどしてイメージサイズを小さくできます。pex を使うと、これらの知識を必要とせずにイメージサイズを小さくできます。<a class="wp-editor-md-post-content-link" href="https://medium.com/@kosala.atapattu/docker-build-for-python-abd7f91d4896">こちらの記事</a>では venv を使った multi-stage build 時のイメージサイズと pex を使った時のイメージサイズが比較しており、pex の利用でコンテナイメージを小さく保てることがわかります。</p> <table> <thead> <tr> <th align="left"></th> <th align="left">image size</th> </tr> </thead> <tbody> <tr> <td align="left">multi-stage build なし・キャッシュ削除なし</td> <td align="left">1.29GB</td> </tr> <tr> <td align="left">venv を使った multi-stage build</td> <td align="left">66MB</td> </tr> <tr> <td align="left">pex</td> <td align="left">47.2MB</td> </tr> </tbody> </table> <p>引用: <a class="wp-editor-md-post-content-link" href="https://medium.com/@kosala.atapattu/docker-build-for-python-abd7f91d4896">Docker build for Python</a></p> <p>実際にチームでは、pex を用いたコンテナイメージを<a class="wp-editor-md-post-content-link" href="https://cloud.google.com/vertex-ai/docs/pipelines">Vertex AI Pipeline</a>で構築した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>パイプラインや<a class="wp-editor-md-post-content-link" href="https://cloud.google.com/vertex-ai/docs/predictions/custom-container-requirements">Vertex AI Endpoint</a>のサービングの場面で積極的に活用しています。</p> <h1 id="プロジェクトの依存関係を制御するPants-の-visibility-機能の活用">プロジェクトの依存関係を制御する:Pants の visibility 機能の活用</h1> <p>コードベース内で不適切な依存関係が形成されるとコードの修正や追加が難しくなったり、バグの原因になります。この章では依存関係を適切に管理するために役立つ、Pants の visibility 機能についてご紹介します。</p> <p><a class="wp-editor-md-post-content-link" href="https://www.pantsbuild.org/docs/validating-dependencies">Pants の visibility</a>は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> やモジュールの依存許可や禁止をコン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%ED%A1%BC%A5%EB">トロール</a>する機能です。これを活用することで、特定の <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a> やモジュールを他のコードから隠蔽したり、公開範囲を制限することが可能となります。これにより、意図しない依存関係の形成を防止できます。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%AB%C8%AF%A5%D7%A5%ED%A5%BB%A5%B9">開発プロセス</a>の中には、一時的な実装のためのコード(以下「sandbox コード」と呼びます)を作成する場面があります。これらの sandbox コードが参照されると、そのコードの改変や廃止が困難になったり、バグの原因になることがあります。</p> <p>このような問題を未然に防ぐため、私たちのモノレポでは「他からの依存禁止ルール」を適用した sandbox <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リを設け、一時的なコードや試験的な実装をそこに配置しています。これにより、sandbox <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ内のコードは他のコードから切り離され、安全に共有・開発することが可能となります。</p> <p>依存禁止ルールが適用された<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リのコードを参照しようとすると、次のようなエラーメッセージが表示され、ルールが適用されてることが確認できます。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>$ pants <span class="synStatement">test</span> projects/foo/:: 10:57:00.<span class="synConstant">07</span> <span class="synStatement">[</span>INFO<span class="synStatement">]</span> Initialization options changed: reinitializing scheduler... 10:57:15.<span class="synConstant">32</span> <span class="synStatement">[</span>INFO<span class="synStatement">]</span> Scheduler initialized. 10:57:19.<span class="synConstant">34</span> <span class="synStatement">[</span>ERROR<span class="synStatement">]</span> <span class="synConstant">1</span> Exception encountered: Engine traceback: <span class="synError">in</span> <span class="synSpecial">`</span><span class="synStatement">test</span><span class="synSpecial">`</span> goal DependencyRuleActionDeniedError: projects/foo/tests/test_main.py has <span class="synConstant">1</span> dependency violation: * BUILD<span class="synStatement">[!</span>projects/sandbox/**<span class="synStatement">]</span> -&amp;gt; projects/sandbox : DENY python_tests projects/foo/tests/test_main.py -&amp;gt; python_sources projects/sandbox:src </pre> <p>コードベースで何でもかんでも公開して参照できる状態すると、知らぬ間に複雑な依存関係が形成されてメンテナンスが大変になることが想定できます。『<a class="wp-editor-md-post-content-link" href="https://abseil.io/resources/swe-book/html/toc.html">Software Engineering at Google</a>』の書籍にも書かれているように公開するターゲットを最小限に止め、依存関係が複雑にならないように心がけています。</p> <h1 id="まとめ">まとめ</h1> <p>ここまでモノレポのメリットを活かしながら、開発効率を高めるための様々な取り組みについて紹介しました。Pants による依存関係管理、pex ファイルの活用、CI の待ち時間の短縮、依存関係の集約管理による効率化を行いました。これらの取り組みによって、モノレポでの開発効率を高めることができました。今後もモノレポの改善を続けながら、事業に素早く貢献できるようにしていきます。</p> <h1 id="link">link</h1> <p><a class="wp-editor-md-post-content-link" href="https://youtrust.jp/recruitment_posts/c31e364b37422c90fd98263e14bbbb6e"> YOUTRUST</a>(9/25 まで) <a class="wp-editor-md-post-content-link" href="https://recruit.caddi.tech/">CADDi Tech</a> <a class="wp-editor-md-post-content-link" href="https://open.talentio.com/r/1/c/caddi-jp-recruit/pages/79797">機械学習エンジニア求人</a></p> caddi-tech 第4回:TerraformとGitHub Actionsで構築するインフラCI hatenablog://entry/6801883189081010381 2023-08-30T15:00:09+09:00 2024-02-08T17:35:38+09:00 ※本記事は、技術評論社「Software Design」(2023年7月号)に寄稿した連載記事「Google Cloudで実践するSREプラクティス」からの転載です。発行元からの許可を得て掲載しております。 はじめに 前回はTerraformの基本的な概念とステート管理について解説しました。 今回からは 2 回にわたり、Infrastructure as Code(IaC)のCI/CD(継続的インテグレーション/継続的デリバリ)パイプラインについて紹介します(図1)。 ▼図1 CADDiのスタックにおける今回の位置付け Google Cloudプロジェクトのインフラ構成をTerraformで定… <p>※本記事は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%BB%BD%D1%C9%BE%CF%C0%BC%D2">技術評論社</a><a class="wp-editor-md-post-content-link" href="https://gihyo.jp/magazine/SD/archive/2023/202307">「Software Design」(2023年7月号)</a>に寄稿した連載記事「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudで実践するSREプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティス」からの転載です。発行元からの許可を得て掲載しております。</p> <h2 id="はじめに">はじめに</h2> <p><a class="wp-editor-md-post-content-link" href="https://caddi.tech/?p=4320&amp;preview=true">前回</a>はTerraformの基本的な概念とステート管理について解説しました。 今回からは 2 回にわたり、Infrastructure as Code(IaC)のCI/CD(<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B7%D1%C2%B3%C5%AA%A5%A4%A5%F3%A5%C6%A5%B0%A5%EC%A1%BC%A5%B7%A5%E7%A5%F3">継続的インテグレーション</a>/継続的デリバリ)パイプラインについて紹介します(図1)。</p> <p>▼図1 CADDiのスタックにおける今回の位置付け <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173452.png" alt="" /></p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudプロジェクトのインフラ構成をTerraformで定義し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsでデプロイするまでを目標とし、前半となる今回は事前備とインフラCIについて焦点を当てていきます。後半となる次回では、インフラCDについて触れつつ、運用をよりスケールさせるためにキャディで取り組んでいる事例について紹介予定です。</p> <h2 id="IaCとCICD">IaCとCI/CD</h2> <p>本連載第2回(本誌2023年5月号)では、IaC化によってLinterによる自動チェックや再現性の担保など、さまざまなメリットが得られることを解説しました。今回はIaCの管理・デプロイについて考えてみましょう。 一般に、作業者のPCなどローカル環境でTerraformを実行するときには、次のような課題が生じます。</p> <ul> <li>作業者以外が自動チェックの実行結果を確認できない</li> <li>実行に必要なシークレットが作業者PCに保管されてしまう</li> <li>誰がいつデプロイしたのか証跡を残しづらい</li> <li>Terraform plan/applyの実行ログが残らない</li> <li>手動オペレーションのため作業者のリソースに依存してしまう</li> </ul> <p>アプリケーション開発の領域において、CI/CDはすでに一般的なプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスとなっています。Pull request(PR)に対してLinterやテストを実行し、問題がなければmainブランチへマージ、アプリケーションコンテナのビルド、デプロイが自動実行されます。 インフラ領域においても、IaC化することで、このプロセスを実現できます。また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsなどを活用してCI/CDパイプラインを構築することで、先ほどの課題を解消・軽減できます。</p> <h2 id="GitHub-Actions概要"><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions概要</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsは、タスク実行やワークフローを自動化するCI/CDサービスです。<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>でホストされている<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>であれば設定ファイルを設置するだけで利用でき、セットアップも容易です。 ここでは、本連載を理解する上で必要となる知識について簡単に解説します。 理解をより深めたい方は、公式ドキュメント<sup id="fnref-4385-1"><a href="#fn-4385-1" class="footnote-ref" role="doc-noteref">1</a></sup>や本誌のバックナンバー<a class="keyword" href="https://d.hatena.ne.jp/keyword/Software%20Design">Software Design</a> 2022年2月号<sup id="fnref-4385-2"><a href="#fn-4385-2" class="footnote-ref" role="doc-noteref">2</a></sup>を参考にしてください。 ワークフローは<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>内の<code>.github/workflows</code><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ配下に<a class="keyword" href="https://d.hatena.ne.jp/keyword/YAML">YAML</a>形式のファイルとして定義します。これらが、記述内容に従ってプッシュなど特定のイベントをトリガーに実行されます。 リスト1はPRをトリガーとしてTerraform組み込みのフォーマッタである<code>terraform fmt</code>を実行する例です。 以下はPRをトリガーとしてTerraform組み込みのフォーマッタである を実行する例です。</p> <p>▼リスト1 .<a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a>/workflows/terraform_ci.yml</p> <pre><code class="line-numbers">name: Terraform CI on: pull_request: branches: - main jobs: tf_version: runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@v3 - uses: hashicorp/setup-terraform@v2 - run: terraform fmt -check -no-color -recursive working-directory: ./terraform </code></pre> <p>ワークフローは名前(<code>name</code>)、トリガー(<code>on</code>)、ジョブ(<code>job</code>)などから構成されます。 onではワークフローをトリガーするイベントを定義します。ここではmainブランチに対するPR関連のアクティビティをトリガー条件としています。 ジョブは、処理タスクを表現する複数のステップから構成されます。ステップでは、shellのコマンド実行や、「アクション」と呼ばれる再利用可能な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>が利用できます。 リスト1の例では<code>actions/checkout</code><sup id="fnref-4385-3"><a href="#fn-4385-3" class="footnote-ref" role="doc-noteref">3</a></sup>でPR元ブランチのコードをワークフローランナー上に展開し、<code>hashicorp/setup-terraform</code><sup id="fnref-4385-4"><a href="#fn-4385-4" class="footnote-ref" role="doc-noteref">4</a></sup>でterraformコマンドを利用するためのセットアップを実施しています。 最後に<code>terraform fmt</code>コマンドを実行しフォーマットのチェックを行っています。 このように、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsでは、アクションを活用しつつ、CI/CDパイプラインで実施すべき処理を<a class="keyword" href="https://d.hatena.ne.jp/keyword/YAML">YAML</a>ファイルに記述します。雰囲気をつかんでいただけたでしょうか。</p> <h2 id="事前準備">事前準備</h2> <p>ここからは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudも含めたCI/CDパイプラインの具体的な構築方法を解説していきます。まずはワークフロー実行に必要なリソースを事前に作成します。 はじめに、Terraformのtfstateを保管するためのStorage<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%B1%A5%C3%A5%C8">バケット</a>を作成します(図2)。前回でも紹介したように、ステートが複数環境から参照されるときには、tfstateをローカルではなくリモートのオブジェクトストレージなどに保管する必要があります。</p> <p>▼図2 tfstate用<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%B1%A5%C3%A5%C8">バケット</a>作成</p> <pre><code class="line-numbers">$ gcloud storage buckets create \ gs://my-tfstate-dev-${RANDOM} \ --location=asia-northeast1 \ --uniform-bucket-level-access </code></pre> <p>なお、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud Storageの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%B1%A5%C3%A5%C8">バケット</a>名は、グローバルに一意でなくてはならないため、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%B1%A5%C3%A5%C8">バケット</a>名にランダム値を追加しています。 次に、Terraformを実行するためのサービスアカウントを作成し、必要な権限を付与します(図3)。<code>${PROJECT_ID}</code>は対象の<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> CloudプロジェクトIDに置き換えてください。今回は編集者ロールを付与しますが、実際は必要に応じた最小限の権限とするのが適切です。</p> <p>▼図3 サービスアカウント作成</p> <pre><code class="line-numbers">$ gcloud iam service-accounts create my-github-actions $ gcloud projects add-iam-policy-binding ${PROJECT_ID} \ --member="serviceAccount:my-github-actions@${PROJECT_ID}.iam.gserviceaccount.com" \ --role="roles/editor" </code></pre> <p>最後に、サンプルのtfファイル(リスト2、3)を用意します。<a class="keyword" href="https://d.hatena.ne.jp/keyword/VPC">VPC</a>ネットワークを構築済みの既存プロジェクトに対してCompute<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を作成します。のちの説明のために、対象のプロジェクトは便宜上開発(dev)環境として扱います。</p> <p>▼リスト2 terraform/modules/<a class="keyword" href="https://d.hatena.ne.jp/keyword/vm">vm</a>/main.tf</p> <pre><code class="line-numbers">variable "name" { type = string } resource "google_compute_instance" "default" { name = var.name machine_type = "e2-micro" boot_disk { initialize_params { image = "debian-cloud/debian-11" size = 10 } } network_interface { network = "default" } } </code></pre> <p>▼リスト3 terraform/environments/dev/main.tf</p> <pre><code class="line-numbers">terraform { backend "gcs" { bucket = "my-tfstate-dev-&lt;&lt;SUFFIX&gt;&gt;" } } provider "google" { project = "&lt;&lt;プロジェクトID&gt;&gt;" region = "asia-northeast1" zone = "asia-northeast1-b" } module "vm" { source = "../../modules/vm" # ① name = "my-vm" } </code></pre> <p>再利用しやすくするためCompute<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>のリソース定義はモジュール化しています。 モジュールは複数のtfファイルを含んだ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リで、1のようにモジュール<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リのパスを指定して利用します。 このtfファイルを利用して<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions上でTerraformを実行してみましょう。</p> <h2 id="Column-IaC-するものしないもの">[Column] IaC するもの/しないもの</h2> <p>IaCは再現性や監査など多く点でメリットがあります。しかし、手動オペレーションをすべて禁止してしまうと、かえって作業が煩雑になったりセキュリティリスクが高まったりするケースもあります。そのような場合には、柔軟に手動オペレーションを許容することも選択肢の1つです。 キャディでは、実施頻度が低く強い権限を要する一部の作業は手動で行っています。<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> CloudではプロジェクトやCloud <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>について、IaCの可否を事前に検討しておけると後の管理がスムーズになります。 IaC化しているリソース、IaC化していないリソースはドキュメントで明文化しておくことをお勧めします。明文化しておくことで開発者が意図せずリソースを手動で編集してしまい、IaCで定義した状態から乖かい離してしまうといった事故の予防につながります。</p> <h2 id="認証方法">認証方法</h2> <p>Terraformから<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud上のリソースを操作するには、認証が必要です。<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsでは<code>google-github-actions/auth</code><sup id="fnref-4385-5"><a href="#fn-4385-5" class="footnote-ref" role="doc-noteref">5</a></sup>アクションが次の2種類の認証方法を提供しています。</p> <p>・サービスアカウントキーを利用する方法 ・Workload Identity連携を利用する方法</p> <h3 id="サービスアカウントキーを利用する方法">サービスアカウントキーを利用する方法</h3> <p>サービスアカウントのキーファイルを生成し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsのシークレットへ登録します(図4)。シークレットは機密性の高いデータを管理するための機能で、ワークフローからは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B4%C4%B6%AD%CA%D1%BF%F4">環境変数</a>として参照できます(図5)。</p> <p>▼図4 サービスアカウントキー作成</p> <pre><code class="line-numbers">$ gcloud iam service-accounts keys create gsa-key.json \ --iam-account=my-github-actions@${PROJECT_ID}.iam.gserviceaccount.com </code></pre> <p>▼図5 Actionsシークレット <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173503.png" alt="" /></p> <p>ワークフローでは、 <code>credentials_json</code> でアクションに対してサービスアカウントキーを与え、認証します(リスト4)。<code>${{ secrets.DEV_GCP_SA_KEY }}</code>が、<code>DEV_GCP_SA_KEY</code>という名前で登録したシークレットの内容を参照する部分です。</p> <p>▼リスト4 サービスアカウントキーでの認証</p> <pre><code class="line-numbers">- uses: google-github-actions/auth@v1 with: credentials_json: '${{ secrets.DEV_GCP_SA_KEY }}' </code></pre> <p>この方法はシンプルですが、セキュリティ面で好ましい方法ではありません。このキーは有効期限がなく、漏洩した際にはキーを使用している全環境でローテーション作業が発生します。 そこで、よりセキュアな手法として、Workload Identity連携を利用した方式が推奨されています。</p> <h3 id="Workload-Identity連携-を利用する方法">Workload Identity連携 を利用する方法</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud の Workload Identity 連携は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>など外部IDプロバイダ(IdP)の認証情報をもとに、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudリソースへのアクセス制御を行うサービスです。外部IdPで発行された<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを検証し、対象サービスアカウントの権限を借用することで、サービスアカウントキーを使用することなく認証ができます(図6)。</p> <p>▼図6 Workload Identity連携イメージ <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173535.png" alt="" /></p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions は <a class="keyword" href="https://d.hatena.ne.jp/keyword/OpenID">OpenID</a> Connect(OIDC)<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを利用した認証をサポートしているので、ワークフローで短命なOIDC<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを生成し、ID連携に利用できます。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsは<a class="keyword" href="https://d.hatena.ne.jp/keyword/OpenID">OpenID</a> Connect(OIDC)<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを利用した認証をサポートしているので、ワークフローで短命なOIDC<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを生成し、ID連携に利用できます。<sup id="fnref-4385-6"><a href="#fn-4385-6" class="footnote-ref" role="doc-noteref">6</a></sup> 先ほどのサービスアカウントキーと異なりOIDC<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンは数時間程度で失効するため、セキュリティリスクを軽減できます。 Workload Identity連携はプールとプロバイダから構成されます。プールは外部IdPにより発行されたIDを管理し、プロバイダは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudと外部IdPにおける属性情報の対応を管理します。 まず、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsで利用するプールとプロバイダを作成します(図7、8)。</p> <p>▼図7 プールの作成</p> <pre><code class="line-numbers">$ gcloud iam workload-identity-pools \ create my-github-actions \ --location=global </code></pre> <p>▼図8 プロバイダの作成</p> <pre><code class="line-numbers">$ gcloud iam workload-identity-pools providers create-oidc github-provider \ --location=global \ --workload-identity-pool=my-github-actions \ --issuer-uri=https://token.actions.githubusercontent.com \ --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.aud=assertion.aud,attribute.repository=assertion.repository" </code></pre> <p>これでOIDC<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを検証する準備ができました。</p> <p>次に、<code>my-github-actions</code> Workload Identityユーザーロールをmy-<a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a>-actionsアカウントに付与し、外部からのアカウント権限借用を許可します(図9)。</p> <p>▼図9 サービスアカウントの権限借用許可</p> <pre><code class="line-numbers">$ gcloud iam service-accounts add-iam-policy-binding my-github-actions@${PROJECT_ID}.iam.gserviceaccount.com \ --role=roles/iam.workloadIdentityUser \ --member="principalSet://iam.googleapis.com/projects/${PROJECT_NUM}/locations/global/workloadIdentityPools/my-github-actions/*" </code></pre> <p>これで、ワークフローから<code>my-github-actions</code> サービスアカウントを使用して<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudリソースを操作できます。 ワークフローではリスト5のように記述します。</p> <p>▼リスト5 Workload Identity連携での認証</p> <pre><code class="line-numbers">- uses: google-github-actions/auth@v1 with: workload_identity_provider: projects/(…略…)/providers/github-provider service_account: my-github-actions@(…略…).com </code></pre> <h2 id="Terraform-planの実行">Terraform planの実行</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> ActionsでTerraformを実行するための準備が整いました。PRに対して<code>terraform plan</code>を実行し、どのようなリソース変更が予定されているのかチェックしてみましょう。 plan結果はActionsの実行ログから参照できますが、PRコメントとして投稿されるとレビュー体験がより良くなります。リスト6はPRに対して<code>terraform plan</code>を実行し、plan結果をPRコメントとして投稿するワークフロー定義です。</p> <p>▼リスト6 .<a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a>/workflows/terraform_ci.yml</p> <pre><code class="line-numbers">name: Terraform CI on: pull_request: branches: - main jobs: terraform_ci: runs-on: ubuntu-latest permissions: # ① contents: read id-token: write pull-requests: write steps: - uses: actions/checkout@v3 - uses: hashicorp/setup-terraform@v2 - uses: google-github-actions/auth@v1 with: workload_identity_provider: projects/(…略…)/providers/github-provider service_account: my-github-actions@(…略…).com - run: terraform init working-directory: ./terraform/environments/dev - id: plan run: terraform plan -no-color working-directory: ./terraform/environments/dev continue-on-error: true - uses: actions/github-script@v6 # ② with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const output = `Terraform Plan: \`${{ steps.plan.outcome }}\` &lt;details&gt;&lt;summary&gt;Show plan&lt;/summary&gt; \`\`\` ${{ steps.plan.outputs.stdout }} \`\`\` &lt;/details&gt;` github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: output }) </code></pre> <p>①:このワークフローではOIDC<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンの生成やコメント投稿を行うため、<code>permission</code>で対応する権限を付与します。 ②:plan 結果のコメント投稿には<code>actions/github-script</code><sup id="fnref-4385-7"><a href="#fn-4385-7" class="footnote-ref" role="doc-noteref">7</a></sup>を利用します。これは<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を用いた処理を<a class="keyword" href="https://d.hatena.ne.jp/keyword/JavaScript">JavaScript</a>で記述できるアクションです。<code>hashicorp/setup-terraform</code><sup id="fnref-4385-8"><a href="#fn-4385-8" class="footnote-ref" role="doc-noteref">8</a></sup>アクションのREADMEには<a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a>-scriptのサンプルも記載されていますので、参考にしてください。 サンプルのtfファイルを含め、現状<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>は図10のファイル構成となっています。</p> <p>▼図10 プロジェクトIaC<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の構成</p> <pre><code class="line-numbers">|-- .github | `-- workflows | `-- terraform_ci.yml `-- terraform |-- environments | `-- dev | `-- main.tf `-- modules `-- vm `-- maint.tf </code></pre> <p>これらのファイルをコミットし、PRを作成するとワークフローが実行され、<code>terraform plan</code>の結果がコメントに投稿されます。</p> <p>▼図11 plan結果のコメント投稿</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173529.png" alt="" /></p> <p>今回は<code>terraform plan</code>のみですが、コードの品質を高めるためにバリデーション(<code>terraform validate</code>)やフォーマット(<code>terraform fmt</code>)も実行すると良いでしょうtfsec<sup id="fnref-4385-9"><a href="#fn-4385-9" class="footnote-ref" role="doc-noteref">9</a></sup>などtfファイルを静的解析し、セキュリティリスクのあるインフラ構成を検知できる<a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a>もあります。 なお、コメント投稿は <code>actions/github-script</code>を利用しましたが、より見やすい形でコメントするアクションも<a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a>で公開されています<sup id="fnref-4385-10"><a href="#fn-4385-10" class="footnote-ref" role="doc-noteref">10</a></sup>。</p> <h2 id="おわりに">おわりに</h2> <p>今回はTerraformと<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsを組み合わせたIaCのCIパイプラインについて紹介しました。実運用する際には開発用・商用など複数環境対応や複数プロジェクト管理についても考慮が必要です。 次回はIaCのCDパイプラインに触れつつ、運用をスケールさせるキャディでの取り組みを紹介します。</p> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn-4385-1" role="doc-endnote"> <a href="https://docs.github.com/ja/actions/using-workflows/workflow-syntax-for-github-actions ">https://docs.github.com/ja/actions/using-workflows/workflow-syntax-for-github-actions </a><a href="#fnref-4385-1" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4385-2" role="doc-endnote"> 本誌2022年2月号第2特集「<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsで簡単・快適 CI/CD」&#160;<a href="#fnref-4385-2" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4385-3" role="doc-endnote"> <a href="https://github.com/marketplace/actions/checkout ">https://github.com/marketplace/actions/checkout </a><a href="#fnref-4385-3" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4385-4" role="doc-endnote"> <a href="https://github.com/marketplace/actions/hashicorp-setup-terraform ">https://github.com/marketplace/actions/hashicorp-setup-terraform </a><a href="#fnref-4385-4" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4385-5" role="doc-endnote"> <a href="https://github.com/marketplace/actions/authenticate-to-google-cloud ">https://github.com/marketplace/actions/authenticate-to-google-cloud </a><a href="#fnref-4385-5" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4385-6" role="doc-endnote"> <a href="https://cloud.google.com/blog/ja/products/identity-security/enabling-keyless-authentication-from-github-actions ">https://cloud.google.com/blog/ja/products/identity-security/enabling-keyless-authentication-from-github-actions </a><a href="#fnref-4385-6" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4385-7" role="doc-endnote"> <a href="https://github.com/marketplace/actions/github-script ">https://github.com/marketplace/actions/github-script </a><a href="#fnref-4385-7" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4385-8" role="doc-endnote"> <a href="https://github.com/hashicorp/setup-terraform ">https://github.com/hashicorp/setup-terraform </a><a href="#fnref-4385-8" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4385-9" role="doc-endnote"> <a href="https://github.com/aquasecurity/tfsec ">https://github.com/aquasecurity/tfsec </a><a href="#fnref-4385-9" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4385-10" role="doc-endnote"> <a href="https://github.com/suzuki-shunsuke/tfcmt ">https://github.com/suzuki-shunsuke/tfcmt </a><a href="#fnref-4385-10" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> </ol> </div> caddi-tech mypy 設定ファイルの読み合わせと修正を実施しました hatenablog://entry/6801883189081010341 2023-08-25T18:48:13+09:00 2024-02-06T13:59:18+09:00 こんにちは MLエンジニアとMLOpsエンジニアを兼任している押条です。 CADDi の機械学習プロジェクトでは Python がよく使われています。 本番環境にリリースするコードのlinter や formatter、test に関しては、 CI で自動的に適用し運用できていました。 一方、型注釈については開発担当者に一任されており、プロジェクトごとにあるものとないものと混在していました。 採用を強化してチームを拡大していこうという動きもあり、開発に携わる人が増える見込みがあります。 そのような背景から、型注釈による既存コードの可読性の向上の重要性が増してきました。 そこで現行の mypy … <p>こんにちは MLエンジニアとMLOpsエンジニアを兼任している押条です。</p> <p>CADDi の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>プロジェクトでは <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> がよく使われています。 本番環境にリリースするコードのlinter や formatter、test に関しては、 CI で自動的に適用し運用できていました。 一方、型注釈については開発担当者に一任されており、プロジェクトごとにあるものとないものと混在していました。 採用を強化してチームを拡大していこうという動きもあり、開発に携わる人が増える見込みがあります。 そのような背景から、型注釈による既存コードの可読性の向上の重要性が増してきました。</p> <p>そこで現行の mypy の設定の読み合わせと mypy 設定の修正を実施しました。 その時の議論の内容と修正後の設定をご紹介しようと思います。</p> <h3 id="目次">目次</h3> <ul> <li>現行ルールがどのようなものか認識合わせする</li> <li>現在のプロダクションコードを参照しながらルールのスコープを決める</li> <li>修正後の設定ファイルの紹介</li> <li>おわりに</li> </ul> <h3 id="現行ルールがどのようなものか認識合わせする">現行ルールがどのようなものか認識合わせする</h3> <p>まずは現在使用している mypy の設定ファイルの読み合わせをしました。 この設定は MLOps チームのテッ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%EA%A1%BC%A5%C9">クリード</a>が厳しめに設定してくれたものでした。 ChatGPT に入力して、説明してもらったのが下記です。</p> <pre><code class="line-numbers">この設定は、`mypy`に関する多くのオプションを含んでいます。各オプションについて簡単に説明し、必要に応じて改善の提案を行います。 1. `allow_redefinition = False`: 同じスコープ内での変数の再定義を禁止します。通常、このような再定義はエラーの原因となります。 2. `allow_untyped_globals = False`: 型注釈がないグローバル変数の使用を禁止します。型情報が不足している場合、予期しないエラーが生じる可能性があります。 3. `check_untyped_defs = True`: 型注釈がない関数やメソッドに対して型チェックを行います。 4. `color_output = True`: エラーメッセージに色を付けて表示します。 5. `disallow_incomplete_defs = True`: 型注釈の一部が欠けている関数やメソッドの定義を禁止します。 6. `disallow_untyped_defs = True`: 型注釈がない関数やメソッドの定義を禁止します。 7. `disallow_untyped_calls = True`: 型注釈がない関数やメソッドの呼び出しを禁止します。 8. `disallow_untyped_decorators = True`: 型注釈がないデコレータの使用を禁止します。 9. `error_summary = True`: エラーメッセージの概要を表示します。 10. `ignore_missing_imports = True`: 型情報がないモジュールのインポートを無視します。多くのサードパーティライブラリは型情報を持っていないため、このオプションが有効になっています。 11. `implicit_reexport = False`: 型注釈がない関数やメソッドの再エクスポートを禁止します。 12. `no_implicit_optional = True`: Optionalの型を明示的に宣言する必要があります。 13. `pretty = True`: エラーメッセージを見やすくフォーマットします。 14. `show_column_numbers = True`: エラーの発生した列番号を表示します。 15. `show_error_codes = True`: エラーコードを表示します。これは、特定のエラーを無視するための設定を行う際に便利です。 16. `show_error_context = True`: エラーメッセージのコンテキストを表示します。 17. `show_traceback = True`: mypyがクラッシュした場合にトレースバックを表示します。 18. `strict = True`: すべてのstrictモード関連のオプションを有効にします。このオプションが有効になっている場合、以下のオプションも自動的に有効になります。 19. `strict_equality = True`: 異なる型同士の等価性の比較を禁止します。 20. `strict_optional = True`: Noneと他の型の混在を許容しないようにします。 21. `warn_no_return = True`: 戻り値がない関数やメソッドでのreturn文の省略に警告します。 22. `warn_redundant_casts = True`: 不要な型変換に警告します。 23. `warn_return_any = True`: `Any`型を返す関数やメソッドに警告します。 24. `warn_unreachable = True`: 到達不能なコードに警告します。 25. `warn_unused_configs = True`: 使用されていないconfigオプションに警告します。 26. `warn_unused_ignores = True`: 使用されていない`# type: ignore`コメントに警告します。 **改善の提案**: 1. `ignore_missing_imports = True`: これはグローバルに適用されるため、可能ならば特定のライブラリにのみこのオプションを適用することをおすすめします。全体的な型の厳格性が低下する可能性があります。 2. `strict = True`: このオプションは多くの他のstrictモード関連のオプションを有効にするため、個別に指定したオプション(例: `strict_equality`, `strict_optional` など)は削除しても良いです。 全体的に、この設定は非常に厳格な型チェックを行うように設定されており、コードの品質を向上させるのに役立ちます。ただし、非常に厳格であるため、既存のコードベースでこれらの設定を有効にすると、多くのエラーが発生する可能性があります。新しいプロジェクトや、既存のプロジェクトでの大規模なリファクタリングを行う際には、これらの設定を検討すると良いでしょう。 warn_redundant_casts = True warn_return_any = True warn_unreachable = True warn_unused_configs = True warn_unused_ignores = True </code></pre> <p>各オプションの役割が何となくわかるようになりました。 prefix が show<em> のオプションや warning</em> のオプションのほとんどについては、True で問題なさそうです。 それ以外のオプションについて、1行ずつ確認して必要があれば議論も実施しました。</p> <h3 id="現在のプロダクションコードを参照しながらルールのスコープを決める">現在のプロダクションコードを参照しながらルールのスコープを決める</h3> <p>オプションの確認を行うにあたって、現在のプロダクションコードに対して無理なく適用できそうかどうかを意識して取り組みました。</p> <p>それでは順番に見ていきましょう。</p> <ol> <li>allow_redefinition</li> </ol> <p>こちらは変数の再定義に関するオプションです。</p> <p>画像解析の <a class="keyword" href="https://d.hatena.ne.jp/keyword/Deep%20Learning">Deep Learning</a> モデル開発の現場では、このようなコードが頻繁に見られると思います。</p> <pre><code class="language-python line-numbers">image : PIL.Image = PIL.Image.open('path_to_image.png') image : np.array = np.array(image) image : torch.Tensor = torch.from_array(image) </code></pre> <p>再定義が許容されない場合以下のようなコードになりそうです。</p> <pre><code class="language-python line-numbers">image : PIL.Image = PIL.Image.open('path_to_image.png') image_arr : np.array = np.array(image) image_tensor : torch.Tensor = torch.from_array(image_arr) </code></pre> <p>リーダブルコードにも記述があるように変数には適切な名前をつけるのが望ましいですが、幾分冗長に思えます。</p> <p>後述する設定項目によって、私たちはできるだけ型注釈を省略せずコードを書こうとしています。 その場合、変数に型情報が紐づいているため、変数名に型の情報を持たせなくて良くなります。</p> <p>redefinition を許可しても可読性が損なわれることはないと判断し、<code>allow_redefinition = True</code> としました。</p> <ol start="2"> <li>allow_untyped_globals</li> </ol> <p>こちらは 型注釈のない<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B0%A5%ED%A1%BC%A5%D0%A5%EB%CA%D1%BF%F4">グローバル変数</a>を許容するかどうかに関するオプションです。</p> <p>そもそも <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B0%A5%ED%A1%BC%A5%D0%A5%EB%CA%D1%BF%F4">グローバル変数</a>は使わない方が良いが、定数の場合 <a class="wp-editor-md-post-content-link" href="https://docs.python.org/ja/3/library/typing.html#typing.Final">Final 型</a> を使用して意図しない変更を検知するようにしていこうという議論になりました。結論は False です。</p> <ol start="3"> <li>check_untyped_defs</li> </ol> <p>型注釈がない関数やメソッドに対して型チェックを行うかどうかに関するオプションです。 型チェックを自動化するために型チェッカを利用しているのでこちらは True です。</p> <ol start="5"> <li>disallow_incomplete_defs</li> </ol> <p>我々が記述するコードについては、型注釈を欠損なく書いていきたいです。こちらも True です。</p> <ol start="6"> <li>disallow_untyped_defs</li> </ol> <p>型注釈がない関数やメソッドの定義に関するオプションです。こちらも True です。</p> <ol start="7"> <li>disallow_untyped_calls</li> </ol> <p>型注釈がない関数やメソッドの呼び出しに関するオプションです。 前述の disallow_untyped_defs = True、disallow_incomplete_defs = True により我々が定義したコードには型注釈がついている状態になっているはずです。 call まで True にしてしまうと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%C9%A5%D1%A1%BC%A5%C6%A5%A3">サードパーティ</a>ライブラリにもルールが適用されてしまいます。 厳しすぎるため、我々のルールでは False にしました。</p> <ol start="8"> <li>disallow_untyped_decorators</li> </ol> <p>型注釈がないデコレータに関するオプションです。 True にしてしまうと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%C9%A5%D1%A1%BC%A5%C6%A5%A3">サードパーティ</a>ライブラリにもルールが適用されてしまいます。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%C9%A5%D1%A1%BC%A5%C6%A5%A3">サードパーティ</a>ライブラリにはデコレータに型がついていない場合があります。 厳しすぎるため、我々のルールでは False にしました。</p> <ol start="10"> <li><code>ignore_missing_imports = True</code>: 型情報がないモジュールのインポートを無視します。多くの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%C9%A5%D1%A1%BC%A5%C6%A5%A3">サードパーティ</a>ライブラリは型情報を持っていないため、このオプションが有効になっています。</li> </ol> <p>True で良さそうです。</p> <ol start="11"> <li><code>implicit_reexport = False</code>: 型注釈がない関数やメソッドの再エクスポートを禁止します。</li> </ol> <p>我々は型注釈のない関数を定義しないので False です。</p> <ol start="12"> <li><code>no_implicit_optional = True</code>: Optionalの型を明示的に宣言する必要があります。</li> </ol> <p>True で良さそうです。</p> <ol start="18"> <li><code>strict = True</code>: すべてのstrictモード関連のオプションを有効にします。このオプションが有効になっている場合、以下のオプションも自動的に有効になります。</li> </ol> <p>strict_equality と strict_optional を明示的に True にしているため strict オプションは削除で良さそうです。</p> <ol start="21"> <li>warn<a class="keyword" href="https://d.hatena.ne.jp/keyword/_no">_no</a>_return</li> </ol> <p>戻り値がない関数やメソッドでのreturn文の省略に対する警告についてのオプションです。</p> <p>↓ このようなコードはエラーにしてほしいため True に設定します。</p> <pre><code class="language-python line-numbers">def example_function(value: int) -&gt; int: if value &gt; 0: return value # ここに戻り値の記述がない </code></pre> <ol start="26"> <li>warn_unused_ignores</li> </ol> <p>使用されていない<code># type: ignore</code>コメントへの警告に関するオプションです。 基本的に True にすべきですが、我々が採用しているビルドシステムとの相性が悪く、一時的に False としています。</p> <h3 id="修正後の設定ファイルの紹介">修正後の設定ファイルの紹介</h3> <p>議論の結果出来上がった設定ファイルは次のようなものになりました。</p> <pre><code class="language-ini line-numbers">[mypy] allow_redefinition = True allow_untyped_globals = False check_untyped_defs = True color_output = True disallow_incomplete_defs = True disallow_untyped_calls = False disallow_untyped_decorators = False disallow_untyped_defs = True error_summary = True ignore_missing_imports = True implicit_reexport = True namespace_packages = True no_implicit_optional = True pretty = True show_column_numbers = True show_error_codes = True show_error_context = True show_traceback = True strict = True warn_no_return = True warn_redundant_casts = True warn_return_any = True warn_unreachable = True warn_unused_configs = True warn_unused_ignores = False </code></pre> <h3 id="おわりに">おわりに</h3> <p>チームで mypy 設定ファイルの読み合わせを実施しました。 今回ご紹介した取り組み以外にも、<a class="keyword" href="https://d.hatena.ne.jp/keyword/python">python</a> typing や pydantic などの型注釈に関するドキュメントの読み合わせなど、チーム全体で知識を底上げしたり知見を共有したりする取り組みを進めています。このような取り組みによって、より安全で高速な開発を推進していきます。</p> <p>我々と一緒に開発を推進してくださるメンバーを募集しています。興味のある方、是非気軽にご連絡ください!</p> <p><a class="wp-editor-md-post-content-link" href="https://recruit.caddi.tech">エンジニア向けサイト</a></p> <p><a class="wp-editor-md-post-content-link" href="https://youtrust.jp/recruitment_posts/77a3f2329a337275ff0ac872544b8e81">カジュアル面談</a></p> <p><a class="wp-editor-md-post-content-link" href="https://open.talentio.com/r/1/c/caddi-jp-recruit/pages/79797">機械学習エンジニアの求人</a></p> caddi-tech 第3回: Terraformの基本とステート管理 hatenablog://entry/6801883189086858154 2023-08-22T16:30:53+09:00 2024-03-04T08:32:53+09:00 ※本記事は、技術評論社「Software Design」(2023年6月号)に寄稿した連載記事「Google Cloudで実践するSREプラクティス」からの転載です。発行元からの許可を得て掲載しております。 はじめに 前回はIaCの考え方や必要性と、筆者らが採用しているTerraformの特徴について紹介しました。今回は今後紹介するプラクティスの前提となるTerraformに触れたことのない方のために、その基本を簡単に紹介します1。 ここで紹介できない事項やTerraformのインストール方法については、HashiCorp 社やGoogleCloudのチュートリアル2を参考にしてください。ぜひ… <p>※本記事は、技術評論社<a class="wp-editor-md-post-content-link" href="https://gihyo.jp/magazine/SD/archive/2023/202306">「Software Design」(2023年6月号)</a>に寄稿した連載記事「Google Cloudで実践するSREプラクティス」からの転載です。発行元からの許可を得て掲載しております。</p> <h1 id="はじめに">はじめに</h1> <p>前回はIaCの考え方や必要性と、筆者らが採用しているTerraformの特徴について紹介しました。今回は今後紹介するプラクティスの前提となるTerraformに触れたことのない方のために、その基本を簡単に紹介します<sup id="fnref-4320-1"><a href="#fn-4320-1" class="footnote-ref" role="doc-noteref">1</a></sup>。 ここで紹介できない事項やTerraformのインストール方法については、HashiCorp 社やGoogleCloudのチュートリアル<sup id="fnref-4320-2"><a href="#fn-4320-2" class="footnote-ref" role="doc-noteref">2</a></sup>を参考にしてください。ぜひそちらも併せてご覧ください。</p> <h1 id="Terraformの基本コンセプト">Terraformの基本コンセプト</h1> <p>Terraformの基本コンセプトは図1のようになっています。</p> <p>▼図1 Terraformのコンセプト <img src="https://assets.caddi.com/tech-blog/wp-content/uploads/2023/07/Terraform-basic.drawio.png" alt="" /></p> <p>コードとして表現するインフラの状態は、Terraform Languageと呼ばれる独自の設定言語で記述し、拡張子<code>*.tf</code> のファイルとして保存します。</p> <p>なおドキュメント<sup id="fnref-4320-3"><a href="#fn-4320-3" class="footnote-ref" role="doc-noteref">3</a></sup>によると、「Terraform Language は、HCL(HashCorp Configuration Language)と呼ばれるHashiCorp社の独自の言語がベースになっている」と説明されています。</p> <p>しかし、Terraformの設定言語を一般化してHashiCorp社のほかの製品でも使えるようにしたものがHCLですので、「Terraform Language≒HCL」ととらえても差し支えはないでしょう。</p> <p>Terraformの実体は単一のバイナリファイルで、<code>terraform plan</code>、<code>terraform apply</code>などのさまざまな機能がサブコマンドとして提供されています。前回も触れたように、クラウドサービスごとの処理はプロバイダ(Provider)と呼ばれるプラグインに分離されています。どのプロバイダを利用するかは、tfファイル内に記述します<sup id="fnref-4320-4"><a href="#fn-4320-4" class="footnote-ref" role="doc-noteref">4</a></sup>。 Terraformがtfファイルの記述に従ってプロバイダを自動ダウンロードするので、利用者がプロバイダのインストールを意識する必要はありません。</p> <h2 id="ステート">ステート</h2> <p>Terraformを理解するうえで重要なのが、ステート(State)です。tfファイルには、Terraformで管理したいクラウドサービス上のリソースたとえばVPC(Virtual Private Cloud)やVM(Virtual Machine)インスタンスなどを記述します。これらリソースの実際の状態と、tfファイル上の記述との対応を保持しているのがステートです。 ステートの実体は<code>*.tfstate</code>という拡張子のJSONファイルで、Terraformが処理を実行するたびに更新されます。本連載では、このファイルを「tfstateファイル」と呼びます。</p> <h2 id="バックエンド">バックエンド</h2> <p>Terraformにおけるバックエンドとは、tfstateファイルの保存先のことです。バックエンドを切り替えることで、さまざまな方法でtfstateファイルを管理できます。 デフォルトのバックエンドは<code>local</code>で、tfstateファイルをローカルストレージに保存します。 Terraformの学習時や個人で使用するときは、これで十分でしょう。 チームでインフラを管理するときは、tfstateファイルを共有しなければなりません。このためgcs やs3といったバックエンド<sup id="fnref-4320-5"><a href="#fn-4320-5" class="footnote-ref" role="doc-noteref">5</a></sup>を使って、オブジェクトストレージ上に配置することが一般的です。筆者らもgcsバックエンドを使ってGCS(Google Cloud Storage)のバケット上でtfstateファイルを管理しています。</p> <h1 id="Terraform-Languageの基本">Terraform Languageの基本</h1> <h2 id="ブロック">ブロック</h2> <p>Terraform Languageでは、クラウド上のリソースをブロックと呼ばれる固まりで記述します。ブロックの文法はリスト1のようになっています。たとえば、Google Cloud上で<code>my-network</code>という名前のVPCネットワークを作成するにはリスト2のように記述します。</p> <p>▼リスト1 ブロックの文法</p> <pre><code class="line-numbers">resource "google_compute_network" "my_network" { name = "my-network" } </code></pre> <p>▼リスト2 my-networkというVPCネットワークを作成</p> <pre><code class="line-numbers">resource "google_compute_network" "my_network" { name = "my-network" } </code></pre> <p>ブロックタイプには、表1に示すようなものがあります。 今回は、主要なブロックタイプについてTerraformの使い方の流れに沿って紹介します<sup id="fnref-4320-6"><a href="#fn-4320-6" class="footnote-ref" role="doc-noteref">6</a></sup>。</p> <p>▼表1 おもなブロックタイプ</p> <table> <thead> <tr> <th>ブロックタイプ</th> <th>説明</th> </tr> </thead> <tbody> <tr> <td>provider</td> <td>プロバイダの設定を記述する</td> </tr> <tr> <td>resource</td> <td>クラウドサービス上のリソースを定義する</td> </tr> <tr> <td>locals</td> <td>リソース内で使用する変数を定義する</td> </tr> <tr> <td>variable</td> <td>外部から設定可能な変数を定義する</td> </tr> <tr> <td>output</td> <td>リソースやモジュールの出力を定義する</td> </tr> <tr> <td>module</td> <td>他のモジュールの読み込みを指示する</td> </tr> <tr> <td>data</td> <td>Terraformが管理しないリソースの参照を定義する</td> </tr> </tbody> </table> <h2 id="resource-ブロック">resource ブロック</h2> <p>resourceブロックはTerraformの主役とも言えるブロックで、クラウド上のリソース定義を表します。resourceブロックでは2つのブロックラベルを指定し、1つめがリソースの種類(リソースタイプ)、2つめがTerraform内でのリソースの識別名を表します。リソースタイプは、プロバイダによってあらかじめ定義されているもので、プロバイダのドキュメントに記載されています<sup id="fnref-4320-7"><a href="#fn-4320-7" class="footnote-ref" role="doc-noteref">7</a></sup>。</p> <p>リスト3の例は、GoogleCloudのプロバイダが提供するgoogle_compute_networkというリソースを使ってVPCネットワークを定義しています。</p> <p>▼リスト3 google_compute_networkを使ってVPCネットワークを定義</p> <pre><code class="line-numbers">resource "google_compute_network" "vpc_network" { name = "terraform-network" } </code></pre> <p><code>vpc_network</code> がTerraform上の識別名で、互いのリソースを参照するときなどはこの名前を使用します。識別名は開発者が自由に決めてかまいません。 ブロック内の引数は、プロバイダが提供するリソースの仕様にしたがって指定します。google_compute_networkでは、nameが必須となっており、これでGoogle Cloud上における実際のVPCの名前を指定します。 ドキュメント<sup id="fnref-4320-8"><a href="#fn-4320-8" class="footnote-ref" role="doc-noteref">8</a></sup>を参照するとほかにもオプショナルな引数があり、たとえばルーティング・モードや、MTUといったパラメータも指定できることがわかります。</p> <h1 id="TerraformによるIaCの流れ">TerraformによるIaCの流れ</h1> <p>Terraformを利用するときの流れは図2のとおりです。<code>init</code>などはそれぞれTerraformコマンドのサブコマンドで、<code>terraform init</code>のように実行します。</p> <p>▼図2 Terraform利用の流れ <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173309.png" alt="" /></p> <p>ここからは、簡単なサンプルを使って操作の流れを説明しましょう。 ここで作成するインフラ構成を図3に示します。Google CloudのVPC(Virtual Private Cloud)の中にサブネットを1つ作成するというものです。</p> <p>▼図3 サンプルのインフラ構成 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173321.png" alt="" /></p> <p>まずサンプルコードの内容を解説してから、Terraform実行の流れを説明します。</p> <h2 id="サンプルコードの解説">サンプルコードの解説</h2> <p>図3のインフラを構築するTerraformのコード(<code>main.tf</code><sup id="fnref-4320-9"><a href="#fn-4320-9" class="footnote-ref" role="doc-noteref">9</a></sup>)をリスト4に示します。</p> <p>▼リスト4 main.tf</p> <pre><code class="line-numbers">provider "google" { ❶ project = "&lt;&lt;プロジェクトID&gt;&gt;" region = "asia-northeast1" } resource "google_compute_network" "my_network" { ❷ name = "my-network" auto_create_subnetworks = false } resource "google_compute_subnetwork" "my_subnetwork" { ❸ name = "my-subnetwork" ip_cidr_range = "10.2.0.0/16" network = google_compute_network.my_network.id ❹ } </code></pre> <h3 id="tfファイルの構成">tfファイルの構成</h3> <p>リスト4は次の3つのブロックで構成されています。</p> <ul> <li>Google Providerへの指示を記述する provider ブロック (❶)</li> <li>VPCを作る resource ブロック (❷)</li> <li>サブネットを作る resource ブロック (❸)</li> </ul> <p>❶のproviderブロックは、表1で紹介した7つのブロックタイプの1つで、プロバイダ共通の情報を定義しています。 たとえば、Google Cloudのプロバイダでは、Terraformの適用先プロジェクトIDなどを指定します。なお、手元で実行してみたい方は、<code>プロジェクトID</code> の部分をご自分のGoogleCloudのプロジェクトIDに変更してください。</p> <h3 id="リソース間の参照">リソース間の参照</h3> <p>リスト4❹では、ほかのリソースの属性を参照しています。たとえば、リスト4❸のサブネットワークは、❷で定義したVPC内に作成します。 <code>google_compute_subnetwork</code>リソースでは、<code>network</code>引数でサブネットの所属するVPCを指定します。VPCはリスト4❷の<code>my_network</code>リソースで定義しているので、これを参照します。 Terraformでは、<code>リソースタイプ.識別子.リソースの属性</code>という形式で参照できるので<code>google_compute_network.my_network.id</code>と記述しています。なお、ここで<code>id</code>という属性は<code>my_network</code>に記述されていませんが、これはプロバイダが内部で自動生成する属性です。</p> <h3 id="ブロックの記述順は自由">ブロックの記述順は自由</h3> <p>Terraform Languageは宣言的ですので、各ブロックの記述順は自由です。リスト4におけるVPCとサブネットのようにリソース間の依存関係がある場合、Terraformは依存関係を自動解析して適用順を決定します。 また、ここでは説明を割愛しますが<code>depends_on</code><sup id="fnref-4320-10"><a href="#fn-4320-10" class="footnote-ref" role="doc-noteref">10</a></sup>で依存関係の明示もできます。</p> <h2 id="init">init</h2> <p>Terraformの初回実行時には、tfファイルのあるディレクトリ<sup id="fnref-4320-11"><a href="#fn-4320-11" class="footnote-ref" role="doc-noteref">11</a></sup>上で<code>terraform init</code>を実行して初期化します。Terraformはtfファイルの内容をチェックし、必要なプロバイダやモジュールのダウンロード、バックエンドの初期化などを行います。 「モジュール」の詳細は今回割愛しますが、Terraformにおけるコードのカプセル化や再利用の単位です。関連するリソースをまとめてモジュール化し、variableやoutputブロックで入出を定義できます。自身のコードをモジュールで分割したり、インターネットに公開されたモジュールを利用したりできます。</p> <p>なお、Google Cloudのプロバイダを使用する際 、Terraform は Application Default Credential(ADC)<sup id="fnref-4320-12"><a href="#fn-4320-12" class="footnote-ref" role="doc-noteref">12</a></sup>を使って認証をするので、次のように<code>gcloud</code>コマンドで事前にログインしておく必要があります。</p> <pre><code class="line-numbers">$ gcloud auth application-default login $ terraform init </code></pre> <p>TerraformがダウンロードしたProviderやモジュールは、ワーキングディレクトリの<code>.terraform</code> という隠しディレクトリに保存されます。</p> <h2 id="plan">plan</h2> <p>Terraformを使ううえで最も重要なのが<code>plan</code>と次項で解説する<code>apply</code>です。 <code>plan</code>では、<code>apply</code>でtfファイルの記述内容をターゲットに適用するときの実行計画を表示します。追加、変更、削除されるリソースや、具体的な変更内容が表示されるので、<code>apply</code>の実行時に、インフラが受ける影響を事前に確認できます。</p> <h3 id="リソース追加の例">リソース追加の例</h3> <p>具体例で確認してみましょう。たとえば、新規リソースを作成するときは、図4のように新しく作成される項目が <code>+</code>記号で示されます(図4は、先ほどのサンプルで新しいVPCが作られるときのplan出力例です)。</p> <p>▼図4 新しいVPCが作られるときのplan出力例</p> <pre><code class="line-numbers">Terraform will perform the following actions: # google_compute_network.my_network will be created + resource "google_compute_network" "my_network" { (…省略…) + name = "my-network" + project = (known after apply) (…省略…) Plan: 2 to add, 0 to change, 0 to destroy. </code></pre> <h3 id="リソース変更の例">リソース変更の例</h3> <p>次に、一度作成したサブネットのオプションを変更するケースを考えます。main.tfにリスト5の❶の部分を追加してから、planを実行してみます(出力例は図5)。</p> <p>▼図5 変更後のplan出力例</p> <pre><code class="line-numbers"># google_compute_subnetwork.my_subnetwork will be updated in-place ~ resource "google_compute_subnetwork" "my_subnetwork" { ❶ id = "projects/xxxx/regions/asia-northeast1/subnetworks/my-subnetwork" name = "my-subnetwork" ~ private_ip_google_access = false -&gt; true ❷ # (11 unchanged attributes hidden) } Plan: 0 to add, 1 to change, 0 to destroy. </code></pre> <p>今回はすでに存在するリソース(<code>my_subnetwork</code>というサブネット)に対して変更が加わるため、2のように変更箇所<code>~</code>記号で示されます。</p> <p>▼リスト5 main.tfへの追加部分</p> <pre><code class="line-numbers">resource "google_compute_subnetwork" "my_subnetwork" { name = "my-subnetwork" (…省略…) private_ip_google_access = true ❶ } </code></pre> <p>ここでの例示は割愛しますが、リソースの削除や作りなおしを伴う変更の場合も、同様にplanで表示されます。</p> <p>また、<code>plan</code>はクラウド上のリソースの状態とステートの比較も行います。このため、Terraform以外の手段でインフラの状態を変更した結果、ステートとの食い違いが発生したときも、<code>plan</code>によって検知できます。 さらに、<code>plan</code>はTerraformのコードをリファクタリングしたときや、プロバイダのバージョンを更新したときなど、既存のインフラに影響が出ないことを確認する手段としても重要です。 リファクタリングの内容によっては、クラウド上のリソースが一度削除されてから再作成されたり、プロバイダの仕様変更によって既存のリソースが影響を受けたりすることもあります。 このようなことでインフラに意図しない影響を与えないためにも、<code>plan</code>による差分チェックはとても重要です。</p> <h2 id="apply">apply</h2> <p><code>apply</code>は、tfファイルの記述に従って、クラウド上のリソースを実際に変更します。前述の<code>plan</code>を実行しなくても<code>apply</code>は可能ですが、運用中のシステムに対して<code>apply</code>をかける前には、planによる影響のチェックがほぼ必須となるでしょう。キャディでは、GitHub上でPull requestが作られたときに<code>plan</code>を自動実行して差分を確認できるようにしています(次回詳しく紹介します)。</p> <h2 id="destroy">destroy</h2> <p><code>destroy</code>は、Terraformが管理するすべてのリソースを実際のインフラから削除します。tfファイル上で一部のリソースを削除した場合は<code>apply</code>で削除されますので、運用中のインフラに対して<code>destroy</code>を使用することはほとんどありません。検証時や開発環境などで「Terraformから作成したリソースをすべて削除したい」といった場面で使用することがほとんどでしょう。</p> <h1 id="ステートの裏側">ステートの裏側</h1> <p>最後に、Terraformを利用するうえで注意すべきステートについて解説します。ステートはTerraformの特徴的な概念であり、実運用中のクラウドインフラをTerafformで管理する際には、そのしくみをしっかり理解しておく必要があります。そうしないと、apply時に予期せぬ影響を与える危険があり、インフラの安定性を損ねるリスクがあるためです。</p> <p>冒頭でも説明したとおり、ステートはTerraformの管理対象リソースの状態を保持するJSON形式のファイルです。terraformコマンドには、ステートを参照する機能もあります。 たとえば、前述のmain.cfをapplyしたあとのステートを表示するには、<code>state list</code>コマンドを実行します<sup id="fnref-4320-13"><a href="#fn-4320-13" class="footnote-ref" role="doc-noteref">13</a></sup>。 図6のように<code>main.tf</code>に記述した3つのリソースが表示されました。</p> <p>▼図6 state listの実行結果</p> <pre><code class="line-numbers">$ terraform state list google_compute_instance.terraform_test google_compute_network.my_network google_compute_subnetwork.my_subnetwork </code></pre> <p>リソースの詳細を表示するには、<code>state show</code> コマンドを実行します(図7)。出力を見ると、purposeなどtfファイルには書かれていない情報もありますね。これは、Terraformがクラウド上のリソースから読み取った状態です。</p> <p>▼図7 satate showの実行結果</p> <pre><code class="line-numbers">$ terraform state show google_compute_network.my_network # google_compute_subnetwork.my_subnetwork: resource "google_compute_subnetwork" "my_subnetwork" { name = "my-subnetwork" gateway_address = "10.2.0.1" ip_cidr_range = "10.2.0.0/16" purpose = "PRIVATE" (…省略…) } </code></pre> <h2 id="importによるドリフトの調整">importによるドリフトの調整</h2> <p>ここで、ステートを意識すべき運用の実例を紹介します。 何らかの原因で発生するtfファイルとステート、クラウド上のリソース実体にズレが発生してしまうことをドリフトと呼びます。たとえば、tfファイル上の変更がapplyされていない状態もドリフトですが、このようなケースはapplyで解消できます。</p> <p>一方で、Terraformで自動解決できないドリフトもあり、これはステートを意識した手作業の修正が必要です。その一例を紹介しましょう。たとえば、何らかの事情でクラウド上のリソースを先に手作業で作成してしまい、後追いでTerraformで定義したいようなケースです(図8)。</p> <p>▼図8 ドリフトの発生例 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173256.png" alt="" /></p> <p>手作業で作ったリソースはTerraformの管理外であるため、このままapplyすると名前が衝突して作成できず、実体を削除しなければapplyできません。実体を削除せずに、コードと一致させるにはどうしたらよいでしょうか。 具体例で考えてみましょう。たとえば、図3とリスト4の状態に対して、手作業で新しいサブネット<code>my-subnetwork2</code>を作成し、その後tfファイルにリスト6のような定義を追加したとします。</p> <p>▼リスト6 新しいサブネット作成後にtfファイルへ追加</p> <pre><code class="line-numbers">resource "google_compute_subnetwork" "my_subnetwork2" { name = "my-subnetwork2" ip_cidr_range = "10.3.0.0/16" network = google_compute_network.my_network.id } </code></pre> <p>この状態でplanを実行すると、createの差分が表示されます。tfファイル上のリソース記述はステートに反映されておらず、クラウド上に作成したサブネットはTerraform管理外であるためです(図8)。このままapplyすると、同じ名前のサブネットがすでに存在しているのでエラーになります(図9)。</p> <p>▼図9 applyでエラーになった</p> <pre><code class="line-numbers">google_compute_subnetwork.my_subnetwork2: Creating... ╷ │ Error: Error creating Subnetwork: googleapi: Error 409: The resource 'projects/xxxx/regions/asia-northeast1/subnetworks/my-subnetwork2' already exists, alreadyExists (…省略…) </code></pre> <p>この状態を解消するために、<code>terraform import</code>コマンドを使用します。図10の例では、クラウド上の<code>my-subnetwork2</code>の状態を、ステート上の<code>google_compute_subnetwork.my_subnetwork2</code>として取りこみます。</p> <p>▼図10 terraform importを実行</p> <pre><code class="line-numbers">$ terraform import google_compute_subnetwork.my_subnetwork2 my-subnetwork2 </code></pre> <p>これでtfファイル、ステート、実体がすべて一致するためplanを実行しても差分が発生しなくなります。手作業で作成したサブネットは、無事Terraformの管理下となりました。 Terraformには、これ以外にもステートの状態を編集するコマンドがいくつかあり、それらを活用することで柔軟な運用が可能です<sup id="fnref-4320-14"><a href="#fn-4320-14" class="footnote-ref" role="doc-noteref">14</a></sup>。</p> <h2 id="セキュリティ上の注意">セキュリティ上の注意</h2> <p>tfstateファイルには、リソースの設定値が含まれるので、取り扱いに注意が必要です。たとえば、TerraformでCloudSQL(GoogleCloudにおけるリレーショナルデータベース)のアカウントを設定するようなケースでは、アカウントのパスワードがtfstateファイルに保存されます。 リモートバックエンドでステート管理する場合は、関係者以外が参照できないようにアクセス権限を適切に設定してください。</p> <p>また、Terraformにはtfstateファイルを暗号化する機能もあり、これを利用するのが最も確実です。筆者らも暗号化を進めているところです。</p> <h2 id="まとめ">まとめ</h2> <p>今回は、Terraformの概念と基本的な使い方、また実運用時に注意すべきステートの扱い方法を中心に紹介しました。次回はGitHub ctions上でTerraformを実行し、Google Cloud上のインフラを自動デプロイするCI/CDパイプラインの構築事例を紹介予定です。</p> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn-4320-1" role="doc-endnote"> Terraformについては、本誌2022年1月号「TerraformではじめるAWS構成管理」や、『WEB+DBPRESS』Vol.128の「ゼロから学ぶTerraformでも詳しく紹介されています。&#160;<a href="#fnref-4320-1" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4320-2" role="doc-endnote"> HashiCorp 社 <a href="https://developer.hashicorp.com/terraform/tutorials">https://developer.hashicorp.com/terraform/tutorials</a> Google Cloud <a href="https://cloud.google.com/docs/terraform?hl=ja ">https://cloud.google.com/docs/terraform?hl=ja </a><a href="#fnref-4320-2" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4320-3" role="doc-endnote"> <a href="https://developer.hashicorp.com/terraform/language ">https://developer.hashicorp.com/terraform/language </a><a href="#fnref-4320-3" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4320-4" role="doc-endnote"> 明示しなくてもTerraformが自動判別してくれますが、プロバイダのバージョンを指定する場合は記述する必要があります。&#160;<a href="#fnref-4320-4" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4320-5" role="doc-endnote"> Terraformがサポートするバックエンドは次のページで紹介されています。<a href="https://developer.hashicorp.com/terraform/language/settings/backends/configuration#available-backends ">https://developer.hashicorp.com/terraform/language/settings/backends/configuration#available-backends </a><a href="#fnref-4320-5" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4320-6" role="doc-endnote"> 各ブロックタイプの説明は次のページで紹介されています。<a href="https://developer.hashicorp.com/terraform/language ">https://developer.hashicorp.com/terraform/language </a><a href="#fnref-4320-6" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4320-7" role="doc-endnote"> GoogleCloudPlatformProviderであれば次のページに記載されています。<a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs ">https://registry.terraform.io/providers/hashicorp/google/latest/docs </a><a href="#fnref-4320-7" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4320-8" role="doc-endnote"> <a href="https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_network ">https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_network </a><a href="#fnref-4320-8" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4320-9" role="doc-endnote"> ここでは<code>main.tf</code>という名前にしていますが、拡張子を<code>.tf</code>とする以外の決まりはありません。ファイルの分割も自由で、実行時のカレントディレクトリ配下のtfファイルがすべて処理対象になります。&#160;<a href="#fnref-4320-9" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4320-10" role="doc-endnote"> <a href="https://developer.hashicorp.com/terraform/language/meta-arguments/depends_on ">https://developer.hashicorp.com/terraform/language/meta-arguments/depends_on </a><a href="#fnref-4320-10" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4320-11" role="doc-endnote"> ワーキングディレクトリと呼びます。&#160;<a href="#fnref-4320-11" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4320-12" role="doc-endnote"> ADCは、アプリケーションがGoogleCloudへアクセスするときに利用する標準的な認証のしくみです。<a href="https://cloud.google.com/docs/authentication/application-default-credentials?hl=ja ">https://cloud.google.com/docs/authentication/application-default-credentials?hl=ja </a><a href="#fnref-4320-12" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4320-13" role="doc-endnote"> リスト4ではバックエンドを指定していないので、一度applyを実行するとローカルに<code>terraform.tfstate</code>という名前でtfstateファイルができているはずです。&#160;<a href="#fnref-4320-13" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4320-14" role="doc-endnote"> <code>statemv</code>、<code>staterm</code>など。詳細は次のページで解説されています。<a href="https://developer.hashicorp.com/terraform/cli/state ">https://developer.hashicorp.com/terraform/cli/state </a><a href="#fnref-4320-14" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> </ol> </div> y-komori CADDi DRAWERにE2Eテスト自動化の導入を進めています hatenablog://entry/6801883189081010445 2023-08-10T12:00:44+09:00 2024-02-08T17:34:18+09:00 こんにちは、DRAWER Enabling QAチームの猿渡です。 この記事ではDRAWER QAチームで進めているE2Eテスト自動化についてご紹介します。 課題 CADDi DRAWERにはQAチームがあります。品質保証業務は、開発エンジニアや外部パートナーなど様々な方と連携し行っています。 現在QAが行っているテストは、システム全体をスコープにしたエンドツーエンド(E2E)テストです。。 CADDi DRAWERでは、DRAWER Product Testing Guidelineにより、以下のテストカテゴリを定義しており、E2Eテストでは、Test Size: Largeの「Story … <p>こんにちは、DRAWER Enabling QAチームの猿渡です。 この記事ではDRAWER QAチームで進めているE2Eテスト自動化についてご紹介します。</p> <h1 id="課題">課題</h1> <p>CADDi DRAWERにはQAチームがあります。品質保証業務は、開発エンジニアや外部パートナーなど様々な方と連携し行っています。</p> <p>現在QAが行っているテストは、システム全体をスコープにしたエンドツーエンド(E2E)テストです。。 CADDi DRAWERでは、DRAWER Product Testing Guidelineにより、以下のテストカテゴリを定義しており、E2Eテストでは、Test Size: Largeの「Story Tests」と「Scenario Tests」のCategoryに対して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A5%D5%A5%C8%A5%A6%A5%A7%A5%A2%A5%C6%A5%B9%A5%C8">ソフトウェアテスト</a>ライフサイクル(STLC)を行っています。</p> <h5 id="DRAWER-Product-Testing-Guideline">[DRAWER Product Testing Guideline]</h5> <table> <thead> <tr> <th>Category</th> <th>Test Size</th> <th>Description</th> <th>Run Timing</th> <th>Quality</th> </tr> </thead> <tbody> <tr> <td>Unit Tests</td> <td>Small</td> <td>依存はStubする。そのDomainやDomain Service、Use Caseの振る舞いが正常なのか検証する。</td> <td>Local, CI</td> <td>内部品質</td> </tr> <tr> <td>Integration Tests</td> <td>Medium</td> <td><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>が外部に依存するDBや外部サービスといった別<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>との結合をテストする。</td> <td>Local, CI</td> <td>内部品質</td> </tr> <tr> <td>Component Tests</td> <td>Small</td> <td>外部依存はStubする。そのService単体として振る舞いが正常に行われているかどうか検証する。</td> <td>Local, CI</td> <td>内部品質</td> </tr> <tr> <td>Scenario-Based Integration Tests</td> <td>Medium</td> <td>システム全体としてシナリオテストが満たせるかどうか<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>のみで検証する。つまり、Scenario Test から UI 操作を除いたもの。Stubは使わない。</td> <td>CI</td> <td>内部品質</td> </tr> <tr> <td>Story Tests</td> <td>Large</td> <td>開発機能の受入条件が満たされているのか検証する。</td> <td>Before Release</td> <td>外部品質</td> </tr> <tr> <td>Scenario Tests</td> <td>Large</td> <td>ユーザーの代表的な操作シナリオを定義して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%B0%A5%EC%A5%C3%A5%B7%A5%E7%A5%F3">リグレッション</a>が発生していないかUIから操作して検証する。</td> <td>Before Release</td> <td>外部品質</td> </tr> </tbody> </table> <p>今のE2Eテストは全て手動テストで実行していることで、今後「E2Eテストがリリースの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DC%A5%C8%A5%EB%A5%CD%A5%C3%A5%AF">ボトルネック</a>になることによる、顧客に対する価値提供が遅れ」が顕在化することが想定されます。</p> <p>CADDi DRAWERの開発チームは、事業の急成長に合わせて拡大し、開発生産性も向上しています。ソフトウェア開発ライフサイクルの高速化に合わせて、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A5%D5%A5%C8%A5%A6%A5%A7%A5%A2%A5%C6%A5%B9%A5%C8">ソフトウェアテスト</a>ライフサイクルも同期させる必要があります。 早くソフトウェアが価値を生み出すために、一定のQuality Gateとして機能しているE2Eテストを高速化させ、顧客への価値提供のリードタイムを短くすることが重要です。</p> <p>さらに、将来的には、マイクロサービスでのE2Eテストによる品質保証にも課題があると考えます。今まで通りにシステム全体に対するE2Eテストでは、テストがフェールすると、その原因となった問題が解決されるまで、すべてのマイクロサービスのリリースがブロックされてしまいます。マイクロサービスが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B6%A1%BC%A5%A4%A5%F3%A5%BF%A1%BC%A5%D5%A5%A7%A1%BC%A5%B9">ユーザーインターフェース</a>を持っている場合のE2Eテストの扱いについては今後のテーマと捉えていますが、独立的なE2Eテストが必要になるのではと考えています。</p> <p>目指すは『テストピラミッド』のようなShift Leftされた状態を思い描きながら、まずは、ピラミッドを登りながら改善を進めるのではなく、ピラミッドの頂上から改善を進め、「リードタイムの短縮」「マイクロサービスでの独立したE2Eテスト」をGoalとし、E2Eテストをできるだけ自動化することへの取り組みを始めました。</p> <h1 id="E2Eテスト自動化に向けて">E2Eテスト自動化に向けて</h1> <p>QAチームが所属するEnablingチームには、ArchitectureチームとSREチームがいます。特にE2E自動化のCI/CDなど環境構築はSREチームと協力しながら進めています。</p> <h2 id="E2Eテスト導入のROIとテスト戦略">E2Eテスト導入のROIとテスト戦略</h2> <ul> <li>E2Eテスト導入のROIとして、ローコード系有償ツールを導入した場合の試算をしました。ただし、自動化によって得られる継続的な価値(Delivery高速化の価値など)を数値化することは難しかったので、金銭コスト、時間コストで効果測定しました。 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173359.png" alt="E2E_ROI" /></li> <li>To-BeのE2Eテスト戦略として、手動テストと自動テストの役割を決めました。自動テストの担当はテスト自動化アーキテクトをメインとしたエンジニア(SDET)が担当、手動テストはテストエンジニア(TE)が担当することでハイブリッドなSTLCとし、品質面で品質保証エンジニア(QA)が伴走する形を考えています。 <table> <thead> <tr> <th></th> <th>Category</th> <th>テスト担当</th> <th>品質担当</th> </tr> </thead> <tbody> <tr> <td>手動テスト</td> <td>Story Tests: Functional Tests</td> <td>Test Engineer(TE)</td> <td>Quality Assurance Engineer(QA)</td> </tr> <tr> <td>自動テスト</td> <td>Scenario Tests: Functional Regression Test</td> <td>Software Development Engineer in Test(SDET)</td> <td>Quality Assurance Engineer(QA)</td> </tr> <tr> <td>自動テスト</td> <td>Scenario Tests: Visual Regression Test</td> <td>Software Development Engineer in Test(SDET)</td> <td>Quality Assurance Engineer(QA)</td> </tr> </tbody> </table></li> </ul> <h2 id="自動化ツール選定-と実行環境">自動化ツール選定 と実行環境</h2> <ul> <li>選定はローコード系と<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D7%A5%F3%A5%BD%A1%BC%A5%B9">オープンソース</a>系で行いました。費用、学習コスト、汎用性、拡張性、コーディングスキルの観点でPros/Consを整理した結果、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D7%A5%F3%A5%BD%A1%BC%A5%B9">オープンソース</a>のPlaywrightを採用しています。詳細は、次項に記載します。</li> </ul> <p>上記のテスト戦略から、まずは「Visual Regression Test」の自動化から取り掛かることにしました。</p> <h1 id="Playwright--reg-suitで実践するVRT">Playwright + reg-suitで実践するVRT</h1> <p>E2Eテストの自動化を、CI/CDのパイプラインを利用して開発のサイクルに組み込む方針で実装を検討してみました。 上述の通りツールはPlaywrightを採用しており、Playwright単体でもVRTは実現できますが、reg-suitというツールを組み合わせるとより高機能なレポートを生成することができます。 (インストールやセットアップについては今回の記事では扱いません)</p> <h2 id="なぜreg-suitを採用したのか">なぜreg-suitを採用したのか?</h2> <ul> <li>VRTをするためにシンプルかつ十分なUIが用意されている</li> <li>シンプルにファイル名での比較をするので利用に際しての難易度が低く、合わせて利用するツールの自由度が高い</li> <li>外部ストレージへの保存がデフォルトで搭載されており、データのポータビリティ性が高い</li> </ul> <h2 id="実装方針">実装方針</h2> <p>CADDi DRAWERの開発チームではgit tagを利用したリリースフローを採用しており、tag間の差分でVRTを実行する方針とします。 reg-suitには<code>reg-simple-keygen-plugin</code>という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>があり、任意のKeyを利用して比較を行うことができます。この<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>のKeyをtagにすることによりtag間の比較を行います。 処理の流れとしては以下のようになります。 1. regconfig.<a class="keyword" href="https://d.hatena.ne.jp/keyword/json">json</a>で設定されているactualDirに画像ファイルを出力する(playwrightのscreenshot機能を利用) 2. regconfig.<a class="keyword" href="https://d.hatena.ne.jp/keyword/json">json</a>に直前のtagと現在のtagを比較するように設定する 3. reg-suitを実行してレポートを作成する</p> <h2 id="git-tagベースのVRTの実装">git tagベースのVRTの実装</h2> <p>regconfig.<a class="keyword" href="https://d.hatena.ne.jp/keyword/json">json</a>は以下のようになります。</p> <pre><code class="language-json line-numbers">{ "core": { "workingDir": ".reg", "actualDir": "directory_contains_actual_images", "thresholdRate": 0, "ximgdiff": { "invocationType": "client" } }, "plugins": { "reg-simple-keygen-plugin": { "expectedKey": "EXPECTED", "actualKey": "ACTUAL" }, "reg-notify-slack-plugin": { "webhookUrl": "&lt;slack incoming webhook url&gt;" }, "reg-publish-gcs-plugin": { "bucketName": "&lt;your bucker name&gt;" } } } </code></pre> <p>expectedKeyとactualKeyは<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsの中で直前のtagと現在のtagで置換します。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsのworkflowは以下のようになります。</p> <pre><code class="language-yaml line-numbers">name: VRT on: push: tags: - 'v*' jobs: tag_push: runs-on: ubuntu-latest permissions: contents: read id-token: write steps: - id: 'auth' name: 'Authenticate to Google Cloud' uses: 'google-github-actions/auth@v0.4.0' with: project_id: ${{ secrets.GCP_PROJECT_ID }} service_account: ${{ secrets.GCP_WIF_SERVICE_ACCOUNT }} workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }} - name: Checkout main branch uses: actions/checkout@v3 with: ref: main fetch-depth: 0 - name: Use Node.js uses: actions/setup-node@v1 with: node-version: "18.x" - name: Run npm install run: | npm ci - name: Install Playwright Browsers run: npx playwright install --with-deps - name: Run playwright run: | npx playwright test - name: Replace reg-suit tag run: | sed -i "s/EXPECTED/$(git tag | tail -2 | head -1)/g" regconfig.json sed -i "s/ACTUAL/$(git tag | tail -1)/g" regconfig.json - name: Run reg-suit run: | npx reg-suit run </code></pre> <p>git tagがpushされるタイミングで実行されます。 reg-suitを実行する際にGCSへの読み取りと書き込みが発生するため、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions内でGCSにアクセスするためにWorkload Identity連携を利用してアクセスできるように設定してあります。 実装方針に則りVRT関連の処理は以下の3 stepになります 1. playwrightを実行してscreenshotを取得する(<code>Run Playwright</code>) 2. regconfig.<a class="keyword" href="https://d.hatena.ne.jp/keyword/json">json</a>の書き換え、expectedKeyとactualKeyをgit tagで置換する(<code>Replace reg-suit key</code>) 3. reg-suitを実行してレポートを出力する(<code>Run reg-suit</code>)</p> <p>実行が完了すると以下のようなslack通知とVRTのレポートが作成されます(サンプルの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A1%BC%A5%F3%A5%B7%A5%E7%A5%C3%A5%C8">スクリーンショット</a>)。 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173405.png" alt="" />!</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173416.png" alt="" /></p> <h2 id="今後の課題">今後の課題</h2> <p>git tagを利用してのVRTを構築しましたが、実際のところアプリケーションのデプロイはArgoCDでのimage tagの書き換えを起点として実行されます。</p> <p>なので<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> ActionsのTriggerを利用した実行ではなく、GKEにある該当Deploymentに対するArgoCDでのSyncオペレーションを起点として実行される仕組みが必要になります。</p> <p>もう1点レポートはGCSに保存されるようになっていますが、レポートを閲覧するためには<a class="keyword" href="https://d.hatena.ne.jp/keyword/Bucket">Bucket</a>を公開設定にする必要がありセキュリティの懸念があります。キャディではCloudflareを利用しているため、Cloudflare <a class="keyword" href="https://d.hatena.ne.jp/keyword/Access">Access</a>を利用することで特定のユーザーのみのアクセスに制限するなどの対策を今後実施していく必要があります。</p> <h1 id="今後のE2Eテスト自動化">今後のE2Eテスト自動化</h1> <p>Visual Regression TestのSTLCへの組込み後、Functional Regression Testの自動化を試みる予定にしています。CADDi DRAWERでは機能拡張が継続的に行われて、合わせて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%B0%A5%EC%A5%C3%A5%B7%A5%E7%A5%F3">リグレッション</a>テストもスケールしています。この増加に対処できるように機能テストの自動化が必須であると考えています。複雑度が高く、探索的テストが効果的なものは手動テスト、それに対して複雑度の低いテストは自動化を進める方針です。 E2Eテストでは手動と自動のハイブリッドなテストでプロダクト品質の管理を行いたいと考えています。</p> <hr /> <p>QAはチーム立ち上げ期で、不確実性の多い環境の中でQAエンジニアとして組織やプロダクト・サービス品質の向上に取り組んでいます。ソフトウェア品質保証、テストエンジニアリング、テスト自動化のご経験のある方、カジュアル面談もやっていますのでぜひお気軽にご連絡ください。</p> <p>エンジニア向け採用サイト <a class="wp-editor-md-post-content-link" href="https://recruit.caddi.tech/" title="https://recruit.caddi.tech/">https://recruit.caddi.tech/</a></p> <p>求人一覧 <a class="wp-editor-md-post-content-link" href="https://open.talentio.com/r/1/c/caddi-jp-recruit/homes/4139" title="https://open.talentio.com/r/1/c/caddi-jp-recruit/homes/4139">https://open.talentio.com/r/1/c/caddi-jp-recruit/homes/4139</a></p> <p>参考文献 <a class="wp-editor-md-post-content-link" href="https://www.oreilly.com/library/view/software-engineering-at/9781492082781/" title="https://www.oreilly.com/library/view/software-engineering-at/9781492082781/">https://www.oreilly.com/library/view/software-engineering-at/9781492082781/</a> <a class="wp-editor-md-post-content-link" href="https://www.infoq.com/jp/news/2021/02/end-to-end-testing-microservices/" title="https://www.infoq.com/jp/news/2021/02/end-to-end-testing-microservices/">https://www.infoq.com/jp/news/2021/02/end-to-end-testing-microservices/</a> <a class="wp-editor-md-post-content-link" href="https://github.com/reg-viz/reg-suit">reg-viz/reg-suit: Visual Regression Testing tool</a></p> caddi-tech CADDiの機械学習モデル開発の流れと継続的な改善 hatenablog://entry/6801883189081010459 2023-08-03T11:40:39+09:00 2024-02-08T17:34:50+09:00 みなさんはじめまして。CADDiで図面解析チームのテックリードをしている稲葉です。今日は、我々のチームがどういった図面解析の機械学習モデルをどのように開発しているのか、それをどのように改善しようとしているかを紹介したいと思います。 目次 どういう図面解析が必要なのか CADDiの機械学習モデル開発の流れ 継続的な機械学習モデルの改善に向けて おわりに どういう図面解析が必要なのか CADDiでは図面活用SaaSであるCADDi DRAWERを提供しています(DRAWERの詳細に関してはこちら)。図面はどういうものが作りたいかを示した設計図なわけですが、PNG画像やPDFなど2次元図面画像で保管… <p> みなさんはじめまして。CADDiで図面解析チームのテッ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%EA%A1%BC%A5%C9">クリード</a>をしている稲葉です。今日は、我々のチームがどういった図面解析の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデルをどのように開発しているのか、それをどのように改善しようとしているかを紹介したいと思います。</p> <h3 id="目次">目次</h3> <ul> <li>どういう図面解析が必要なのか</li> <li>CADDiの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデル開発の流れ</li> <li>継続的な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデルの改善に向けて</li> <li>おわりに</li> </ul> <h2 id="どういう図面解析が必要なのか">どういう図面解析が必要なのか</h2> <p> CADDiでは図面活用<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>であるCADDi DRAWERを提供しています(DRAWERの詳細に関しては<a class="wp-editor-md-post-content-link" href="https://caddi-inc.com/drawer/">こちら</a>)。図面はどういうものが作りたいかを示した設計図なわけですが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/PNG">PNG</a>画像やPDFなど2次元図面画像で保管されており、構造化されていないデータである事が多いです。作りたいものが何を素材としているか、どのように加工すべきかなどが画像になっているため、人の目では分かってもコンピュータ上では管理し易い状態になっていません。そのため特定の図面を自動で探し出すのは難しく、ベテランの記憶や勘に頼っていたりします。そこで、DRAWERでは検索・活用し易くするため図面画像から重要な項目を認識して構造化する解析処理が行われています。また、認識する対象範囲もさらに広げることが望まれています。</p> <p> 現状では、どういう認識モデルを開発しているかというと、</p> <ul> <li>類似特徴抽出 <ul> <li>形状など</li> </ul></li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/OCR">OCR</a> <ul> <li>全文字列</li> <li>表題欄(図面番号や名称など)</li> <li>寸法</li> </ul></li> <li>記号検出 <ul> <li>溶接記号など</li> </ul></li> </ul> <p>などといったものです(図1)。図面解析だけでもかなり多いですね。さらに、図1に示した例のような図面ばかりではなく、一度紙に印刷されたものをスキャンして画像として取り込まれた図面もありますし、手書きの図面もあります。また、図面の描き方やフォーマットはお客様に依っても様々です。もちろん画像処理・<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%EB%A5%B4%A5%EA%A5%BA%A5%E0">アルゴリズム</a>で解ける部分もあるのですが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>の方が向いているタスクや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>に頼らざるを得ないタスクも多いです。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173426.png" alt="" /> 図1. 図面解析の例</p> <h2 id="CADDiの機械学習モデル開発の流れ">CADDiの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデル開発の流れ</h2> <p> CADDiでの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデル開発の流れの話を紹介したいと思います。みなさんにとっては釈迦に説法の部分もあると思いますし、これが一番良い開発フローだ!と思っているわけでもないのですが、CADDiならではの部分もあると思いますのでお付き合いください。</p> <p> 世の中で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデル開発の流れは色々なところで語られているのですが、自分は<a class="keyword" href="https://d.hatena.ne.jp/keyword/Uber">Uber</a>さんの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%AB%C8%AF%A5%D7%A5%ED%A5%BB%A5%B9">開発プロセス</a>の概念図(図2)がしっくり来ているのでこれをベースに各工程でどういうアウトプットを定義しているかを紹介していきます。全体の大きな改善ループに加え、PoCフェーズの試行錯誤が表されています。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173438.png" alt="" /> 図2. <a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデル開発の流れの例 (<a class="wp-editor-md-post-content-link" href="https://pub.towardsai.net/ml-engineering-lessons-uber-learned-from-running-ml-at-scale-eb0f53ed9029">ML Engineering Lessons Uber Learned from Running ML at Scale</a>より引用)</p> <h4 id="1-Define">1. Define</h4> <h5 id="1-1-問題設定">1-1. 問題設定</h5> <p> このステップでのアウトプットはPRD: Product Requirements DocumentとDesign Docです。プロジェクトを始める際は、主にプロダクトマネージャらが下記に示す項目を含んだPRDを書いてくれています。</p> <ul> <li>背景</li> <li>スコープ(やること、やらないこと)</li> <li>ターゲットユーザ</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a></li> <li>機能要件</li> <li>非機能要件</li> <li>リリーススケジュール</li> </ul> <p> そして、このPRDを叩き台として関係する開発チームが合同で読み合わせを行い、詳細を具体化したり、懸念点を洗い出したり、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>でやるべきなのかどうかも含めて議論します。また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>に限らないですが認識モデルを作る際は、入出力の定義・評価方法・目標値も大事になります。図面特有の記号などはJIS規格で種類や補助記号(図3に記号例を示します)が定められているのですが、いきなり全ての記号に対応しようとすると<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>が増大しリリースに時間がかかってしまうため、今必要とされているスコープに合わせて調節することもあります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>はやってみないと・実際にデータを見てみないと性能がどれくらい出るかを見積もるのは難しいですが、評価方法や目標値も<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E6%A1%BC%A5%B9%A5%B1%A1%BC%A5%B9">ユースケース</a>に合わせて仮置きではありますが決めています。  ここで話し合った内容を基に、<a class="wp-editor-md-post-content-link" href="https://github.com/eugeneyan/ml-design-docs">こちらのml-design-docフォーマット</a>から必要な部分を抜粋し、Design Docを書いています。後のステップを進める中で、ここで決定した内容に追加・修正をすることはもちろんあります。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173433.png" alt="" /> 図3. 表面粗さJIS記号の例(<a class="wp-editor-md-post-content-link" href="https://xtech.nikkei.com/atcl/nxt/column/18/01309/00035/?P=2">表面粗さと溶接を図面で指示するJIS記号</a>より引用)</p> <h5 id="1-2-アノテーション定義">1-2. <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>定義</h5> <p> このステップでのアウトプットは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>定義書・作業マニュアルです。図面という特殊<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>の画像タスクですので、オープンデー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%BB%A5%C3%A5%C8">タセット</a>をそのまま活用できることは少なく(もちろん事前学習に使うことはできます)、人間による正解値(<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>)が必要なことがほとんどです。実際に図面を見つつ決まったスコープに従って<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>の実例や例外ケースなどをまとめていきます。このような<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>定義は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>知識がかなり必要なところなので、実際に図面からモノを作る<a class="wp-editor-md-post-content-link" href="https://caddi-inc.com/manufacturing/">CADDi MANUFACTURING事業</a>で培った経験を持つ方のお力を借りています。最近では、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C9%A5%E1%A5%A4%A5%F3">ドメイン</a>知識をカバーし、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>のQuality, Cost, and Delivery: QCDを高める役割を持ってくれるAnnotation <a class="keyword" href="https://d.hatena.ne.jp/keyword/Ops">Ops</a>チームが発足したため、とても進め易くなりました。</p> <h4 id="2-Prototype">2. Prototype</h4> <h5 id="2-1-データセット作成">2-1. デー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%BB%A5%C3%A5%C8">タセット</a>作成</h5> <p> このステップでのアウトプットは入力データと出力したい<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>のセットから成るデー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%BB%A5%C3%A5%C8">タセット</a>です。お客様から預かっている大量の図面データをサンプリングして、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>定義に従って<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>していきます。Annotation <a class="keyword" href="https://d.hatena.ne.jp/keyword/Ops">Ops</a>に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>のQCDを管理してもらっていると先ほど述べましたが、それぞれで実施している取り組みを紹介します。</p> <p> まず、Qualityに関してはオンボーディング、Q&amp;A、二段階承認フロー、抜き取り検査を実施しています。オンボーディングでは、実際にアノテータさんに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>タスクの例題をいくつか実施してもらい、想定する<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>ができるようになるまで練習していただいています。Q&amp;Aでは、アノテータさんから<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>の判断に困った際の質問・回答を<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>定義書・作業マニュアルとして更新し、できる限りアノテータさん毎にブレが生じないように、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>に迷いが生じないようにしています。質問は基本的にAnnotation <a class="keyword" href="https://d.hatena.ne.jp/keyword/Ops">Ops</a>のメンバーが対応していますが、認識モデルの学習で問題になりそうなところは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>エンジニアと相談して決めています。二段階承認フローは、最初に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>するアノテータさんとは別のアノテータさんが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>をチェックし、修正が必要であればコメントを残し再度<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>プロセスに戻すようにしています。抜き取り検査は全て承認が通ったデータをいくつかサンプリングし、Annotation <a class="keyword" href="https://d.hatena.ne.jp/keyword/Ops">Ops</a>のメンバーが評価しています。この際の良品率をKPIとして追っています。</p> <p> 次に、Costに関してですが、基本的に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>速度を上げるために、プレ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>とショートカットキーの利用促進を実施しています。プレ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>は、皆さんご存じの通り、既に認識モデルがある場合はその認識モデルの結果を初期<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>として登録することです。ショートカットキーの利用促進は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>ツールにデフォルトで”クラスの切替”や”<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>削除”などのショートカットキーが設定されているため、それらをアノテータさんに共有しています。キーボードだけでなく、マウスのボタンにもキーを設定することもできるため試行錯誤しています。</p> <p> 最後に、Deliveryの部分ですが、予実管理を毎日行っています。デー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%BB%A5%C3%A5%C8">タセット</a>作成の期日に間に合いそうに無ければ、期日を調整するかアノテータさんの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>と他の作業の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>割合を調整しています。</p> <p> これらの取り組みは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>ツールの選定も重要になります。以前は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D7%A5%F3%A5%BD%A1%BC%A5%B9">オープンソース</a>を活用していたこともありましたが、セキュリティ面や上記で示したようなことがプロセスとして組めるかどうかを考慮した結果、<a class="wp-editor-md-post-content-link" href="https://fastlabel.ai/?utm_term=fastlabel&amp;utm_campaign=%E3%82%A2%E3%83%8E%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AE%E5%BA%83%E5%91%8A&amp;utm_source=adwords&amp;utm_medium=ppc&amp;hsa_acc=2260653046&amp;hsa_cam=14119327678&amp;hsa_grp=125084307733&amp;hsa_ad=536976345050&amp;hsa_src=g&amp;hsa_tgt=kwd-1395419680791&amp;hsa_kw=fastlabel&amp;hsa_mt=b&amp;hsa_net=adwords&amp;hsa_ver=3&amp;gad=1&amp;gclid=Cj0KCQjwiIOmBhDjARIsAP6YhSWnq67e1StDm97UGP06ajPi0KSsnVu_0Ed1kMDFG4s5THbrQCPy1iEaAga8EALw_wcB">FastLabel</a>さんのツールを利用させていただいています。</p> <h5 id="2-2-学習評価">2-2. 学習・評価</h5> <p> このステップでのアウトプットは学習・評価コードと評価レポートです。図面解析チームにはkaggle含め経験豊富な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>エンジニアが居ますので、バリバリ開発してくれています。実際どういう技術を使っているかや技術スタックは、また別の機会にメンバーから紹介する予定ですのでここでは割愛します。</p> <h4 id="3-Production">3. Production</h4> <h5 id="3-1-デプロイ">3-1. デプロイ</h5> <p> このステップでのアウトプットは<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>サーバとドキュメント類(モデルカード、テスト結果)です。<a class="wp-editor-md-post-content-link" href="https://caddi.tech/archives/4123">ML API基盤</a>に関しては既にTech Blogに書かれていますので、ぜひ読んでみてください!テストは、PoCの性能が再現できているかの性能テストと想定リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トでどれくらいのレイテンシ・<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%EB%A1%BC%A5%D7%A5%C3%A5%C8">スループット</a>で処理できるかを見積もる負荷テストを実施しています。PRD作成時に要件を決めていますので、満たせているかどうかを確認しています。</p> <h4 id="4-Measure">4. Measure</h4> <h5 id="4-1-監視">4-1. 監視</h5> <p> デプロイしたWeb <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>のレイテンシ・<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%EB%A1%BC%A5%D7%A5%C3%A5%C8">スループット</a>などは日々監視・ロギングしており、サービス提供に問題が無いかを確認しています。次項で示す<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデルの改善にも関連するのですが、性能面での監視やフィードバックの貰い方の仕組み化も進めているところです。</p> <h2 id="継続的な機械学習モデルの改善に向けて">継続的な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデルの改善に向けて</h2> <p> ご存じの通り、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデルは一度作って終わりではありません。新しいお客様に契約いただくことで図面のバリエーションも増えますし、中々現れないレアケースの認識対象もあります。なので、継続的な改善が必要なのですが、そのために”重要なデータを集める仕組み”と”<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>パイプライン”の構築を考えています。改善ループのイメージを図4に示します。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173446.png" alt="" /> 図4. <a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデルの改善ループ</p> <p> 重要なデータというのは、性能向上に寄与するデータや顧客価値を毀損しているデータのことを指しています。集め方としては下記が考えられます。</p> <ol> <li>能動学習: 不確実性が高いデータを現状のモデルを使ってマイニングする</li> <li>お客様とやりとりがあるCustomer Successチームや実際にデータを処理する<a class="keyword" href="https://d.hatena.ne.jp/keyword/Ops">Ops</a>チームなど社内全員で課題データを挙げて収集する</li> <li>ユーザであるお客様にDRAWERアプリ内から直接課題データを挙げてもらう</li> </ol> <p>これらの内、まずは2の仕組みを作り社内の人間であれば誰でも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>タスクとして登録できるようにして試験運用を開始しました(図5)。DRAWERで登録してある図面画像は一意に定まるIDで管理されていますので、ID群とタスク名と登録した理由を入力して実行するだけで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>ツールに登録され、リンク先から実際に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>することができます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>ツールは<a class="wp-editor-md-post-content-link" href="https://fastlabel.ai/?utm_term=fastlabel&amp;utm_campaign=%E3%82%A2%E3%83%8E%E3%83%86%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%B5%E3%83%BC%E3%83%93%E3%82%B9%E3%81%AE%E5%BA%83%E5%91%8A&amp;utm_source=adwords&amp;utm_medium=ppc&amp;hsa_acc=2260653046&amp;hsa_cam=14119327678&amp;hsa_grp=125084307733&amp;hsa_ad=536976345050&amp;hsa_src=g&amp;hsa_tgt=kwd-1395419680791&amp;hsa_kw=fastlabel&amp;hsa_mt=b&amp;hsa_net=adwords&amp;hsa_ver=3&amp;gad=1&amp;gclid=Cj0KCQjwiIOmBhDjARIsAP6YhSWnq67e1StDm97UGP06ajPi0KSsnVu_0Ed1kMDFG4s5THbrQCPy1iEaAga8EALw_wcB">FastLabel</a>さんのツールを利用していますが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>タスクを管理するWebAPIや<a class="wp-editor-md-post-content-link" href="https://github.com/fastlabel/fastlabel-python-sdk">FastLabel Python SDK</a>が揃っているため、容易にこのようなフローを作ることができました。多くの人間で<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>登録をするとノイズになるようなデータが含まれるのでは?という懸念もありますが、デー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BF%A5%BB%A5%C3%A5%C8">タセット</a>化されるためには他のアノテータの承認が必要となるため、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>の品質は担保できると考えています。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173420.png" alt="" /> 図5. <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%CE%A5%C6%A1%BC%A5%B7%A5%E7%A5%F3">アノテーション</a>登録UI</p> <p> <a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>パイプラインは上記のようにして収集したデータを活用し、定期的に”前処理-学習-評価-デプロイ”を自動実行する仕組みですが、絶賛開発中です。MLOpsチームのメンバーがまた紹介してくれますのでここでは詳細を割愛しますが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデルの更新サイクルを高速化するため、また<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>エンジニアが新しいモデルの開発など得意な領域に注力できるようにするため、開発を進めています。</p> <p> これらの仕組みを組み合わせることで、お客様などからいただいた課題に迅速に対応してサービス品質を向上させ、より良い顧客体験が産み出せるようにしていきたいと思っています。この辺りの話は今井が<a class="wp-editor-md-post-content-link" href="https://speakerdeck.com/imaimai0/caddi-drawer-shi-zu-midepin-zhi-wozuo-rutu-mian-jie-xi">Data-centric AI勉強会で話した資料</a>もありますので、ぜひ目を通していただけると嬉しいです。</p> <h2 id="おわりに">おわりに</h2> <p> CADDiの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデル開発の流れと継続的な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデルの改善に向けてどのようなことに取り組んでいるかを紹介しました。図面解析は新機能の開発も進んでおり、DRAWERの機能としても増えつつあるのですが、まだまだ必要な機能がありますし、各認識モデルの改善も必要です。また、今回は2次元図面画像解析の話をしたのですが、2次元図面だけでなくもちろん3次元図面(3DCAD)も今後扱えるようにしていきたいと思っています。やりたいことは沢山あるので、ぜひ一緒に開発を推進してくださるメンバーを募集しています。興味のある方、是非気軽にご連絡ください!</p> <p>エンジニア向けサイト <a class="wp-editor-md-post-content-link" href="https://recruit.caddi.tech/">https://recruit.caddi.tech</a></p> <p>カジュアル面談 <a class="wp-editor-md-post-content-link" href="https://youtrust.jp/recruitment_posts/980e714200110ad87604cb5c9e517027">https://youtrust.jp/recruitment_posts/980e714200110ad87604cb5c9e517027</a></p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>エンジニアの求人 <a class="wp-editor-md-post-content-link" href="https://open.talentio.com/r/1/c/caddi-jp-recruit/pages/79797">https://open.talentio.com/r/1/c/caddi-jp-recruit/pages/79797</a></p> caddi-tech CADDi DRAWERのリアーキテクチャにEventStormingを導入しました hatenablog://entry/6801883189081010473 2023-08-02T12:00:22+09:00 2024-03-04T11:40:29+09:00 こんにちは、DRAWER Enabling Architectureチームの刈部です。 この度、弊社はシリーズCの資金調達を実施しました。これを受けTech Blogを盛り上げようというPRの施策に乗っかり本稿に繋がるのですが、なかなか筆が乗らず気づいたら調達の発表から1ヶ月近く経ってしまいました。計画的に生きたい。 content.caddi-inc.com さて、この記事ではDRAWER開発チームにEventStormingを導入した件について、導入時の課題や良かった効果について紹介しようと思います。 EventStormingとは? 本題に入る前にEventStormingに関する簡単な… <p>こんにちは、DRAWER Enabling Architectureチームの刈部です。</p> <p>この度、弊社はシリーズCの資金調達を実施しました。これを受けTech Blogを盛り上げようというPRの施策に乗っかり本稿に繋がるのですが、なかなか筆が乗らず気づいたら調達の発表から1ヶ月近く経ってしまいました。計画的に生きたい。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fcontent.caddi-inc.com%2Fjp%2Finfo%2Fseries-c%2F" title="総額118億円のシリーズC資金調達を実施" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://content.caddi-inc.com/jp/info/series-c/">content.caddi-inc.com</a></cite></p> <p>さて、この記事ではDRAWER開発チームにEventStormingを導入した件について、導入時の課題や良かった効果について紹介しようと思います。</p> <h1 id="EventStormingとは">EventStormingとは?</h1> <p>本題に入る前にEventStormingに関する簡単な紹介です。 EventStormingとは、ドメインモデリング手法のひとつです。ドメインエキスパートとステークホルダーがビジネスプロセスを協働して整理することを通じて、サブドメインや境界付けられたコンテキストを見つけ出します。</p> <p>EventStormingでは3つのアクティビティを順に行います。</p> <ol> <li>Big Picture: ビジネスプロセス上の意味のあるイベントを思いつくだけ付箋に書き出して時系列に並べます。</li> <li>Process Modeling: 書き出されたイベントの発火条件やデータフローを整理します。</li> <li>Software Design: Aggregateを抽出します。</li> </ol> <p>EventStormingのアクティビティを進め方については<a href="https://www.eventstorming.com/resources/">本家の記事</a>でも抽象的な説明が多いです。私自身正しく理解できている自信がありませんが、個人的にはBPM(Business Process Modeling)とDDD(Domain-Driven Design)を組み合わせたようなものだと簡単に理解しています。</p> <h1 id="EventStormingを導入した経緯">EventStormingを導入した経緯</h1> <p>現在CADDi DRAWERの開発チームでは、事業の急拡大に対しシステムと開発組織をスケーラブルにするためリアーキテクチャを行っています。リアーキテクチャではサービス導入当初から運用されているモノリシックなシステムの分割やコアドメインとなるドメインモデルやデータモデルの整理が含まれています。 まずそれらに仕掛かるために現状のシステムについて整理する必要がありました。下図は同僚が作成した資料になります。</p> <p><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240304/20240304114026.png" width="800" height="404" loading="lazy" title="" class="hatena-fotolife" itemprop="image"></span></p> <p>この資料は画面から呼び出されるバックエンドのAPIと図面解析パイプラインなどが、それぞれどのテーブルを参照・更新しているのか整理したものになります。</p> <p>これにより現状の把握は劇的にしやすくなりました。一方で「これらの操作は誰がどのようなユースケースで行っており、ビジネスプロセス上どのような意味を持つのか」という文脈は理解できません。ビジネスプロセスを整理した上でリアーキテクチャを進めるためにEventStormingを採用しました。</p> <h2 id="苦戦したこと">苦戦したこと</h2> <p>実際にEventStormingのアクティビティに取り組んでみると下記の課題にあたりました。</p> <h3 id="解釈がメンバー間で揃わない">解釈がメンバー間で揃わない</h3> <p>EventStormingの各アクティビティについて明確な作法はありません。チームのメンバー同士でCommandやEventの粒度や書き方が異なることが度々ありました。共通言語がより少ないエンジニアと非エンジニアの間では「何を書くか」ではなく「どう書くか」について擦り合わせる時間が多く必要となりました。 例えば「xボタンがクリックされた」や「xが確認された」のようなイベントです。それらはたしかに”発生する出来事”ではありますが、ビジネスプロセスにおける”重要な出来事”ではありません。</p> <h3 id="既存仕様に引っ張られる">既存仕様に引っ張られる</h3> <p>EventStormingに限らないですが、ビジネスプロセスにおける”重要な出来事”ではなく今現在のUIやシステムの制約において”発生する出来事”にどうしても引きづられてしまいました。EventStormingのアクティビティに明確な作法がないため、行き詰まった時に既存仕様や既存設計から発想を拡げやすいからです。</p> <h3 id="異常系の扱い">異常系の扱い</h3> <p>設計が進んでいくと様々な考慮事項が見えてきました。特に異常系をEventStormingで表現するのは難しく、異常の内容に応じて細かく分岐を書こうとすると本来描きたいビジネスプロセスにノイズが多く発生するほか、矢印だらけの図になり非常に見づらくなってしまいます。 難しいのは異常系のハンドリングそれ自体ではなく(技術的にはもちろん難しいわけですが)、EventStormingによって解像度が上がった時、我々エンジニアは未考慮のものを未考慮のままにして前に進みづらい性格をしているということです。そうするとビジネスプロセスよりも異常系の場合分けに時間を浪費してしまいます。</p> <h2 id="工夫したこと">工夫したこと</h2> <p>身も蓋もないですが、まずはとにかく慣れることに集中しました。EventStormingもDDDも誰がやっても同じ結果になるものではなく、解像度を上げながら対話によってドメインモデルを深化するしかないと割り切りました。正しくEventStormingを行うことよりも、我々の扱っているビジネスプロセスの関心事に集中すると、自然と枝葉が削がれていきました。 また、小さな成功体験を生むためにもまずは正常系のプロセスを1本通すことを優先し、細かい分岐は後回しとしました。</p> <p>EventStormingは後のアクティビティになるにつれて点と点が結ばれていきます。すると前半に出した付箋にどのような意味を持つのか、または持たないのか答え合わせされていきます。これによりビジネスプロセスの中で本質的に何に関心を持つべきなのか学習が進みます。</p> <p>下図が実際にチームで行った成果物になります。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173347.png" alt="" /></p> <p>これによってチーム内外で同じものを指差してコミュニケーションが取れるようになりました。システム間のI/Fについても貼られたDomainEventを参考に設計しやすいため、開発者間で認識がズレにくいという効果もありました。</p> <h1 id="そして全体へ">そして全体へ...</h1> <p>前述の通りDRAWERは事業の急拡大に応じてチーム数が急増しました。しかし、システム境界やチーム境界が曖昧な部分があり、チーム間に歪みを生み、ディスコミュニケーションが起きていました。</p> <p>今回のリアーキテクチャを期にビジネスプロセス全体をEventStormingによって整理して、見つけた境界付けられたコンテキストによってシステム設計や組織設計もしようという流れになりました。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173353.png" alt="" /></p> <h2 id="良かった点">良かった点</h2> <ul> <li>大まかに境界付けられたコンテキストが見えてきました。それによりチームやシステムの分割の材料になりました。</li> <li>現状のチームの隙間に存在するEventや認識の違いが表出しました。またシステムに関係する箇所だけではなく、人力のオペレーションの複雑さも表出しました。</li> </ul> <p>全体へ導入する進め方について、結果論ではありますがトップダウンに進めたことは功を奏しました。リソースや開発の優先度の調整が可能なポジションにある人が全体の旗振りをするのはとても効果的でした。</p> <h1 id="今後の展望">今後の展望</h1> <p>EventStormingによってビジネスプロセスやドメインモデルの解像度が非常に上がりました。チームを越えて認識を揃えられる点や実装に落とし込みやすい点においても非常に強力なフレームワークでした。 一方で、EventStormingの本当の難しさはやって終わりではなく育てていくことにあると思います。</p> <h2 id="全体最適の共通言語として">全体最適の共通言語として</h2> <p>組織が大きくなりチームが分割されていくと徐々に意思決定がサイロ化していきます。チーム間で情報が非対称であったりコミュニケーションコストが大きいと、自分達がコントロールしやすい選択をしてしまいます。 そんな時、各チームでの最適な意思決定を行うのではなく今回のEventStormingの成果物に立ち戻れるようにしていきたいです。プロダクトの機能はシステムだけでなくオペレーションも含めて価値であるということを強く意識し、局所最適の積み重ねではなく全体最適を志向し続ける必要があります。</p> <h2 id="ビジネスプロセスの洗練">ビジネスプロセスの洗練</h2> <p>また、EventStormingの成果物を定期的に見直して不要なドメインイベントを減らしていきたいと考えています。不要な分岐が減ることはデータやプロセスの標準化に繋がるだけではなく、システムの運用容易性にも繋がり、システムの拡張性や高いアジリティの獲得に繋がると考えています。</p> <hr /> <p>定型句になりますが採用についてです。複雑で絡み合ったドメインモデルをソフトウェアに落とし込むことに興味がある方、良い機能を生み出す組織や文化を作ることに興味がある方を募集しています。カジュアル面談もやっていますのでぜひお気軽にご連絡ください。</p> <p><iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Frecruit.caddi.tech%2F" title="CADDi 採用情報 for Engineer/Designer" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://recruit.caddi.tech/">recruit.caddi.tech</a></cite> <iframe src="https://hatenablog-parts.com/embed?url=https%3A%2F%2Fopen.talentio.com%2Fr%2F1%2Fc%2Fcaddi-jp-recruit%2Fhomes%2F4139" title="CADDi 採用情報 / キャディ株式会社" class="embed-card embed-webcard" scrolling="no" frameborder="0" style="display: block; width: 100%; height: 155px; max-width: 500px; margin: 10px 0px;" loading="lazy"></iframe><cite class="hatena-citation"><a href="https://open.talentio.com/r/1/c/caddi-jp-recruit/homes/4139">open.talentio.com</a></cite></p> <p>参考文献</p> <ul> <li><a href="https://www.eventstorming.com/resources/">Resources - EventStorming</a></li> <li><a href="https://www.oreilly.com/library/view/domain-modeling-made/9781680505481/">Domain Modeling Made Functional [Book]</a></li> <li><a href="https://www.oreilly.com/library/view/practical-process-automation/9781492061441/">Practical Process Automation [Book]</a></li> <li><a href="https://youtu.be/jC9lE4YqgyY">&#x30A4;&#x30D9;&#x30F3;&#x30C8;&#x30B9;&#x30C8;&#x30FC;&#x30DF;&#x30F3;&#x30B0;&#x5165;&#x9580;&#x3010;&#x30CE;&#x30FC;&#x30AB;&#x30C3;&#x30C8;&#x7248;&#x3011; - YouTube</a></li> <li><a href="https://blog.kinto-technologies.com/posts/2022-11-01-eventstorming/">EventStorming&#x3067;&#x30E2;&#x30C7;&#x30EA;&#x30F3;&#x30B0;&#x3057;&#x3066;&#x307F;&#x305F; | KINTO Tech Blog | &#x30AD;&#x30F3;&#x30C8;&#x30C6;&#x30C3;&#x30AF;&#x30D6;&#x30ED;&#x30B0;</a></li> <li><a href="https://yoskhdia.hatenablog.com/entry/2018/11/09/200556">&#x65B0;&#x3057;&#x3044;&#x30E2;&#x30C7;&#x30EA;&#x30F3;&#x30B0;&#x624B;&#x6CD5;: EventStorming (&#x30A4;&#x30D9;&#x30F3;&#x30C8;&#x30B9;&#x30C8;&#x30FC;&#x30DF;&#x30F3;&#x30B0;) &#x3092;&#x306F;&#x3058;&#x3081;&#x308B;&#x305F;&#x3081;&#x306E;&#x6E96;&#x5099; - yoskhdia&rsquo;s diary</a></li> <li><a href="https://zenn.dev/tkatsuma/articles/97931405cbcd94">&#x30A4;&#x30D9;&#x30F3;&#x30C8;&#x30B9;&#x30C8;&#x30FC;&#x30DF;&#x30F3;&#x30B0;&#x5C0E;&#x5165;</a></li> </ul> caddi-tech CADDiプロダクト横断の認証認可基盤を開発している話 hatenablog://entry/6801883189081010513 2023-08-01T16:20:47+09:00 2024-02-26T11:30:20+09:00 注意! 2023年8月時点の内容となりますので、参考情報としてご覧ください。現在、アーキテクチャを見直し、同等の機能をより効率的に実現できる構成にして随時開発中です。機会が来たら新しいアーキテクチャの構成を紹介します CADDi Platformグループの前多です。 私たちはCADDiのプロダクト横断の技術課題を解消するための活動をしています。 これまでの活動の詳細は 信頼性を高めるサービス基盤と技術選定を見てください。 これまでの活動はクラウドインフラや開発環境の整備などが大半でしたが、今後のCADDiのプロダクト開発を発展させるために、プロダクト共通で必要となるサービス基盤の開発にも着手し… <p><strong>注意! 2023年8月時点の内容となりますので、参考情報としてご覧ください。現在、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>を見直し、同等の機能をより効率的に実現できる構成にして随時開発中です。機会が来たら新しい<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>の構成を紹介します </strong></p> <p>CADDi Platformグループの前多です。</p> <p>私たちはCADDiのプロダクト横断の技術課題を解消するための活動をしています。 これまでの活動の詳細は <a class="wp-editor-md-post-content-link" href="https://caddi.tech/archives/4251">信頼性を高めるサービス基盤と技術選定</a>を見てください。</p> <p>これまでの活動は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>インフラや開発環境の整備などが大半でしたが、今後のCADDiのプロダクト開発を発展させるために、プロダクト共通で必要となるサービス基盤の開発にも着手しています。</p> <p>現在私たちが開発しているのは、CADDiプロダクト全体で利用する想定の認証認可基盤です。 認証認可に関する製品は、<a class="wp-editor-md-post-content-link" href="https://auth0.com/">Auth0</a>などの<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>をはじめ、他にもさまざまな製品があります。 私たちが開発している認証認可基盤は、これらの製品をただ導入するだけではなく、CADDiの事業形態に合わせて複数の製品や自作のサービスを組み合わせて構築したものです。</p> <p>この記事では、CADDiのプロダクトの変遷および、独自の認証認可基盤を開発する理由と設計について記します。</p> <p>なお余談ですが、私たちの認証認可基盤には Notcher というコードネームが付いています。 Notcher というのは切り込みを入れるためのハサミのことです。 かつての日本の鉄道では、紙の切符に駅員の方がハサミで切り込みを入れて、駅のホームに入ることができました。 つまり、切り込みを入れた切符は<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンであり、私たちの認証認可基盤も認証の結果として<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを発行することから、コードネームの由来となっています。</p> <h2 id="CADDiプロダクトの変遷">CADDiプロダクトの変遷</h2> <p>CADDiの事業は製造業の部品の発注から納品までを<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B0%EC%B5%A4%C4%CC%B4%D3">一気通貫</a>で行う <a class="wp-editor-md-post-content-link" href="https://caddi-inc.com/manufacturing/">CADDi MANUFACTURING</a>から始まっています。 CADDi が企業から発注を受け、部品の製造をパートナーに依頼し検品との納品までを行うため、製造過程を管理するためのプロダクトを内製しました。 少々古い内容ですが、内製プロダクトについては <a class="wp-editor-md-post-content-link" href="https://caddi.tech/archives/2211">こちらの記事</a>などが参考になるでしょう。</p> <p>その後も私たちは事業の内容や拡大に伴い、いくつかのプロダクトを作っていきました。 そこで、大量に発生する紙の図面と製品を効率よく管理するために、図面にIDを付与し、紙のデータをデジタル化して管理するプロダクトが生まれました。</p> <p>そのプロダクトを開発した経験から、図面をデジタル化して有効活用するニーズは製造業に広くあるのではという仮説が生まれ、 AIによる類似図面の判定機能を追加して<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>化したものが、<a class="wp-editor-md-post-content-link" href="https://caddi-inc.com/drawer/">CADDi DRAWER</a>です。 ここに来て私たちは、内製プロダクトを<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>化し、かつCADDi社内での利用から社外への展開をはじめました。</p> <p>もちろん上記で触れた以外にもCADDiでは多くのプロダクトがあります。 そして、将来これらのプロダクト群を連携させ、社外にも展開していく構想があります。 そのためには次のような考慮が必要です。</p> <ul> <li>プロダクト群を一貫したユーザー体験で提供する</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を統一された方式で公開し、CADDiプロダクト群や <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A1%BC%A5%C9%A5%D1%A1%BC%A5%C6%A5%A3">サードパーティ</a>のツールとも連携する</li> <li>社内のプロダクト開発やプロダクト間連携の標準化・効率化・品質担保を推進する</li> </ul> <p>この構想を実現するための下地として、認証認可基盤の開発を始めました。</p> <h2 id="認証認可基盤が必要な理由">認証認可基盤が必要な理由</h2> <p>CADDi のプロダクトは社内プロダクトから始まったため、社員が使えれば良いということで認証認可は最低限の実装となっていました。</p> <p>CADDi では社員アカウントは <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Workspaceで管理しています。内製プロダクトの認証は <a class="wp-editor-md-post-content-link" href="https://auth0.com/learn/social-login">Auth0のSocial Login</a>を使って、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Workspaceのアカウントでログインしていました。 社内利用ということもあり、当時は社員のログインだけできればよく、認可の要件はありませんでした。</p> <p><a class="wp-editor-md-post-content-link" href="https://caddi-inc.com/drawer/">CADDi DRAWER</a> の認証についても、当時は使い慣れたAuth0で利用者を管理することにしました。 CADDi Drawer専用のAuth0テナントでアカウント管理をしていて、利用者の所属企業などはユーザーの属性として保持しています。</p> <p>この頃から、あるプロダクトから別プロダクトのデータを取得したいといった要望が出てきます。</p> <p>しかし人による認証を念頭に置いていたため、<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>間の認証についての検討が十分ではありませんでした。 また、他にも社外提供を前提としたプロダクトの開発が始まったり、 CADDi Drawerでも会社ごとに独立した2FAやパスワードポリシーを設定したり、認可制御をしたいといった要望も出てきました。</p> <p>拡大し続けるプロダクトについていけるように、今まで劣後してきた認証認可について考え直す必要性が出てきました。</p> <h2 id="認証認可基盤の設計">認証認可基盤の設計</h2> <p>新しく認証認可基盤を設計するにあたり次のことを重視して設計しました。 また、OIDC/OAuth2 という認証認可の標準に則ることを前提としています。</p> <ul> <li>マルチテナントのユーザ管理</li> <li>認証と認可の分離</li> <li>セキュアな<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>間通信</li> </ul> <h3 id="マルチテナントのユーザー管理">マルチテナントのユーザー管理</h3> <p>これまでのプロダクトの認証は、プロダクトの性質に合わせて複数のAuth0テナントに分散していました。 また、CADDi Drawer ではユーザー属性に所属企業の情報を持たせることでユーザーの所属を設定していました。</p> <p>この状態のままプロダクトを拡大していくと以下の点で運用しづらくなります。</p> <p>まず、SSO(<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%F3%A5%B0%A5%EB%A5%B5%A5%A4%A5%F3%A5%AA%A5%F3">シングルサインオン</a>)のような認証機能の使いやすさを大きく損ねます。 プロダクトごとにAuth0テナントのような認証サービスが分かれていると、複数のプロダクトにわたって共通でログインすることができません。 同じIDであってもプロダクトごとにログインをしたりパスワードが分かれてしまったりします。</p> <p>従来では、認証サービスから<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> workspace によるSocial Login によって社員は同じIDが使用できましたが、社外利用のユーザーにとってはそうでありません。 将来のプロダクト増加と利用企業増加に伴う、単一の認証サービスが必要です。</p> <p>そして、単一の認証サービスであっても利用する企業ごとにユーザーを管理できるマルチテナントの機能も必要です。 現在のCADDi Drawerでは利用企業のユーザー管理はCADDiで実施しています。</p> <p>将来的には、CADDiのプロダクトを利用する企業自身でユーザー管理を行うことを理想としています。 また、利用企業のセキュリティ基準に合わせた<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BB%A5%AD%A5%E5%A5%EA%A5%C6%A5%A3%A5%DD%A5%EA%A5%B7%A1%BC">セキュリティポリシー</a>や認証機能を実現することも想定しています。 現在でも企業ごとに、MFAやIP制限といった追加ルールを提供していますが、 さらに <a class="keyword" href="https://d.hatena.ne.jp/keyword/SAML">SAML</a>や SocialLogin、 パスワードポリシーの設定といった内容を企業ごとに独立して設定したいという要望に対応したいと考えています。</p> <p>このような背景から、単一システムでマルチテナントに対応するユーザー認証サービスの実現を根幹として、認証認可基盤の設計を進めました。 当初は独自開発を考えていませんでしたが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a>、有償製品、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>のサービスなど広く調査した結果、この要件を完全に満たしコスト的にも満足できるものはありませんでした。</p> <p>ここでネックとなったのが、 「CADDiのプロダクトを利用するユーザーは、企業の中の一部門の方に限られる」 という背景です。 つまり、利用テナント数は多いがテナント内のユーザー数は決して多くないという想定です。 多くの製品では、テナント数の制約に上限があるか追加コストが必要でした。 また、ユーザー数が増えるほどコストが下がるような恩恵もこの形態では得られません。</p> <p>そこで、認証サービスについては単一の製品で機能を実現することは諦め、低コストのID管理サービスに自分達で必要な機能を追加するという方針をとっています。</p> <h3 id="認証と認可の分離">認証と認可の分離</h3> <p>マルチテナントのユーザー認証と同時に検討していたのが、認証と認可の分離です。 一般的にマルチテナントというと、認証認可に関する機能すべてが独立したものとして扱われます。 例えば、署名なOIDC/OAuth2の<a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a>である <a class="wp-editor-md-post-content-link" href="https://www.keycloak.org/">Keycloak</a> のマルチテナント機能は、 テナントごとにユーザー管理だけでなく、OIDC/OAuth2クライアントの設定が全て独立するというものです。</p> <p>CADDiのプロダクトにおけるマルチテナントの要件を検討した結果、すべての設定が独立していてはまずいケースがあることがわかりました。 CADDiにおけるテナントとはプロダクトを利用する企業(顧客やパートナー)です。 マルチテナントとして全ての設定が独立している場合、あるプロダクトはその企業専用のデータを持つことになります。 私たちのプロダクトも各テナントごとに専用の設定でデプロイする必要があります。 (もっと正確に言えば、プロダクトごとに発行したOAuth2クライアントを識別して取り回す必要があります。) また、プロダクトによっては企業間で問い合わや質疑応答をするといったような、単一のプロダクトを複数の企業で使うような形式もありえます。</p> <p>つまり私たちのプロダクトは利用者がマルチテナントである必要はあるが、プロダクトは複数テナントのユーザーを扱える必要があります。</p> <p>ここで思い至ったのは、ユーザーの認証とプロダクトの認可を分離するというア<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%C7%A5%A2">イデア</a>です。 プロダクトのログインは単一だが、ログインのプロセス中で利用者が所属テナントを入力してログインします。 プロダクトは認証の結果として、テナントと利用者IDの2つの属性でユーザーを特定します。 似た仕組みとしては <a class="wp-editor-md-post-content-link" href="https://auth0.com/docs/manage-users/organizations">Auth0 Organizations</a>がありますが、 私たちの認証認可基盤では企業ごとの独立性を、より高めています。</p> <p>このような認可・認証の分離を実現するために、<a class="wp-editor-md-post-content-link" href="https://www.ory.sh/hydra/">Ory Hydra</a>を認可サービスとして採用しました。 Hydra は <a class="keyword" href="https://d.hatena.ne.jp/keyword/OpenID">OpenID</a> Foundationによって認証された OIDC/OAuth2 の<a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a>のライブラリです。 特徴的なポイントは OAuth2の機能に特化していて、ログインに関する機能は Pluggableになっていることです。 詳細は <a class="wp-editor-md-post-content-link" href="https://www.ory.sh/docs/hydra/login-consent-flow">Hydraのログインフローのドキュメント</a>を参照してください。 Hydraはログインプロセスの中で設定に記載されたログインエンドポイントを呼び出し、ログインエンドポイントは認証結果をHydraに返します。 Hydraはその結果から、IdToken,AccessTokenを発行し、そのあとはHydraがログインセッションや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを管理します。</p> <p>この仕様を守っていれば任意のログイン処理が利用できるため、前述のマルチテナント認証サービスをHydraと組み合わせることで要件を実現しています。</p> <p>余談ですが、Oryは<a class="wp-editor-md-post-content-link" href="https://www.ory.sh/kratos/">Kratos</a>というID管理の<a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a>も提供していて、これらを組み合わせた <a class="wp-editor-md-post-content-link" href="https://www.ory.sh/">Ory</a>というIDaaSを展開しています。 残念ながら、このサービスも私たちの想定するマルチテナントの機能はありませんでしたので、今回はHydraの<a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a>版を使用しています。</p> <h3 id="セキュアなAPI間通信">セキュアな<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>間通信</h3> <p>最後に<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>間通信です。</p> <p>私たちがプロダクトに実装してきた認証処理は、主に人がログインする前提で設計していたため、プロダクト間で安全に<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を呼ぶための仕組みやルールが不足していました。 過去にはブラウザでログインした後に、開発者ツールでアクセス<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを抜き出して <a class="keyword" href="https://d.hatena.ne.jp/keyword/curl">curl</a>でAuthorization Header に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを設定するといったようなことまでしていました。</p> <p>そのほかに、内部利用を想定していたため、そもそも認証がかかっていない<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>もあります。</p> <p>私たちが主に利用している Auth0 にも <a class="wp-editor-md-post-content-link" href="https://auth0.com/blog/using-m2m-authorization/">Mchine to Machine Token (M2M Token)</a> という仕組みがあります。 ただし、M2M Tokenは月間の発行上限が決まっているため、内部利用ならともかく外部公開を前提とすると、想定外の使い方をするクライアントがいた場合、発行上限を迎える可能性が常にあるため、少々使い勝手が悪いものでした。 また、<a class="wp-editor-md-post-content-link" href="https://auth0.com/docs/get-started/apis/set-logical-api">Auth0は複数のAPIの呼び出しを複数のAudienceとして設定できないという仕様</a> があるため、<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>連携を拡大していく方針と相性が良くありませんでした。</p> <p>そこで、まずは<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>用のアクセス<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを発行する機能として、前述の Hydraを使用します。 Hydraは Client Credentials Grantによるアクセス<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンの発行ができます。 また最近のアップデートによってWebHookでアクセス<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンをカスタマイズできるようになったため、任意の属性をクライアントごとに付与できます。</p> <p>次に、リソースサーバー(<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>)を管理する仕組みを自作しました。</p> <p>リソースサーバーにはOAuth2スコープや、WebHookで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンをカスタマイズするための<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">スクリプト</a>を設定できるようにしました。 この仕組みとHydraを連携して、クライアントがアクセス可能なリソースサーバーとスコープを厳密に管理できるようにしています。</p> <p>最後に、<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>側の実装を簡略化するプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスの提供です。</p> <p>私たちの認証認可基盤はあくまでプロダクトの認証の結果として<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを発行し、それをカスタマイズすることだけです。 プロダクトで行う<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンの検証などはプロダクト側の責務ですが、なるべく実装を省力化することも視野に入れています。 私たちはプロダクトを GKEや Cloud Run上で稼働させていて、 GKE ではサービスメッシュを導入し、Cloud Runでは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>コンテナの設定ができるようになりました。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>コンテナ上で、アクセス<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンの検証を実施したり、<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を呼び出した時に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>コンテナで透過的にアクセス<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを発行してリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トに付与するといった仕組みを検討・実装しています。</p> <h2 id="まとめ">まとめ</h2> <p>これまでに解説した内容を図にまとめると次のようになります。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173336.png" alt="achitecture" /></p> <p>複数テナントのユーザーアカウントを認証する認証サービスと、プロダクトからのOAuth2リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを処理する認可サービス(hydra)があり、 これらを設定するためのAdmin UIがあります。</p> <p>認証認可基盤を利用するプロダクト(クライアントとリソースサーバー)は、Amdin UI上からクライアントとリソースサーバーの情報を設定しておきます。</p> <p>認可サービスにOAuth2に従った認証リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを行うことで、その結果としてアクセス<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンを得るので、アクセス<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンをリソースサーバーへのリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トに付与します。</p> <p>その際、人によるログインであれば認証サービスのログイン処理が行われテナントを特定してログインを実施します。またその場合、ID<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンも同時に発行されます。</p> <p>リソースサーバーでは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B5%A5%A4%A5%C9%A5%AB%A1%BC">サイドカー</a>自身でアクセス<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A1%BC%A5%AF">トーク</a>ンの検証を行い、問題なければ処理を続行します。</p> <p>以上が現時点での認証認可基盤の内容です。 認証認可基盤はまだ完成しておらず、今後も次のような機能を実装予定です。</p> <ul> <li>ユーザー管理<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>とUIの作成</li> <li>パスワードポリシーなどのセキュリティ機能</li> <li>運用監視の向上</li> </ul> <p>また、認証認可基盤の開発がひと段落したら、他にもプロダクト横断の共通機能の開発を継続的に行っていく予定です。</p> <p>私たちのグループでは以下のような方をお待ちしています。</p> <ul> <li>拡大していくプロダクトの開発を支えたり共<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%B2%BD">通化</a>することに興味がある方</li> <li>認証認可、OIDC/Oauth2に興味がある方</li> <li>Platform Engineeringに興味がある方</li> <li>SREに興味がある方</li> <li>SREに興味がありつつ、開発もしたい方</li> </ul> <p>興味がある方はぜひ一度お話しましょう。</p> <ul> <li><p><a class="wp-editor-md-post-content-link" href="https://recruit.caddi.tech/">CADDi エンジニア向け採用情報</a></p></li> <li><p><a class="wp-editor-md-post-content-link" href="https://open.talentio.com/r/1/c/caddi-jp-recruit/pages/78398/apply">カジュアル面談お申し込みフォーム</a></p></li> </ul> caddi-tech 第2回 クラウドネイティブなインフラのTerraformによるIaC化 hatenablog://entry/6801883189086857883 2023-07-21T14:28:13+09:00 2024-02-28T20:07:51+09:00 ※本記事は、技術評論社「Software Design」(2023年5月号)に寄稿した連載記事「Google Cloudで実践するSREプラクティス」からの転載です。発行元からの許可を得て掲載しております。 はじめに 第1回(本誌2023年4月号)では、キャディにおけるGoogle Cloudを中心としたサービス基盤の全体像を紹介し、信頼性向上のために筆者らが心掛けている技術選定基準について触れました。 今回からは数回にわたって、Terraformを中心としたIaC(Infrastructure as Code)の実践例を紹介予定です(図1)。 ▼図1 CADDiスタックにおけるTerrafo… <p>※本記事は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%BB%BD%D1%C9%BE%CF%C0%BC%D2">技術評論社</a><a class="wp-editor-md-post-content-link" href="https://gihyo.jp/magazine/SD/archive/2023/202305">「Software Design」(2023年5月号)</a>に寄稿した連載記事「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudで実践するSREプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティス」からの転載です。発行元からの許可を得て掲載しております。</p> <h1 id="はじめに">はじめに</h1> <p>第1回(本誌2023年4月号)では、キャディにおける<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudを中心としたサービス基盤の全体像を紹介し、信頼性向上のために筆者らが心掛けている技術選定基準について触れました。</p> <p>今回からは数回にわたって、<a class="wp-editor-md-post-content-link" href="https://www.terraform.io/">Terraform</a>を中心としたIaC(Infrastructure as Code)の実践例を紹介予定です(図1)。</p> <p>▼図1 CADDiスタックにおけるTerraformの位置付け <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208163500.png" alt="" /></p> <p>今回は「インフラの信頼性」という側面で考えるIaCの重要性と、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudを中心とした<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>ベースのシステムのIaCにおいて、なぜTerraformとの相性が良いのかを説明します。</p> <h1 id="クラウドネイティブにおけるIaCの必要性"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>ネイティブにおけるIaCの必要性</h1> <p>システムのインフラをコードとして記述すること、すなわちInfrastructure as Code(IaC)は、すでに当たり前のプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスとなりつつあります。たとえば、オンプレミス上のシステムや、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D1%A5%D6%A5%EA%A5%C3%A5%AF%A5%AF%A5%E9%A5%A6%A5%C9">パブリッククラウド</a>上で<a class="keyword" href="https://d.hatena.ne.jp/keyword/Amazon%20Web%20Services">Amazon Web Services</a>(<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a>)のEC2など<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B2%BE%C1%DB%A5%DE%A5%B7%A5%F3">仮想マシン</a>(<a class="keyword" href="https://d.hatena.ne.jp/keyword/VM">VM</a>)ベースのシステムを構築する際は、Ansibleを使ってIaCを実現する事例が増えてきました。また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> CloudFormationを利用して<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a>上のリソースを作成している方も多いでしょう。</p> <h2 id="手作業によるインフラ管理の課題とIaCによる解決">手作業によるインフラ管理の課題とIaCによる解決</h2> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a>、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Microsoft">Microsoft</a> Azureなどの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D1%A5%D6%A5%EA%A5%C3%A5%AF%A5%AF%A5%E9%A5%A6%A5%C9">パブリッククラウド</a>は、いずれもWeb上のUIから<a class="keyword" href="https://d.hatena.ne.jp/keyword/VM">VM</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>を起動するなどの操作が可能で、手軽なことがメリットの1つです。その一方、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>ネイティブな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>で実運用可能なシステムを構築するには、さまざまなサービスを組み合わせる必要があり、膨大な設定が必要となります。 これらをWebUIからいわゆる「ポチポチ」で設定するのは非現実的で、言うまでもなく次のような問題が生じます。</p> <ul> <li>構築作業に時間がかかる</li> <li>設定内容を記録しにくい</li> <li>全体の見通しが悪く、レビューしにくい</li> </ul> <p>WebUIによるインフラ構築は手軽であるため、学習やコンセプト検証など、試行錯誤が主体の作業とは相性が抜群です。しかし、実運用するシステムを構築・維持するうえでWebUIだけが設定手段となっている場合は、従来のサーバ構築よりも非効率になってしまいます。</p> <p>また、WebUIでは設定内容の記録が困難なことも大きな問題です。ここから、さまざまな問題が派生します。 通常、システム運用時には、開発環境、テスト環境、本番環境など、複数の環境が必要になります。これらを手作業で同じように構築するのは手間がかかりますし、ミスも発生します。 記録が困難であることからノウハウを残しにくく、インフラの品質も属人的スキルに依存しがちになります。変更管理ができないことから、インフラの変更意図がわかりにくく、障害発生時に元の構成に戻すことも困難でしょう。</p> <p>設定内容のレビューをするにしても、Web上の管理画面から確認が必要で、大規模なシステムでは確認箇所が広範囲にわたります。このように見通しが悪い状況ではレビュー効率が悪く、問題を適切に見つけることも困難です。</p> <h3 id="IaCによる解決">IaCによる解決</h3> <p>そこで必要となるのが、IaCの考え方です。 旧来なら、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B7%A5%A7%A5%EB%A5%B9%A5%AF%A5%EA%A5%D7%A5%C8">シェルスクリプト</a>などでOSの状態を変更したり、設定ファイルを書き換えたりといった手続き的な処理の自動化も、広義のIaCと捉えることもできました。すなわち、「サーバ構築手順書」をそのままプログラム化するような考え方です。</p> <p>また、主要な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスでは、インフラ操作のための<a class="keyword" href="https://d.hatena.ne.jp/keyword/CLI">CLI</a>コマンドが提供されています。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a>であれば<code>aws</code>、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudでは<code>gcloud</code>、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Microsoft">Microsoft</a> Azureなら<code>az</code>といったコマンドです。 WebUIの代わりにこれらの<a class="keyword" href="https://d.hatena.ne.jp/keyword/CLI">CLI</a>を利用することで、効率や記録の問題はある程度解決できるでしょう。 しかし、<a class="keyword" href="https://d.hatena.ne.jp/keyword/CLI">CLI</a>コマンドによる構築は手続き的です。このため、見通しの悪さやレビューのしにくさの改善に対しては、あまり寄与しません。 現代の理想的なIaCとしては、インフラの「状態」を宣言的に記述することが考えられます。ツールによって、記述されたコードの状態と等しくなるように、インフラを自動設定するのです。</p> <h2 id="インフラの信頼性に寄与するIaC">インフラの信頼性に寄与するIaC</h2> <p>さて、IaCというと「自動化による効率改善」というイメージが強いかもしれません。もちろんこれは大きなメリットであり、「開発」「テスト」「本番」など、複数環境の構築も容易になります。 しかし、筆者らがそれ以上に大きなメリットと捉えているのが、次に挙げるような信頼性向上への寄与です。</p> <ol> <li>レビューしやすくなる</li> <li>ツールによるチェックができる</li> <li>変更管理ができる</li> <li>再現性・自動デプロイに繋げられる</li> <li>再利用化で知見が共有できる</li> </ol> <h3 id="プルリクエストによる相互レビュー">プルリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トによる相互レビュー</h3> <p>IaC化されたコードは<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>などの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>で管理でき、アプリケーションコードと同じようにプルリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トによるレビューが可能になります。これによって、構築や変更前にチェックができ、品質向上につながります。</p> <h3 id="ツールによる自動チェック">ツールによる自動チェック</h3> <p>さらに、チェックツールを導入することでレビュー自体を半自動化できます。 たとえば、筆者らがIaCツールとして採用しているTerraformでは、<a class="wp-editor-md-post-content-link" href="https://github.com/terraform-linters/tflint">tflint</a>や<a class="wp-editor-md-post-content-link" href="https://github.com/aquasecurity/tfsec">tfsec</a>といったチェックツールがあります。tflintでは、設定内容の妥当性、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CC%BF%CC%BE%B5%AC%C2%A7">命名規則</a>、ベストプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスに従っているかなど、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%ED%A5%B0%A5%E9%A5%DF%A5%F3%A5%B0%B8%C0%B8%EC">プログラミング言語</a>の静的チェックツールと同じようなチェックが可能です。tfsecでは、セキュリティ上問題となる設定がないかの確認ができます。 このような自動チェックを事前に実施することで、人間によるレビューはより本質的な点に絞ることができ、作業効率と精度の両面に寄与できます。</p> <h3 id="変更管理が可能">変更管理が可能</h3> <p>コード<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>で変更管理が可能になることで、構成変更に起因する障害が発生したとき、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>もしやすくなります。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>ネイティブなシステムでは、アプリケーションとインフラ構成が密接に絡みます。たとえば、アプリケーションが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D1%A5%D6%A5%EA%A5%C3%A5%AF%A5%AF%A5%E9%A5%A6%A5%C9">パブリッククラウド</a>の提供するキューイングサービス(<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a>ならSQS、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> CloudならCloud Pub/Subなど)を利用するとします。その場合、アプリケーションコードと、キューを構成するインフラコードのバージョンには整合性が求められるでしょう。 インフラがIaC化されて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>上で管理されていれば、アプリケーションとインフラの整合性を取るのが容易になります。</p> <h3 id="再現性">再現性</h3> <p>IaC化の大きなメリットは再現性が得られることですが、これには信頼性向上という側面もあります。たとえば、誤って環境を壊してしまっても元に戻すことができるので、ダウンタイムを最小化できます。 筆者らは過去に、あるプロダクトの開発環境において不注意で<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>(<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a>)<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>を削除してしまったことがありました。幸いにもこれらはIaC化されていたため、比較的短時間で復旧できました。 また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%B6%A5%B9%A5%BF%A5%EA%A5%AB%A5%D0%A5%EA">ディザスタリカバリ</a>観点でも事業の継続性に寄与します。</p> <h3 id="再利用化による知見の共有">再利用化による知見の共有</h3> <p>手作業によるインフラ構築は、ノウハウが担当者の中に閉じがちで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B0%C5%CC%DB%C3%CE">暗黙知</a>となりやすいです。 TerraformやAnsibleなど、たいていの構成管理ツールでは、コードをモジュール化して再利用する機能が提供されています。ノウハウをモジュール化して再利用することで、インフラの品質向上が望めます。</p> <p>たとえば、Terraformの<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud向けモジュールでは、管理者がシステムにアクセスするための「踏み台サーバ(Bastion server)」を構築するためのモジュール<a class="wp-editor-md-post-content-link" href="https://registry.terraform.io/modules/terraform-google-modules/bastion-host/google/latest">bastion-host</a>が公開されています。 このモジュールでは、ネットワークや認証のしくみもセットで構築してくれるので、踏み台サーバ構築のノウハウが再利用できる状態と言えます。</p> <h2 id="IaCのデメリット">IaCのデメリット</h2> <p>一方で、次のような点はIaCのデメリットと捉えられることもあります。</p> <ul> <li>初期構築が大変</li> <li>変更コストがかかる</li> </ul> <h3 id="初期構築の大変さ">初期構築の大変さ</h3> <p>初期構築について、とくに慣れない段階では時間がかかります。筆者らも開発チームからIaCについて相談を受ける際は、開発初期段階では無理にIaC化しなくても良いとアド<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%A4%A5%B9">バイス</a>しています。 IaC化を進めるには、手作業で構築してから、ツールでコード生成するのが1つの方法です。 たとえばTerraformの場合、主要な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D1%A5%D6%A5%EA%A5%C3%A5%AF%A5%AF%A5%E9%A5%A6%A5%C9">パブリッククラウド</a>に対応している<a class="wp-editor-md-post-content-link" href="https://github.com/GoogleCloudPlatform/terraformer">Terraformer</a>や、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudであれば、<a class="wp-editor-md-post-content-link" href="https://cloud.google.com/docs/terraform/resource-management/export">gcloud resource-configコマンド</a>といった選択肢があります。 Terrformerは、利用する<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスに対応するTerraformプロバイダの手動インストールが必要という手間はありますが、さまざまな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>からコード生成できる点が魅力的です。 gcloud resource-configは、普段からgcloudコマンドを利用していれば、追加インストールが不要なので、手軽に利用できます。 筆者も現段階では両者を軽く試用した程度ですが、Terraformerの方がTerraformコードをきちんとファイルに分けて出力してくれるため、見通しが良さそうと感じています。 いずれにせよ、自動生成されたコードは土台と割り切ってしまい、それらを参考にしながらコードをきれいにしていく必要はあるでしょう。 また、組織として知見が溜まってくれば、ベースとなるコードを共有することで、イニシャルコストを下げられそうです。筆者らもこの点に関してはまだ手探りの段階です。</p> <h3 id="変更コスト">変更コスト</h3> <p>IaC化されたインフラの変更コストは、信頼性向上との裏返しではあります。インフラのコードをGit<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>で管理し、プルリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トをレビューしてから反映するというプロセスは、WebUIから変更するのに比べると手間がかかる印象を持たれるでしょう。 実際のところ、次回以降で説明するようにCI/CDパイプラインを構築することで多くの作業は自動化でき、それほどの手間はかからなくなります。</p> <p>しかし、とくに慣れないうちは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BF%B4%CD%FD%C5%AA">心理的</a>ハードルが高くなるのは否めません。たとえば「パフォーマンス調整のために<a class="keyword" href="https://d.hatena.ne.jp/keyword/K8s">K8s</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%B9%A5%BF">クラスタ</a>上でノードプールの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A4%A5%F3%A5%B9%A5%BF%A5%F3%A5%B9">インスタンス</a>数を変更する」といったケースでも、プルリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トとレビューが必要になります。 一方で、プルリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トに変更理由を記載して妥当性をレビューできるのは、インフラの信頼性を保つうえで大きなメリットです。このため、信頼性とアジリティ(機敏性)のバランスが重要です。 キャディのプロダクトにおけるインフラについては、IaCによる管理を基本としつつも、「検証のための一時的な変更などは、開発環境に限ってWebUIからの変更をOKとする」というルールを導入しました。このようにして、信頼性と変更コストのバランスをとっています。</p> <h1 id="Terraformを採用する理由">Terraformを採用する理由</h1> <p>筆者らがIaCツールとしてTerraformを採用する大きな理由は、次の2点です。</p> <ol> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービス上のリソースが扱える</li> <li>さまざまな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスに対応している</li> </ol> <h2 id="クラウドサービス上のリソースが扱える"><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービス上のリソースが扱える</h2> <p>IaCツールというとAnsibleが有名であり、本誌でもたびたび取り上げられています。しかし、AnsibleとTerraformでは、図2のように位置付けが少し異なります。</p> <p>▼図2 AnsibleとTerraformの想定ターゲット <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173328.png" alt="" /></p> <p>Ansibleの想定している主な役割はサーバ設定であり、OSよりも上のレイヤがターゲットです。一方Terraformは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>上のリソース作成や設定変更を主な役割と想定しており、Ansibleがカバーするレイヤをメインターゲットとしていません。</p> <p>AnsibleとTerraformはそれぞれ基本的なしくみも異なります。Ansibleは設定対象のサーバ(マネージドノードと呼ばれる)に<a class="keyword" href="https://d.hatena.ne.jp/keyword/SSH">SSH</a>でログインして「モジュール」と呼ばれる小さなプログラムを送り込み、そのモジュールがマネージドノードを設定します(図3上)。 一方、Terraformではターゲットとなる<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスのWeb <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を呼び出すことで、リソース作成や変更を実現します(図3下)。</p> <p>▼図3 Ansible(上)とTerraform(下)のしくみの違い <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208163454.png" alt="" /></p> <p>このような設計思想の違いはありますが、互いの領域が重なっていないわけではありません。 Ansibleでは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a>や<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudのリソースを構築するためのモジュールが提供されており、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスのリソース管理もできます。 一方、TerraformでもProvisionersという機能でAnsibleのようなサーバの内部の設定変更も実現できます。 ただし、Terraformに関しては<a class="wp-editor-md-post-content-link" href="https://developer.hashicorp.com/terraform/language/resources/provisioners/syntax">Provisioners</a> のドキュメントによると、あくまでも「最後の手段」と位置付けられていることから、主要な使い方でないことがわかります。</p> <p>なお、各<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスへの対応状況については注意が必要です。 筆者らが利用している<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudについては、Terraformでは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a>とHashiCorpのTerraformチームがメンテナンスする公式の<a class="wp-editor-md-post-content-link" href="https://github.com/hashicorp/terraform-provider-google">Provider</a> が提供されており、頻繁にメンテナンスされています。 一方Ansibleの<a class="wp-editor-md-post-content-link" href="https://galaxy.ansible.com/google/cloud">Google Cloud向けコレクション</a>の開発状況は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>の履歴を見る限りそれほど開発が活発ではなかったものの、2022年12月からリリース頻度が早くなっており、今後に注目です。</p> <p>なお、Ansibleにおける「コレクション」とは、一連の機能に関するモジュールの集まりのことです。</p> <h2 id="幅広いクラウドサービスへの対応">幅広い<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスへの対応</h2> <p>IaCツールを選定する際、まず<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービス固有のツールが選択肢に挙がるでしょう。 <a class="wp-editor-md-post-content-link" href="https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/">「AWS CloudFormation」</a> や、<a class="wp-editor-md-post-content-link" href="https://cloud.google.com/deployment-manager/docs?hl=ja">「Google Cloud Deployment Manager」</a>などです。 これらのツールは、当該<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスとの高い親和性が利点である一方、幅広い<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスには対応できないという弱点があります。</p> <p>Terraformには「Provider」という<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D7%A5%E9%A5%B0%A5%A4%A5%F3">プラグイン</a>機構があり、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスの対応はそれぞれの Provider によって実現されています。 Providerは、<a class="wp-editor-md-post-content-link" href="https://registry.terraform.io/">Terraform Registry</a>で公開されており、コード内で宣言するだけでTerraformが自動的にProviderをダウンロードしてくれます。このようなしくみから、Providerがあれば、さまざまな<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスに対応できます。 もちろん、必要に応じてProviderの自作もできます。</p> <p>また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a>、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud、Azureなど主要な<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D1%A5%D6%A5%EA%A5%C3%A5%AF%A5%AF%A5%E9%A5%A6%A5%C9">パブリッククラウド</a>については、HashiCorp社によるオフィシャルProviderが提供されているため、安心して活用できます。<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudでは、Terraformの利用に関する<a class="wp-editor-md-post-content-link" href="https://cloud.google.com/docs/terraform?hl=ja">公式ドキュメント</a>も充実しています。</p> <p>キャディのインフラでは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudを中心としつつも、さまざまなXaaSを組み合わせているため、インフラをコード化するツールとしてTerraformが最適といえるのです。</p> <h1 id="Column-Ansibleと比較してTerraformが良い点">[Column] Ansibleと比較してTerraformが良い点</h1> <p>筆者自身、キャディへ入社する前はAnsibleを長く利用していました。いささか主観的ですが、双方を使った経験から、Ansibleと比べてTerraformのほうが良いと感じる点を紹介します。なお、筆者が最後にAnsibleを触ったのは2021年秋ごろなので、その後改善されている点があればご了承ください。</p> <h4 id="純粋に宣言的な書き方ができる">純粋に宣言的な書き方ができる</h4> <p>Ansibleでは手続き的な書き方ができてしまうので、<a class="keyword" href="https://d.hatena.ne.jp/keyword/YAML">YAML</a>でプログラミングをするようなイメージになりがちです。また、コードを書くときにも冪等性を意識した書き方をする必要があり、習熟に時間がかかりました。コードを読み解くのにも慣れが必要です。 一方、Terraformのコードは純粋に宣言的なので、「あるべき姿を記述する」ことに集中でき、コードの可読性も高いです。冪等性は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>サービスまたは TerraformのProvider内部で担保されるためです。</p> <h4 id="エラーメッセージが読みやすい">エラーメッセージが読みやすい</h4> <p>Ansibleのエラーは、メッセージを含む<a class="keyword" href="https://d.hatena.ne.jp/keyword/JSON">JSON</a>が改行なしに出力されるため、読み解くのが非常に困難です。Terraform のエラーは整形されてわかりやすく表示されます(図A)。これはTerraformに限らず、HashiCorp製品全般に言えます。</p> <p>▼図A Terraformのエラー表示例 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208173302.png" alt="" /></p> <h4 id="環境の影響を受けない">環境の影響を受けない</h4> <p>設計上仕方のないことですが、Ansible自体が<a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a>で動作するため、ホストで実行される<a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a>の影響を受けます。 古いホストでAnsibleを動かす際、Ansibleが要求するバージョンの<a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a>がインストールされていないことがありました。 このため、venv<sup id="fnref-4294-1"><a href="#fn-4294-1" class="footnote-ref" role="doc-noteref">1</a></sup>でAnsible用の<a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a>環境を隔離するといった対応が必要になりました。 TerraformはGo言語で実装されており、ランタイムが不要なバイナリとして提供されるため、このような影響を受けることがありません。</p> <h4 id="特性の差に対する理解が必要">特性の差に対する理解が必要</h4> <p>一方で、TerraformにはState<sup id="fnref-4294-2"><a href="#fn-4294-2" class="footnote-ref" role="doc-noteref">2</a></sup>というAnsibleには無い概念があるため、その考え方を理解しないと混乱してしまいます。Stateとは管理対象リソースの状態を保存したファイルのことで、TerraformはStateを中心にコードと実環境の差分をチェックする考え方です。AnsibleはPlaybookの記述内容を正とすることで、冪等性を実現します。 また前述のように、TerraformはOSよりも上のレイヤに対しては使えません。たとえば、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>上に構築した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B2%BE%C1%DB%A5%DE%A5%B7%A5%F3">仮想マシン</a>の内部を設定したいといった場合は、依然としてAnsibleが有力な選択肢です。このようなケースで<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>自体のリソース管理もしたい場合は、すべてをAnsibleで統一するというのも良いかもしれません。</p> <p>キャディではすべてのアプリケーションをDockerコンテナ化しており、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B2%BE%C1%DB%A5%DE%A5%B7%A5%F3">仮想マシン</a>を使用するケースは踏み台サーバなど限られています。 AnsibleがカバーしているOSよりも上のレイヤはDockerfile として記述できるため、すべてをTerraformでカバーできています。</p> <h1 id="まとめ">まとめ</h1> <p>今回はまず、信頼性の高いインフラを運用する基盤となるIaCの考え方を紹介しました。そして、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudを中心とした<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>ネイティブなインフラを構築している弊社にとって、IaCツールとしてTerraformが最適であると判断した理由を解説しました。 今後は<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> ActionsとTerraformの組合せでインフラのCI/CD実践例を紹介する予定です。次回はまず、Terraformに触れたことがない方向けにTerraformの基本を解説します。お楽しみに。</p> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn-4294-1" role="doc-endnote"> 仮想環境を分離する<a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a>の標準機能です。&#160;<a href="#fnref-4294-1" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> <li id="fn-4294-2" role="doc-endnote"> Stateについては次回に詳しく解説します。&#160;<a href="#fnref-4294-2" class="footnote-backref" role="doc-backlink">&#8617;&#xFE0E;</a> </li> </ol> </div> y-komori 第3回 Digital Native Leader’s Meetup に参加しました hatenablog://entry/6801883189081010566 2023-07-11T10:00:51+09:00 2024-03-05T01:31:08+09:00 こんにちは。DRAWER SRE の廣岡です。最近は開発チーム内の権限付与方針の整備や、他チームのインフラ構築のサポートなどに取り組んでいます。 さて、キャディではサービス構築のために Google Cloud のマネージドサービスを多く利用しており、そのご縁で先日 Google Cloud 様主催の「Digital Native Leader’s Meetup」という企画に招待いただきました。本稿はこのイベントの参加レポートとなります。少しでもイベントの雰囲気を感じていただけると幸いです。 Digital Native Leader’s Meetup とは 本イベントは、Google Clo… <p>こんにちは。DRAWER SRE の廣岡です。最近は開発チーム内の権限付与方針の整備や、他チームのインフラ構築のサポートなどに取り組んでいます。</p> <p>さて、キャディではサービス構築のために Google Cloud のマネージドサービスを多く利用しており、そのご縁で先日 Google Cloud 様主催の「Digital Native Leader’s Meetup」という企画に招待いただきました。本稿はこのイベントの参加レポートとなります。少しでもイベントの雰囲気を感じていただけると幸いです。</p> <h1 id="Digital-Native-Leaders-Meetup-とは">Digital Native Leader’s Meetup とは</h1> <p>本イベントは、Google Cloud 様が主催するエンジニア向けのネットワーキングイベントです。今回はキャディの窓口担当の方にご紹介いただき、参加いたしました。</p> <p>第3回となる今回ではデータベースをテーマに、Google Cloud 製品のアップデート情報や、他のユーザー企業様によるサービス利用事例のライトニングトーク、参加者同士のアンカンファレンスが実施されました。これらを通して Google Cloud のサービス利用や、利用シーンにおける知見を交換し、プロダクト開発を加速するのが目的となっています。</p> <h2 id="会場の様子">会場の様子</h2> <p>会場は Google 渋谷オフィスでのオフライン開催でした。ユーザー企業からは40人程度が参加していたかと思います。軽食やお酒も用意いただいており、カジュアルにお話を聞くことができました。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208163506.jpg" alt="" /></p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208163510.jpg" alt="" /></p> <h1 id="イベントの内容">イベントの内容</h1> <p>イベントのコンテンツとしては以下の通りでした。 - Google Cloud サービスアップデート - ユーザー企業様による Lightning Talk - 参加者によるアンカンファレンス Google Cloud のサービスアップデートについては非公開情報のため、本記事では掲載できないのですが、Google の技術力を感じさせる内容であり、今後のアップデートが楽しみになりました。</p> <h2 id="ユーザー企業様による-Lightning-Talk">ユーザー企業様による Lightning Talk</h2> <p>LT では、AlloyDB へのデータベース移行事例や、Cloud Spanner の入門・導入事例の紹介がありました。</p> <p>AlloyDB はパフォーマンスやスケーラビリティ、可用性に優れたフルマネージドデータベースサービスです。事例紹介ではデータベースの移行サービスである Database Migration Service と合わせた取り組みを紹介いただきました。</p> <p>データベースの移行といえばかなり慎重を要する作業ですが、これらのマネージドサービスを用いてうまく移行を進められたとのことでした。また実際使ってみたからこそわかるような AlloyDB の良い点や、将来への改善要望なども伺うことができ、非常に参考になりました。</p> <ul> <li><a class="wp-editor-md-post-content-link" href="https://cloud.google.com/alloydb?hl=ja">PostgreSQL 向け AlloyDB</a></li> <li><a class="wp-editor-md-post-content-link" href="https://cloud.google.com/database-migration?hl=ja">Database Migration Service | Google Cloud</a></li> </ul> <p>Cloud Spanner は強整合性とグローバルなスケーラビリティ、そして高い可用性を持つフルマネージドデータベースサービスです。LT では Spanner の入門的な内容と、導入事例が紹介されていました。実際に導入した感想としては、やはりスケーラビリティが非常に優れているとのことでした。</p> <p>個人的に Spanner は大規模サービス向けだと思っていたのですが、小規模にも柔軟にスケールできるというお話があり、意外に感じると共に見識を深めることができました。</p> <ul> <li><a class="wp-editor-md-post-content-link" href="https://cloud.google.com/spanner?hl=ja">Cloud Spanner</a></li> <li><a class="wp-editor-md-post-content-link" href="https://cloud.google.com/alloydb?hl=ja#section-6">PostgreSQL 向け AlloyDB - AlloyDB と Google Cloud のそれ以外の PostgreSQL オプションを比較する</a></li> </ul> <h2 id="参加者によるアンカンファレンス">参加者によるアンカンファレンス</h2> <p>アンカンファレンスでは、各テーブルの参加者でお互いの業務で抱えている悩みや、データベースサービス活用における洞察などを共有、ディスカッションしました。</p> <p>私は SRE という業務の性質上、データベースを扱うシーンが多いわけではありません。一方で図面活用 SaaS である DRAWER では、大量の図面処理に応じてデータベースへの負荷が高くなるシーンもあり、そういった場合の対策や AlloyDB、Spanner などとの適性についてもディスカッションさせていただきました。</p> <p>本イベントはデータベースがメインテーマでしたが、私のテーブルでは私と同じ SRE として勤めている方のほかに、プロダクトマネージャーや CTO に近いような立場で開発に関わっている方もおり、さまざまな観点から議論ができました。また Google Cloud の担当者の方も同席いただいていたため、各サービスの疑問点や改善要望などもカジュアルに話すことができ非常に有意義でした。</p> <p>他にもいろいろな内容がディスカッションされていました。以下に一部を記載します(サービスや事業の特定を避けるために一部修正しています)。</p> <ul> <li>実際 Spanner は使ってみてどうか?冗長性はどのように設定しているか?</li> <li>Spanner はトラフィックの変動にはどれくらい追従できるか?</li> <li>データベースのバックアップ、復旧訓練はどのように実施しているか?</li> <li>データベースのコスト適正化のために実施していることは?、など</li> </ul> <p>お互いが関わっているサービスは別々なのですが、データの特性や運用上の悩みなどは共通しているものもあり、純粋にエンジニアのミートアップとしても楽しい時間を過ごすことができました。</p> <p><img src="https://assets.caddi.com/tech-blog/wp-content/uploads/2023/07/スクリーンショット-2023-07-04-20.30.21-1024x577.png" alt="" /></p> <h1 id="終わりに">終わりに</h1> <p>Google Cloud 様主催のネットワーキングイベントである、Digital Native Leader’s Meetup に参加させていただきました。</p> <p>特にアンカンファレンスでは、実際に使ったからこそ分かるような勘所など、貴重なお話を伺うことができました。データベースのように慎重な選定や運用が求められる領域において、このような先進的な事例を共有いただけるのは非常にありがたいと感じました。</p> <p>また Google Cloud の皆様の配慮により、快適に聴講やディスカッションに参加できました。ご招待いただき改めてお礼を申し上げます。</p> <p>本イベントで得られた知識や洞察をもとに、キャディでは引き続き製造業の変革に貢献するようなプロダクト開発を進めていきます。ご興味のある方はぜひお気軽にご連絡いただけると幸いです。</p> <ul> <li><a class="wp-editor-md-post-content-link" href="https://recruit.caddi.tech/">CADDi Tech</a></li> </ul> caddi-tech 第1回 信頼性を高めるサービス基盤と技術選定 hatenablog://entry/6801883189081010595 2023-06-28T18:00:50+09:00 2024-02-08T16:34:45+09:00 ※本記事は、技術評論社「Software Design」(2023年4月号)に寄稿した連載記事「Google Cloudで実践するSREプラクティス」からの転載です。発行元からの許可を得て掲載しております。 はじめに キャディ株式会社の前多です。筆者はPlatformグループという部署で、クラウドインフラの整備や開発組織横断の技術課題の解消に携わっています。 キャディでは製造業向けのビジネスを展開しており、社内外向けにSaaSを含む多くサービスを運用しています。また、事業展開にあわせて常に新たなプロダクトが開発されています。 各サービスには担当の開発チームが組織されていて、開発・運用に責任を負… <p>※本記事は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%BB%BD%D1%C9%BE%CF%C0%BC%D2">技術評論社</a><a class="wp-editor-md-post-content-link" href="https://gihyo.jp/magazine/SD/archive/2023/202304">「Software Design」(2023年4月号)</a>に寄稿した連載記事「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudで実践するSREプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティス」からの転載です。発行元からの許可を得て掲載しております。</p> <h2 id="はじめに">はじめに</h2> <p>キャディ株式会社の前多です。筆者はPlatformグループという部署で、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>インフラの整備や開発組織横断の技術課題の解消に携わっています。 キャディでは製造業向けのビジネスを展開しており、社内外向けに<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>を含む多くサービスを運用しています。また、事業展開にあわせて常に新たなプロダクトが開発されています。</p> <p>各サービスには担当の開発チームが組織されていて、開発・運用に責任を負っています。筆者らPlatformグループは、開発チームが自律的にユーザーへの価値提供に集中できることを目標に、SREプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスの導入、信頼性の高いサービス基盤やサービス横断の機能開発といった活動をしています。 サービスの開発・運用主体は開発チームであるため、筆者らは個々のサービスに対するインフラ構築やサービス運用、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A1%BC%A5%AD%A5%C6%A5%AF%A5%C1%A5%E3">アーキテクチャ</a>設計や言語・ライブラリ等の技術選定といった作業を行いません。開発チームがこれらを主体的に進められるよう、サービスの基盤や監視基盤を整えたり、ガイドの作成や啓蒙、SREプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスの実践サポートなどが主な役割です。</p> <p>筆者らPlatform グループと開発チームの関係は次の図のようになります。 詳しくは<a class="wp-editor-md-post-content-link" href="https://caddi.tech/archives/4088">弊社のブログ</a>で紹介していますので、興味ある方はぜひ ご覧ください。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208163442.png" alt="" /></p> <p>現在のキャディは事業成長フェーズにあり、開発組織の拡大に伴い、さまざまな課題が発生しています。このような状況に対応するため、Platformグループでは、少人数でも<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EC%A5%D0%A5%EC%A5%C3%A5%B8">レバレッジ</a>の効くような戦略的・技術的な解決手法の提供も目指しています。 その取り組みの1つが、高い信頼性と開発組織のスケールへ追従できるサービス基盤の提供です。 この連載では、筆者らが提供するGoogleCloudを中心とした組織横断の基盤について、技術選定の方針と、採用している各種技術要素について解説していきます。</p> <h2 id="信頼性とは何か">信頼性とは何か</h2> <p>信頼性とは、サービスが一定の条件下で要求された機能を果たす性質であり、サービス利用者が遅延や障害などにより機会損失する度合いを管理していくことです。 ソフトウェアエンジニアリングによってサービスの信頼性を向上させる役割を果たすのが、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a>によって提唱されたSRE(Site Reliability Engineering)です。</p> <p>書籍『SRE サイトリライアビリティエンジニアリング ―<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a>の信頼性を支えるエンジニアリングチーム』や、<a class="wp-editor-md-post-content-link" href="https://cloud.google.com/sre">Google Cloud の SRE ページ</a>では、SREの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%A4%A5%F3%A5%C9%A5%BB%A5%C3%A5%C8">マインドセット</a>やツールセットについてべられています。</p> <p>高い信頼性を示すには、信頼性を数値化して計測することが必要です。以前から信頼性の尺度として<a class="keyword" href="https://d.hatena.ne.jp/keyword/MTBF">MTBF</a>(<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CA%BF%B6%D1%B8%CE%BE%E3%B4%D6%B3%D6">平均故障間隔</a>)、<a class="keyword" href="https://d.hatena.ne.jp/keyword/MTTR">MTTR</a>(平均復旧時間)といったものがあり、最近ではSREプ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E9%A5%AF">ラク</a>ティスの1つとして紹介されたSLI(サービスレベル指標)、SLO(サービスレベル目標)を使うこともあります注4。筆者らもサービスごとにSLI、SLOを定義して監視・運用することを標準化し、開発チームへの導入を始めたところです。 信頼性を高く保つためには、信頼性の可視化だけではなく、サービスの品質を底上げしていくための取り組みも必要です。そのために筆者らが導入している技術要素を次に見ていきます。</p> <h3 id="コラム-信頼性の尺度">コラム : 信頼性の尺度</h3> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/MTBF">MTBF</a>(<a class="keyword" href="https://d.hatena.ne.jp/keyword/%CA%BF%B6%D1%B8%CE%BE%E3%B4%D6%B3%D6">平均故障間隔</a>)はサービスが故障せずに稼働できる平均時間で、長いほど故障がしづらいと言える尺度です。一方<a class="keyword" href="https://d.hatena.ne.jp/keyword/MTTR">MTTR</a>(平均復旧時間)はサービスが故障から復旧までにかかる平均時間で、短いほど故障から復旧が早いと言える尺度です。 この2つの尺度から<a class="keyword" href="https://d.hatena.ne.jp/keyword/MTBF">MTBF</a>÷(<a class="keyword" href="https://d.hatena.ne.jp/keyword/MTBF">MTBF</a>+<a class="keyword" href="https://d.hatena.ne.jp/keyword/MTTR">MTTR</a>)を計算すると<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B2%D4%C6%AF%CE%A8">稼働率</a>が得られます。たとえば<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B2%D4%C6%AF%CE%A8">稼働率</a>が99% なら、年間でサービスが停止しているのは約87時間、月間では約7時間です。信頼性を計測する方法の1つとして、目標とする<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B2%D4%C6%AF%CE%A8">稼働率</a>を設定して実際の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B2%D4%C6%AF%CE%A8">稼働率</a>を計測します。 SLI(サービスレベル指標)とSLO(サービスレベル目標)は、サービスの利用者がサービスを安定して使えているかという観点で設定します。SLIは、サービスの状態を計測して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%EA%CE%CC">定量</a>化した値です。サービスの特性によって独自に決めます。汎用的なものとして、サービスへのリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トの遅延時間やエラー率などがあります。 一方でSLOは、サービスが安定して稼働しているかを判断するためのSLIの目標値です。 たとえば「月間のエラー率(SLI)を1%以下にする」といったものです。SLOを満たしていないようであれば、SLOを満たすために改善作業を実施します。</p> <p>SLIとSLOについて詳しくは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a>が公開している <a class="wp-editor-md-post-content-link" href="https://sre.google/intl/ja_jp/resources/practices-and-processes/art-of-slos/">The Art of SLOs</a> を参照してください。</p> <h2 id="技術選定の観点">技術選定の観点</h2> <p>サービス基盤の技術選定に際して筆者らは次の4点を重視しています。</p> <ul> <li>IaC(Infrastructure as Code)</li> <li>自動化</li> <li>可観測性(Observability)</li> <li>セキュリティ</li> </ul> <h3 id="IaCInfrastructure-as-Code">IaC(Infrastructure as Code)</h3> <p>筆者らが使用する<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D1%A5%D6%A5%EA%A5%C3%A5%AF%A5%AF%A5%E9%A5%A6%A5%C9">パブリッククラウド</a>や<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>の構築作業は、可能な限りコード化(IaC)し、CI/CDパイプラインに載せて作業を自動化しています。<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>が提供されているツールを選択し、UIが提供されていたとしても手動による変更は原則として行いません。 こうすることで、複数環境の構築や複製が簡単になったり、環境に加えた変更の差分が明確になるといった利点があります。また、コード化によってインフラ構築のノウハウが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B7%C1%BC%B0%C3%CE">形式知</a>化されるので、属人性の排除や手順書に基づくインフラ構築といった煩雑な作業からの解放につながります。 IaCと次に説明する自動化により、サービス拡大に伴うインフラ構築の負荷を最低限に減らすことができます。</p> <h3 id="自動化">自動化</h3> <p>信頼性を高く保つためには、サービスの品質を上げることが重要です。 そのためには「テストをしてバグを減らす」「最新のライブラリを使う」「新機能や改善を取り込んだサービスを早くリリースする」などの行動が必要です。これらの行動を繰り返すことで品質は向上します。 繰り返しの速度を上げるためには、自動化が欠かせません。 自動化の方法として CI(継続的イングレーション)とCD(継続的デプロイメント)が知られています。 キャディでもこの2つを合わせたCI/CDパイプラインを整備して、サービスのテスト、ビルド、デプロイを繰り返し実行できるようにしています。 また、自動化の推進によって、特定の人しかデプロイができないといったような属人化を減らすことにもつながります。</p> <h3 id="可観測性Observability">可観測性(Observability)</h3> <p>信頼性を計測するためには、稼働しているサービスから指標となるデータを取得する必要があります。また、パフォーマンスの劣化やサービス障害に対する調査も、勘に頼ったり場当たり的に行ったりするのではなく、実測値に基づいて行うことが重要です。 そのために、サービスや利用するツールからログやメトリクスなどのデータが取得できること、それらのデータを一元的に収集して分析可能であることを重視します。</p> <h3 id="セキュリティ">セキュリティ</h3> <p>キャディでは創業当初からシステムのすべてが<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D1%A5%D6%A5%EA%A5%C3%A5%AF%A5%AF%A5%E9%A5%A6%A5%C9">パブリッククラウド</a>や<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>にあるため、社内ネットワークのような閉じたネットワークはありません。また、多くの社員がリモートワークをしています。 そこで、筆者らはゼロトラストネットワークの考え方の基づいてサービスを構築しています。 システムへのリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トは、原則として正当性の検証が必要であり、そのための認証認可や監査のしくみの標準化を進めています。 また、<a class="keyword" href="https://d.hatena.ne.jp/keyword/DDoS%B9%B6%B7%E2">DDoS攻撃</a>のような脅威からサービスを守るための方法も検討しています。</p> <h2 id="安定したサービス基盤に使う技術">安定したサービス基盤に使う技術</h2> <p>前述した技術選定の観点をふまえて、キャディのサービスが稼働している環境で実際に使っている技術を端的に紹介します。詳しい内容は今後の連載で掘り下げていきます。 なお、これらのツールは現時点のキャディにマッチしていると考えているものであり、唯一の正解だとは考えていません。必要に応じて見直し、ときにはツールを入れ替えるなどの判断もしていきます。</p> <p>これから紹介する技術の全体像は次の図を見てださい。必要な要素以外は割愛してあります。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208163435.png" alt="" /></p> <h2 id="Google-Cloud"><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud</h2> <p>キャディのサービスは、ほぼすべてがGoogleCloudのインフラ上で動いています。 筆者らがおもに使っている<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudのサービスは次のものです。</p> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a> Engine (GKE / マネージド <a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>)</li> <li>Cloud Run (コンテナベースのPaaS)</li> <li>BigQuery (データ分析基盤)</li> <li>Cloud <a class="keyword" href="https://d.hatena.ne.jp/keyword/SQL">SQL</a> (<a class="keyword" href="https://d.hatena.ne.jp/keyword/RDBMS">RDBMS</a>)</li> <li>Vertex AI (<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>プラットフォーム)</li> <li>Anthos Service Mesh (マネージド サービスメッシュ)</li> <li>Cloud Logging (ログ収集)</li> <li>Cloud Trace(分散トレーシング)</li> </ul> <p>創業間もない2018年当時、サービスをすばやく開発・提供するには<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D1%A5%D6%A5%EA%A5%C3%A5%AF%A5%AF%A5%E9%A5%A6%A5%C9">パブリッククラウド</a>を使うことが必然でした。 創業当時の社員にGoogleCloudの選定理由を聞いたところ、実は明確な理由があったわけではなく、社員のアカウント管理で<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Workspaceを使っていたから、とりあえず<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudを選択したとのことでた。 また、2018年当時はアプリケーションをコンテナ化してデプロイすることが注目された時期であり、当時の開発メンバーもコンテナ化を検討していました。そのときに<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudの東京リージョンでGKEが開始されたのは、大きな後押しになりました。 もちろん、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud以外の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D1%A5%D6%A5%EA%A5%C3%A5%AF%A5%AF%A5%E9%A5%A6%A5%C9">パブリッククラウド</a>にも類似のサービスはありますがGoogleCloudを使っていて良かった点をいくつか紹介します。</p> <p>まずは BigQuery です。キャディでは BigQueryに社内のデータを集約し、開発組織以外の社員もデータを閲覧・分析しています。 データ容量がスケールでき、BigQueryにデータを投入する方法が豊富であるため、データ分析基盤として初期投資が不要で使いやすいことが利点です。</p> <p>次にCloud Runです。コンテナを基本としたアプリケーションの基盤としてキャディではGKEを使っていますが、小規模のサービスではCloud Runを使う機会も増えています。コンテナ化の知見がそのまま利用できるのに加え、運用監視に必要な可観測性を最初から備えているためです。 さらにAnthos Service Meshも良かった点ですが、これついては後述します。 また、筆者が個人的に気に入っている点は次の2点です。</p> <ul> <li>プロジェクト単位で<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud のサービスをまとめられる</li> <li>IAMによる権限制御ができることです。</li> </ul> <p>多数のキャディのサービスを<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudのプロジェクト単位でまとめることで、効率よく管理できています。</p> <h2 id="IaCに関する技術">IaCに関する技術</h2> <p>IaCに関しては次の2つを使用しています。</p> <ul> <li>Terraform</li> <li>Argo CD</li> </ul> <h3 id="Terraform">Terraform</h3> <p><a class="wp-editor-md-post-content-link" href="https://www.terraform.io/">Terraform</a> は<a class="keyword" href="https://d.hatena.ne.jp/keyword/OSS">OSS</a>のインフラ構築ツールです。イン<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D5%A5%E9%A5%EA">フラリ</a>ソースの構成をコードとして記述し、その内容を現在のインフラ構成と比較・検証して差分反映できるため、インフラ構築作業や設定変更を自動化できます。</p> <p>同様のツールはAnsibleや<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a> Cloud Formationなどほかにも存在しますが、Terraformは「Provider」というしくみで拡張できるようになっており、<a class="keyword" href="https://d.hatena.ne.jp/keyword/AWS">AWS</a>や<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudなどの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D1%A5%D6%A5%EA%A5%C3%A5%AF%A5%AF%A5%E9%A5%A6%A5%C9">パブリッククラウド</a>だけでなく、キャディで採用している<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>についてもProviderが提供されています。そのため、Terraformのコードでインフラの大半を制御できます。</p> <h3 id="Argo-CD">Argo CD</h3> <p><a class="wp-editor-md-post-content-link" href="https://argo-cd.readthedocs.io/">Argo CD</a>は<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>への継続的デリバリーを通じて行うツールです。 Git<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>をソースとしてを継続的デリバリーを行う手法を「GitOps」と呼びます。Argo CDは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>へのデプロイをGitOpsに沿って行います。</p> <p>通常、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>へのデプロイは、デプロイ内容を記述した<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>ファイル<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a> <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>やkubectlコマンドに指定して実施します。 この作業は、ファイル数が増えると煩雑になるほか、ファイルの変更を追従して<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>に反映することが困難になります。 Argo CDは、git<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>にある<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>ファイルを取得し、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>への<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A5%CB%A5%D5%A5%A7%A5%B9%A5%C8">マニフェスト</a>ファイルの適用状況を可視化します。また、差分検知、履歴管理、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%ED%A1%BC%A5%EB%A5%D0%A5%C3%A5%AF">ロールバック</a>、自動反映といった機能も備えています。 権限制御可能なWeb UIがあるため、Argo CDを通して<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>にデプロイされているサービスの構成を把握したり、管理者のみがArgo CD経由でデプロイ操作をしたりするといった操作もできます。</p> <h2 id="自動化に関する技術">自動化に関する技術</h2> <p>自動化に関する技術は次の2つを使用します。</p> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions</li> <li>Renovate</li> </ul> <h3 id="GitHub-Actions"><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions</h3> <p><a class="wp-editor-md-post-content-link" href="https://docs.github.com/ja/actions">GitHub Actions</a>は、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>上で提供されるCI/CDツールです。<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>へのプッシュやプルリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トなどのイベントをトリガーとして、任意の処理を実行できます。 キャディでは当初CI/CD基盤としてCircleCIを採用していましたが、現在では<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actionsへの一本化を進めています。その理由は次の3点です。</p> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a>との親和性に優れていて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>を外部に渡す必要がない</li> <li>ナレッジや<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DE%A1%BC%A5%B1%A5%C3%A5%C8%A5%D7%A5%EC%A5%A4%A5%B9">マーケットプレイス</a>による共通処理の豊富</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/OpenID">OpenID</a> Connect連携が可能</li> </ul> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/OpenID">OpenID</a> Connect連携によって<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudのリソースをクレデンシャルを介することなく扱えます。 これによって、CI/CDパイプラインから安全にTerraformのコマンドが実行できるようになり、インフラ構築の自動化に役立ちます。 筆者らは、プルリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トのマージをトリガーとしてTerraformを実行し、インフラ構築作業をCI/CDで行うことを徹底しています。</p> <h3 id="Renovate">Renovate</h3> <p><a class="wp-editor-md-post-content-link" href="https://www.mend.io/free-developer-tools/renovate/">Renovate</a>は、依存性の更新を自動化するツールです。 現在のソフトウェア開発では、さまざまなツールやライブラリに依存していますが、それらは絶えずアップデートされています。なかには<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C0%C8%BC%E5%C0%AD">脆弱性</a>の対策によるアップデートもあるため、そのような更新は早めに気づき対応する必要があります。 Renovateは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の内容から依存性を抽出し、最新版があればその内容や更新をプルリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トとして作成します。 キャディでは100を超える<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>があり、多くの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>の依存性の更新チェックを自動化するためにRenovateを導入しています。</p> <h2 id="可観測性に関する技術">可観測性に関する技術</h2> <p>可観測性に関しては次の4つの技術を使います。</p> <ul> <li>Anthos Service Mesh</li> <li>Datadog</li> <li>Cloud Logging</li> <li>Cloud Trace</li> </ul> <h3 id="Anthos-Service-Mesh">Anthos Service Mesh</h3> <p>Anthos Service Meshは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudが提供するマネージドのサービスメッシュです。 サービスメッシュは、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Kubernetes">Kubernetes</a>上のサービスに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%AF%A5%BB%A5%B9%A5%ED%A5%B0">アクセスログ</a>やメトリクスといった可観測性を与えたり、ネットワークのセキュリティ向上や<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%BF%AE%C0%A9">通信制</a>御といったさまざまな機能を追加したりします。サービスメッシュを導入することで、開発者の作ったサービスに対して、一定品質の可観測性やセキュリティを一律に付与できます。 <a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AA%A1%BC%A5%D7%A5%F3%A5%BD%A1%BC%A5%B9">オープンソース</a>のサービスメッシュとしてはIstioが有名ですが、多くの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%B3%A5%F3%A5%DD%A1%BC%A5%CD%A5%F3%A5%C8">コンポーネント</a>を連携させる必要があるため、導入や運用の負荷が非常に高いのが難点です。 Anthos Service Meshは、Istioベースでありながら、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudによるフルマネージドサービスであるため導入が簡単です。自動バージョンアップなども備えているため、運用負荷が低減できます。</p> <h3 id="Datadog">Datadog</h3> <p><a class="wp-editor-md-post-content-link" href="https://www.datadoghq.com/">Datadog</a>は複数の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AF%A5%E9%A5%A6%A5%C9">クラウド</a>に対応した運用監視<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>です。キャディで実行する大半のサービスのログやメトリクスを収集し運用監視を行っています。SLI/SLOをはじめとした負荷状況・稼働状況の可視化、サービス異常を検知と通知、外形監視によるサービスの死活監視、証明書期限チェックなどに活用しています。 運用監視サービスは多くの製品がありますが、次の点から選定しました。</p> <ul> <li>複数の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D1%A5%D6%A5%EA%A5%C3%A5%AF%A5%AF%A5%E9%A5%A6%A5%C9">パブリッククラウド</a>や<a class="keyword" href="https://d.hatena.ne.jp/keyword/SaaS">SaaS</a>に対応して運用監視を一元化できる</li> <li>ログとメトリクスどちらも収集してアラートの対象にできる</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボードの可視性や調査時の検索性が良い</li> </ul> <h3 id="Cloud-LoggingとCloud-Trace">Cloud LoggingとCloud Trace</h3> <p>Cloud Loggingは<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloudが提供するログ収集サービスで、Cloud Traceは分散トレーシングのサービスです。 どちらも<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Cloud内部のアプリケーションやインフラの可観測性に関するデータを収集します。 Cloud LoggingのデータはDatadogでも収集しており、Datadogと役割が重複していますが、次のように使い分けています。</p> <ul> <li>Datadog : 検索や可視化に優れるため、リアルタイムログ検索や<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C0%A5%C3%A5%B7%A5%E5">ダッシュ</a>ボード、アラートの一元化に使用</li> <li>Cloud Logging : Datadogに収集していない一部のログの参照や、過去のログの検索に使用 (Detadogにすべてのログを集約するとコストがかかり、ログの保存期間にも制約があるため)</li> </ul> <p>Cloud Traceは、複数のサービスのパフォーマンスデータを収集して可視化できるため、どのサービスが遅延や障害を起こしているかを調査するのに役立ちます。Datadogにも同様のサービス、<a class="wp-editor-md-post-content-link" href="https://docs.datadoghq.com/ja/tracing/">DatadogAPM</a>がありますが、 Cloud Traceのほうが低コストであるためCloudTraceを使っています。</p> <p>また、監視サービスの <a class="wp-editor-md-post-content-link" href="https://cloud.google.com/monitoring">Cloud Monitoring</a>もありますが、Datadogと 重複するので積極的には利用していません。 ですが、<a class="wp-editor-md-post-content-link" href="https://cloud.google.com/managedprometheus">Google Cloud Managed Service for Prometheus</a>が登場したことで、より扱いや すくなりメトリクス収集の範囲が広がるという期待があり注目しています。</p> <h2 id="セキュリティに関する技術">セキュリティに関する技術</h2> <p>セキュリティに関しては<a class="wp-editor-md-post-content-link" href="https://www.cloudflare.com/">Cloudflare</a> を利用しています。Cloudflareは、インターネット上で提供するサービスに対して、<a class="keyword" href="https://d.hatena.ne.jp/keyword/CDN">CDN</a>、<a class="keyword" href="https://d.hatena.ne.jp/keyword/TLS">TLS</a>、ネットワークセキュリティ、エッジコンピューティングなど、さまざまな機能を提供します。</p> <p>当初はキャディのWebサイトを動かしていた<a class="keyword" href="https://d.hatena.ne.jp/keyword/WordPress">WordPress</a>の負荷軽減のために<a class="keyword" href="https://d.hatena.ne.jp/keyword/CDN">CDN</a>を導入する目的で利用を開始しました。 しかし、現在ではセキュリティに関する機能を有効活用するために、すべてのサービスをCloudflareの<a class="keyword" href="https://d.hatena.ne.jp/keyword/CDN">CDN</a>経由で公開しています。 利用している機能の一部は次の通りです。</p> <ul> <li><a class="wp-editor-md-post-content-link" href="https://www.cloudflare.com/ja-jp/dns/">Cloudflare DNS</a>: <a class="keyword" href="https://d.hatena.ne.jp/keyword/DNS">DNS</a>レコードと<a class="keyword" href="https://d.hatena.ne.jp/keyword/TLS">TLS</a>証明書を管理する</li> <li><a class="wp-editor-md-post-content-link" href="https://www.cloudflare.com/ja-jp/products/zero-trust/access/">Cloudflare Access</a>: Cloudflare にホストしているサービスに認証プロキシを設定できる。社内システムへのアクセスを<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> Workspaceのアカウントで認証できるようになる</li> <li><a class="wp-editor-md-post-content-link" href="https://www.cloudflare.com/ja-jp/waf/">Cloudflare WAF</a>: <a class="keyword" href="https://d.hatena.ne.jp/keyword/DDos%B9%B6%B7%E2">DDos攻撃</a>や<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%D4%C0%B5%A5%A2%A5%AF%A5%BB%A5%B9">不正アクセス</a>を検知しアクセスの遮断や通知を行う</li> <li><a class="wp-editor-md-post-content-link" href="https://www.cloudflare.com/ja-jp/products/workers/">Cloudflare Workers</a>: Cloudflareのネットワーク内でリク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トに応じて任意のプログラムを実行できる仕組。重要なデータへのアクセスに対して高度な認証を適用したり、監査ログを取得するために使用したりする</li> </ul> <h2 id="まとめ">まとめ</h2> <p>今回は、信頼性を高めるための技術選定の4つの観点(IaC、自動化、可観測性、セキュリティ)について解説し、それらの観点から現在筆者らが使用している技術について紹介しました。 最初からこれらの観点があったわけではなく、試行錯誤を積み重ねた結果として今の形に型化できました。注力する技術を型化したことにより、技術トレンドに応じて柔軟に使用する技術を組み替えていけると考えています。 次回からは、各技術トピックについて詳しく解説していきます。どれか1つでも興味のある技術があれば幸いです。お楽しみに。</p> caddi-tech 機械学習API基盤にregression test を追加する hatenablog://entry/6801883189081010615 2023-04-26T15:24:23+09:00 2024-02-08T16:34:34+09:00 こんにちは、キャディでMLOpsをやっている志水です。機械学習の推論基盤にregression testを追加したところ依存パッケージのアップデート等が楽になり開発者体験がすごくよくなったので、その詳細について書きます。 [toc] 推論基盤の運用 MLOpsチームでは機械学習モデルの推論API基盤を開発運用していています。こちらに関しての詳細は以前のTechブログ をご参照ください。 チームでGoogleのソフトウェアエンジニアリング本を読んだことをきっかけに、現在のプロダクトで改良できる部分を議論しました。 図1. 現状のデプロイフローと、起きえるエラーについて議論した図 現状のデプロイフ… <p>こんにちは、キャディでMLOpsをやっている志水です。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>の推論基盤にregression testを追加したところ依存パッケージのアップデート等が楽になり開発者体験がすごくよくなったので、その詳細について書きます。</p> <p>[<a class="keyword" href="https://d.hatena.ne.jp/keyword/toc">toc</a>]</p> <h1 id="推論基盤の運用">推論基盤の運用</h1> <p>MLOpsチームでは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>モデルの推論<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>基盤を開発運用していています。こちらに関しての詳細は<a class="wp-editor-md-post-content-link" href="https://caddi.tech/archives/4123">以前のTechブログ</a> をご参照ください。</p> <p>チームで<a class="wp-editor-md-post-content-link" href="https://caddi.tech/archives/4217">Googleのソフトウェアエンジニアリング本を読んだこと</a>をきっかけに、現在のプロダクトで改良できる部分を議論しました。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208163428.png" alt="" /> 図1. 現状のデプロイフローと、起きえるエラーについて議論した図</p> <p>現状のデプロイフローでは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>エンジニアが以下を手動で行っています。</p> <ol> <li>実験時に作成したデータとモデルファイル(.ptファイル)を用意</li> <li>TorchServe でサーブするために<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>用のDockerコンテナを作成</li> <li>dev環境へのデプロイと疎通確認</li> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/stg">stg</a>環境へのデプロイと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C9%E9%B2%D9%BB%EE%B8%B3">負荷試験</a></li> <li>prod環境へのデプロイ</li> </ol> <p>ホワイトボードでデプロイフローと既存のテストを整理した結果、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>エンジニアが開発時に出した結果と最終的な<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>が同じ結果を出すことを保証するために、手動のテストでカバーしている範囲が多いことがわかりました。 (既存のCIのテストは前処理の違いによる不具合が起きたことで追加したUnit testが大半でした。)</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>エンジニアとも相談し、自動化するテストの構成や内容についていくつかのパターンを出しました。</p> <ul> <li>「これからのパッケージアップデートに対して現在と推論が変わらないことを保証る」を目的に、サンプル図面に対して推論を行い、過去の推論結果と比較するregression test</li> <li>「実験当時の推論結果と、デプロイされたものの精度の一致」を目的に、実験時のデータと推論結果が一致することを確認するテスト</li> </ul> <p>実装の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B9%A9%BF%F4">工数</a>やそこから得られるbenefitを天秤にかけ、「これからのパッケージアップデートに対して現在と推論が変わらないことを保証する」ことを目的に、サンプル図面に対して推論を行い、そこで今までの推論結果と同じ結果を返していることを保証するregression test をCIに追加することにしました。</p> <h1 id="regression-testの追加">regression testの追加</h1> <p>regression test の構成は</p> <ul> <li>サンプル図面とその推論結果を用意し docker-compose.yml で <a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を立てて推論リク<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A8%A5%B9">エス</a>トを送り(下記batchプロセス)</li> <li>返ってきた現在の推論結果が用意しておいた推論結果と一致することを確認する</li> </ul> <p>ようにしました。</p> <p><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208163422.png" alt="" /> 図2. サンプル図面</p> <p>このようなサンプル図面に対して、以下のような推論結果を予め用意しています。</p> <pre><code class="line-numbers">図面_id,pred,name WA-20220616-ABC-01/RT-1,9.607873916625977,thickness </code></pre> <p>これをCI内で実行するための docker-compose.yml は以下のようになります。</p> <pre><code class="line-numbers"># docker-compose.yml services: api: build: context: . dockerfile: Dockerfile ports: - "7080:7080" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:7080/ping"] interval: 30s timeout: 10s retries: 5 batch: build: context: . dockerfile: tools/Dockerfile # 中でテストを呼び出す depends_on: api: condition: service_healthy environment: API_PORT: 7080 API_HOST: api </code></pre> <h1 id="CI">CI</h1> <p>CIでテストが走ってくれれば、エンジニアが人手で確認することなくデプロイされている推論<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>が以前のバージョンと乖離していないことを保証でき、安心できます。 キャディでは複数の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を運用しているため <a class="wp-editor-md-post-content-link" href="https://github.com/dorny/paths-filter">paths-filter</a> を活用し、変更があった<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>だけテストが発火するようにしています。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/github">github</a> actions 内では以下のようにシンプルにテストを実行しています。</p> <pre><code class="line-numbers">docker compose build docker compose up api -d docker compose run batch </code></pre> <p>この docker-compose.yml を使ったテストは1つのマシンで複数のプロセスが走るという、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a>のソフトウェアエンジニアリングに登場するmedium size のテストになります。関数ごとのテストはunit testなどのsmall testでカバーします。 small test がたくさんあり、medium(やlarge)のテストがそれより少ない状態がピラミッド型のバランスのいいtest suiteとされています。それを実現するためにsmall testは<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AB%A5%D0%A5%EC%A5%C3%A5%B8">カバレッジ</a>高くたくさん書いており、regression testのようなmedium sizeのテストはクリティカルな部分のみに書いています。</p> <h1 id="依存パッケージの更新">依存パッケージの更新</h1> <p>我々のチームでは<a class="wp-editor-md-post-content-link" href="https://docs.renovatebot.com/python/">renovate</a> というツールによって依存パッケージのバージョンアップデートを管理しています</p> <p>regression test を導入するまでは、例えばPyTorchにセキュリティパッチがあたりバージョンが上がった場合に、<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>を作成した各<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>エンジニアに想定した結果と精度が変わっていないかを確認してもらう必要があり、手間も時間もかかっていました。</p> <p>regression test を導入してからはrenovateでパッケージバージョンが上がるPRが上がってきた場合、CI上のregression test通っていれば推論<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>の挙動の一貫性についてある程度自信が持てるため、依存関係の更新が高速かつ安全になりました。</p> <p>当初想定してた以上に開発者としての体験はよく、今後も積極的にテストを開発していくモメンタムがチームに生まれています。</p> <h1 id="終わりに">終わりに</h1> <p>今回は推論<a class="keyword" href="https://d.hatena.ne.jp/keyword/API">API</a>基盤に対するregression testの導入に至った経緯と、その具体的な方法について紹介しました。</p> <p>キャディでは一緒に働く仲間を探しているので、<a class="wp-editor-md-post-content-link" href="https://herp.careers/v1/caddi/requisition-groups/9cea9714-ced6-43c6-97a6-83dd15215549">募集要項</a> から気になる求人があればご気軽にご連絡いただければ幸いです。</p> caddi-tech 輪読会を1Q通して運営してみて hatenablog://entry/6801883189081010620 2023-04-06T11:26:47+09:00 2024-02-08T16:34:20+09:00 こんにちは、キャディでMLOps をやっている志水です。昔から本を読むのが好きなので輪読会には以前から興味があり、今回Q(四半期)を通して運営したのでその様子を共有します。 [toc] なぜ輪読会を始めたか ばんくしさんが「Googleのソフトウェアエンジニアリング本はいい本だからみんな読んだ方がいいけど、重い本だから輪読会をやるといいよ」ということを常日頃から言っていました(遡ったら2022/01あたりから言ってました)。やろうやろうとはなっていたもののやっていなくて、1章を読み込んで資料をまとめてTech全員に招待を送って第1回を開催したのが昨年の10月でした。 そこから少しずつ姿を変えて… <p>こんにちは、キャディでMLOps をやっている志水です。昔から本を読むのが好きなので輪読会には以前から興味があり、今回Q(四半期)を通して運営したのでその様子を共有します。</p> <p>[<a class="keyword" href="https://d.hatena.ne.jp/keyword/toc">toc</a>]</p> <h2 id="なぜ輪読会を始めたか">なぜ輪読会を始めたか</h2> <p>ばんくしさんが「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a>のソフトウェアエンジニアリング本はいい本だからみんな読んだ方がいいけど、重い本だから輪読会をやるといいよ」ということを常日頃から言っていました(遡ったら2022/01あたりから言ってました)。やろうやろうとはなっていたもののやっていなくて、1章を読み込んで資料をまとめてTech全員に招待を送って第1回を開催したのが昨年の10月でした。 そこから少しずつ姿を変えて現在に至ります。</p> <h2 id="輪読会のフォーマット">輪読会のフォーマット</h2> <p>現在は輪読会を毎週金曜日に開催しており、週ごとに次のフォーマットで進めています。</p> <pre><code class="line-numbers">事前準備: 1. 該当章をあらかじめ各々で読んでおく 2. 読んでいて思ったことや関連するアイデアを Miro (オンラインホワイトボード)に付箋コメントとして書き起こす 当日: 1. 深堀りしたい付箋コメントに 1人 3票,4分で投票する 2. 似ているコメントをグループ化し、グループごとに得票の多いものから議論する 3. 議論後、自分達のチームに何を持ち帰れるかを言語化してみる 4. 必要に応じてworking agreementに加筆、あるいはチーム内で方針を合意する </code></pre> <p>やり始めた頃は、事前に担当者が該当章のまとめを作ってきて、それを全員で聞くようなフォーマットを試したこともありましたが、労力の割に得られるものが少ない感触がありました。毎回会の開催後に+/Δ(プラス/デルタという振り返りの1手法で会自体の良かった点と改善点を挙げていく)を何回か繰り返して現在のフォーマットに落ち着き、いい感じになりました。</p> <p>こんな感じで意見を出した後に投票し、表が集まったものを中心に議論していきます。 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208163413.png" alt="" /></p> <p>初回は大体1章を読みますが、以降は特に順序を決めず都度投票して来週読む章を決めていきます。次の章より次の本が多く票が集まるようになったら次の本に行くイメージです。本のボリュームにもよりますが、大体3〜7週間で次の本に変わります。</p> <p>投票の様子 <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208163418.png" alt="" /></p> <h2 id="カバーした本">カバーした本</h2> <p>前Qは以下の3つの本を対象に輪読会を実施しました。</p> <h3 id="Software-Engineering-at-Google">Software Engineering at <a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a></h3> <p><a href="https://www.amazon.co.jp/Software-Engineering-Google-Lessons-Programming/dp/1492082791">https://www.amazon.co.jp/Software-Engineering-Google-Lessons-Programming/dp/1492082791</a></p> <p>最初に読んだのは『<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a>のソフトウェアエンジニアリング』です。去年の末から読み始め、チームでは9章分をカバーしました。リーダーシップ、テスト、CI/CD等エンジニアリングを広くカバーしていて最高の本でした。実務に直結する指針もたくさんあります。</p> <h3 id="Team-Topologies">Team Topologies</h3> <p><a href="https://www.amazon.co.jp/Team-Topologies-Organizing-Business-Technology/dp/1942788819">https://www.amazon.co.jp/Team-Topologies-Organizing-Business-Technology/dp/1942788819</a></p> <p>次に読んだのが『チーム<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%DD%A5%ED%A5%B8%A1%BC">トポロジー</a>』(通称ちいとぽ)です。チームがソフトウェアを作る、という思想をベースにチームのあり方やチーム間のコミュニケーションのあるべき姿を示していて、チーム運営をしていく上での共通認識や共通言語ができたのがすごくよかったです。またキャディ全体でもTECH組織の組織形態はこの影響を強く受けています。</p> <h3 id="Designing-Data-Intensive-Applications">Designing Data-Intensive Applications</h3> <p><a href="https://www.amazon.co.jp/Designing-Data-Intensive-Applications-Reliable-Maintainable/dp/1449373321">https://www.amazon.co.jp/Designing-Data-Intensive-Applications-Reliable-Maintainable/dp/1449373321</a></p> <p>前Qの最後は『データ指向アプリケーションデザイン』を読みました。データベース内のデータ構造や分散合意、列指向フォーマットのバイナリ配列、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%D0%A5%C3%A5%C1%BD%E8%CD%FD">バッチ処理</a>とストリーム処理など、データに関わる仕事をしてきた上でもう一歩踏み込んだ理解を進めてくれる非常にいい本でした。</p> <h2 id="なぜ輪読会がいいか">なぜ輪読会がいいか</h2> <p>ここで改めて輪読会の良さを<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>してみます。</p> <p>実務的なメリットとして一例を挙げると、推論基盤のテストを設計する際に「<a class="keyword" href="https://d.hatena.ne.jp/keyword/Google">Google</a> のソフトウェアエンジニアリング」の輪読会が役に立ちました。輪読会以前は開発チームにテストに関する統一的な見解は無かったのですが、輪読会をきっかけに「我々が他チームに提供する機能は何なのか」や「それを担保するにはどうすればよいか」について議論が発展しました。本をベースにした適切なテスト設計について共通の指針をチーム全体が持てたことで、実装もスムーズに進みました。</p> <p>もう少しふわっとしたところでは、チームの引き出しや共通言語が増えた感触があって、チームの雑談やちょっとした話の中でちいとぽの言葉が出てきたりソフトウェアエンジニアリング本の言葉が出てきているように感じます。</p> <p>個人的には自分の感じているモヤっとした課題を既に<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B8%C0%B8%EC%B2%BD">言語化</a>してくれている部分も多く、マネージャーやチームとの1on1のなかで何がやりたいかや何を期待しているかを少しうまく表現できるようになった気がします。 おまけとしてこの輪読会を起点に本全てを読破できるようになり、読書量が増えました。</p> <p>またチームメンバーからの意見をまとめたところ</p> <ul> <li>自分が普段読まない本や、名著なのは知っているけど読むのが大変な本を読むきっかけになる</li> <li>チームとの議論があることがわかっているので、目が滑らず咀嚼するモチベーションがある</li> </ul> <p>のような意見もありました。確かに深く読もうというプレッシャーがあるのはいいことですね。</p> <h2 id="他チームの輪読会">他チームの輪読会</h2> <p>またこのTechブログを書いている時に、他人のカレンダーを覗いてたところ別のチームでも輪読会をやっていることを発見したので、少しだけお邪魔させていただいて話を聞きました。</p> <p>山下さんがリードするキャディの別プロダクトのチームでは業務で必要になりそうな知識を先取りして読むように輪読会を運営していて、「<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DC%A5%C8%A5%E0%A5%A2%A5%C3%A5%D7">ボトムアップ</a>DDD」「<a class="keyword" href="https://d.hatena.ne.jp/keyword/SQL">SQL</a><a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%A2%A5%F3%A5%C1%A5%D1%A5%BF%A1%BC%A5%F3">アンチパターン</a>」などの書籍をカバーし、「チームが同じ言葉で話せるようになる」「レビューがスムーズになる」などのいい影響があったとのことでした。大体1ヶ月に1冊くらいのペースで、全員で意見を書き出して議論するようなフォーマットで、図らずも同じような形式になっているのが面白かったです。輪読会がチームを超えて組織全体に浸透していて本好きの一員としては嬉しい限りです。</p> <h2 id="終わりに">終わりに</h2> <p>今回は輪読会の具体的な開催方法とその良さを紹介しました。 キャディでは一緒に働く仲間を探しているので、<a class="wp-editor-md-post-content-link" href="https://herp.careers/v1/caddi/requisition-groups/9cea9714-ced6-43c6-97a6-83dd15215549">募集要項</a> から気になる求人があればご気軽にご連絡いただければ幸いです。</p> caddi-tech AI 組織のモノレポ紹介 hatenablog://entry/6801883189081010624 2023-03-11T15:03:07+09:00 2024-02-26T17:31:14+09:00 AI 組織のモノレポ紹介 はじめに こんにちは、西原です。AI Lab の MLOps チームでエンジニアとプロダクトオーナーを兼任しています。私たちは、日々機械学習(ML)の成果を素早くシステムに取り入れ、安定した運用を実現するための仕組み作りに取り組んでいます。この一環として 2022 年秋からはモノレポ構成での開発に移行しました。モノレポの採用背景やモノレポでの取り組みについて紹介します。 TL;DR 車輪の再発明を防ぎ、開発効率を向上することを目的にモノレポへ移行 モノレポのビルドシステム Pants を使って、異なる Python バージョンのプロジェクトを管理 モノレポ移行によって… <h1 id="AI-組織のモノレポ紹介">AI 組織のモノレポ紹介</h1> <h2 id="はじめに">はじめに</h2> <p>こんにちは、西原です。AI Lab の MLOps チームでエンジニアとプロダクトオーナーを兼任しています。私たちは、日々<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B5%A1%B3%A3%B3%D8%BD%AC">機械学習</a>(ML)の成果を素早くシステムに取り入れ、安定した運用を実現するための仕組み作りに取り組んでいます。この一環として 2022 年秋からはモノレポ構成での開発に移行しました。モノレポの採用背景やモノレポでの取り組みについて紹介します。</p> <h2 id="TLDR">TL;DR</h2> <ul> <li><a class="keyword" href="https://d.hatena.ne.jp/keyword/%BC%D6%CE%D8%A4%CE%BA%C6%C8%AF%CC%C0">車輪の再発明</a>を防ぎ、開発効率を向上することを目的にモノレポへ移行</li> <li>モノレポのビルドシステム Pants を使って、異なる <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> バージョンのプロジェクトを管理</li> <li>モノレポ移行によって開発効率の向上を実感しており、今後もモノレポの運用と改善を継続していく</li> </ul> <ul class="table-of-contents"> <li><a href="#AI-組織のモノレポ紹介">AI 組織のモノレポ紹介</a><ul> <li><a href="#はじめに">はじめに</a></li> <li><a href="#TLDR">TL;DR</a></li> <li><a href="#モノレポの概要">モノレポの概要</a></li> <li><a href="#モノレポに移行するまで">モノレポに移行するまで</a></li> <li><a href="#Pants-とは">Pants とは</a></li> <li><a href="#モノレポへの移行">モノレポへの移行</a><ul> <li><a href="#移行方針">移行方針</a></li> <li><a href="#アプリケーションコードの移行">アプリケーションコードの移行</a></li> <li><a href="#インフラコードの移行">インフラコードの移行</a></li> </ul> </li> <li><a href="#モノレポの静的解析設定">モノレポの静的解析設定</a></li> <li><a href="#トランクベース開発とデプロイ">トランクベース開発とデプロイ</a><ul> <li><a href="#トランクベース開発">トランクベース開発</a></li> <li><a href="#デプロイ">デプロイ</a></li> <li><a href="#main-以外のブランチからの実行をブロック">main 以外のブランチからの実行をブロック</a></li> <li><a href="#run-name-を使った概要表示">run-name を使った概要表示</a></li> <li><a href="#同時実行制限">同時実行制限</a></li> </ul> </li> <li><a href="#モノレポにおける-CI">モノレポにおける CI</a><ul> <li><a href="#差分テスト">差分テスト</a></li> <li><a href="#テストカバレッジの計測">テストカバレッジの計測</a></li> </ul> </li> <li><a href="#今後の展望">今後の展望</a></li> <li><a href="#まとめ">まとめ</a></li> <li><a href="#参考">参考</a></li> </ul> </li> </ul> <h2 id="モノレポの概要">モノレポの概要</h2> <p>モノレポは複数のプロジェクトやアプリケーションの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%BD%A1%BC%A5%B9%A5%B3%A1%BC%A5%C9">ソースコード</a>、リソースを分割せずに、全てを 1 つの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>で管理する開発手法です。プロジェクトやアプリケーションを個別の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>で管理する方法(poly repo/multi repo)に対して、モノレポを採用することで次のようなことが期待できます。</p> <ul> <li>コード共有が容易になるため、同じ機能を複数のプロジェクトで実装する必要がなくなる</li> <li>コードの品質を統一的に管理できるため、バグやセキュリティの問題を早期に発見できる</li> <li>ビルドやテスト、デプロイなどの自動化が容易になる</li> <li>開発者が 1 つの<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>に集まることで開発者同士のコミュニケーションが促進され、開発効率やコード品質の向上につながる</li> </ul> <p>対照的に、モノレポを採用するにあたって気をつける点として同じコードベースを複数人で編集することによるコンフリクトへの注意や依存関係の管理、ビルドシステムを使った効率的な設計等があります。</p> <h2 id="モノレポに移行するまで">モノレポに移行するまで</h2> <p>AI Lab では 2022 年秋まで poly repo で開発していました。poly repo では<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>に関わるメンバーの得意・不得意によって、リンター、フォーマッター、テスト、CI/CD の作り込みに差がありました。<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>を横断して最低限の仕組みを構築するようにしましたが、当時は<a class="wp-editor-md-post-content-link" href="https://docs.github.com/ja/actions/using-workflows/sharing-workflows-secrets-and-runners-with-your-organization">Organization 横断の workflow</a>もまだなく、似たような作業の繰り返しで生産的な作業とは思えませんでした。一方で、これらの作業を怠るとメンテナンス性が低下し、継続的な改善・運用が難しくなるため放置もできません。</p> <p><a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> のテンプレー<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C8%A5%EA%A5%DD%A5%B8">トリポジ</a>トリや base となるコンテナイメージの利用による Docker イメージの共<a class="keyword" href="https://d.hatena.ne.jp/keyword/%C4%CC%B2%BD">通化</a>を試みましたが問題を解決できず、モノレポへの移行を検討しました。モノレポに移行することで開発に必要なリソースが全て 1 つの場所に集約され、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>に関わる全員が共通の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%B3%AB%C8%AF%A5%D7%A5%ED%A5%BB%A5%B9">開発プロセス</a>やツールを使用できます。これによりリンター、フォーマッター、テスト、CI/CD などの開発ツールの統一が容易になり、メンテナンス性や開発効率の向上が期待できます。</p> <p>モノレポに移行するにあたって依存関係の管理やビルド効率に関する懸念があったため、これらを解決できそうなツール(ビルドシステム)を調査したところ Bazel と Pants が選択肢として挙がりました。最終的に AI Lab の中心的な技術スタックである <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> をネイティブでサポートしている Pants を採用することに決めました。</p> <h2 id="Pants-とは">Pants とは</h2> <p><a class="wp-editor-md-post-content-link" href="https://github.com/pantsbuild/pants">Pants(pantsbuild/pants)</a> はあらゆる規模のコードベースに対応できるスケーラブルなビルドシステムで、特にモノレポとの相性が良いです。依存関係解決ツール、テストランナー、リンター、フォーマッター、パッケージャーなど数十の基本ツールをとりまとめ、扱えるようにします。記事執筆時点の Pants v2.15.0 では <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a>、Go、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Java">Java</a>、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Scala">Scala</a>、Shell、Docker をサポートしています。Pants の特徴として、静的解析による依存関係<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%E2%A5%C7%A5%EA%A5%F3%A5%B0">モデリング</a>、実行結果のキャッシング、並列実行やリモート実行等があります。</p> <h2 id="モノレポへの移行">モノレポへの移行</h2> <h3 id="移行方針">移行方針</h3> <p>モノレポへの移行は Pants のキャッチアップと並行して進めることになりました。AI Lab が抱えている問題を モノレポと Pants が本当に解決できるのか不安があったため、モノレポ移行が失敗した場合に切り戻せるように AI Lab が所有するコードの一部を対象に移行を始めました。AI Lab には主にアプリケーション用、ML モデル作成用、インフラ用のコードがあります。このうち、アプリケーションとインフラのコードは CI/CD が整っており、失敗した場合でも元に戻すことが容易だったためこれらのコードを対象にモノレポ移行を進めました。</p> <h3 id="アプリケーションコードの移行">アプリケーションコードの移行</h3> <p>移行するアプリケーションは Poetry と Docker を使用して構成されていましたが、アプリケーションごとに異なる <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> バージョンを使用していました。Pants の事前調査で Poetry をサポートしていることや、異なる <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> バージョンを管理できることがわかっていたため、移行の際は事前の作業なしに<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構成含む全てをそのままモノレポに取り込みました。Pants は requirements.txt を使った管理、 <a class="wp-editor-md-post-content-link" href="https://github.com/pantsbuild/pex">pex</a> を使ったコード実行もできますが移行のハードルを下げるために Poetry を使った構成をそのまま引き継ぎました。コード例のように<code>pants.toml</code>の記述を行い、モノレポ移行した各アプリケーションに BUILD ファイルを追加して Pants が動くようにしていきます。<code>./pants tailor ::</code>コマンドを使うと BUILD ファイルの追加を補助してくれます。 <code>./pants test</code> コマンドを実行してテストが通過すればアプリケーションの移行は終わりです。</p> <pre class="code toml" data-lang="toml" data-unlink>[GLOBAL] pants_version = &#34;2.15.0&#34; backend_packages = [ &#34;pants.backend.python&#34;, ]</pre> <h3 id="インフラコードの移行">インフラコードの移行</h3> <p>次にインフラ用コードの移行について紹介します。もともと Terraform で書かれたコードはインフラ用のモノレポで管理されていましたが、今回の移行を機 にアプリケーションと同じ<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>で管理することにしました。</p> <p>移行前は開発用、検証用、本番用それぞれの tfstate が 1 つあり、その tfstate に複数のアプリケーションが含まれている状態でした。この状態では管理する state の数が増えるにつれて、<code>plan</code> や <code>apply</code> の実行時間が長くなるだけでなく、追加・変更・削除の際の tfstate の lock により、コンフリクトする恐れがあります。今後アプリケーションが増えると、この状況が開発の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%DC%A5%C8%A5%EB%A5%CD%A5%C3%A5%AF">ボトルネック</a>になると考え、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>移行のタイミングで 1 つの tfstate で 、環境ごとの 1 アプリケーションを管理するように tfstate を分割をすることにしました。</p> <p>コードを新しい<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>に集約した後、<code>terraform state</code>コマンドを使用して tfstate をアプリケーションごとに分割して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>移行を終えました。移行後の<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%C7%A5%A3%A5%EC%A5%AF%A5%C8">ディレクト</a>リ構成を簡略化した例が以下になります。</p> <pre class="code lang-sh" data-lang="sh" data-unlink>. ├── .github ├── pants ├── pants.ci.toml ├── pants.toml ├── projects │ ├── app_1 │ │ ├── src │ │ │ ├── BUILD │ │ │ └── main.py │ │ ├── BUILD │ │ ├── docker-compose.yaml │ │ ├── Dockerfile │ │ ├── poetry.lock │ │ ├── pyproject.toml │ │ └── tests │ │ ├── BUILD │ │ └── test_main.py │ └── app_N │ ├── src │ │ ├── BUILD │ │ └── main.py │ ├── BUILD │ ├── docker-compose.yaml │ ├── Dockerfile │ ├── poetry.lock │ ├── pyproject.toml │ └── tests │ ├── BUILD │ └── test_main.py └── terraform ├── app_1 │ ├── environments │ │ ├── development │ │ │ └── main.tf │ │ ├── staging │ │ └── ... │ └── modules │ ├── cloud_run.tf │ ├── iam.tf │ └── ... └── app_N ├── environments │ ├── development │ │ └── main.tf │ ├── staging │ └── ... └── modules ├── cloud_run.tf ├── iam.tf └── ... </pre> <h2 id="モノレポの静的解析設定">モノレポの静的解析設定</h2> <p>モノレポ移行の当初の目的であったリンター、フォーマッター、テスト、CI/CD の作り込みと全体への適用を行いました。<code>pants.toml</code> にモノレポで有効にするリンターやフォーマッターの設定を記述します。以下は、Pants で使う静的解析の設定例です。</p> <pre class="code toml" data-lang="toml" data-unlink>[GLOBAL] pants_version = &#34;2.15.0&#34; backend_packages = [ &#34;pants.backend.python&#34;, &#34;pants.backend.python.lint.black&#34;, &#34;pants.backend.python.lint.flake8&#34;, &#34;pants.backend.python.lint.isort&#34;, &#34;pants.backend.python.lint.pylint&#34;, &#34;pants.backend.python.lint.docformatter&#34;, &#34;pants.backend.python.lint.bandit&#34;, &#34;pants.backend.python.lint.autoflake&#34;, &#34;pants.backend.python.lint.pyupgrade&#34;, &#34;pants.backend.python.typecheck.mypy&#34;, &#34;pants.backend.docker&#34;, &#34;pants.backend.docker.lint.hadolint&#34;, ]</pre> <p>Pants では、<a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> 開発で使うリンターやフォーマッターを一通りサポートしており、flake8 extension も利用できます。Pants でサポートしていないツールも、plugin 機能を使って自前でルールを書くことでモノレポに適用できます。Pants の<code>fmt</code>、<code>fix</code>、<code>lint</code>、<code>check</code>コマンドを実行して動作確認できたら静的解析の設定は終わりです。</p> <p>余談:Pants v2.16 では <a class="wp-editor-md-post-content-link" href="https://github.com/charliermarsh/ruff">Ruff</a> の導入が予定されています。Ruff は Rust で書かれた高速な <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> リンターであり、Pants の<a class="wp-editor-md-post-content-link" href="https://t.co/D36BrJAkCP">リンター比較実験</a>からも高速に動作することが期待できます。リンターの実行時間が長いと CI の実行が長くなったり、手元でリンターを実行するハードルも高くなりますが、高速に動作する Ruff によって改善されることを期待しています。</p> <h2 id="トランクベース開発とデプロイ">トランクベース開発とデプロイ</h2> <h3 id="トランクベース開発">トランクベース開発</h3> <p>モノレポでは<a class="wp-editor-md-post-content-link" href="https://cloud.google.com/architecture/devops/devops-tech-trunk-based-development?hl=ja">トランクベースの開発</a>スタイルを採用しており、main ブランチ以外の永続的なブランチを作成しないようにしています。一般に、寿命の長いブランチを作成し、デプロイするタイミングで多数の変更をリリース用ブランチに取り込む手法もあります。この手法ではデプロイの影響範囲が大きくなり、デプロイの失敗率が高くなります。デプロイが失敗すると多数の変更の中から問題を特定することになり、調査にかかる負担が大きくなると考えました。そこでトランクベースの開発スタイルを採用し、小さなパッチを基本とした開発を目指しています。小さなパッチの場合、コードレビューの負荷も低減され、フロー効率も向上すると考えています。トランクベースの開発スタイルを採用することで、小さな単位のパッチを意識することと、main ブランチに積極的に変更を取り込むうえで本番環境に影響を出さない仕組み(feature flag や versioning など)を活用していくことを期待しています。</p> <h3 id="デプロイ">デプロイ</h3> <p>アプリケーションのデプロイや<code>terraform apply</code>は <a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions を使って行うようにしました。<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions の workflow ファイルは<code>.github/workflows</code>配下に置く必要があります(<a class="wp-editor-md-post-content-link" href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#about-yaml-syntax-for-workflows">公式ドキュメント</a>)。1 つの workflow ファイルで 1 つのアプリケーションのみデプロイできるようにするとファイル数が増加し、見通しが悪くなります。ファイルの増加を防ぐために、<code>workflow_dispatch</code> を使用して単一のファイルから複数のアプリケーションをデプロイできるようにしています。デプロイする際は、実行ブランチ、デプロイする環境、および対象のアプリケーションを選択して実行します。これにより、新しいアプリケーションが増えても、1 つの workflow ファイルで管理できるようになります。</p> <p align="center"> <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208163402.png" /> </p> <p>ただし、1 つのファイルに複数のアプリケーションのデプロイ設定を書くとファイルが肥大化し、メンテナンスが難しくなります。メンテナンス性を保つために各アプリケーションごとにデプロイ用の設定ファイルを用意し、workflow 内からそれらのファイルを参照するようにしています。</p> <h3 id="main-以外のブランチからの実行をブロック">main 以外のブランチからの実行をブロック</h3> <p>検証環境と本番環境では main ブランチからのみ実行できるようになっており、その他のブランチを指定して実行した場合はブロックされるようにしています。main ブランチは、コードレビューを通過したコードのみを取り込むようになっており、誤操作によって未レビューのコードが本番環境に反映されることを防ぎます。</p> <h3 id="run-name-を使った概要表示">run-name を使った概要表示</h3> <p>1 つの workflow ファイルから複数のアプリケーションをデプロイする場合、ジョブ実行一覧の画面でどのアプリケーションが誰によって、どの環境に、そしていつデプロイされたかの履歴を確認することが難しくなります。<a class="keyword" href="https://d.hatena.ne.jp/keyword/GitHub">GitHub</a> Actions の <a class="wp-editor-md-post-content-link" href="https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#run-name">run-name</a> を使用して、実行の概要を表示することで該当するジョブを見つけやすくしました。</p> <p align="center"> <img src="https://cdn-ak.f.st-hatena.com/images/fotolife/c/caddi-tech/20240208/20240208163408.png" /> </p> <h3 id="同時実行制限">同時実行制限</h3> <p>デプロイ時に同じ動作が連続して実行されないように<a class="wp-editor-md-post-content-link" href="https://docs.github.com/ja/actions/using-jobs/using-concurrency">同時実行を制限</a>しています。下記のように<code>inputs</code>の値を使って group を設定してみたのですがうまくできず、<a class="wp-editor-md-post-content-link" href="https://github.com/orgs/community/discussions/35341">こちらのディスカッション</a>にあるように<code>github.event.inputs</code>を使うとうまく group の設定ができました。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Deploy Workflow <span class="synIdentifier">concurrency</span><span class="synSpecial">:</span> <span class="synComment"> # group: ${{ github.workflow }}---${{ inputs.appName }} これだと ${{ inputs.appName }} の値が常に空文字になる</span> <span class="synIdentifier">group</span><span class="synSpecial">:</span> ${{ github.workflow }}---${{ github.event.inputs.appName }}<span class="synComment"> # こっちだと入力した値が反映される</span> <span class="synIdentifier">cancel-in-progress</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">workflow_dispatch</span><span class="synSpecial">:</span> <span class="synIdentifier">inputs</span><span class="synSpecial">:</span> <span class="synIdentifier">appName</span><span class="synSpecial">:</span> <span class="synIdentifier">description</span><span class="synSpecial">:</span> <span class="synConstant">&quot;Name of the App you want to deploy.&quot;</span> <span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synComment"># 省略</span> </pre> <p>これまで紹介した取り組みをまとめた workflow の例が次のコードになります。</p> <ol> <li>run-name を使った概要表示</li> <li>同時実行制限</li> <li>実行ブランチ、デプロイする環境、対象のアプリケーションを選択</li> <li>main 以外のブランチからの実行をブロック</li> </ol> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synIdentifier">name</span><span class="synSpecial">:</span> Deploy Workflow <span class="synIdentifier">run-name</span><span class="synSpecial">:</span> Deploy to ${{ inputs.application }} by @${{ github.actor }}<span class="synComment"> # ①</span> <span class="synIdentifier">concurrency</span><span class="synSpecial">:</span> <span class="synIdentifier">group</span><span class="synSpecial">:</span> ${{ github.workflow }}---${{ github.event.inputs.application }}<span class="synComment"> # ②</span> <span class="synIdentifier">cancel-in-progress</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">on</span><span class="synSpecial">:</span> <span class="synIdentifier">workflow_dispatch</span><span class="synSpecial">:</span> <span class="synIdentifier">inputs</span><span class="synSpecial">:</span><span class="synComment"> # ③</span> <span class="synIdentifier">environment</span><span class="synSpecial">:</span> <span class="synIdentifier">description</span><span class="synSpecial">:</span> environment <span class="synIdentifier">type</span><span class="synSpecial">:</span> environment <span class="synIdentifier">required</span><span class="synSpecial">:</span> <span class="synConstant">true</span> <span class="synIdentifier">application</span><span class="synSpecial">:</span> <span class="synIdentifier">type</span><span class="synSpecial">:</span> choice <span class="synIdentifier">description</span><span class="synSpecial">:</span> application <span class="synIdentifier">options</span><span class="synSpecial">:</span> <span class="synStatement">- </span>app1 <span class="synStatement">- </span>app2 <span class="synIdentifier">jobs</span><span class="synSpecial">:</span> <span class="synIdentifier">deploy</span><span class="synSpecial">:</span> <span class="synIdentifier">runs-on</span><span class="synSpecial">:</span> ubuntu-latest <span class="synIdentifier">if</span><span class="synSpecial">:</span> ${{ inputs.environment == <span class="synConstant">'development'</span> || github.ref_name == <span class="synConstant">'main'</span> }} # ④ development環境はどのブランチからでも実行できるが、それ以外はmainブランチからのみ実行できる <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">run</span><span class="synSpecial">:</span> echo <span class="synConstant">&quot;Deploy to ${{ inputs.application }} by @${{ github.actor }}&quot;</span> </pre> <h2 id="モノレポにおける-CI">モノレポにおける CI</h2> <h3 id="差分テスト">差分テスト</h3> <p>モノレポ CI の構築において差分検知がポイントだと考えました。モノレポには複数のプロジェクトが存在し、毎回の CI 実行で変更と関係ないプロジェクトまでテストすると時間とお金がかかります。一方で、変更によって影響を受けるコードがテストされなかった場合、コードが壊れていることを検知できず広範囲に影響を及ぼすことがあります 。そして、デプロイと同様にプロジェクトが増えた際に何の工夫もしなければ CI 用の workflow ファイル数が増加していきます。</p> <p>Pants には差分検知機能があるため、基本的に Pants を使って差分検知します。Docker Compose を使用した ML 推論の regression test や、Terraform など Pants で管理されていないものは <a class="wp-editor-md-post-content-link" href="https://github.com/dorny/paths-filter">paths-filter</a>を使用して差分検知しています。</p> <p>Pants の差分検知は実行時にオプションを追加するだけで実行できるため導入負荷が低く、Pants を初めて使う人でも簡単に差分実行できます。AI Lab モノレポで全体テストをすると 30 分かかっていましたが、差分実行を導入すると CI の時間が p90 で 3 分になりました。Pants 採用後はできるだけ早く差分実行を導入することをお勧めします。</p> <pre class="code lang-sh" data-lang="sh" data-unlink><span class="synComment"># origin/mainとの差分を計算するオプション</span> <span class="synComment"># --changed-since=origin/main --changed-dependees=transitive</span> <span class="synComment"># origin/mainとの差分をテストする例</span> ./pants <span class="synStatement">test</span> <span class="synSpecial">--changed-since=origin/main</span> <span class="synSpecial">--changed-dependees=transitive</span> </pre> <p>CI の実行時間を短縮するためにキャッシュの活用にも取り組んでいます。Pants でキャッシュを使って CI を高速化する方法が公式の<a class="wp-editor-md-post-content-link" href="https://www.pantsbuild.org/docs/using-pants-in-ci#directories-to-cache">ドキュメント</a>と<a class="wp-editor-md-post-content-link" href="https://github.com/pantsbuild/example-python/blob/main/.github/workflows/pants.yaml">サンプルコード</a>で紹介されており、こちらを参考にして AI Lab モノレポでもキャッシュを活用して CI の時間が短くなるように努めています。</p> <p>先述したとおり CI では差分実行を採用していますが、差分実行だけだと<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>全体が正常に動作しているか不安もあります。そこで、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>全体のテストを日次で実行して<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>全体が壊れてないか確認することで、不具合を発見できるようにしました。このように差分実行によって時間と費用を節約し、全体テストによって<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>全体が正常か確認することで QCD のバランスが取れた CI システムを実現しました。</p> <h3 id="テストカバレッジの計測">テスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AB%A5%D0%A5%EC%A5%C3%A5%B8">カバレッジ</a>の計測</h3> <p>Pants には<a class="wp-editor-md-post-content-link" href="https://www.pantsbuild.org/docs/python-test-goal#coverage">テストカバレッジを計測する機能</a>があり、<a class="wp-editor-md-post-content-link" href="https://github.com/MishaKav/pytest-coverage-comment">MishaKav/pytest-coverage-comment</a>と組み合わせて<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AB%A5%D0%A5%EC%A5%C3%A5%B8">カバレッジ</a>をレポートしています。ML 推論の regression test は Pants で管理していないため、この<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AB%A5%D0%A5%EC%A5%C3%A5%B8">カバレッジ</a>に反映されていませんがモノレポ全体のテスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AB%A5%D0%A5%EC%A5%C3%A5%B8">カバレッジ</a>を計測したところ 86%でした。組織として数値目標は設定していませんが、テストを書いて当たり前の文化が数字として表れていると思います。次の <a class="keyword" href="https://d.hatena.ne.jp/keyword/yaml">yaml</a> は差分テストを実施した後テスト<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%AB%A5%D0%A5%EC%A5%C3%A5%B8">カバレッジ</a>を Pull Request にコメントする例です。</p> <pre class="code lang-yaml" data-lang="yaml" data-unlink><span class="synComment"># 省略</span> <span class="synIdentifier">steps</span><span class="synSpecial">:</span> <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/checkout@v3 <span class="synStatement">- </span><span class="synIdentifier">uses</span><span class="synSpecial">:</span> pantsbuild/actions/init-pants@v2 <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Setup Python <span class="synIdentifier">uses</span><span class="synSpecial">:</span> actions/setup-python@v4 <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">python-version</span><span class="synSpecial">:</span> <span class="synConstant">3.9</span> <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Test <span class="synIdentifier">run</span><span class="synSpecial">:</span> ./pants --changed-since=origin/main --changed-dependees=transitive test <span class="synStatement">- </span><span class="synIdentifier">name</span><span class="synSpecial">:</span> Pytest coverage comment <span class="synIdentifier">uses</span><span class="synSpecial">:</span> MishaKav/pytest-coverage-comment@&amp;lt;sha&amp;gt; <span class="synIdentifier">with</span><span class="synSpecial">:</span> <span class="synIdentifier">pytest-xml-coverage-path</span><span class="synSpecial">:</span> dist/coverage/python/coverage.xml </pre> <p>ML モデルの推論は前処理によって結果が変わります。前処理で使っているライブラリに変更があった場合、推論に影響があるかどうかを判断できるように Pants を使ったテストや regression test をしています。モノレポでは renovate を活用して外部ライブラリのバージョン更新をしており、これらのテストが通れば安心して main ブランチへマージできる仕組みになっています。</p> <h2 id="今後の展望">今後の展望</h2> <p>ここまでモノレポ移行の背景やモノレポでの取り組みについて紹介してきました。モノレポ移行によって開発効率が向上したことを実感していますが、まだまだやりたいことが残っています。今後、検討・検証したいことの一部を紹介します。</p> <ul> <li>ML モデル作成用コードをモノレポで管理 <ul> <li>現在はモデル作成用コードは別<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>で管理している。モデル作成用コードもモノレポで管理することで品質を統一的に管理し、デプロイまでの繋ぎこみをスムーズにしたい</li> </ul></li> <li>各プロジェクトごとに管理している外部ライブラリをまとめて管理 <ul> <li>各プロジェクトごとにやっている外部ライブラリ管理を辞め、<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>内で使用している外部ライブラリを一箇所に集約して管理することでメンテナンス性が向上する見込み</li> </ul></li> <li>Pants との相性から Poetry を継続するか、Poetry を辞めて requirements.txt で外部ライブラリを管理するかの判断 <ul> <li>(余談: 最近 <a class="wp-editor-md-post-content-link" href="https://github.com/python-poetry/poetry/releases/tag/1.4.0">Poetry 1.4</a> がリリースされて install が高速になりましたね)</li> </ul></li> <li>コードの依存関係整備 <ul> <li>コードがプロジェクト内で完結しているため、レポジトリ内に似たようなコードが存在する</li> <li>同じことをやっているコードをまとめ、プロジェクト横断で使えるようにする</li> <li>ML の前処理も、一つのコードを使いまわすことで前処理で差分がでることを防ぎ、推論結果を安定させられる見込み</li> </ul></li> <li>custom plugin の作成 <ul> <li>モノレポの中で共通の処理を plugin 化して統一のインターフェースで実行できるようにする</li> </ul></li> <li>pex を使った <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> 実行</li> </ul> <h2 id="まとめ">まとめ</h2> <p>AI Lab では<a class="keyword" href="https://d.hatena.ne.jp/keyword/%BC%D6%CE%D8%A4%CE%BA%C6%C8%AF%CC%C0">車輪の再発明</a>を避け、開発効率を向上するためにモノレポへ移行しました。モノレポ移行により移行前に抱えていた課題を解消でき、開発効率の向上を実感しています。Pants を使用して <a class="keyword" href="https://d.hatena.ne.jp/keyword/Python">Python</a> バージョンの異なるプロジェクトを管理しています。モノレポでは、何か 1 つのプロジェクトが終了しても<a class="keyword" href="https://d.hatena.ne.jp/keyword/%A5%EA%A5%DD%A5%B8%A5%C8%A5%EA">リポジトリ</a>のメンテナンスが止まることはなく、永続的にメンテナンスすることが前提となります。そのため、開発効率を向上させるための投資がしやすくなりました。ML パイプラインの改善などの他の取り組みと併せて、今以上に事業への価値提供ができるように今後も改善に取り組んでいきます。</p> <h2 id="参考">参考</h2> <ul> <li><a class="wp-editor-md-post-content-link" href="https://www.pantsbuild.org/">Pants</a></li> <li><a class="wp-editor-md-post-content-link" href="https://monorepo.tools/">monorepo.tools</a></li> <li><a href="https://www.pantsbuild.org/docs/media">https://www.pantsbuild.org/docs/media</a></li> <li><a href="https://developer.hashicorp.com/terraform/language/state/locking">https://developer.hashicorp.com/terraform/language/state/locking</a></li> <li><a href="https://developer.hashicorp.com/terraform/language/settings/backends/gcs">https://developer.hashicorp.com/terraform/language/settings/backends/gcs</a></li> </ul> caddi-tech