Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

関西Ruby会議08に「『1ヶ月でWebサービスを作る会』で出会った rails new、 そして今に至る rails new」というタイトルで登壇します

こんにちは、Classi でソフトウェアエンジニアをやっている id:kiryuanzu です。

2025年6月28日(土) に京都府京都市の先斗町歌舞練場にて開催される関西Ruby会議08で「『1ヶ月でWebサービスを作る会』で出会った rails new、そして今に至る rails new」というタイトルで登壇させていただくことになりました。

regional.rubykaigi.org

今回はこの発表のプロポーザルを提出した時の背景や、登壇の意気込みについて事前にお伝えしたいと思います。

プロポーザルを提出した際の背景

実は、LT形式ではない少し長めの発表(今回は20分)のプロポーザルを提出したのはこの関西Ruby会議08が初めてでした。
プロポーザルを出すことを本格的に意識した際、まずは関西Ruby会議08の運営の方が書かれた「関西Ruby会議の再開に寄せて」という記事を熟読し、運営の方がどのようなトークを求めているかを自分なりに考えてみました。

note.com

かなり抽象的なテーマですが、Rubyのコードが1行でも発表に含まれているのであれば問題ありません。むしろその人にしか話せない作ったものの「好き」や「苦労」や「思考」などのストーリーを聞きたいと思っています。

「その人にしか話せない作ったものの『好き』や『苦労』」というキーワードを目にした時、自分にとってのそれは学生時代から趣味としていた個人でのWebサービス開発がまず浮かびました。

その上で「聞く人も何か作ろうと思えるような話」も意識しつつ、趣味のPodcastの音声データを自前で配信する個人Webサービスを開発した際の体験談をテーマにプロポーザルを提出するに至りました。
発表の概要は以下のリンクを参照ください。 https://regional.rubykaigi.org/kansai08/presentations/kiryuanzu

発表の意気込み

発表内容は個人開発の体験談がメインとなります。その上で「どうしたら自分の個人サービスをローンチまでやりきれるのか」についても深掘りをする予定です。
この発表を通して、サービスを作っていてローンチを目指している方や、直近の自分のように久々に個人開発したいけれど作る勢いがまだつかない方が「自分もやってみよう」と思えるようなお話ができたらと思います。

先日アップされたタイムテーブル解説記事でも自分が伝えたい発表のイメージを共有してくださっています。こちらも参考にしていただけると幸いです。

関西Ruby会議08 チーフオーガナイザーの ydah さんにタイムテーブルを解説してもらいました - ANDPAD Tech Blog

tech.andpad.co.jp

ydah: そうなんですよね。 まさに「どうしたら個人サービスをローンチまでやりきれるか」についての話を自らの経験をもとに話していただけると思っていて、それは本当に貴重な体験談ですし、個人開発で悩んでる人にとっても、すごく勇気づけられる内容になりそうだなと思っています。 「rails newはするけどローンチまでいかない」って、とても共感する人多いと思うんですよね。これから個人開発を始めたい方にとって非常に参考になるトークだと思います。

筆者以外のトークもたいへん丁寧に解説されています。現地に参加される方は必見の記事です!

おわりに

当日は大変ありがたいことに Classi のメンバーが現地で応援しにきてくださるとのことです!

id:kiryuanzu含む一部のメンバーは Day0 の晩餐会当日の Official Party にも参加予定です。現地でぜひたくさん交流しましょう!

それでは、当日みなさんにお会いできることを楽しみにしております!!

Classiエンジニアの「OSSやっていきの集い」 〜4ヶ月間の取り組みと、初めてのOSSコントリビューション〜

こんにちは。Classiでソフトウェアエンジニアをしている中村( id:kozy4324 )です。

今回は、社内で継続している技術活動「OSSやっていきの集い」についてご紹介します。活動開始から4ヶ月の間に取り組んできたこと、そして実際にOSSプロジェクトへプルリクエストを送ってマージされた経験についてまとめました。

OSSやっていきの集いとは

この活動は、SmartHRさんの取り組み「OSSやっていきの集い」から着想を得て、名称もそのままお借りして始めました。

OSSに関心はあるものの、実際に関わるのはハードルが高く感じられることがあります。私自身もそうでした。 一人では難しく感じることでも、仲間と取り組めば少しずつ前に進めるのではないか。そんな思いからスタートした活動です。

社内でこの活動を始める際には参加するメリットを明確にすることも意識しました。 活動を通じて期待できるアウトカムは以下のようなものです。

組織にとってのメリット

  • OSSに貢献するエンジニアが増え、企業としての発信力も高まる
  • 技術広報の題材として活用できる
  • 開発スキルの向上が期待できる
  • チームを越えた交流が生まれる

個人にとってのメリット

  • OSSに関わる心理的ハードルを下げられる
  • 実践を通じた学びが得られる
  • 社外発信や登壇の機会にもつながる

また、東京Ruby会議12前夜祭での発表 「Rubyと暮らし、OSSに貢献し、登壇する ── 半年間続けた『OSSやっていきの集い』の成果と学び」 にも背中を押されました。実践例を知ることで自分たちの活動がどう発展しうるか、イメージを持てるようになりました。

立ち上げから4ヶ月で取り組んだこと

活動の最初のテーマとして、Rubyのコード整形・静的解析ツールであるRuboCopを選びました。 まずは「RuboCopを知る・使う」ことからスタートし、段階的に理解を深めていきました。

RuboCopの活用に向けた取り組み

  • 基礎固め
    2月4週目〜3月1週目(約2週間)
    RuboCopのGitHubリポジトリや公式ドキュメントを読み、基本的な使い方を習得しました。

  • Copの実装を読む
    3月2週目〜5月2週目(約2ヶ月)
    Style/RedundantConditionなど具体的なCopのコードを読み解きながら、抽象構文木(AST)の概念を学習しました。

  • カスタムCopの作成
    5月3週目〜6月3週目現在(約1ヶ月経過)
    社内プロジェクトで使えそうなカスタムCopの作成にチャレンジしています。例えば社内ライブラリに対して不適切な使い方を検知するCopなど、アイデアレベルから持ち寄り、それをどう実装できるかを参加者で会話しながら取り組んでいます。

  • リリース情報の追跡と適用
    2月4週目〜6月3週目現在(約4ヶ月経過)
    RuboCopのリリースノートを定期的に確認し、社内プロジェクトでのバージョンアップにも取り組んでいます。 エラーが発生した際は「これは貢献のチャンスかもしれない」と前向きに捉える意識も芽生えはじめました。

RuboCop 1.76.0で気づいたエラーメッセージの違和感

