TECH PLAY

Finatextホールディングス

Finatextホールディングス の技術ブログ

32

はじめに 株式会社ナウキャスト でデータエンジニアをしている沼尻です。 この記事では、私が担当している「マッピング」という業務についてご紹介したいと思います。マッピングと言われてもピンと来ないと思いますが、あまり語られることのない(それがゆえに何と呼称したらよいかさえ定かではない)データエンジニアリングの重要な一領域だと思っていて、他社さんにも類似する業務が存在するのではないかと思っています。この記事をきっかけにして、他社さんと情報交換や技術交流などができたら嬉しいですし、ひいては、将来的なマッピング(ないしその類似業務)に関する知識の体系化につながれば幸いです。 マネージャーやエンジニアの募集もしていますので、ご興味を持っていただけたら、この記事の最後に掲載している求人をご確認いただければと思います。 マッピングとは何か ナウキャストでは、パートナーから様々なオルタナティブデータ(POSデータ、決済データ、クレジットカードデータ、人流データなど)のご提供を受けていますが、これらのデータはそのままでは活用できる形にはなっていません。そのため、様々な仕方でユースケースに応じた加工を施しています。例えば、個人情報を特定できないように匿名加工化した決済データの利用明細に、内製したマスタデータを紐づけてクライアントに提供している例があります。(データそのものを加工して分析用途で提供することもあれば、レポート業務を受託し社内で分析することもあります。) このように、発生源の異なるデータを紐づけることをマッピングと呼んでいます。マッピングは、そのままでは分析に活用しづらいオルタナティブデータを分析可能なデータに変換する、まさにデータに付加価値を与える仕事です。 例として、決済データに関するマッピングの種類とユースケースを簡単に図示すると以下のような感じになります。 マッピングはどう作られるのか 「店舗名×ブランド」を例に、マッピングの作り方を説明します。 新規ブランド追加のフロー 新しいブランドを作りたい場合は以下のフローでデータを作成します。 マスタリング まず欲しいブランドのレコードを作成します。また、そのブランドに紐づけたい店舗名が含むだろうキーワードを作成します。 マッチング クレンジングした店舗名とキーワードを単純に文字列検索でマッチさせ、「店舗名×ブランド」のマッピングの候補を機械的に作成します。 HI (Human Intelligence) チェック マッピングの候補を人目でチェックし、OK/NGの判定結果をデータベースに登録します。「NGが多い=キーワードが悪い」なので、必要に応じてマスタリングからやり直します。 新規店舗チェックと継続的な品質の維持 一度すべての店舗名に対してマッチングした後、そのブランドに紐づけるべき実在店舗が増えたり、既存店舗の店舗名が何かしらの理由で変わったりします。これらの「新規店舗」を継続的にマッチングしていかないと、ブランド毎の売上を時系列で見たときに、ある時点からの売上が欠損して見えてしまう可能性があります。このようなことが起こらないように、月に何回か、一度作成したキーワードと直近の新規店舗だけをマッチングさせ、HIチェックをしています。これを「新規店舗チェック」と呼んでいます。このように、マッピングは一度行ったら終わり、ではなく、品質を維持するための継続的な努力が不可欠です。 HIチェックとマシンマッピング ブランド数が増えてくると、すべてのブランドについて人目で新規店舗チェックすることが現実的に厳しくなってきます。そのため、キーワード作成時のHIチェックの結果、精度がよかった(NGが無かった)キーワードについては、その後の人目での新規店舗チェックを省略できるようにしています。つまり、機械的なマッチングの結果をそのまま正しいマッピングとして採用します。このように(キーワード作成時を除いて)機械で完結するマッピングを「マシンマッピング」と呼んでいます。 リバイズ 一度作成したブランドやキーワードを作り直すことを「リバイズ」と呼んでいます。以下のようなイベントがトリガーになります。 ブランドリバイズの主要トリガー - 実世界でブランドの統廃合が起こったとき - クライアントからブランド細分化などの要望があったとき キーワードリバイズの主要トリガー - ブランドリバイズに応じてキーワードも変更する必要があるとき - 新規店舗が増えて既存キーワードの精度が悪くなってきたとき クレンジングの例 マッチングの際に表記ゆれをある程度キャンセルするために店舗名とキーワードをルールベースでクレンジングしています。クレンジングのイメージを膨らませていただくために、わかりやすいルールの例をいくつかご紹介します。 キーワードの例 キーワードは、少ない数でより多くの店舗名がマッチするように作成されるため、ほぼほぼ正規表現で作成しています。またキーワードには、例えば下表のようにクレンジングで吸収しきれなかった店舗名の表記ゆれを吸収する役割もあります。 マッピングの体制 ナウキャストのマッピングチームには、マスタリング等を行うオペレータと私のようなエンジニア、そしてチーム全体を管理するマネージャーがいます。 オペレーター(数名) - マスタリング - HIチェック エンジニア(数名) - オペレーターが使う業務システムの構築 - データ受領からデリバリーまでのパイプラインの構築 - デリバリー=社内のデリバリー部隊に対するデリバリー マネージャー - 各所から上がってくる要望のハンドリング - ステークホルダー(特にデータホルダー)との関係性構築 「オペレーター」という名前に慣れているのでこの呼称を使っていますが、業務内容としては決められたルールに則って正誤チェックするOperationの面だけではなく、元データの特性と(複数ある)ユースケースを睨んで最適なマスタをデザインし、何を正解(のマッピング)とするのかの基準を決める役割、つまりDevelopmentの面もあるため、「オペレーター」という言葉ではその職掌を表現し切れないとは思っています。 デリバリーという観点で見たときのマッピングチームを含む社内体制とステークホルダーの関係は、以下のようになっています。(※イメージです。) データホルダーユニット データホルダーから受領したデータに最低限の加工を施してマッピングチームを含む社内の利用者に提供します。 データコンシューマーユニット データホルダーユニットが加工したデータをプロダクトに落とし込んでクライアント(データコンシューマー)にデリバリーします。 バージョンリリースとマッピングの品質保証 プロダクトサイドにいるマッピングチームとしては基本的に「現実世界の最新の状況を反映したマッピングデータを素早く提供したい」というモチベーションがありますが、クライアントに分析レポートを提供するアナリストには「過去の分析と前提条件を揃えたい」という気持ちもあり、マッピングに対する変更が必ずしも歓迎されない場合があります。そこで、下記のようなバージョニングを行っています。 パッチバージョンアップ 新規店舗チェックによるマッピングの増加が含まれます。これをしないと、例えばあるブランドの売上を可視化した際に直近の売上が欠損してしまう可能性があります。日次バッチで自動的に実行されます。 マイナーバージョンアップ ブランドやキーワードなどのマスタデータの変更とそれに伴うマッピングデータの変更が含まれます。年数回、手動で実施します。 メジャーバージョンアップ データホルダーから受領したデータ自体への変更やバージョニングの仕組み自体を変更する場合など、上記以外の大きな変更をする場合はメジャーバージョンを上げます。 マイナーバージョンアップの際に、エンジニアが追加・変更があったブランドの売上の時系列データを可視化し、品質チェックをしています。この業務をEDA(Explanatory Data Analysis)と呼んでいます。一般的なEDAとは少し意味がズレていますがナウキャストの歴史的な経緯でそう呼んでいます。ただし、すべてのブランドをEDAすると大変なので、数より質が求められるユースケースで利用するブランドに限定してEDAをしています。 このように、マッピングデータを作るオペレーターだけではなく、エンジニアも品質保証プロセスに参加しています。 オペレーターによる品質保証 店舗名とブランドや業種の文字列の目視確認によるマッピングの確からしさを保証 エンジニアによる品質保証 (主に)ブランド毎の売上を可視化し売上系列に異常がないことを保証 そして、これらの品質保証において具体的にどのような品質項目をどのようにして保証するかについては、マッピングチームとデータコンシューマーユニットの間で合意した社内SLAにて定めています。 エンジニアリング観点で見たマッピングの特徴 Human In the Loop 筆者はデータエンジニアなので、基本的にすでに存在するデータを集めてガシャガシャしてユーザーに届けることを生業としてきましたが、マッピングのためのシステムの場合、データ受領からデリバリーの間にオペレーターによるマスタリングとHIチェックが介在するため、Human In the Loop なシステムになっています。その意味では、オペレーターは、AIの教師データを作成するアノテーターに似ているところがあります。 OLTP or OLAP マスタリングだけを取るとOLTP的なので、RDBがあって管理画面がある一般的な業務システムのノウハウが生かせるのですが、キーワードで大量の店舗名を取ってきたり、EDAでトランザクションログを集計したりする必要があり、その場合はOLAP的な側面もあります。したがってデータベースとしてはRDB(Amazon Aurora)とDWH(Snowflake)を併用しています。ただ、サービスが跨っていてかなり非効率になってきているため、将来的には Snowflake Unistore への全面移行を検討しています。 データのバージョニング マッピングは現実世界の変化に合わせてアップデートしていかなければならないため、バージョニングが不可欠です。そして、先に説明したように新規店舗チェックのようなパッチバージョンアップが存在するため、単純なスナップショットでは済みません。そのため、すべてではないですが、一部のマッピングデータについては、テーブルをGitのブランチに見立ててコミット・マージ・リリースを行っています。 featureブランチ - オペレーターの変更を即時反映するブランチ stageブランチ - EDAしたい変更を含むfeatureブランチをかき集めたブランチ mainブランチ - EDAしたデータをリリースするためのブランチ このようなデータハンドリングの戦略を仮に「データブランチパターン」と名付け、あらゆるマッピングデータに適用できるデザインパターンとして定義できないか議論を重ねています。 マッピングはどういう場面で必要になるか 冒頭でマッピングを「発生源の異なるデータを紐づけること」と定義しました。(「異なるドメインで発生したデータを紐づけること」と言い換えてもいいかもしれません。)発生源が異なると、共通のIDがなかったり、同じ実体を指す名称項目はあるが表記ゆれが激しかったりするため、容易に2つのデータを紐づけることができません。そのようなときにマッピングが必要になります。マッピングが必要になるケースは、大別すると以下のようなパターンに分類できるかと思います。 複数の組織から上がってくるデータを取りまとめるとき: 組織ごとにデータ作成のシステムとプロセスが異なると当然表記ゆれが発生します また、組織が分かれていると、同じブランド体系を同じルールで店舗に付与する、みたいなことが実行困難になります 「複数の組織」は社外の場合もあれば社内の場合もあるかもしれません 例えば筆者が扱っているデータの一つにクレジットカードの利用明細がありますが、クレジットカード会社とそのカードが使える実際の店舗との間にはアクワイアラという別の組織が介在する場合があり、表記ゆれの補正とブランドのような属性情報の事後的な付与が必要になります 自社データとオープンデータやサードパーティデータを紐づけるとき: 商談を管理するシステムがあるとして、法人名は登録されているが法人番号がない場合に、政府のオープンデータから法人名と住所を使って法人番号を紐づけたい、みたいなケースが考えられます おそらくこうしたマッピングが必要になるケースはすでに様々な企業の中で発生していて、あまり表沙汰になることはありませんが、マッピングに類するようなことをされている企業も多く存在するのではと思っています。また、自社データだけではデータ活用のオプションに限りがあるため、オープンデータやサードパーティデータと紐づけたいというような需要は今後さらに高まるのではないかと予想しています。 マッピングの類似概念 マッピングに類する業務をされている他社さんと話していると、業務はとても似ているのですが、呼び方が違ったりします。他社と会話する際にはまず、この辺りの用語の定義について認識を合わせた方がよさそうです。 クレンジング 類似概念としてよく出てくるのが「クレンジング」です。データ分析の前処理一般をクレンジングと総称しているのだと思いますが、その場合はマッピングはクレンジングの一工程になるでしょう。他方で、マッピングの前処理としても表記ゆれ等のクレンジングは必要です。筆者の見解としては、クレンジングは表記ゆれの補正やL2正則化、True-Falseを0–1に変換するなど、意味が変容しない程度の値の変換と考えていて、(異なるエンティティ同士を紐づける)マッピングはその中に納まり切らないのではと考えています。 名寄せ 「名寄せ」という言葉もよく出てきます。例えば店舗名だったり法人名だったりの表記ゆれ補正と考えてもらえればよいでしょう。名寄せしたら同じ値になる店舗名同士をマッピングしていると考えれば、確かに名寄せとマッピングは近い概念です。ただしマッピングの場合、店舗のような同じもの(エンティティ)同士をマッピングすることもあれば、店舗名とブランドのような異なるエンティティ間のマッピングもあります。前者を同一エンティティマッピング、後者をクロスエンティティマッピングと呼ぶとしたら、名寄せは同一エンティティマッピングという特殊な種類のマッピングといえるでしょう。 Labeling or Tagging or 分類 ナウキャストでは歴史的にマッピングという言葉を使っているのでそれに慣れていますが、ラベリングでもタギングでも分類でもよかったかもしれません。ただし、これらの場合は、ラベルとラベル付けされるもの、タグとタグ付けされるもの、分類するものと分類されるものの存在を暗に前提にしており、同一エンティティマッピングのような概念を内に含むことはできません。したがって、マッピングの方が概念として広く便利なのではと考えています。 LLMとマッピングの未来 最後に、LLMについても触れておきたいと思います。 マッピングはほとんど文字列操作になるため、LLMとの相性はよさそうに思えます。実際、筆者も「店舗名×業種」のマッピングをLLMで試してみた感じ、そこそこの結果を返してくれました。ただし、すべてのプロセスがLLMに置き換わるわけではなく、LLMが解ける問題に落とし込むまでの部分は引き続き人間が担うことになるでしょう。つまり、LLMを最大限活用したマッピング業務の未来は以下のようになるのではないかと考えています。 このように外注先が担っていた役割をLLMに置き換えた場合、以下のようなメリットがあると考えられます。 人のコストは上昇していくがLLMのコストは下がっていくため長期で見たらLLMの方がコストが安い 契約を交わすなど、組織を跨ぐことによる事務コストが不要 人数を集めたりタスクがない期間が生まれないようにアサイン調整したりみたいなことも不要 これにより、マッピングの生産性がスケーラブルになるでしょう。他方で、クリアしなければならない課題もありそうです。 人間と同程度には冪等なレスポンスを得るためのプロンプトの開発 品質を落とさずにスケーラビリティを最大限活用するためのレビュー体制の構築 LLMに沢山マッピングさせることができるようになったとして、レビューする人数は簡単に増やせないため、品質を落とさないように対象をサンプリングしてレビューする仕組みを構築する必要がある ナウキャストでは、目下これらの課題の解決に取り組んでいます。 仲間を募集中 ナウキャストのマッピングチームでは、一緒に働いてくださる方を常時募集しています。実はCEOがマネージャーを兼務しており(ベンチャーらしいですね)、専任のマネージャーを一番優先度高く募集しています。 【一人目のプロジェクトマネージャー】マッピングチームのマネジメントメンバーを募集! - 株式会社Finatextホールディングス もちろん、データエンジニアも募集中です。 データ事業エンジニアポジション の求人一覧 - 株式会社Finatextホールディングス 冒頭にも書きましたが、社外の「仲間」も募集中です。マッピングと似たことやってるよみたいな企業の方がいらっしゃいましたら、苦労を共有したいので是非お声がけください。絶対に仲良くなれると思います。 LinkedIn: https://www.linkedin.com/in/shota-numajiri-245476135/ X: https://twitter.com/numa5h0 最後まで読んでいただきありがとうございました! データに付加価値を与える技術 was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
アバター
Photo by Annie Spratt via  Unsplash 2023年7月までFinatextグループで長期インターンをしていた @88888888_kota が、体験記を書いてくれました。心理的安全性を保つために実施していたことなども紹介しています。 ぜひご覧ください! Finatextインターンを終えて〜心理的安全性の重要性〜 Finatextインターンを終えて〜心理的安全性の重要性〜 was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
アバター
この記事は Finatextグループ10周年記念アドベントカレンダー の23日目の記事です。 昨日は三浦さんが「 あなたに届けるため、僕たちは一瞬の刹那を駆け抜ける -生命体と夜空の星について- 」という記事を公開しています。 はじめに Finatext保険事業でデータ周りのエンジニアをしている高橋といいます。 今年の9月に、Finatextから以下のリリースを出しました。 組込型保険を可能にするSaaS型デジタル保険システム「Inspire」、新たにBIツールを提供し、保険業界のデータ活用を支援 私は、このリリースにあるSaaS型デジタル保険システム「Inspire」のダッシュボード機能の開発を行っています。この機能は、AWSのBIツール「 QuickSight 」を用いて開発しています。 図: 開発中のダッシュボードのサンプル 今回はその実装面についてお話しようと思います。 重要なポイントは次の通りです。 埋め込みによるダッシュボード提供 API GatewayとLambdaを用いたダッシュボード表示 IaCによるQuickSightアセットの管理 IAM Identity Centerと事前プロビジョニングによるユーザー管理 順番に説明していきます。 1. 埋め込みによるダッシュボード提供 QuickSightには埋め込みという機能があります。これはダッシュボードの閲覧や分析をアプリケーションやウェブサイト上に実装する機能です。 埋め込み方法は簡単でUIやAPIから埋め込み用のURLを生成するだけです。 ダッシュボード閲覧の場合、下表の通り埋め込み方法としてユーザー認証有りとユーザー認証無しの二種類があります。 https://medium.com/media/31e8cc435d88815a1598f36b4ce78506/href ユーザー認証有りだとQuickSight上でのInspireユーザーの登録・管理コストがネックになります。そこで今回はユーザー認証なしの埋め込みを採用しました。 2. API GatewayとLambdaを用いたダッシュボード表示 Inspire上でダッシュボードを提供するにあたり、ユーザー情報をもとに認証・認可を行い適切なダッシュボードを表示する必要があります。この機構はAPI GatewayとLambdaを組み合わせて構築しました。 図: ダッシュボード表示に関する構成図 手順は次のとおりです。 (1). ダッシュボード閲覧のリクエストを送る Inspireにログインしてダッシュボードのページを開くとFetch APIによるリクエストが送られます。 (2). OPTIONSメソッドによるプリフライトリクエスト API GatewayではプリフライトリクエストにLambdaを用いない方法もありますが、今回は少し複雑な処理が必要だったためLamdbaを用いています。 (3). 認証・認可を行うLambdaをキック プリフライトリクエストが成功すれば後続の認証・認可用のLambdaを実行します。このLambdaは公式で Lambdaオーソライザー と名前がついていて、トークンなどの検証および、検証結果を受けての後続処理の制御を行います。 (4). LambdaオーソライザーでJWTの検証を行う Inspireの認証・認可ではInspire自身が発行したJWTを使っているため、その検証を行います。ここでは予め用意した検証用のエンドポイントを使用します。この検証のコードはLambdaオーソライザーの関数にまとめています。 補足ですが、Lambdaオーソライザーはキャッシュなどの必要最低限の設定のみ変更可能で、メソッドなどは変更できない(見れない)ようになっています。 (5). GETメソッドでLambdaをキック 検証が成功し認証・認可が通れば、ダッシュボードを取得するためのGETメソッドを実行します。ここでも中心となる処理はLambdaが担います。 (6). ダッシュボードの埋め込みURLを取得する JWTのペイロードに含まれたユーザー情報をもとに、適切なダッシュボードの埋め込みURLを取得します。 (7). ユーザーにダッシュボードを表示する 取得した埋め込みURLはフロントでiframeで読み込みます。これによりユーザーにダッシュボードを表示することができます。 3. IaCによるQuickSightアセットの管理 アセットとはQuickSight上のダッシュボードやデータセットなどのリソースの総称です。 FinatextではGitやTerraformを用いたコード管理を行っています。そのため、アセットの定義をコード化することは効率的な運用を行ううえで重要です。 幸いなことにここ数年、AWS社は新しいAPIを公開することでQuickSightのIaCを推し進めています。 参考: BI トランスフォーメーションを加速する Amazon QuickSight API の新機能 これらのAPIを用いてアセットをコード管理し、効率的なバージョン管理やデプロイを実現することができました。 4. IAM Identity Centerと事前プロビジョニングによるユーザー管理 社内のエンジニアやPM向けにQuickSightへサインインできる環境を構築する必要があります。 以前、 AWSサービスで始めるスタートアップ向けデータ分析基盤構築 でお話した通り、FinatextではIAM Identity Centerで一元的なユーザー管理を行っており、今回の実装でもそのままです。 IAM Identity CenterのユーザーがQuickSightにサインインするにはQuickSight上でのユーザー作成を行う必要があります。QuickSightユーザーを作成する方法として自己プロビジョニングと事前プロビジョニングの二種類があります。違いは下表の通りです。 https://medium.com/media/712e0af6cb340fd5b2a77c802f30a263/href 事前プロビジョニングはQuickSightユーザーを予め作成するため、権限等の設定を利用開始の事前に行える利点があります。また自己プロビジョニングはユーザーが初回サインインを行うまでQuickSightユーザーが作成されないため、登録漏れなどのリスクがあります。 厳格なユーザー管理を行いため、今回は事前プロビジョニングを採用しました。 最後に ダッシュボード機能は近日公開予定です。乞うご期待ください。 明日は松崎さんによる「プロポーズプロジェクト」という記事です。お楽しみに! Inspireのダッシュボード機能実装について was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
アバター
この記事は Finatextグループ10周年記念アドベントカレンダー の19日目の記事です。 昨日は @toshipon さんが「 レガシーなマクロ付きExcelファイルをAPI化する話 」という記事を公開しています。 はじめに こんにちは、Fiantextグループのナウキャストでデータエンジニアをしている瀬能です。 ナウキャストではDatahubという snowflakeを用いたデータ分析基盤 を活用しています。先日、Snowflakeのsnowsight上でStreamlitが実行できるようになりましたので、社内でのStreamlitの普及のために社内勉強会を実施しました。今回はそちらの勉強会で使った資料をこちらで公開します。 Streamlitとは StreamlitはWebアプリを作成するためのpythonのフレームワークです。特にデータ分析やLLMを使用したチャット形式のアプリの作成などに特化しています。 StreamlitはHTMLやCSS、JavaScriptを使わなくても、pythonのみで普段データ分析に使用しているpythonスクリプトを簡単にwebアプリ化できます。pythonさえ知っていれば開発できるハードルの低さと、既存の分析用ノートブックなどからの移行のしやすさが一番の利点だと思います。 Streamlitの特徴 Streamlitのチュートリアルは公式や先人の作成された記事に任せて、ここではStreamlitを開発していく上で知っておくと役に立つStreamlitの特徴について記載します。 Streamlitのサーバー Streamlitは動的なWebアプリなので、実行するためにサーバーを起動する必要があります。ちょうどjupyter notebookを起動するのと同じように、streamlit_app.pyを作成し、streamlitをimportして下記のコマンドを実行することでサーバーが立ち上がります。 streamlit run streamlit_app.py Streamlitでの入力とデータの表示 streamlitでWebアプリにUIを配置するには専用の関数を呼び出します。ユーザーの入力を受け取って画面上に表示する例を示します。 import streamlit as st x = st.slider('x') st.write(x, 'square is ' x * x) 他にもデータフレームやグラフを表示することもできます。 import pandas as pd import streamlit as st df = pd.DataFrame( [['a', 100, True], ['b', 200, False], ['c', 300, False]], columns=['col1', 'col2', 'col3']) st.dataframe(df) st.bar_chart(df, x='col1', y='col2') Streamlitのデータフロー Streamlitはフレームワークによくある複雑なアーキテクチャなどを必要とせず、通常のpythonと同じようにアプリを書くことができます。 これを実現するために、Streamlitは画面上のコンポーネントを更新する必要がある場合にpythonスクリプト全体を上から下まで再実行します。 この再実行は以下の状況で起こります。 ソースコードが変更された時 ユーザーがアプリのウィジェットを操作する時 例えばアプリの実行に5秒かかる場合はユーザーが操作するたびにその間待たされるようになるわけですが、それを解決するためにStreamlitには @st.cache_data デコレータなどの便利な機能があり、重たい処理をキャッシュしてスキップできるようになっています。 下記のコードを実行して見てください。これはロードしたデータを選択した閾値で絞り込むためのサンプルスクリプトですが、スライダーを操作するたびにスクリプト全体が再実行され、sleep関数によって待たされることがわかります。 このようなケースでは @st.cache_data デコレータが役に立ちます。下記のコードのコメントアウトを解除して実行して下さい。今度は load_data 関数の戻り値がキャッシュされ、再実行がスムーズに行われることが確認できると思います。 import time import pandas as pd import streamlit as st # @st.cache_data def load_data(): time.sleep(3) return pd.DataFrame([['a', 100, True], ['b', 200, False], ['c', 300, False]], columns=['col1', 'col2', 'col3']) with st.spinner('loading data...'): df = load_data() x = st.slider('Select a threshold', min_value=0, max_value=500, value=100, step=100) st.dataframe(df[df['col2'] >= x]) フォーム 上記のデータフローによる再実行は単に重たい処理が再実行されてしまう以外にも問題があります。 例えばユーザーが認証情報を入力したり、ダッシュボードでデータに対する複雑なフィルターを適用する時など、いちいち再実行するのではなく一連の入力が終わった後にまとめて再実行したいことがあると思います。 そのような場合にはst.form()とst.form_submit_button()を利用すれば複数のユーザーインターフェースをフォームにまとめてsubmitボタンを押すまで再実行を保留することができます。 import streamlit as st with st.form('key'): mail = st.text_input('Enter your mail') password = st.text_input('Enter your password', type='password') submitted = st.form_submit_button('Submit') if submitted: st.write('Your mail is', mail) st.write('Your password is', password) Streamlitでどんなアプリが作れるのか データ可視化 LLMチャットボット ダッシュボード 簡単なデータ入力(アノテーション)アプリ など様々なアプリを作成できます。 公式の ギャラリー では実際に触ったりコードを見られるデモがいくつもあるので参考になります。 Streamlitが得意なこと ユーザー入力周りのウィジェットは充実している。 グラフの可視化。自前の可視化用関数はまだ不十分だが、plotlyやaltair、bokehなどのサードパーティのグラフを埋め込むことができる。plotlyなどインタラクティブなグラフを生成するものは、Streamlit上でもインタラクティブに扱える。 データの可視化。st.dataframe()でソートしたり拡大・縮小できる状態で可視化できる。 st.data_editor()で上記のデータフレームを直接インタラクティブに編集できる。 Streamlitが苦手なこと 細かなレイアウトの調整や、ウィジェットのスタイルの調整など レイアウトは現状sidebar, tab, columns, expander, containerくらいしかないのでアラインメントやマージンなどの調節ができない 基本的にFront-endでCSS触るような調整ができない スタイルの調整はHTMLを用いたカスタムコンポーネントを使わないとできない モーダル、カルーセル、ナビゲーションバーなどの複雑なコンポーネントは未対応 バージョン更新でトグルボタンが追加されたりしているので今後に期待 Streamlit in Snowflake とは Streamlit in Snowflakeとは、SnowflakeのWebインターフェースであるSnowsight上でStreamlitを開発・実行できるサービスです。Snowflakeのコンピューティング環境や認証基盤を利用できるので既にSnowflakeのデータ基盤を持っている場合は非常に簡単にStreamlitアプリを構築・展開できます。 StreamlitとStreamlit in Snowflake の違い Streamlit in Snowflakeにはサポートされていない機能がいくつか存在します。代表的なものを下記に示します。 Streamlit in Snowflakeは現在Streamlit ver1.22.0まで対応 。 Streamlitの最新版はver1.29.0 。(2023年12月時点) st.file_uploaderやst.download_buttonなどのいくつかの関数 カスタムコンポーネントやカスタムテーマ、Streamlitのサーバー設定など st.image, st.media, st.videoを含むメディアエレメント 詳細: https://docs.snowflake.com/ja/developer-guide/streamlit/limitations Streamlit in Snowflakeを使うメリット StreamlitのアプリでSnowflakeの強力なコンピューティング環境とインフラが使用できる。 想定ユーザーに対してSnowflakeの権限を付与できる場合、Snowflakeの認証基盤を利用できる。 Snowflake Native Apps を使ってStreamlitのアプリ自体をマネタイズすることができる。 Streamlit in Snowflakeを使うデメリット Streamlit in SnowflakeはWarehouseを使ってStreamlitのサーバーの実行とStreamlit内部からのクエリの実行を行っているが、Streamlitのサーバーの実行はアプリを使用している間常にWarehouseが起動しているので、コストが増大しやすい。また、現状サーバーの実行に使うWarehouseとアプリが呼び出すクエリの実行に使うWarehouseは分けられないため、クエリ実行に大きなWarehouseが必要になればその分だけアプリの実行時間あたりにかかるコストも増加する。 利用できるpythonライブラリに制限がある。Streamlit in Snowflakeの環境はanacondaで提供されているため、 snowflake anaconda channel に記載されていない外部パッケージを利用するためには、パッケージを直接SnowflakeのStageにアップロードする必要がある。 詳細: https://docs.snowflake.com/ja/developer-guide/streamlit/about-streamlit#guidelines-for-selecting-a-warehouse-in-sis まとめ Streamlit in Snowflakeによって、pythonのみでデータ分析やダッシュボードなどのWebアプリが作れるStreamlitが、Snowflake環境で簡単に使えるようになりました。ナウキャストでは現在社内の定常的なデータ分析の一部をStreamlit in Snowflakeで行ったりしており、今後もStreamlitの活用を進めていく予定です。 明日は辻中さんによる「3つのポイントで振り返る世界のオルタナティブデータ業界動向2023」という記事です。お楽しみに! おわりに ナウキャストでは一緒に働く仲間を募集中です。少しでも興味を持たれた方は採用デックを是非ご覧ください! https://medium.com/media/600e9bbfa21fd003c9763cc58f2217d4/href オルタナティブデータ分析サービスの開発を促進する、データ基盤エンジニア募集! - 株式会社Finatextホールディングス Streamlit Bootcamp #1 was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
アバター
こんにちは、Finatextの @ toshipon です。この記事は Finatextグループ10周年記念アドベントカレンダー の18日目の記事です。昨日は河本さんが「 メガベンチャーから来たエンジニアおじさんが振り返る、スタートアップ選びのポイント 」という記事を公開しています。 この記事の概要 多くの企業では、既存の業務プロセスにおいて Excel マクロや関数を活用した複雑な計算処理をオフラインで実施しているケースが存在します。最近、これらの計算処理をオンラインサービスに転換するニーズをチラホラとお見受けします。本記事では、既存のExcelファイルを用いて、APIを介して処理結果をオンラインで取得する方法について解説します。 免責事項 本記事では、Microsoftの製品を使用する上でのライセンスに関連する情報を記載しています。ライセンス体系は常にアップデートされているため同様のユースケースにて Microsoft 製品を利用する際には、必ず Microsoftのサポートに問い合わせ、適切な検討をおこなっていただければと思います。 また、今回紹介する Excel をサーバーサイド環境で実行するといった構成は、GUIアプリケーションをプログラム上から操作するため、一定の不安定さに課題がある背景から Microsoft ではサーバー上での Microsoft Office アプリケーションの自動化は推奨しておらず、サポートもされていません。 Excel を用いたサーバーサイドオートメーションの検討をする際には、スクラッチでシステムを作り変える等、別の代替手段も合わせて検討する必要があります。 前提条件 既存のExcelファイルの活用 本記事の対象は、現在 Excelマクロや関数を用いた処理を行っている業務です。これらの既存のExcelファイルをAPI経由での処理に適応させる方法を紹介します。 Microsoft 365 のライセンスについて サーバーサイドでMicrosoft 365製品を利用するには、専用のライセンスが必要です。「 Microsoft 365 E3 Unattended 」というライセンスをVMサーバー単位で購入することで、サーバーサイドオートメーションを実現できます。2023年12月時点で、このライセンスはAzure上のVMサーバーでのみ利用可能で、AWSが提供する Windows Server 2022 on EC2 では使用できない点に注意が必要です。 サーバーサイドオートメーションの定義と制約 サーバーサイドオートメーションは、人の操作介入なしにExcelを操作する処理を指します。サーバーサイドオートメーションで Microsoft 365製品を利用する際には、特定の定められた制約に従う必要があります。具体的なライセンス利用のシナリオは、主に以下の2つが挙げられます。 Microsoft 365 ライセンスを保有するクライアントによってトリガーされ、サーバーサイドオートメーションを実行し Excel操作が行われる 定期バッチやメール受信など人の介入がないイベントをトリガーにサーバーサイドオートメーションを実行し Excel 操作が行われる これらのシナリオに応じたAPIサーバーのアーキテクチャ設計が必要です。 Excelを使ったAPIサーバーの実現手段 既述の2つのシナリオを踏まえて、以下の方法でExcelを活用したAPIサーバーを実現します。 1. Microsoft 365 ライセンスを保有するクライアントがAPIを呼び出す場合 このケースでは、クライアントがAPIを呼び出し、サーバー側でExcelの処理を実行します。 上記図のように、Microsoft 365 ライセンスを保有しているクライアントは、ライセンス規約上、Excel 操作を行うサーバーサイドオートメーションのプロセスを直接トリガーすることができるため、Web ページを通してAPI経由で Excel の計算結果を受け取ることが可能になります。 例えば、Azure Application Gateway からロードバランシングされた複数のVMサーバーで構成された環境で Excel の処理を行い、APIの結果として返却することも可能です。Microsoft 365 E3 ライセンスは VM サーバーの台数分購入が必要になります。 2. クライアントがMicrosoft 365 ライセンスを持っていない場合 このケースでは、サーバーサイドでクライアントがトリガーするのではなく、独立してExcelの処理を実行し、その結果をAPI経由で提供します。 Microsoft 365 ライセンスを保有していないクライアントは、ライセンス規約上、Excel 操作を行うプロセスを直接トリガーすることができないため、定期バッチや非クライアントがトリガーする処理を通して Excel の計算処理を実行します。 定期バッチ処理等で実行した Excel の処理結果をキャッシュサーバーに保持し、クライアントは Azure Functions を通してキャッシュのデータをAPIの結果として返却して受け取ります。直接的に Excel の結果を受け取るとはできないですが、間接的にAPIのレスポンスとしてExcel の処理結果の取得をします。 Excelをプログラムから操作する 今回、Excelをプログラムから実行する方法として、Pythonの「 pywin32 」ライブラリを使用するケースを紹介します。このライブラリは、Excelのマクロや関数、オブジェクト操作に対応しており、他のライブラリに比べて比較的 Excel の操作可能範囲が広いです。以下に Python の Flask サーバー経由で Excel の計算結果を API のレスポンスとして返却するサンプルコードを記載します。 python from flask import Flask import win32com.client import pythoncom from contextlib import contextmanager app = Flask(__name__) @contextmanager def co_initialize(): pythoncom.CoInitialize() # Excelを操作する場合は必須 try: yield finally: pythoncom.CoUninitialize() def set_value(name, ws, val): obj = None for shape in ws.Shapes: if shape.Name == name: obj = shape if obj is not None and hasattr(obj, 'ControlFormat') and hasattr(obj, 'FormControlType'): ctl = obj.ControlFormat if obj.FormControlType == 2: # dropdown ctl.ListIndex = val elif obj.FormControlType == 1: # checkbox ctl.Value = val @app.route('/calc') def calc(): with co_initialize(): excel = win32com.client.Dispatch("Excel.Application") excel.Visible = False # ExcelをGUI上表示しない excel.DisplayAlerts = False # ポップアップアラートが出ると止まってしまうので抑制 wb = excel.Workbooks.Open(r"C:\PathToFile\hoge.xlsm", ReadOnly=True) ws = wb.Worksheets("シート1") set_value("DRP_ドロップダウン1", ws, 1) set_value("CHK_チェックボックス2", ws, 1) result = {"data": ws.Range("A1").Value} return result if __name__ == '__main__': app.run(debug=True) このように、Python から Excel のセルやオブジェクトを操作して、最終的にマクロや Excel 関数によって計算された結果を セルの値を読み取ることで取得ができるようになります。 技術的な課題 Excelプログラムの実行における不安定さと処理速度 サーバーサイドで Excel のプログラムを実行する際の不安定な動作や処理速度の問題があります。これらに対処するため、以下のような方法も検討する必要があります。 VMサーバーにヘルスチェックを設置し、応答がない場合は自動的にサーバーをシャットダウンするために、VMサーバーのオートスケーリングを組んでそれを利用して新しいVMサーバーに切り替えます 大量のリクエストが押し寄せる場合を想定して、リクエストは一時的にキューに保存し、非同期で処理するようにし急激なサーバー負荷を抑制します Excelの実行結果はキャッシュサーバーに保持し、同じ計算の再実行を避けることで処理効率を向上させます 最後に 今回は、既存のExcelファイルを活用したシステム構成に関する内容を紹介いたしました。この例に限らず弊社では既存の業務を SaaS プロダクトに置き換えてアジリティを持ってお客様に商品展開や顧客体験の提供の手助けをするためのチャレンジが沢山あります。ご興味のある方は是非ご連絡いただけると、より具体的なお話をさせていただきたいと思います。 https://hd.finatext.com/recruit/ 明日は瀬能さんによる「Streamlit Bootcamp #1」についての記事です。お楽しみに! 関連記事 Microsoft 365 Unattended License overview Office のサーバーサイド オートメーションについて レガシーなマクロ付きExcel ファイルを API 化する話 was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
アバター
Hello! I’m Todd, a Data Engineer at Nowcast. This article is the 16th day of the Finatext Group 10th Anniversary Advent Calendar. Yesterday, Sugawara-san published an article titled “ 『投資がわからない』の正体 ~投資を始めるベストタイミングとは~ ”. What is data versioning. Much like how software engineers version their software, data engineers need to version their data. Generally speaking if the contents of the data changes, we should introduce a new version so we are able to distinguish between the “old” and “new” data. A change in the data can mean a number of different things — for instance adding or removing columns, introducing new files/tables to the dataset, or something more subtle like changing the data-type of a column, or the way the data in a given column is generated. If there is client receiving an on-going data feed, and we suddenly change something, it could cause issues in software that is consuming the data feed. In most cases, the datasets we work with at Nowcast are composed of more than 1 table — there are many different components that all require their own versioning — so the problem isn’t as simple as attaching a number to each dataset that increments whenever we make a change. Why is data versioning important Especially in the case of on-going feeds that are being consumed by other data pipelines, it is important to implement some kind of data versioning. As mentioned above, sudden changes to the data can cause issues — in the case of large changes, such as adding/removing/renaming columns, it could cause pipelines that consume the data to fail/breach SLA. It may seem like subtle changes such as the way a column is being generated are less damaging, but this could cause systems such as machine learning models to act in unexpected ways, so they should also be considered carefully. Simply put, to prevent problems downstream we should avoid changing live data feeds, and properly introduce versions. As well as avoiding sudden changes in a data feed, it is also important to continue to provide old versions of a data feed to consumers, such that they have time to transition from old versions to new versions. Some Best Practises So we know why we should version data — but how should it be done? Below are some best practises we’ve developed at Nowcast over the last few years. Version names should be easy to understand. The data consumers need to understand the order of the data versions — something like Major-Minor-Patch (1.0.0, 1.0.1, 1.0.2 .. etc), or just the date is good for this. At nowcast we often use Major-Minor-Patch, where Patch is a data generation method/mapping change, Minor is a large change such as adding/removing columns, or changes to the source data. Major is reserved for large versioning changes in the source data. Old versions of a feed should continue to be delivered in the same directory. It isn’t always possible, but when it is, older versions of the data need to be continued to be delivered to consumers. Even if it is just for long enough for them to transition to newer versions of the data. Shutting off a feed without giving anyone time to migrate to an alternative is a good way to break any downstream systems that are dependant on the feed. You can think of this flow like depreciation — feeds shouldn’t just be shut off, they need to be deprecated with functioning replacements beforehand — and then they need to be maintained for a suitable amount of time. Depending on the criticality of the feed and the number of consumers, this deprication period can last years — as such the software side of the data pipeline should be designed with this in mind. The data generation process associated with a given version should be idempotent. For any given version, the ETL code (ELT, data pipeline, whatever you want to call it) that generates the data should work idempotently if possible. In other words, running the an ETL job for some input data source over a given time period should always result in the same output. This is important for the reproducibility of the data, but also makes checking the data’s integrity much easier. As a data engineer it is always a relief to know that I can regenerate any dataset that I’m using. Building this kind of idempotent pipeline generally means avoiding randomly generated IDs, such as UUID4. New versions should be delivered in new directories. We should always separate data from different versions in some easy-to-understand way. If using cloud file storage like S3 or GCS it is good practise to seperate versions by directory. In Snowflake something like Schema can be used — or at the very least table suffixes. The main thing to avoid is having a single directory with the latest version in as the main point of data ingestion — and then every time a new version is released updating this directory to contain the new data. This is not much better than having no versioning at all in that it will still cause consumer’s code to break. How not to handle versioning To illustrate the difference between good and bad data version management lets consider an example consumer transaction dataset with 3 different tables: transaction: contains consumer transaction data metadata: contains some information about the entire panel, such as monthly users, monthly sales volume etc mapping: contains mappings that allow us to join transactions to companies As the data engineer in charge of managing this data feed, we need to reflect a change in the data to our data feed — for instance we could be removing 1 column and adding a new one, but the mapping and metadata won’t change In the beginning we have a single directory called data_feed, that contains all of our data: └── data_feed ├── transaction (v1) ├── metadata (v1) └── mapping (v1) One way to release this new data is just to replace the v1 data with the new (v2) data: └── data_feed ├── transaction (v2) ├── metadata (v2) └── mapping (v2) But what if the consumer was using the column we just deleted? we would have just broken their data feed — so lets try something a bit different. We need to continue to send the old data in order for them to migrate — why don’t we try delivering both v1 and v2! └── data_feed ├── v1 │ ├── transaction (v1) │ ├── metadata (v1) │ └── mapping (v1) └── v2 ├── transaction (v2) ├── metadata (v2) └── mapping (v2) This is slightly better than the original attempt, but there is still an issue, we moved the data without giving the consumers time to migrate from the old unversioned structure. While this structure will work well going forward, we should provide consumers with both the original file format and the newly versioned format to give them time to migrate: └── data_feed ├── transaction (v1, temporary) ├── metadata (v1, temporary) ├── mapping (v1, temporary) ├── v1 │ ├── transaction (v1) │ ├── metadata (v1) │ └── mapping (v1) └── v2 └── transaction (v2) └── metadata (v2) └── mapping (v2) We should provide the exact same data in the exact same locations as before until all consumers have updated to the new v1 directory. After the consumers have updated we can remove the files stored directly under the data_feed directory — so if clients want to use v1, they can read it from the v1 directory. As mentioned earlier this process is analogous to deprication in software engineering. You wouldn’t delete a function from library code before users have a chance to move over to the new method — in principal this is the same. How can we keep track of data versioning As mentioned earlier datasets are generally composed of multiple tables — each of the tables should be versioned, and a global version should be used to refer to a specific configuration of table versions. Up until the adoption of DBT, many projects managed their versioning using a JSON file, much like below: [ { "version_number": "1.0.0", "release_date": "2022–01–15", "table_versions": { "raw_transaction": "1", "aggregated_transaction": "1", "metadata": "1", "mapping": "1" }, "storage_locations": [ "/data_feed/", "/data_feed/v1/" ] }, { "version_number": "1.0.1", "release_date": "2022–02–26", "table_versions": { "transaction": "1", "aggregated_transaction": "2", "metadata": "1", "mapping": "1" }, "storage_locations": [ "/data_feed/v2/" ] }, { "version_number": "1.0.2", "release_date": "2022–04–11", "table_versions": { "transaction": "2", "aggregated_transaction": "3", "metadata": "1", "mapping": "1" }, "storage_locations": [ "/data_feed/v3/" ] } ] Using this approach we can easily track the composition of each global version — as well as any metadata associated with the versions such as the release date or storage locations. It is also language agnostic and easy to implement, although there are some downsides to using this approach. Disadvantages of this approach This approach is hard to standardize because it’s not constrained by any programming frameworks — JSON structures can be subtly different from project to project. Another issue that isn’t easily solved is that different tables can depend on each other — for instance we have 2 different types of ‘transaction’ — `raw` and `aggregated`, where `aggregated` depends on `raw`. If we bump the version of `raw`, `aggregated` will also need to be bumped — but an update to the code that builds the `aggregated` data would cause only `aggregated` to be bumped. It’s hard to capture these relationships when using a JSON structure like this. For complex datasets with many components and many layers using JSON to manage versions like this becomes unwieldy. Advantages of Data Versioning in DBT As of late 2022, we have been writing our ETL/ELT code at Nowcast using DBT . Among the many advantages it has over the python based ETL code we were using previously, the way it handles versions, and especially dependencies between different tables, is much cleaner than our previous approach. for any given table, the versions of any dependant tables are stored in the tables definition — for instance lets say we have our `transaction` table from earlier: select row_id, date, user_id, transaction_location, sq transaction_amount from {{ source('example_dataset', 'original_transaction') }} lets call this `raw_transaction`, and we want to make an aggregated transaction table — we can define it by referencing the raw table: select date, transaction_location, count(*) as txn, sum(transaction_amount) as aggregated_sales from {{ ref('raw_transaction', v=1) }} group by 1, 2 order by 1, 2 By using the `v` parameter we have specified the exact version we want to use. Lets say that we want to make a second version of the aggregated data with unique user count, but using the same v1 raw data, we can do so like below: select date, transaction_location, count(*) as txn, sum(transaction_amount) as aggregated_sales, count(distinct(user_id)) as aggregated_sales from {{ ref('raw_transaction', v=1) }} group by 1, 2 order by 1, 2 This is very simple to implement: we just make a new file called aggregated_transaction_v2, and add our new column. Further down the line, the raw data could be updated, when this happens we can make v3 aggregated_transaction data by bumping the dependency: select date, transaction_location, count(*) as txn, sum(transaction_amount) as aggregated_sales, count(distinct(user_id)) as aggregated_sales from {{ ref('raw_transaction', v=2) }} group by 1, 2 order by 1, 2 Effectively what we are doing here is handling the dependency tree as part of the code, it makes it much easier to understand which tables depend on other tables. You can read more about version management in DBT  here . Conclusion In this article we introduced the concept of data versioning, explained why it’s important, and then went over some approaches to versioning data including DBT, which Nowcast uses along with Snowflake to build our data pipelines. We showed the advantages of DBT over a more manual approach. If you are interested in Alternative Data or Data Engineering, please don’t hesitate to reach out! Tomorrow, Kawamoto-san will publish an article about his experience transitioning from an established company to a startup. Improving Version Management of Data was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
アバター
こんにちは、Finatextの @taiki45 です。この記事は Finatextグループ10周年記念アドベントカレンダー の14日目の記事です。昨日は河端さんが 「保険業界の Vertical SaaS『Inspire』のビジネスモデル」 という記事を公開しています。 インターネットサービス業界でDesign Docsと呼ばれているプラクティスがあります。これは、それなりに規模のあるなにかを作ったりタスクを行う前に、解決する問題や考慮事項や取りうる選択肢やそのトレードオフなどを文書としてまとめたもの(design doc)を書くというプラクティスです。 Design Docsプラクティスは、複数のチームやある程度の多さのステークホルダーをalignするという側面もあって大企業でよく使われていると思うのですが、中規模(300人程度)の企業でもDesign Docsプラクティスを行うことで、より精度の高い意思決定を行い、プロジェクトのリスクを軽減して、ソフトウェア開発を効率的にすることができるので、その実践や気をつけていることについて紹介します。 Design Docsとは 一番有名であろうGoogleのDesign Docsのプラクティスについて説明しているこの記事があるので、(この記事ではあまり重要ではないですが)一般的なDesign Docsとはなにかについてはこれを参照してください: https://www.industrialempathy.com/posts/design-docs-at-google/ この記事ではdesign docsに書く内容として、 解決したい問題、モチベーション、考慮事項 取れる選択肢とそれぞれのpros/cons どの選択肢を採用するか、それはなぜか あたりを想定しています。 Design Docsの利点 ある程度の大きさのタスクや問題解決をする際に、その意思決定の精度を効率的に高めたいというのがモチベーションです。 例えば、全体最適を度外視すると、会社全員1つの部屋に集めて自分がこれからしようとすることを説明して意見やツッコミをもらうのが、局所的には最も効率良く意思決定の精度が高くなります。しかしこのアプローチは組織の規模が大きくなるとすぐに破綻します。次善の策として、文章として問題やアプローチを言語化することで、問題領域の特定の部分(例えばセキュリティや他のチームの開発しているプラットフォームやコンポーネント)について、効率的にコミュニケーションして前提の間違いその他に気づいて、意思決定の精度を高めるというものです。 また、コミュニケーション以外の部分での利点として、思考を言語化・文章化することにより、考慮漏れや実は調査が足りてなかった事項が見つかることが多いです。 実践で気をつけていること 書く前 Design docを書くことはめちゃくちゃオーバーヘッドが大きいです。特に中規模の組織では合意形成という面での利益が少ないかあるいは無いので、大規模な組織よりさらに書くことのコスパについて慎重になる必要があります。 a. 問題及びそのアプローチがほぼ自明ではないか? b. PR descriptionで実装後に説明するので十分ではないか? c. Quick chatやミーティングの方が効率的ではないか? d. 量があまり多くなくかつ議論の余地があまりない場合GitHub issueに書く方が効率的ではないか? e. プロトタイピングを行う方が効率的ではないか? このあたりを考慮しています。 (b)について、手戻りのコストが低い場合、事前に文章を書くコストを省いて、いきなり実装を出してコミュニケーションした方が正確性が高い意思決定ができるので常にそういう選択肢もおいています。(a)に近いですね。 (c)について、中規模の環境ではそもそもステークホルダーやコミュニケーションパスが少量なケースが多いので、直接のコミュニケーションの方が効率的なケースは多々あります。 (d)のGitHub issueとの使い分けで言うと、量が少なくissueで見やすいかつ議論の余地が少ない問題についてはissueの方が実装に近い場所に情報を置けるのでissueの方が情報を置く場所として適していると考えています。議論の余地についていうと、GitHub issueは複数人がコメントを多く投稿していくと、それぞれがどの議論なのか整理しにくいというツールの限界があるため使い分けています。 (e)について、手を動かさないとわからないことが多い問題の場合、design docsを経由せずにプロトタイピングから直接本実装に移行する方が効率的なケースです。 またdesign docを書くか書かないかの2択ではなく、例えばあるdesign docを書いてそれをベースに口頭でのコミュニケーションを行うことも有効なケースもあります。 さらにdesign docを書く場合もいきなり書き始めないことが効率的なケースも多々あるのでそこにも注意しています。例えばよくわからないことが多い場合、その状態でもdesign docを書いたとしても中身も薄くアプローチの精度も低いので書き手・読み手ともに時間を損するだけになります。そのようなケースでは、調査・ヒアリングを繰り返したり、手を動かしてある程度のものを作って、問題の周辺領域に詳しくなった後にdesign docを書き始める方がよいです。 書く際 a. コンテキストがない人は読めなくてよい b. 後から読む人への解説書にはしない c. レビュー前に全て詰める、レビューは何往復もしないし、レビュー後の積極的な更新もしない (a)について、まず最もありがちなpit fallとしてドキュメントとしての体裁を整えることに労力を費やしてしまうことです。思考の言語化と議論のためのツールなので、「コンテキストがない人も読めるようにbackgroundのセクションなどを詳細に書くこと」をしないのはコスパのために重要です。 (b)について、これは一般的なプラクティスと違う点かもしれないですが、後からdesign docsを読んで有益になることを求めないようにしています。あくまでdesign docsは本格的な実装に至る前の議論フェーズでの書捨ての道具として使って、アーキテクチャや実装に関わる意思決定について全て網羅しないことで、書くことと更新するコストを低くしようとしています。もちろん、後から意思決定の一部を参照することで将来の実装の読み手の材料になることは目指してますが、意思決定の根拠の一部であることや古い情報であることは積極的に認めています。これはプロダクトやソフトウェアのサイズが大規模になりにくいという組織上の性質があるからですね。 (c)について、文書としての価値を高める労力をかけないと言い換えてもいいかもしれないです。レビューはあくまで考慮事項の確認や方向性の合意であって、文書の良し悪しについては(ほとんど)レビューしないことが重要です。もちろん、よくわかっていない事柄についてdesign docをWIPの状態で調査したことなどをまとめておいて相談しつつクリアにする、という使い方は有用ですが、よくわかっていない事柄が多すぎる場合は、design docを書く前に別の方法でコミュニケーションしたり調査を進めてからdesign docを書いて方向性の合意を取る方がレビューコストも下がってうまくいくと考えています。 (c)のレビュー後の更新については、(b)と関連していて議論が終わってしまえば基本的にそのdesign docは不要くらいの考えで運用していて、実装に入った後にわかったことや方向転換について詳細に残さないことで更新するコストを下げています。もちろんコスト少なく更新できるケースでは更新した方がよいですが、大抵は出来上がった文書を更新するのは難しいので。 運用時 a. 書いて議論した結果なんか違ったねで破棄(archive)することはある b. 承認はいらない。LGTMくらい。 (a)について、ドキュメントを書き慣れない人がドキュメントを書くことで成果物について気持ちを持つことはあると思いますが、間違った意思決定を防げたことで十分にコストは回収しているので、サンクコストを恐れないのは大事です。また、実装前の設計の精度が高い状況は少ないので設計にコストをかけすぎないために、design docsを書き直して過去のものをリンクしたり、機能ごとやフェーズ毎にスコープを分割するという手もあります。 (b)について、「design docsが承認されないと次に進めない」とはしていないです。他にいい言葉が思いつかないのでレビューという言葉を使ってますが、レビュープロセスは「なんかよさそう」くらいでもOKで運用しています。Design docsを書くフェーズでは厳密な計画や設計はできない、と考えているからで、セキュリティやコンプライアンスその他のrequiredなレビューは別のフェーズで行っています。もちろん、セキュリティその他の関心事をdesign docsの中で議論したり詰められるケースであれば、そこでやるのが良いですし、そのための道具だと考えています。 (b)について、承認フローはないのですが、合意形成のツールとして使う場面もあります。ですが、これくらいのサイズの組織ではあまりそれに囚われずに「やればできる」の精神で進めることも大事なので重要視をしていないです。 テンプレート Design docsを議論を行う道具として使う性質上、文章を指定しつつコメントすることが効率的にできるツールが望ましいのでFinatextではGoogle Docsを利用しています。Google Docsにはテンプレート機能があるのでそれを用意して新規にdesign docsを書くときのレールにしています。 タイトルとメタ情報 タイトルの下に以下のメタ情報欄を用意しています。 Owner: この議論をドライブする人 Contributors: 他にいれば Team: チームもしくはプロジェクト Short self link: TODO Status: WIP | In-Review | Reviewed | Implemented Created: 1/1/2024 Updated: 1/1/2024 Reviewers: レビューアーまたは読んで合意したい人 オーナーやチーム情報は後になって聞きたい時に履歴を見ずに誰やどのチームに聞くといいのかわかるようにつけています。 Statusフィールドは今の状態を太字にする運用にしています。 Short self linkはGoogle DocsのURLがランダムな文字列で内容と関連性が薄く使いづらいので用意しています。Finatextではまだ社内短縮URLサービス用意してないのでしばらく “TODO” ですが…(機運を高めています) Overview ドキュメントが長ければ概要を書く。 と書いていて省略可能にしています。 Background 共有しておくべき前提を書く。なければ省略。壮大になりすぎないように注意。Design Docsにおいては「コンテキストがわかる人だけが議論できればOK」という前提が重要。 というplaceholder messageを書いています。 Problems to Solve どんな問題や改善すべき状況を解決したいのかを書く。一番重要。「どこまでを考慮するのか」のようなスコープを言語化することも重要。 Proposal 上記で言語化した解決したい問題について、解決するために「取れる選択肢とそれぞれのpros/cons」と「実際にどのアプローチを選択するか、なぜか」を書く。 また、それを実現するにあたって、 - 調査したこと・まだ調査してないこと - 考慮すべき事項 も言語化する。図とかも使うとなおよし 。 FAQ Proposalセクションにうまく盛り込めなかったけど他の人が疑問に思ったを書くと良い。あとからうまくProposalを更新していくのは大変なので。 とplaceholder messageを書いているように、レビュープロセス等でわかった情報などをうまく本文更新できない時に使っています。 Further Work ここでの議論のスコープ外ではあるが、当然思い浮かぶような事項について補足する。 特にプロダクト開発に近い領域だと、あれもこれもアイディアや解決したい(が、リソース的に優先度は上げられない)問題が思い浮かぶので、夢リストとして書いておくことでレビューアー・読み手の思考コストを下げようとしています。 まとめ Finatextでは、Design Docsプラクティスを「問題と解決策を整理する・議論する」「特定の領域に詳しい人の意見をもらう」ための道具として活用しています。他の中規模な組織でも応用でき有用な気がしているのでこの記事で紹介しました。 明日は菅原さんによる 「結局、投資初心者はいつ投資を始めればいいのか?~思い立った日がベストな投資タイミング~」 についての記事です。お楽しみに! 最後に、Finatextではソフトウェアエンジニアに限らず様々な職種で募集しています!金融という難しい領域ながら、信頼できるメンバーとともにおもしろいアプローチでチャレンジできる環境なので、興味がある方は下のURLからなり、ぼくのメールアドレスやLinkedinやXのDMなどからコンタクトください! https://hd.finatext.com/recruit/ Adapting Design Docs Practice for Mid-Sized Companies was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
アバター
この記事はFinatextグループ10周年記念アドベントカレンダーの11日目の記事です。 昨日は狩野さんが「 1人目QAとして他業種から飛び込むときのマインドセット 」という記事を公開しています。 こんにちは、Finatextの @s_tajima です。 今回は、Finatextグループにおいて、「競争優位性を生み出す技術」をより増やしていくためにEnabling Teamを立ち上げた話です。弊社のEnabling Teamが、どんな役割で、どんな特徴を持った立ち上げ方をしていて、どんなテーマに取り組んでいるかといったお話を書いています。 Enabling Teamとは Enabling Teamは、 Team Topologies で紹介されている組織形体の1つで、以下のように説明されています。 イネイブリングチームは、特定のテクニカル(プロダクト)ドメインのスペシャリストから構成され、能力ギャップを埋めるのを助ける。複数のストリームアラインドチームを横断的に支援し、適切なツール、プラクティス、フレームワークなどアプリケーションスタックのエコシステムに関する調査、オプションの探索、正しい情報に基づく提案を行う。 マシュー・スケルトン,マニュエル・パイス. チームトポロジー 価値あるソフトウェアをすばやく届ける適応型組織設計 (Japanese Edition) (p. 151). Kindle Edition. つまりEnabling Teamによって、難易度の高い実装や機能を、より適切な方法で実現できる可能性を高められると考えています。 なぜEnabling Teamが必要か 「競争優位性を生み出す技術」になりうるのは、得てして簡単に習得できるものではなく、現時点では組織の中の誰も持っていない、より高度な技術・ノウハウである可能性が高いと考えています。 また、プロジェクトの通常の開発スケジュールの中で、重みのある新しい技術検証→実践導入という対応をするのは難易度が高いと考えています。特定プロジェクトの個別要因に引きづられすぎず、最大公約数的・汎用的な設定やアーキテクチャを突き詰めて考えておくことも大事です。そのために、Enabling Teamをプロジェクトやプロダクトとは切り離し、独立した組織として立ち上げることにしました。 FinatextにおけるEnabling Teamの特徴 FinatextにおけるEnabling Teamの役割は、「今のプロダクト開発の 直線的な マイルストーンには乗らない技術を検証し、習得したりノウハウを溜めた上で、これを実プロダクト・実環境に導入するサポートをおこなうこと」です。 これを前提に、大事だと考えているポイントが4つあります。部分的には、一般的なEnabling Teamの役割やプラクティスとは異なる部分もあるかもしれません。 1つ目は、「メンバーがすでに持っているノウハウや知識に頼るのではなく、まずはEnabling Teamにアサインされたメンバーが新たにその能力を習得することを前提とすること」です。Enabling Teamとして取り組みたいテーマは、多くが難易度が高かったり、そのノウハウを保有する人の希少性が高い領域となります。よって、「誰かがすでにその知識を持っていること」を前提とするべきではないと考えました。 2つ目は、「実プロダクト・実環境への導入を(強めの)前提とすること」です。 CoP(Communities Of Practice)やR&Dとして、学習や技術検証やその研究成果の発表までを主な目的とするのではなく、実際に社内のどこかで使われている状況にすることまでをゴールとしています。こうすることで、Enabling Teamが解散してもそのノウハウが社内で維持されることを担保できます。逆にいうと、状況の変化等で実環境への導入の目処がなくなったのであれば、そのテーマへの取り組みは終了させるべきと考えています。 3つ目は、「”Enabling Team” として1つのチームにするのではなく、テーマごとにそれぞれ独立したチームとすること」で す。Enabling Teamとして取り組みたいテーマは多岐にわたります。すべてのテーマを1つのEnabling Teamとして少数人数で担当するのではなく、それぞれのテーマごとに適任者を見つけてアサインするほうが良いと考えました。一方で、Enabling Teamとしてのメタな技能(スキルトランスファーに長けているとか、プロダクトに特定機能を導入するのがうまいとか)の重要性は感じてはいるので、中長期では専任者を置くことも視野には入れています。 4つ目は、「システムの運用の責任を持たせないこと」です。これは、2つ目で書いた、チームを解散させることを意識しています。システムの運用責任を持つ前提があるのであれば、チームを短期で解散させるべきではないため、これも大事なポイントです。 どのようなテーマに取り組んでいるか 2023年12月現在では、以下のようなテーマに取り組んでいます。 認証・認可基盤 Open ID Connect / OAuth / Financial-grade API / Passkeys といったキーワードを軸に、それぞれの仕様や実装を理解し、プロダクトに適切に導入するチームです。 マルチリージョン 金融庁による「オペレーショナル・レジリエンス確保に向けた基本的な考え方」に記載された内容への対応や、大阪リージョンの拡充をきっかけとして、システムのマルチリージョン化の取り組みをするチームです。 コンタクトセンター 主にAmazon Connectを活用し、より洗練されたコンタクトセンター(≠ コールセンター) の実装をするチームです。 Amazon Qを始めとするGenerative AI / LLMの活用もスコープとしています。 ナレッジベース 社内の知識を管理し、オンボーディングの効率化や、業務プロセスの疑問を解消することを手助けするための仕組みを作ります。 これも、Generative AIやLLMの活用を想定しています。 以上、簡単にですがFinatextグループのEnabling Teamのご紹介でした。 明日は、Takuma Kobayashiによる「Finatextは僕を変えた〜23卒エンジニアの入社エントリ〜」です! Enabling Teamを立ち上げました was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
アバター
この記事は、 Finatextグループ10周年記念アドベントカレンダー の10日目の記事です。昨日は關さんが「 法人向けPCのスマートな買い方 」という記事を公開しています。 こんにちは!FinatextでQAエンジニアをしている狩野(かりの)です。 7年くらい前からQA(Quality Assurance 品質保証)エンジニアを名乗り、ゲーム → AI医療機器 → ヘルスケアDX → 金融と全然異なる業種を渡り歩いた私が「他業種に1人目QAとして飛び込むときのマインドセット」について語ります。 これから1人目QAとして他の業種へ飛び込もうと考えている方や、異業種から当社へ飛び込もうとしている方に対して、不安に押しつぶされそうなココロに勇気を与えられれば!ということを踏まえつつ以下へスクロールをお願いします。 1. 開拓者精神を持つ 1人目QAという言葉の通り、飛び込んだ先に同じロールモデルで業務しているメンバーはおりません。そもそもQAやQC(Quality Control 品質管理)といった言葉すら浸透していない場合があります。 その環境に対して不安に思うことは一切ありません。あなたが「主役」なのです。主体性を持った行動が肝心です。 今までに身に着けた知識や経験を発揮し、周りを巻き込んでいくというフロンティアスピリッツをココロにいだき、チームメンバーに対してQA/QCの存在の大事さを展開していく。その心構えが何よりも大事です。 2. 朱に交われば赤くなる いくら開拓者精神を持っていても知らないこと、わからないことは山ほどあります。焦る必要は一切ありません。あなたがQA/QCのメンバーとしてやっていくというココロを持ち続けていれば、自ずと知識と経験は身についていきます。 最初は業務経験がないことで、MECE(漏れなくダブりなく)なテストケースが作れなくても、細かくPDCAサイクルを繰り返したり、チームメンバーとレビューを繰り返すことで、自ずとその企業や環境に適切な設計ができるようになります。 わからない技術やツールがあっても、何度も繰り返して使うことで誰よりも詳しくなることがあり、それをチームメンバーと共有することでさらに自身の技術向上が見込めたりします。 3. アンラーニング / リスキリング 「前まではこうだったから」「このやり方が私に取って最適だから」 業務をいかに早く効率よく進めていくために、このような考え方があるのは大事です。ただし、これは1人目QAにはふさわしくないマインドセットです。今まで経験したことがすべての最適解であるとは限りません。飛び込んだ先の企業や環境が培ってきたことを理解することが大事です。 QA/QCはその名の通りプロダクトの品質を司るポジションのため、開発されたプロダクトが確かに存在します。プロダクトが作られた背景や要件をよく理解することが求められます。文書化されたこと以上をヒアリングして理解するためには学び直しが必要です。仮に今までやってきたやり方が使えたとしてももっと効率の良いやり方があるかもしれません。 特にQAエンジニアに関する技術はココ数年で大きく発展しました。ハードウェア性能の向上から生成系AIの登場。これらをうまく業務に組み込み、「今やっていることの進化系は何か?」を考えながら、今のプロダクトの品質に目を落とすと、過去の経験以上に大きなQA/QC業務が見えるようになります。 4. 大事なのはドメイン知識 精神論のようなことばかりを書きましたが、QA/QCにとって最も大事なのは「ドメイン知識」だと考えます。 自分がQA/QCを担当するプロダクト、自分がマネジメントするチーム、自分が開発するテストツールなど、環境に即した技術やプロセスを理解した上で適用しなければなりません。 ドメイン知識は一朝一夕では身につかないことが多いです。ただし、これを一人で身につける必要はありません。すでにいるメンバーから教わったり、チームで細かく実験して共有してみたり、資格を取得してみたり、方法はたくさんあります。 ドメイン知識を得るために最適な方法は1つとは限りません。あらゆることに興味を持って業務に励むことで、自ずとドメイン知識は獲得できます。気になったことを積極的にチームメンバーにヒアリングしたり、共有することで業務に必要なドメイン知識やその周辺を取り巻く環境を知ることができます。知識を得るための行動が鍵です。 5. 法規制、コンプライアンス意識 ドメイン知識を身につけるうえで、必ず登場するのが法規制、コンプライアンスです。裏を返せば、それらを知る行動を取ることで、業種の共通知を知ることができます。 私の経験してきた業種において、ゲームであれば CESA や GooglePlay / AppStore が用意しているガイドライン、医療であれば薬機法や省令/規約、金融であれば金商法や協会のガイドライン。もちろんこれらだけではなく、より多くの「ルール」が存在します。 QA/QCを担当するにはこれらを幅広く知り、ときには違反がないようにブレーキをかけることもあります。ブレーキを掛ける際の背景を知らないままにチームメンバーや顧客に伝えるのはNGです、理解が得られません。ルールに納得する必要性は無いかもしれませんが、ルールができた背景を知っておくことはとても重要です。 業種によって変わるもの: 技術 開発環境やチームメンバーが変われば、用いる技術や開発プロセスは大きく異なります。 JSTQB や ISO25000シリーズ のようなテストの標準体系があったとしても、それらをどのように運用するかは組織によって変わります。 わかり易い言葉をあげるとしたら「テスト自動化」。 スクラムのような細かく開発を繰り返すような体制や、自社プラットフォームで横展開するようなプロダクトを開発するのであれば、E2E(end to end: 頭から終わりまで通しで行う)テストの運用や、ユニットテストの拡充/徹底は開発スピードの向上に貢献します。 逆に、業務システムの刷新のようなウォーターフォール開発に関しては、E2Eテストではなく、文書生成の自動化や、レビュープロセスにかかるチェックリスト作成の自動化など、プロダクトの周辺を効率化することを求められたりします。代表的なのは RPA(Robotic Process Automation)です。いかに同じことの繰り返しをミスなく効率よく行えるようにするかで、システムの品質向上に貢献します。 他業種に飛び込んでいく1人目QAを生業としているヒトは、このような様々な経験をテスト・デバッグ・検証のポジションで身につけられること発揮できることで喜びを得られるヒトが多いのではないでしょうか。 金融のQAエンジニアの面白さ Finatextは「金融をサービスとして再発明する」ことをミッションとしている企業です。銀行、証券、保険といった様々な金融業界に対して、自社のエンジニアによって多くのプロダクトやソリューションが開発されております。 私はそのなかの1人目QAとしてジョインしました。1つのプロダクトのテストメンバーとしてのテスト計画や設計/実施、E2Eテストの自動化、レビュープロセスの整備、社内勉強会でのQAというポジションや役割の展開などを進めていますが、まだまだやることは多く存在します。同じQAポジションのメンバーも増えてきて、よりQAとしての活躍の場を広げているところです。 上記のマインドセットをもとに日々業務に邁進し、当社が提供するサービスの品質向上を目指しております。 マインドセットとしては以上になります。 「他業種に飛び込んで身近なことを話題にしたQA/QC活動をしたい」 「金融に興味があって、多少のエンジニアリングスキルはあるのだけど何から手を付けてよいかわからない」 「とりあえず金融で新しいことにチャレンジしたい」 そんな事を考えていらっしゃる方、どうぞFinatextでQAエンジニアとして始めてみませんか? 明日は CTO/CISO 田島さんによる「 Enabling Teamを立ち上げました 」についての記事です。お楽しみに! 1人目QAとして他業種から飛び込むときのマインドセット was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
アバター
この記事は Finatextグループ10周年記念アドベントカレンダー の7日目の記事です。 昨日は大木さんが「 AWS re:Invent 2023に参加してきました 」という記事を公開しています。 はじめに こんにちは、Finatextグループの ナウキャスト でデータエンジニアをしている六車( X: @mt_musyu )です。 Terraformを導入してIaC化をしたのちに、IaCのコードレビューの仕組み、安全なTerraform Applyの仕組みを構築することが必要になります。 ナウキャストでは Atlantis というTerraformのPlanやApplyをGithub Pull Request上で制御できるツールを用いて、レビューから適用まで楽に運用ができるようになっています。 ref: Snowflake×dbt×Terraformでモダンなデータ基盤開発してみた ただ、Atlantisを利用するためには自前でAtlantisのサーバーを立てる必要があり、そのためにはサーバーの管理や運用が必要になります。 社内でのリソースの確保や運用のための人員の確保が難しい場合には、Atlantisを利用することを諦めてしまうこともあるかと思います。 今回はよりスモールにGitHub ActionsのみでAtlantis likeなTerraformのCICDの仕組みを構築する方法を紹介します。 ユースケース 今回は複数のGoogle Cloud のプロジェクトを管理するために、TerraformのCICDの仕組みを構築します。 管理するプロジェクト構成としては以下のような構成を想定しています。 二つのプロジェクトはすでに存在しているものとします。 project-a-dev project-a-prod なお、Terraformのディレクトリ構成はprojectごとにディレクトリを分け、その中にprojectごとのTerraformのコードを配置するようにしています。 またブランチ戦略としては、mainブランチのみの運用とします。 ex. project-a-devのTerraformコードは environments/project-a-dev に配置する。その内容を反映させたい場合、mainブランチに対してPRを作成する。 root/environments ├ project-a-dev └ project-a-prod Terraform Planの実行 Terraform PlanはPRが作成された時(と新たなpushが生じた時)とPRのコメント上で terraform plan [project名] と入力された時に実行されるようにします。 また、Terraform Planの実行結果はPRのコメント上に追記されるようにします。 jobs: terraform_plan: name: Terraform Plan runs-on: ubuntu-latest permissions: id-token: write contents: read pull-requests: write defaults: run: working-directory: "environments/${{inputs.project}}/" # プロジェクトごとにディレクトリを分けている steps: - name: Checkout Repo if: github.event_name == 'pull_request' uses: actions/checkout@v2 - name: Checkout Repo if: github.event_name == 'issue_comment' uses: actions/checkout@v2 with: ref: refs/pull/${{ github.event.issue.number }}/merge - name: Get PR Number id: get-pr-number run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "pr_number=${{ github.event.number }}" >> "$GITHUB_OUTPUT" elif [ "${{ github.event_name }}" == "issue_comment" ]; then echo "pr_number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" fi - name: Authenticate to GCP uses: google-github-actions/auth@v2 with: create_credentials_file: true workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.SERVICE_ACCOUNT }} - name: Setup Terraform uses: hashicorp/setup-terraform@v3 - name: Terraform Plan id: plan if: steps.lock-check.outputs.locked == 'false' run: | echo "plan_output_path=$(pwd)/plan_output.txt" >> "$GITHUB_OUTPUT" terraform plan -no-color > plan_output.txt 2>&1 - name: Comment Plan Result on PR if: always() && steps.lock-check.outputs.locked == 'false' uses: actions/github-script@v5 with: script: | const fs = require('fs'); const planOutput = fs.readFileSync('${{ steps.plan.outputs.plan_output_path }}', 'utf8'); const commentBody = ` Ran Plan for dir: \`${{inputs.project}}\` <details> <summary>Show Output</summary> \`\`\` ${planOutput} \`\`\` </details> To apply this plan, comment: \`terraform apply ${{inputs.project}}\` To plan this project again, comment: \`terraform plan ${{inputs.project}}\` `; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: commentBody }); Terraform Applyの実行 Terraform ApplyはApproveされたPRのコメント上で terraform apply [project名] と入力された時に実行されるようにします。 Terraform Planと同様にTerraform Applyの実行結果はPRのコメント上に追記されるようにします。 jobs: terraform_apply: name: Terraform Apply runs-on: ubuntu-latest permissions: id-token: write contents: read pull-requests: write defaults: run: working-directory: "environments/${{inputs.project}}/" steps: - name: Checkout Repo if: github.event_name == 'issue_comment' uses: actions/checkout@v2 with: ref: refs/pull/${{ github.event.issue.number }}/merge - name: Check PR Approval Status id: check-pr-status uses: actions/github-script@v5 with: script: | const { data: reviews } = await github.rest.pulls.listReviews({ owner: context.repo.owner, repo: context.repo.repo, pull_number: context.issue.number }); const RequestChanges = reviews.some(review => review.state === 'CHANGES_REQUESTED'); const Approved = !RequestChanges && reviews.some(review => review.state === 'APPROVED'); core.setOutput('approved', Approved); - name: Comment and exit if not approved if: steps.check-pr-status.outputs.approved == 'false' uses: actions/github-script@v5 with: script: | await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: 'Failed: Pull request is not approved.' }); process.exit(1); - name: Authenticate to GCP uses: google-github-actions/auth@v2 with: create_credentials_file: true workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.SERVICE_ACCOUNT }} - name: Setup Terraform uses: hashicorp/setup-terraform@v3 - name: Terraform Init run: terraform init -no-color - name: Terraform Apply id: apply if: steps.lock-check.outputs.locked == 'false' && steps.check-pr-status.outputs.approved == 'true' run: | echo "apply_output_path=$(pwd)/apply_output.txt" >> "$GITHUB_OUTPUT" terraform apply -auto-approve -no-color > apply_output.txt 2>&1 - name: Comment Apply Result on PR if: always() && steps.lock-check.outputs.locked == 'false' && steps.check-pr-status.outputs.approved == 'true' uses: actions/github-script@v5 with: script: | const fs = require('fs'); const applyOutput = fs.readFileSync('${{ steps.apply.outputs.apply_output_path }}', 'utf8'); const commentBody = ` Ran Apply for dir: \`${{inputs.project}}\` <details> <summary>Show Output</summary> \`\`\` ${applyOutput} \`\`\` </details> `; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: commentBody }); (補足)Google Cloudとの認証 GitHub Actions では OpenID Connect (OIDC) がサポートされています。 OIDC を使用することによりサービスアカウントキーなどを用意することなく 認証を行うことができます。 ここでは深く触れませんが、以下の箇所でOIDCによる認証を行っています。 - name: Authenticate to GCP uses: google-github-actions/auth@v2 with: create_credentials_file: true workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.SERVICE_ACCOUNT }} Configuring OpenID Connect in Google Cloud Platform - GitHub Docs Terraformのロック機構 以上でGitHub ActionsのみでTerraformのCICDの仕組みを構築することができました。 しかし、上記のままであれば複数人が同時にTerraform Apply, Planを実行してしまうと、競合が発生してしまい、危険です。 例えばある人がTerraform Planを実行してレビューしている最中に、別の人がTerraform Applyを実行してしまうと、Planの内容とApplyの内容に齟齬が生まれてしまいます。 そのためTerraformの実行を制限するロック機構の仕組みが必要です。 チームで活発にTerraformを用いてインフラを管理していく上ではTerraformのロック機構の構築は必要と思います。 今回、Terraform実行のロック機構で以下の3つの仕組みを採用・構築しました。 Terraform state locking GitHub Actions jobの同時実行の排他制御 labelを使用したPRごとのロック機構 Terraform state locking こちらはTerraformの標準機能で、Terraformのstateファイルをロックする機能です。 これにより、複数人が同時にTerraform Apply, Planを実行することを防ぐことができます。 state locking機能を利用するためには、state lockingに対応したbackendを指定する必要があります。 今回の構成では、Google Cloud Storage (GCS)を利用したbackendを利用しています。GCSはstate lockingに対応しているため、state locking機能を利用することができます。 Backend Type: gcs | Terraform | HashiCorp Developer GitHub Actions jobの同時実行の排他制御 こちらはGitHub Actionsの標準機能で、同時に実行されるjobの数を制限する機能です。 Using concurrency - GitHub Docs 簡単にまとめると以下のような機能です。 group タグのようなものでワークフローの実行をグループ化するための識別子。 このグループ名がついているものは、同時に実行されない。 cancel-in-progress(True or False) True: 同じグループ内で新しいワークフローがトリガーされると、進行中のワークフローがキャンセルされる。 False: 新しいワークフローが待機し、既存のワークフローが完了するまで実行されない。同じグループ内でCICDが同時に実行されることはなくなるが、jobの成功、失敗にかかわらずワークフローが終了すると後続のワークフローが動き出す。 今回は 同じprojectのplan, applyを同一groupに設定 cancel-in-progressをFalseに設定 としました。 この設定により、同じprojectのplan, applyは同時に実行されないようになります。 labelを使用したPRごとのロック機構 こちらはAtlantisのLocking機能を参考にしました。 そもそもなぜPRごとのlock機能が必要かはAtlantisのドキュメントに書かれています。 Locking | Atlantis 今回Terraform Applyの実行はmainブランチに対してPRがmergeされた時ではなく、PRがApproveされた状態かつコメント追加で実行されるようにしました。 となるとmainブランチのTerraformのコードと実際のインフラの状態が異なってしまう可能性があります。 実際にこの状態が不健全と考え、Terraform ApplyのタイミングはPRがmergeされたときに実行される方もいらっしゃるかと思います。 しかし、Terraform Applyは失敗することが度々あり(Terraform planやvalidateはOKでも)、その検知はPRがmergeされた後になってしまいます。となるとdurtyなTerraformのコードがmainブランチに存在することになる、また修正のPRを追加で用意する必要が出てきます。 なので、Terraform ApplyはPRがmergeされる前にPRのコメント追加をトリガーに実行されるのみとしました。 ただ、何の制限もなくPRでTerraform Applyを実行できてしまうと危険なのでApproveされたPRでのみTerraform Applyを実行できない制限と追加で、PRごとにlabelを用いたTerraformのlock機能を構築しました。 以下がlabelを用いたPRごとのlock機能の仕組みです。 Terraform Planが実行されるとそのPRに対して terraform_lock というラベルが付与される もし違うPRでterraform plan or terraform applyが実行されようとしたら他のopenなPRに対して terraform_lock ラベルがついてないか確認する。付いていたらcancel、付いてなかったら実行、 terraform_lock のラベルを付与 terraform_apply ラベルがついたPRならterraform applyは実行できる、mainブランチにmergeされたら terraform_lock ラベルは除去される もしあるPRから terraform_lock ラベルを除去したかったらPRのコメントで terraform unlock と入力したら除去される labelを用いたPRごとのTerraform実行lockの仕組み 以下は実際の実装です。(planもapplyも同じような実装です) jobs: terraform_plan: name: Terraform Plan runs-on: ubuntu-latest permissions: id-token: write contents: read pull-requests: write defaults: run: working-directory: "environments/${{inputs.project}}/" steps: - name: Checkout Repo if: github.event_name == 'pull_request' uses: actions/checkout@v2 - name: Checkout Repo if: github.event_name == 'issue_comment' uses: actions/checkout@v2 with: ref: refs/pull/${{ github.event.issue.number }}/merge - name: Get PR Number id: get-pr-number run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "pr_number=${{ github.event.number }}" >> "$GITHUB_OUTPUT" elif [ "${{ github.event_name }}" == "issue_comment" ]; then echo "pr_number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" fi - name: Check for 'terraform_lock' labels on other PRs id: lock-check run: | LOCKED_PRS=$(curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ "https://api.github.com/repos/${{ github.repository }}/pulls?state=open" | \ jq -r "[.[] | select(.number != ${{ steps.get-pr-number.outputs.pr_number }}) | select(.labels[].name == \"terraform_lock\") | {number: .number, html_url: .html_url}] | @json") echo "LOCKED_PRS=$LOCKED_PRS" >> "$GITHUB_ENV" if [[ "$LOCKED_PRS" != "[]" ]]; then echo "locked=true" >> "$GITHUB_OUTPUT" else echo "locked=false" >> "$GITHUB_OUTPUT" fi - name: Comment and exit if locked if: steps.lock-check.outputs.locked == 'true' uses: actions/github-script@v5 with: script: | const lockedPrs = JSON.parse(process.env.LOCKED_PRS); let commentBody = 'Failed: Another pull request is currently executing:'; lockedPrs.forEach(pr => { commentBody += ` [#${pr.number}](${pr.html_url})`; }); commentBody += '\n\nTo continue, please execute the `terraform unlock` command in the currently executing pull request:'; lockedPrs.forEach(pr => { commentBody += ` [#${pr.number}](${pr.html_url})`; }); await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: commentBody }); process.exit(1); - name: Add 'terraform_lock' label to this PR if: steps.lock-check.outputs.locked == 'false' run: | curl -X POST -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ -H "Content-Type: application/json" \ "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.get-pr-number.outputs.pr_number }}/labels" \ -d '{"labels": ["terraform_lock"]}' 以下は terraform unlock の実装です。 jobs: remove-lock-label: if: github.event_name == 'pull_request' || (github.event_name == 'issue_comment' && github.event.comment.body == 'terraform unlock') runs-on: ubuntu-latest steps: - name: Get PR Number id: get-pr-number run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "pr_number=${{ github.event.number }}" >> "$GITHUB_OUTPUT" elif [ "${{ github.event_name }}" == "issue_comment" ]; then echo "pr_number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" fi - name: Remove 'terraform_lock' label run: | curl -X DELETE -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.get-pr-number.outputs.pr_number}}/labels/terraform_lock" GitHub Actionsの全体 以上の設計をまとめたGitHub Actionsの全体は以下のようになります。 CI_workflow_base.yml name: Terraform Plan base on: workflow_call: inputs: project: required: true type: string secrets: WORKLOAD_IDENTITY_PROVIDER: required: true SERVICE_ACCOUNT: required: true jobs: terraform_plan: name: Terraform Plan runs-on: ubuntu-latest permissions: id-token: write contents: read pull-requests: write defaults: run: working-directory: "environments/${{inputs.project}}/" steps: - name: Checkout Repo if: github.event_name == 'pull_request' uses: actions/checkout@v2 - name: Checkout Repo if: github.event_name == 'issue_comment' uses: actions/checkout@v2 with: ref: refs/pull/${{ github.event.issue.number }}/merge - name: Get PR Number id: get-pr-number run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "pr_number=${{ github.event.number }}" >> "$GITHUB_OUTPUT" elif [ "${{ github.event_name }}" == "issue_comment" ]; then echo "pr_number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" fi - name: Check for 'terraform_lock' labels on other PRs id: lock-check run: | LOCKED_PRS=$(curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ "https://api.github.com/repos/${{ github.repository }}/pulls?state=open" | \ jq -r "[.[] | select(.number != ${{ steps.get-pr-number.outputs.pr_number }}) | select(.labels[].name == \"terraform_lock\") | {number: .number, html_url: .html_url}] | @json") echo "LOCKED_PRS=$LOCKED_PRS" >> "$GITHUB_ENV" if [[ "$LOCKED_PRS" != "[]" ]]; then echo "locked=true" >> "$GITHUB_OUTPUT" else echo "locked=false" >> "$GITHUB_OUTPUT" fi - name: Comment and exit if locked if: steps.lock-check.outputs.locked == 'true' uses: actions/github-script@v5 with: script: | const lockedPrs = JSON.parse(process.env.LOCKED_PRS); let commentBody = 'Failed: Another pull request is currently executing:'; lockedPrs.forEach(pr => { commentBody += ` [#${pr.number}](${pr.html_url})`; }); commentBody += '\n\nTo continue, please execute the `terraform unlock` command in the currently executing pull request:'; lockedPrs.forEach(pr => { commentBody += ` [#${pr.number}](${pr.html_url})`; }); await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: commentBody }); process.exit(1); - name: Add 'terraform_lock' label to this PR if: steps.lock-check.outputs.locked == 'false' run: | curl -X POST -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ -H "Content-Type: application/json" \ "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.get-pr-number.outputs.pr_number }}/labels" \ -d '{"labels": ["terraform_lock"]}' - name: Authenticate to GCP uses: google-github-actions/auth@v2 with: create_credentials_file: true workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.SERVICE_ACCOUNT }} - name: Setup Terraform uses: hashicorp/setup-terraform@v3 - name: Terraform Init run: terraform init -no-color - name: Terraform Format run: terraform fmt -recursive -check - name: Terraform Validate run: terraform validate -no-color - name: Terraform Plan id: plan if: steps.lock-check.outputs.locked == 'false' run: | echo "plan_output_path=$(pwd)/plan_output.txt" >> "$GITHUB_OUTPUT" terraform plan -no-color > plan_output.txt 2>&1 - name: Comment Plan Result on PR if: always() && steps.lock-check.outputs.locked == 'false' uses: actions/github-script@v5 with: script: | const fs = require('fs'); const planOutput = fs.readFileSync('${{ steps.plan.outputs.plan_output_path }}', 'utf8'); const commentBody = ` Ran Plan for dir: \`${{inputs.project}}\` <details> <summary>Show Output</summary> \`\`\` ${planOutput} \`\`\` </details> To apply this plan, comment: \`terraform apply ${{inputs.project}}\` To plan this project again, comment: \`terraform plan ${{inputs.project}}\` `; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: commentBody }); CD_workflow_base.yml name: Terraform Apply base on: workflow_call: inputs: project: required: true type: string secrets: WORKLOAD_IDENTITY_PROVIDER: required: true SERVICE_ACCOUNT: required: true jobs: terraform_apply: name: Terraform Apply runs-on: ubuntu-latest permissions: id-token: write contents: read pull-requests: write defaults: run: working-directory: "environments/${{inputs.project}}/" steps: - name: Checkout Repo if: github.event_name == 'issue_comment' uses: actions/checkout@v2 with: ref: refs/pull/${{ github.event.issue.number }}/merge - name: Get PR Number id: get-pr-number run: echo "pr_number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" - name: Check for 'terraform_lock' labels on other PRs id: lock-check run: | LOCKED_PRS=$(curl -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ "https://api.github.com/repos/${{ github.repository }}/pulls?state=open" | \ jq -r "[.[] | select(.number != ${{ steps.get-pr-number.outputs.pr_number }}) | select(.labels[].name == \"terraform_lock\") | {number: .number, html_url: .html_url}] | @json") echo "LOCKED_PRS=$LOCKED_PRS" >> "$GITHUB_ENV" if [[ "$LOCKED_PRS" != "[]" ]]; then echo "locked=true" >> "$GITHUB_OUTPUT" else echo "locked=false" >> "$GITHUB_OUTPUT" fi - name: Comment and exit if locked if: steps.lock-check.outputs.locked == 'true' uses: actions/github-script@v5 with: script: | const lockedPrs = JSON.parse(process.env.LOCKED_PRS); let commentBody = 'Failed: Another pull request is currently executing:'; lockedPrs.forEach(pr => { commentBody += ` [#${pr.number}](${pr.html_url})`; }); commentBody += '\n\nTo continue, please execute the `terraform unlock` command in the currently executing pull request:'; lockedPrs.forEach(pr => { commentBody += ` [#${pr.number}](${pr.html_url})`; }); await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: commentBody }); process.exit(1); - name: Check PR Approval Status id: check-pr-status uses: actions/github-script@v5 with: script: | const { data: reviews } = await github.rest.pulls.listReviews({ owner: context.repo.owner, repo: context.repo.repo, pull_number: context.issue.number }); const RequestChanges = reviews.some(review => review.state === 'CHANGES_REQUESTED'); const Approved = !RequestChanges && reviews.some(review => review.state === 'APPROVED'); core.setOutput('approved', Approved); - name: Comment and exit if not approved if: steps.check-pr-status.outputs.approved == 'false' uses: actions/github-script@v5 with: script: | await github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: 'Failed: Pull request is not approved.' }); process.exit(1); - name: Add 'terraform_lock' label to this PR if: steps.lock-check.outputs.locked == 'false' run: | curl -X POST -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ -H "Content-Type: application/json" \ "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.get-pr-number.outputs.pr_number }}/labels" \ -d '{"labels": ["terraform_lock"]}' - name: Authenticate to GCP uses: google-github-actions/auth@v2 with: create_credentials_file: true workload_identity_provider: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ secrets.SERVICE_ACCOUNT }} - name: Setup Terraform uses: hashicorp/setup-terraform@v3 - name: Terraform Init run: terraform init -no-color - name: Terraform Apply id: apply if: steps.lock-check.outputs.locked == 'false' && steps.check-pr-status.outputs.approved == 'true' run: | echo "apply_output_path=$(pwd)/apply_output.txt" >> "$GITHUB_OUTPUT" terraform apply -auto-approve -no-color > apply_output.txt 2>&1 - name: Comment Apply Result on PR if: always() && steps.lock-check.outputs.locked == 'false' && steps.check-pr-status.outputs.approved == 'true' uses: actions/github-script@v5 with: script: | const fs = require('fs'); const applyOutput = fs.readFileSync('${{ steps.apply.outputs.apply_output_path }}', 'utf8'); const commentBody = ` Ran Apply for dir: \`${{inputs.project}}\` <details> <summary>Show Output</summary> \`\`\` ${applyOutput} \`\`\` </details> `; github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: commentBody }); terraform_lock ラベル除去の定義; CI_terraform_unlock.yml name: Terrafrom Unlock on: issue_comment: types: [created] pull_request: branches: - main types: [closed] jobs: remove-lock-label: if: github.event_name == 'pull_request' || (github.event_name == 'issue_comment' && github.event.comment.body == 'terraform unlock') runs-on: ubuntu-latest steps: - name: Get PR Number id: get-pr-number run: | if [[ "${{ github.event_name }}" == "pull_request" ]]; then echo "pr_number=${{ github.event.number }}" >> "$GITHUB_OUTPUT" elif [ "${{ github.event_name }}" == "issue_comment" ]; then echo "pr_number=${{ github.event.issue.number }}" >> "$GITHUB_OUTPUT" fi - name: Remove 'terraform_lock' label run: | curl -X DELETE -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \ "https://api.github.com/repos/${{ github.repository }}/issues/${{ steps.get-pr-number.outputs.pr_number}}/labels/terraform_lock" また、今回のユースケースでは二つのproject(ディレクトリ)に対してTerraformのCICDを構築する必要があったため、 CI_workflow_base.yml と CD_workflow_base.yml をそれぞれ再利用可能なワークフローとして定義し、 CI_project-a-dev.yml と CD_project-a-dev.yml のようにそれぞれのprojectごとにワークフローを定義しました。 CI_project-a-dev.yml name: Terraform Plan project-a-dev on: pull_request: types: [opened, synchronize] paths: - "environments/project-a-dev/**" issue_comment: types: [created] concurrency: group: project-a-dev cancel-in-progress: false jobs: call_CI_workflow: if: github.event_name == 'pull_request' || (github.event_name == 'issue_comment' && github.event.comment.body == 'terraform plan project-a-dev') uses: ./.github/workflows/CI_workflow_base.yml with: project: project-a-dev secrets: WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} CD_project-a-dev.yml name: Terraform Apply project-a-dev on: issue_comment: types: [created] concurrency: group: project-a-dev cancel-in-progress: false jobs: call_CD_workflow: if: github.event_name == 'issue_comment' && github.event.comment.body == 'terraform apply project-a-dev' uses: ./.github/workflows/CD_workflow_base.yml with: project: project-a-dev secrets: WORKLOAD_IDENTITY_PROVIDER: ${{ secrets.WORKLOAD_IDENTITY_PROVIDER }} SERVICE_ACCOUNT: ${{ secrets.SERVICE_ACCOUNT }} 実際の運用イメージ 実際にPR上でTerraform planを実行している様子は以下の通りです。 また、すでにTerraform Planが実行され terraform_lock ラベルがついているPRが存在されている場合に新しいPRでTerraform Planを実行しようとすると以下のようなコメントがPRに追加され、Terraform Planが実行できません。 Terraform Apply実行時にPRがapproveされていない場合は以下のようなコメントがPRに追加されます。 まとめ 以上がGitHub Actionsを用いたAtlantis likeなTerraformのCICDの構築方法でした。 今回の構築では以下のようなメリットがあります。 個人のアカウントでTerraformを実行する強い権限を与える必要がなく、PR上でのTerraformの実行のみで済むため、ガバナンスを効かせることができる PR上のコメントでTerraform実行ができ、その結果もPR上で確認できるため、Terraformの実行の可視化ができる ローカルでTerraform環境を用意する必要が必須ではないため、プログラミング経験、Gitの利用経験があればどなたでも作業可能 PRごとのlock機能があるため、複数人でのTerraformの実行が安全にできる PRにTerraformのplan/applyの履歴が残ることで、audit log的な過去に誰がいつどんな変更をしたのか確認が容易 GitHub Actionsのみで構築したため、コスト少なく運用できる 大規模にTerraform運用をしていく場合ではAtlantisなどの専用のツールを利用した方が良いかと思いますが、小規模であればGitHub Actionsのみで構築することで、最低限のガバナンスを効かせてTerraform運用を実現できるかと思います。 明日は増田さんによる「 『○○』として働く~私がBizDevもHRも両方やる理由~ 」という記事です。お楽しみに! ナウキャストでは一緒に働く仲間を募集中です! 興味のある方は以下の資料をぜひご覧ください! https://medium.com/media/600e9bbfa21fd003c9763cc58f2217d4/href GitHub ActionsのみでAtlantis likeなTerraform CICDの仕組みを構築する was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
アバター
この記事は、 Finatextグループ10周年記念アドベントカレンダー の6日目の記事です。昨日は翁長さんが「 Amazon AuroraとSnowflakeをDynamic Tableを用いてリアルタイム連携したお話 」という記事を公開しています。 こんにちは、エンジニアの大木です。 先日、ラスベガスで開催された AWS re:Invent 2023 に参加してきました。弊社からは合計3人での参加で、自分自身は初めての参加でした。 弊社CTOをはじめ、既に多くの方が参加レポートを書いていますが、初参加のエンジニアが現地で具体的にどんな体験をしたのか、自分の記録も兼ねて記事にしたいと思います。 Keynote 4日間に渡って毎日実施される Keynote では、それぞれでAWSの新しいサービスや機能が発表されます。新サービスについては特に Amazon Q が今後の仕事の進め方を大きく変えていく可能性がある、という意味で最も気になりました。 ただ技術者としては、新サービスの発表が最も少なかった、最終日の Dr. Werner Vogels の Keynote が現地で聞けて良かったな、と感じます。内容は各所で書かれているのでスキップしますが、技術者としてどういうスタンスでいるべきか、どういう考え方をすべきか、深く考えさせられました。 Sessions re:InventでAWSが提供するSessionには複数のtypeがあります。 私は、GameDay, Chalk Talk, Workshop, Breakout Sessionに参加しました。 GameDay … 3–4人のチームで、課題を解いていくセッション Chalk Talk … 100人程度の参加者で、ホワイトボードでスピーカーと対話形式で進めるセッション Workshop … 各自、ハンズオンで課題を進めていくセッション Breakout Session … 数百人に対して、スピーカーが発表するセッション 以下は参加したSessionのメモです。 GameDay GHJ302: AWS GameDay Championship: Network topology Titans Unicorn Rentalsという会社のエンジニアとして、ネットワーク周りのタスクを進めていくものでした。 個人的にはVPC IPAMやTransit Gateway, Network Firewall等、普段は触ることが無いサービスを利用するのは良い経験になったと同時に、最後に時間が足りなくなったこともあり少しくらいは事前知識を仕入れてきたら良かったな〜と思うこともありました。 Chalk Talk FSI313: Build highly resilient multi-Region databases for financial services component failoverではなく、full-stack failoverの話。 最初はUDR(Uni-directional replication), active-passiveが基本となるという話だったが、質問者との対話の中で最後はBDR(Bi-directional Replication), active-activeについてのセッションになった。 UDR, active-passiveにおいてのfailover 1. application trafficをregion1で止める 2. region2でreplication lagが0になるのを待つ 3. region2のdatabaseをprimary databaseにpromoteする 4. reconcileして、必要に応じてtransactionをregion2に反映する 5. DNS Cutoverする 6. region1にread replicaを作成する BDR, active-activeの選択肢 - VPC Peeringでcross-regionの書き込みを実施する - AuroraのWrite Forwardingを利用 AWS Secrets Managerでdatabase credentialsだけで無く、endpoint等を保持することでFailover時にまとめて更新可能 WorkShop SAS403: SaaS survivor: Building a rich multi-tenant operations experience API Gateway/Lambda/DynamoDBを利用しているSaaSにおいて、以下の課題をハンズオン形式で解いていきました。 監視と通知をtenantごと分割する - API Gateway Lambda AuthorizerにてtenantID/tenantNameをログ出力し、Metrics FilterでDimensionを設定 tenantのパフォーマンスがお互いに影響しないようにする - API GatewayのUsage PlanとLambdaのConcurrency Limitを利用 インフラレベルでtenant分離を実現する - API Gateway Lambda AuthorizerにてCognito Identity Poolとget_credentails_for_identityを利用して、tenantに特化したcredentialsを取得 - このcredentialsのaccess keyを、実際の処理を実行するLambdaに渡し、Lambdaはそれをもってresourceにアクセスする tenant分離の監査 - Step Functionsを利用して、定期的にAthenaでクエリを実行し、違反したものをCloudWatchへ送信 Breakout Session SAS308: SaaS anywhere: Designing distributed multi-tenant architectures SaaS anywhereとは、アプリケーションの一部をproviderではなくtenantに配置する形式で、近年増えているtopic SaaS anywhereは設計が複雑になるが、その中でもSaaSのCore Valuesを維持することが大事 - Agility, Economy of scale, Operational efficiency, Growth, Reduced Cycle time Distributed data stores - databaseのみtenantに配置するモデル - compliance文脈で、tenantがdataを自らcontrolしたい場合など - アクセスはassume roleでやりましょう Distributed application services - 一部のapplicationをtenantに配置するモデル - tenant分離には有効 - PrivateLinkを利用するとよりreliableになる Remote application plane - Control Planeのみproviderに配置するモデル - compliance文脈や、tenantにLegacyな文化がある場合など - providerのCloudFormationで各環境に同じようにデプロイする ARC309: Using zonal autoshift to automatically recover from an AZ impairment Hard FailureとGray Failure - Hard Failureは1つのAZが完全にダウンするもので、過去10年で起きたのは、ある小さなregionでの1回限り - Gray FailureはあるAZの一部の調子が少し遅くなったり、一部処理がエラーとなるもの。ちょくちょく起きている AWSのShared Responsibility ModelにおけるAZ impairment - Lambda/DynamoDB/S3/API Gatewayに関しては、AZ impairmentからの復旧はAWSが完全に責任を持つ - AWSは、AZ impairmentが起きても良いように、常に1つのAZ分のcapacityを空けている - Lambda Patternがよく利用されるのは、customerがAZ impairmentについて考えなくても良いためというのもある - 反対にEC2/RDS/ELBに関しては、customerが復旧に責任を持つ マイクロサービスアーキテクチャにおけるAZ impairment - siloedモデルはdetectionもrecoveryも容易 - non-siloedはより疎結合で、スケーラブル zonal shiftとzonal autoshift - ELBではzonal shiftとzonal autoshiftが利用できる - zonal autoshiftでは、practice runがrequired(1週間に30分zonal shiftがapplyされる) 最終的には、cost(capacity)とuser experience(復旧時間)のtradeoff API401: Advanced serverless workflow patterns and best practices Step Functionsが実は様々なユースケースで利用できるよという話 Lambdaでの簡単な処理は、Step Functionsに移行できるものがある - 例えばDynamoDBへの操作をStep Functionsは提供している - 自身でコードを書かなくて良いというメリットがある StandardとExpressの話 - Step Functionsは料金が高いと言われるが、Expressを利用すれば(Standardに比べて)とても安くなる - idempodentな処理で5分以内に完了するものはExpressが良い可能性が高い その他便利機能・パターン - Intrinsic functions, Callback Pattern, Saga Pattern, redrive, http request, distributed map state… - https://serverlessland.com/workflows - https://serverlessland.com/explore/reinvent2023-api401 EBC (Executive Briefing Center) 出発前にAWSの方にアレンジしていただき、Amazon Cognitoに関するEBCを開いていただきました。 EBCは、少人数でAWSのExpertと直接議論ができる場で、弊社からは渡米した3人、AWSからExpertが3人参加しました。 内容はNDAのため記載できませんが、弊社からはユースケースの説明や機能要望等、AWSからは現状の開発の状況、ロードマップ、内部のアーキテクチャについて共有をしていただきました。 EXPO re:Invent では、たくさんの企業がブースを出して自社の宣伝をしています。 DatadogやSentry, SonarCloud等、既に利用しているサービスのブースも出ていましたが、初めて知るサービスのブースが非常に多く、数時間にわたって気になったブースをアプローチし、デモを見せてもらったり内容を聞いたりしていました。 TIPS ホテル 私は The LINQ Hotel + Experience に泊まりました。 re:Inventのメイン会場であり、毎朝Keynoteが行われる The Venetian からは徒歩10分ほどで移動可能で、値段もリーズナブルなので、とてもおすすめです。 AWSが提供するホテルによっては The Venetian まで1時間弱かかることもあり、少しでも夜間の睡眠時間を稼ぐためには、The Venetian から近いホテルが良いと思います。 なお、 The LINQ Hotel + Experience からは駐車場を通ると The Venetian までのショートカットもありました。 持ち物 毎日10キロ以上は歩くので、履き慣れたスニーカーは必須です。 また、現地では忙しくなるため、飛行機では少しでも長く寝たほうが良く、快眠グッズも持っていくのをおすすめします。 自分は、ノイズキャンセリングヘッドフォンと、質の良いアイマスクを持っていきました。 また、各所でswag(お土産)をもらうことが多いため、スーツケースは半分程度空けていくことをおすすめします。 交通 空港との行き帰りはタクシーと路線バス、会場間の移動ではモノレール、シャトルバス、Uberを利用しました。 モノレールやシャトルバスは無料で利用できますが、一部会場からはモノレールが遠かったり、シャトルバスも大規模Sessionの後だと長蛇の列ができていたりするので、計画的な移動が大事です。 シャトルバスに関しては、夕方は渋滞もしていました(直前に行われていたF1の影響で、道も狭くなっていたようです) 空港との行き帰りはタクシーやUberがおすすめではありますが、路線バスに乗れば1/10以下の値段で移動することができます。 英語 現地では英語のやり取りが多く発生します。KeynoteやSessionでは特にリスニング力が重要ですが、例えばEXPOでは企業の出展ブースで現地のエンジニアと1対1で会話することになるため、スピーキング力も必要です。 弊社では週に一度、福利厚生で 英会話レッスン を受けることができます。自分は元々英会話には比較的自信があったことに加えて、このレッスンを数年間受講していたこともあり、現地のコミュニケーションでも大きく困ることはありませんでした。 最後に 以上、AWS re:Invent 2023の参加記でした。 この1週間は、普段の業務から離れて、技術的な事に頭を全力で使う事ができて、とても贅沢な体験だったなと思います。 なお、Finatextでは一緒にビジネスを成長していけるエンジニアを募集しています! FinatextではAWSをメインのクラウドとして使っており、かつ各エンジニアの裁量も大きいため、AWSのサービスを業務として使い倒せる環境が整っています。 re:Inventに参加された方も参加されていない方も、ご興味がある方はぜひお声がけください! Finatext 開発チーム 採用情報 明日は六車さんによる「 GitHub ActionsのみでAtlantis likeなTerraform CICDの仕組みを構築する 」の記事です。お楽しみに! AWS re:Invent 2023に参加してきました was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
アバター
この記事は Finatextグループ10周年記念アドベントカレンダー 5日目の記事です。 昨日は齊藤さんが「 愛される会社であり続けるために。人気漫画から見えるヒント 」という記事を公開しています。 はじめに ナウキャストでデータエンジニアをしている翁長と申します。 現在、クレジットカードの売上データに対し店舗・ブランド・企業名・業種名など様々なセグメントで分析するためのマッピングを実現するチームにてお仕事をしてます。 マッピングとは 少しばかりナウキャストのマッピングチームの業務について紹介させてください。私達マッピングチームとしての業務は自動化部分とオペレーターの関わる人力部分の2種類が存在します。 一次処理に該当する自動化部分については、あるタグを作成したキーワードに基づいて自動で付与する処理や、店舗名に対しブランドや業種名などを付与する処理を日次でAirflowを用いて実行するマッピング処理となります。こちらは完全にエンジニア側が構築・運用するシステムです。 もう一部は店舗名をもとに業種やブランドなどを調査し、タグの判定や値、キーワードの策定までを含めて行う人力作業となります。こちらはマッピング調査などで培った知見を元にオペレーターと呼ばれる方々が策定してく業務となっており、この策定されたブランドや業種・キーワードの有効性チェックをアドホックに実行したり、データベースに反映するシステムを開発・運用するのがエンジニア側の任務となっています。 移行の背景 現在マッピングチームの用いているデータではRDSに売上データに対するマッピングの結果を保存・活用しています。また、データ同士の結びつきは莫大で様々な処理を逐一RDSからデータを取得しPythonで加工処理と、RDSとアプリ処理側双方に負担がかかり、正直処理が早いとは言えない環境となってました。 現在はこのデータの処理部分をSnowflakeのマシンパワーに期待し、マッピング処理の高速化を実現している過程となっています。 基盤の紹介 現在のマッピングの基盤は個人情報が含まれていない環境をcentral, 含まれているデータを扱う際はsecure環境と、Snowflakeの環境を別途分けて構築・運用しています。 ナウキャストのマッピングチームが扱うクレジットカードの売上情報には一部個人情報に該当するデータが入っているため、パブリックな環境に出しては行けない情報があります。その情報セキュリティの担保を実現したセキュアな環境を実現するため、SnowflakeにはPrivateLinkを使用した接続を実現しています。 また、3次利用として情報セキュリティを担保しながらEC2やWorkspacesからのSnowflakeへの接続を実現したり、Streamlit in Snowflakeをデータ可視化に利用したりとマッピングチーム内では積極的に活用を進めています。 問題点 今回のチャレンジ要素となっていたDynamic Tableを用いたリアルタイム連携実装の前には、一日一回のRDSフルバックアップを用いたバルク更新でSnowflakeのデータ更新を構築していました。 (日次の更新を実行している基盤については、DSPUに所属するKevinさんが以前に 公開した記事 がございます、ぜひとも御覧ください!) 日次更新でもRDSに比べると見違えるような処理速度の上昇が達成できましたが、前述した通りマッピングの作業結果を保存するリソース部分はAWS環境内のRDSとなっており、すぐにでも作業結果をSnowflake側に反映し、他の作業との整合性の担保やデータの取得を必要とする場面が生まれてきます。そのためにAWSのRDS側にデータの変化が起きた際に、Snowflake側にできるだけ早くデータを同期させるシステムの構築が求められたというのが始まりです。 解決の道筋 リアルタイム連携の実現方法ですが、他のテックブログに偉大な先輩方がAmazon RDS (Amazon Aurora)-> Snowflakeのリアルタイム連携の実現方法を執筆して共有しております。 しかし、大方2021年の記事が最新となっておりSnowflakeのサポートにリアルタイム連携を実現させる何かしらの新しいアーキテクチャやコネクタ等は実装されたか確認したところ、2023年7月頃の返答としては「特に用意がない」とのことでした。 返答を踏まえ、同等のリソースで要件を実現しようと試みたところSnowflake側に新しくDynamic Tableがpublicになったことをウェビナー等で拝聴し、私共がパイプラインの管理で使用しているdbtで構築を実現できることから、チャレンジ要素として基盤に入れ込むこととしました。 AWS DMSについて AWS DMSではデータソースとしてAmazon Aurora MySQLに接続し、バイナリログを用いて変更データキャプチャ(CDC)を取得しています。このCDCデータにはデータ内容の他にDMLの判断に使えるColumnやCDCとしての取り込み時刻など、後述する増分更新に使える情報が付与されており、Snowflake側へ取り込み後は容易にデータを活用することが可能となっていました。 また、接続情報についてはKMS、SSLとその証明書(DMS専用で別途取り込む必要あり)、DB接続用のSecrets Manager、IAM Roleとセキュリティ要件を満たした接続方法を提供してくれています。 接続元のRDSに応じた細かな設定が可能 構築部分ではマッピングチームではネットに上がっているこれまでの事例や費用の概算を鑑み、サーバレスではなくインスタンスでDMSを運用することにしました。 当初インスタンスサイズ的にはd3.smallで問題なく捌けていたのですが、5万件を超えるデータの変更が起きた際にメモリ不足エラーでタスクが停止する事象が発生しだしたため、現在は余裕を持ってd3.largeで運用中しています。 運用時に非常にありがたく感じた点は、Database migration tasksのMapping rulesで柔軟にschemaやdatabaseのcolumnなどの取り込みを柔軟に指定・変更できる点や、他のAWSアカウントのS3へのPUT, DELETEも容易に実現できる点。 躓いたポイントとしては、参照するインスタンスがPrivate IPしか持たない場合、特定のバージョンより特定のリソースに対してPrivatelink経由でしかアクセスできない点などがありました。 また、AWS DMSは結構に停止するという記事をいくつか見かけましたが、初期のリソース不足で発生したエラー以外は一度も完全停止したことがないので安心&拍子抜けというのが現状です。 運用についてはDMSのevent subscriptionも用意があるため、簡単にtaskの状況をSlack等に連携できました。 Taskの状態に変化があるとeventが発火される Snowflake側について AWS DMSを用いてchange data captureのデータをSnowflakeに連携することができたので、このままでも問題なくリアルタイム連携は実現可能です。しかしながら、このCDCのPipeline構築前に作成した日次更新のExternal TableがSnowflake上にすでに存在するので、こちらを利用しバックアップからの増分更新を実現することにしました。 このExternal Tableで日次処理時のリカバリが可能なので、万一AWS DMSのタスクが停止しCDCの更新が滞ったとしても復旧の工数が最小限で済むこともメリットだと考えてます。 Dynamic TableはStream, Taskを一本化できる新しいTableであり、管理が容易になるというメリットがあります。ただ、構築時にいくつかの縛りや考慮点があったため、その点を記載します。 参照先のテーブルとしてExternal Table, Viewは不可 参照するテーブルを再作成した際、一部のデータ量が多いテーブルのみ更新エラーが発生した 予測ではありますがDynamic Tableの仕様でTime Travelを利用している節があるので、参照先のテーブルを永続テーブルとしないとエラーとなるようです。また、dbtで参照するTableを再作成した際に何故かデータ量の多いテーブルのみエラーが頻発したため、中身の洗い替えのみを行いテーブルの再作成を避けることで解決しました。 (Snowflakeのサポート様本当にお世話になりました。) 以下はdbtのpre_hookを用い、先に洗い替えを実行している例になります。 {{ config( materialized="incremental", pre_hook=""" {% if is_incremental() %} TRUNCATE TABLE {{ this }} {% endif %} """, ) }} select ... まとめ public公開当初は参照テーブル起因でエラーが頻発したり、他アカウントへの共有が直には無理だったり、SISで参照不可だったりと色々物足りない感のあったDynamic Tableですが、現状はそれらも解決し非常に簡単に自動更新されるTableを作成できるようになりました。皆様もぜひDynamic Tableを触ってデータ基盤に活用して頂ければと願います。 おわりに ナウキャストでは一緒に働く仲間を募集中です。少しでも興味を持たれた方は採用デックを是非ご覧ください! https://medium.com/media/600e9bbfa21fd003c9763cc58f2217d4/href オルタナティブデータ分析サービスの開発を促進する、データ基盤エンジニア募集! - 株式会社Finatextホールディングス 明日は大木さんによる「 AWS re:Invent 2023に参加してきました 」という記事です。お楽しみに! Amazon AuroraとSnowflakeをDynamic Tableを用いてリアルタイム連携したお話 was originally published in The Finatext Tech Blog on Medium, where people are continuing the conversation by highlighting and responding to this story.
アバター