ある日、RuboCopを1.76.0にアップデートしたところ次のような指摘を受けるようになりました。

C: Style/ItBlockParameter: Avoid using numbered parameters for multi-line blocks.
            some_ids = some_ids.map do ...
                       ^^^^^^^^^^^^^^^

実際のコードは以下のようになっていました。

some_ids = some_ids.map do
  do_something(with: it)
end

調査したところ、Style/ItBlockParameterEnforcedStyleのデフォルト値がv1.76.0で変更されたことが原因でした。

https://github.com/rubocop/rubocop/releases/tag/v1.76.0

  • #14066: Add EnforcedStyle: allow_single_line as the default to Style/ItBlockParameter. (@koic)

v1.75.xまではonly_numbered_parametersがデフォルトでしたが、v1.76.0からはallow_single_lineが新たに追加され、こちらがデフォルトとなっています。その結果、設定を明示していないプロジェクトでは複数行ブロック内でのitの使用が許容されなくなりました。

このようにエラーの理由は理解できたのですが、表示されるメッセージには少し違和感がありました。

Avoid using numbered parameters for multi-line blocks.

今回のコードでは「numbered parameter(_1, _2など)」は使っておらず、itを使っているにもかかわらずその利用を避けるようにというメッセージに読めてしまいます。

コードとエラーメッセージを見比べても、なぜ警告されているのかが直感的に理解しづらい状況でした。

RuboCopへの修正プルリクエストとマージ

このメッセージの違和感を解消するためエラー文言の修正に特化したプルリクエストを作成しました。

そしてありがたいことに当日中にマージしていただくことができました。これは「OSSやっていきの集い」を始めた当初から目指していた「実際にOSSに貢献できる経験を得る」という目標の達成でもあります。

日々の開発のなかでOSSを継続的に利用していたからこそ、違和感に気づき、改善のきっかけを掴むことができました。 ゼロからのスタートでも数ヶ月で貢献のチャンスを得られたことは非常に嬉しい成果です。

これからの展望

「OSSやっていきの集い」はまだ始まったばかりの取り組みです。今後もRuboCopの内部実装を読み解きながら、カスタムCopの開発や小さな改善の提案を通じて、OSSとの関わりを深めていきたいと考えています。

この活動を通じて、ClassiのエンジニアがOSSと自然に関わり、学びを得て、最終的にはOSSコミュニティに貢献できるような文化を育てていければと思います。

Classi 2025年新卒エンジニア研修「そーだい塾」を開催しました

 こんにちは。Classiのみんなの頼れるお兄さん id:Soudai です。 Classiでは例年、新卒エンジニア向けの研修「そーだい塾」を実施しており、今年も2時間の研修を3回にわたって開催しました。

 今日はこの研修をご紹介しながら、当日の雰囲気やスライドには書いていない部分や大切な考え方をご紹介します。

  1. 第1回:リリースの極意
  2. 第2回:日々の仕事の中で成長する
  3. 第3回:具体と抽象と現場

第1回:リリースの極意

 第1回では、ソフトウェアエンジニアにとって最も重要な「リリース」について取り上げました。

登壇資料

speakerdeck.com

speakerdeck.com

伝えたかった狙い

 まずはエンジニアたるもの、リリースから始まるということでリリースをテーマにしました。 このセッションは例年やっています。

tech.classi.jp

 当日は新卒だけでなく、 id:lacolaco をはじめとしたClassiのエンジニアも参加して、リリースに対する考え方やリリースの重要性の議論も行いました。 2~3年も経てば、データベースの変更が必要な機能のリリースも必要になりますし、数ヶ月もかかるような大きなコードベースのプロジェクト単位のリリースも行うようになります。そういう時にこの話を思い出してほしいのです。そうすれば事前に事故を防ぐことができます。そうやって経験していく中で「これは、そーだいさんが言ってたやつだ」となってほしいなと思っています。

第2回:日々の仕事の中で成長する

 第2回では、エンジニアとしての成長と仕事のコツを紹介しました。 毎日8時間働いていくのですから、その中で成長し、仕事で成果を出し、評価を得ることが重要です。 そのために必要なスキルや考え方を伝えました。

登壇資料

speakerdeck.com

speakerdeck.com

伝えたかった狙い

 この回の資料は、まず知っていることがとても重要な内容を中心に構成しました。 早く知れば知るほど複利が効く内容で、出来なくてもまずは知っておいて出来るようになるまで繰り返すことが大切です。

 意識していればできるようになるスキルですし、逆に言えばこれができない場合はどこかで社会人として躓くことになるでしょう。 だからこそ早めに知識として知っておき、身につけてほしいということで紹介しました。

第3回:具体と抽象と現場

 最終回では、エンジニアにとって重要な思考法である「抽象化と具体化」について深く掘り下げ、それを具体的にどのように実践するかを紹介しました。

登壇資料

speakerdeck.com

speakerdeck.com

伝えたかった狙い

 この話は去年はしておらず、完全な新作です。 抽象化と具体化は、ソフトウェアエンジニアにとって非常に重要なスキルであり、これを身につけることでより良いソフトウェアを作ることができます。 無意識に行っていることも多いですが、意識的に行うことでより効果的に活用でき、再現性が上がります。

 抽象化と具体化の往復を仕事で行っていくうえで、現在の自分の抽象度がどの位置なのか把握することから始めましょう。ということを伝えました。

 そして最後に実際に仕事をするうえで、抽象化と具体化をどのように活用していくかを紹介しました。 顧客が求めているものを理解し、言われたことを作るのではなく、必要なものを作るためにも抽象化と具体化の往復が重要です。

終わりに

 Classiでは2021年から4年間にわたって新卒エンジニア向けの研修「そーだい塾」を実施しています。 もちろん新卒のメンバーがこの研修を受けることによって、すぐに仕事ができるようになるわけではありません。 ですが、2年後、3年後に振り返ったときに「そーだい塾で学んだことが役に立った」と思ってもらえるような内容を提供しているつもりです。

 そして実際に今の2年目、3年目のメンバーからは「改めて今年の新卒研修を眺めていると、そーだい塾で学んだことの解像度が上がっている」と言ってもらえています。 こういう若者の成長を感じられることも、そーだい塾をやっていてよかったなと思う瞬間ですね。

 今年の新卒のメンバーが実際の業務を通じて成長し、数年後に「そーだいさんの言ってたこと、今ならわかる!」と言ってもらえる日が来ることを楽しみにしています。


参考記事

NPS70超。社内AI議事録ツール「Turing」がClassiの"当たり前"になるまで

こんにちは、エンジニアの鳥山です。

Classiでは、2024年の12月ごろから当時最新であったGemini 1.5 Proを用いた議事録生成ツール「Turing」を開発、リリースし、社内のメンバーに使ってもらっています。

tech.classi.jp

本記事では、

  • リリースからおよそ半年経った今、Turingはどの程度普及したのか
  • 利便性や運用性の向上のためにどんな継続的改善をしたのか

の大きく2点に焦点を当てて紹介し、最後にTuringの成功要因についても考察します。

モデルのアップデートや新技術の発表が毎週のように起きる昨今、個人的にはAIをプロダクトに取り入れることのみならず、社内ツールとして使うことにも大きな可能性を感じています。

本記事は単体でもお読み頂けますが、Turingのアーキテクチャなどより詳細な部分にご興味をお持ちの方は、ぜひ前述のブログ記事も併せてお楽しみください。

  • Turingはどの程度普及したのか
  • 利便性や運用性の向上のための工夫
    • Slack任意チャンネルへの投稿
    • インスタンス起動時刻の最適化
  • 成功の要因、そして今後の展望
続きを読む

ライブラリの継続的アップグレードを支援する活動にQAメンバーとして参加した1年のふりかえり

こんにちは。プロダクト本部プラットフォーム部QAチームの牛木です。

弊社にはライブラリの継続的アップグレードを支援する活動があります。この活動にQAメンバーとして1年間参加しました。活動の内容や、活動を通じて感じたことをお伝えします。

ライブラリの継続的アップグレードを支援する活動とは

この活動は、EOL*1を迎えたライブラリやフレームワークを使わないように、” 継続的 ” にバージョンを ” アップグレードしよう ” という取り組みです。サポート状況の最新情報や、現時点での対応状況などを収集し見える化することにより、対応が遅れているチームに対しリマインドやバージョンアップのサポートなどを行う活動になります。

なぜこのような活動を行っているのでしょうか?

EOLを迎えたライブラリやフレームワークは、セキュリティパッチが提供されなくなるため、脆弱性が発見されても修正されることはありません。もし脆弱性が放置された場合、自社で対応する必要が生じ、対応が遅れるとプロダクト全体がセキュリティインシデントにつながるリスクが高まります。

Webサービスを提供する弊社にとって、セキュリティリスクへの対策は常に重要な課題であり、この活動はその一環として行われています。

QAメンバーとしてなぜ活動に参加することになったのか

QAチームはプロダクト品質の担保を重要な責務と捉えています。プロダクト品質は、機能要件のみならず、セキュリティ品質を含む非機能要件も含まれます。プロダクトの安全性を確保するセキュリティ品質は、プロダクト品質を語る上で不可欠です。QAチームとしても積極的に関与し、貢献したいと考えていました。

また、日々の開発におけるライブラリのアップグレードはプログラム変更の一環であり、本来QAプロセスを経てリリースされるべきです。多くのOSSに支えられている弊社のプロダクトにとって、常に最新の状態を維持するために、継続的なライブラリのアップグレードは避けられません。

QAチームは、このライブラリのアップグレードという一連のプロセスを、開発からリリース、そしてその後の安定稼働まで一貫して見据え、最適化していくことを重要な役割と捉え、上流工程から品質保証に積極的に関与する「シフトレフト」を実践しています。この活動にQAメンバーとして参加することで、要件定義や設計段階からセキュリティ品質の観点を取り入れることが期待できます。それは、セキュリティリスクを低減し、プロダクト全体の品質向上に貢献できる良い機会になると考えました。

1年間何をしたか

週1回の定例MTGを実施し、以下の内容を実施しました。 参加メンバーは、開発エンジニアと私です。

① 使っているライブラリやフレームワークのリリース情報をチェック

新しいバージョンがリリースされた際、リリース内容からSecurity Fixが含まれていないか確認します。新しいバージョンがリリースされたかどうかの確認は、endoflife.dateをよく利用しています。Security Fixが含まれていないかの確認は、CVE*2に関連する改修情報を重点的に確認します。

② Dependabot アラート*3をチェック

新規にCritical・High・Mediumのアラートが発生していないか確認します。 Dependabotアラートは、GitHub のSecurityから閲覧できます。

(Criticalアラートは即潰すをいうルールが定着し、現在ではCriticalアラートがありません!)

③ 自社の開発に影響を与える可能性があるかを判断(トリアージ)

上記①、②で確認されたSecurity FixやDependabotアラートが、自社の開発に影響を与える可能性があるか判断を行います。この判断は私では困難であり、開発エンジニアが判断します。緊急度が高いと判断された場合は、速やかに該当開発チームに連絡します。緊急度がそれほど高くないと判断された場合は、週次で開催される各チームのエンジニアがテックイシューを共有しあう定例のMTGで情報共有するに留めています。

①②どちらも毎週何かしら検知されるため、原則として全てのSecurity FixとDependabotアラートを週次の定例MTGで共有しています。なぜなら「影響しない」「対応不要」という判断はリスクを伴うからです。本当に影響がないのか、対応しなくて良いのかという根拠は、実際にはソースコードの調査が必要ですし、現時点では影響がなくても、将来的なコード修正によって影響が出る可能性もあります。したがって、この活動を通して上がってきたリスクを共有し、アップグレードの推奨は行いますが、最終的な影響確認と対応判断は開発チームに委ねています。つまり、ここでのトリアージは、①②の内容が実際に影響するかどうかを判断するよりも、対応を促進させるための追加情報を提供する場なのです。

④ 月末にEOLのバージョンを使っていないかチェック

リポジトリとバージョンのマトリックス表を使って可視化をしています。EOLを迎えているバージョンを使用しているリポジトリや、間もなくEOLを迎えるバージョンを使用しているリポジトリを塗りつぶして、月末の定例MTGで共有し、関係者へ啓蒙を行います。

①②④のチェックは私が担当しました。 ③の判断や、Security FixやDependabotアラートの読み解きは、開発エンジニアの知見やGeminiのDeep Research*4を活用して実施しています。

この4つの取り組みの中で最も優先度が高いのは「④月末にEOLのバージョンを使っていないかチェック」です。なぜならEOLを迎えたバージョンはSecurity Fixが行われないため、仮に脆弱性が混入していても攻撃されるまで被害に気づけないからです。また、攻撃された場合、すぐさまアップグレードや自社での脆弱性修正は困難です。

その上で、日々のSecurity FixやDependabotアラートへの対応(①、②)を定期的に促すことで、④のようなリスクの高い状況に陥るのを事前に防ぐことができます。これらの日々の対応がスムーズに進む開発チームは、ライブラリアップデートのサイクルが定着し、結果としてEOLを迎えたライブラリの使用を避ける仕組みが自然と構築されます。

この活動の重要な視点

この活動は、全てのライブラリやフレームワークを常に最新バージョンにすることや、Dependabotアラートを全て解消することが目的ではありません。

この活動の重要な視点は、「セキュリティリスク」と「事業リスク」を比較し、相対的な重要性を評価した上で、最適なコストバランスの下でセキュリティリスクを低減させることです。

先に述べた通り、EOLを迎えたライブラリやフレームワークは、セキュリティパッチが提供されなくなるため、もし脆弱性が放置された場合、自社で対応する必要があります。セキュリティリスクが顕在化した場合、開発を全面的に停止して脆弱性対応に追われることになるかもしれません。重要なリリースが控えている時期であれば、顧客に影響が出ますし、最悪の場合サービスの一時停止という判断を迫られる可能性があります。これは、「現時点では発生していないものの、発生すれば事業継続に重大な悪影響を及ぼす事象」です。

このような事態を未然に防ぐため、本活動は管理ではなく、あくまで情報提供と啓蒙活動に重点を置いています。

しかし、可能な限り迅速な対応を推奨しているのも事実です。Security Fixの多くはパッチバージョンとしてリリースされるため、影響範囲が比較的小さいと判断できます。影響範囲の小さい修正をこまめに適用することで、影響範囲を詳細に調査する手間を省くことができます。修正を長期間にわたって累積させると、バージョン間の差分が大きくなり、いざ適用する際に予期せぬ不具合が発生し、プロダクトを壊してしまうリスクが高まります。

QAメンバーとして参加して気づいたQA活動との接点

この活動を通じて、QAチームのテスト施策がセキュリティ品質の防止策としても有効的であることを見出すことができました。

QAチームが整備しているE2E自動テストは、ライブラリやフレームワークのアップグレード対応における迅速な対応を促進する上で重要な役割を果たしています。パッチバージョンの適用に伴う主なリスクは、意図しない変更によるプロダクトの予期せぬ動作です。QAチームでは、基本的な機能フローを網羅した自動テストを準備しており、バージョンアップに伴うリリースの際、この自動テストによるチェックのみで品質を担保できる体制を整えています。

詳しくは以前に書かれた記事をご覧ください。 tech.classi.jp

また、標準観点表やテストテンプレートは、XSSやSQLインジェクションといった脆弱性を検知するのに最適なテストパターンとなっています。 認可に関するテストでは、権限マトリクスを活用したテストテンプレートが定着しており、自動テストにも権限パターンやURL直接入力といった観点が、特定機能だけではなく全機能に対して標準整備され、定期的に自動実行されています。

QAチームが日頃行っている品質保証活動は、機能要件の確認にとどまらず、非機能要件であるセキュリティ品質の確保にも貢献していることを強く実感できました。

QAメンバーとして参加して直面した課題

しかしながら、現状の活動からさらに一歩踏み出し、活動を拡張していくことには、正直なところ難しさを感じています。

第一に、ライブラリやフレームワークに関する深い知識に加え、Webアプリケーションの脆弱性に関する専門的な知識を習得した上で、それが自社プロダクトにどのような影響を与えるかを「判断」するには、現状の活動だけでは不十分でした。この1年は知識や仕組みの理解に留まっていましたが、今後は攻撃の再現手順を理解した上で、実際のコードの状態を確認し、攻撃が成り立つかを確認することが必要だと感じます。実践を行うことで「判断」に繋げることができると考えています。

第二に、この活動が「管理」ではなく「啓蒙と推奨」を重視する以上、その啓蒙を裏付ける専門的な見解や、実施が遅れがちな開発チームへの働きかけは、技術負債の解消という大きな課題と密接に関連しており、QAチームとして直接的な成果を出すことは容易ではありません。例えば、EOLを迎えたライブラリのアップグレード対応は、開発チームが本来進めたい施策を一時中断させ、影響調査やプロダクトのコード修正に多くの工数を割く必要があります。こうした作業は、プロダクト全体の安定性にも関わるため、QAチームだけで推進するのは現実的ではありません。

以下は過去に実施したAngularアップデート対応に関する記事です。こちらにはアップグレード対応時に実際に遭遇した課題が記載されています。 tech.classi.jp

この活動の目指す場所

もし、利用中のライブラリやフレームワークに重大な脆弱性が報告された際、この活動が提供できる価値は2つあると考えています。

① 迅速な現状把握

この活動を通じて全体像を継続的に可視化・共有しているため、どのリポジトリで該当のライブラリやフレームワークが使われているか、また脆弱性のあるバージョンが利用されているかといった情報を即座に特定できます。これにより、緊急時の調査時間を大幅に短縮することが可能です。

② スムーズな対応体制の確立

脆弱性のあるバージョンが判明し、修正適用済みの新バージョンへアップグレードが必要となった場合でも、日々の開発を通してライブラリアップデートのサイクルが定着している開発チームであれば、すぐに作業に取りかかれます。

この活動が最終的に目指すのは、この活動がなくとも上記①、②の対応が開発チームで自律的に行える状態です。 そうなれば、この活動を切り出す必要はなく、適切なアップグレードが当たり前である状態と言えると思います。その状態こそがこの活動のゴールだと認識しています。

おわりに

社内では、これらの将来的なセキュリティリスクへの対策を「セキュリティコストの前払い」と呼んでいます。後々大きな負債として積み上がるだけでなく、実際に問題が発生した際の対応コストは計り知れません。日々の開発の中でどれだけ前払いできるかが重要となります。

この「前払い」をいかに促進できるかが、この活動の次の段階における重要なステップだと考えています。現在利用しているプラットフォームの機能を最大限に活用し、より自動的かつセキュアに開発できるような仕組みづくりが、今後取り組むべき内容だと感じています。

例えば、Content Security Policy*5を活用し脆弱性混入時にすぐ検知できる仕組みや、ライブラリのアップデートを最適化する仕組みなどです。詳しくは以前に書かれた記事をご覧ください。 tech.classi.jp tech.classi.jp

この活動に参加したことで、QAチームの施策がセキュリティ品質に貢献していることに気づけたことは嬉しい収穫でした。一方で、この活動を次の段階へ発展させるにあたり、QAチームの枠組みでどこまで実現できるかはまだ明確ではありません。自身のQA活動にこの経験をどう活かすべきか、模索段階であり、自身の視座の低さを痛感しています。 今後は、QAメンバーとして活動に参加する意義を明確にし、開発チームへの働きかけを強化することで、セキュリティ品質の向上に貢献できるQAチームを目指します。

*1:End of Lifeの略で、セキュリティの更新やバージョンアップの停止をはじめ、サポートの提供が終了することを意味します。

*2:Common Vulnerabilities and Exposuresの略で、公開されているソフトウェアの脆弱性やセキュリティリスクを識別するための共通の識別番号です。 CVE - CVE

*3:GitHubが提供する機能の一つで、プロジェクトが使用している依存関係(ライブラリやパッケージなど)に既知のセキュリティ上の脆弱性が発見された際に、開発者に通知するものです。 Dependabot アラートについて - GitHub Docs

*4:GoogleのAIモデルであるGeminiを活用した、詳細かつ包括的なリサーチレポートを自動的に生成する機能です。Gemini Deep Research — your personal research assistant

*5:ウェブサイトに組み込まれたセキュリティメカニズムであり、主にXSSやデータインジェクション攻撃などの、悪意のあるコンテンツの挿入によって引き起こされる攻撃を軽減するための機能です。コンテンツセキュリティポリシー (CSP) - HTTP | MDN

Pull Request ごとに S3 + CloudFront へ SPA のプレビュー環境をデプロイする

Classi でソフトウェアエンジニアをやっている koki です。
S3 + CloudFront でホスティングしている SPA (Single Page Application) で Pull Request ごとにプレビュー環境をデプロイする仕組みを作ってみたところ、かなり体験が良かったので紹介します。

前提

Classi で提供している学習トレーニング機能には、それを裏で支えるコンテンツ管理システム ( 以下、内部 CMS ) が存在しています。
この内部 CMS については以下の記事でも簡単に紹介されているので、こちらをご参照ください。

tech.classi.jp

内部 CMS のフロントエンドは Angular を使用した SPA になっており、 Amazon S3Amazon Cloudfront を使用してホスティングされています。

抱えていた課題 / つくったもの

内部 CMS のフロントエンド開発をしている中で、他メンバーが作成した Pull Request をレビューする際、動作確認するためには「ローカルに branch を pull して」「ローカルで起動する」必要があり、レビューするまでの手間がかかるという課題がありました。
必要に応じてスクリーンショットや画面録画を Pull Request に貼るなどの運用もしていましたが、それでも限界があります。

「Pull Request ごとにサクッとプレビューできる環境が欲しいよね〜」ということで、仕組みを作りました。

Pull Request 作成時のプレビュー環境がデプロイされるイメージ

この記事では Pull Request ごとに SPA のプレビュー環境を S3 + CloudFront へデプロイする仕組みや構成などについて解説していきます。

インフラ構成について

全体の構成としては以下のようなイメージで、一般的な S3 + CloudFront での静的ホスティング構成に CloudFront Function を加えただけの非常にシンプルな構成です。

この記事では、 https://pr-<PR番号>.classi.example のような URL から各 Pull Request ごとのプレビュー環境にアクセスできる状態を目指します。

全体構成図

💡 Route53 はあくまで DNS なので実際にリクエストが図のように Route53 を経由してそのまま CloudFront Distribution に送られるわけではありませんが、わかりやすくするために簡略化してます。

ひとつずつ順を追って説明していきます。
なお、今回は Terraform を用いてリソースを構築していきます。

S3 バケットを作成する

まずは S3 バケットを作成します。
この S3 バケットは SPA のビルド済み静的ファイルを配置するために使用します。

resource "aws_s3_bucket" "example" {
  bucket = "<任意のバケット名>"
}

# デフォルトの暗号化の設定 (SSE-S3)
resource "aws_s3_bucket_server_side_encryption_configuration" "example" {
  bucket = aws_s3_bucket.example.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

# パブリックアクセスを全てブロック
resource "aws_s3_bucket_public_access_block" "example" {
  bucket = aws_s3_bucket.example.id

  ignore_public_acls      = true
  restrict_public_buckets = true
  block_public_acls       = true
  block_public_policy     = true
}

この段階では特に変わった設定はしていません。

CloudFront Distribution を作成する

上述した S3 バケットをオリジンとする CloudFront Distribution を作成します。
また、 CloudFront Distribution 経由で S3 バケットにアクセスできるように OAC の作成および S3 バケットポリシーの設定もしています。

data "aws_cloudfront_origin_request_policy" "cors_s3_origin" {
  name = "Managed-CORS-S3Origin"
}

data "aws_cloudfront_cache_policy" "caching_disabled" {
  name = "Managed-CachingDisabled"
}

resource "aws_cloudfront_distribution" "example" {
  enabled = true

  # S3 バケットをオリジンに設定
  origin {
    origin_id                = aws_s3_bucket.example.id
    domain_name              = aws_s3_bucket.example.bucket_regional_domain_name
    origin_access_control_id = aws_cloudfront_origin_access_control.example.id
  }

  # ここでは一旦デフォルトのドメインを使用しておく
  viewer_certificate {
    cloudfront_default_certificate = true
  }

  default_cache_behavior {
    target_origin_id       = aws_s3_bucket.example.id
    viewer_protocol_policy = "redirect-to-https"
    cached_methods         = ["GET", "HEAD"]
    allowed_methods        = ["GET", "HEAD"]
    compress               = true # gzip 圧縮を有効化

    origin_request_policy_id = data.aws_cloudfront_origin_request_policy.cors_s3_origin.id

    # キャッシュポリシーは要件に応じて調整してください
    # この例ではキャッシュを無効にしています
    cache_policy_id = data.aws_cloudfront_cache_policy.caching_disabled.id
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }
}

resource "aws_cloudfront_origin_access_control" "example" {
  name                              = "<任意のOAC名>"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}
# S3 バケットポリシー
resource "aws_s3_bucket_policy" "example" {
  bucket = aws_s3_bucket.example.id
  policy = data.aws_iam_policy_document.allow_cloudfront_to_access_s3.json
}

# CloudFront Distribution 経由でのみ S3 バケットにアクセスできるようにする設定
data "aws_iam_policy_document" "allow_cloudfront_to_access_s3" {
  statement {
    sid       = "AllowCloudFrontToAccessS3"
    actions   = ["s3:GetObject"]
    resources = ["${aws_s3_bucket.example.arn}/*"]

    principals {
      type        = "Service"
      identifiers = ["cloudfront.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "aws:SourceArn"
      values   = [aws_cloudfront_distribution.example.arn]
    }
  }
}

ドメインの設定や CloudFront Function の関連付けも行いますが、その辺りは後述します。

Route53 Hosted Zone とレコード / ACM 証明書を作成する

プレビュー環境に使用するドメイン名で Route53 Hosted Zone を作成します。
次のコードは preview.classi.example というドメイン名で作成する例です。

resource "aws_route53_zone" "example" {
  name = "preview.classi.example"
}

また、この例では Apex ドメインの Hosted Zone ( 今回の例では classi.example ) に NS レコードを作成して権限移譲を行う必要があります。
詳細な手順については以下の記事がわかりやすいので、こちらをご参照ください。

dev.classmethod.jp

💡 Apex ドメインの Hosted Zone をそのまま利用しても動作上は問題ありませんが、環境分離の観点からプレビュー環境用の Hosted Zone は分けて作成する方が管理上は望ましいでしょう。


作成した Route53 Hosted Zone 内に *.preview.classi.example という名前で A レコードを作成し、 CloudFront Distribution に関連付けます。

resource "aws_route53_record" "example" {
  # プレビュー環境用の Hosted Zone
  zone_id = aws_route53_zone.example.zone_id

  name = "*.preview.classi.example"
  type = "A"

  # 先ほど作成した CloudFront Distribution を関連付ける
  alias {
    name                   = aws_cloudfront_distribution.example.domain_name
    zone_id                = aws_cloudfront_distribution.example.hosted_zone_id
    evaluate_target_health = false
  }
}

ワイルドカード ( * ) を使用することで任意のサブドメインをマッチさせることができます。
Route53 における Hosted Zone およびレコードでのワイルドカードの使用について詳しくは以下の公式ドキュメントをご参照ください。

docs.aws.amazon.com


続いて、 *.preview.classi.example の ACM 証明書を作成します。
なお、この ACM 証明書は CloudFront Distribution に関連付けして使用するため、 us-east-1 リージョンに作成する必要があることに注意してください。 *1

provider "aws" {
  alias  = "virginia"
  region = "us-east-1"
}

resource "aws_acm_certificate" "example" {
  # us-east-1 リージョンに作成する
  provider                  = aws.virginia
  domain_name               = "*.preview.classi.example"
  subject_alternative_names = ["preview.classi.example"]
  validation_method         = "DNS"

  lifecycle {
    create_before_destroy = true
  }
}

# DNS 検証用の Route53 レコード
resource "aws_route53_record" "example_certificate_validation" {
  for_each = {
    for options in aws_acm_certificate.example.domain_validation_options : options.domain_name => {
      name  = options.resource_record_name
      type  = options.resource_record_type
      value = options.resource_record_value
    }
  }

  zone_id = aws_route53_zone.example.zone_id
  name    = each.value.name
  type    = each.value.type
  records = [each.value.value]
  ttl     = 3600
}

先ほど作成した CloudFront Distribution にカスタムドメインと ACM 証明書を関連付けます。

 resource "aws_cloudfront_distribution" "example" {
   # ...

+  aliases = ["*.preview.classi.example"]

   viewer_certificate {
-    cloudfront_default_certificate = true
+    acm_certificate_arn      = aws_acm_certificate.example.arn
+    ssl_support_method       = "sni-only"
+    minimum_protocol_version = "TLSv1.2_2021"
   }
 }

これにより、例えば https://hoge.preview.classi.example のような URL で CloudFront Distribution にアクセスできるようになります。

CloudFront Function を作成する

リクエストされたドメインに応じて配信するファイルを切り替える CloudFront Function を作成します。

任意のファイル名で次の JavaScript プログラムを作成します。
処理内容の詳細についてはコメントをご参照ください。

// example.js
function handler(event) {
  const request = event.request;
  const host = request.headers.host.value;

  // ページへのアクセスに対しては `index.html` を返す
  // (例: `/`, `/foo`, `/foo/bar` など)
  // 拡張子が含まれるファイルへのアクセスに対しては、そのまま返す
  // (例: `/foo.js`, `/foo/bar.css` など)
  if (!request.uri.includes(".")) {
    request.uri = "/index.html";
  }

  // ドメインが `pr-<PR番号>.preview.classi.example` にマッチしない場合は 403 を返す
  const matches = host.match(/^pr-(\d+)\.preview\.classi\.example$/);
  if (!matches) {
    return { statusCode: 403, statusDescription: "forbidden" };
  }

  // `s3://<S3バケット>/pull-requests/<PR番号>/<ファイルパス>` を配信する
  const pullRequestNumber = matches[1];
  request.uri = `/pull-requests/${pullRequestNumber}${request.uri}`;

  return request;
}

この JavaScript プログラムを使用する CloudFront Function を作成し、 CloudFront Distribution に関連付けます。

resource "aws_cloudfront_function" "example" {
  name    = "<任意の関数名>"
  runtime = "cloudfront-js-2.0"
  code    = file("${path.module}/path/to/example.js")
}
 resource "aws_cloudfront_distribution" "example" {
   # ...

   default_cache_behavior {
     # ...

+    function_association {
+      event_type   = "viewer-request"
+      function_arn = aws_cloudfront_function.example.arn
+    }
   }
 }

これにより、リクエストされたドメイン名やパスに応じて対応するファイルが配信されるようになります。

  • https://pr-111.preview.classi.examples3://<S3_BUCKET>/pull-requests/111/index.html を返す
  • https://pr-111.preview.classi.example/sub/pages3://<S3_BUCKET>/pull-requests/111/index.html を返す
  • https://pr-222.preview.classi.example/hoge/fuga.pngs3://<S3_BUCKET>/pull-requests/222/hoge/fuga.png を返す

https://pr-111.preview.classi.example にアクセスする例

https://pr-222.preview.classi.example にアクセスする例


これでプレビュー環境をホスティングする仕組みの構築は完了です。
あとは Pull Request ごとに S3 バケット内に静的ファイルをアップロードすればいいだけです。

デプロイフロー

以下は Pull Request が作成 / 更新されるたびにプレビュー環境をデプロイする GitHub Actions ワークフローの例です。
処理内容はただ SPA をビルドして静的ファイルを S3 バケットの /pull-requests/<PR番号>/ 配下にアップロードするだけです。

name: Deploy Preview

on:
  pull_request:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      id-token: write # OIDC による認証を有効にするために必要
    steps:
      - uses: actions/checkout@v4

      # アプリケーションのセットアップ ~ ビルドを行う
      # ...(省略)

      # プレビュー環境用の S3 バケットにファイルをアップロードする
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: <IAMロールのARN>
          aws-region: ap-northeast-1
      - run: aws s3 sync "<ビルドしたファイル群が格納されているディレクトリのパス>" "s3://<プレビュー環境用のS3バケット>/pull-requests/${PR_NUMBER}/" --acl private
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}

      # 必要に応じて Pull Request にデプロイ結果等をコメントする
      # ...(省略)

これにより、 Pull Request を作成するたびに S3 バケットに SPA の静的ファイルがアップロードされ、 https://pr-<PR番号>.preview.classi.example からアプリケーションにアクセスできるようになります。

💡 上記の例ではわかりやすくするために actions/checkout​​aws-actions/configure-aws-credentials を Git タグで参照していますが、実際にはコミット SHA を指定して参照するのがセキュリティ的には望ましいです。

# 例
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2

リリースされたアクションバージョンのコミットSHAを使用するのが、安定性とセキュリティのうえで最も安全です。

GitHub Actions のワークフロー構文 - GitHub Docs

実際、 tj-actions/changed-files や reviewdog/action-* などのアクションの Git タグが書き換えられ、これらのアクションを Git タグで参照している GitHub Actions Workflow 内で悪意のあるコードが実行されてしまうという出来事もありました。

unit42.paloaltonetworks.com

なお、 Git タグからコミット SHA への書き換えは pinact などのツールによって自動化が可能です。

プレビュー環境の削除

プレビュー環境は一時的なものなので、レビュー後は削除する必要があります。

今回は S3 バケットのライフサイクルルール *2 で S3 バケット内のオブジェクトを自動削除することで、プレビュー環境の削除を実現します。

# 14日後にプレビュー環境用のS3バケットのオブジェクトを削除する設定
resource "aws_s3_bucket_lifecycle_configuration" "example" {
  # プレビュー環境用のS3バケット
  bucket = aws_s3_bucket.example.id

  rule {
    id     = "expiration"
    status = "Enabled"

    # バケット全体に適用
    filter {
      prefix = ""
    }

    expiration {
      days = 14
    }
  }
}

ライフサイクルルールを使わずに Pull Request がマージされたときに S3 からファイルを削除する GitHub Actions Workflow を構築してもいいのですが、そもそも実装コストがかかることや、実装に不具合があった場合などにファイルが削除されずに残り続ける可能性がある、という懸念があります。 ライフサイクルルールを使えば、複雑な Workflow を構築することなく削除漏れを防止できるという利点があります。

なお、 Pull Request が Open のままでも一定期間以上放置されるとプレビュー環境が消えてしまうというデメリット (?) はありますが、必要に応じてワークフローを Re-run するだけですぐ再デプロイが可能なので、大した問題にはなりません。

まとめ

今回の仕組みを導入してからフロントエンドの Pull Request レビューが大変捗るようになったので、とてもおすすめです。

参考

今回のこちらの仕組みについては以下の記事を参考にさせていただきました。
こちらの記事では CloudFront Function ではなく Lambda@Edge を利用したパターンが紹介されています。あわせてご参照ください。

levelup.gitconnected.com

Appendix

デプロイ完了後に Pull Request に URL 付きのコメントをつける

プレビュー環境のデプロイ完了後に Pull Request に URL 付きのコメントを作成するようにするとサクッとプレビュー環境にアクセスできるので便利です。

以下はプレビュー環境のデプロイ完了後に peter-evans/find-comment アクションと peter-evans/create-or-update-comment アクションを使用して Pull Request にコメントを作成する例です。
もしワークフローが複数回実行されても、複数のコメントが作成されるのではなく単一のコメントが更新され続けます。

github.com github.com

name: Deploy Preview

# ...(省略)

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      # ...(省略)
      pull-requests: write # Pull Request にコメントを追加するために必要
    steps:
      # プレビュー環境のデプロイ
      # ...(省略)

      # 2 週間後の日付 (= ライフサイクルルールによって削除される日付) を取得
      - name: Get expiration date
        id: expiration
        env:
          TZ: Asia/Tokyo
        run: echo "date=$(date -d "+14 days" "+%Y-%m-%d %a")" >> "${GITHUB_OUTPUT}"

      # デプロイ完了後に Pull Request にコメントを追加する
      - uses: peter-evans/find-comment@v3
        if: github.event_name == 'pull_request'
        id: find-comment
        with:
          issue-number: ${{ github.event.pull_request.number }}
          comment-author: 'github-actions[bot]'
          body-includes: 'Deployed to Preview Environment'
      - uses: peter-evans/create-or-update-comment@v4
        if: github.event_name == 'pull_request'
        with:
          comment-id: ${{ steps.find-comment.outputs.comment-id }}
          issue-number: ${{ github.event.pull_request.number }}
          body: |
            ## :rocket: Deployed to Preview Environment :rocket:

            - **https://pr-${{ github.event.pull_request.number }}.preview.classi.example** (commit: ${{ github.event.pull_request.head.sha }})

            :information_source: This preview environment will be deleted on _**${{ steps.expiration.outputs.date }}**_.
          edit-mode: replace

プレビュー環境での API 通信について

Classi の内部 CMS のフロントエンドアプリケーションでは元々ローカル開発用に msw を導入しており、プレビュー環境でもそれをそのまま流用して API 通信を全てモックにしています。

www.npmjs.com

そのため、各プレビュー環境は backend とは切り離された状態での動作確認が可能となっています。

アプリケーションの特性によってはプレビュー環境でも実際の backend と通信させても問題ないものもあると思われるので、そこは要件に合わせて設計するといいでしょう。


GitHub ActionsとSchemaSpyを活用したデータベースドキュメント自動生成とGitHub Pagesへの公開方法

こんにちは、kobitoチームのアインです。この記事では、最近チームで導入したデータベースのドキュメント自動生成の仕組みとその経緯について紹介します。

なぜ自動化しようと思ったか

サービスを運用する上で、データベースのドキュメント管理は重要な業務の一つです。データベースのスキーマが変更される度に、ドキュメントを手作業で更新する必要があります。しかし、このような手動更新には以下の課題があります。

  • 記録漏れや実際のデータベースとの乖離が生じやすい
  • 複数人が同時に開発すると、ドキュメントと実際のデータベースの状態に差が生じやすい

これらの課題を解決するため、GitHub Actionsを活用したドキュメント自動生成の仕組みを導入しました。これにより、データベースの変更があるたびに最新かつ正確なドキュメントが自動的に生成されます。さらに、生成されたドキュメントをGitHub Pagesでホスティングすることで、社内メンバーはいつでも簡単に最新のデータベースドキュメントを参照でき、コミュニケーションの円滑化にもつながります。その結果、開発者はドキュメント管理の作業負担を減らし、本質的な開発業務やユーザーへの価値提供に集中できるようになります。

なお、GitHub Pages を社内限定公開に設定できるのは GitHub Enterprise のみのため、導入時には注意してください。

データベースドキュメントの自動生成にはSchemaSpy を使用しています。

SchemaSpy について

SchemaSpy(スキーマスパイ)は、データベースのスキーマを解析し、自動的に詳細なドキュメントや図表をHTML形式で生成するオープンソースツールです。

SchemaSpyが生成するドキュメントには、テーブルの一覧、テーブル間のリレーション、各テーブルの詳細な構造情報(カラム、インデックスなど)が含まれており、視覚的に把握しやすくなっています。 実際に生成されるドキュメントの例として、EpiViruSurfデータベースのドキュメントをご覧ください。

データベースドキュメントホームページを例として

SchemaSpyをローカル環境で実行する方法

1.準備

本手順は、PostgreSQLデータベースを使用していること、およびSchemaSpyが正常に動作しているデータベース環境に対して適用することを前提としています。 データベースのドキュメントを自動生成するためには、まず対象となるデータベースの接続情報を設定する必要があります。 この接続情報はファイルまたはコマンドライン引数で指定できますが、今回は設定ファイルを使用して指定します。

データベースの接続情報(ホスト名、ポート番号、データベース名、ユーザー名、パスワードなど)を設定ファイル(schemaspy.properties)に記述します

# データベースのタイプ(例:PostgreSQL)
schemaspy.t=pgsql11
# JDBCドライバへのパス(任意)
schemaspy.dp=drivers
schemaspy.host=localhost
schemaspy.port=5432
schemaspy.db=database
schemaspy.u=postgres
schemaspy.p=password
# ドキュメント生成後のファイルを出力するフォルダ
schemaspy.o=output
# ドキュメント作成対象のスキーマ名(例:public)
schemaspy.s=public

また、ドキュメントが出力されるフォルダ(output)をあらかじめ作成しておく必要があります。

2. データベースドキュメントの生成

Dockerを利用してドキュメントを生成します。以下のコマンドで実行できます。

docker run \
  -v "[path-to-output-folder]:/output" \
  --net="host" \
  -v "[path-to-config-file]:/schemaspy.properties" \
  schemaspy/schemaspy:snapshot

詳細な使い方や最新情報については、SchemaSpyの公式ドキュメント を参照してください。 生成されたドキュメントは ./output/ ディレクトリに出力されます。 ( 出力先は設定ファイルで変更可能 )。生成されたドキュメントをローカルで確認するには./output/ ディレクトリのindex.htmlをブラウザで開くことができます

GitHub Actionsを使ってSchemaSpyを自動実行・デプロイする際の注意点

今回作成したデータベースドキュメントを社内メンバーがいつでも参照できるように、GitHub Pagesへのデプロイを行います。デプロイには、GitHub Actionsを使った方法を紹介します。なお、GitHub Pagesへの初期設定の手順については、以下の記事で詳しく紹介していますので、参照してください。

docs.github.com

データベースが使用可能な状態であることを事前に確認する

GitHub Actionsのワークフローでデータベースを起動すると、データベースが完全に使用可能な状態になるまでには少し時間がかかります。このため、データベースがまだ使用可能でない状態でSchemaSpyを実行するとエラーが発生し、ドキュメントを正常に生成できません。そのため、Dockerのヘルスチェック機能を利用して、データベースが使用可能な状態であることを事前に確認する必要があります。 具体的には、以下のオプションを設定します。

  • --health-cmd pg_isready
    • PostgreSQLの起動確認
  • --health-interval 10s
    • 10秒間隔で起動状態を確認

この方法により、データベースが確実に使用可能な状態になってからドキュメントの生成を開始できるため、エラーを回避できます。

権限が不足して出力先のファイルを生成できない

ワークフロー上でデータベースドキュメントを生成する際、SchemaSpyは出力先となるフォルダがあらかじめ存在し、十分な権限が付与されていることを要求します。 具体的には、書き込み権限(write)と実行権限(execute)を事前に付与する必要があります。

また、GitHub Actionsを使ってGitHub Pagesへデプロイする際にも、そのフォルダに対して読み取り権限(read)が必要です。

つまり、ワークフロー内でデータベースドキュメントを生成する際、GitHub Actions上のUbuntu環境で動作するSchemaSpyが、出力フォルダに対して十分なファイル操作を行えるようにするため、以下のLinuxファイルシステム上の権限を事前に設定する必要があります。

  • 読み取り権限(read)
    • フォルダ内のファイルをGitHub Actionsが読み取れるようにする
  • 書き込み権限(write)
    • SchemaSpyがドキュメントを生成し、ファイルを作成・編集できるようにする
  • 実行権限(execute)
    • フォルダへのアクセスおよびディレクトリ内へのファイル書き込みを許可する

これらを適切に設定することで、ワークフローでのデータベースドキュメント生成およびGitHub Pagesへのデプロイがスムーズに行えます。

実際のGitHub Actionsワークフロー例

ここまで、データベースドキュメントを自動生成する際の注意点について説明してきました。 では実際に、それらの設定をどのようにGitHub Actionsのワークフローに落とし込んでいくのかを見ていきましょう。

以下は、main ブランチに変更が push されるたびに、SchemaSpy を使ってデータベースのドキュメントを生成し、GitHub Pages に自動デプロイする例です。

name: Generate DB docs with SchemaSpy and Deploy to GitHub Pages

on:
  push:
    branches:
      - main

jobs:
  generate-and-deploy-db-docs:
    services:
      postgres:
        image: postgres:14
        ports:
          - "5432:5432"
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pages: write
      id-token: write
    steps:
      # setup
      - name: Checkout repository
        uses: actions/checkout@v4
      - name: Prepare a directory for SchemaSpy output
        run: mkdir -m 777 output

      # db migration
      # ...省略

      # generate
      - name: Run SchemaSpy to generate ERD
        run: |
          docker run \
            -v "${GITHUB_WORKSPACE}/output:/output" \
            --net="host" \
            -v "${GITHUB_WORKSPACE}/db/schemaspy.properties:/schemaspy.properties" \
            schemaspy/schemaspy:snapshot

      # publish
      - uses: actions/upload-pages-artifact@v3
        with:
          path: ./output
      - uses: actions/deploy-pages@v4

GitHub Pages へのデプロイが完了すると https://<OWNER>.github.io/<REPOSITORY> から データベースドキュメントを閲覧できるようになります。

まとめ

今回はチームで導入した、データベーススキーマからドキュメントを自動生成する仕組みについて、その経緯を紹介しました。特別な方法ではありませんが、自動化されたことで、より良いドキュメントを作るために設計や説明文にも力を入れる意識が高まりました。

もしデータベースドキュメントの管理や自動生成のためのツール選定に迷っている方がいれば、ぜひ一度この方法を試してみてください

© 2020 Classi Corp.