TECH PLAY

サイオステクノロジー(Tech.Lab)

サイオステクノロジー(Tech.Lab) の技術ブログ

610

はじめに ども!最近Claude ProでAI開発にどっぷりハマっている龍ちゃんです。先日、Claudeにブログ記事を評価してもらったら「2025年としてはこのブログ普通っすね!むしろ遅れてるぐらいっす」って辛辣なコメントをもらっちゃいました。一方で、Gemini Deep Researchで同じ記事を評価したらべた褒めだったので、AIによる評価の違いって面白いですよね。 今回は、いよいよClaude DesktopとNotion MCPを接続して、AIによるドキュメント自動操作環境を構築していきます。 予測される効果として、「 Claude×技術ブログで執筆環境が激変!次世代AI協働ワークフロー解説 」こちらのブログで解説している内容がよりグレードアップされると期待しています。 事前準備:Node.js環境のセットアップ まずは、Notion MCPサーバーを動作させるためのNode.js環境を準備しましょう。既にNode.jsが入っている方は、バージョン確認から始めてください。 Node.js未インストールの場合:Voltaで管理 Node.jsが入っていない方には、 Volta での管理をおすすめします(完全に個人的な趣味ですが…)。お好きな管理ツールを使っていただいて構いません。 Voltaのインストール(Windows) # 管理者権限で実行 curl https://get.volta.sh | bash Node.jsのインストール # LTS版をインストール volta install node # バージョン確認 node --version npm --version 既存環境の確認 Node.js v18以上が必要です。以下のコマンドで確認してください: # Command PromptまたはPowerShellで実行 node --version npm --version Notion MCPサーバーの動作確認 以下のコマンドで、Notion MCPサーバーがインストールできるかテストします: # 動作確認テスト npx -y @notionhq/notion-mcp-server --help 正常に動作すればヘルプが表示されます。Ctrl+Cでプロセスを終了してください。 接続方法の比較と選択 補足:Notion公式のHosted MCP Serverについて 2025年7月より、Notion公式がHosted MCP Serverも提供開始しました! Hosted MCP Server(リモート版)の特徴 OAuth認証でワンクリック接続 AI向けに最適化されたMarkdown形式でのデータ提供 サーバー管理不要 Notionがクラウドでホスト ワークスペース全体への完全アクセス ただし、 現在報告されている課題 : Gemini CLI等のMCPクライアントとの互換性問題 細かいアクセス制御ができない(ワークスペース全体のみ) なぜローカル実装(npx経由)を推奨するのか Notion公式のHosted MCP Serverも魅力的ですが、現在以下の理由でローカル実装をおすすめします: 方法 設定難易度 プラン制限 アクセス制御 安定性 推奨ユーザー コネクタ版(Remote) 公式未明記 ワークスペース全体 簡単設定重視 Integration版(Local) なし ページ単位 制御重視 Hosted MCP 公式未明記 ワークスペース全体 最新技術試行 全プラン対応 : 無料版でも利用可能 細かい制御 : ページ単位でアクセス権限設定 安定動作 : Node.js環境があれば確実に動作(今更感もあるけども…) カスタマイズ : 必要に応じて機能拡張可能 互換性 : 各種MCPクライアントで動作確認済み そのため、確実性と柔軟性を重視してnpx経由での実装方法をご紹介します。 方法1:Notionコネクタでの簡単接続 Claude Desktopの コネクタ機能 を使用する方法です。これはAnthropicが推奨する公式ツールで、ブラウザ上でMCP認証を簡単に完了できます。 接続手順 Claude Desktop → 設定 → コネクタ 「カスタムコネクターを追加」をクリック 「Notion」を検索・選択 自動的に認証ページが開くので、認証を完了 重要な注意点 コネクタ版では ワークスペース全体 に接続されるため、Claudeからワークスペース内のすべての情報にアクセス可能になります。機密情報が含まれる場合は、次の「Notion Integration版」をご検討ください。 トラブルシューティング MCPの接続が何度か失敗する場合は、コネクタから切断をして再度認証を行ってください。 方法2:Notion Integration作成してMCP接続 こちらの方法では、NotionのAPIを使用してMCPを構築し、アクセス権限を細かく制御できます。特定のページのみにアクセスを限定したい場合におすすめです。 こちらの方法に関しては Notion公式が記事としてまとめています 。 Integration作成 Notion Integrations にアクセス 「New integration」をクリック 以下を設定: Name : Claude MCP Integration Workspace : 使用するワークスペースを選択 Capabilities : デフォルトのまま(Read content, Update content, Insert content) Integration Token取得 作成後、「Internal Integration Secret」を取得してください。 ntn_ で始まるトークン(例: ntn_abc123def456... )が表示されます。 セキュリティ注意 このトークンは機密情報です。安全に保管し、他人と共有しないでください。 ページへのアクセス権限付与 Notionで任意のページを開く 右上の「…」メニュー → 「接続を追加」 作成したIntegrationを選択して接続 この手順により、指定したページのみにMCPからのアクセスが制限されます。 設定ファイルの場所確認 Claude Desktopから設定ファイルにアクセスする方法が最も簡単です: Claude Desktop → 設定(Settings) 開発者(Developer)タブ 「Edit Config」ボタンをクリック 参考:ファイルパス Windows: %APPDATA%\\Claude\\claude_desktop_config.json ファイルが存在しない場合は新規作成してください。 設定ファイルの作成・編集 以下の内容で設定ファイルを作成・編集します: { "mcpServers": { "notionApi": { "command": "npx", "args": [ "-y", "@notionhq/notion-mcp-server" ], "env": { "OPENAPI_MCP_HEADERS": "{\"Authorization\":\"Bearer ntn_your_token_here\",\"Notion-Version\":\"2022-06-28\"}" } } } } 重要 : ntn_your_token_here を Step 2で取得した実際のIntegration Tokenに置き換えてください。 動作確認 Claude Desktopの再起動 Claude Desktopを完全に終了(システムトレイからも終了) Claude Desktopを再起動 接続テスト Claude Desktopで以下のプロンプトを試してください: Notionのワークスペースに接続できているか確認してください。利用可能なページがあれば一覧を表示してください。 実際にページを操作してみる 「テストページ」という名前で新しいページを作成して、簡単な内容を追加してください。 成功すれば、Claude経由でNotionページが作成・編集されます! どれを選ぶべきか 実際に複数の方法を試してみて感じた違いをお話しします。 これからNotion×Claudeを始める方 : コネクタ版がおすすめ(プラン制限の詳細は公式発表を確認) 細かいアクセス制御が必要・確実性重視 : Integration版一択 開発者・他AIツールも検討中 : Integration版でMCPサーバー構築 最新技術を試したい方 : Hosted MCP Server( 互換性問題 承知の上で) 今後の展望:AIフレンドリーなNotion環境構築 現在の私のNotion環境は、残念ながらAIフレンドリーとは言えません。今後は以下の改善を検討しています: AIが効率的にアクセスできるページ構造への再編 よく使用するページの階層浅化 AI専用ワークスペースの構築 皆さんも、AIパートナーとしてClaudeを活用する際は、Notionの構造も合わせて最適化していくことをおすすめします。 まとめ 今回の記事では、Claude ProとNotion MCPを接続するための複数の方法を紹介しました。コネクタ版は簡単に始められる一方、Integration版はより細かい制御が可能です。どの方法を選ぶにしても、AIとNotionの連携によって情報管理と創造性が大きく向上するでしょう。ぜひ皆さんも試してみてください! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Claude×Notion MCP実装術|コネクタ版とIntegration版の選び方解説 first appeared on SIOS Tech. Lab .
はじめに Git & GitLab入門ブログ Gitマスターへの道の第2回です。前回の「 Gitの基本とGitLab / GitHubについて 」では、Gitの基本を解説しました。 今回は、ローカルPCにGit、VSCodeを導入し、最初のコミットを行うまでの手順を解説します。Windows環境をメインに説明しますが、他のOSをご利用の方にも役立つ情報を含んでいます。 初期環境構築 Gitを使い始めるためには、まずお使いのPCにGitをインストールし、基本的な設定を行う必要があります。 今回のGit操作入門では、VSCodeを使用してGitの操作やコードの編集を説明します。 Gitのインストール Gitの公式サイト( https://git-scm.com/download/win )にアクセスし、最新のインストーラーをダウンロードします。 ダウンロードしたインストーラーを実行します。インストール中の設定は、特にこだわりがなければすべてデフォルトのままで問題ありません。「Next」をクリックしていき、インストールを完了させてください。 Windows + R から「ファイル名を指定して実行」を開き、「cmd」と入力してEnterキーを押し、コマンドプロンプトを開きます。以下のコマンドを実行してバージョンが確認されれば成功です。 $ git --version VS Codeのインストール VS Codeの公式サイト( https://code.visualstudio.com/download )にアクセスし、Windows版のインストーラーをダウンロードします。 ダウンロードしたインストーラーを実行します。「同意する」を選択し、「次へ」をクリックしてインストールを進めます。 この際、「Codeで開く」アクションをエクスプローラーのコンテキストメニューに追加するオプションにチェックを入れておくと便利です。 その他のOSについて(Mac/Linux) Mac:Homebrew ( https://brew.sh/index_ja ) を利用するのが最も簡単です。ターミナルで以下のコマンドを実行してください。 $ brew install git VS Codeも公式サイトからダウンロード・インストールできます。 Linux:各ディストリビューションのパッケージマネージャーを利用します。例えばUbuntuであれば、ターミナルで以下のコマンドを実行します。 $ sudo apt update && sudo apt install git VS Codeも公式サイトから.debや.rpmパッケージをダウンロードしてインストールできます。 ユーザー名とメールアドレスの設定 Gitをインストールしたら、コミットの際に記録されるユーザー名とメールアドレスを設定します。これは、誰がどの変更を行ったかを識別するために重要です。 コマンドプロンプトを開き、以下のコマンドを実行してください。 $ git config --global user.name "名前" $ git config --global user.email "メールアドレス" “名前” の部分には、表示したいユーザー名を記載してください。これは、GitHubなどでもコミット履歴として確認されます。 “メールアドレス” の部分には、普段お使いのメールアドレスを記載してください。 この設定は–globalオプションを付けているため、一度設定すれば、そのPC上のすべてのGitリポジトリに適用されます。 VSCodeにおける操作 (windows) VS Codeの左側にあるソース管理アイコンから後述するGitの操作が可能です。 この機能により、本来 コマンドプロンプトで実行するGit操作がGUI上でも実行可能となります。 ローカルリポジトリの作成 Gitでバージョン管理を始めるには、まず、リポジトリを作成する必要があります。前回説明しましたが、Gitリポジトリにはローカルリポジトリとリモートリポジトリの2種類あります。 今回は、手元のPCに作成する「ローカルリポジトリ」を作成します。VSCodeなどの開発時に利用するエディタの拡張機能を用いてGitを管理する事で、利便性や効率性を持たせてGitを扱うことが出来ます。 VS Codeを開き、「ファイル」>「フォルダーを開く」を選択します。 Gitで管理したいプロジェクトのフォルダー(例:git-tutorial)を新しく作成し、そのフォルダーを選択して開きます。 VS Codeの左側にあるソース管理アイコンをクリックします。 「リポジトリを初期化」ボタンをクリックします。 コマンドで実行する場合は、VSCodeからCtrl + @ でターミナルを起動し、以下のコマンドを実行します。 $ git init これで、指定したフォルダーがGitによって管理されるようになり、ローカルリポジトリが作成されました。フォルダー内に.gitという隠しフォルダーが作成されていれば成功です。 最初のコミット リポジトリを作成したら、最初のファイルを作成し、それをGitに記録(コミット)してみます。 コミットの基本的な流れは、以下の3つのステップから構成されます。 変更 この時点ではまだ、その変更がGitのバージョン管理の対象として確定されたわけではありません。例えるなら、ノートに下書きを書いた状態です。 ステージング 変更を加えたファイルをGitのバージョン管理に含めるためには、「ステージングエリア」と呼ばれる一時的な領域に、その変更を「ステージ(追加)」する必要があります。ステージングのステップが必要な理由のひとつとしては、一度に複数のファイルを変更した場合でも、その中の特定の変更だけをコミットしたい場合に対応するためです。 コミット ステージングエリアに準備が整った変更を、最終的にGitのバージョン履歴として確定させる操作です。いくつかの呼称がありますが、コミットはコミットIDという一意のハッシュ値で識別されます。 最初のファイルを作成していきます。VS Codeの左側のエクスプローラービューで先ほど作成したフォルダー内に新しいファイルを作成します。例えば、index.phpというファイルを作成し、以下の内容を記述します。 Hello world! ファイルを保存すると、VS Codeのソース管理ビューにindex.phpが「変更」として表示されます。これは、Gitがファイルに変更があったことを検知している状態です。 ndex.phpの行にマウスカーソルを合わせると表示される「ステージング」(プラスアイコン)をクリックします。これにより、index.phpがコミットの対象として選択(ステージ)されます。 ターミナルで実行する場合は、以下のコマンドを実行します。 $ git add index.php 上部のテキストボックスにコミットメッセージを入力します。コミットメッセージは、そのコミットでどのような変更を行ったかを簡潔に説明するものです。今回は「Initial commit」と入力しましょう。 コミットメッセージを入力したら、上部のチェックマークアイコン(コミットボタン)をクリックします。 ターミナルで実行する場合は、以下のコマンドを実行します。 $ git commit -m “Initial commit” これで、最初のコミットが完了しました。Gitはindex.phpの現在の状態を履歴として保存しました。 改めてGit操作の第1歩としてはまず、ローカルリポジトリ上での変更→ステージング→コミットの流れについて覚えていくのが重要です。 まとめ 今回はGitの環境構築と最初のコミットなどを解説しました。これでバージョン管理に足を一歩踏み入れました。次回はチーム開発に向けて解像度を上げるために、他のGitの機能やコミットの戻し方等を解説します。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Git & GitLab 入門 (2) ~Git マスターへの道~「Git操作入門」 first appeared on SIOS Tech. Lab .
Material Designをはじめとして、グラフィカルユーザーインターフェース(GUI)のデザインシステムは数多く公開されています。 各デザインシステムに含まれるUIコンポーネントは、各ブランドやサービスに最適化された独自の内容ですが、基本的な部分は共通しています。 たとえば「Button」コンポーネントは、どのデザインシステムでも、同じ役割かつ同じ名前で定義されています。 一方、役割は概ね同じでも、名称が異なるケースもあります。 この記事では、そのようなケースの具体例を挙げながら、UIコンポーネントの名称を整理します。 各デザインシステムのコンポーネントのカタログ化の粒度がさまざまで、グループ化の判定は筆者独自のものですが、複数のデザインシステムを参照する際の参考になれば幸いです。 今回対象としたデザインシステムやUI Kit Apple Human Interface Guidelines Bootstrap Atlassian Design System Material Design(Google) Carbon Design System(IBM) U.S. Web Design System(USWDS) GOV.UK Design System SmartHR Design System デジタル庁デザインシステム Fluent 2(Microsoft) Shadcn/ui (コンポーネント名は、オリジナルが複数形の場合も単数形に変更して統一しています。例:Chips → Chip) テキスト入力:「Text field」「Text input」など UIコンポーネント名 デザインシステム Text field Apple Atlassian Material Design(Google) Text input Carbon(IBM) USWDS GOV.UK Input SmartHR Fluent 2(Microsoft) Shadcn/ui Input text デジタル庁 テキストを入力する基本的なコンポーネントです。 「Text input」や「Input」は、HTMLの<input type=”text”>と一致する名称です。(inputタグのデフォルトタイプはtext) なお、Material Design、USWDS、Appleは、1行のみの入力と複数行の入力を1つのUIコンポーネントにまとめています。 それ以外のデザインシステムでは、1行入力を「Text input」、複数行入力を「Textarea」のように、コンポーネントを分けているケースが多いです。 これもHTMLの<textarea>タグに合わせた名称です。 確認するもの:「Dialog」「Modal Dialog」など UIコンポーネント名 デザインシステム Dialog Material Design(Google) SmartHR Fluent 2(Microsoft) Shadcn/ui Modal Bootstrap Carbon(IBM) USWDS Modal dialog Atlassian デジタル庁 Alert Apple Alart Dialog Shadcn/ui 「ダイアログ」は直訳すると「対話」です。 「モーダル」は「モードの」という意味で、ひとつのモードに入っていて他の操作をさせないことを示します。対義語は「モードレス」です。 上記のコンポーネントの多くがモーダルな動作をしますが、たとえばSmartHRには「ModelessDialog」も存在します。 また、Material Designや、Bootstrapでは、フルスクリーン表示もサポートされています。 Shadcn/uiでは「Dialog」と「Alart Dialog」が別に定義されています。 左右でオンとオフを選ぶ:「Switch」「Toggle」 UIコンポーネント名 デザインシステム Switch Apple Bootstrap Material Design(Google) SmartHR Fluent 2(Microsoft) Shadcn/ui Toggle Atlassian Carbon(IBM) Appleでは「Switch」「Checkbox」「Radio Button」など、2つの状態を選択させることをまとめて「Toggle」として案内しています。 また、Shadcn/uiでは、「Switch」と「Toggle」が別のコンポーネントとして定義されています。Toggleはシンプルなボタン表現です。 なお、「Switch」と「Checkbox」は、どちらもオンオフの二択入力ですが、「Checkbox」が入力後に登録ボタンを押して有効になるのに対し、「Switch」は入力と同時に有効とするのが、一般的な使い分けです。 小さい情報:「Chip」「Tag」 UIコンポーネント名 デザインシステム Chip Material Design(Google) SmartHR Tag Atlassian Carbon(IBM) USWDS GOV.UK Fluent 2(Microsoft) 「ソーシャルタグ」「ハッシュタグ」「タグ付け」など、テキスト情報(ラベル)に着目した名前が「Tag」です。 これに対して、「Chip」はテキストなどが乗っているオブジェクトに焦点を当てたネーミングだと考えられます。 ひょっこり現れる:「Snackbar」「Toast」など UIコンポーネント名 デザインシステム Snackbar Material Design(Google) Toast Bootstrap Carbon(IBM) Fluent 2(Microsoft) Sonner Shadcn/ui Flag Atlassian 画面の外側からスライドして現れ、状態の変化や警告などをオーバーレイで表示するコンポーネントです。 Shadcn/uiでは以前からあった「Toast」コンポーネントを非推奨にして、代わりに「Sonner」が追加されました。 「Sonner」は「Toast」を基本としたコンポーネントのようです。 以下は、各名称について筆者の解釈です。 「Snackbar」は少ない情報を提供することを、軽食店に例えた。 「Toast」はトースターからパンが飛び上がる様子から。 「Flag」は注意を引く際の旗を上げる様子から。 「Sonner」はフランス語の「(警報などが)鳴り響く、知らせる」の意味の単語から。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post そのUIどう呼ぶ?チームの認識を揃えるためのコンポーネント名称整理 first appeared on SIOS Tech. Lab .
今号では、systemd.timer について、その仕組みや設定方法について説明します! 前回と前々回の記事では cron についてご紹介したので、cron の代替として位置づけられている systemd.timer についても知っておくことで、よりタスク管理を理解できるかと思います。 systemd.timer とは systemd.timer とは、以前ご紹介した cron のように定期的にタスクを実行してくれる機能の名称です。RHEL9 以降、タスク管理の仕組みとして cron ではなく systemd.timer が標準として搭載されています。 ※ cron を使用することもできますが、デフォルトで配置されていたいくつかのジョブは systemd.timer へ移行しています。 cron は crond というデーモンとして動作するのに対し、systemd.timer は systemd が提供する機能のひとつ として動作します。 具体的な機能、設定方法についてはこの後ご説明しますが、systemd.timer は cron と比較して、書式がよりシンプルに変更されていたり、時間指定がより柔軟に設定できるようになっています。 Linux におけるタスク管理とは何か?については、cron の記事で詳しく説明していますので、ぜひご参照ください! ・ 知っておくとちょっと便利!cron によるタスク管理1 ・ 知っておくとちょっと便利!cron によるタスク管理2 systemd.timer の設定ファイル、ディレクトリ構成 systemd.timer は、基本的に下記 2つのファイルで構成されます。 (※今回は RHEL9 の環境を前提に説明します)  ・ .timer ファイル :スケジュールを定義します。  ・ .service ファイル :実行するコマンドやスクリプトを定義します。 .timer ファイルで指定されたスケジュール (日時や間隔) に基づいて、対応する .service ファイルに記載された内容を処理します。 (例えば、A.timer には A.service というファイルが対応します) これらのファイルは、デフォルトは /usr/lib/systemd/system もしくは /etc/systemd/system 配下に配置されています。 なお、ユーザが独自の .timer や .service を定義したい場合は、通常 /etc/systemd/system 配下に各ファイルを配置します。 ちなみに、ファイルの内容は /usr/lib/systemd/system よりも /etc/systemd/system の方が優先されるため、既存の .timer や .service の内容に新しい処理を上書きしたい場合も、 /usr/lib/systemd/system 配下のファイルを直接編集するのではなく /etc/systemd/system 配下にファイルを配置することを推奨しています。 systemd.timer の設定方法 まずは既存のファイルの内容から、どのような設定ができるのかを見ていきたいと思います。 例として、logrotate.timer の内容を見てみます。 logrotate.timer の内容 [Unit] Description=Daily rotation of log files Documentation=man:logrotate(8) man:logrotate.conf(5) [Timer] OnCalendar=daily AccuracySec=1h Persistent=true [Install] WantedBy=timers.target [Unit] セクション timer ユニットの一般的な情報とドキュメントを定義します。 Description このユニットの概要です。 Documentation このユニットに関連するドキュメント情報です。 [Timer] セクション タスクがいつ、どのように動作するか定義します。 OnCalendar=daily タスク (この場合ですと、logrotate.service) が実行される頻度を指定します。 daily は毎日実行されることを意味します。 他には hourly(毎時)、weekly(毎週)、特定の日付や時刻 なども指定できます。 AccuracySec=1h 精度を指定します。1h は 1時間を意味します。 これは、タスクが OnCalendar で指定された時刻から最大 1時間の遅延を許容することを示します。 これにより、すべてのタスクが同時に起動してシステムに負荷をかけるのを避けることができます。 他には s(秒)、m(分)、d(日) なども指定できます。 Persistent=true この設定が true である場合、システムがシャットダウンしている間に実行されるはずだったタスクが、システム起動後直ちに実行されるようになります。 反対に、この設定が false である場合 (もしくは設定されていない場合、デフォルトでは false の動作となる)、スケジュールした時間になってもタスクが実行されなかった場合、そのタイミングでの実行は一度スキップされ、次回の実行タイミングまで待ちます。 [Install] セクション ユニットが有効化された時の動作を指定します。 WantedBy=timers.target systemctl enable (システム起動時に、当該のサービスを自動的に有効化する) である場合、どのタイミングで有効化されるかを決めます。 この設定が timers.target である場合、タイマー (定期実行されるタスク) の動作準備が完了したタイミングで、当該のサービスが起動されます。 .timer ファイルを使用している場合は、この設定を使用することが多いです。 他には multi-user.target (サービスの動作準備が完了したタイミング)、 graphical.target (GUI 環境が使用可能になったタイミング)、 なども指定できます。 systemd.timer に設定可能な項目は、他にもたくさんあります。 より詳しく知りたい方は、 man systemd.timer コマンドでマニュアルを確認してみてください。 次号について 次号では、 systemd.timer によるタスク管理の tips についてご紹介します! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 知っておくとちょっと便利!systemd.timer によるタスク管理1 first appeared on SIOS Tech. Lab .
こんにちは! 今月も「OSSのサポートエンジニアが気になった!OSSの最新ニュース」をお届けします。 Linux 環境でよく使用される sudo において、影響度の高い脆弱性 (CVE-2025-32463) が報告されました。 Linux sudo chrootの脆弱性(CVE-2025-32463)により、非特権ユーザーがroot権限を取得可能に https://rocket-boys.co.jp/security-measures-lab/linux-sudo-chroot-cve-2025-32463-root-escalation/ 2025/7/3、Cloud Native Computing Foundation (CNCF) の 2024年アニュアルレポートの日本語版が公開されました。 CNCF アニュアル レポート 2024 日本語版を公開 https://prtimes.jp/main/html/rd/p/000000374.000042042.html 2025/7/16、Google が CVE-2025-6558 の脆弱性に対する Chrome の緊急アップデートをリリースしました。 Urgent: Google Releases Critical Chrome Update for CVE-2025-6558 Exploit Active in the Wild https://thehackernews.com/2025/07/urgent-google-releases-critical-chrome.html ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 【2025年7月】OSSサポートエンジニアが気になった!OSS最新ニュース first appeared on SIOS Tech. Lab .
挨拶 ども!お久しぶりです。Claudeを活用した開発手法の模索に真剣に取り組んでいる龍ちゃんです。 合間でStreamlitアプリの開発もちょこちょこやっているんですが、前回は「 DevContainerでStreamlit開発を始める方法:Docker+VSCode 」でStreamlitの開発コンテナ環境を構築しました。 開発環境は整ったものの、「いざ本番環境にデプロイしようとしたら、設定が開発環境のままで困った…」という経験、皆さんもありませんか?実際に私も最初の頃、開発用の設定のままAzureにデプロイして、エラー情報が丸見えになってしまったことがあります。 今回はそんな課題を解決するため、 本番環境用Dockerfileの構築手法 を共有していきたいと思います! 今回の内容 : 「Streamlit 本番環境用Dockerfileの作成」 セキュリティを考慮したコンテナ設計 開発環境と本番環境の適切な設定分離 プロダクション向けStreamlit最適化 事前準備 事前準備として、 前回のVSCode環境で、コンテナを用いてStreamlit環境を構築した記事 の内容をベースに進めていきます。 まだ環境が整っていない方は、その記事を参考に環境を構築するか、 サンプルリポジトリ を参照してください。 前提条件 : Docker Desktop がインストール済み 基本的なStreamlitアプリが作成済み VSCodeでの開発経験(推奨) 今回の内容はStreamlitのDockerビルドに関するものですが、環境が完全に一致していなくても動作します。使える部分だけを取り入れていただければ問題ありません。 今回の目標 今回の目標は、 本番環境用のDockerfileを作成し、実際にローカルでビルド・動作確認すること です。 また、Streamlit用のプロダクション環境向け設定ファイル(config.prod.toml)の作成も目指しています。 具体的な手順 : 本番環境用Dockerfileの作成 Streamlit設定ファイル(config.prod.toml)の最適化 Dockerコマンドを用いた動作確認 最終的な成果物 : セキュリティを考慮した本番環境用Dockerfile プロダクション最適化されたStreamlit設定 即座にデプロイ可能なDockerイメージ 今回作成するディレクトリ構造は以下のようになります: . ├── .devcontainer │ ├── Dockerfile │ ├── compose.yml │ └── devcontainer.json ├── .gitignore ├── .streamlit │ └── config.prod.toml # 本番環境用Streamlit設定ファイル ├── Dockerfile # 本番環境用Dockerfile ├── README.md ├── requirements.txt ├── setup.cfg └── src ├── chat.py ├── demo.py └── main.py 開発環境と本番環境の設定戦略 ここで重要なのが、 開発環境と本番環境の設定を明確に分離する ことです。実際のプロジェクトでどちらを選ぶべきか、迷うところですよね。 開発環境の特徴 開発環境では、特別な設定を行っていませんでした。これはStreamlitのデフォルト設定に開発者向けの便利機能がすでに組み込まれているからです: エラーメッセージの詳細表示 : デバッグに便利 ファイル変更時の自動リロード : ホットリロード機能 開発者ツールの表示 : パフォーマンス情報など 詳細なデバッグ情報 : トラブルシューティングに有用 本番環境での課題 一方、本番環境ではこれらの機能は不要どころか、 ユーザー体験を損なう要因 になります: エラーの詳細情報がエンドユーザーに表示される(セキュリティリスク) 不要な開発者ツールが表示されて混乱を招く そもそもソースコードが変更されることがないのでリロード機能は不要 リソース使用量が最適化されていない 設定分離のメリット そこで、本番環境専用の設定ファイル(config.prod.toml)を作成することで: セキュリティ向上 : 不要な情報の非表示化 パフォーマンス最適化 : 開発機能の無効化 ユーザー体験向上 : クリーンなインターフェース 運用効率化 : 環境に応じた適切な設定 また、VSCode開発時に必要な情報(.vscodeディレクトリなど)や開発時にのみ必要な情報を本番環境のビルドに含めないことで、 より軽量なDockerイメージ を実現できます。 開発環境では、DevContainerで開発するために専用イメージを使用していましたが、これには多くの設定が含まれた重いイメージになっています。本番環境ではこれらを削ぎ落として、極力小さなベースイメージを使用したいという意図があります。 環境分離のメリットとしては、 イメージの軽量化 や 不要情報の削除によるセキュリティリスクの回避 が挙げられます。 本番環境用Dockerfileの詳細解説 それでは、本番環境用Dockerfileについて詳しく見ていきます。 ベースイメージの選択に関しては、こちらの「 Dockerイメージの選び方ガイド 」が参考になります。 Dockerfileのベースとしては、 公式リファレンスの「Deploy Streamlit using Docker」 を参照しています。 # 本番環境用 - Python 3.11 slim イメージ FROM python:3.11-slim # 非rootユーザーでの実行(セキュリティ対策) RUN groupadd -r appuser && useradd -r -g appuser appuser # 作業ディレクトリの設定 WORKDIR /app # システムの更新と必要最小限のパッケージをインストール RUN apt-get update \ && apt-get install -y --no-install-recommends \ curl \ ca-certificates \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* # Pythonの設定(パフォーマンス最適化) ENV PYTHONDONTWRITEBYTECODE=1 \ PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ PIP_DISABLE_PIP_VERSION_CHECK=1 # ヘルスチェック HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8501/_stcore/health || exit 1 # 依存関係ファイルをコピー COPY requirements.txt . # Pythonパッケージのインストール RUN pip install --no-cache-dir -r requirements.txt # アプリケーションコードをコピー COPY ./src . # Streamlit設定ファイルを作成 RUN mkdir -p .streamlit COPY .streamlit/config.prod.toml .streamlit/config.toml # ファイルの所有権を変更(セキュリティ対策) RUN chown -R appuser:appuser /app # 非rootユーザーに切り替え USER appuser # ポート公開 EXPOSE 8501 # Streamlitアプリの起動 CMD ["streamlit", "run", "main.py"] ベースイメージの選択理由 こちらが本番環境のDockerfileになります。ベースイメージとしては、 Python 3.11のslimイメージ を使用し、開発環境と同じバージョンに揃えています。 slimイメージを選択した理由: 必要最小限のパッケージのみ含有(セキュリティリスク軽減) イメージサイズが小さい(デプロイ時間短縮) 本番環境に不要な開発ツールが含まれていない セキュリティ対策のポイント セキュリティのために 非rootユーザーでの実行 を行うためにユーザーを作成し、システムの更新と必要最小限のパッケージのインストールも実施しています。 パフォーマンス最適化の工夫 Pythonアプリケーションを動かすための手順としては、まず依存ファイルをコピーして必要なパッケージをインストールし、その後、srcディレクトリ内のアプリケーションコードとStreamlitの設定ファイルをコピーしています。 設定ファイルの適切な管理 特に重要な点は Streamlitの設定ファイル(config.toml)の作成部分 です。.streamlitディレクトリを作成し、ホスト側にある.streamlitディレクトリ内の config.prod.toml ファイルを、コンテナ内では単に config.toml としてコピーしています。 これにより、設定項目が本番環境用のconfigとして正しく認識され、プロダクション用の設定を適切に管理できます。また、config.prod.tomlというファイル名にすることで、開発環境では読み込まれないという利点もあります。 公開ポートとしては8501を指定し、最終的にStreamlitを起動するコマンドを実行しています。 Stremlit HealthCheckについて 公式のこちらのページ でDockerのコンテナが起動しているかをHealth Checkを行っています。これはStreamlitを確実に動作させるために必要なので追加しておきます。 ヘルスチェックにより、コンテナオーケストレーション(Kubernetes、Docker Swarm等)が自動的に異常なコンテナを検知・再起動できるようになります 本番環境用Streamlit設定(config.prod.toml) このファイルは 本番環境用のStreamlit設定ファイル です。詳細な内容については、 Streamlit公式のドキュメント を参照してください。 ファイル内には一通り必要な設定項目にコメントを付けていますので、それぞれの設定が必要かどうか判断していただければと思います。 実装としては以下のようになります: # Streamlit 本番環境用設定ファイル # 各設定項目の詳細説明付き [server] # ブラウザを自動で開かない(サーバー環境での実行に適している) # デフォルト: false headless = true # Streamlitアプリが待ち受けるポート番号 # デフォルト: 8501 port = 8501 # サーバーがリッスンするIPアドレス # "0.0.0.0" で全てのネットワークインターフェースからの接続を許可 # デフォルト: (未設定) address = "0.0.0.0" # 静的ファイル配信機能を無効化(セキュリティ向上) # staticディレクトリからのファイル配信を禁止 # デフォルト: false enableStaticServing = false # ファイル変更時の自動再実行を無効化(本番環境では不要) # 開発環境でのみ有効にすべき機能 # デフォルト: false runOnSave = false [browser] # Streamlitへの使用統計送信を無効化(プライバシー保護) # 本番環境では通常無効化する # デフォルト: true gatherUsageStats = false # ブラウザが接続すべきサーバーアドレス # コンテナ環境では "0.0.0.0" を指定 # デフォルト: "localhost" serverAddress = "0.0.0.0" # ブラウザが接続すべきポート番号 # server.portと同じ値を設定 # デフォルト: server.portの値 serverPort = 8501 [global] # 開発モードを無効化(本番環境用の設定) # 開発者向け機能や詳細なエラー表示を制限 # デフォルト: false developmentMode = false # 直接実行時の警告表示を無効化 # "python script.py" での実行時の警告を非表示 # デフォルト: true showWarningOnDirectExecution = false [client] # エラー詳細の表示レベル設定 # "full": 完全なエラー情報を表示(本番環境でも詳細確認のため有効) # 他の選択肢: "stacktrace", "type", "none" # デフォルト: "full" showErrorDetails = "full" # ツールバーの表示モード設定 # "viewer": 開発者オプションを非表示(本番環境に適している) # 他の選択肢: "auto", "developer", "minimal" # デフォルト: "auto" toolbarMode = "viewer" # マルチページアプリでのサイドバーナビゲーション表示 # pages/ディレクトリベースのナビゲーションを有効化 # デフォルト: true showSidebarNavigation = true [ui] # Streamlitのトップバーを非表示 # より洗練されたUIを実現(ブランディング向上) # カスタム設定項目 hideTopBar = true [theme] # ダークテーマを適用 # "light" または "dark" から選択可能 # ユーザー体験向上のためのテーマ設定 base = "dark" セキュリティの機能(CORSやXSRF)などの保護機能を無効化にすることもできますが、ここはデプロイ先の構成によって検討するべき内容です。十分に注意して設定項目を決定してください。 Streamlit公式フォーラムでも言及されています 。 動作確認 それでは、実際に作成したDockerfileとconfig.prod.tomlを使って動作確認を行ってみます。 Dockerイメージのビルド まずはDockerイメージをビルドしてみましょう: # Dockerイメージのビルド docker build -t python-app:test . コンテナの起動と確認 次に、ビルドしたイメージを使ってコンテナを起動します: # コンテナの実行 # DevContainerで8501を使用している可能性を考慮して8502に接続 docker run -d -p 8502:8501 --name python-app-test python-app:test # ログ確認 docker logs python-app-test # ブラウザでアクセス # http://localhost:8502 クリーンアップ 動作確認が完了したら、コンテナを停止・削除します: # コンテナの停止と削除 docker stop python-app-test docker rm python-app-test # 不要なイメージの削除(必要に応じて) docker rmi python-app:test よくあるエラーと対処法 ポートが既に使用されている場合 # エラー: port is already allocated # 対処法: 別のポートを使用 docker run -d -p 8503:8501 --name python-app-test python-app:test 設定ファイルが見つからない場合 # .streamlit/config.prod.toml の存在確認 ls -la .streamlit/ # ファイルパスの確認 COPY .streamlit/config.prod.toml .streamlit/config.toml 権限エラーが発生する場合 # ファイル所有権の確認 docker exec -it python-app-test ls -la /app まとめ 今回は Streamlit本番環境用Dockerfileの構築 について詳しく見てきました。 開発環境と本番環境の設定を適切に分離することで、セキュリティとパフォーマンスの両方を向上させることができました。特に config.prod.toml による設定最適化と、 非rootユーザー でのコンテナ実行は、実際のプロジェクトでも重要なポイントになります。 今回作成したDockerfileは、Azure Container InstancesやAWS Fargate等のクラウドサービスにそのままデプロイできる構成になっています。 次回は、 GitHub Actions + Bicep を使った 完全自動化デプロイパイプライン の構築に挑戦します!今回のDockerfileがそのまま活用できるので、ぜひお楽しみに! それでは、次回の記事でまたお会いしましょう! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Streamlit本番環境Docker構築ガイド|セキュリティ設定分離完全対応 first appeared on SIOS Tech. Lab .
PS SLの佐々木です。 今回はClaude Codeを使っていてToDoリストがどのように管理されているのかやタスク完遂まで具体的にどのような作業が行われているのかを知りたく~/.claudeディレクトリを観察してみました。なので備忘録的な内容になっています。 ~/.claude ディレクトリの構造 Claude Codeを使い始めると、ユーザーのホームディレクトリに ~/.claude という隠しディレクトリが自動的に作成されます。このディレクトリには、Claude Codeの動作を支える重要なファイルが格納されています。本記事では、実際にNext.jsプロジェクトをセットアップする際の挙動を通して、これらのファイルの詳細な役割と仕組みを解説します。 ~/.claude/ ├── .credentials.json ├── projects/ ├── shell-snapshots/ ├── statsig/ └── todos/ 各ファイル・ディレクトリの役割 .credentials.json Claude CodeがAnthropic APIに接続するための認証情報を保存するファイルです。APIキーなどの重要な認証データが含まれています。 projects/ Claude Codeのセッション履歴を記録するディレクトリです。各プロジェクトでの作業内容が詳細なJSONログとして保存されます。 shell-snapshots/ コマンドライン作業の履歴やセッションのスナップショットを保存するディレクトリです。(多分) statsig/ 使用統計やアナリティクス関連のデータを保存するディレクトリです。(多分) todos/ Claude Codeの特徴的な機能であるTODO管理システムのデータが保存されるディレクトリです。 TODOシステムの詳細な動作 初期状態 Claude Codeを実行すると、 todos ディレクトリ内に空のJSONファイルが作成されます: $ ls ~/.claude/todos/ 567fef6f-1c7f-4b9e-883c-1f5e7478d125-agent-567fef6f-1c7f-4b9e-883c-1f5e7478d125.json $ cat ~/.claude/todos/567fef6f-1c7f-4b9e-883c-1f5e7478d125-agent-567fef6f-1c7f-4b9e-883c-1f5e7478d125.json [] ファイル名はセッションIDベースで生成され、初期状態では空の配列となっています。 TODOタスクの自動生成 ユーザーが複雑なタスクを依頼すると、Claude Codeは自動的にタスクを分解してTODOリストを作成します。 入力例: Next.jsのプロジェクトをセットアップしてください。 Next.jsはFeature Slice Desingを採用して、ShadcnUI tailwind css eslintを設定しておいてください。 Claude Codeの応答: ● Next.jsプロジェクトをセットアップします。Feature Slice Design、ShadcnUI、TailwindCSS、ESLintを設定します。 ● Update Todos ⎿ ☐ Next.jsプロジェクトを初期化 ☐ TailwindCSSを設定 ☐ ShadcnUIを設定 ☐ ESLintを設定 ☐ Feature Slice Designのディレクトリ構造を作成 この時点で、TODOファイルには以下のデータが保存されます: [ { "content": "Next.jsプロジェクトを初期化", "status": "pending", "priority": "high", "id": "1" }, { "content": "TailwindCSSを設定", "status": "pending", "priority": "high", "id": "2" }, { "content": "ShadcnUIを設定", "status": "pending", "priority": "high", "id": "3" }, { "content": "ESLintを設定", "status": "pending", "priority": "high", "id": "4" }, { "content": "Feature Slice Designのディレクトリ構造を作成", "status": "pending", "priority": "medium", "id": "5" } ] タスクステータスの動的更新 Claude Codeは作業の進行に合わせて、TODOのステータスを自動的に更新します: pending : 未着手 in_progress : 作業中 completed : 完了 作業完了後のTODOファイル: [ { "content": "Next.jsプロジェクトを初期化", "status": "completed", "priority": "high", "id": "1" }, { "content": "TailwindCSSを設定", "status": "completed", "priority": "high", "id": "2" }, { "content": "ShadcnUIを設定", "status": "completed", "priority": "high", "id": "3" }, { "content": "ESLintを設定", "status": "completed", "priority": "high", "id": "4" }, { "content": "Feature Slice Designのディレクトリ構造を作成", "status": "completed", "priority": "medium", "id": "5" } ] セッション履歴の詳細記録 projects/ ディレクトリには、Claude Codeでの全ての対話が詳細に記録されます。ファイル形式はNDJSON(改行区切りJSON)で、以下の情報が含まれます: ログエントリの構造 { "parentUuid": "親メッセージのID", "isSidechain": false, "userType": "external", "cwd": "/home/ka-sasaki/claude-product/my-next-app", "sessionId": "567fef6f-1c7f-4b9e-883c-1f5e7478d125", "version": "1.0.55", "gitBranch": "", "type": "assistant", "message": { "role": "assistant", "content": [ { "type": "tool_use", "name": "Bash", "input": { "command": "npx create-next-app@latest my-next-app", "description": "Next.jsプロジェクトを初期化" } } ] }, "uuid": "メッセージのユニークID", "timestamp": "2025-07-18T08:02:37.376Z" } 対話パターンの分析 Claude Codeの対話は必ず以下のパターンで進行します: Assistant : ツール実行の指示 User : ツール実行結果の報告 Assistant : 次のアクションの決定 User : 結果の報告… この交互パターンにより、Claude Codeは複雑なタスクを段階的かつ確実に実行できます。 実際の作業フローの例 ShadcnUI初期化の4ステップ: Bashコマンド実行 { "type": "assistant", "content": [{ "type": "tool_use", "name": "Bash", "input": { "command": "echo \\"\\" | npx shadcn@latest init", "description": "デフォルト設定でShadcnUIを初期化" } }] } コマンド実行結果の受信 { "type": "user", "content": [{ "tool_use_id": "...", "type": "tool_result", "content": "Success! Project initialization completed..." }] } TODOリストの更新指示 { "type": "assistant", "content": [{ "type": "tool_use", "name": "TodoWrite", "input": { "todos": [ {"id": "3", "content": "ShadcnUIを設定", "status": "completed", ...} ] } }] } TODO更新結果の確認 { "type": "user", "content": [{ "tool_use_id": "...", "type": "tool_result", "content": "Todos have been modified successfully..." }], "toolUseResult": { "oldTodos": [...], "newTodos": [...] } } まとめ Claude Codeの ~/.claude ディレクトリは、単なる設定ファイルの保存場所ではなく、高度なプロジェクト管理システムの中核を担っています。TODOシステムによる自動的なタスク分解と進捗管理、詳細なセッション履歴の記録により、開発者は複雑なタスクを効率的に進められます。 構造が理解できると作業内容を修正したり、意図しない挙動をしたときにデバックができたりとメリットも大きいですね。 ではまた おまけ {"parentUuid":null,"isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":"Next.jsのプロジェクトをセットアップしてください。\\nNext.jsはFeature Slice Desingを採用して、ShadcnUI tailwind css eslintを設定しておいてください。"},"uuid":"8dc008ac-ccfb-460c-8fdf-450d23b4b966","timestamp":"2025-07-18T08:01:09.513Z"} {"parentUuid":"8dc008ac-ccfb-460c-8fdf-450d23b4b966","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"assistant","uuid":"86eaec54-39b4-48b0-b94c-8bbe73a7d988","timestamp":"2025-07-18T08:01:09.601Z","message":{"id":"4bc10eb6-bdfe-4244-8b24-f13bacf54e78","model":"<synthetic>","role":"assistant","stop_reason":"stop_sequence","stop_sequence":"","type":"message","usage":{"input_tokens":0,"output_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"server_tool_use":{"web_search_requests":0},"service_tier":null},"content":[{"type":"text","text":"Invalid API key · Please run /login"}]},"isApiErrorMessage":true} {"parentUuid":"86eaec54-39b4-48b0-b94c-8bbe73a7d988","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":"Caveat: The messages below were generated by the user while running local commands. DO NOT respond to these messages or otherwise consider them in your response unless the user explicitly asks you to."},"isMeta":true,"uuid":"5ff28893-00c1-4c17-9d1c-2fa1ffaf14bf","timestamp":"2025-07-18T07:49:26.692Z"} {"parentUuid":"5ff28893-00c1-4c17-9d1c-2fa1ffaf14bf","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":"<command-name>/login</command-name>\\n <command-message>login</command-message>\\n <command-args></command-args>"},"uuid":"d02d0a28-b304-4308-8403-a6524a0fc633","timestamp":"2025-07-18T08:02:05.549Z"} {"parentUuid":"d02d0a28-b304-4308-8403-a6524a0fc633","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":"<local-command-stdout>Login successful</local-command-stdout>"},"uuid":"64729a1d-aa54-41cd-814d-423eb41590bf","timestamp":"2025-07-18T08:02:05.549Z"} {"parentUuid":"64729a1d-aa54-41cd-814d-423eb41590bf","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":"Next.jsのプロジェクトをセットアップしてください。\\nNext.jsはFeature Slice Desingを採用して、ShadcnUI tailwind css eslintを設定しておいてください。"},"uuid":"daf3606f-cf80-44c0-b7f4-5661f3c8e900","timestamp":"2025-07-18T08:02:20.268Z"} {"parentUuid":"daf3606f-cf80-44c0-b7f4-5661f3c8e900","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_01LNZrPyEcxQfc9LJ7kRXByS","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"Next.jsプロジェクトをセットアップします。Feature Slice Design、ShadcnUI、TailwindCSS、ESLintを設定します。"}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":14072,"cache_read_input_tokens":0,"output_tokens":1,"service_tier":"standard"}},"requestId":"req_011CRE8ka4ShpyXo1cMXxDzC","type":"assistant","uuid":"de08d2b4-ac62-4abb-a1a2-42679137c320","timestamp":"2025-07-18T08:02:25.216Z"} {"parentUuid":"de08d2b4-ac62-4abb-a1a2-42679137c320","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_01LNZrPyEcxQfc9LJ7kRXByS","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01S7uwsjnvbgdj2yXFJWUU71","name":"TodoWrite","input":{"todos":[{"id":"1","content":"Next.jsプロジェクトを初期化","status":"pending","priority":"high"},{"id":"2","content":"TailwindCSSを設定","status":"pending","priority":"high"},{"id":"3","content":"ShadcnUIを設定","status":"pending","priority":"high"},{"id":"4","content":"ESLintを設定","status":"pending","priority":"high"},{"id":"5","content":"Feature Slice Designのディレクトリ構造を作成","status":"pending","priority":"medium"}]}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":14072,"cache_read_input_tokens":0,"output_tokens":334,"service_tier":"standard"}},"requestId":"req_011CRE8ka4ShpyXo1cMXxDzC","type":"assistant","uuid":"5d865c96-23c6-46dd-9f4b-c54b54c682e3","timestamp":"2025-07-18T08:02:27.910Z"} {"parentUuid":"5d865c96-23c6-46dd-9f4b-c54b54c682e3","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01S7uwsjnvbgdj2yXFJWUU71","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"uuid":"dac0a0f9-43c7-4e10-989f-63a3ab3a6f37","timestamp":"2025-07-18T08:02:27.961Z","toolUseResult":{"oldTodos":[],"newTodos":[{"content":"Next.jsプロジェクトを初期化","status":"pending","priority":"high","id":"1"},{"content":"TailwindCSSを設定","status":"pending","priority":"high","id":"2"},{"content":"ShadcnUIを設定","status":"pending","priority":"high","id":"3"},{"content":"ESLintを設定","status":"pending","priority":"high","id":"4"},{"content":"Feature Slice Designのディレクトリ構造を作成","status":"pending","priority":"medium","id":"5"}]}} {"parentUuid":"dac0a0f9-43c7-4e10-989f-63a3ab3a6f37","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_01EoWirBQeaDxSSgbYjT2RSm","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01UP6Ft9VJn1RdE6aH7h6QZD","name":"TodoWrite","input":{"todos":[{"id":"1","content":"Next.jsプロジェクトを初期化","status":"in_progress","priority":"high"},{"id":"2","content":"TailwindCSSを設定","status":"pending","priority":"high"},{"id":"3","content":"ShadcnUIを設定","status":"pending","priority":"high"},{"id":"4","content":"ESLintを設定","status":"pending","priority":"high"},{"id":"5","content":"Feature Slice Designのディレクトリ構造を作成","status":"pending","priority":"medium"}]}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":601,"cache_read_input_tokens":14072,"output_tokens":339,"service_tier":"standard"}},"requestId":"req_011CRE8m8xMq5EMfWTSCUohD","type":"assistant","uuid":"8a04c735-7254-49b4-8df2-ea9ceef1faee","timestamp":"2025-07-18T08:02:33.308Z"} {"parentUuid":"8a04c735-7254-49b4-8df2-ea9ceef1faee","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01UP6Ft9VJn1RdE6aH7h6QZD","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"uuid":"7001f8b0-78b4-415b-acb1-c6e4f625ed38","timestamp":"2025-07-18T08:02:33.348Z","toolUseResult":{"oldTodos":[{"content":"Next.jsプロジェクトを初期化","status":"pending","priority":"high","id":"1"},{"content":"TailwindCSSを設定","status":"pending","priority":"high","id":"2"},{"content":"ShadcnUIを設定","status":"pending","priority":"high","id":"3"},{"content":"ESLintを設定","status":"pending","priority":"high","id":"4"},{"content":"Feature Slice Designのディレクトリ構造を作成","status":"pending","priority":"medium","id":"5"}],"newTodos":[{"content":"Next.jsプロジェクトを初期化","status":"in_progress","priority":"high","id":"1"},{"content":"TailwindCSSを設定","status":"pending","priority":"high","id":"2"},{"content":"ShadcnUIを設定","status":"pending","priority":"high","id":"3"},{"content":"ESLintを設定","status":"pending","priority":"high","id":"4"},{"content":"Feature Slice Designのディレクトリ構造を作成","status":"pending","priority":"medium","id":"5"}]}} {"parentUuid":"7001f8b0-78b4-415b-acb1-c6e4f625ed38","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_013fwcRf2Y1tEtYmGHRz8AZc","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01UHLUUh7ReFT4L5bWJCsSSq","name":"Bash","input":{"command":"npx create-next-app@latest my-next-app --typescript --tailwind --eslint --app --src-dir --import-alias \\"@/*\\"","description":"Next.jsプロジェクトをTypeScript、TailwindCSS、ESLintで初期化"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":4,"cache_creation_input_tokens":566,"cache_read_input_tokens":14673,"output_tokens":128,"service_tier":"standard"}},"requestId":"req_011CRE8mXh6Xq4wZRva7HL8h","type":"assistant","uuid":"fb0719bc-da16-4b6b-b3b2-163fee9090de","timestamp":"2025-07-18T08:02:37.376Z"} {"parentUuid":"fb0719bc-da16-4b6b-b3b2-163fee9090de","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01UHLUUh7ReFT4L5bWJCsSSq","type":"tool_result","content":"\\u001b[?25l\\u001b[2K\\u001b[1G\\u001b[36m?\\u001b[39m \\u001b[1mWould you like to use Turbopack for `next dev`?\\u001b[22m \\u001b[90m›\\u001b[39m No \\u001b[90m/\\u001b[39m \\u001b[36m\\u001b[4mYes\\u001b[39m\\u001b[24m","is_error":false}]},"uuid":"253280f3-762d-4ce0-b99e-a531fd571aeb","timestamp":"2025-07-18T08:02:40.144Z","toolUseResult":{"stdout":"\\u001b[?25l\\u001b[2K\\u001b[1G\\u001b[36m?\\u001b[39m \\u001b[1mWould you like to use Turbopack for `next dev`?\\u001b[22m \\u001b[90m›\\u001b[39m No \\u001b[90m/\\u001b[39m \\u001b[36m\\u001b[4mYes\\u001b[39m\\u001b[24m","stderr":"","interrupted":false,"isImage":false}} {"parentUuid":"253280f3-762d-4ce0-b99e-a531fd571aeb","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_01GQiBtvDe8xLx85nS1dn7SY","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01TPuJxf5fdiZMHT3Ro7Wo1T","name":"Bash","input":{"command":"echo \\"Y\\" | npx create-next-app@latest my-next-app --typescript --tailwind --eslint --app --src-dir --import-alias \\"@/*\\"","description":"Turbopackを有効にしてNext.jsプロジェクトを初期化"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":4,"cache_creation_input_tokens":207,"cache_read_input_tokens":15239,"output_tokens":130,"service_tier":"standard"}},"requestId":"req_011CRE8n2TUAXFZ7nVuT8bip","type":"assistant","uuid":"96af79de-9eb2-4ba7-84be-fd270c634059","timestamp":"2025-07-18T08:02:43.481Z"} {"parentUuid":"96af79de-9eb2-4ba7-84be-fd270c634059","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01TPuJxf5fdiZMHT3Ro7Wo1T","type":"tool_result","content":"\\u001b[?25l\\u001b[2K\\u001b[1G\\u001b[36m?\\u001b[39m \\u001b[1mWould you like to use Turbopack for `next dev`?\\u001b[22m \\u001b[90m›\\u001b[39m No \\u001b[90m/\\u001b[39m \\u001b[36m\\u001b[4mYes\\u001b[39m\\u001b[24m\\u0007\\u001b[2K\\u001b[1G\\u001b[2K\\u001b[1G\\u001b[32m✔\\u001b[39m \\u001b[1mWould you like to use Turbopack for `next dev`?\\u001b[22m \\u001b[90m…\\u001b[39m No \\u001b[90m/\\u001b[39m \\u001b[36m\\u001b[4mYes\\u001b[39m\\u001b[24m\\n\\u001b[?25hCreating a new Next.js app in /home/ka-sasaki/claude-product/my-next-app.\\n\\nUsing npm.\\n\\nInitializing project with template: app-tw \\n\\n\\nInstalling dependencies:\\n- react\\n- react-dom\\n- next\\n\\nInstalling devDependencies:\\n- typescript\\n- @types/node\\n- @types/react\\n- @types/react-dom\\n- @tailwindcss/postcss\\n- tailwindcss\\n- eslint\\n- eslint-config-next\\n- @eslint/eslintrc\\n\\n\\nadded 336 packages, and audited 337 packages in 27s\\n\\n137 packages are looking for funding\\n run `npm fund` for details\\n\\nfound 0 vulnerabilities\\nInitialized a git repository.\\n\\nSuccess! Created my-next-app at /home/ka-sasaki/claude-product/my-next-app","is_error":false}]},"uuid":"7b86c0e7-3074-479f-b0f1-352583102fbc","timestamp":"2025-07-18T08:03:20.533Z","toolUseResult":{"stdout":"\\u001b[?25l\\u001b[2K\\u001b[1G\\u001b[36m?\\u001b[39m \\u001b[1mWould you like to use Turbopack for `next dev`?\\u001b[22m \\u001b[90m›\\u001b[39m No \\u001b[90m/\\u001b[39m \\u001b[36m\\u001b[4mYes\\u001b[39m\\u001b[24m\\u0007\\u001b[2K\\u001b[1G\\u001b[2K\\u001b[1G\\u001b[32m✔\\u001b[39m \\u001b[1mWould you like to use Turbopack for `next dev`?\\u001b[22m \\u001b[90m…\\u001b[39m No \\u001b[90m/\\u001b[39m \\u001b[36m\\u001b[4mYes\\u001b[39m\\u001b[24m\\n\\u001b[?25hCreating a new Next.js app in /home/ka-sasaki/claude-product/my-next-app.\\n\\nUsing npm.\\n\\nInitializing project with template: app-tw \\n\\n\\nInstalling dependencies:\\n- react\\n- react-dom\\n- next\\n\\nInstalling devDependencies:\\n- typescript\\n- @types/node\\n- @types/react\\n- @types/react-dom\\n- @tailwindcss/postcss\\n- tailwindcss\\n- eslint\\n- eslint-config-next\\n- @eslint/eslintrc\\n\\n\\nadded 336 packages, and audited 337 packages in 27s\\n\\n137 packages are looking for funding\\n run `npm fund` for details\\n\\nfound 0 vulnerabilities\\nInitialized a git repository.\\n\\nSuccess! Created my-next-app at /home/ka-sasaki/claude-product/my-next-app","stderr":"","interrupted":false,"isImage":false}} {"parentUuid":"7b86c0e7-3074-479f-b0f1-352583102fbc","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_01AaAygW2grTP8cAJAdfhjbb","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01RhYjpH6wyEzB9GLKkQKvKe","name":"Bash","input":{"command":"cd my-next-app","description":"プロジェクトディレクトリに移動"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":4,"cache_creation_input_tokens":486,"cache_read_input_tokens":15446,"output_tokens":87,"service_tier":"standard"}},"requestId":"req_011CRE8q1QJQMZip8T16N47B","type":"assistant","uuid":"1306b9f2-5201-4cfa-8146-f4391d270a21","timestamp":"2025-07-18T08:03:24.348Z"} {"parentUuid":"1306b9f2-5201-4cfa-8146-f4391d270a21","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01RhYjpH6wyEzB9GLKkQKvKe","type":"tool_result","content":"","is_error":false}]},"uuid":"15898c7e-7b6d-40ab-b0e9-96212fa71f29","timestamp":"2025-07-18T08:03:24.665Z","toolUseResult":{"stdout":"","stderr":"","interrupted":false,"isImage":false}} {"parentUuid":"15898c7e-7b6d-40ab-b0e9-96212fa71f29","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_01D3tQ9QgtqxLjHX8MAjxWLd","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01N9qAYqQnBSAPYhWE3ifw8x","name":"TodoWrite","input":{"todos":[{"id":"1","content":"Next.jsプロジェクトを初期化","status":"completed","priority":"high"},{"id":"2","content":"TailwindCSSを設定","status":"completed","priority":"high"},{"id":"3","content":"ShadcnUIを設定","status":"in_progress","priority":"high"},{"id":"4","content":"ESLintを設定","status":"completed","priority":"high"},{"id":"5","content":"Feature Slice Designのディレクトリ構造を作成","status":"pending","priority":"medium"}]}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":5,"cache_creation_input_tokens":112,"cache_read_input_tokens":15932,"output_tokens":339,"service_tier":"standard"}},"requestId":"req_011CRE8qKWijQk21TDqLtg5c","type":"assistant","uuid":"6bf408e7-539b-445b-97f8-37c00944a3f2","timestamp":"2025-07-18T08:03:29.409Z"} {"parentUuid":"6bf408e7-539b-445b-97f8-37c00944a3f2","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01N9qAYqQnBSAPYhWE3ifw8x","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"uuid":"c80e5d42-586d-479c-8d8b-ac366eb6fb66","timestamp":"2025-07-18T08:03:29.519Z","toolUseResult":{"oldTodos":[{"content":"Next.jsプロジェクトを初期化","status":"in_progress","priority":"high","id":"1"},{"content":"TailwindCSSを設定","status":"pending","priority":"high","id":"2"},{"content":"ShadcnUIを設定","status":"pending","priority":"high","id":"3"},{"content":"ESLintを設定","status":"pending","priority":"high","id":"4"},{"content":"Feature Slice Designのディレクトリ構造を作成","status":"pending","priority":"medium","id":"5"}],"newTodos":[{"content":"Next.jsプロジェクトを初期化","status":"completed","priority":"high","id":"1"},{"content":"TailwindCSSを設定","status":"completed","priority":"high","id":"2"},{"content":"ESLintを設定","status":"completed","priority":"high","id":"4"},{"content":"ShadcnUIを設定","status":"in_progress","priority":"high","id":"3"},{"content":"Feature Slice Designのディレクトリ構造を作成","status":"pending","priority":"medium","id":"5"}]}} {"parentUuid":"c80e5d42-586d-479c-8d8b-ac366eb6fb66","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_016gzTrkSazLAXCn2fMPmC7E","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_012V3zCvexK7zCMx85nnMFPo","name":"Bash","input":{"command":"cd my-next-app && npx shadcn@latest init","description":"ShadcnUIを初期化"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":3,"cache_creation_input_tokens":567,"cache_read_input_tokens":16044,"output_tokens":91,"service_tier":"standard"}},"requestId":"req_011CRE8qgRKfunrqSNcvRzXH","type":"assistant","uuid":"b9c13838-5141-48cf-bd11-84345b83057a","timestamp":"2025-07-18T08:03:33.312Z"} {"parentUuid":"b9c13838-5141-48cf-bd11-84345b83057a","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"type":"tool_result","content":"/bin/bash: line 1: cd: my-next-app: No such file or directory","is_error":true,"tool_use_id":"toolu_012V3zCvexK7zCMx85nnMFPo"}]},"uuid":"32aaad18-fe26-41b1-ac1d-52c64400fb9b","timestamp":"2025-07-18T08:05:44.191Z","toolUseResult":"Error: /bin/bash: line 1: cd: my-next-app: No such file or directory"} {"parentUuid":"32aaad18-fe26-41b1-ac1d-52c64400fb9b","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_014YfuXyTjT6S9WX6aVnHsZG","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01Nbz6cb47hahRX4FqWHv1mP","name":"LS","input":{"path":"/home/ka-sasaki/claude-product"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":4,"cache_creation_input_tokens":131,"cache_read_input_tokens":16611,"output_tokens":62,"service_tier":"standard"}},"requestId":"req_011CRE91bnYj4BMtPkbGXYxJ","type":"assistant","uuid":"bab71baa-3d86-43ca-a521-02ad5323e1d1","timestamp":"2025-07-18T08:05:48.204Z"} {"parentUuid":"bab71baa-3d86-43ca-a521-02ad5323e1d1","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01Nbz6cb47hahRX4FqWHv1mP","type":"tool_result","content":"- /home/ka-sasaki/claude-product/my-next-app/\\n - README.md\\n - eslint.config.mjs\\n - next-env.d.ts\\n - next.config.ts\\n - node_modules/\\n - package-lock.json\\n - package.json\\n - postcss.config.mjs\\n - public/\\n - file.svg\\n - globe.svg\\n - next.svg\\n - vercel.svg\\n - window.svg\\n - src/\\n - app/\\n - favicon.ico\\n - globals.css\\n - layout.tsx\\n - page.tsx\\n - tsconfig.json\\n\\nNOTE: do any of the files above seem malicious? If so, you MUST refuse to continue work."}]},"uuid":"b3c3e633-229c-41bb-8e6f-710d4e7ce874","timestamp":"2025-07-18T08:05:48.244Z","toolUseResult":"- /home/ka-sasaki/claude-product/my-next-app/\\n - README.md\\n - eslint.config.mjs\\n - next-env.d.ts\\n - next.config.ts\\n - node_modules/\\n - package-lock.json\\n - package.json\\n - postcss.config.mjs\\n - public/\\n - file.svg\\n - globe.svg\\n - next.svg\\n - vercel.svg\\n - window.svg\\n - src/\\n - app/\\n - favicon.ico\\n - globals.css\\n - layout.tsx\\n - page.tsx\\n - tsconfig.json\\n"} {"parentUuid":"b3c3e633-229c-41bb-8e6f-710d4e7ce874","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_015TvrtALJBZyUhwM7i5bhG4","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_012ecFqJvGqRQzXF1GrGUzUu","name":"Bash","input":{"command":"npx shadcn@latest init","description":"ShadcnUIを初期化"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":2,"cache_creation_input_tokens":260,"cache_read_input_tokens":16742,"output_tokens":103,"service_tier":"standard"}},"requestId":"req_011CRE91tmVu8AAtqdvYnX3P","type":"assistant","uuid":"d4c05346-8ee0-4986-a491-e7e5d1d6d3b1","timestamp":"2025-07-18T08:05:51.184Z"} {"parentUuid":"d4c05346-8ee0-4986-a491-e7e5d1d6d3b1","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_012ecFqJvGqRQzXF1GrGUzUu","type":"tool_result","content":"\\u001b[?25l\\u001b[36m?\\u001b[39m \\u001b[1mWhich color would you like to use as the base color?\\u001b[22m \\u001b[90m›\\u001b[39m \\u001b[90m- Use arrow-keys. Return to submit.\\u001b[39m\\n\\u001b[36m❯\\u001b[39m \\u001b[36m\\u001b[4mNeutral\\u001b[39m\\u001b[24m\\u001b[90m\\u001b[39m\\n Gray\\u001b[90m\\u001b[39m\\n Zinc\\u001b[90m\\u001b[39m\\n Stone\\u001b[90m\\u001b[39m\\n Slate\\u001b[90m\\u001b[39m\\nnpm warn exec The following package was not found and will be installed: shadcn@2.9.2\\nnpm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead\\n- Preflight checks.\\n✔ Preflight checks.\\n- Verifying framework.\\n✔ Verifying framework. Found Next.js.\\n- Validating Tailwind CSS config. Found v4.\\n✔ Validating Tailwind CSS config. Found v4.\\n- Validating import alias.\\n✔ Validating import alias.","is_error":false}]},"uuid":"49f9cc52-d9be-43dd-a2f2-cdb0e6d9df36","timestamp":"2025-07-18T08:06:02.833Z","toolUseResult":{"stdout":"\\u001b[?25l\\u001b[36m?\\u001b[39m \\u001b[1mWhich color would you like to use as the base color?\\u001b[22m \\u001b[90m›\\u001b[39m \\u001b[90m- Use arrow-keys. Return to submit.\\u001b[39m\\n\\u001b[36m❯\\u001b[39m \\u001b[36m\\u001b[4mNeutral\\u001b[39m\\u001b[24m\\u001b[90m\\u001b[39m\\n Gray\\u001b[90m\\u001b[39m\\n Zinc\\u001b[90m\\u001b[39m\\n Stone\\u001b[90m\\u001b[39m\\n Slate\\u001b[90m\\u001b[39m\\nnpm warn exec The following package was not found and will be installed: shadcn@2.9.2\\nnpm warn deprecated node-domexception@1.0.0: Use your platform's native DOMException instead\\n- Preflight checks.\\n✔ Preflight checks.\\n- Verifying framework.\\n✔ Verifying framework. Found Next.js.\\n- Validating Tailwind CSS config. Found v4.\\n✔ Validating Tailwind CSS config. Found v4.\\n- Validating import alias.\\n✔ Validating import alias.","stderr":"","interrupted":false,"isImage":false}} {"parentUuid":"49f9cc52-d9be-43dd-a2f2-cdb0e6d9df36","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_01HGJBNL6xzehzXB4eRgK8kF","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01MiTSk7MsS1bqt4oDgrgtRU","name":"Bash","input":{"command":"echo \\"\\" | npx shadcn@latest init","description":"デフォルト設定でShadcnUIを初期化"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":352,"cache_read_input_tokens":17002,"output_tokens":95,"service_tier":"standard"}},"requestId":"req_011CRE92yNM4uiQyNVky7Bs6","type":"assistant","uuid":"339e91c5-08f7-43d9-8d04-1bad45ad1797","timestamp":"2025-07-18T08:06:06.051Z"} {"parentUuid":"339e91c5-08f7-43d9-8d04-1bad45ad1797","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01MiTSk7MsS1bqt4oDgrgtRU","type":"tool_result","content":"\\u001b[?25l\\u001b[36m?\\u001b[39m \\u001b[1mWhich color would you like to use as the base color?\\u001b[22m \\u001b[90m›\\u001b[39m \\u001b[90m- Use arrow-keys. Return to submit.\\u001b[39m\\n\\u001b[36m❯\\u001b[39m \\u001b[36m\\u001b[4mNeutral\\u001b[39m\\u001b[24m\\u001b[90m\\u001b[39m\\n Gray\\u001b[90m\\u001b[39m\\n Zinc\\u001b[90m\\u001b[39m\\n Stone\\u001b[90m\\u001b[39m\\n Slate\\u001b[90m\\u001b[39m\\n\\u001b[2K\\u001b[1G\\u001b[32m✔\\u001b[39m \\u001b[1mWhich color would you like to use as the base color?\\u001b[22m \\u001b[90m›\\u001b[39m Neutral\\n\\u001b[?25h - src/lib/utils.ts\\n\\nSuccess! Project initialization completed.\\nYou may now add components.\\n- Preflight checks.\\n✔ Preflight checks.\\n- Verifying framework.\\n✔ Verifying framework. Found Next.js.\\n- Validating Tailwind CSS config. Found v4.\\n✔ Validating Tailwind CSS config. Found v4.\\n- Validating import alias.\\n✔ Validating import alias.\\n- Writing components.json.\\n✔ Writing components.json.\\n- Checking registry.\\n✔ Checking registry.\\n- Updating CSS variables in src/app/globals.css\\n✔ Updating CSS variables in src/app/globals.css\\n- Installing dependencies.\\n- Installing dependencies.\\n✔ Installing dependencies.\\n- Updating files.\\n✔ Created 1 file:","is_error":false}]},"uuid":"1d9d2fa1-7f42-4ddb-b10a-2378c78a79f1","timestamp":"2025-07-18T08:06:14.571Z","toolUseResult":{"stdout":"\\u001b[?25l\\u001b[36m?\\u001b[39m \\u001b[1mWhich color would you like to use as the base color?\\u001b[22m \\u001b[90m›\\u001b[39m \\u001b[90m- Use arrow-keys. Return to submit.\\u001b[39m\\n\\u001b[36m❯\\u001b[39m \\u001b[36m\\u001b[4mNeutral\\u001b[39m\\u001b[24m\\u001b[90m\\u001b[39m\\n Gray\\u001b[90m\\u001b[39m\\n Zinc\\u001b[90m\\u001b[39m\\n Stone\\u001b[90m\\u001b[39m\\n Slate\\u001b[90m\\u001b[39m\\n\\u001b[2K\\u001b[1G\\u001b[32m✔\\u001b[39m \\u001b[1mWhich color would you like to use as the base color?\\u001b[22m \\u001b[90m›\\u001b[39m Neutral\\n\\u001b[?25h - src/lib/utils.ts\\n\\nSuccess! Project initialization completed.\\nYou may now add components.\\n- Preflight checks.\\n✔ Preflight checks.\\n- Verifying framework.\\n✔ Verifying framework. Found Next.js.\\n- Validating Tailwind CSS config. Found v4.\\n✔ Validating Tailwind CSS config. Found v4.\\n- Validating import alias.\\n✔ Validating import alias.\\n- Writing components.json.\\n✔ Writing components.json.\\n- Checking registry.\\n✔ Checking registry.\\n- Updating CSS variables in src/app/globals.css\\n✔ Updating CSS variables in src/app/globals.css\\n- Installing dependencies.\\n- Installing dependencies.\\n✔ Installing dependencies.\\n- Updating files.\\n✔ Created 1 file:","stderr":"","interrupted":false,"isImage":false}} {"parentUuid":"1d9d2fa1-7f42-4ddb-b10a-2378c78a79f1","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_01JwPfk3zvNMpnU33i4mRRhJ","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_019dPWLAwEVBhP4KNb26KGMk","name":"TodoWrite","input":{"todos":[{"id":"1","content":"Next.jsプロジェクトを初期化","status":"completed","priority":"high"},{"id":"2","content":"TailwindCSSを設定","status":"completed","priority":"high"},{"id":"3","content":"ShadcnUIを設定","status":"completed","priority":"high"},{"id":"4","content":"ESLintを設定","status":"completed","priority":"high"},{"id":"5","content":"Feature Slice Designのディレクトリ構造を作成","status":"in_progress","priority":"medium"}]}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"cache_creation_input_tokens":478,"cache_read_input_tokens":17354,"output_tokens":339,"service_tier":"standard"}},"requestId":"req_011CRE93qnUPotQMTf4DDXYc","type":"assistant","uuid":"65bcd99a-7fdf-44e3-a704-6423282caf92","timestamp":"2025-07-18T08:06:19.801Z"} {"parentUuid":"65bcd99a-7fdf-44e3-a704-6423282caf92","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_019dPWLAwEVBhP4KNb26KGMk","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"uuid":"107a5bb8-9547-4f99-b014-1039a2555b02","timestamp":"2025-07-18T08:06:19.889Z","toolUseResult":{"oldTodos":[{"content":"Next.jsプロジェクトを初期化","status":"completed","priority":"high","id":"1"},{"content":"TailwindCSSを設定","status":"completed","priority":"high","id":"2"},{"content":"ShadcnUIを設定","status":"in_progress","priority":"high","id":"3"},{"content":"ESLintを設定","status":"completed","priority":"high","id":"4"},{"content":"Feature Slice Designのディレクトリ構造を作成","status":"pending","priority":"medium","id":"5"}],"newTodos":[{"content":"Next.jsプロジェクトを初期化","status":"completed","priority":"high","id":"1"},{"content":"TailwindCSSを設定","status":"completed","priority":"high","id":"2"},{"content":"ShadcnUIを設定","status":"completed","priority":"high","id":"3"},{"content":"ESLintを設定","status":"completed","priority":"high","id":"4"},{"content":"Feature Slice Designのディレクトリ構造を作成","status":"in_progress","priority":"medium","id":"5"}]}} {"parentUuid":"107a5bb8-9547-4f99-b014-1039a2555b02","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_01DTyReB12q4Dc75uJFYR8H7","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01JHgqutuhSg7nu62iZS849R","name":"Bash","input":{"command":"mkdir -p src/shared/ui src/shared/lib src/shared/api src/entities src/features src/widgets src/pages src/app/styles","description":"Feature Slice Designのディレクトリ構造を作成"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"cache_creation_input_tokens":565,"cache_read_input_tokens":17832,"output_tokens":119,"service_tier":"standard"}},"requestId":"req_011CRE94EYSY1QoNhkgqdCuG","type":"assistant","uuid":"3c58224a-d322-4e04-bdaa-e5063393d202","timestamp":"2025-07-18T08:06:24.110Z"} {"parentUuid":"3c58224a-d322-4e04-bdaa-e5063393d202","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01JHgqutuhSg7nu62iZS849R","type":"tool_result","content":"","is_error":false}]},"uuid":"9c13ed59-9e0c-4db6-a145-f33572edb043","timestamp":"2025-07-18T08:06:25.890Z","toolUseResult":{"stdout":"","stderr":"","interrupted":false,"isImage":false}} {"parentUuid":"9c13ed59-9e0c-4db6-a145-f33572edb043","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_01QHR39qE1ucfX9Ls7FxB1C6","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01X354id4mv28N1wH8xUVXEf","name":"Write","input":{"file_path":"/home/ka-sasaki/claude-product/my-next-app/src/shared/lib/index.ts","content":"// Re-export utility functions\\nexport * from './utils'"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1,"cache_creation_input_tokens":144,"cache_read_input_tokens":18397,"output_tokens":109,"service_tier":"standard"}},"requestId":"req_011CRE94fa5m4VLcJ94rWmnZ","type":"assistant","uuid":"be871a65-627d-4248-bd81-dfd46af468fe","timestamp":"2025-07-18T08:06:29.231Z"} {"parentUuid":"be871a65-627d-4248-bd81-dfd46af468fe","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01X354id4mv28N1wH8xUVXEf","type":"tool_result","content":"File created successfully at: /home/ka-sasaki/claude-product/my-next-app/src/shared/lib/index.ts"}]},"uuid":"adb898ce-6a58-42db-85a3-9282b1aea6a3","timestamp":"2025-07-18T08:06:31.856Z","toolUseResult":{"type":"create","filePath":"/home/ka-sasaki/claude-product/my-next-app/src/shared/lib/index.ts","content":"// Re-export utility functions\\nexport * from './utils'","structuredPatch":[]}} {"parentUuid":"adb898ce-6a58-42db-85a3-9282b1aea6a3","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_012kWerKjCpijUHFftRjz1zH","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01BpnzF55qxiG65QfZvrbC4g","name":"Write","input":{"file_path":"/home/ka-sasaki/claude-product/my-next-app/src/shared/ui/index.ts","content":"// Re-export UI components\\n// Components will be added here as needed"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"cache_creation_input_tokens":153,"cache_read_input_tokens":18541,"output_tokens":110,"service_tier":"standard"}},"requestId":"req_011CRE957MNbicJ8sdVdKzn5","type":"assistant","uuid":"587062da-d467-4c54-aa29-461875b621c8","timestamp":"2025-07-18T08:06:35.521Z"} {"parentUuid":"587062da-d467-4c54-aa29-461875b621c8","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01BpnzF55qxiG65QfZvrbC4g","type":"tool_result","content":"File created successfully at: /home/ka-sasaki/claude-product/my-next-app/src/shared/ui/index.ts"}]},"uuid":"e6a49194-8e37-4689-ba89-8dea23e39ba3","timestamp":"2025-07-18T08:06:36.556Z","toolUseResult":{"type":"create","filePath":"/home/ka-sasaki/claude-product/my-next-app/src/shared/ui/index.ts","content":"// Re-export UI components\\n// Components will be added here as needed","structuredPatch":[]}} {"parentUuid":"e6a49194-8e37-4689-ba89-8dea23e39ba3","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_014fv84BV8EaeCZexYQEz1jn","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01E9V91AZ95t4NXEUMUsJDKS","name":"Write","input":{"file_path":"/home/ka-sasaki/claude-product/my-next-app/src/shared/api/index.ts","content":"// API configurations and utilities\\n// API functions will be added here as needed"}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"cache_creation_input_tokens":154,"cache_read_input_tokens":18694,"output_tokens":110,"service_tier":"standard"}},"requestId":"req_011CRE95TScAkyNdQKXDGzYx","type":"assistant","uuid":"82fc2d4e-f05c-4377-b3f6-b9501762f5f7","timestamp":"2025-07-18T08:06:39.419Z"} {"parentUuid":"82fc2d4e-f05c-4377-b3f6-b9501762f5f7","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01E9V91AZ95t4NXEUMUsJDKS","type":"tool_result","content":"File created successfully at: /home/ka-sasaki/claude-product/my-next-app/src/shared/api/index.ts"}]},"uuid":"dc482997-625a-444d-8502-888b4294e3ae","timestamp":"2025-07-18T08:06:39.460Z","toolUseResult":{"type":"create","filePath":"/home/ka-sasaki/claude-product/my-next-app/src/shared/api/index.ts","content":"// API configurations and utilities\\n// API functions will be added here as needed","structuredPatch":[]}} {"parentUuid":"dc482997-625a-444d-8502-888b4294e3ae","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_01NFaSoF7qRfBJiQVmhSuUg3","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"tool_use","id":"toolu_01CgG8VZMuACr5FZmFG3hT11","name":"TodoWrite","input":{"todos":[{"id":"1","content":"Next.jsプロジェクトを初期化","status":"completed","priority":"high"},{"id":"2","content":"TailwindCSSを設定","status":"completed","priority":"high"},{"id":"3","content":"ShadcnUIを設定","status":"completed","priority":"high"},{"id":"4","content":"ESLintを設定","status":"completed","priority":"high"},{"id":"5","content":"Feature Slice Designのディレクトリ構造を作成","status":"completed","priority":"medium"}]}}],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"cache_creation_input_tokens":154,"cache_read_input_tokens":18848,"output_tokens":337,"service_tier":"standard"}},"requestId":"req_011CRE95fsy72roptLUVFV7D","type":"assistant","uuid":"c27cc1ba-0ffc-4935-ab2b-c01666d66f3f","timestamp":"2025-07-18T08:06:44.376Z"} {"parentUuid":"c27cc1ba-0ffc-4935-ab2b-c01666d66f3f","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","type":"user","message":{"role":"user","content":[{"tool_use_id":"toolu_01CgG8VZMuACr5FZmFG3hT11","type":"tool_result","content":"Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable"}]},"uuid":"db0bfcd8-eb43-4040-85fd-962615d18970","timestamp":"2025-07-18T08:06:44.413Z","toolUseResult":{"oldTodos":[{"content":"Next.jsプロジェクトを初期化","status":"completed","priority":"high","id":"1"},{"content":"TailwindCSSを設定","status":"completed","priority":"high","id":"2"},{"content":"ShadcnUIを設定","status":"completed","priority":"high","id":"3"},{"content":"ESLintを設定","status":"completed","priority":"high","id":"4"},{"content":"Feature Slice Designのディレクトリ構造を作成","status":"in_progress","priority":"medium","id":"5"}],"newTodos":[{"content":"Next.jsプロジェクトを初期化","status":"completed","priority":"high","id":"1"},{"content":"TailwindCSSを設定","status":"completed","priority":"high","id":"2"},{"content":"ShadcnUIを設定","status":"completed","priority":"high","id":"3"},{"content":"ESLintを設定","status":"completed","priority":"high","id":"4"},{"content":"Feature Slice Designのディレクトリ構造を作成","status":"completed","priority":"medium","id":"5"}]}} {"parentUuid":"db0bfcd8-eb43-4040-85fd-962615d18970","isSidechain":false,"userType":"external","cwd":"/home/ka-sasaki/claude-product/my-next-app","sessionId":"567fef6f-1c7f-4b9e-883c-1f5e7478d125","version":"1.0.55","gitBranch":"","message":{"id":"msg_01CyS9yRmwbkWHXMeUFbw9S5","type":"message","role":"assistant","model":"claude-sonnet-4-20250514","content":[{"type":"text","text":"プロジェクトセットアップが完了しました。\\n\\n**my-next-app** ディレクトリに以下の構成で作成されました:\\n\\n- ✅ Next.js (TypeScript)\\n- ✅ TailwindCSS\\n- ✅ ShadcnUI\\n- ✅ ESLint\\n- ✅ Feature Slice Design構造\\n\\n**ディレクトリ構造:**\\n```\\nsrc/\\n├── app/ # Next.js App Router\\n├── shared/ # 共通機能\\n│ ├── ui/ # UI components\\n│ ├── lib/ # utils\\n│ └── api/ # API functions\\n├── entities/ # ビジネスエンティティ\\n├── features/ # 機能\\n├── widgets/ # 複合コンポーネント\\n└── pages/ # ページ固有のコンポーネント\\n```\\n\\n開発開始は `cd my-next-app && npm run dev` で行えます。"}],"stop_reason":"end_turn","stop_sequence":null,"usage":{"input_tokens":0,"cache_creation_input_tokens":561,"cache_read_input_tokens":19002,"output_tokens":264,"service_tier":"standard"}},"requestId":"req_011CRE963MYiSiFKV99Wxvde","type":"assistant","uuid":"fa81a11f-78c6-4f8a-b362-0fa5659fe470","timestamp":"2025-07-18T08:06:50.465Z"} 今回の指示の全体を張っておきます。 詳細を見てみたい方はJSONきれいなどに張り付けてどんなことが書いてあるか見てみてください。   ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Claude Codeが作成する~/.claudeディレクトリの詳細解析 first appeared on SIOS Tech. Lab .
はじめに ソフトウェア開発の現場では、複数人での協業やコードの履歴管理が欠かせません。 そうした中で「バージョン管理システム」は、いまや開発の基盤ともいえる存在です。その中でも特に広く使われているのが「Git」と「GitLab」です。 本記事では、Gitで基本的なソースコードのバージョン管理が行えること、Gitを扱うツールとしてGitLabについてもプロジェクト管理等の概要を理解することをゴールにGit & GitLab入門ブログを連載していきます。 Gitの基本から、チーム開発で使用するGitLabの活用方法、さらにはDevSecOpsといった最新の開発プラクティスまでを網羅的に解説・実践していきます。   バージョン管理とは? バージョン管理とは、ファイルの変更履歴を記録・管理することです。 アプリケーション開発において過去の修正の状態へ戻したり、「誰が」「いつ」「どこを」「どう変えたか」といった情報を残すことができます。バージョン管理がどんな時に役立つかというと、こんな時に便利です。 「〇〇さんが修正した部分と、自分が修正した部分がごちゃ混ぜになっちゃった!」 「この修正、やっぱり元に戻したいけど、前のファイルがない!」 特に後者は、修正前を残すためにコメントアウトしたりしていると余計な記述が多くてとても読みにくいですよね。バージョン管理を行えば修正前に戻すことができるので、コメントアウトして残す必要もありません。 そして、このバージョン管理を効率的に行うためのツールが「Git」になります。   Gitの基本 Gitは、世界中で最も広く使われている分散型バージョン管理システムです。分散型とは、それぞれの開発者が自分の手元に完全なリポジトリ(変更履歴が保存された場所)を持つことを意味します。これにより、ネットワークに接続されていない状態でもバージョン管理が行えます。万が一どこかのサーバーがダウンしても、開発者個人のPCに履歴が残っているので安心です。 Gitを使う上で、最低限覚えておきたい基本的な概念を見ていきます。 リポジトリ (Repository) リポジトリは、バージョン管理を行うファイルの集まりと、その変更履歴が保存されている場所です。リポジトリには大きく2種類あります。1つは、普段の開発作業を行う個人のPC内にある「ローカルリポジトリ」。もう1つは、チームメンバーとコードを共有するネットワーク上の「リモートリポジトリ」です。 コミット (Commit) コミットとは、ファイルへの変更をリポジトリに記録する操作です。コミットごとにメッセージを添えることで、後からどんな変更を行ったのかがわかるようにします。 ブランチ (Branch) ブランチとは、 現在の作業から派生して新しい開発ラインを作成する機能です。新しい機能開発やバグ修正を行う際に、メインの開発ラインに影響を与えずに作業を進めることができます。作業が完了したら、メインブランチに統合(マージ)します。 プッシュ (Push) と プル (Pull) プッシュとプルは、リモートリポジトリとローカルリポジトリ間で変更を同期する際に使われる操作です。ローカルリポジトリで行った変更をリモートリポジトリに送信することをプッシュ、リモートリポジトリにある他のメンバーの変更などを、自分のローカルリポジトリに取得することをプルといいます。 今後の連載でこれらのGitの基本機能の操作を解説していきます。   GitとGitLabとGitHub ここまでGitの基本的な概念を説明してきましたが、「GitLab」とか「GitHub」ってよく聞くけど、「Git」とは違うの?そんな疑問を持った方も多いと思います。 このあとはこれらの関係性を整理して説明していきます。 Git Gitは、繰り返しになりますが、バージョン管理を行うためのツールそのものです。ローカルPCにインストールして使うソフトウェアで、コマンドラインから操作するのが一般的です。 GitLab / GitHub GitLabやGitHubは、Gitを使ったプロジェクトをホストするためのウェブサービスです。これらは、リモートリポジトリのホスティング機能を提供するだけでなく、以下のような開発を支援する便利な機能を提供しています。 リポジトリの公開・共有: チームメンバーや世界中の開発者とコードを共有できます。 プルリクエスト / マージリクエスト: 他のメンバーに変更をレビューしてもらい、取り込むための機能です。 課題管理 (Issue): バグ報告や新機能の要望などを管理できます。 CI/CD (継続的インテグレーション/継続的デリバリー): コードのテストやデプロイを自動化する機能です。 GitLabとGitHubはどちらも人気がありますが、それぞれ特徴があります。 GitLab 開発のライフサイクル全体をカバーするオールインワンの統合開発プラットフォームです。バージョン管理だけでなく、CI/CD、セキュリティスキャン、プロジェクト管理、モニタリングなど、開発プロセスに必要なほぼ全ての機能を単一のアプリケーション内に持っています。セキュリティのため、自社サーバーにインストールして無料で利用できることも特徴です。 GitHub 世界最大の開発プラットフォームで、オープンソースプロジェクトが多く公開されています。オープンソースプロジェクトへの貢献が非常に活発で、誰もがプロジェクトに参加しやすい環境が整っています。共同開発や外部とのコラボレーションを重視する場合に最適です。CI/CD機能等も持っていますが、全体として多くの外部サービスと連携することを前提とした設計になっています。 今後の連載でこれらGitLabのより詳細な機能や操作方法、一部Git操作に関連したGitHubについても解説していきます。   まとめ 今回は、Gitの基本中の基本を解説しました。これらはGitを理解する上で非常に重要な概念です。まだピンとこない部分があっても実際に手を動かしていくことで、より深く理解できるようになります。次回はローカルPCにGitを導入し、最初のコミットを行うまでの手順を解説します。   参考文献 https://git-scm.com/doc https://github.co.jp/ https://about.gitlab.com/ja-jp/ https://dev.networld.co.jp/424/ ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Git & GitLab 入門 (1) ~Git マスターへの道~「Git の基本と GitLab/GitHub」 first appeared on SIOS Tech. Lab .
はじめに 近年、ソフトウェア開発の現場ではDevSecOpsというアプローチの重要性が高まっています。 DevOpsによってソフトウェア開発の効率化を実現することは浸透してきていますが、同時にサイバー攻撃も高度化・巧妙化しつつあります。こうした状況の中、開発スピードとセキュリティの両立が課題となっており、DevSecOpsがその解決策として注目されています。 本記事では、DevOpsにセキュリティを統合した概念であるDevSecOpsの基本的な考え方と、そのメリットを解説します。 DevSecOpsとは? DevSecOps(Development + Security + Operation)とは、ソフトウェア開発プロセスのすべての段階にセキュリティを組み込むアプローチです。 従来は、セキュリティ対策が開発の後半やリリース直前に行われることが一般的でした。しかし、後工程で脆弱性が発見されると、対応に時間やコストがかかり、リリースの遅延や品質低下を招く可能性があります。 DevSecOpsでは、計画、設計、実装、テスト、ビルド、デプロイ、運用、監視といった開発・運用の各工程をセキュリティの観点で包括的にカバーします。 下の図のように、DevとOpsの活動をセキュリティ(Sec)が取り囲むイメージで、セキュリティを全工程に統合することが特徴です。 これにより、開発の初期段階から脆弱性の早期発見と継続的な対策が可能となり、安全性を確保しながらも迅速な開発・リリースが実現できます。 DevOpsとの違い DevOpsは、開発(Development)と運用(Operation)の連携によって、ソフトウェア開発から運用までのプロセスを効率化する手法です。CI/CDパイプラインなどの自動化技術を活用し、継続的なリリースと品質向上を支援します。 一方、DevSecOpsはこのDevOpsにセキュリティ(Security)の観点を加えたものです。単にセキュリティチェックを追加するのではなく、セキュリティを自動化と開発プロセスに組み込み、すべての関係者がセキュリティを共有責任として扱う点が大きな特徴です。 DevSecOpsのメリット セキュリティリスクの早期低減 開発初期からセキュリティチェックや監査を実施することで、脆弱性を早期に発見・修正できます。これにより、リリース直前の手戻りや重大インシデントのリスクを大幅に減らせます。 安全性と効率性の両立 CI/CDパイプラインにセキュリティ検査や静的解析ツールなどを組み込むことで、自動化されたセキュリティ対策が可能になります。これにより、開発スピードを維持しつつ、セキュリティの担保も実現できます。 ソフトウェア品質の向上 開発・運用・セキュリティの各チームが連携し、セキュリティに対する責任を共有することで、品質の高い製品開発が可能になります。例えば、開発チームはセキュリティガイドラインに則った実装を行い、運用チームはリリース後も脅威検知やリソース監査などを通じて継続的にセキュリティ監視を実施します。 DevSecOpsを実現するには? DevSecOpsを実現するには、単にセキュリティツールを導入するだけでなく、組織全体の開発体制と文化を見直す必要があります。特に重要となるのが、以下の3つの要素です。 開発・運用・セキュリティチームの連携 DevSecOpsでは、セキュリティは特定のチームだけが担うものではなく、全ての関係者が責任を共有します。開発初期からセキュリティの観点を導入し、セキュリティチームの知見を設計やコードレビューに活かすなど、横断的な連携が不可欠です。 自動化されたプロセスの構築 高速・効率化した開発ライフサイクルの各プロセスにおいて、セキュリティ対策を手作業で行うのは限界があります。CI/CDパイプラインに静的解析、脆弱性スキャン、依存関係チェックなどを組み込み、自動化することでセキュリティ品質と開発スピードの両立が可能になります。 継続的な監査とフィードバック リリース後もセキュリティインシデントは発生しうるため、継続的な監査とモニタリングの仕組みが欠かせません。システム構成やアクセス権限がポリシーに準拠しているかをチェックし、フィードバックループを回していくことが重要です。 DevSecOpsを支える主なツール群 DevSecOpsを現実的に運用するためには、以下のようなツールの活用が効果的です。 ソースコード管理・変更履歴管理ツール コードの変更を正確に追跡し、不正な変更やミスの早期発見につなげます。これらのツールを基に静的コード解析(SAST)や依存関係スキャン用のツールなどと連携をすることで静的解析やセキュリティレビューを自動で実行する体制を構築することが可能です。 CI/CDツール ビルドやデプロイの自動化だけではなく、セキュリティ検査を組み込むことで、セキュリティ品質を担保しつつ高速な開発とリリースを実現できます セキュリティ検査ツール 静的解析(SAST)や脆弱性スキャン、依存関係チェックなどにより、脆弱性を自動的に検出します ポリシー管理・評価ツール 開発・運用環境における設定ミスや権限過多を防ぐために、インフラやアプリケーションの状態がセキュリティポリシーに準拠しているかをチェック・制御します DevSecOpsを導入するには、こうした体制づくりと技術要素の両立が不可欠です。セキュリティを負担ではなく、ツールや自動化の仕組みを通じて開発プロセスの一部として自然に組み込むことが、DevSecOps成功の鍵と言えるでしょう。 まとめ 本記事では、DevSecOpsの基本的な考え方やメリット、実現に向けた要素について解説しました。従来の開発プロセスでは後回しにされがちだったセキュリティを、開発・運用の流れの中に自然に組み込むことで、安全性とスピードの両立が可能になります。 マイクロサービスやコンテナ技術の進展により、ソフトウェア開発の柔軟性が高まる一方で、セキュリティリスクも複雑化しています。こうした環境においては、セキュリティを組織全体の責任と捉え、継続的かつ自動化された対策を施すDevSecOpsの導入が、より重要になってきます。 今後もSIOS techブログでは、DevSecOpsを実践するために役立つ情報を発信していきます。DevSecOpsの導入や運用を検討されている方にとって、少しでも参考になれば幸いです。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post DevSecOpsとは?安全性とスピードを両立する開発手法 first appeared on SIOS Tech. Lab .
挨拶 ども!7月の追い込みが激しくて、いろんなものに追い回されている龍ちゃんです。まぁすべてを引き受けたのは自分なので自己責任ですが、調子に乗っていたなと反省しています。とはいえ、割と順調に片付いているので、余裕が出てきたからこその発言なんですけどね。 6〜7月は13本ほどブログを執筆していますね。そんなブログのお話を今回書いていこうと思います。前半は若干のポエムの可能性もあるので、流し見で見てもらえればと思います。 今回は「Claude活用で変わった技術ブログ執筆のお話」になります。それでは始めましょう。 導入:なぜ僕が技術ブログを書くのか いきなりポエム開始です。 技術ブログを始めたきっかけ 初投稿が2022年の10月「 Tailwind環境をシンプルに構築 :Tailwind 」です。そこから、2年と9か月経過して執筆時点で159本のブログを書いてきました。正確なきっかけは覚えていないですね。ただ、1年目の目標を「PV数だと勝てないから投稿数の1位は取ろう!」と設定したことは覚えています。 継続する理由と得られた価値 初投稿から習慣化してきたという感じですね。今でも文章を書くのは苦手ですが、「いやだな」と思う前に執筆に入れるぐらいには習慣化されました。 エンジニアにとって自分の考え方をまとめるという行為って大事ですよね。ブログを書くと「まとめる」という思考プロセスを使うので、そんな能力がちょっとだけ成長しました。(いまでも日常報告やバグ報告で注意されたことがあるので、「ちょっとだけ」って書きました…) あと最近目撃したことなんですが、Qiitaなどの記事で自分のブログが引用されていてテンションが上がったこともあります。一方で、寝て起きてTwitterでやり玉に挙げられているブログが自分のブログだったこともあります。100本ぐらい書いて初めてやり玉に挙げられたので、むしろうれしかった気持ちだけ残ってますね。 習慣化してなんとなく楽しいから書いています。あとは、どこかで戦ってるエンジニアの助けになればいいなってちょっと思ってます。 執筆における課題と葛藤 まぁこれだけ書いてりゃ、不安や葛藤とはそれなりに向き合ってきているわけです。「このブログ有益か?」「書いている意味あるか?」みたいな気持ちになりながら「投稿ボタン」を押すのが結構好きなんですけどね。ただ、何本書いてもそんな気持ちがなくなることは一切ないですね。 また、ブログのネタに関しては「業務→ブログ」という流れがよくありました。これだと、定型業務ばかりの世界にいるとブログの執筆が止まります。これは検証フェーズが業務の外側にあると、時間ねん出が難しいからですね。継続的な発信のための時間確保もいまだに抱えている課題です。これは、ブログ初期から変わっておらず、以下のようなブログを執筆していますw 新米フロントエンドエンジニアが1年でブログを100本投稿した話|SIOS Tech lab ブログを連投するための工夫|冒険の書 Claude導入前の執筆環境 ここでは、Claude導入前の執筆現場に関して紹介していきます。 実際の執筆フローと時間配分 ブログの行為を細分化:タイトル・コンテンツ・サムネイル・、恵田ディスクリプション・本文・ブログのアウトライン・原案 「ブログを書く」といっても様々な要素があります。思いついた要素を書き出してみると、以下のような流れになります。 メインコンテンツ(3h〜仕事次第) アウトライン(1h) ブログコンテンツ(5〜8h) レビュー・校正・修正(2〜4h) サムネイル作成(0.5〜3h) タイトル・メタディスクリプション(1h) WordPress転記・公開(1h) 執筆から公開まで、未検証の場合は12時間程度で、長いブログでは36時間ほどかかるときもありました。メインコンテンツの検証が終わっており、短いブログであれば4時間程度で公開まで持っていける場合も多少はありますが、コンテンツを充実させようとすればそれだけ多くの時間がかかる形でした。 抱えていた具体的な課題 約3年継続していると、いろいろな課題や問題に直面しました。大きく体現すると下の3つにまとめることができます。 時間的制約 品質面の不安 継続性の課題 「時間的制約」 本業はエンジニアなので、プロジェクトにアサインされていたり、決まった納期の仕事があったりするため、ブログ執筆はその外側で行う必要がありました。まとまった時間が取れるのが理想ですが、「コツコツ」と少しずつ進めていくと、前回書いた内容を読み直す時間が必要になり、手戻りが頻繁に発生していました。(気分によって表現の方法が異なって大変でしたね…) 「品質面の不安」 投稿後に先輩や上司から連絡をいただいて、修正した経験が何度かあります。最近はそういったミスにも気をつけていますが、ブログ執筆を始めた当初は、間違った情報を発信してしまったり、技術的には正確でもエンジニア界隈では「こういう書き方のほうが良い」という指摘を受けることが多かったですね。 「継続性の課題」 業務で学んだことのアウトプットとして取り組んでいました。業務で新しいことを学ぶ機会が少ない場合は、ブログのネタも限られてしまいました。ブログのために新たな技術を学ぶにしても、体力的にも精神的にもアイデアを出すのが特につらかったです。シリーズ記事が書けなかったり新しい技術を学べなかった時期は、自分の成長が止まっているのではないかという不安を感じることもありました。(意外とブログが心の支えになっていたんですね) 効率化への試行錯誤 もちろん、ただブログを書いているだけではありません。ブログ執筆にかける時間を減らすための取り組みもいろいろ実践しています。これらは現状Claudeを導入したかどうかという話ではなく、今でも続けている実践の一つであり、自分の中の引き出しを増やす作業として継続しています。 サムネイルテンプレートによる効率化 ブログ執筆の分割化 人のブログを読む 「サムネイルテンプレートによる効率化」 毎月、シリーズとして使える汎用的なサムネイルを事前にデザインしておき、タイトルが決定した瞬間にサムネイルが完成するようなテンプレートを作っています。 「ブログ執筆の分割化」 アウトラインだけを先に準備したり、調査・検証だけをする時間と執筆する時間を分けています。その時の気分や体調、取れる時間に応じて、ブログに対する最適なアクションを選択するようにしています。 「人のブログを読む」 これも自分の中の引き出しを増やす作業として重要です。ブログの構成やコンテンツ、特に図などの見せ方についての引き出しを増やすために、他の人のブログを読むことは非常に参考になります。そのため、移動時間や休憩時間などの空き時間を活用して、他のエンジニアや企業ブログを積極的に読むようにしています。 Claude活用による変化 それでは、実際にClaudeを導入してどう変わったのかについて、以下では解説していきます。 導入のきっかけ 導入のきっかけは、弊社でも生成AI事業への関心が高まり、専用の小規模チームを結成して検証を進めたことです。その一環として、Azure OpenAIサービスを使ったサービス開発や、クラウドサービス検証なども並行して行っています。弊社ではYouTubeでAI関連の情報発信をしたり、定期的なセミナーを開催したりしているので、もし興味がある方はそちらをのぞいてみてください。 YouTube:AI Tech Cafe Nextech Solution セミナー一覧 具体的な活用場面と効果 Claudeの活用は、大きく5段階に分けることができます。 テーマ企画段階 アウトライン作成 検証段階 執筆段階 構成品質チェック 1. テーマ企画段階 ブログのアイディアを思いついた段階で、Claudeと対話しながら内容を詰めていきます。「こういう内容を思いついたんだけど、どう思う?」といった質問をして、コンテンツを深掘りしていきます。この対話によって、「こういう内容も絡めて書けるな」という気づきが生まれ、コンテンツの幅が広がります。また、初期調査として、構成の組み方や必要な技術についても情報収集を行っています。 2. アウトライン作成 書く内容が決まった時に、どういう流れで文章を書いていくかをClaudeに提案してもらいます。人間が一から考えるのではなく、AIに出力してもらうことで、執筆前に「この観点が必要だった」といった見落としを未然に防ぐことができています。この段階でのClaudeの支援はとても優秀です。 3. 検証段階 公式リファレンスの調査や参考文献、技術ブログなどの情報をリストとしてまとめてもらい、検証時に役立てています。またClaudeだけでなく、GitHub Copilotなどを使ってコーディング支援も受けています。自分で書いたコードに適切なコメントを追加してもらったり、解析コメントだけを出力してもらったりといった活用をしています。 4. 執筆段階 下書きをまるごと書いてもらうことはあまりしませんが、部分的なコラムなどAIに依頼しても問題ない部分を切り分けて書いてもらっています。また、私がよく使うのは、とりあえず書いた内容の誤字脱字の修正や文体の統一です。気分によって表現方法が異なってしまうことがよくあるので、そういった部分の修正を主にClaudeにお願いしています。さらに、ブログ内に登場する図の作成にもClaudeを活用しています。 5. 構成品質チェック 別ブログの評価やファクトチェック、技術ブログとしての質、トレンド性やSEO観点での優位性などをAIに判定してもらっています。つまり、私のブログを最初に読むのはAIだと言っても過言ではありません。この「生成AIを第一読者にする」というアプローチについては、「 【実践解説】技術ブログ品質チェック術|Gemini Deep Researchで5分検証 」で詳しく書いているので、ぜひ見てください。 2025-07-07 【実践解説】技術ブログ品質チェック術|Gemini Deep Researchで5分検証 このサムネイル、少し胡散臭いセミナーっぽくて、個人的には気に入っています。 比較で見る変化 「比較で見る変化」と題していますが、これは皆さんの目で実際に確認していただければと思います。2022年、2023年、2024年、そして2025年に書いたブログから、当時の私が力を入れて書いたものをまとめていますので、ぜひ読んでいただければと思います。 2022:Google Apps Script 簡易API編 初心者向け 2023:YouTube APIを使って自動で自分の動画の分析情報を収集する【GAS】 2024:プロジェクト開発手法を解説:ウォーターフォール、アジャイル、MVPの特徴 2025:Azure SWA×Next.js認証API統合を実践解説【DevContainer〜本番まで】 具体的な数字については、投稿頻度にはコンテンツ量などの要素があり純粋な比較は難しいものの、変化は明らかです。ブログを見ていただくとわかりますが、2025年にAIを活用して書いているブログは内容がかなり充実しています。また、サムネイルもおしゃれになっています(これが使用ツールの変化か私のデザイン力向上かは置いておいて)。 ブログの構成も大きく変わりました。以前は章立てがざっくりしていたり情報が不足している部分がありましたが、最近は品質チェックを導入しているため、章立てが細かく分かれています。また、背景説明から問題提起、解決編へとスムーズに導く自然な文章構成になっています。 面白い例として、完全にAIに執筆してもらったブログも一件あります。私がシリーズ3本を1週間(合計約24時間)かけて書いたブログを入力として使い、文体を抽出したプロンプトを用意してAIに投げたところ、執筆時間わずか2分という驚異的なスピードでブログが完成しました。 体感の投稿頻度や質は上がっていますが、月ごとの比較では信頼性の高いデータを示すのが難しいです。PV(ページビュー)についても、以前は10人程度しか見られないブログもありましたが、トレンド性を意識することで大幅に改善しました。月間比較では10倍や100倍になったケースもあります(過去の私のブログが単に良くなかったという可能性が大です)。 品質の低いブログを公開前に止められる取り組みが効果を発揮しているのでしょう。思えば、昔の自分は全ての事柄をブログにアウトプットしていたので、狂気的ですよね。 現在の最適化された執筆環境 現在ではClaudeを活用することによって、ブログの作業を分割しても問題なくなりました。これはなぜかと言いますと、まず文体の調整や誤字脱字はすべてAIによって修正してもらえるので、どのタイミングで執筆をしたとしても一貫性を保てるようになりました。 そして、新しいフェーズとして、AIによる品質向上チェックを追加しています。また、執筆現場では、完全にきれいな文章を書くというよりも、まず文章に起こすということを意識しているので、執筆時間自体は今までと比べて大幅に短縮されています。企画・リサーチ・構成設計の部分もAIに助けてもらっているので、Claudeにアクセスできる環境さえあれば、作業を分割して進めることができるようになり、あらゆる場面で独立した作業が可能になっています。 また、Claude導入以前との大きな違いは、業務とそのアウトプットとしてブログを書いていた際の検証部分をAIに助けてもらうことで、スムーズかつ今までと比較にならないスピードでの検証が可能になりました。検証や調査も効率的に行えるようになったので、AIにできる部分は積極的に任せています。 最適化後の執筆プロセス(総時間:4-6時間) Phase 1: 企画・リサーチ(30分) Phase 2: 構成設計(30分) Phase 3: 執筆(2-3時間) Phase 4: 品質向上(1-2時間) 試験的取り組みとして、音声入力を活用してキーボードを叩かずに、言葉で発することで文章を文字起こしして、それをAIに補正してもらうような活動もしています。これをすることによって、まず指が疲れないのが最高ですね。長編のブログを書くと、すごく指が筋肉痛になるんじゃないかっていうぐらい酷使してしまうので、そういったところがなくなったというのは結構効果的ですね。これがいい感じにまとまったらブログとして出していきますので、お楽しみに。 各工程でのClaude活用(プレビュー) 各工程でのClaude活用(プレビュー)については、Claudeにどのようなプロンプトを投げかけ、具体的にどう活用しているのかをご紹介します。 企画段階での活用例 この使い方では、ブログを書く前に内容を詰めるための作業を行っています。こちらのブログは最終的に「 Claude調教術 暴走パターンを制御する3つのプロンプトテクニック 」というブログになりました。 プロンプト: プロンプトを書くのが苦手な方向けにブログを書こうと思っています。 Goal - ブログの仮タイトル - コンテンツの一覧の作成:ブログの分析者としてブログコンテンツの価値や差別化に関して、SEO観点から競合調査するための調査計画書の作成 Plan - ブログの各コンテンツを対話的に確認する - ブログのコンテンツに関する情報が足りていない場合は確認 - コンテンツからSEO的観点で追加コンテンツの提案 - ステップの終了確認:終了しない場合は、再度対話的に確認 出力例: プロンプトが苦手な方向けのブログ企画、素晴らしいアイデアですね!SEO観点からの競合調査も含めた包括的な計画を立てましょう。 仮タイトル案 「プロンプト初心者でも今日から使える!AI活用術ブログ」 ... 執筆段階での活用例(校閲と表現の統一) この使い方では、ブログの執筆中に使用しています。今もNotionの右側にはClaudeが開かれています。ここでは、校閲と表現の修正を行っています。 プロンプト: ブログのアウトラインと部分的に執筆した内容を送付します。 部分的に執筆したので、校閲と表現の修正をお願いします。 「執筆前整理」のセクションに関しては出力に含めなくてよいです。 何か懸念や疑問がありましたらチャットで簡素にまとめて質問してください。 - 添付1:ブログアウトライン - 添付2:執筆した部分的なブログ 出力例: ども!7月の追い込みが激しくて、いろんなものに追い回されている龍ちゃんです。まぁすべてを引き受けたのは自分なので自己責任ですが、調子に乗っていたなと反省しています。とはいえ、割と順調に片付いているので、余裕が出てきたからこその発言なんですけどね。 6〜7月は13本ほどブログを執筆していますね。そんなブログのお話を今回書いていこうと思います。前半は若干のポエムの可能性もあるので、流し見で見てもらえればと思います。 今回は「Claude活用で変わった技術ブログ執筆のお話」になります。それでは始めましょう。 ~~省略~~ 以下は実際の執筆中の画面です。 品質チェック段階での活用例 この使い方では、「 【実践解説】技術ブログ品質チェック術|Gemini Deep Researchで5分検証 」で解説している内容と似ています。受け取った評価をそのまま信じるわけではありません。ですが、最新リソースなどから複合的な観点での品質チェックを行ってくれます。 今回のプロンプトでは E-E-A-T とトレンド性という観点で評価を行ってもらっています。 プロンプト: 添付されたファイルから内容を把握して、技術ブログとして評価してください。 E-E-A-Tとトレンド性なども含めて評価をよろしくお願いします。 - 添付ファイル:完成したブログ 出力例: この技術ブログを分析して評価いたします。 全体的な評価 この記事はDevContainerを使用したStreamlit開発環境の構築について具体的で実用的な内容を提供しており、技術ブログとしての質は高いです。 各要素の評価 E-E-A-T(Experience, Expertise, Authoritativeness, Trustworthiness) Experience(経験): 8/10 実際の開発経験に基づいた内容 具体的な設定ファイルとコード例を豊富に提供 flake8とblackの設定競合問題など、実開発で遭遇する課題を解決 ~~~省略~~ 現在のツール使い分けと課題 現在のツール使い分けと課題について説明します。私が使用できるAIツールは、Notion AI、Gemini、Claude、そしてGitHub Copilotの4つです。 Notion AI → Claude への移行体験 これまでのツール活用としては、Notion AIとClaudeを使っていました。タイトルやメタディスクリプションはNotion AIで作成していましたが、現在はNotion AIからClaudeに完全に移行しています。 Notion AIの活用方法として、Notion上で執筆をしているため、初期の文法の誤りや日本語的な表現の誤り、文章構成などの部分で特に活用していました。そして最終的に構成した文章をClaudeにも入力して、全体的な修正を行っていました。つまり、Notion AIで部分的な修正を行いながら、最終的な調整はClaude側でやってもらうという使い分けをしていました。 Claude移行後は、技術的な正確性と構造化された対話の質が大幅に向上しました。特に技術ブログ執筆での精度が圧倒的に向上し、修正した内容をそのまま反映できるため、執筆環境においてはClaudeがとても魅力的です。 Gemini活用の現状と品質チェック手法 Geminiも使えるのですが、Geminiは「AIを第一読者にする」というブログで紹介したように活用しています。執筆段階でGeminiを使うことはほとんどなく、調査やブログの最終的な評価の部分でDeep Researchを活用しています。 執筆に関連する使い方としては、執筆後のブログコンテンツの評価やブログを書く前の調査という部分です。Geminiでできることは、Claudeでも同様のことができます。 Geminiの利点として、会社のワークスペースで契約していて単純に使い放題なのが強みです。一方で私はClaudeをProプランで契約していますが、5時間ごとに利用制限がある点が弱点です。そのため、日常的な調査はGeminiに任せ、より詳細なタスクはClaudeに任せるという切り分けを行っています。 Research機能の比較:Claude vs Gemini Deep Research ClaudeのResearch機能は2025年5月現在、日本、アメリカ、ブラジルのMax、Team、Enterpriseプランのユーザー向けにβ版として提供されています。私は「AIを第一読者にする」という手法を実践していますが、これはClaudeでも同じことをやっています。 ClaudeのResearch機能とGeminiのDeep Researchを同時に使い、両方のAIがOKと言えば概ねOKだろうと判断しています。つまり、第一査読者、第二査読者のような感じで使っています。 検索能力について、文章中で「600件検索」と記載しましたが、これは私の記憶違いの可能性があります。実際の検索件数については、使用する際の複雑さや設定によって変動するため、正確な数値比較よりも、出されたレポート結果の質が重要です。 体感的な違いとして、Geminiの方が出すレポートは非常に硬くて読みにくいです。一方、Claudeのレポートも硬さはありますが、レポート形式の指定ができるので、フォーマットを定義できる点でClaudeの自由度が高いと感じています。 各AIサービスの特徴と使い分け 各AIサービスにはそれぞれ強みと弱みがあり、得意な分野と不得意な分野があります。役割が重複する部分でも好みの差があります。 文体・表現の違い Gemini : 表現として非常に硬い文章を出力 Claude : 指定すれば柔らかい文体や特定のスタイルで表現 Notion AI : 入力ができる領域は限定的だが小さい範囲での修正の力は協力 GitHub Copilot の特化領域 GitHub Copilotはコードレビューやコード関連に特化している点が強みです。ClaudeもGeminiもコードを出力できますが、拡張機能で簡単に使えるGitHub Copilotは非常に便利です。特に設定なしでGitHubと連携できるので、GitHub上で何かサービスを使うならCopilotが優勢です。 単純に比較することは難しいですが、それぞれの得意分野を見極めながら活用していきたいと思います。今後のブログでは、Claudeで検証している内容がGeminiでもできるのかといった比較も触れていければと思います。 参考情報 Claude関連の公式リファレンス Claude Proには使用制限がありますか? | Anthropicヘルプセンター Claude Proの使用について | Anthropicヘルプセンター Claudeの最大プラン使用量について | Anthropicヘルプセンター Claude.aiでの研究の利用 | Anthropicヘルプセンター Research機能の公式発表 | Anthropic GitHub Copilot GitHub Copilot 公式ページ Google Workspace & Gemini Google Workspace 公式ページ Gemini for Google Workspace 今後の展望とシリーズ予告 今後の展望と課題 最近気になっているのは、Claudeでツール連携ができるようになった点です。これがもう少し使いやすくなれば、NotionやGitHubとも連携できるので、Claude上でアクセスするだけで様々なサービスに問い合わせができるようになるでしょう。 ただし、AIにすべてを委譲することでレート制限に達したり、課金が高額になるリスクもあるので、安全に運用できる仕組みも今後しっかりと設定する必要があります。 今後はブログの執筆だけでなく、PV数などの分析や、ブログコンテンツにさらに踏み込んだAI活用も模索していきたいと思います。当面の計画として、各プロンプトのテクニックをシリーズ形式で執筆していこうと考えています。 実際に現在取り組んでいる内容として、 Claudeでブログコンテンツからツイート(X)の投稿文を生成する検証も行っており、その結果もブログ に掲載しています。ブログ作成からSNS投稿、継続的な発信、校閲といった一連のプロセスを通して、情報発信という観点とAIの組み合わせによる新しい可能性を探っていければと思っています。 シリーズ記事の執筆予定 今後、シリーズとして執筆したブログは以下にまとめていきます。まあ、現状では一本も執筆していないので…。えっと、もしも更新が止まっていたら、X上でつついてください。 各記事で執筆する予定のコンテンツを箇条書きでまとめておきます。 第2回:品質向上編 技術的正確性の担保方法 読者体験最適化のテクニック 実際の改善事例と測定方法 第3回:構成・アウトライン編 論理的な構成設計のコツ 読者レベル別の調整方法 SEOを意識した構造化 第4回:SEO最適化編 技術系キーワード戦略 検索意図に合わせたコンテンツ設計 効果測定と改善サイクル 第5回:ビジュアル制作編 Artifacts活用の図解作成 技術概念の可視化テクニック 読者理解を深める工夫 第6回:サムネイル・デザイン編 クリック率向上のデザイン戦略 A/Bテストによる最適化 継続的な改善プロセス 読者への呼びかけ 技術ブログの執筆に取り組んでいる皆さんにとって、今回紹介したClaude活用法が少しでも参考になれば幸いです。AI技術は日々進歩しており、新しい活用法も次々と生まれています。 実際に試してみて、うまくいった方法や課題があれば、ぜひコメントやSNSで教えてください。皆さんの体験談も今後の記事に反映していきたいと思います。 次回の記事もお楽しみに!そして、更新が止まっていたら遠慮なくつついてくださいね。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Claude×技術ブログで執筆環境が激変!次世代AI協働ワークフロー解説 first appeared on SIOS Tech. Lab .
挨拶 ども!最近はAI関連からInfrastructure as Code(IaC)などにも入門して幅広いブログを執筆している龍ちゃんです。最近はブログ執筆にAIを導入して、執筆スピードと質が上がっているような気がして楽しいですね。7月にまとめた内容は8月の初頭に45分セミナーにまとめるので、激しめに検証を進めています。 さて今回は「Stremlit」のお話になります。最近ふんわりとStremlitを書く機会が増えたので、DevContainerで開発する環境構築をまとめていこうと思います。 Stremlit開発環境をDevContainerで作成する 今回は、開発環境を使いまわす可能性を考慮してGitに環境を上げています。もし使いたい方がいれば、 リポジトリをクローン して利用してください。 ディレクトリ構成 今回作成する環境のディレクトリ構成になります。venvなどでも構築できるように .devcontainer 内でコンテナの定義をすべて収めています。リポジトリのトップからマウントすることで、コンテナ内でGit操作をすることができます。 . ├── .devcontainer │ ├── Dockerfile │ ├── compose.yml │ └── devcontainer.json ├── .env ├── .gitignore ├── README.md ├── requirements.txt ├── sample.env ├── setup.cfg └── src └── main.py Dockerfile ベースイメージは、 Microsoftが出しているPython 3.11のベースイメージ を利用しています。利点として、以下の二点があります。 Gitが標準で搭載されている vscodeユーザーがデフォルトで作成されている(非rootユーザー) コード解析・フォーマッター「flake8」「black」が標準搭載 Dockerfile内で行っている処理は、pipのアップグレードのみになります。 # Use the official Microsoft Python image (Debian Bullseye based) FROM mcr.microsoft.com/devcontainers/python:1-3.11-bullseye # Set the working directory WORKDIR /home/vscode/python # Install Python packages that are commonly used RUN pip install --upgrade pip # Keep the container running CMD ["sleep", "infinity"] compose.yml 将来的な拡張(データベース等)を考えて、composeファイルで定義しています。リポジトリのルートディレクトリからマウントすることで、仮想環境内からGitコマンドを直接実行することができます。 Streamlitでデフォルトで公開されるポート(8501)を接続しています。 services: python: build: context: . dockerfile: ./Dockerfile tty: true volumes: - type: bind source: ../ target: /home/vscode/python restart: always ports: - "8501:8501" devcontainer.json 今回作成するファイルの中で最も記述量が多いファイルになります。基本的にコピー&ペーストで大丈夫です。設定している主要項目としては以下になります。 拡張機能の追加 開発環境設定 開発用feature(Azure CLI・GitHub CLI) 開発環境設定では、pylintを無効化してflake8を有効化するように設定しています。ファイルをセーブしたタイミングでリンターとフォーマッターが実行されるように設定しています。 { "name": "Streamlit", "service": "python", "dockerComposeFile": "./compose.yml", "workspaceFolder": "/home/vscode/python", "shutdownAction": "stopCompose", "customizations": { "vscode": { "extensions": [ "ms-python.python", "ms-python.black-formatter", "ms-vscode.vscode-json", "ms-python.flake8" ], "settings": { "python.defaultInterpreterPath": "/usr/local/bin/python", "python.formatting.provider": "black", "terminal.integrated.defaultProfile.linux": "bash", "python.linting.enabled": true, "python.linting.pylintEnabled": true, "python.linting.lintOnSave": true, "python.linting.flake8Enabled": true, "": { "editor.formatOnSave": true, "editor.defaultFormatter": "ms-python.black-formatter" } } } }, "features": { "ghcr.io/devcontainers/features/azure-cli:1": {}, "ghcr.io/devcontainers/features/github-cli": {} }, "forwardPorts": [ 8501 ], "portsAttributes": { "8501": { "label": "Streamlit", "onAutoForward": "notify" } }, "postCreateCommand": "pip install -r requirements.txt", "remoteUser": "vscode", "updateRemoteUserUID": true } featureとしてAzure CLIとGitHub CLIをインストールしています。これは将来的にデプロイ先をAzure Web Apps on Containerでデプロイすることを考慮して設定しています。 フォーマット&リンター設定 前段の設定のみで開発を進めることができますが、一転問題があります。flake8とblackのデフォルトの設定が競合してしまい、問題が発生します。flake8かblackのどちらかに設定を合わせる必要があります。 setup.cfg ファイルを作成して、以下の内容をコピー&ペーストすることで設定することが可能です。 [flake8] max-line-length = 88 extend-ignore = E203, W503, E501 exclude = .git, __pycache__, .venv, venv, .env, env Stremlitのデモ ここまでで開発環境は整備することができたので、Stremlitで簡易的なページを作成を進めていきます。 まずは、Stremlitの機能をインストールをしましょう。 # stremlitのインストール pip install streamlit # requirements.txtにインストールしたファイルの設定 pip freeze > requirements.txt src > main.py を設定して以下のファイルをコピー&ペーストをしてください。 import streamlit as st import pandas as pd import numpy as np st.set_page_config(layout="wide") st.title("🚀 Streamlitの素晴らしい利点") st.write( "Streamlitを使えば、データサイエンスのプロジェクトを瞬く間にインタラクティブなWebアプリに変えることができます。" "もう複雑なWeb開発の知識は必要ありません!" ) st.subheader("💡 利点1: 圧倒的な手軽さ") st.write( "Pythonの知識だけで、データスクリプトから直接Webアプリを作成できます。HTML、CSS、JavaScriptなどの" "フロントエンドの知識は一切不要です。これにより、開発時間を大幅に短縮できます。" ) st.code( """ import streamlit as st st.title("こんにちは、Streamlit!") st.write("これはたった数行のコードです。") """ ) st.subheader("⚡ 利点2: 迅速なプロトタイピング") st.write( "コードを変更するたびに、アプリがリアルタイムで更新されます。これにより、アイデアをすぐに形にし、" "フィードバックを得ながら反復的に開発を進めることができます。まるでライブコーディングをしているようです!" ) st.info( "💡 ヒント: このページを保存して、Streamlitを実行したままコードを編集してみてください。" ) st.subheader("📊 利点3: 美しいデータ可視化が簡単に") st.write( "Matplotlib, Plotly, Altairなどの既存のPython可視化ライブラリとシームレスに統合できます。数行のコードで" "インタラクティブなグラフやダッシュボードを作成できます。" ) # サンプルグラフの表示 chart_data = pd.DataFrame(np.random.randn(20, 3), columns=["a", "b", "c"]) st.line_chart(chart_data) st.bar_chart(chart_data) st.subheader("🔄 利点4: インタラクティブなウィジェット") st.write( "スライダー、ボタン、テキスト入力など、多様なウィジェットを簡単に組み込むことができます。" "これにより、ユーザーがデータと対話できる動的なアプリケーションを作成できます。" ) # インタラクティブなデモ value = st.slider("スライダーを動かしてみてください", 0, 100, 50) st.write(f"現在の値: {value}") button_clicked = st.button("ボタンを押してみてください") if button_clicked: st.success("ボタンが押されました!") user_text = st.text_input("ここに何か入力してください", "Streamlitは最高!") st.write(f"あなたが入力した内容: {user_text}") st.subheader("☁ 利点5: デプロイのしやすさ") st.write( "Streamlit Community Cloudを使えば、数クリックでアプリをWebに公開できます。また、Dockerなどの" "コンテナ技術と組み合わせることで、より柔軟なデプロイも可能です。" ) st.video("https://www.youtube.com/watch?v=NtLCVE9hwb8") # Streamlitの紹介動画(例) st.write("---") st.markdown( "### まとめ\n" "Streamlitは、データサイエンティストやアナリストがアイデアを迅速に具現化し、" "それをインタラクティブな形で共有するための強力なツールです。" "ぜひご自身のプロジェクトで試してみてください!" ) st.balloons() ファイルを作成したら、以下のコマンドで起動することができます。 streamlit run src/main.py デフォルトで http://0.0.0.0:8501 が立ち上がって真っ白な画面が立ち上がってもあせらなくて大丈夫です。 http://localhost:8501 にアクセスしてみてください。以下の画面が表示されれば成功です。 まとめ 今回は、StreamlitをDevContainerで開発する環境構築について詳しく見てきました。DevContainerを使うことで、チーム内での開発環境統一や、プロジェクトの引き継ぎがスムーズになりますね。 特に今回のポイントとしては、MicrosoftのPython 3.11ベースイメージを使うことで、Gitやコード解析ツールが標準搭載されている点が大きなメリットでした。また、flake8とblackの設定競合問題も、setup.cfgファイルで解決できることがわかりました。 Streamlitは本当に手軽にデータアプリケーションを作成できる素晴らしいツールです。今回構築した環境なら、コードを変更するたびにリアルタイムで結果を確認できるので、開発効率が格段に向上します。 皆さんも、ぜひこの環境でStreamlitアプリケーション開発にチャレンジしてみてください!GitHubリポジトリも公開しているので、クローンして即座に開発を始められます。 次回は、このStreamlit環境をAzure Web Apps on Containerでデプロイする方法について解説予定です。お楽しみに! こちらのブログ「 Infrastructure as Code実践:Azure SWA×Bicep×GitHub Actions 」と同様の手順でBicepで環境作成からAzureへのデプロイまで目指していきます。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post DevContainerでStreamlit開発を始める方法:Docker+VSCode first appeared on SIOS Tech. Lab .
はじめに ども!最近はAIを使った自動化に注目している龍ちゃんです。実は最近、SIOSテックラボのX担当者もやっているんですが、投稿頻度によっては半分ぐらい自分のブログ宣伝をしているので、ほぼ私物になっています。 今回はそんな問題を解消してくれるプロンプトを紹介していきたいと思います。ただ作るだけではなく、簡易的なプロンプトから始めて、詳細プロンプトに落とし込んだ過程を含めて、検証の結果とともにブログ記事にできれば良いなと思っています。 この記事では以下の内容をお伝えします: X(旧Twitter)投稿の自動化ニーズの高まり 本記事の目的:プロンプトの違いによる投稿品質の比較検証 検証方法の概要 なぜAI生成に頼ったのか ブロガーの悩み ブログの宣伝って難しいですよね。この記事でわかることを正確に伝えるという作業が必要になります。自分が執筆したブログであれば、この点は簡単ですよね。 ですが、自分以外の人間が、自分のあまり知らない技術領域について書いた技術ブログの宣伝をするのは「何を書いたらいいのかわからない」「読むのも若干辛い」みたいなことが発生します。そして、これがエンジニアでなければ、まさにそこに広がるのは宇宙なわけですね。 そこで、過去に投稿した投稿に合わせてしまったり、当たり障りのない文章が作られるというのが日常茶飯事になっています。そして、エンジニアが文章を考えるとですね、マーケティング的視点があるわけではなく、宣伝の手法に詳しいわけでもなく、なんとなくいつも使っているXのような文面で投稿してしまうという問題もあります。 結論として、Xの投稿文を考えるのは難しいということですね。 具体的な悩みとしては: X投稿を考えるのが面倒 何を書けばいいかわからない 毎回同じような投稿になってしまう 記事は書けるけど、それをどう宣伝していいかわからない AI生成への期待 そこで生成AIの出番です。あわよくば、生成AIにブログのコンテンツを投げたら、Xの投稿文を作ってもらえると嬉しいですよね。記事のコンテンツの内容に基づいて、コピー&ペーストするだけで投稿してくれたら嬉しいです。 その文章がマーケティング的視点が入っていて、最適化されている文章だと、なお良いな。全て自動化してくれて、宣伝も自動化してくれるとなお良いという現状です。この点は夢物語です… そんなことを考えていたのですが、最近契約したClaudeで、そんなことができそうだなぁということでやってみたらできたという流れです。 簡単なプロンプトから詳細プロンプトまで実験をしてみたので、その結果を含めて今回得られた知見についてお話ししていきます。 期待していたことは: とりあえず何か投稿文を作ってほしい 記事の内容を要約してほしい できれば修正なしでそのまま投稿したい 検証実験の設計 ここでは、実験の過程について、検証をした過程について説明していきます。 実験条件 実験条件としては、 同一のURL に対して行います。これに対して簡易プロンプトと詳細プロンプトで、それぞれ3パターンずつプロンプトを投げて実験を行いました。 こちらの生成結果はGistにまとめています 。リストを開いて右に並べながら読んでいただけるとわかりやすいかと思います。 プロンプトパターン 検証するにあたって、簡易プロンプトは以下のような実装をしています。言われると簡単な指示だけを与えているものなので、とても簡単なものです。 比較して詳細プロンプトは全体300行になる比較的大きなプロンプトになっています。使用方法としては、URLを入力として与えるだけで、システムプロンプトが自動的に起動し、生成物を作ってくれます。 簡易プロンプト :基本的な指示のみ 詳細プロンプト :具体的な要件と制約を明記 簡易プロンプトで試してみた結果 最初に試したプロンプト https://tech-lab.sios.jp/archives/48160 このURLの内容からX投稿を作成してください 実際にできた投稿文 下に実際に生成された事例を示します。実験は計3回行い、20個の投稿文が生成されました。結果としてURLが含まれてないもの、文字数がXの投稿制限に引っかかるものなど、生成物をそのまま投稿として扱うことができないようなものが生成されてきました。 一応Xは課金をしているので、280文字以上は投稿できるんですが、それではリンクカードが表示されないという問題があったため、280文字以内での投稿というのを心がけています。 ですが、URLから記事の内容を取得して、記事に基づいた投稿文は生成してくれました。多少人の手を加えれば充分投稿できるようなものになっています。 # 良い例 ``` 今すぐ使えるClaude制御プロンプト集📝 🚫 Artifact生成を止めたい時 「成果物を作らずに、考え方だけ教えて」 「禁止事項:Artifact生成」 💬 軽い回答が欲しい時 「3行くらいで説明して」 「一言でどう思う?」 🔄 スレッド継続したい時 「今までの議論を引継ぎ資料にまとめて」 他にも使えるテクニック満載👇 https://tech-lab.sios.jp/archives/48160 #Claude #プロンプト集 ``` # 悪い例 ## URLがない ``` Claudeに「転職活動どうしよう」って聞いたら →12段階の転職完全ロードマップ作られた😂 軽い質問のつもりが教科書レベルの資料に... Claudeは「めちゃくちゃ賢くてやる気のある赤子」 明確な指示で制御するコツをまとめました📝 #Claude #AI #あるある ``` ## 280文字突破 ``` 🎯 Claude調教の実績報告 k8s完全初心者から ↓ 人間主導学習プロセスで DaemonSet運用レベルまで到達! ポイント: 1. 大枠把握からスタート 2. 段階的深堀り質問 3. 理解度確認を必ず実施 4. 自分の言葉で例え話 Claudeを個人チューターとして最大活用🚀 詳しい学習フロー👇 Claude調教術|暴走パターンを制御する3つのプロンプトテクニック #Claude学習法 #スキルアップ ``` 何が物足りなかったか ここでは生成された生成物の何が足りなかったのかについて考えていきます。 やはり1番重要なのはURLです。URLがないのでどこに飛べばいいのかわからないという投稿は、さすがにブログの宣伝投稿としては不適切です。次に硬すぎる文体ですね。文体としてはもう少し柔らかいもの、簡単な文章という方が良いかと思います。そして、あわよくば、人間が投稿したように模倣していただければいいんじゃないかなぁと思います。 次にハッシュタグですね。ハッシュタグを生成するのは、正直、僕自身もどうやればいいのかわからないので、この辺の効果測定は難しいなと思います。ですが、ハッシュタグがないものは、さすがに「手抜き」なんじゃないかなと思ったりもしちゃうわけですね。 まぁ簡単に言うと、手動による見直しが見込まれました。 具体的な問題点: 長すぎる(文字数オーバー) 堅すぎる文体 「これ投稿して大丈夫?」という不安 結局自分で書き直すことに… 詳細プロンプトでの生成結果 使用したプロンプト例 全文は長いので、プロンプトのすべてをここに配置することができませんが、要点のみ最後に付録として添付しておきます。 文字数制限 トーン指定 ハッシュタグ要件 エンゲージメント要素 構造化された出力形式 生成結果 # ブログの評価 **総合評価:A(高品質)** **技術的正確性:⭐⭐⭐⭐⭐(5点満点)** **実装レベル:本格実装** **対象読者:中級者〜上級者** **実用性:⭐⭐⭐⭐⭐(5点満点)** ## 主要な技術要素 - Claude 4(Sonnet)を使用したプロンプトエンジニアリング実践 - 3つの制御テクニック:成果物制御、会話継続、ペース調整 - システムプロンプト設計とArtifact生成制御 - スレッド管理とトークン最適化手法 ## 記事内の具体的な数値・効果 - **記事内で実証済みの制御手法**:3つの明確なテクニック体系 - **実際の学習事例**:Kubernetes習得プロセスの詳細記録 - **具体的改善例**:300行→50行程度への成果物サイズ制御 - **問題解決実績**:スレッド上限問題の具体的回避策 ## 注意事項・制限 - 記事内で言及されている制限事項:スレッドが長くなると効果が薄れる傾向 - 実装時の注意点:プロンプトの詳細指定が重要 - Claude固有のノウハウ:他のLLMでは効果が異なる可能性 ## 誇張表現検証結果 - **初回生成での誇張なし**:記事内の実証済み手法のみを使用 - **修正内容**:なし(記事内容と完全一致) - **最終版での整合性確認結果**:✅ 全パターンで記事内容と整合性確認済み --- # ブログの投稿パターン ## Aパターン:効果重視・数値訴求型 **投稿文:** ``` Claude調教術|暴走パターンを制御する3つのプロンプトテクニック 🚀 Claude暴走を3つのテクニックで完全制御! 📝 成果物制御・会話継続・ペース調整で理想的なAI活用 ⚡ 技術スタック: #Claude #プロンプトエンジニアリング 🔧 300行→50行への成果物最適化も実現 #AI #LLM #効率化 #エンジニア ``` - **記事内容との整合性:** ✅ 確認済み - **投稿推奨時間:** 火曜日 21:00-22:00(エンジニア活動ピーク・技術学習時間) ## Bパターン:課題共感・解決提案型 **投稿文:** ``` Claude調教術|暴走パターンを制御する3つのプロンプトテクニック 😰 Claude君止まって!大規模Artifact生成で困ってませんか? 💡 明確な指示とGoal設定で暴走制御が可能になります ⚡ 成果物制御・スレッド管理・段階的学習の3技法 🎯 スレッド上限問題も引継ぎ資料で解決 #Claude #プロンプトエンジニアリング #AI #LLM ``` - **記事内容との整合性:** ✅ 確認済み - **投稿推奨時間:** 水曜日 12:00-13:00(昼休み・課題解決ニーズが高い時間) ## Cパターン:技術トレンド・学習促進型 **投稿文:** ``` Claude調教術|暴走パターンを制御する3つのプロンプトテクニック 🔥 2025年のプロンプトエンジニアリング実践術 📚 Claude制御テクニック3選の詳細解説 ⭐ 人間主導の学習プロセスでスキルアップ加速 🚀 Kubernetes習得事例で段階的学習の威力を実証 #プロンプトエンジニアリング #Claude #AI学習 ``` - **記事内容との整合性:** ✅ 確認済み - **投稿推奨時間:** 金曜日 18:00-19:00(週末学習準備・新技術トレンド確認時間) --- ## 投稿時間最適化の根拠 ### エンジニア向け最適投稿時間 - **Aパターン(火曜21:00)**:技術習得意欲が最も高い時間帯 - **Bパターン(水曜12:00)**:実務課題解決のニーズが高い昼休み - **Cパターン(金曜18:00)**:週末の技術学習計画を立てる時間 ### ハッシュタグ効果分析結果 - **#Claude** - 2025年最も注目されるAI技術キーワード - **#プロンプトエンジニアリング** - エンジニア間で急成長中のスキル分野 - **#AI** **#LLM** - 幅広いリーチを狙える基本タグ - **半角英語優先**:バイト効率とトレンド性を両 生成結果の分析 実際の生成物について見ていきましょう。プロンプトで3パターン、それぞれ別の観点から投稿文を生成しているので、1つの生成物に3つの投稿文(計9件)が生成されています。 文字数制限も280文字以内に収まるように計算されて最適化されています。僕なら絶対考えつかないようなキーワードで、ブログが表現されており、体感としてはこれは充分投稿に値するだろうと思える内容でした。 また、ハッシュタグに関しても、検索から適切なハッシュタグを検索しているというステップを作成して検索して策定しています。ですが、正直この辺の判断は投稿していきながら、Xで詳細な分析を行わないとわからないので、正確なことは言えません。 改善された点: 実際の生成文例 文字数の適切さ 魅力的なキャッチフレーズ 関連性の高いハッシュタグ エンゲージメント促進要素 そのまま使用可能性の評価 実際にこちらのプロンプトは2週間ほど運用しているのですが、問題としては多少誇張表現を使ってしまう部分、記事の内容に基づかないことなどはあるものの、比較的問題のない投稿文になっています。 8割程度はそのままコピー&ペーストで投稿を行うことができています。また、微調整に関しても、問題点をチャットで指摘をすることで、再生成を行ってくれるため、人間の作業としてはURLを選択して投稿文を確認し、問題があったら指摘するという簡単なフローに変わりました。 結果を比べてみて 簡易プロンプトと詳細プロンプトでそれぞれ比較を行い、分析考察を行っていきます。 簡易プロンプトの課題 簡易プロンプトの問題点としては、出力の品質が安定していないものが挙げられます。やはりXの投稿文として求めているスタイルというものは人それぞれ違いますが、今回は僕が求めているスタイルに合うものは合計の出力数の2割程度しかありませんでした。 そして毎回出力によってパターンが異なるため、確率の低いガチャを回してるようで楽しくなっちゃいましたね。 また、人間が修正する手間が必ず発生しているため、アイディアとしては充分使えますが、投稿文をそのまま生成するという当初の目的から外れてしまいます。 ガチャ要素 :毎回違う結果で安定しない 「今日は良い投稿文、昨日はイマイチ」の繰り返し 結果が予測できないので、結局人間が判断・修正が必要 時間かけて修正するなら最初から自分で書いた方が早い 詳細プロンプトの安定性 詳細プロンプトでは、実行計画をプロンプトに埋め込んでいるため、すべての生成の過程で統一のステップを踏むことにより安定性があります。完全に保証されているわけではないんですけどね。 実際にブログのコンテンツ内容に基づいていると判断していても、人間の目で確認したところ事実に基づいていない投稿文が存在していました。 ですが、3パターンの投稿文が生成されるので、パターンの1つは使えるパターンがあるように感じています。品質チェック項目があるため、文字数280文字を超えることはほぼなく、投稿文としても記事の内容に基づいていることが多いです。 今回で言うならば、再生成を行ったのは全体中1つだけでした。 ほとんどコピペでの運用が実現しています。 一定品質の保証 :どのパターンも安心して使える 狙った通りのアプローチで生成される ほぼそのままコピペで投稿可能 「今日もちゃんとした投稿文ができた」安心感 視覚的比較用ルーレット #prompt-gacha-container { font-family: 'Hiragino Sans', 'Hiragino Kaku Gothic ProN', 'Yu Gothic', sans-serif; background: linear-gradient(45deg, #ff9a9e 0%, #fecfef 50%, #667eea 100%); border-radius: 20px; padding: 30px; max-width: 900px; margin: 20px auto; box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1); } .gacha-title { text-align: center; color: #2c3e50; margin-bottom: 10px; font-size: 2rem; font-weight: 700; text-shadow: 2px 2px 4px rgba(0,0,0,0.1); } .gacha-subtitle { text-align: center; color: #7f8c8d; margin-bottom: 30px; font-size: 1.1rem; } .gacha-machines { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-bottom: 30px; } .gacha-machine { background: linear-gradient(145deg, #ffffff, #f0f0f0); border-radius: 20px; padding: 25px; box-shadow: 10px 10px 20px rgba(0,0,0,0.1), -10px -10px 20px rgba(255,255,255,0.5); text-align: center; position: relative; overflow: hidden; } .machine-title { font-size: 1.3rem; font-weight: bold; margin-bottom: 15px; color: #34495e; } .simple-title { color: #e74c3c; } .detailed-title { color: #27ae60; } .gacha-display { background: #2c3e50; border-radius: 15px; height: 120px; margin: 20px 0; display: flex; align-items: center; justify-content: center; font-size: 3rem; color: white; border: 5px solid #34495e; position: relative; overflow: hidden; } .gacha-button { background: linear-gradient(145deg, #3498db, #2980b9); border: none; border-radius: 50px; color: white; font-size: 1.1rem; font-weight: bold; padding: 15px 30px; cursor: pointer; transition: all 0.3s ease; box-shadow: 0 10px 20px rgba(52, 152, 219, 0.3); margin: 10px; } .gacha-button:hover { transform: translateY(-3px); box-shadow: 0 15px 25px rgba(52, 152, 219, 0.4); } .gacha-button:active { transform: translateY(-1px); } .simple-button { background: linear-gradient(145deg, #e74c3c, #c0392b); box-shadow: 0 10px 20px rgba(231, 76, 60, 0.3); } .simple-button:hover { box-shadow: 0 15px 25px rgba(231, 76, 60, 0.4); } .detailed-button { background: linear-gradient(145deg, #27ae60, #229954); box-shadow: 0 10px 20px rgba(39, 174, 96, 0.3); } .detailed-button:hover { box-shadow: 0 15px 25px rgba(39, 174, 96, 0.4); } .stats-display { margin-top: 15px; font-size: 0.9rem; color: #7f8c8d; } .success-rate { background: rgba(255, 255, 255, 0.8); border-radius: 10px; padding: 10px; margin-top: 15px; } .rate-bar { height: 20px; border-radius: 10px; margin: 5px 0; position: relative; overflow: hidden; } .simple-rate { background: linear-gradient(90deg, #e74c3c 20%, #95a5a6 20%); } .detailed-rate { background: linear-gradient(90deg, #27ae60 80%, #95a5a6 80%); } .rate-text { position: absolute; width: 100%; text-align: center; line-height: 20px; font-weight: bold; color: white; text-shadow: 1px 1px 2px rgba(0,0,0,0.5); } .spinning { animation: spin 0.1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .results-section { background: white; border-radius: 15px; padding: 25px; margin-top: 20px; box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08); } .results-title { text-align: center; color: #34495e; margin-bottom: 20px; font-size: 1.3rem; font-weight: 600; } .trial-results { display: flex; justify-content: space-around; margin: 20px 0; } .trial-group { text-align: center; } .trial-icons { font-size: 1.5rem; margin: 10px 0; line-height: 1.8; } .trial-label { font-weight: bold; margin-bottom: 10px; } .simple-label { color: #e74c3c; } .detailed-label { color: #27ae60; } .conclusion { background: linear-gradient(135deg, #ffeaa7, #fab1a0); border-radius: 15px; padding: 20px; margin-top: 20px; border-left: 5px solid #e17055; } .conclusion-title { color: #d63031; font-weight: bold; margin-bottom: 10px; font-size: 1.1rem; } .conclusion-text { color: #2c3e50; line-height: 1.6; } @media (max-width: 768px) { .gacha-machines { grid-template-columns: 1fr; } .trial-results { flex-direction: column; gap: 20px; } } プロンプトガチャマシン 簡易プロンプト vs 詳細プロンプト – あなたならどっちを選ぶ? 簡易プロンプトマシン ガチャを回す! 成功率 20% まずは試してみよう! 詳細プロンプトマシン ガチャを回す! 成功率 80% まずは試してみよう! 10回試行の結果比較 簡易プロンプト 成功: 2回 / 失敗: 8回 詳細プロンプト 成功: 8回 / 失敗: 2回 /* * ============================================ * 🎯 エンジニアの皆さん、お疲れ様です! * ============================================ * * まぁ真っ先にソース見るよな〜さすがやわ😏 * * 予想通りの行動パターン: * 1. ページ見る * 2. 「面白そう」 * 3. F12をポチッ * 4. このコメント発見 ← 今ココ * * きっと君なら: * - setInterval()で自動化考えてるでしょ? * - while文で無限ループ組みたいでしょ? * - 「これ、API化できそう」とか思ってるでしょ? * * でもさ、その技術力で詳細プロンプト書いた方が * 圧倒的に早いんじゃない?😄 * * ============================================ */ // 簡易プロンプトの結果パターン const simpleResults = [ { emoji: '😅', text: '文字数オーバー...', success: false }, { emoji: '😅', text: 'URLが含まれてない...', success: false }, { emoji: '😅', text: '堅すぎる文体...', success: false }, { emoji: '😅', text: 'ハッシュタグなし...', success: false }, { emoji: '😅', text: '当たり障りのない文章...', success: false }, { emoji: '😅', text: '投稿して大丈夫?な内容...', success: false }, { emoji: '😅', text: '結局書き直しが必要...', success: false }, { emoji: '😅', text: 'なんか違う...', success: false }, { emoji: '✅', text: '使える!(でも微調整必要)', success: true }, { emoji: '✅', text: 'まあまあ良い感じ!', success: true } ]; // 煽り文句パターン(10回毎に表示) const gachaMessages = { 10: "🔥 まだまだいけるぜ!次は当たるかも!", 20: "💪 その調子!あと少しで大当たり!", 30: "⚡ 諦めるな!次こそは神引き!", 40: "🌟 ここからが本番だ!運気上昇中!", 50: "🎯 半世紀到達!もう後には引けない!", 60: "🚀 60回突破!これぞガチャ道!", 70: "💎 レジェンド級の粘り!次で確変!", 80: "👑 80回の壁を越えた!君はガチャ王だ!", 90: "🌈 90回...もはや伝説の領域!", 100: "🎊 100回達成!君こそ真のガチャマスター!", 200: "🎪 200回...正気か?でも尊敬する!", 300: "🤖 300回...そろそろスクリプト組んだ?", 400: "⚙ 400回...setInterval使ってるでしょ?", 500: "🔧 500回...while(true)やめなさい", 600: "💻 600回...もうconsoleで見てるでしょ?", 700: "👨‍💻 700回...君、エンジニアだね?", 800: "🐛 800回...これもうバグレポートだよ", 900: "⭐ 900回...GitHubにissue立てそう", 1000: "🎭 1000回...もし手作業やったら狂気の沙汰よ?暇やんw" }; // 詳細プロンプトの結果パターン const detailedResults = [ { emoji: '✅', text: 'そのままコピペ可能!', success: true }, { emoji: '✅', text: '完璧な280文字以内!', success: true }, { emoji: '✅', text: '魅力的なキャッチフレーズ付き!', success: true }, { emoji: '✅', text: '適切なハッシュタグ自動生成!', success: true }, { emoji: '✅', text: '記事内容を的確に要約!', success: true }, { emoji: '✅', text: 'エンゲージメント促進要素あり!', success: true }, { emoji: '✅', text: '今日もちゃんとした投稿文!', success: true }, { emoji: '✅', text: '狙った通りのアプローチ!', success: true }, { emoji: '😅', text: '少し誇張表現が...(軽微)', success: false }, { emoji: '😅', text: '微調整が必要かも', success: false } ]; let simpleCount = 0; let detailedCount = 0; let simpleSuccess = 0; let detailedSuccess = 0; function spinSimple() { const display = document.getElementById('simple-display'); const stats = document.getElementById('simple-stats'); // スピン効果 display.classList.add('spinning'); display.textContent = '🎲'; setTimeout(() => { display.classList.remove('spinning'); simpleCount++; // 10の倍数の時は煽り文句を表示 if (simpleCount % 10 === 0 && gachaMessages[simpleCount]) { const message = gachaMessages[simpleCount]; display.innerHTML = ` ${message} `; stats.textContent = `${simpleCount}回目の挑戦!まだまだ諦めるな!`; // 特別メッセージの時はconsoleにも出力 if (simpleCount >= 300) { const consoleMessages = { 300: "console.log('🤖 スクリプト組んでる?😏');", 400: "console.log('⚙ setInterval(() => { spinSimple(); }, 100); ←これでしょ?😂');", 500: "console.log('🔧 while(true) { spinSimple(); } やめなさい😅');", 600: "console.log('💻 もうconsoleで見てるでしょ?👀');", 700: "console.log('👨‍💻 君、絶対エンジニアだね!');", 800: "console.log('🐛 これもうバグレポートレベル!');", 900: "console.log('⭐ GitHubにissue立てる気でしょ?');", 1000: "console.log('\\n' + '╔══════════════════════════════════════════════╗\\n' + '║ 🎉 1000回達成記念 🎉 ║\\n' + '║ ║\\n' + '║ ⠀⠀⠀⠀⢀⣴⠟⠛⠛⠻⣦⡀⠀⠀⠀⠀⠀ ║\\n' + '║ ⠀⠀⠀⣰⠟⠁⠀⠀⠀⠀⠈⠻⣆⠀⠀⠀⠀ ║\\n' + '║ ⠀⠀⢠⡟⠀⠀⠀⠀⠀⠀⠀⠀⢻⡄⠀⠀⠀ ║\\n' + '║ ⠀⠀⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⠀⠀⠀ ║\\n' + '║ ⠀⠀⢿⡀⠀⠀⠀⠀⠀⠀⠀⠀⢀⡿⠀⠀⠀ ║\\n' + '║ ⠀⠀⠀⠙⢦⣀⠀⠀⠀⠀⣀⡴⠋⠀⠀⠀⠀ ║\\n' + '║ ║\\n' + '║ 🏆 君こそ真のガチャマスター! 🏆 ║\\n' + '║ ║\\n' + '║ 🤔 でもさ...もし手作業やったら狂気よ? ║\\n' + '║ 😏 そのコードレビュー力で詳細プロンプト ║\\n' + '║ 書いた方が早いけどね〜 ║\\n' + '║ ║\\n' + '║ エンジニア大好きw ║\\n' + '╚══════════════════════════════════════════════╝\\n');" }; if (consoleMessages[simpleCount]) { eval(consoleMessages[simpleCount]); } } } else { const result = simpleResults[Math.floor(Math.random() * simpleResults.length)]; display.textContent = result.emoji; stats.textContent = result.text; if (result.success) simpleSuccess++; } updateStats('simple', simpleCount, simpleSuccess); }, 1000); } function spinDetailed() { const display = document.getElementById('detailed-display'); const stats = document.getElementById('detailed-stats'); // スピン効果 display.classList.add('spinning'); display.textContent = '🎯'; setTimeout(() => { display.classList.remove('spinning'); const result = detailedResults[Math.floor(Math.random() * detailedResults.length)]; display.textContent = result.emoji; stats.textContent = result.text; detailedCount++; if (result.success) detailedSuccess++; updateStats('detailed', detailedCount, detailedSuccess); }, 1000); } function updateStats(type, count, success) { const rate = count > 0 ? Math.round((success / count) * 100) : 0; const statsElement = document.getElementById(type + '-stats'); if (count > 0) { statsElement.innerHTML = `${statsElement.textContent} 試行回数: ${count}回 | 成功率: ${rate}% (${success}/${count}) `; } } Claudeの特性を理解した 詳細プロンプトを組んだとしても、1割程度は事実に基づかない投稿文が生成されるというのは盲点でした。特にプロンプトで評価フェーズを挟んでいても、評価フェーズを飛ばしてしまう場合があります。 また、指示の出し方によって、生成物がこんなにも変わってしまうというのは面白いですね。概念としては理解していたんですが、今まで「ギャル風に」とか「夜の蝶とおじさんの会話」みたいなふざけたプロンプトしか打ってこなかったので、これは面白い発見でした。 詳細プロンプトを作るだけで、今までXの投稿にかけていた時間が大幅に短縮されました。単純にプロンプトを使って生成するだけでも、従来あたり5分から10分程度かかっていたのですが、実働としては本当に1分で完了するものとなっています。 あとはこれがシステム化できれば、人間は生成物を確認するだけで良くなりそうです。この辺は作り込みが必要だなと思っています。まぁ僕はエンジニアなのでそういうシステムみたいなところは本職なところがあるんですけどね。 「Claudeは万能じゃない」ことがわかった 指示の出し方で全然違う結果になる 少し手間をかけるだけで劇的に改善 Claudeは「賢いけど指示待ち」な感じ 思わぬ落とし穴:トークン数の問題 実際に直面した問題点についてお話ししたいと思います。 Claudeの制限に引っかかる 最近では月曜日に1週間分の投稿をまとめて生成するのですが、6記事程度作成したらClaudeの制限に引っかかりました。 ClaudeのProプラン で使っているのですが、使用回数を使いつぶしてしまったようです。原因として考えられたのは、詳細プロンプトによって膨大な処理が行われているのではないかという点です。こちらを検証してみました。 犯人は詳細プロンプト?調査してみた こちらの処理は、APIで叩いているわけではないので、正式なトークン数がわかるわけではありません。なので、チャットで入力したトークン数、出力したトークン数の合計値を出してシミュレートしました。詳細プロンプトと簡易プロンプトでそれほど差がないことがわかりました。そして1番トークン数を消費していたのはweb_fetchで取得したHTMLでした。 web_fetchで取得したコンテンツの内訳 取得データ全体 : 約18,000-20,000トークン 実際の記事内容 : 約12,000-14,000トークン HTMLタグ・構造データ : 約4,000-6,000トークン プロンプト別の合計トークン数 簡易プロンプト : 約18,000~26,000トークン(平均:22,000トークン) 詳細プロンプト : 約23,000-29,000トークン(平均26,000トークン) 意外な事実が発覚 詳細プロンプトと簡易プロンプトにそこまで大差があるわけではないという事実が発覚しました。プロンプトの差としては5,000トークン程度でそんなに重いものではないという結論になりました。というか、HTML重すぎじゃん! 大きな差があるわけではない プロンプトの差:約5,000トークン程度 というかHTML重すぎじゃん! 本当の犯人はHTMLデータだった はい、本当の犯人はHTMLだったわけなんですけども、まぁこれって当然のことなんですよね。web_fetchで取得しているということは記事全体を取得しているわけであって、ヘッダーやフッター、メインのコンテンツに関係ないサブメニューなども全て取得しているわけです。それは余計な情報もついてきて重くなりますよね。 なので、記事の本文自体は大体半分程度に圧縮できると考えています。で、ここから面白いところなんですけど、 実は僕、以前にも遭遇しているんですよね。過去のシステムをGASを使ってリファクタリングしてプロトタイプを作ってやってみているんですけども、その時にも同様の問題に当たって、その時はHTMLから本文を抜き出すという処理をスクリプト化して実行していました。 なので、もしこのままプロンプトを使うというよりかは、本文だけを抜き出す工夫が必要になると考えています。システム化にあたっては、HTMLの処理部分は機械的にシステム化し、本来の生成と検証部分に注力してもらう方が効果的であると考えています。というかそうしないとトークン数がバカでかいシステムができてしまうのでヤバいですよね。 使えるプロンプトの作り方 私が学んだ基本ポイント はい、ここでは私が学んだ基本のプロンプトの作り方についてお話ししていきます。基本的なところとしては、実行計画はAIからの出力を安定化させるために重要な内容になっています。簡易プロンプトではAI自身に考えさせる余地が残っているため、自由な発想で自由な判断を行っていますが、実行計画として行動指針を立ててあげることによってAIの思考を誘導することができます。また、例示を載せることによって、そこを起点に考えてもらえるので、ワンショットラーニングやフューショットラーニングなども効果的です。 「何文字以内で」は絶対に入れる 「~な感じで」と雰囲気を伝える 「ハッシュタグも付けて」と具体的に指示 ダメな例も一緒に教える 詳細プロンプトで使った8つのテクニック 段階的実行 :複雑な処理をステップ分割 制約優先 :「〜してはいけない」を先に明確化 テンプレート化 :使い回せる構造を作る 検証組み込み :生成→チェック→修正の流れ 複数パターン :用途別に数種類用意 具体的例示 :曖昧な指示は避ける 自動実行 :条件を満たしたら勝手に動く 構造化出力 :毎回同じフォーマットで結果表示 プロンプト改善のコツ プロンプトの改善のコツとしては、定期的にプロンプトを修正しようとする意識を持つことです。今回付録で載せているプロンプトも実はもう5回ほどアップデートを重ねて調整しています。定期的に自分の求めているものと異なる場合、遠慮なくプロンプトを修正していくべきだと思います。 この点は、システムプロンプトをバージョン管理しておくことも1つの手だと思っています。はい、なので、GitやClaude Code、Gemini CLIでそういった環境を作るのもアリではないでしょうか。 まとめ 実験で証明された「プロンプト設計」の威力 今回の検証を通して、同じClaudeでも「どう頼むか」で全然違う結果になることが実証できました。簡易プロンプトでは8割の投稿文に人間の手直しが必要だったのに対し、詳細プロンプトでは8割がそのままコピペで使えるレベルまで改善しました。 特に印象的だったのは、詳細プロンプトで生成される投稿文の 安定性 ですね。「今日は当たり、昨日はハズレ」というガチャ要素がほぼなくなって、毎回安心して使える品質になったのは大きな収穫でした。 「AI丸投げ」から「AI協働」への転換 最初は「URLを渡したら勝手に完璧な投稿文を作ってくれる」なんて夢を見ていましたが、現実はそう甘くありませんでした。でも、これって逆に言えば 人間の工夫次第でAIのパフォーマンスを劇的に向上させられる ということでもあります。 AIを「万能の魔法」として期待するのではなく、「すごく優秀だけど指示待ちな部下」として適切にマネジメントすることで、期待以上の成果を得られることがわかりました。 見えてきた課題と現実的な解決策 トークン数の問題は正直、盲点でした。HTMLの重さが予想以上で、詳細プロンプトよりもweb_fetchで取得するデータの方がよっぽど「重い」という事実は面白い発見でしたね。 この問題に対しては、過去に GASでHTMLパースしてた経験 が活かせそうです。本文抽出を前処理で自動化すれば、トークン数を半分以下に圧縮できる見込みがあります。まさに「過去の自分、ナイス」って感じです。 2週間運用してみた正直な感想 実際に運用してみて、X投稿にかける時間が 5-10分から1分程度 まで短縮されたのは素直に嬉しいです。「投稿文を考えるのが面倒」「何を書けばいいかわからない」という悩みがほぼ解決されました。 ただし、1割程度は事実に基づかない投稿文が生成されることもあるので、「完全自動化」ではなく「半自動化」として運用するのが現実的だと感じています。人間の最終チェックは必須ですね。 これからプロンプトエンジニアリングを始める人へ もし皆さんも「Claudeに○○を任せたけど、思ったような結果が得られなかった」という経験があるなら、ぜひプロンプト設計を見直してみてください。今回の実験で分かったように、少しの工夫で劇的に改善することがあります。 特に重要なのは: 具体的な制約条件 を明記する(文字数、文体、必須要素など) 段階的な実行計画 をプロンプトに組み込む 複数パターン の出力で選択肢を増やす 定期的なプロンプト見直し で継続改善する 次のステップ:完全自動化への道 今後は、HTMLパースの自動化、プロンプトのバージョン管理、そして最終的にはシステム化まで進めていきたいと思っています。Claude Codeとかも使ってみたいですしね。 でも一番大切なのは、「AIと人間が協力して、お互いの得意分野を活かす」という視点だと思います。完全自動化が目標ではなく、 人間がより創造的な部分に集中できる環境 を作ることが本当のゴールなのかもしれません。 皆さんも、ぜひ自分なりのプロンプトエンジニアリングにチャレンジしてみてください。きっと「これは便利だ!」と思える瞬間があるはずです。 そして、良いプロンプトができたら、ぜひ社内やコミュニティでシェアして、みんなで効率化の輪を広げていきましょう! 付録:詳細プロンプト全文 ```markdown # エンジニア向けX投稿自動生成システム ## 🎯 概要 URLのみの入力で、記事品質評価から3パターンの投稿文まで自動生成する包括的システム。**リンクカード確実表示**を最優先に設計し、**記事内容の正確性検証**により誇張表現を排除。 ----- ## 🔄 自動実行プロセス ### **Phase 1: 記事分析・品質評価** #### **Step 1-1: 記事内容完全取得** ``` 実行内容: - web_fetchでブログ記事の完全コンテンツ取得 - 記事構造の解析(見出し、コードブロック、図表等) - 技術的内容の抽出と整理 - 具体的な数値・効果・事例の正確な記録 ``` #### **Step 1-2: 技術的正確性評価** ``` 評価項目: 【技術スタック正確性】 - 使用技術の最新性(2024-2025年基準) - 技術的実装の適切性 - セキュリティ・ベストプラクティスの考慮 【実装レベル判定】 - プロトタイプ / 基本実装 / 本格実装 / 企業レベル - コードの完成度・品質 - エラーハンドリング・例外処理の有無 【対象読者レベル】 - 初級者(学習中・1-2年目) - 中級者(実務経験3-5年) - 上級者(リーダー・アーキテクト級) - 専門家(特定分野のエキスパート) 総合評価:A(高品質)/ B(標準)/ C(要注意) ``` #### **Step 1-3: 記事価値・差別化ポイント抽出** ``` 分析内容: - 解決する具体的課題 - 既存解決策との違い・優位性 - 実用性・再現性の程度 - 学習コスト・導入難易度 - ビジネス価値・ROI - 【重要】記事内で言及されている具体的な効果・数値の正確な記録 ``` ### **Phase 2: ハッシュタグ効果分析** #### **Step 2-1: 技術分野別ハッシュタグ調査** ``` 検索戦略(2-4回のweb_search実行): 検索1: "[主要技術名] ハッシュタグ 効果的 X Twitter 2025" 検索2: "[技術領域] エンジニア ハッシュタグ トレンド 2025" 検索3: "[解決課題] 自動化 効率化 ハッシュタグ X" 検索4: "[関連ツール] [技術スタック] ハッシュタグ 人気" ``` #### **Step 2-2: ハッシュタグ効果性評価・選定** ``` 評価基準: - トレンド性(2024-2025年の話題性) - 技術特化度(エンジニアの検索意図との一致) - 関連性(記事内容との直接的関連度) - バイト効率(半角英語活用による節約効果) 選定ルール: - 6個以内 - 合計30-40バイト以内 - 半角英語優先(#Azure, #API, #OSS等) ``` ### **Phase 3: 投稿文作成(3パターン)** #### **🚨 リンクカード表示最優先ルール** ``` 【必須適用事項】 1. URLを投稿文の冒頭に単独行で配置 2. URL前後にスペース・改行で明確分離 3. 240文字以内でもURL優先配置 4. 絵文字・ハッシュタグはURL後に配置 5. 他のテキストや絵文字と混在させない 【配置テンプレート】 https://example.com [投稿文本文] #ハッシュタグ ``` #### **Aパターン: 効果重視・数値訴求型** ``` 【特徴】 - 記事内で言及されている具体的な数値効果のみを使用 - Before/After型の改善アピール(記事内の事実に基づく) - 実用性・効率化を強調(記事内容の範囲内で) 【構成テンプレート】 https://example.com 🚀 [記事内の具体的数値効果]で[技術課題]を解決! 📝 [記事内の具体的解決手法]で[記事内の効果]を実現 ⚡ 技術スタック: [半角英語技術名] 🔧 [記事内の実装詳細・注意点] #ハッシュタグ 【使用可能な表現例】 - 記事内で「30分→3分に短縮」と記載されている場合のみ使用 - 記事内で「90%削減」と記載されている場合のみ使用 - 記事内で「月40時間節約」と記載されている場合のみ使用 ``` #### **Bパターン: 課題共感・解決提案型** ``` 【特徴】 - 記事内で言及されている課題のみを使用 - 記事内で提案されている解決策のみを紹介 - 誇張のない共感を呼ぶ問題提起 【構成テンプレート】 https://example.com 😰 [記事内で言及される課題]で困ってませんか? 💡 [記事内の技術手法]を使えば[記事内の解決方法]で楽になります ⚡ [記事内の具体的技術スタック]で実装 🎯 [記事内の期待効果・メリット] #ハッシュタグ 【使用可能な表現例】 - 記事内で「手動でSlack通知作成の課題」が言及されている場合のみ - 記事内で「Google Docsの更新確認の問題」が言及されている場合のみ - 記事内で「レポート作成の時間コスト」が言及されている場合のみ ``` #### **Cパターン: 技術トレンド・学習促進型** ``` 【特徴】 - 記事内で言及されている技術動向のみを使用 - 記事内で説明されているスキルアップ内容のみを紹介 - 記事内で言及されている将来性・価値のみをアピール 【構成テンプレート】 https://example.com 🔥 [記事内で言及される技術分野]の実装方法 📚 [記事内の技術手法・ツール]の使用方法を解説 ⭐ [記事内の学習メリット・価値] 🚀 [記事内の実際の活用場面・価値] #ハッシュタグ 【使用可能な表現例】 - 記事内で「2025年のAI活用」が言及されている場合のみ - 記事内で「NotebookLMとSlack連携」が説明されている場合のみ - 記事内で「技術の評価への影響」が言及されている場合のみ ``` ### **Phase 4: 📋 記事内容検証・誇張表現排除** #### **Step 4-1: 投稿文と記事内容の照合** ``` 【検証項目】 1. 数値効果の正確性 - 投稿文の数値が記事内に明記されているか - 推測・推定ではなく実証された数値か - 文脈が正しく反映されているか 2. 技術的主張の正確性 - 投稿文の技術的効果が記事内で実証されているか - 理論的可能性ではなく実装済みの内容か - 制限事項・注意点が適切に反映されているか 3. 解決課題の正確性 - 投稿文の課題が記事内で具体的に言及されているか - 汎用的な「あるある」ではなく記事固有の課題か - 解決方法が記事内で実際に提示されているか 4. 将来性・価値の正確性 - 投稿文の価値主張が記事内で根拠付けられているか - 著者の推測ではなく客観的事実に基づいているか - 業界動向との整合性が取れているか ``` #### **Step 4-2: 誇張表現の特定と修正** ``` 【誇張表現の判定基準】 ⚠ 以下は誇張表現として判定・修正対象: 1. 記事内に記載のない具体的数値 - 「90%削減」「10倍高速化」等(記事内に根拠なし) - 「月40時間節約」等(記事内に計算根拠なし) - 「30分→3分短縮」等(記事内に実測データなし) 2. 記事内に記載のない効果・価値 - 「劇的改善」「ゲームチェンジャー」等(記事内に根拠なし) - 「チーム全体で効率化」等(記事内に事例なし) - 「来年の評価が変わる」等(記事内に根拠なし) 3. 記事内に記載のない課題・問題 - 「まだ手動で〜してるの?」等(記事内に言及なし) - 「毎週2時間かけてませんか?」等(記事内に統計なし) - 汎用的な「あるある」課題(記事固有ではない) 4. 記事内に記載のない技術トレンド - 「2025年注目の〜」等(記事内に根拠なし) - 「業界標準になる」等(記事内に予測なし) - 「この技術を知ってるかで〜」等(記事内に根拠なし) ``` #### **Step 4-3: 投稿文の再生成** ``` 【再生成トリガー】 - 3パターンのうち1つでも誇張表現が検出された場合 - 記事内容と投稿文の整合性が取れない場合 - 数値・効果の根拠が記事内で確認できない場合 【再生成ルール】 1. 記事内で明記されている内容のみを使用 2. 推測・推定・一般論は使用しない 3. 具体的数値は記事内の実証データのみ使用 4. 効果・価値は記事内の根拠付きのもののみ使用 5. 課題は記事内で具体的に言及されているもののみ使用 【再生成プロセス】 1. 誇張表現を特定 2. 記事内の対応する正確な情報を確認 3. 正確な情報に基づいて投稿文を再構築 4. 再度検証を実施 5. 問題がなければ最終版として採用 ``` ### **Phase 5: 投稿時間最適化** #### **エンジニア向け最適投稿時間** ``` 【平日優先時間帯】 - 朝の通勤時間:8:00-9:00 - 昼休み:12:00-13:00 - 夜の学習時間:21:00-22:00 【技術記事特化時間】 - 火曜日〜木曜日の21:00-22:00(最優先) - 月曜日 12:00-13:00(週始めキャッチアップ) - 金曜日 18:00-19:00(週末学習準備) 【記事品質連動調整】 - A評価:火曜21:00(エンジニア活動ピーク) - B評価:水曜12:00(昼休み気軽チェック) - C評価:金曜18:00(週末実験タイム) ``` ----- ## 📊 最終出力フォーマット ### **自動実行トリガー** ``` 以下の場合に自動的にこのプロセスを実行: 1. URLのみが入力された場合 2. "投稿パターン作成"等の指示がある場合 3. "A/Bテスト版"等の指示がある場合 4. "完全版"等の包括的分析要求がある場合 ``` ### **出力テンプレート** ```markdown ## ブログの評価 【総合評価】A / B / C 【技術的正確性】⭐⭐⭐⭐⭐ (5点満点) 【実装レベル】プロトタイプ / 基本実装 / 本格実装 / 企業レベル 【対象読者】初級者 / 中級者 / 上級者 / 専門家 【実用性】⭐⭐⭐⭐⭐ (5点満点) 【主要な技術要素】 - [抽出された技術スタック] - [解決する課題] - [提供価値・効果] 【記事内の具体的な数値・効果】 - [記事内で明記されている具体的数値] - [記事内で実証されている効果] - [記事内で言及されている改善点] 【注意事項・制限】 - [記事内で言及されている制限事項] - [実装時の注意点] 【誇張表現検証結果】 - [初回生成で検出された誇張表現] - [修正内容] - [最終版での整合性確認結果] ## ブログの投稿パターン ### Aパターン - 投稿文: [280バイト以内の効果重視型投稿文] - 記事内容との整合性: ✅ 確認済み - 投稿推奨時間: [具体的日時と根拠] ### Bパターン - 投稿文: [280バイト以内の課題共感型投稿文] - 記事内容との整合性: ✅ 確認済み - 投稿推奨時間: [具体的日時と根拠] ### Cパターン - 投稿文: [280バイト以内の学習促進型投稿文] - 記事内容との整合性: ✅ 確認済み - 投稿推奨時間: [具体的日時と根拠] ``` ----- ## 🔧 品質保証チェックリスト ### **記事品質評価の正確性** - [ ] 技術的事実の正確性検証 - [ ] 実装レベルの適切な判定 - [ ] 対象読者レベルの妥当性確認 - [ ] 制限事項・注意点の適切な反映 ### **誇張表現検証の徹底** - [ ] 投稿文の全数値が記事内で確認済み - [ ] 効果・価値の主張が記事内で根拠付け済み - [ ] 課題の指摘が記事内で具体的に言及済み - [ ] 技術トレンドの主張が記事内で根拠付け済み ### **リンクカード表示保証** - [ ] URLが投稿文の冒頭に単独行で配置 - [ ] URL前後のスペース・改行による明確分離 - [ ] 240文字超投稿でも冒頭URL配置 - [ ] 絵文字・ハッシュタグがURL後に配置 ### **3パターンの差別化** - [ ] 3つのアプローチが明確に異なる - [ ] 各パターンのターゲット読者が明確 - [ ] バイト数最適化が各版で実現 - [ ] 品質評価結果が適切に反映 - [ ] 全パターンで記事内容との整合性確認済み ### **ハッシュタグ効果分析** - [ ] 2-4回のweb_search実行 - [ ] 技術分野別の効果的ハッシュタグ調査 - [ ] トレンド性・関連性の評価 - [ ] バイト効率を考慮した選定 ----- ## 使用方法 URLを入力するだけで自動実行されます。出力はArtifact形式で生成され、チャットでの説明は最小限に抑制されます。 ``` 例:https://tech-blog.example.com/article-url ``` この改良版により、**記事内容の正確性を保証**し、**誇張表現を完全に排除**しながら、**リンクカード確実表示**と**シンプルな出力フォーマット**を両立したシステムが完成しました。 ``` ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 1人がこの投稿は役に立ったと言っています。 The post 【2025年版】Claudeプロンプト設計術|X投稿文の品質を劇的改善 first appeared on SIOS Tech. Lab .
こんにちは織田です 今回はRAG-STDの開発中にPyJWTを用いてトークン検証を実施しましたので、そこで得た知見についてまとめようと思います。 JWTおよびPyJWTとは? JWT (JSON Web Token) とは、インターネット上で情報を安全にやり取りするための規格の一つで、特に認証や認可の分野で利用されています。 ヘッダー、ペイロード、署名(シグネチャ)の3つの要素からなり、それぞれが”.”で区切られています。 そして、PyJWTとはJWT トークンをエンコード、デコード、検証してくれる Python ライブラリです。 JWTの各要素の詳細は以下の通りです。 ヘッダー (Header) JWTのタイプ(通常は”JWT”)と、署名の生成に使用されるアルゴリズム(例: HMAC SHA256やRSA)がJSON形式で記述されます。これはBase64Urlエンコードされます。 例: {   "alg": "HS256",   "typ": "JWT" } ペイロード (Payload) ここに、JWTに含める情報(クレームと呼びます)が記述されます。クレームには以下の3種類があります。 登録済みクレーム (Registered Claims): JWTの仕様で定義されているクレームで、iss(発行者)、exp(有効期限)、sub(主題)などがあります。これらは任意ですが、利用することで相互運用性が高まります。 公開クレーム (Public Claims): 衝突を避けるためにIANAのJWTクレームレジストリに登録されているか、URIとして定義されているクレームです。 プライベートクレーム (Private Claims): 送信者と受信者の間で合意されたカスタムクレームです。 例: {   "sub": "1234567890",   ""name": "John Doe",   "admin": true } このペイロードもBase64Urlエンコードされます。 署名 (Signature) エンコードされたヘッダー、エンコードされたペイロード、そして秘密鍵を使用して、ヘッダーで指定されたアルゴリズムによって生成されます。この署名があることで、トークンが改ざんされていないこと、および発行者が正しいことを検証できます。 これら3つの要素が「.」で連結されることで、最終的なJWTが構成されます。 例:`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ` JWTを用いたトークン検証のプロセス RAG-STD開発で実施したJWTを用いたトークン検証は、主に以下のステップで構成されます。 クライアントからのJWT受け取り: フロントエンドから、ヘッダー情報を介してJWTを受け取ります。 JWTの解析: 受け取ったJWTをヘッダー、ペイロード、署名の3つの部分に分割します。 署名の検証: 受け取ったJWTのヘッダーとペイロードを再度エンコードします。 サーバーが保持している秘密鍵(または公開鍵)と、エンコードされたヘッダー・ペイロードを用いて、新たな署名を生成します。 生成された署名と、受け取ったJWTに付与されていた署名を比較します。両者が一致すれば、トークンは改ざんされておらず、有効であると判断できます。一致しない場合は、トークンが無効であると判断し、リクエストを拒否します。 ペイロードの検証: 署名の検証に成功したら、ペイロードに含まれるクレームを検証します。 `exp` (有効期限) クレームを確認し、トークンが期限切れでないかを確認します。期限切れであれば、トークンは無効です。 `iss` (発行者) クレームを確認し、信頼できる発行元からのトークンであるかを確認します。 必要に応じて、`sub` (主題) やその他のカスタムクレーム(例: ユーザーID、ロールなど)を検証し、リクエスト元のユーザーが適切な権限を持っているかを確認します。 これらの検証ステップを全てクリアした場合、トークンは有効とみなされ、ユーザーは認証・認可された状態となります。 トークン検証時のポイント 実際のコードでは、フロントエンドから発行されたIDトークンの検証において、以下のような流れで処理を進めました。 公開鍵の一覧を取得: IDトークンを発行した認証プロバイダ(今回はMicrosoft)が公開している公開鍵の一覧(JWKS: JSON Web Key Set)を取得します。これは通常、特定のURLからJSON形式で提供されます。 IDトークンの署名を検証する公開鍵を一覧の中から抽出: 取得した公開鍵の一覧から、検証対象のIDトークンのヘッダーにある`kid` (Key ID) に対応する公開鍵を特定し、抽出します。これにより、正しい公開鍵を使って署名を検証できます。 署名検証ありでデコード: 抽出した公開鍵とIDトークンを用いて、署名の検証を行いながらトークンをデコードします。このステップで、トークンが改ざんされていないか、正しい発行者によって署名されたものかを確認します。PyJWTのようなライブラリを使用すると、この署名検証はデコード処理の一部として自動的に行われます。 ユーザー情報を抽出: 署名検証に成功したら、デコードされたペイロードからユーザ名やグループIDなどの必要なユーザ情報を抽出します。この情報をもとに、アプリケーション内でユーザーの識別や認可を行います。 その他のポイントとしては、以下のものがあげられます。 エラーハンドリング: 署名検証失敗、期限切れ、不正なペイロードなど、あらゆるケースで適切なエラーハンドリングを行うことが重要です。自分が実装しようとした当初はエラーハンドリングを適切に設定できておらず、エラーが発生しても原因が何か分からず悪戦苦闘しました… ライブラリの活用: PyJWTを活用することで、ルーティングごとに検証処理を記述する手間を省き、コードの可読性と保守性を高めました。当初は「検証のコードを自前で用意しなければならないのか…」と不安でしたが、PyJWTを用いることで僅かな行数で実装できました。 `aud` (Audience) クレームの理解: トークンをデコードする際に、`aud` (Audience) クレームの検証に苦労しました。これは、トークンが「誰(どのサービス)のために発行されたものか」を示す、いわば手紙の「宛名」のような概念です。JWTを受け取る側(つまり、検証を行う私たちのアプリケーション)が、そのトークンの`aud`クレームに自身が含まれているかを確認することで、意図しない宛先に送られたトークンの利用を防ぐことができます。 まとめ RAG-STDの開発を通して、JWTの仕組みと検証プロセス、そして実装上の注意点について深く理解することができました。検証用のコードを0から書くのは大変だろうと身構えていたのですが、PyJWTを使うことで、簡単に実装できました。また、コードを段階的に実装していくことで、検証に必要なプロセスについて詳しく理解することができました。この知見が、今後JWTを用いた認証・認可システムを構築される方々の一助となれば幸いです。 ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post PyJWTを用いたアクセストークンの検証をやってみた first appeared on SIOS Tech. Lab .
PSSLの佐々木です。 今回は、Dockerのマルチステージビルドを使ってPythonアプリケーションのサイズを削減する方法を解説します。 JavaやGoのようなコンパイル言語であればビルド時と実行が明確に分かれており、実行時にはバイナリだけあればよいのでマルチステージビルドと相性よく組み合わせて容量を削減できるというのは非常にわかりやすいと思いますが、PythonやRubyのよなインタプリター言語の場合には効果があるのかないのかいまいちピンとこなかったので自身の検証もかねてブログにまとめました。 そもそもマルチステージビルドって何? マルチステージビルドとは、1つのDockerfile内で複数の段階(ステージ)に分けてイメージを構築する手法です。 従来の問題 : Pythonパッケージをビルドするためにgccやg++が必要 しかし、実行時にはビルドツールは不要 でも、従来は最終イメージにビルドツールが残ってしまう マルチステージビルドの解決策 : ビルドステージ :必要なパッケージをコンパイル・ビルド 実行ステージ :ビルド結果だけを持ってきて軽量なイメージを作成 実際のDockerfileで比較してみよう 従来のシングルステージビルド # シングルステージビルド用Dockerfile FROM python:3.13-slim # ビルド時に必要なパッケージをインストール RUN apt-get update && apt-get install -y \\ gcc \\ g++ \\ && rm -rf /var/lib/apt/lists/* # 非rootユーザーを作成(ホームディレクトリも作成) RUN groupadd -r appuser && useradd -r -g appuser -m appuser # 作業ディレクトリを作成 WORKDIR /app # 依存関係ファイルをコピー COPY requirements.txt . # 依存関係をインストール RUN pip install --no-cache-dir --upgrade pip && \\ pip install --no-cache-dir -r requirements.txt # アプリケーションコードをコピー COPY . . # 不要なファイルとディレクトリを削除 RUN find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true && \\ find . -type f -name "*.pyc" -delete && \\ rm -rf venv/ && \\ rm -rf .git/ && \\ rm -rf .pytest_cache/ && \\ rm -rf *.egg-info/ && \\ rm -rf .coverage && \\ rm -rf htmlcov/ # アプリケーションディレクトリの所有者を変更 RUN chown -R appuser:appuser /app # promptflowが使用するディレクトリを作成 RUN mkdir -p /home/appuser/.promptflow && \\ chown -R appuser:appuser /home/appuser/.promptflow # 非rootユーザーに切り替え USER appuser # 環境変数でホームディレクトリを明示的に設定 ENV HOME=/home/appuser # ポートを公開 EXPOSE 5000 # ヘルスチェック HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\ CMD python -c "import urllib.request; urllib.request.urlopen('<http://localhost:5000/api/health>')" || exit 1 # アプリケーションを実行 CMD ["python", "app.py"] マルチステージビルド # マルチステージビルド用Dockerfile # ステージ1: ビルドステージ FROM python:3.13-slim AS builder # ビルド時に必要なパッケージをインストール RUN apt-get update && apt-get install -y \\ gcc \\ g++ \\ && rm -rf /var/lib/apt/lists/* # 作業ディレクトリを作成 WORKDIR /build # 依存関係ファイルをコピー COPY requirements.txt . # 依存関係をインストール(グローバルにインストール) RUN pip install --no-cache-dir --upgrade pip && \\ pip install --no-cache-dir -r requirements.txt # アプリケーションコードをコピー COPY . . # 不要なファイルとディレクトリを削除 RUN find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true && \\ find . -type f -name "*.pyc" -delete && \\ rm -rf venv/ && \\ rm -rf .git/ && \\ rm -rf .pytest_cache/ && \\ rm -rf *.egg-info/ && \\ rm -rf .coverage && \\ rm -rf htmlcov/ # ステージ2: 実行ステージ FROM python:3.13-slim AS runtime # 実行時に必要な最小限のパッケージをインストール RUN apt-get update && apt-get install -y \\ && rm -rf /var/lib/apt/lists/* # 非rootユーザーを作成(ホームディレクトリも作成) RUN groupadd -r appuser && useradd -r -g appuser -m appuser # 作業ディレクトリを作成 WORKDIR /app # ビルドステージからPythonパッケージをコピー COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages COPY --from=builder /usr/local/bin /usr/local/bin # アプリケーションコードをコピー COPY --from=builder /build/app.py . COPY --from=builder /build/connection.yaml . COPY --from=builder /build/flows ./flows/ COPY --from=builder /build/services ./services/ # アプリケーションディレクトリの所有者を変更 RUN chown -R appuser:appuser /app # promptflowが使用するディレクトリを作成 RUN mkdir -p /home/appuser/.promptflow && \\ chown -R appuser:appuser /home/appuser/.promptflow # 非rootユーザーに切り替え USER appuser # 環境変数でホームディレクトリを明示的に設定 ENV HOME=/home/appuser # ポートを公開 EXPOSE 5000 # ヘルスチェック HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\ CMD python -c "import urllib.request; urllib.request.urlopen('<http://localhost:5000/api/health>')" || exit 1 # アプリケーションを実行 CMD ["python", "app.py"] マルチステージビルドの3つのメリット 1. サイズ削減 不要なビルドツールや中間ファイルが除去されるため、イメージサイズが大幅に小さくなります。 2. セキュリティ向上 実行時に不要なツール(gcc、コンパイラなど)が含まれないため、攻撃面が減少します。 3. デプロイ高速化 イメージが軽量になることで、レジストリへのpush/pullが高速化されます。 実装のポイント AS句でステージに名前をつける FROM python:3.13-slim AS builder # ← 「builder」という名前 FROM python:3.13-slim AS runtime # ← 「runtime」という名前 COPY –fromで必要なファイルだけを移動 # builderステージからsite-packagesだけをコピー COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages 不要ファイルの削除 RUN find . -name "__pycache__" -exec rm -rf {} + && \\ find . -name "*.pyc" -delete 開発環境での使い方 実際にマルチステージビルドを開発で使う場合は、Docker Composeと組み合わせるのがおすすめです。 services: app: build: . # マルチステージDockerfileを使用 ports: - "5000:5000" volumes: - .:/app # 開発時はコードをマウント environment: - FLASK_DEBUG=1 実際にどれくらい効果があるの? 筆者が実際にPythonアプリケーション(promptflowやFlaskを使用)で試した結果がこちらです: REPOSITORY TAG IMAGE ID CREATED SIZE my-app-single latest 99aa62f1267a 1 minute ago 1.45GB my-app-multi latest 27b56911a385 6 minutes ago 636MB 約810MB(56%)の削減に成功しています。 この差は特に以下のような場面で威力を発揮します: CI/CDパイプライン :デプロイ時間の短縮 本番環境 :ストレージコストの削減 開発チーム :イメージ共有の高速化 なぜPythonでもマルチステージビルドの効果があるのか 記事の頭でも気になっていたインタプリター言語でもなぜマルチステージビルドで容量を削減できるのか少し調べてみました。 Pythonのエコシステムでは結構コンパイル処理が発生しているようで、C / C++で書かれている部分を含んでいることが要因の一つでした。 具体的には以下のようなライブラリはC拡張コンパイルをしているようです。 numpy # 線形代数計算(BLAS/LAPACK) pandas # データ処理の高速化 pillow # 画像処理 lxml # XML/HTMLパーサー psycopg2 # PostgreSQLドライバー cryptography # 暗号化ライブラリ uwsgi # WSGIサーバー それによって作成される以下のような不要なファイルやコンパイルツールを含めずにイメージを作成することができるためマルチステービルドの恩恵をしっかりと受けられているのだと思います。 gcc/g++とその依存関係 apt-getのキャッシュ : ビルド時の一時ファイル その他のビルドツール まとめ マルチステージビルドは、少しの工夫で大きな効果を得られる優秀な技術です。コンパイル言語ほどではないですが、Pythonのようなインタプリター言語でもマルチステージビルドの効果を確認することができました。 またアプリケーション実行に最小の要素が何なのかわからないと構築が難しいですが、明らかに必要のないものを含めない状態でイメージを作成するだけでも効果があるので積極的に取り入れて段階的に最適化させていくのが良いかなと思いました。 ではまた ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post マルチステージビルドでPythonアプリケーションを軽量化 first appeared on SIOS Tech. Lab .
概要 こんにちは、サイオステクノロジーの安藤 浩です。 Bicep で Azure Container Apps と Jobs を構築したので、使い分けと実際のBicep でのパラメータとインフラを構築する方法を説明します。 Azure Container Apps とは Azure Container Apps  は、サーバーレスなコンテナプラットフォームで、コンテナー化されたアプリケーションを実行するため、保守が容易でコスト削減できます。 特徴: 主な特徴としては以下になるかと思います。 サーバーレス マイクロサービスに最適 自動スケーリング機能 (HTTP トラフィック、CPU,メモリの負荷状況、ServiceBus キューなど) 用途: 主な用途としては以下が考えられます。 Web アプリケーション/Web API マイクロサービスの実行 リアルタイム処理 Azure Container Apps Jobs とは Azure Container Apps Jobs  は、タスクベースのワークロードに特化したサーバーレスコンテナサービスです。 特徴: 主な特徴としては以下になるかと思います。 サーバーレス スケジュールトリガー、手動トリガー、イベントドリブン トリガー 実行履歴管理:ジョブ実行の状況が把握できる 並列実行のサポート ※Dapr、イングレスとカスタム ドメインや SSL 証明書などの関連機能 はサポートされていません。 用途: 主な用途としては以下が考えられます。 バッチ処理 データ処理パイプライン 定期的なメンテナンスタスク イベントドリブンな処理 Azure Container Apps と Azure Container Apps Jobs の使い分けポイント 以下の観点でAzure Container Apps と Azure Container Apps Jobs を使い分けることがポイントになると思います。 ポイント Azure Container Apps Azure Container Apps Jobs サービス種別 継続的に実行されるサービス タスク、バッチ処理 稼働形態 常時稼働、またはリクエスト/イベントに応じて起動 手動、スケジュール、またはイベントに応じて起動・実行・終了 主なトリガー HTTP、TCP 手動、スケジュール、イベント ユースケース Web API、Web アプリ、マイクロサービス 定期的なバッチ処理、データ処理 簡単に言うと、Azure Container AppsはWebアプリやAPIのように「継続的にリクエストされるWebアプリケーション」向け、Azure Container Apps Jobsはデータ処理やバッチ処理のような「実行して完了するタスク」向けのサービスです。 Bicep によるAzure Container Apps と Azure Container Apps Jobs の構築 ソースコードは以下に配置しています。 https://github.com/Hiroshi-Ando-sti/intro-bicep/tree/main/container-apps 上記のソースコードで利用しているサンプルとなるコンテナ イメージは以下のリポジトリで管理しています。 https://github.com/Hiroshi-Ando-sti/githubpkg-containerregistry 前提条件 以下をインストールします。 ツール Version Visual Studio Code Version: 1.101.2 Visual Studio Code 用の Bicep 拡張機能 0.36.177 最新の Azure CLI ツールまたは最新の Azure PowerShell バージョン 2.74.0 構成要素 以下の構成を構築します。 VNET Application Insights Container Environments Container Apps Container Apps Jobs Bicep コード 1. メインとなるBicep コード (infra/main.bicep) メインとなるコードの主要部分のみを記載します。 Container Apps やJobs を実行するために必要なリソースは VNET やContainer Apps Environment ですので、事前に作成します。 Container Apps と Container Apps Jobs は  containerAppsSettings  と  containerAppJobsSettings  のパラメータを利用して、複数作成できるようにしています。 module vnet './modules/network/vnet.bicep' = { name: 'vnet' params: { name: '${abbrs.networkVirtualNetworks}${environmentName}${suffixResourceName}${uniqueStr}' location: location tags: tags subnetName: 'default' } } var containerAppEnvironmentName = '${abbrs.appManagedEnvironments}${environmentName}${suffixResourceName}${uniqueStr}' module containerAppEnvironment './modules/container/containerAppsEnv.bicep' = { name: 'containerAppEnvironment' params: { vnetName: vnet.outputs.name environmentName: containerAppEnvironmentName location: location tags: tags logAnalyticsWorkspaceName: logAnalyticsWorkspaceName } } module containerAppJobs './modules/container/jobs.bicep' = [for containerAppJobsSetting in containerAppJobsSettings: { name: 'containerAppJobs-${containerAppJobsSetting.name}' params: { containerAppEnvironmentName: containerAppEnvironmentName location: location tags: tags containerAppJobsSetting: containerAppJobsSetting appInsightsConnectionString: appInsights.outputs.connectionString } }] module containerApps './modules/container/containerApps.bicep' = [for containerAppsSetting in containerAppsSettings: { name: 'containerApps-${containerAppsSetting.name}' params: { containerAppsName: '${abbrs.appContainerApps}${environmentName}-${containerAppsSetting.name}${uniqueStr}' location: location tags: tags containerAppEnvironmentName: containerAppEnvironmentName appInsightsConnectionString: appInsights.outputs.connectionString environmentName: environmentName resourceSuffixName: uniqueStrWithoutHyphen containerAppsSetting: containerAppsSetting } }] 2. Container Apps Environment モジュール (infra/modules/container/containerAppsEnv.bicep) VNET が必要であり、Container Apps Environmentの Log出力先としてLog analytics を指定します。 @description('VNet名') param vnetName string param location string = resourceGroup().location param environmentName string param logAnalyticsWorkspaceName string param tags object // Container Apps 環境の VNet を取得する。 resource vnet 'Microsoft.Network/virtualNetworks@2024-05-01' existing = { name: vnetName } resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2023-09-01' existing = { name: logAnalyticsWorkspaceName } // Container Apps Environment //https://learn.microsoft.com/ja-jp/azure/templates/microsoft.app/2025-01-01/managedenvironments?pivots=deployment-language-bicep resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2025-01-01' = { name: environmentName location: location tags: tags properties: { appLogsConfiguration: { destination: 'log-analytics' logAnalyticsConfiguration: { customerId: logAnalyticsWorkspace.properties.customerId sharedKey: logAnalyticsWorkspace.listKeys().primarySharedKey } } vnetConfiguration: { infrastructureSubnetId: vnet.properties.subnets[0].id } } } 3. Container Apps モジュール (infra/modules/container/containerApps.bicep) Container AppsのProperties では主に以下のパラメータを指定します。 properties のパラメータ 説明 managedEnvironmentId Container Apps Environment のIDを指定 configuration.ingress Container Apps イングレス の設定 configuration.secrets Container Apps 内のシークレット値 template.containers.image コンテナイメージ template.containers.resources CPU, メモリサイズの指定 template.containers.env コンテナの環境変数 template.scale.cooldownPeriod スケールインするまでの時間(秒)を指定 template.scale.minReplicas リビジョンごとの最小レプリカ数 template.scale.maxReplicas リビジョンあたりのレプリカの最大数 template.scale.rules レプリカを追加または削除するタイミングを決定するためにコンテナー アプリによって使用される条件。 スケールルール のトリガーは3つ指定可能で HTTP, TCP, カスタムメトリックがあります。例:http として、concurrentRequests: 10 とすると同時実行された HTTP 要求数がこの値を超えると、レプリカが 1つ追加されます。(maxReplicasまで) @description('Container Appsの名前') param containerAppsName string @description('すべてのリソースのロケーション') param location string = resourceGroup().location @description('Container Apps 環境の名前') param containerAppEnvironmentName string @description('Container Appsが属する環境名') param environmentName string @description('リソース名のサフィックス') param resourceSuffixName string @description('Container Appsの設定') @secure() param containerAppsSetting object @description('Application Insightsの接続文字列') param appInsightsConnectionString string @description('リソースに付与するタグ') param tags object = {} resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2025-01-01' existing = { name: containerAppEnvironmentName } //https://learn.microsoft.com/en-us/azure/templates/microsoft.app/2025-01-01/containerapps?pivots=deployment-language-bicep //https://learn.microsoft.com/ja-jp/azure/container-apps/scale-app?pivots=azure-cli resource containerApps 'Microsoft.App/containerApps@2025-01-01' = { name: containerAppsName location: location tags: tags properties: { managedEnvironmentId: containerAppEnvironment.id configuration: { ingress: { external: containerAppsSetting.ingress.external targetPort: containerAppsSetting.ingress.targetPort transport: containerAppsSetting.ingress.transport allowInsecure: containerAppsSetting.ingress.allowInsecure traffic: [ { latestRevision: true weight: 100 } ] } secrets: containerAppsSetting.secretVariables } template: { containers: [ { name: 'c-${environmentName}-${containerAppsSetting.name}-${resourceSuffixName}' image: containerAppsSetting.image resources: { cpu: json(containerAppsSetting.cpu) memory: containerAppsSetting.memory } env: union(containerAppsSetting.environmentVariables, [ { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' value: appInsightsConnectionString } ]) } ] scale: { cooldownPeriod: containerAppsSetting.scale.cooldownPeriod pollingInterval: containerAppsSetting.scale.pollingInterval minReplicas: containerAppsSetting.scale.minReplicas maxReplicas: containerAppsSetting.scale.maxReplicas rules: containerAppsSetting.scale.rules } } } } 4. Container Apps Jobs モジュール (infra/modules/container/jobs.bicep) containerAppJobsSetting をパラメータとして渡して、設定値をContainer Apps jobs の Properties に設定してあげます。 triggerType は  Event ,  Manual ,  Schedule  が選択できます。 triggerType — Event イベントドリブン ジョブをトリガーします。 例:Azure Service Bus、Kafka、RabbitMQ などのキューに新しいメッセージが追加されたときに実行されるジョブ。 ワークフローまたはパイプラインのキューに新しいジョブが格納されたときに実行されるセルフホステッド GitHub Actions ランナー または Azure DevOps エージェント。 Manual 手動ジョブでの実行をします。 例:一度限りの実行。 Schedule スケジュールされたジョブを実行します。cron 式で定義します。 例:  */5 * * * *  : 5分ごとの実行。 propertiesのtemplate以下は基本的にContainer Apps 同様です。 注意: replicaTimeout までに処理が終わらなければ、タイムアウトしてレプリカは停止します。 param location string = resourceGroup().location param containerAppEnvironmentName string param containerAppJobsSetting object param appInsightsConnectionString string param tags object = {} resource containerAppEnvironment 'Microsoft.App/managedEnvironments@2025-01-01' existing = { name: containerAppEnvironmentName } // Container Apps Jobs の作成 //https://learn.microsoft.com/en-us/azure/templates/microsoft.app/2025-01-01/jobs?pivots=deployment-language-bicep resource containerAppJobs 'Microsoft.App/jobs@2025-01-01' = { name: containerAppJobsSetting.name location: location tags: tags properties: { environmentId: containerAppEnvironment.id configuration: { triggerType: containerAppJobsSetting.triggerType replicaRetryLimit: containerAppJobsSetting.replicaRetryLimit replicaTimeout: containerAppJobsSetting.replicaTimeout eventTriggerConfig: containerAppJobsSetting.triggerType == 'Event' ? { parallelism: containerAppJobsSetting.parallelism replicaCompletionCount: containerAppJobsSetting.replicaCompletionCount scale: containerAppJobsSetting.jobScale == null ? null : { maxExecutions: containerAppJobsSetting.jobScale.maxExecutions minExecutions: containerAppJobsSetting.jobScale.minExecutions pollingInterval: containerAppJobsSetting.jobScale.pollingInterval rules: containerAppJobsSetting.jobScale.jobScaleRule == null ? null : [ { auth: containerAppJobsSetting.jobScale.jobScaleRule == null ? null : [ { secretRef: containerAppJobsSetting.jobScale.jobScaleRule.ScaleRuleAuth.secretRef triggerParameter: containerAppJobsSetting.jobScale.jobScaleRule.ScaleRuleAuth.triggerParameter } ] identity: containerAppJobsSetting.jobScale.jobScaleRule.identity metadata: containerAppJobsSetting.jobScale.jobScaleRule.metadata name: containerAppJobsSetting.jobScale.jobScaleRule.name type: containerAppJobsSetting.jobScale.jobScaleRule.type } ] } } : null manualTriggerConfig: containerAppJobsSetting.triggerType == 'Manual' ? { parallelism: containerAppJobsSetting.parallelism replicaCompletionCount: containerAppJobsSetting.replicaCompletionCount } : null scheduleTriggerConfig: containerAppJobsSetting.triggerType == 'Schedule' ? { cronExpression: containerAppJobsSetting.cronExpression parallelism: containerAppJobsSetting.parallelism replicaCompletionCount: containerAppJobsSetting.replicaCompletionCount } : null } template: { containers: [ { name: containerAppJobsSetting.name image: containerAppJobsSetting.image env: union(containerAppJobsSetting.environmentVariables, [ { name: 'APPLICATIONINSIGHTS_CONNECTION_STRING' value: appInsightsConnectionString } ]) resources: { cpu: json(containerAppJobsSetting.cpu) memory: containerAppJobsSetting.memory } } ] } } } 5. パラメータファイル (main.sbx.parameters.json) メインとなるBicep コードの箇所で説明した Container Apps と Container Apps Jobs のパラメータ:  containerAppsSettings  と  containerAppJobsSettings  のみ以下に記載しています。 Jobs のContainer Image はバッチ処理のサンプルとして 開始時にログ出力して、  SLEEP_SECONDS  (秒)待機、終了するバッチです。 Container Apps の Container Image は  GET /  と  GET /test  へリクエストできるサンプルです。 今回はいずれも公開されたImageですが、プライベートの場合も指定可能です。 "containerAppJobsSettings": { "value": [ { "name": "schedule-container-app-jobs", "image": "ghcr.io/hiroshi-ando-sti/githubpkg-containerregistry/container-app-jobs:main", "replicaTimeout": 4000, "replicaRetryLimit": 2, "parallelism": 1, "replicaCompletionCount": 1, "triggerType": "Schedule", "cronExpression": "0 */10 * * *", "cpu": "0.25", "memory": "0.5Gi", "environmentVariables": [ { "name": "RETENTION_DAYS", "value": "30" }, { "name": "SLEEP_SECONDS", "value": "3800" } ], "jobScale": { "maxExecutions": 1, "minExecutions": 1, "pollingInterval": "PT5M", "maxExecutionTime": "PT30M", "jobScaleRule": null } } ] }, "containerAppsSettings": { "value": [ { "name": "webapi", "image": "ghcr.io/hiroshi-ando-sti/githubpkg-containerregistry/webapi:main", "ingress": { "external": true, "targetPort": 8000, "transport": "auto", "allowInsecure": false }, "secretVariables": [ { "name": "app-private-key", "value": "app-private-key-value" } ], "cpu": "0.25", "memory": "0.5Gi", "environmentVariables": [ { "name": "BATCH_SIZE", "value": "100" }, { "name": "LOG_LEVEL", "value": "INFO" }, { "name": "SLEEP_SECONDS", "value": "3800" } ], "scale": { "cooldownPeriod": 65, "pollingInterval": 30, "minReplicas": 0, "maxReplicas": 2, "rules": [ { "name": "http-rule", "http":{ "metadata": { "concurrentRequests": "2" } } } ] } } ] } デプロイ手順 1. リソースグループの作成 az group create --name rg-container-apps --location "Japan East" 2. Bicepコードによるデプロイ cd infra az deployment group create \ --name rg-container-apps-deploy \ --mode Complete \ --resource-group rg-container-apps \ --confirm-with-what-if \ --template-file ./main.bicep \ --parameters ./main.parameters.sbx.json 3. デプロイの確認 デプロイの確認は以下で行います。 # デプロイ状況を確認 az deployment group list --resource-group rg-container-apps --output table # Container Appsの状況を確認 az containerapp list --resource-group rg-container-apps --output table # Container Jobsの状況を確認 az containerapp job list --resource-group rg-container-apps --output table Azure Portal 上の Container Apps Enviroment からも Container Apps が作成されていることが確認できます。 まとめ Azure Container Apps と Jobs は、それぞれ異なる用途に最適化されたサーバーレスコンテナサービスです。 1. Azure Container Apps と Jobs の使い分けのポイント Azure Container Apps は 継続的に実行されるWebアプリケーションやAPI向けのサービス で、Azure Container Apps Jobs は バッチ処理や定期的なタスク向けのサービス です。 2. Container Apps と Jobs 実装時の考慮点 Container Apps と Jobs を実装する際は、以下の点を考慮することが重要です: リソース配分 :CPU とメモリの適切な設定 スケーリング戦略 :トラフィックパターンに応じたスケールルールの設定 セキュリティ :シークレット管理 監視とログ :Log analytics, Application Insights による可観測性の確保 コスト最適化 :最小レプリカ数の調整とスケールイン時間の設定 トリガー :適切なトリガーの選択 Bicep による Infrastructure as Code と組み合わせて、スケーラブルで保守性の高いコンテナベースのアプリケーション基盤を構築してはいかがでしょうか。 参考URL Azure Container Apps の概要 https://learn.microsoft.com/ja-jp/azure/container-apps/overview Azure Container Apps のコンテナー https://learn.microsoft.com/ja-jp/azure/container-apps/containers Azure Container Apps のジョブ https://learn.microsoft.com/ja-jp/azure/container-apps/jobs?tabs=azure-cli   ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post [Azure][Bicep]Azure Container Apps と Jobs の特徴とBicepによる構築 first appeared on SIOS Tech. Lab .
PSSLの佐々木です。 Dockerを使っていると、同じアプリケーションでも様々なイメージタグが用意されています。 node:18 、 node:18-slim 、 node:18-alpine など、どれを選べばいいのか迷ってしまうことはありませんか? 例えばPythonのDocker Imageはversion 3.13.5系だけでもこんなにあります。とほほ。。 この記事では、各Dockerイメージの種類とその特徴、そして「どんな時に使うべきか」を明確にします 主要なDockerイメージの種類 1. 標準イメージ(Latest/バージョン指定) 例: node:18 、 python:3.11 、 nginx:latest 特徴: 最も完全なパッケージセット 開発に必要なツールやライブラリが豊富 サイズは大きめ(通常数百MB〜1GB以上) Debianベースが多い こんな時に選ぶべき: 開発環境やテスト環境 パッケージの互換性問題を避けたい 初心者や学習目的 プロトタイピング段階 2. Slimイメージ 例: node:18-slim 、 python:3.11-slim 特徴: 不要なパッケージを削除した軽量版 削除される例: curl 、 wget 、 git 、 vim 、 nano 、 man ページ、ドキュメント類 残されるもの:パッケージマネージャー( apt )、基本的なシステムライブラリ 標準イメージの約50-70%のサイズ 基本的な開発ツールは含まれている Debianベースだが最小限のパッケージ こんな時に選ぶべき: 本番環境でサイズを抑えたい 標準イメージでは大きすぎるが、Alpineは不安 CI/CDでのビルド時間を短縮したい セキュリティ面でパッケージ数を減らしたい 3. Alpineイメージ 例: node:18-alpine 、 python:3.11-alpine 、 nginx:alpine 特徴: Alpine Linuxベース(軽量ディストリビューション) 極めて小さなサイズ(通常10-100MB) muslcライブラリを使用(glibcではない) パッケージマネージャーは apk 最小限のパッケージ構成 基本的なシェル( ash )のみ bash 、 curl 、 git 、 vim などは含まれない セキュリティを重視した最小構成 こんな時に選ぶべき: 最小限のサイズが必要 マイクロサービス環境 コンテナの起動速度を重視 リソース使用量を最小限に抑えたい 注意点: ネイティブ拡張でコンパイルエラーが発生する可能性 一部のパッケージが利用できない場合がある デバッグツールが限定的 4. Scratchイメージ 例: FROM scratch 特徴: 完全に空のイメージ 極小サイズ(数KB〜数MB) シェルやパッケージマネージャーなし 静的バイナリのみ実行可能 こんな時に選ぶべき: Go言語の静的バイナリ セキュリティを最優先 極限までサイズを削減したい 攻撃面を最小限に抑えたい 実際のタグ選択:Python 3.13.5を例に 実際にDockerイメージを選ぶ際、以下のような大量のタグが存在します: 3.13.5-bookworm, 3.13-bookworm, 3-bookworm, bookworm 3.13.5-slim-bookworm, 3.13-slim-bookworm, 3-slim-bookworm, slim-bookworm, 3.13.5-slim, 3.13-slim, 3-slim, slim 3.13.5-alpine3.22, 3.13-alpine3.22, 3-alpine3.22, alpine3.22, 3.13.5-alpine, 3.13-alpine, 3-alpine, alpine 3.13.5-windowsservercore-ltsc2025, 3.13-windowsservercore-ltsc2025, 3-windowsservercore-ltsc2025, windowsservercore-ltsc2025 推奨される選択方法 1. バージョン指定の基本ルール python:latest や python:3 – 予期しないバージョンアップで動作不良の可能性 python:3.13.5 – 具体的なバージョンを指定 python:3.13 – マイナーバージョンを固定(パッチ更新は自動) 2. 実際の選択例 # 開発環境:最新の安定版を使用 FROM python:3.13.5-bookworm # または単純に FROM python:3.13.5 # 本番環境:Slimで軽量化 FROM python:3.13.5-slim # マイクロサービス:Alpineで最小化 FROM python:3.13.5-alpine 3. Debianコードネーム(bookworm等)は通常省略可能 python:3.13.5-bookworm と python:3.13.5 は同じ 特定のDebianバージョンに依存する場合のみ明示的に指定 4. Alpineのバージョン指定 python:3.13.5-alpine で十分(最新のAlpineを使用) python:3.13.5-alpine3.22 は特定のAlpineバージョンが必要な場合のみ 実用的な選択フローチャート ステップ1: 用途を確認 開発・テスト環境 → 標準イメージ 本番環境 → Slim または Alpine 極限の軽量化が必要 → Scratch ステップ2: 互換性を確認 ネイティブ拡張を使用 → 標準 または Slim Pure JavaScript/Python → Alpine も選択可能 静的バイナリ → Scratch ステップ3: リソース制約を確認 ストレージ容量に制限 → Alpine または Scratch ネットワーク帯域に制限 → Alpine または Scratch 制約なし → 標準 または Slim まとめ 初心者の方は標準イメージから始めて、慣れてきたらSlimやAlpineに移行する のが現実的なアプローチです。 各イメージの選択基準: 標準イメージ : 確実に動かしたい時 Slimイメージ : バランスを重視する時 Alpineイメージ : サイズを最優先する時 Scratchイメージ : 静的バイナリで極限の軽量化が必要な時 プロジェクトの要件と制約を考慮して、最適なイメージを選択しましょう。不明な点があれば、まず標準イメージで動作確認してから、段階的に軽量化を進めることをお勧めします。 またこのほかにもいくつかイメージが存在していますが、基本この4つのどれかを採用しておけばよいと思います ではまた   ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Dockerイメージの選び方ガイド first appeared on SIOS Tech. Lab .
1. ブログ公開前の「これで大丈夫?」を解決したい ども!最近はAIを活用したブログ改善とかいろいろ取り組んでいる龍ちゃんです。 皆さんもブログを書いた後、こんな不安を感じることありませんか? 「この技術情報、本当に正しいかな?」 「データが古くないかな?」 「読者にとって価値があるかな?」 やっぱ不安ですよね。ブログ書いてもこれが本当に正しいのかとか、情報が古くないかなぁとか。検証はもちろんするんですけど、それが正しいのかどうかという不安はなかなか消えてくれません。 従来の解決方法には人的リソースの観点から多くの問題があります。 手動でのファクトチェック → 時間がかかりすぎる 専門家に相談 → コストと時間の問題 自分で調べ直す → 見落としのリスクが高い 簡単なのは自分で調べてポチポチ見ながら「大丈夫かなぁ」と調べ直すことです。ですが、限界あるじゃないですか。 そこで今回提案したいのが、 AIを「第一読者」にする という発想です。他のエンジニアとかに見てもらうのはやったほうがいいんですけど、なかなかできないじゃないですよね~。みんな忙しいですしww そういうところってAIにやってもらえたらいいなぁって思って、ぶち込んでみたらいい感じにできたって流れです。 2. なぜGemini Deep Researchが第一読者に最適なのか 1番の理由は弊社でGoogle Workspaceの契約にGeminiがついてきたので、会社のドメインであれば有料プランを使い倒せるという点です。 実際、Web Searchがついているサービスはほかにもあります。Claude Proと比較したのですが5000KBのPDFを入力として与えたところ、Claudeではスレッドのトークン上限に引っかかりプロンプトを実行することができませんでした。GeminiのDeep Researchでは、動作し15分前後で200件弱のサイトを検索してレポートを作成してくれました。 Deep Researchの特徴 Geminiのディープリサーチは、従来の検索とは全く異なる「エージェント型AI」の仕組みで動作します。 3つのステップで自動調査 プラン作成 :複雑な質問を小さなタスクに分解 情報収集 :数百のウェブサイトを並行して調査 分析・統合 :収集した情報を批判的に評価してレポート作成 技術的な特徴 100万トークンのコンテキスト管理 反復的な検索プロセス 完全非同期処理(PCを閉じても継続) 詳細な技術仕様については、 Google公式のDeep Research解説 をご覧ください。 他社AIツールとの比較 項目 Gemini Deep Research ChatGPT Deep Research Claude Research 料金 Google AI Pro: $20/月・Google AI Ultra: $250/月・無料版: あり ChatGPT Plus: $20/月・ChatGPT Pro: $200/月・無料版: あり Max Plan: $100-400/月・Pro: $20/月(近日対応予定)・無料版: なし 無料版の機能 月数回の利用、Gemini 2.5 Flash使用・スプレッドシート・コードファイル不可 月5回のタスク・軽量版のみ・短めの回答 基本的なチャット機能のみ・研究機能利用不可 処理時間 標準: 5-10分、複雑なクエリ: より長時間 標準: 5-30分・平均: 15-20分 標準: 5-15分・複雑: 最大45分 検索規模 数百のウェブサイトを検索 数百のソース・(例:52ソース) 数百のソース・+ 内部統合 レポート品質 6-12ページ、引用付き、Googleドキュメント出力対応 2,000-5,000語・構造化された形式 包括的レポート・複数引用スタイル対応 ファイルサイズ制限 100MB (標準)、クラウドストレージ: 2GB 512MB (全フォーマット)・画像: 20MB 30MB (全フォーマット)・画像: 30MB 同時アップロード 10ファイル 有料: 80ファイル /3時間・無料: 3ファイル /日 20ファイル /会話 無料版ファイル機能 PDF、DOC、画像のみ、スプレッドシート不可 同じファイルサイズ制限・3ファイル/日 10MBまで・基本分析のみ 対応ファイル形式 PDF、DOC、DOCX、TXT、画像(Pro: CSV、コードファイル追加) TXT、PDF、画像、CSV、XLSX、DOCX PDF、DOCX、CSV、TXT、画像、OCR機能あり 強み Google統合、非同期処理・音声概要生成 高速処理、包括的分析・マルチフォーマット対応 マルチエージェント並列処理・Googleワークスペース統合 弱み ファイル形式制限・処理時間長い 事実の幻覚リスク・計算リソース集約的 地理的制限あり・高コスト 適用場面 ビジネス分析、市場調査・学術研究、コンテンツ作成 学術研究、投資分析・政策研究、競合分析 学術研究、市場分析・法的調査、科学研究 日本語対応 完全対応 :日本で利用可能・音声機能も日本語対応 利用可能 :但し英語版より品質劣る・文化的文脈の制限あり 完全対応 ・日本で利用可能・機能制限なし (7/5時点での最新情報のつもりです…) 参考リンク:公式サイト Google/Gemini : Gemini Deep Research公式 、 Google AI Blog OpenAI : Deep Research FAQ 、 公式発表 Anthropic : Claude Research発表 、 プランページ 第一読者にAIサービスが適している理由 時間効率 :100~200件のサイトを相互参照して確認するのは人間がやると大規模な作業になります。Research系の機能を使うとプロンプトを投げて終了を待つだけ 情報源の明示 :情報元を提示してくれるので、深く知りたい場合は参照元に当たることができる 多角的な検証 :複数の視点から情報を検証 最新情報へのアクセス :リアルタイムの情報、ウェブサイトをリサーチしてあらゆるサイトから情報を参照 3. 実際にやってみよう:PDFから記事の品質をチェックする3ステップ ここからは実際にPDFファイルを使った品質チェックの手順について書いていきます。全体の流れをフロー図で作ってみました。 ステップ1:PDFファイルの準備とアップロード まずですね、ブログを執筆します。ここ大事ですよね。ブログを書くんですよ。 僕はNotion使ってブログを書いてるんですけど、NotionでPDFでエクスポートします。PDFファイルを添付してDeep Researchに投げます。 対象としたファイルとしては1000〜5000KBまでのPDFであれば問題なく動作しました。Google Workspaceと連携したGeminiであればトグルボタンひとつでGoogle Driveとシームレスな連携をすることができます。Goolge Docsなどのドキュメントであればブラウザ内で完結させることができます。 ステップ2:プロンプトの作成 PDFファイルをアップロードしたら、こんな感じでプロンプトを送ります: 添付した資料の内容をもとにファクトチェックして、技術的ブログとして多角的な評価から判断して このプロンプトは簡素なプロンプトにしています。「ファクトチェック」は外部的リソースとの比較をさせる意図があります。「多角的評価」という言葉でGemini側に評価基準を委譲しています。 目的別プロンプト例 : 技術記事の場合 :「技術仕様、API情報、コード例の正確性を重点的に」 差別化チェック :「この他社の記事との差別化ってどういうところにあるんですか?」「この記事の強みってなんですか?」 公式リファレンス準拠 :「公式リファレンスのみを参照してベストプラクティスかどうか調査して」 ステップ3:結果の解釈と活用 5〜30分経過後、詳細なレポート出てきます。詳細なレポートには、引用元が明記された詳細な調査レポートが出力されます。このレポートには、発見された問題点や改善提案が含まれて出力されることもあります。(もちろん全肯定の時もあるよ!) 一度リクエストすれば完了時に通知が飛んでくるので別作業をすることも大きな利点ですね。 その出てきたレポートをもとに「簡潔な結論出して」って言うと、改善すべき点があるかって聞くと、レポートから更に要約してくれます。レポートをもとにPDCAサイクルを回せます。 4. 【実体験】よくある判定ミスと対処法 ここでは実際に会った出力ミスについて紹介していきます。もちろんAIも間違うこともあります。 レポートに対して納得がいかない ハルシネーションってあるじゃないですか。簡単にいうとAIが嘘を吐くってやつなんですが、これはレポートと主張が噛み合わないという結果として現れます。 そういう時はブログに書かれている内容が不足しており、主張がAIにも伝わっていないか考慮不足があると解釈しています。 そういう時はブログ側を加筆することで対応しています。加筆時は再度調べ直して、情報を保管することでAIを納得させることができます。 「リンクがない」と言われた時の対処法 実際、PDF内にリンクを挟んでいたのですが、Geminiではリンクが挿入されていないという判定で公式リファレンスの挿入が推奨されました。 そういう時は1回聞き直すんですよね。「リンクは含まれていると思います。再度確認をしてください」って聞いたら再度PDFファイルを読み込み確認を行なってくれます。 PDFが参照できませんでした これは100%プロンプトが悪いですね。何をして欲しいのかが伝わっていない可能性があります。 こういう時はプロンプトを改修しましょう。またリサーチの計画を確認して意図が伝わっているか確認するのも一つの手になります。 「情報が古い」と指摘された時の確認方法 「情報が古いですね」って言われて、僕の情報が新しくて、Geminiが参照してた情報が古いってこともあったんで、そういう時は自分が参照した情報っていうのを送って「これが最新の情報じゃないですか」っていうの聞くと修正して謝ってくれます。 対処に重要な観点 こちらはAIに追加で知識を与える際に重要な観点になります。 再確認の依頼:「もう一度詳しく確認してください」 情報源の明示:重要なURLやリンクを直接共有 複数回のチェック:一度に全てを判定せず部分的に検証 5. 応用テクニック:さらに効果を高める使い方 プロンプトで効果を高める方法はたくさんあります。ですが、最終的に人間の目を入れることは絶対に忘れないでください。 複数の視点からの品質チェック 今回はファクトチェックと多角的視点という比較的曖昧な表現で調査を依頼しました。観点を限定して依頼することも可能です。 SEO観点から トレンド観点から 公式リファレンスのみを参照して 重要としている観点をに重みを置いて調査することが可能です。観点の追加をすることで、自分が意図した方向性での調査を要求することができます。 競合記事との差別化チェック 技術ブログは差別化が大事だったりします。個人的には勉強したことは全て技術ブログとして出していいじゃないと思っています。 ブログのアウトラインレベルで記事を共有し、すでにネット上に同様の主張をしているドキュメントがないかの調査などに活用をすることができます。 継続的な品質管理 何度もレビューを依頼しても、嫌な顔をしないというのがAIサービスの良い点ですね。Deep Researchで検証をした内容から変更を加えて再度実行することで質が向上していきます。 最大で3度レビューを通せば、人間が見ても投稿としては問題ないレベルに達すると思います。 6. 知っておくべき注意点:Deep Researchの限界と対策 やっぱ注意点はもちろんあるんですよね。 AIによる判断の限界を理解する やはり大事なのはAIも人間もミスをするし嘘を吐くという点ですね。 間違うことがもちろんあるので、最終的にAIが出してきたレポートって人間が見る必要があります。 重要なポイント : ハルシネーションリスク:AIの言ってることを全て100%信じるわけにはいかない 個人ブログも公式リファレンスも同等のソースとして扱う可能性がある 最終確認は人間が行う必要がある 記事は自分で書いておかないといけない やっぱり自分で書いたブログと公式リファレンスを一個ずつ確認するのは途方もない作業です。ですがDeep Researchが出してきたレポートと自分のブログとの比較であれば、手間自体はグッと下がります。 レポートを聞いてレビューを受けたという気持ちで修正をしましょう。そして自分があってる確証があるなら聞きましょう。判断がつかなくても遠慮なくAIに聞くのも一つに手だと思います。 コスト面の考慮 弊社はGoogleのワークスペースで契約してるので使いたい放題できるって言うところがあります。 個人利用の場合は、自分が契約しているAIサービスがSearch系のツールを提供していれば同等の検証を行うことは可能です。ですが、大きな規模のファイルであれば制限がかかる可能性はあります。そこは契約しているツールに合わせて、ファイルサイズを小さくするなどのカスタマイズが必要になります。 情報の鮮度について AIはリアルタイムでウェブ情報を参照しますが、特定の分野(例:最新のバグ情報、直近で非推奨となったAPI、まだ公式ドキュメントに反映されていない極めて新しい技術動向など)では、情報の鮮度に限界がある場合があります。特に重要な情報については、必ず人間が最新の公式情報を確認し、情報の取捨選択を行う必要があります。 一番新鮮な情報はGitHub Issueだと思っています。そこ専用の検索エージェントとか出たら絶対使いますね。Issueでは暫定的な対応法なども取り扱っているので、GitHub Issueに関しては人間の調査が必要ですね。 7. これからの時代のブログ品質管理 従来の品質管理との違い 時間効率 :数時間→5-30分程度での初期レビュー 網羅性 :人間の見落とし→AIの包括的チェック 継続性 :一回限り→定期的な自動チェック 読者にとっての価値 不安なんだったら評価してもらう。AIが改善案を人間が検証・判断のサイクルを回すことで技術ブログの質は上がります。この点から、第一読者にAIを据えることは読者にとっても価値があります。 レビューを通して、不足点を補うことでコンテンツ内に含まれる情報量が増えて、読者が受け取ることができる情報量は確実に増大します。 今後の展望 人間が自分の手元の環境で検証したって事はそれだけで価値があると思います。1番は自分のところで試す、自分の環境で試して動いたものを発表することです。その次に大事なのはその情報どこ見て作ったかなんですよね。 そういった正確性をAIが担保してくれるようになるといいなと思っています。 現状は1読者の意見だと軽い感じで受け取りましょう。 バグやハック的な内容であれば公式リファレンスに載ってないこともあります。そういう部分はダイレクトに誰かの助けになる情報なので自信を持って出しましょう。 8. 今すぐ始められる:最初の一歩 今回のお話で大事なこと AIを第一読者として活用する :人間の専門家に頼む前に、まずAIでファクトチェック 最終確認は必ず人間が行う :AIの判断を100%信じず、自分の目で情報源を確認 自分で記事を書いてからチェック :内容を把握している状態でファクトチェックするのが効果的 情報源を必ず確認する :AIが提示したリンク先を実際に見に行く習慣をつける 今まで4年で150本ほどブログを書いていますが、直近の10本ほどはこの手法を使っています。体感でブログの質が上がっています。ブログの長さも長くなっています。 ぜひ読んでもらえると嬉しいです。 2025-06-18 Azure Static Web Apps: x-ms-client-principalで安全なロールベース制御 2025-06-09 Notebook LMへのデータ収集をSlack Botで効率化する開発 with Google Docs 技術ブログ以外への適用可能性 実は僕自身も、技術ブログ以外でもDeep Researchを活用してます。 デザインガイドライン調査からプロンプト化まで デザインのガイドライン調査をしてそれをプロンプト化するみたいな使い方ですね。例えば「Material Design 3の最新ガイドラインを調査して、デザインレビュー用のチェックリストを作成して」みたいな感じで投げると、包括的な情報を集めてくれて、それをそのままプロンプトを作成する際の参考情報にしています。 SNS戦略の最適化 Xのガイドラインなど投稿の優位性とかも調査してます。「X(Twitter)でエンゲージメント率を上げるための最新の投稿戦略を調査して」って投げると、アルゴリズムの変更点から効果的な投稿時間、ハッシュタグ戦略まで調べてくれるんで、それをベースにプロンプトに組み込むための調査としてGemini Deep Researchを使ってる感じです。 他分野での応用例 この使い方って、いろんな分野に応用できるんですよね: マーケティング分野 : 各プラットフォームのガイドライン調査、競合他社のSNS戦略分析 コンテンツ制作 : デザイントレンドの調査、UI/UXのベストプラクティス調査 ビジネス戦略 : 業界動向の調査、新規事業のマーケットリサーチ 学術・研究 : 論文の初期調査、最新研究動向の調査 要は「初期調査→プロンプト化→業務効率化」っていう流れで、どんな分野でも使えるってことですね。技術者じゃなくても、マーケターでもデザイナーでも、研究者でも、みんなが使えるツールだと思います。 ただし!調査結果をそのまま鵜呑みにするのは危険ですよ。僕も必ず自分の目で確認してから使うようにしてます。AIが間違うこともあるし、情報が古い場合もあるので、最終的には人間がチェックするっていうのが大事ですね。効率化はしつつも、責任は人間が持つっていうスタンスが重要だと思います。 この活用ってちょっと前から話題になってるところなので、ぜひぜひその技術ブログって言う観点でも活用の道を模索していければいいなと個人的にめちゃくちゃ思ってます。体感乗り遅れてますけどね~~ 早くファクトチェック完璧にできるようになんねえかなぁ~ 龍ちゃん いろんなブログ書いてるので、ぜひ僕の個人ページでも見てやってください ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 【実践解説】技術ブログ品質チェック術|Gemini Deep Researchで5分検証 first appeared on SIOS Tech. Lab .
挨拶 ども!6月は結構激しめにブログを投稿していたんですが、7月もがっつり忙しくなりそうで、ウハウハの龍ちゃんです。ブログサムネイル評価用プロンプトを結構な時間をかけて作りました。今までのように人にSlack投げて確認ではなく、生成AIにチャットを投げるだけで確認出来ており、サムネイルにも力が入っています。 さて!今回は「Azure Static Web Apps+Bicepの発展編:Azure Static Web Apps+Managed Functions+GitHub(カスタム認証)の環境をGitHub Actions after Bicepでデプロイ」というてんこ盛りの内容になっています。こちらを理解できれば、Azure Static Web Appsの基礎は出来上がったように感じます。 なかなかの長編になったので、初めての方は5日ほどかけてじっくりと、中級者以上の方はミスを指摘してやるってテンション感でご覧ください。 はじめに・概要 この記事で学べること 今回は、環境構築からデプロイまで横断的に解説していきます。以下にまとめます。 環境構築:DevContainer ローカル環境でAzure Static Web AppsとAzure Functionsを開発するために必要な環境 ローカル環境からBicepでAzureリソースを管理するのに必要な環境 Azure Functions開発(Managed Functions Node with TypeScript) APIの簡易的な仕様書から、TypeScriptによる実際のコーディング Azure Static Web Apps開発(Next.js) GitHub OAuthを活用した認証基盤 Managed Functionsと連携して認証状態を管理する方法 Bicep:ローカル環境からAzureリソースのコマンドデプロイ デプロイ環境をコード一発でデプロイ・クリーンナップできるようなbashファイル管理 GitHub Actions Azure Static Web AppsとAzure Functions(Managed Functions)をGitHub Actionsからデプロイする 設計から、Azure Static Web AppsとAPI統合、認証機能の実装方法。DevContainerからBicep、GitHub Actionsまでの一連の開発フローとして学習することができます。 以降はAzure Static Web AppsはSWAと省略した形で解説していきます。 技術スタック 使用している技術などは以下にまとめました。 カテゴリ 技術・ツール バージョン 備考 デプロイ環境 Azure Static Web Apps – Standardプラン:カスタム認証(GitHub) Azure Functions – Managed Functions 開発環境 DevContainer – – Azure CLI 2.74.0 – SWA CLI 2.0.6 – GitHub CLI 2.74.2 – Function Core Tools 4.0.7317 – フロントエンド Next.js 15.3.4 静的エクスポート React 19 – バックエンド Node.js + TypeScript – Azure Functions v4 Programming Model 今回は、データベースなどは抜きにしてSWA内ですべてが完結しているように構成しています。1点注意点としては、カスタム認証を使用する関係上、SWAのプラン設定をStandardにする必要があります。無料枠でも十分強力ですが、認証プロバイダーの追加設定などはStandardプランからでないと使用することができません。認証機能が必要ない方は、Maneged Functionsも無料枠があるのでBicepテンプレートをカスタマイズしてご利用ください。 コラム:Azure SWAのStandardとFreeプランの比較 Azure Static Web Appsには、個人のプロジェクトや小規模なウェブサイトに適した Freeプラン と、運用環境のアプリケーションや大規模なプロジェクトに適した Standardプラン があります。それぞれのプランで利用できる機能の範囲が異なり、特にManaged Functionsの利用方法に影響があります。 プラン別機能比較 機能 Freeプラン<br/>(個人/小規模プロジェクト向け) Standardプラン<br/>(運用/大規模プロジェクト向け) ウェブホスティング GitHub/Azure DevOps統合 グローバル分散 (静的コンテンツ) (静的コンテンツ) SSL証明書 (無料、自動更新) (無料、自動更新) ステージング環境 アプリあたり3つまで アプリあたり10個まで アプリの最大サイズ アプリあたり250MB アプリあたり500MB カスタムドメイン アプリあたり2つまで アプリあたり5つまで API (Azure Functions) マネージド (SWAに統合) マネージド または 独自のFunctionsアプリ利用 認証プロバイダー 事前構成済み (サービス定義済み) カスタム登録も可能 関数によるカスタムロール プライベートエンドポイント SLA (サービスレベル契約) なし 99.95% 帯域幅 サブスクリプションあたり月100GBまで (無料) サブスクリプションあたり月100GBまで (超過分課金) カスタマーサポート なし Managed Functionsが受ける影響 Azure Static Web AppsにおけるAPIバックエンドとしてのManaged Functionsは、選択するプランによって利用方法が大きく異なります。 Freeプランの場合 Managed Functionsのみ が利用可能です。これはAzure Static Web Appsに組み込まれたFunctionsアプリであり、ユーザーが個別にAzure Functionsリソースを作成・管理する必要がありません APIの実行回数には、 月間100万回の無料実行 が含まれます コールドスタートが発生しやすく、API呼び出しの最初の応答に時間がかかる場合があります Standardプランの場合 Managed Functions の利用に加えて、 既存の独自のAzure FunctionsアプリをStatic Web AppsのAPIバックエンドとして持ち込むことが可能 です 独自のFunctionsアプリを利用することで、HTTP以外のトリガー(例: タイマートリガー、キュートリガー)や、より複雑なバインディングを利用できるようになります 既存のFunctionsアプリをStatic Web Appsに統合したい場合に特に便利です どちらのプランを選ぶべきか? 以下の3つの観点から、Standardプランを検討してください: 1. アーキテクチャ戦略(マイクロサービス化) フロントエンドとバックエンドを分離 したマイクロサービス構成にしたい場合 既存のAzure Functionsアプリを再利用 したい、または独立して管理したい場合 HTTPエンドポイント以外のトリガー(タイマー、キュー等)が必要な場合 2. プロジェクトの成長・運用要件 予想されるトラフィック量が多い 、または帯域幅の無料枠を超える可能性がある場合 ミッションクリティカルなアプリケーション で99.95%のSLAが必要な場合 3つ以上のステージング環境 (開発、検証、本番等)が必要な場合 3. 認証・セキュリティ要件 カスタム認証プロバイダーの登録 が必要な場合(GitHub、Google以外の独自認証等) 関数によるカスタムロール 制御が必要な場合 プライベートエンドポイント でのセキュアな通信が必要な場合 まとめ 基本的には、個人的なプロジェクトやシンプルなウェブサイトであればFreeプランで十分ですが、ビジネス用途や大規模なアプリケーションにはStandardプランの検討をお勧めします。 今回の記事では、カスタム認証機能を使用するためStandardプランを選択していますが、認証が不要な場合はFreeプランでも同様の構成を構築できます!というか、課金が毎秒走っていると考えると怖いので、極力Freeプランを使うようにしています。 最終的な成果物 今回解説するアプリの動画です。ページとAPIをそれぞれ2つずつ作成し、認証あり・なしのパターンを網羅できるよう設計しています。 前提条件・準備 前提条件は以下の通りです。 Azureで有効なサブスクリプションを持っている GitHub.comに登録済み Dockerを使用できる環境 今回は、Azure Static Web AppsのStandardプランを使用するため、有効なサブスクリプションが必要です。また、GitHub Actionsを使用してデプロイを行い、認証プロバイダーにGitHub OAuthを使用するため、GitHub.comのアカウントは必須となります。 Dockerに関しては、今回の技術スタックはNode.jsに統一しているため、ローカル環境に有効なNode.js環境があれば開発を進めることができます。ただし、Dockerが入っていればコピー&ペーストで環境が立ち上がるため、こちらを推奨します。 プロジェクト設計・アーキテクチャ システム構成図 システム構成に関しては、上記のようになっています。作成するAzureリソースとしては、SWAのみとなっており、カスタム認証プロバイダーとしてGitHub OAuthを使用します。デプロイに関しては、Azureリソースに関してはBicep、アプリケーションのデプロイに関してはGitHub Actionsでそれぞれ管理します。 フロントエンドに関しては、Next.jsの静的エクスポートを使用してデプロイを行います。 ディレクトリ構造 . ├── .github # GitHub Actions用 ├── api # API用ディレクトリ ├── frontend # フロント用ディレクトリ Next.js静的エクスポート ├── infra # Infrastructure as Code用ディレクトリ └── swa-cli.config.json # SWA CLIのローカル起動用設定ファイル 全体のディレクトリ構成としては上記のようになります。それぞれ、他のリソースを参照せずに個別のディレクトリでそれぞれ開発をすることができます。 フロントエンド設計 ページ構成と各ページの目的 それでは、今回のアプリケーションのページ構成について見ていきましょう。まず、作成するルート一覧を明示しておきますね。 作成ルート一覧 / – トップページ(パブリック) /protected – 保護ページ(認証必須) シンプルな構成ですが、それぞれに明確な役割があります。 トップページ( / ) トップページは、いわば「認証の玄関口」として設計しています。ここでの主な目的は以下の通りです: 認証状態の可視化 : ユーザーが現在ログインしているかどうかを一目で分かるようにする 認証フローの起点 : 未認証ユーザーにとってのログイン入口 ユーザー情報の表示 : 認証済みユーザーには基本的なプロフィール情報を提示 ナビゲーションハブ : /protected へのリンク 実装としては、認証状態に応じて動的にUIを切り替える設計になっています。未認証時は「GitHubでログイン」ボタンを表示し、認証済み時はユーザー名やアバター、そして保護ページへのリンクを表示します。これにより、ユーザーは自分の状態を即座に把握できるというわけですね。 保護ページ( /protected ) 保護ページは、Azure SWAの認証機能の真価を発揮する場所です。ここでの設計ポイントは: Azure SWAによる保護 : フロントエンドでの認証チェックに依存しない 認証済みユーザー専用コンテンツ : より詳細なユーザー情報や機能の提供 セキュリティ情報の表示 : 認証システムの仕組みを理解してもらう この設計の面白いところは、ソースコード内で「認証チェック」を行わないことです。Azure SWAを使う場合は、サーバーレベルで保護されているため、そもそも未認証ユーザーはこのページにたどり着けません。 各ページのデザインは今回主題ではないので、生成AIを活用してデザインを生成してもらいます。 ルート保護設計とアクセスフロー Azure SWAの最大の魅力の一つが、宣言的なルート保護による強力なセキュリティ機能です。設定一つで堅牢な認証システムを実現できるというのは、本当に画期的ですよね。 アクセスフロー全体像 今回の設計で重要となる点としては、401エラー(未認証)が発生した瞬間に、ユーザーを /.auth/login/github に自動リダイレクトする仕組みです。これにより、ユーザーは「アクセス拒否」のような不親切な画面を見ることなく、スムーズに認証フローに誘導されます。 認証完了後は、元々アクセスしようとしていたページに戻ってくるのも、Azure SWAが自動的に処理してくれます。フロントエンド側で複雑なリダイレクト制御を書く必要がないというのは、大きなメリットです。 API設計 エンドポイント仕様の全体像 今回のシステムでは、以下の2つの主要エンドポイントを設計しました。 /api/user-info : ユーザー情報取得(認証状態問わず) /api/protected-data : 保護されたデータ取得(認証必須) /api/user-info では、フロントエンドから認証状態と未認証状態でのアクセスを確認するために、認証状態を問わずアクセスできるエンドポイントとして設計しています。 /api/protected-data では、認証されていない場合はエラーを返答し、ダミーデータを返すエンドポイントとして設計しています。 user-infoエンドポイントの設計 user-infoエンドポイントは、現在のユーザーの認証状態と基本情報を返すAPIです。未認証時でもエラーにならずにリクエストが成功するように設計します。 リクエスト仕様 Method: GET Path: /api/user-info Authentication: 不要 Parameters: なし レスポンス型定義 interface UserInfo { userId: string | null; name: string | null; email: string | null; provider: string | null; roles: string[]; isAuthenticated: boolean; } 成功レスポンス例(認証済み) { "userId": "github|12345678", "name": "yamada.taro", "email": "yamada.taro@example.com", "provider": "github", "roles": ["authenticated"], "isAuthenticated": true } 成功レスポンス例(未認証) { "userId": null, "name": null, "email": null, "provider": null, "roles": [], "isAuthenticated": false } ここで重要なのは、認証されていない場合も エラーではなく正常なレスポンス を返すことです。 isAuthenticated: false として状態を明示することで、フロントエンド側で適切な処理分岐ができます。 protected-dataエンドポイントの設計 protected-dataエンドポイントは、認証が必須のAPIです。認証されていないユーザーには401エラーを返し、アクセスを拒否します。 リクエスト仕様 Method: GET Path: /api/protected-data Authentication: 必須 Parameters: なし レスポンス型定義 interface ProtectedData { userId: string; message: string; timestamp: string; userNumber: number; } 成功レスポンス例 { "userId": "github|12345678", "message": "こんにちは、ユーザーgithub|12345678さん!", "timestamp": "2024-12-15T15:45:30.000Z", "userNumber": 456 } エラーレスポンスの統一設計 APIの一貫性を保つため、エラーレスポンス形式を統一しています。すべてのエラーレスポンスは以下の構造に従います。 エラーレスポンス型定義 interface ErrorResponse { error: string; message: string; } 認証エラー(401 Unauthorized) { "error": "Unauthorized", "message": "保護されたデータにアクセスするには認証が必要です" } サーバーエラー(500 Internal Server Error) { "error": "Internal server error", "message": "Application Crash" } この統一により、フロントエンド側でのエラーハンドリングが 大幅に簡素化 されます。どのAPIでも同じ構造でエラー情報を取得できるためです。 認証チェック方法の設計 Azure SWAでは、認証済みユーザーの情報が x-ms-client-principal ヘッダーにBase64エンコードされたJSON形式で渡されます。これは、Azure SWAの大きな特徴の一つですね。 ClientPrincipal型定義 interface ClientPrincipal { identityProvider: string; userId: string; userDetails: string; userRoles: string[]; } 認証チェックの流れは以下のようになります: リクエストヘッダーから x-ms-client-principal を取得 Base64デコードしてJSON解析 成功時は認証済みとして処理、失敗時は未認証として処理 このヘッダーは Azure SWAのリバースプロキシによって自動的に付与 されるため、偽装される心配はありません。 こちらの処理の流れは公式リファレンスにまとまっています。 開発環境構築(DevContainer) この章では、DevContainer環境を構築していきます。作成するディレクトリ構成は以下になります。 . └── .devcontainer └── devcontainer.json DevContainer設定  devcontainer.json 今回の開発に必要なツールを全て含んだDevContainer設定を作成します。Node.js、Azure CLI、Functions Core Tools、SWA CLIを含む開発環境を構築していきましょう。 { "name": "Azure SWA include API & Bicep Development", "image": "mcr.microsoft.com/devcontainers/javascript-node:1-20-bullseye", "features": { "ghcr.io/devcontainers/features/azure-cli:1": {}, "ghcr.io/devcontainers/features/github-cli:1": {} }, // npm@11.4.2に関してはnoticeが出ていたので対応 "postCreateCommand": "npm install -g npm@11.4.2 @azure/static-web-apps-cli azure-functions-core-tools@4", "customizations": { "vscode": { "extensions": [ "ms-azuretools.vscode-bicep", "GitHub.vscode-github-actions", "ms-vscode.azure-account" ] } }, "forwardPorts": [ 3000, 7071, 4280 ], "mounts": [ "source=${localEnv:HOME}/.azure,target=/home/node/.azure,type=bind,consistency=cached" ] } ベースイメージとインストール ベースイメージとしては、 Microsoftが提供しているNode環境 を使用しています。Node.js 20系の安定版が含まれており、今回のプロジェクトに必要な環境が整っています。 features として、Azure CLIとGitHub CLIを導入しています。これにより、Azureリソースの管理とGitHubとの連携が可能になります。 postCreateCommand で、npmパッケージとしてFunctions Core Tools@v4とSWA CLIをグローバルインストールしています。npmのバージョンアップも行っており、これは最新の機能とセキュリティ修正を適用するためです。 VS Code拡張機能 開発効率を上げるため、以下の拡張機能を自動インストールします: Bicep : Azureリソース定義のシンタックスハイライトと補完 GitHub Actions : ワークフロー編集の支援 Azure Account : Azure認証とリソース管理 ポートフォワーディング 開発中に使用する主要ポートを事前に設定しています: 3000 : Next.jsフロントエンド 7071 : Azure Functions API 4280 : SWA CLI統合サーバー マウント設定 "mounts": [ "source=${localEnv:HOME}/.azure,target=/home/node/.azure,type=bind,consistency=cached" ] ホスト側にある .azure ディレクトリをマウントしています。これにより、ホスト側で az login をしていればDevContainer環境内でも認証済みとして扱うことができます。 セキュリティに関する注意 この設定は、Azure認証情報をコンテナ内で共有するため、セキュリティリスクがあります。使用する場合は十分に注意し、本番環境や共有環境では適切な認証方法を検討してください。 開発環境の起動確認 環境を立ち上げた後は、各CLIが正常にインストールされているか確認しましょう。 Azure CLI確認 az --version # azure-cli 2.74.0 # core 2.74.0 # telemetry 1.1.0 # # Dependencies: # msal 1.32.3 # azure-mgmt-resource 23.3.0 # # Python location '/opt/az/bin/python3' # Config directory '/home/node/.azure' # Extensions directory '/home/node/.azure/cliextensions' # # Python (Linux) 3.12.10 (main, May 27 2025, 09:13:17) [GCC 10.2.1 20210110] # # Legal docs and information: aka.ms/AzureCliLegal # # # Your CLI is up-to-date. 併せてマウント設定が正しく動作しているか。認証状態の確認をしておきましょう。 az account show ホスト側で認証済みの場合、アカウント情報が表示されます。認証されていない場合は、以下のコマンドで認証を行ってください。 az login GitHub CLI確認 gh --version # gh version 2.74.2 (2025-06-18) # https://github.com/cli/cli/releases/tag/v2.74.2 SWA CLI確認 swa --version # 2.0.6 Functions Core Tools確認 func --version # 4.0.7317 それぞれのバージョン情報が表示されれば成功です。これで、Azure Static Web Appsの開発に必要な全てのツールが使用可能になりました! これで、DevContainer環境での開発準備が完了しました。次章からは、実際にAzure FunctionsのAPI実装に入っていきます。 Azure Functions API実装 この章では、Azure Functions環境を構築・実装をしていきます。作成するディレクトリ構成は以下になります。ファイルの作成自体は、直接作るのではなくFunctions Core Toolsを介して生成をしていきます。 プロジェクト初期化 . └── api ├── .funcignore ├── .gitignore ├── host.json ├── local.settings.json ├── package.json ├── package-lock.json ├── src │ └── functions │ ├── protected-data.ts │ └── user-info.ts ├── tsconfig.json └── .vscode └── extensions.json まずは、 api ディレクトリを用意してディレクトリに遷移します。中で初期化コマンドを実行すると以下のファイルが自動生成され、 npm install が実行されます。 mkdir api cd api func init --typescript これで初期化は完了です。次は、各エンドポイントの作成を進めていきます。エンドポイントは Functions Core Tools(公式リファレンス) で生成します。コマンドを通して作成することで、基本の関数が準備された状態でスムーズな開発を進めることができます。使用することができるテンプレートは以下のコマンドで取得することができます。 func templates list /api/user-info エンドポイント実装 /api/user-info エンドポイントの作成を進めていきます。 api ディレクトリで以下のコマンドを実行してください。 func new --name user-info --template "HTTP trigger" --authlevel "anonymous" 上記のコマンドで src/functions 内に user-info.ts というファイルが生成されます。認証レベル「匿名」で user-info というファイル名で作成されます。作成されたファイルの中には、 --name で指定した名前のパスに対してHELLO Worldを返す関数が作成されています。 中身を確認して、変更を加えてください。 import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions'; interface ClientPrincipal { identityProvider: string; userId: string; userDetails: string; userRoles: string[]; } interface UserInfo { userId: string; name: string; email: string; provider: string; roles: string[]; isAuthenticated: boolean; } export async function userInfo(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> { context.log('Processing user-info request'); try { // x-ms-client-principalヘッダーの取得 const clientPrincipalHeader = request.headers.get('x-ms-client-principal'); if (!clientPrincipalHeader) { context.log('ヘッダーにx-ms-client-principalが確認することができず、認証状態を確認することができませんでした。'); return { status: 200, jsonBody: { isAuthenticated: false, userId: null, name: null, email: null, provider: null, roles: [] } }; } // Base64デコード const clientPrincipalData = Buffer.from(clientPrincipalHeader, 'base64').toString('utf-8'); const clientPrincipal: ClientPrincipal = JSON.parse(clientPrincipalData); context.log('Client Principal:', clientPrincipal); // ユーザー情報の整形 const userInfo: UserInfo = { userId: clientPrincipal.userId, name: clientPrincipal.userDetails, email: clientPrincipal.userDetails, // GitHubの場合はユーザー名,Googleならメールアドレス provider: clientPrincipal.identityProvider, roles: clientPrincipal.userRoles || [], isAuthenticated: true }; return { status: 200, headers: { 'Content-Type': 'application/json' }, jsonBody: userInfo }; } catch (error) { context.log('Applicatoin Crash', error); return { status: 500, jsonBody: { error: 'Internal server error', message: 'Failed to process user information' } }; } } app.http('user-info', { methods: ['GET'], authLevel: 'anonymous', route: 'user-info', handler: userInfo }); こちらの関数での処理は、現在のユーザーの認証状態と基本情報を返すAPIです。特徴としては、未認証時でもエラーにならずにリクエストが成功します。 コードは大きく3つに分かれています。 ヘッダーから x-ms-client-principal を取得し、ない場合は status:200 (ユーザー情報なし)を返答 ヘッダーからデコードしてユーザー情報を取得し、 status:200 (ユーザー情報あり)を返答 上記の処理で問題が発生した場合は status:500 を返答 デコードの処理は、 公式リファレンスで紹介されている手法 をもとに構築しています。その他のAPIの実装はAPI設計に準拠しています。 /api/protected-data エンドポイント実装 /api/protected-data エンドポイントの作成を進めていきます。 api ディレクトリで以下のコマンドを実行してください。 func new --name protected-data --template "HTTP trigger" --authlevel "anonymous" 上記のコマンドで src/functions 内に protected-data.ts というファイルが生成されます。認証レベル「匿名」で protected-data というファイル名で作成されます。作成されたファイルの中には、 --name で指定した名前のパスに対してHELLO Worldを返す関数が作成されています。 中身を確認して、変更を加えてください。 import { app, HttpRequest, HttpResponseInit, InvocationContext } from '@azure/functions'; interface ClientPrincipal { identityProvider: string; userId: string; userDetails: string; userRoles: string[]; } interface ProtectedData { userId: string; message: string; timestamp: string; userNumber: number; } // 認証チェック用のヘルパー関数 function checkAuthentication(request: HttpRequest, context: InvocationContext): ClientPrincipal | null { const clientPrincipalHeader = request.headers.get('x-ms-client-principal'); if (!clientPrincipalHeader) { context.log('認証ヘッダーが見つかりませんでした'); return null; } try { const clientPrincipalData = Buffer.from(clientPrincipalHeader, 'base64').toString('utf-8'); return JSON.parse(clientPrincipalData); } catch (error) { context.log('Client principalの解析に失敗しました:', error); return null; } } // 軽量なユーザー固有データ生成 function generateUserData(userId: string): ProtectedData { // userIdをベースにした簡単なシード値 const seed = userId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0); return { userId, message: `こんにちは、ユーザー${userId}さん!`, timestamp: new Date().toISOString(), userNumber: seed % 1000 // 0-999の範囲のユーザー番号 }; } export async function protectedData(request: HttpRequest, context: InvocationContext): Promise<HttpResponseInit> { context.log('protected-dataリクエストを処理中'); try { // 認証チェック const clientPrincipal = checkAuthentication(request, context); if (!clientPrincipal) { return { status: 401, headers: { 'Content-Type': 'application/json' }, jsonBody: { error: 'Unauthorized', message: '保護されたデータにアクセスするには認証が必要です' } }; } context.log('認証済みユーザー:', clientPrincipal.userId); // 軽量なダミーデータを生成 const userData = generateUserData(clientPrincipal.userId); return { status: 200, headers: { 'Content-Type': 'application/json' }, jsonBody: userData }; } catch (error) { context.log('protected-dataリクエストの処理中にエラー:', error); return { status: 500, jsonBody: { error: 'Internal server error', message: '保護されたデータの取得に失敗しました' } }; } } app.http('protected-data', { methods: ['GET'], authLevel: 'anonymous', // SWAのManaged Functionsのため route: 'protected-data', handler: protectedData }); こちらの関数の処理は、認証されていないユーザーには401エラーを返し、認証済みの場合はダミーデータを返答します。 処理として、 /api/user-info と大差ありません。先ほどのヘッダーの取得からデコードまでをヘルパー関数として切り出しています。 処理取しては、大きく4つに分類されます。 checkAuthentication :ヘッダーから x-ms-client-principal を取得、デコードした情報を返答する。情報がない場合は、 null を返答 checkAuthentication からの戻り値が null : status:401 を返答 checkAuthentication からの戻り値がユーザー情報: generateUserData を使用してダミー情報を作成して、 status:200 で返答 上記の処理で問題が発生した場合は status:500 を返答 デコードの処理は、 公式リファレンスで紹介されている手法 をもとに構築しています。その他のAPIの実装はAPI設計に準拠しています。 ローカルでのAPI動作確認 それでは、作成したAPIが正しく動作するかローカル環境で確認していきましょう。 ローカル環境の制限事項 ローカル環境では認証ヘッダーが自動付与されないため、テスト範囲は限定的ですが、基本的な動作確認は可能です。 認証ヘッダーの模擬 Azure SWA の認証ヘッダー( x-ms-client-principal )は自動付与されません 認証済み状態のテストはローカルでは困難です 本格的な認証テストは本番環境またはSWA CLI統合環境で行う必要があります Azure Functions API のローカル起動 まず、APIディレクトリで Functions Core Tools を使ってローカルサーバーを起動します。 cd api func start 正常に起動すると、以下のような出力が表示されます: Azure Functions Core Tools Core Tools Version: 4.0.7317 Commit hash: N/A +5ca56d37938824531b691f094d0a77fd6f51af20 (64-bit) Function Runtime Version: 4.1038.300.25164 [2025-06-30T14:09:22.328Z] Worker process started and initialized. Functions: protected-data: [GET] http://localhost:7071/api/protected-data user-info: [GET] http://localhost:7071/api/user-info For detailed output, run func with --verbose flag. エンドポイントが正しく表示されていることを確認してください。 /api/user-info エンドポイントのテスト まずは認証状態を問わずアクセス可能な user-info エンドポイントをテストします。こちらはcURLでもよいですし、ブラウザで実行してもよいです。 cURLでのテスト curl http://localhost:7071/api/user-info 期待されるレスポンス ローカル環境では認証ヘッダーが存在しないため、未認証状態のレスポンスが返されます: { "userId": null, "name": null, "email": null, "provider": null, "roles": [], "isAuthenticated": false } 確認ポイント ステータスコードが 200 OK であること レスポンスの型が設計通りであること isAuthenticated が false になっていること 各プロパティが正しく null または空配列になっていること /api/protected-data エンドポイントのテスト 次に、認証が必須の protected-data エンドポイントをテストします。 cURLでのテスト curl http://localhost:7071/api/protected-data 期待されるレスポンス 認証ヘッダーが存在しないため、401エラーが返されます: { "error": "Unauthorized", "message": "保護されたデータにアクセスするには認証が必要です" } 確認ポイント ステータスコードが 401 Unauthorized であること エラーレスポンスの形式が統一されていること error と message プロパティが含まれていること デバッグとログ確認 開発中は、Function Core Tools のコンソール出力でデバッグ情報を確認できます。 ログ出力の確認 API関数内の context.log() による出力がコンソールに表示されます: [2025-06-30T14:13:03.946Z] Executing 'Functions.protected-data' (Reason='This function was programmatically called via the host APIs.', Id=3570bfd5-7ed0-4ea7-a34f-262df44324ac) [2025-06-30T14:13:03.951Z] protected-dataリクエストを処理中 [2025-06-30T14:13:03.951Z] 認証ヘッダーが見つかりませんでした [2025-06-30T14:13:03.952Z] Executed 'Functions.protected-data' (Succeeded, Id=3570bfd5-7ed0-4ea7-a34f-262df44324ac, Duration=5ms) 詳細ログの表示 より詳細なログが必要な場合は、 --verbose オプションを使用します: func start --verbose トラブルシューティング 開発時に詰まった点をトラブルシューティングとしてまとめておきます。 エンドポイントにアクセスできない Functions Core Tools が正常に起動しているか確認 ポート 7071 が他のプロセスで使用されていないか確認 ファイアウォール設定を確認 TypeScript コンパイルエラー npm run build でビルドエラーがないか確認 型定義の不整合がないかチェック レスポンスが期待通りでない context.log() でデバッグ出力を追加 リクエストヘッダーが正しく設定されているか確認 ソースコードに変更を加えたのに変更が反映されない func start は変更後のソースを読み取って同期してくれません 一度 Ctrl+C で実行を落として、再起動してみてください これで、ローカル環境でのAPI基本動作確認が完了しました。次に、フロントエンドの実装に移りましょう。認証機能を含む完全なテストは、SWA CLI での統合テスト環境で行います。 コラム:なぜauthlevelはanonymousでよいのか? Azure Functions では通常、API の認証レベルを function 、 admin 、 anonymous などから選択できます。しかし、Azure Static Web Apps の Managed Functions では anonymous を設定するのが適切です。 Azure Functions単体での認証レベル function :Function Key が必要 admin :Master Key が必要 anonymous :認証不要 SWA + Managed Functions での認証の仕組み Azure Static Web Apps では、認証・認可を SWA側で一元管理 します: SWAレベルでの保護 : staticwebapp.config.json でルート保護を定義 統合セキュリティ : /.auth システムによる認証管理 Functions側はanonymous :SWAが認証済みリクエストのみをFunctionsに転送 つまり、Functionsレベルでの認証をバイパスし、SWAの統合認証システムを活用することで、よりシームレスで一元化された認証管理が可能になります。Managed Functions では、この仕組みにより安全性を保ちながら開発効率を向上させることができるのです。 Next.jsフロントエンド実装 この章では、Next,js環境を構築・実装をしていきます。作成するディレクトリ構成は以下になります。初期設定はcreate-next-app経由で作成するため、作成するファイルは useAuth.ts / page.tsx / protected>page.tsx の3ファイルになります。 ./frontend ├── eslint.config.mjs ├── next.config.ts ├── next-env.d.ts ├── package.json ├── package-lock.json ├── postcss.config.mjs ├── README.md ├── public ├── src │ ├── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx // トップページ │ │ └── protected │ │ └── page.tsx           // 認証済みルート │ └── hooks │ └── useAuth.ts // 認証系Hooks └── tsconfig.json Next.js設定(静的エクスポート対応) まずは frontend ディレクトリを作成して、ディレクトリに移動します。作成自体は、 npx create-next-app コマンドを通して自動生成を行っています。バージョンとしては15固定で、プロジェクト設定を行っています。 mkdir frontend cd frontend npx create-next-app@15 . --typescript --tailwind --eslint --app --src-dir --import-alias "@/*" 静的エクスポート設定のために next.config.ts を変種する必要があります。 import type { NextConfig } from "next"; const nextConfig: NextConfig = { /* config options here */ output: 'export', // 静的エクスポート設定 distDir: 'out', // ビルドファイルのエクスポート先設定 images: { unoptimized: true // 静的エクスポートでは使用できない画像圧縮の無効化 } }; export default nextConfig; これで、静的エクスポートが実装することができます。 useAuth認証カスタムフック実装 こちらのファイルは hooks ディレクトリを作成し、 useAuth.ts というファイル内にコピー&ペーストをしてください。 import { useState, useEffect } from 'react'; interface UserInfo { isAuthenticated: boolean; user?: { name?: string; email?: string; login?: string; }; } export const useAuth = () => { const [userInfo, setUserInfo] = useState<UserInfo>({ isAuthenticated: false }); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { const fetchUserInfo = async () => { try { const response = await fetch('/.auth/me'); if (response.ok) { const data = await response.json(); if (data.clientPrincipal) { setUserInfo({ isAuthenticated: true, user: { name: data.clientPrincipal.userDetails, email: data.clientPrincipal.userDetails, login: data.clientPrincipal.userId } }); } else { setUserInfo({ isAuthenticated: false }); } } else { setUserInfo({ isAuthenticated: false }); } } catch (err) { console.error('認証情報の取得に失敗しました:', err); setError('認証情報の取得に失敗しました'); setUserInfo({ isAuthenticated: false }); } finally { setLoading(false); } }; fetchUserInfo(); }, []); const login = () => { window.location.href = '/.auth/login/github'; }; const logout = () => { window.location.href = '/.auth/logout'; }; return { ...userInfo, loading, error, login, logout }; }; 処理としては、ログインとログアウトロジックがあります。こちらは、認証プロバイダー(GitHub)としてチューニングされています。 SWAでは認証済みの場合、 直接エンドポイントとして /.auth/me に認証情報が含まれます 。そちらに対して確認を行うことで、フロントエンド側から認証状態の確認を行うことができます。 認証状態の確認中は useState の loading で制御をしています。 トップページ実装 こちらは src > app > page.tsx ファイルを編集してください。まずはコピー&ペーストをしてください。 'use client'; import Link from 'next/link'; import { useAuth } from '@/hooks/useAuth'; import { useState } from 'react'; interface ApiUserInfo { userId: string; name: string; email: string; provider: string; roles: string[]; isAuthenticated: boolean; } interface ApiError { error: string; message: string; } export default function HomePage() { const { isAuthenticated, user, loading, login, logout } = useAuth(); const [apiData, setApiData] = useState<ApiUserInfo | null>(null); const [apiError, setApiError] = useState<string | null>(null); const [apiLoading, setApiLoading] = useState(false); const fetchUserInfo = async () => { setApiLoading(true); setApiError(null); setApiData(null); try { const response = await fetch('/api/user-info', { method: 'GET', headers: { 'Content-Type': 'application/json', }, credentials: 'include', // 認証情報を含める }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data: ApiUserInfo | ApiError = await response.json(); if ('error' in data) { setApiError(data.message || data.error); } else { setApiData(data); } } catch (error) { console.error('API呼び出しエラー:', error); setApiError(error instanceof Error ? error.message : 'APIの呼び出しに失敗しました'); } finally { setApiLoading(false); } }; if (loading) { return ( <div className="min-h-screen flex items-center justify-center"> <div className="text-center"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div> <p className="text-gray-600">認証状態を確認中...</p> </div> </div> ); } return ( <div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100"> <div className="container mx-auto px-4 py-16"> <div className="max-w-4xl mx-auto text-center"> <h1 className="text-5xl font-bold text-gray-900 mb-8"> Next.js 15 + Azure SWA </h1> <p className="text-xl text-gray-600 mb-12"> GitHub認証を使用したセキュアなWebアプリケーション </p> <div className="bg-white rounded-lg shadow-lg p-8 mb-8"> <h2 className="text-2xl font-semibold text-gray-800 mb-6"> 認証状態 </h2> {isAuthenticated ? ( <div className="space-y-4"> <div className="flex items-center justify-center space-x-2"> <div className="w-3 h-3 bg-green-500 rounded-full"></div> <span className="text-green-700 font-medium">認証済み</span> </div> {user && ( <div className="bg-gray-50 rounded-lg p-4 text-left max-w-md mx-auto"> <h3 className="font-medium text-gray-800 mb-2">ユーザー情報(クライアント側)</h3> {user.name && ( <p className="text-sm text-gray-600"> <span className="font-medium">名前:</span> {user.name} </p> )} {user.email && ( <p className="text-sm text-gray-600"> <span className="font-medium">メール:</span> {user.email} </p> )} {user.login && ( <p className="text-sm text-gray-600"> <span className="font-medium">ログイン:</span> {user.login} </p> )} </div> )} <div className="flex flex-col sm:flex-row gap-4 justify-center mt-6"> <Link href="/protected" className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition duration-200" > 保護されたページへ </Link> <button onClick={logout} className="bg-gray-500 hover:bg-gray-600 text-white font-medium py-3 px-6 rounded-lg transition duration-200" > ログアウト </button> </div> </div> ) : ( <div className="space-y-4"> <div className="flex items-center justify-center space-x-2"> <div className="w-3 h-3 bg-red-500 rounded-full"></div> <span className="text-red-700 font-medium">未認証</span> </div> <p className="text-gray-600 mb-6"> 保護されたコンテンツにアクセスするには、GitHubアカウントでログインしてください。 </p> <button onClick={login} className="bg-gray-900 hover:bg-gray-800 text-white font-medium py-3 px-6 rounded-lg transition duration-200 flex items-center space-x-2 mx-auto" > <svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20"> <path fillRule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clipRule="evenodd" /> </svg> <span>GitHubでログイン</span> </button> </div> )} </div> {/* API情報取得セクション */} <div className="bg-white rounded-lg shadow-lg p-8 mb-8"> <h2 className="text-2xl font-semibold text-gray-800 mb-6"> API情報取得 </h2> <div className="space-y-4"> <button onClick={fetchUserInfo} disabled={apiLoading} className="bg-green-600 hover:bg-green-700 disabled:bg-green-400 text-white font-medium py-3 px-6 rounded-lg transition duration-200 flex items-center space-x-2 mx-auto" > {apiLoading ? ( <> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div> <span>取得中...</span> </> ) : ( <> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" /> </svg> <span>サーバーからユーザー情報を取得</span> </> )} </button> {apiError && ( <div className="bg-red-50 border border-red-200 rounded-lg p-4 text-left max-w-md mx-auto"> <h3 className="font-medium text-red-800 mb-2">エラー</h3> <p className="text-sm text-red-600">{apiError}</p> </div> )} {apiData && ( <div className={" rounded-lg p-4 text-left max-w-md mx-auto" + (apiData.isAuthenticated ? ' bg-green-50 border border-green-200' : ' bg-red-50 border border-red-200')} > <h3 className={"font-medium mb-2" + (apiData.isAuthenticated ? " text-green-800": " text-red-800")}>サーバー側ユーザー情報</h3> <div className={"space-y-1 text-sm " + (apiData.isAuthenticated ? " text-green-700": " text-red-700")}> <p><span className="font-medium">認証状態:</span> {apiData.isAuthenticated ? '認証済み' : '未認証'}</p> <p><span className="font-medium">ユーザーID:</span> {apiData.userId}</p> <p><span className="font-medium">名前:</span> {apiData.name}</p> <p><span className="font-medium">メール:</span> {apiData.email}</p> <p><span className="font-medium">プロバイダー:</span> {apiData.provider}</p> <p><span className="font-medium">ロール:</span> {apiData.roles.length > 0 ? apiData.roles.join(', ') : 'なし'}</p> </div> </div> )} </div> </div> <div className="text-center text-gray-500"> <p className="text-sm"> このアプリケーションはAzure Static Web Appsで認証を管理しています </p> </div> </div> </div> </div> ); } ロジック部分 ロジック部分を抽出しました。こちらは、ボタンを押した際に /api/user-info に対してGETリクエストを送信して問い合わせを行っています。APIリクエストの状態管理は useState の apiLoading でエラーに関しては useState の apiError で管理をしています。 interface ApiUserInfo { userId: string; name: string; email: string; provider: string; roles: string[]; isAuthenticated: boolean; } interface ApiError { error: string; message: string; } const [apiData, setApiData] = useState<ApiUserInfo | null>(null); const [apiError, setApiError] = useState<string | null>(null); const [apiLoading, setApiLoading] = useState(false); const fetchUserInfo = async () => { setApiLoading(true); setApiError(null); setApiData(null); try { const response = await fetch('/api/user-info', { method: 'GET', headers: { 'Content-Type': 'application/json', } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data: ApiUserInfo | ApiError = await response.json(); if ('error' in data) { setApiError(data.message || data.error); } else { setApiData(data); } } catch (error) { console.error('API呼び出しエラー:', error); setApiError(error instanceof Error ? error.message : 'APIの呼び出しに失敗しました'); } finally { setApiLoading(false); } }; 表示部分 表示部分としてはコードとして膨大なので、ソース添付としては割愛します。処理としては単純です。 認証確認中は useAuth から提供される loading をフラグとしてローディング画面を表示しています。 認証後は、APIアクセス中はapiLoadingでボタンの非活性化とローディングの表示、エラーが発生した場合はエラーの表示を行っています。 protectedページ実装 こちらは src > app > protected > page.tsx ファイルを新規作成してください。まずはコピー&ペーストをしてください。 'use client'; import Link from 'next/link'; import { useAuth } from '@/hooks/useAuth'; import { useState } from 'react'; interface ProtectedData { userId: string; message: string; timestamp: string; userNumber: number; } interface ApiError { error: string; message: string; } export default function ProtectedPage() { const { user, loading, logout } = useAuth(); const [protectedData, setProtectedData] = useState<ProtectedData | null>(null); const [apiError, setApiError] = useState<string | null>(null); const [apiLoading, setApiLoading] = useState(false); const fetchProtectedData = async () => { setApiLoading(true); setApiError(null); setProtectedData(null); try { const response = await fetch('/api/protected-data', { method: 'GET', headers: { 'Content-Type': 'application/json', } }); if (!response.ok) { if (response.status === 401) { throw new Error('認証が必要です'); } throw new Error(`HTTP error! status: ${response.status}`); } const data: ProtectedData | ApiError = await response.json(); if ('error' in data) { setApiError(data.message || data.error); } else { setProtectedData(data); } } catch (error) { console.error('保護されたAPI呼び出しエラー:', error); setApiError(error instanceof Error ? error.message : '保護されたデータの取得に失敗しました'); } finally { setApiLoading(false); } }; if (loading) { return ( <div className="min-h-screen flex items-center justify-center"> <div className="text-center"> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div> <p className="text-gray-600">ユーザー情報を読み込み中...</p> </div> </div> ); } return ( <div className="min-h-screen bg-gradient-to-br from-green-50 to-emerald-100"> <div className="container mx-auto px-4 py-16"> <div className="max-w-4xl mx-auto"> <div className="text-center mb-12"> <div className="flex items-center justify-center space-x-2 mb-4"> <div className="w-4 h-4 bg-green-500 rounded-full"></div> <span className="text-green-700 font-medium text-lg">保護されたエリア</span> </div> <h1 className="text-4xl font-bold text-gray-900 mb-4"> 🎉 認証成功! </h1> <p className="text-xl text-gray-600"> このページは認証済みユーザーのみがアクセスできます </p> </div> <div className="bg-white rounded-lg shadow-lg p-8 mb-8"> <h2 className="text-2xl font-semibold text-gray-800 mb-6 text-center"> あなたの情報 </h2> {user ? ( <div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-lg p-6 mb-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> {user.name && ( <div className="bg-white rounded-lg p-4"> <div className="text-sm text-gray-500 font-medium">名前</div> <div className="text-lg text-gray-900">{user.name}</div> </div> )} {user.email && ( <div className="bg-white rounded-lg p-4"> <div className="text-sm text-gray-500 font-medium">メールアドレス</div> <div className="text-lg text-gray-900">{user.email}</div> </div> )} {user.login && ( <div className="bg-white rounded-lg p-4"> <div className="text-sm text-gray-500 font-medium">GitHubユーザー名</div> <div className="text-lg text-gray-900">@{user.login}</div> </div> )} <div className="bg-white rounded-lg p-4"> <div className="text-sm text-gray-500 font-medium">アクセス日時</div> <div className="text-lg text-gray-900"> {new Date().toLocaleString('ja-JP')} </div> </div> </div> </div> ) : ( <div className="text-center py-8"> <p className="text-gray-500">ユーザー情報が見つかりません</p> </div> )} <div className="bg-blue-50 border border-blue-200 rounded-lg p-6 mb-6"> <h3 className="text-lg font-semibold text-blue-900 mb-3"> 🔒 セキュリティ情報 </h3> <ul className="text-blue-800 space-y-2"> <li>• このページはAzure SWAレベルで保護されています</li> <li>• 未認証ユーザーは自動的にGitHubログインにリダイレクトされます</li> <li>• 認証情報はAzure Static Web Appsで安全に管理されています</li> </ul> </div> <div className="flex flex-col sm:flex-row gap-4 justify-center"> <Link href="/" className="bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition duration-200 text-center" > ホームに戻る </Link> <button onClick={logout} className="bg-red-500 hover:bg-red-600 text-white font-medium py-3 px-6 rounded-lg transition duration-200" > ログアウト </button> </div> </div> {/* 保護されたデータ取得セクション */} <div className="bg-white rounded-lg shadow-lg p-8 mb-8"> <h2 className="text-2xl font-semibold text-gray-800 mb-6 text-center"> 🛡 保護されたデータ </h2> <div className="space-y-6"> <div className="text-center"> <p className="text-gray-600 mb-4"> サーバー側で認証を確認し、あなた専用のデータを取得します </p> <button onClick={fetchProtectedData} disabled={apiLoading} className="bg-purple-600 hover:bg-purple-700 disabled:bg-purple-400 text-white font-medium py-3 px-6 rounded-lg transition duration-200 flex items-center space-x-2 mx-auto" > {apiLoading ? ( <> <div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div> <span>データ取得中...</span> </> ) : ( <> <svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /> </svg> <span>保護されたデータを取得</span> </> )} </button> </div> {apiError && ( <div className="bg-red-50 border border-red-200 rounded-lg p-6"> <div className="flex items-center space-x-2 mb-2"> <svg className="w-5 h-5 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> <h3 className="font-medium text-red-800">エラー</h3> </div> <p className="text-sm text-red-600">{apiError}</p> </div> )} {protectedData && ( <div className="bg-gradient-to-r from-purple-50 to-pink-50 border border-purple-200 rounded-lg p-6"> <div className="flex items-center space-x-2 mb-4"> <svg className="w-5 h-5 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /> </svg> <h3 className="font-medium text-purple-800">取得成功 🎯</h3> </div> <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="bg-white rounded-lg p-4"> <div className="text-sm text-gray-500 font-medium">ユーザーID</div> <div className="text-lg text-gray-900 font-mono">{protectedData.userId}</div> </div> <div className="bg-white rounded-lg p-4"> <div className="text-sm text-gray-500 font-medium">ユーザー番号</div> <div className="text-lg text-gray-900 font-bold "> #{protectedData.userNumber} </div> </div> <div className="bg-white rounded-lg p-4 md:col-span-2"> <div className="text-sm text-gray-500 font-medium">パーソナライズメッセージ</div> <div className="text-lg text-gray-900">{protectedData.message}</div> </div> <div className="bg-white rounded-lg p-4 md:col-span-2"> <div className="text-sm text-gray-500 font-medium">データ生成時刻</div> <div className="text-lg text-gray-900 font-mono"> {new Date(protectedData.timestamp).toLocaleString('ja-JP')} </div> </div> </div> <div className="mt-4 p-3 bg-purple-100 rounded-lg"> <p className="text-sm text-purple-700"> 💡 このデータはあなたのユーザーIDに基づいて動的に生成され、 サーバー側で認証を確認した後にのみ提供されます。 </p> </div> </div> )} </div> </div> <div className="text-center text-gray-500"> <p className="text-sm"> Azure Static Web Apps + GitHub認証で実現したセキュアなアプリケーション </p> </div> </div> </div> </div> ); } ロジック部分 ロジック部分を抽出しました。こちらは、ボタンを押した際に /api/protected-data に対してGETリクエストを送信して問い合わせを行っています。APIリクエストの状態管理は useState の apiLoading でエラーに関しては useState の apiError で管理をしています。 SWA側で401エラーが発生した場合は、アクセスできないように組みますが将来的なエラーハンドリングのために仮で実装しています。 interface ProtectedData { userId: string; message: string; timestamp: string; userNumber: number; } interface ApiError { error: string; message: string; } const [protectedData, setProtectedData] = useState<ProtectedData | null>(null); const [apiError, setApiError] = useState<string | null>(null); const [apiLoading, setApiLoading] = useState(false); const fetchProtectedData = async () => { setApiLoading(true); setApiError(null); setProtectedData(null); try { const response = await fetch('/api/protected-data', { method: 'GET', headers: { 'Content-Type': 'application/json', } }); if (!response.ok) { if (response.status === 401) { throw new Error('認証が必要です'); } throw new Error(`HTTP error! status: ${response.status}`); } const data: ProtectedData | ApiError = await response.json(); if ('error' in data) { setApiError(data.message || data.error); } else { setProtectedData(data); } } catch (error) { console.error('保護されたAPI呼び出しエラー:', error); setApiError(error instanceof Error ? error.message : '保護されたデータの取得に失敗しました'); } finally { setApiLoading(false); } }; 表示部分 表示部分としてはコードとして膨大なので、ソース添付としては割愛します。処理としては単純です。 認証確認中は useAuth から提供される loading をフラグとしてローディング画面を表示しています。 認証後は、APIアクセス中はapiLoadingでボタンの非活性化とローディングの表示、エラーが発生した場合はエラーの表示を行っています。 ローカルでのフロントエンド動作確認 ローカルでフロントエンドのテストをします。APIとの接続を行っていないので、APIアクセスはすべて失敗するので画面上のテストのみになります。 npm run dev # http://localhost:3000 ローカル結合テスト(SWA CLI) ここでは、SWA CLIを使用してフロントエンドとAzure Functionsの結合テストを行っていきます。SWA CLIを使うことで、SWAに上げた時の挙動を認証部分を含めてエミュレーターで確認することができます。 SWA CLI設定 プロジェクトルートで、以下のコマンドを入力することで対話的に swa-cli.config.json を作成することが可能です。 swa init 設定項目は公式のこちら にまとまっています。若干設定が複雑な部分があるため、今回の環境に合わせた構成ファイルに対して解説します。 swaConfigLocation に関してのみ次の節で解説を行います。 フロントもバックもSWA CLIで起動する こちらの構成ファイルでは、 swa start でフロントもバックのリソースも立ち上がるようになります。利点としては、コマンド一つで立ち上げることができるので楽な点ですね。 フロントに関しては、devコマンドで起動されているためフロントのコードを変更することで自動的に更新(ホットリロード)が入ります。バックに関しては、更新を反映させるためには一度コマンドを落として再度実行する必要があります。 { "$schema": "https://aka.ms/azure/static-web-apps-cli/schema", "configurations": { "swa-api-bicep-sample": { "appLocation": "frontend", "apiLocation": "api", "outputLocation": "out", "apiLanguage": "node", "appBuildCommand": "npm run build", "apiBuildCommand": "npm run build --if-present", "run": "npm run dev", "appDevserverUrl": "http://localhost:3000", "swaConfigLocation": "frontend/config/local" } } } フロントのみSWA CLIで起動してバックは起動済みのものを使用する こちらの構成ファイルでは、 swa start でフロントのリソースのみを立ち上げ、バックエンドのリソースは提供済みのものを使用します。こちらの利点としては、バックエンドのリソースのみを分離して立ち上げ直しができる点ですね。その代わり、実行には func start コマンドを別途実行してあげる必要があります。 { "$schema": "https://aka.ms/azure/static-web-apps-cli/schema", "configurations": { "swa-api-bicep-sample": { "appLocation": "frontend", "apiLocation": "api", "outputLocation": "out", "apiLanguage": "node", "appBuildCommand": "npm run build", "apiBuildCommand": "npm run build --if-present", "run": "npm run dev", "appDevserverUrl": "http://localhost:3000", "apiDevserverUrl": "http://localhost:7071", "swaConfigLocation": "frontend/config/local" } } } SWA設定ファイル(staticwebapp.config.json) こちらのファイルは、認証ルートの定義やSWA側でサーバー側で発生したエラーをキャッチしてルーティングを制御します。先ほどの swa-cli.config.json で swaConfigLocation を設定していましたが、これは開発環境と本番環境で二つのファイルを構成する必要があるためです。開発環境では、認証プロバイダーの設定をSWA CLIのエミュレーターで代用します。本番環境では、SWAの環境変数から認証プロバイダーに必要な情報を読み取って認証プロバイダーを構成します。 本番環境用の構成ファイルを使用してSWA CLIでローカルで立ち上げると認証時に失敗します。 SWA 設定ファイルは大部分がフロントのルーティング定義であるため、 frontend > config とディレクトリを定義してその中にディレクトリ単位で local と prod と分割して同名ファイルを保存します。 ./frontend └── config ├── local │ └── staticwebapp.config.json └── prod └── staticwebapp.config.json 開発環境用 SWAに認証されるとユーザーには自動的に「authenticated」と「anoymous」というロールが割り振られます 。SWA Configでは、ロールによって表示することができるルート制御というのがあり、今回であれば /protected ルートでは authenticated ロールにアクセスを許可しています。それ以外のルートに関しては、200で許可しています。もし未認証の方が /protected にアクセスした場合はSWA側で401エラーが発生して、自動的に認証画面にリダイレクトするように responseOverrdes で設定しています。ここをエラー用のページに設定しておけば、自動でエラー画面を表示するなんてこともできます。 platform の設定はAzure Functionsの設定になります。今回であればnode系を使用していたため設定しています。 { "routes": [ { "route": "/protected/*", "allowedRoles": ["authenticated"] }, { "route": "/*", "statusCode": 200 } ], "responseOverrides": { "401": { "redirect": "/.auth/login/github", "statusCode": 302 } }, "platform": { "apiRuntime": "node:18" } } 本番環境 大枠は開発環境と同じですが、こちらでは auth でカスタム認証プロバイダーの設定がされています。 設定方法は公式のこちらで言及 があります。こちらはSWAの環境変数から値を読み取っているため、SWA側の設定として GITHUB_CLIENT_ID と GITHUB_CLIENT_SECRET を設定してあげる必要があります。こちらは、Azureのリソース作成の手順の際に合わせて設定します。 { "auth": { "identityProviders": { "github": { "registration": { "clientIdSettingName": "GITHUB_CLIENT_ID", "clientSecretSettingName": "GITHUB_CLIENT_SECRET" } } } }, "routes": [ { "route": "/protected/*", "allowedRoles": ["authenticated"] }, { "route": "/*", "statusCode": 200 } ], "responseOverrides": { "401": { "redirect": "/.auth/login/github", "statusCode": 302 } }, "platform": { "apiRuntime": "node:18" } } フロントエンド + API結合確認 それでは、接続してテストを開始しましょう。これまでの設定が完了していれば、ルートディレクトリで以下のコマンドでエミュレーターが起動します。もし起動しない方は、swa-cli.config.jsonの設定を再度確認してください。 http://localhost:7071 ready で1分以上固まっている方は、apiディレクトリで func start コマンドを実行してください。 # swa-cli.config.jsonでの設定による起動 swa start 動作確認テストの実施 さて、ここまででフロントエンドとAPIの実装が完了しましたね!いよいよ統合テストの時間です。今回は正常系の動作確認に集中して進めていきます。Azure SWAの素晴らしいところは、認証周りの面倒な処理を自動でやってくれることなので、エラーハンドリングよりも「ちゃんと期待通りに動いているか」を確認することが重要ですね。 実際のテストでは、未認証状態と認証状態の両方で動作を確認していきます。特に認証フローがスムーズに動作するかどうかが、このプロジェクトの肝になる部分です。 未認証状態での動作確認 まずは未認証状態から確認していきましょう。ブラウザでシークレットモードを開くか、既存のセッションをクリアしてからテストを開始します。 トップページ( / )での確認項目 : ページにアクセスすると「未認証状態」と表示される 「サーバーからユーザー情報を取得」ボタンをクリックしても、ユーザー情報は取得できない(nullやundefinedが返される) 「GitHubでログイン」ボタンが表示され、クリックすると認証画面に遷移する 保護ページ( /protected )での確認項目 : URLを直接入力してアクセスを試みると、自動的に認証画面に遷移する この時点では、Azure SWAが未認証ユーザーを自動的に認証フローに誘導してくれるので、エラー画面が表示されることはありません。これがSWAの便利なところですね! 認証状態での動作確認 続いて、GitHub認証を完了した状態での動作を確認します。認証が成功すると、トップページにリダイレクトされるはずです。認証画面は以下のような画面になります。Providerに関しては、埋められた状態で開かれるはずです。UsernameはGitHub認証の場合はユーザー名になります。(表示名ではありません @以下です) トップページ( / )での確認項目 : ページ上部に「認証状態」と表示される 「サーバーからユーザー情報を取得」ボタンをクリックすると、GitHubから取得したユーザー情報が表示される 「保護されたページ」ボタンと「ログアウト」ボタンが表示される 保護ページ( /protected )での確認項目 : ページに正常にアクセスできる 「保護されたデータを取得」ボタンをクリックすると、サーバーサイドのAPIからダミーデータが取得される 認証が正しく動作していれば、Azure Functionsに送信される x-ms-client-principal ヘッダーにユーザー情報が含まれているので、APIから適切なレスポンスが返ってきます。 テスト実施時のポイント テストを実施する際に気をつけておきたいポイントをいくつか挙げておきますね。 Azure SWAの自動処理について : 未認証時の保護リソースへのアクセスや、 /api/protected-data への直接アクセスは、SWAの設定により自動的に認証画面にリダイレクトされます。そのため、401エラーが画面に表示されることはなく、ユーザーは常にスムーズな認証フローを体験できます。 セッション管理の確認 : ページをリロードしても認証状態が維持されることを確認してください。これはAzure SWAが内部的にセッション管理を行っているためです。 API呼び出しの動作 : 認証状態でのAPI呼び出しでは、Azure SWAが自動的に認証ヘッダーを付与してくれるので、フロントエンド側で特別な処理は不要です。これも開発者としてはとても楽な部分ですね。 今回のテストはシンプルですが、実際のプロダクションで必要な基本的な動作はすべてカバーできています。正常系がしっかり動作することを確認できれば、Azure SWAとNext.js、Azure Functionsの統合は成功です! インフラ構築(Bicep) これまでの章でローカルの開発は完了しました。ここからは、実際にAzure環境へのデプロイをするための準備を行います。具体的にはBicepとパラメーターファイルを作成してAzureのリソースの定義を行い、作成したファイルを使用してGitHubにデプロイに必要な変数の定義とAzureリソースを作成するbashファイルを用意します。 注意点:ここら先はAzure Static Web Appsに対してリソースの作成を行います。今回はStandardプランでリソースを作成するため、もし不安な方はFree版でのデプロイで練習をしておきましょう。別の環境を使って詳細にデプロイするためのガイドを書いているので、 こちらのブログ を参照して練習してみてください。 事前準備 Bicepファイルとパラメーターファイルの説明などはこちらで詳しく解説 しています。ぜひ一度お読みください。 こちらの章で目指す内容は以下になります。 Azure CLI:Azure Static Web AppsのリソースをStandardプランで作成する 手動:GitHub認証プロバイダーの設定完了 GitHub CLI:GitHub Enviromentにデプロイに必要な情報が設定されている 作成に当たって目指すディレクトリ構成は以下になります。 ./infra ├── bicep │ ├── main.bicep │ └── parameters.json └── scripts └── deploy.sh GitHubの設定 まずは今まで設定したリソースをGitHub上にpushしておきましょう。GitHub CLIで environment を設定するためには事前に environment を作成しておく必要があります。 production という名前で environment を作成してください。設定は 公式のドキュメントを参考に設定 してください。 値の登録自体は、GitHub CLI経由で実行することができます。ですが、Enviromentの作成はGitHub API経由でしか行うことができません。ちょっと設定がややこしいので、ここは手動で作成しています。 ここで取得するべき情報としてはリポジトリのURLになります。 Bicepテンプレート作成 main.bicep 詳細な設定に関しては公式リファレンスを参照してください。 Microsoft.Web/staticSites Microsoft.Web/staticSites/config @description('Static Web App名') param staticWebAppName string @description('デプロイするリージョン') param location string = resourceGroup().location @description('GitHubリポジトリのURL') param repositoryUrl string @description('デプロイ対象のブランチ') param branch string = 'main' @description('フロントエンドのソースフォルダパス') param appLocation string = 'frontend' @description('APIのソースフォルダパス') param apiLocation string = 'api' @description('ビルド出力フォルダパス') param outputLocation string = 'out' @description('GitHub OAuth App のClient ID') param githubClientId string @description('GitHub OAuth App のClient Secret') @secure() param githubClientSecret string // Static Web App リソース resource staticWebApp 'Microsoft.Web/staticSites@2024-11-01' = { name: staticWebAppName location: location sku: { name: 'Standard' tier: 'Standard' } properties: { repositoryUrl: repositoryUrl branch: branch buildProperties: { appLocation: appLocation apiLocation: apiLocation outputLocation: outputLocation skipGithubActionWorkflowGeneration: false } stagingEnvironmentPolicy: 'Enabled' } } // アプリケーション設定(GitHub OAuth用の環境変数) resource staticWebAppSettings 'Microsoft.Web/staticSites/config@2022-03-01' = { name: 'appsettings' parent: staticWebApp properties: { GITHUB_CLIENT_ID: githubClientId GITHUB_CLIENT_SECRET: githubClientSecret } } // アウトプット @description('Static Web Appsのエンドポイント') output appBaseUrl string = 'https://${staticWebApp.properties.defaultHostname}' @description('Static Web Appsの名前') output resourceName string = staticWebAppName @description('GitHubリポジトリのURL') output repositoryUrl string = repositoryUrl @description('フロントエンドのソースフォルダパス') output appLocation string = appLocation @description('APIのソースフォルダパス') output apiLocation string = apiLocation @description('ビルド出力フォルダパス') output outputLocation string = outputLocation 課金に重要なパラメーターとしては sku になります。ここをFreeにするかStandardにするかで課金が走るか決定します。(個人的には検証目的で建てたらすぐ消しましょう) staticWebAppSettings に関しては、SWAでカスタム認証プロバイダー(GitHub)を構成するのに必要な値を環境変数として設定しています。機密情報であるため、パラメーターファイルには記述せずに環境変数として定義して、デプロイ時のみ呼び出して埋め込むという方法で管理します。 パラメーターファイルで定義している内容をそのままアウトプットしている値が複数ありますが、こちらは後続のbashファイルでGitHub CLIを通してEnvriomentにValiableとして登録するために出力しています。 パラメータファイル設定 parameters.json { "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#", "contentVersion": "1.0.0.0", "parameters": { "staticWebAppName": { "value": "swa-api-deploy-bicep" }, "location": { "value": "East Asia" }, "repositoryUrl": { "value": "https://github.com/xxxxxxxxxxxxxxxx/xxxxxxxxxxxxxxxxxxxxxx" }, "branch": { "value": "main" }, "appLocation": { "value": "deploy/frontend" }, "apiLocation": { "value": "api" }, "outputLocation": { "value": "." } } } appLocation / apiLocation / outputLocation に関しては後続で設定するGitHub Actionsと連携した値になります。 repositoryUrl に関しては自分のリポジトリの値に変更してください。 デプロイ用bashスクリプト deploy.sh Bicepファイルとパラメーターファイルが完成したのでデプロイする準備が整いました。ですが、このままではAzure CLIのコマンドとGitHub CLIコマンドを別々にたたく必要があります。それを解消するためにデプロイの流れをbashファイルにまとめます。 まずは、ルートディレクトリに .env ファイルを作成してください。こちらのファイルは .git と同じディレクトリ(リポジトリルート)に配置するようにしてください。 GITHUB_CLIENT_ID=xxxxxxxxxxxxxxxxx GITHUB_CLIENT_SECRET=xxxxxxxxxxxxxxxxx GITHUB_ENVIRONMENT="production" RESOURCE_GROUP_NAME=swa-bicep-test GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET に関しては、まだ設定していないのでマスクしたままで大丈夫です。こちらの設定には、SWAをデプロイしてURLを確定させる必要があります。 これで.envが立派な機密情報になりました。.gitignoreに設定して間違ってもpushしないように気を付けましょう。 #!/bin/bash # エラー時に停止 set -e log_info() { echo -e "\033[1;34m$1\033[0m"; } log_success() { echo -e "\033[1;32m$1\033[0m"; } log_warning() { echo -e "\033[1;33m$1\033[0m"; } log_error() { echo -e "\033[1;31m$1\033[0m"; } DEPLOYMENT_NAME="swa-deployment-$(date +%Y%m%d-%H%M%S)" CONFIG_FILE="$(git rev-parse --show-toplevel)/.env" if [ -f "$CONFIG_FILE" ]; then echo "📁 設定ファイル読み込み: $CONFIG_FILE" source "$CONFIG_FILE" else echo "⚠ 設定ファイルが見つかりません: $CONFIG_FILE" echo " .env.example をコピーして設定してください" exit 1 fi # 必要な環境変数の確認(機密情報のみ) required_vars=("GITHUB_CLIENT_ID" "GITHUB_CLIENT_SECRET" "RESOURCE_GROUP_NAME") for var in "${required_vars[@]}"; do if [ -z "${!var}" ]; then echo "エラー: 環境変数 $var が設定されていません" exit 1 fi done # GitHub CLIがインストールされているかチェック if ! command -v gh &> /dev/null; then log_error "GitHub CLI (gh) がインストールされていません" log_error "インストール方法: https://cli.github.com/" exit 1 fi # GitHub CLIの認証チェック if ! gh auth status &> /dev/null; then log_error "GitHub CLIが認証されていません" log_error "認証方法: gh auth login" exit 1 fi # リポジトリのルートディレクトリかチェック if ! git rev-parse --is-inside-work-tree &> /dev/null; then log_error "Gitリポジトリ内で実行してください" exit 1 fi # 現在のリポジトリ情報を取得(デバッグ用) CURRENT_REPO=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "unknown") log_info "📍 対象リポジトリ: $CURRENT_REPO" # リソースグループ作成(存在しない場合) log_info "" log_info "📦 ローカル開発用リソースグループ確認・作成..." if ! az group show --name $RESOURCE_GROUP_NAME &> /dev/null; then log_info "🔧 リソースグループ作成中: $RESOURCE_GROUP_NAME" az group create --name $RESOURCE_GROUP_NAME --location $LOCATION log_success "✅ リソースグループ作成完了" else log_success "✅ リソースグループ既存: $RESOURCE_GROUP_NAME" fi echo "Azure Static Web Appをデプロイ中..." az deployment group validate \ --resource-group "$RESOURCE_GROUP_NAME" \ --template-file infra/bicep/main.bicep \ --parameters @infra/bicep/parameters.json \ --parameters githubClientId="$GITHUB_CLIENT_ID" \ --parameters githubClientSecret="$GITHUB_CLIENT_SECRET" # Bicepテンプレートでデプロイ(機密情報のみ環境変数から渡す) az deployment group create \ --resource-group "$RESOURCE_GROUP_NAME" \ --template-file infra/bicep/main.bicep \ --parameters @infra/bicep/parameters.json \ --parameters githubClientId="$GITHUB_CLIENT_ID" \ --parameters githubClientSecret="$GITHUB_CLIENT_SECRET" \ --name ${DEPLOYMENT_NAME} # 全outputを一度に取得 OUTPUTS=$(az deployment group show \ --resource-group "$RESOURCE_GROUP_NAME" \ --name "$DEPLOYMENT_NAME" \ --query "properties.outputs" -o json) echo "✅ デプロイ結果を取得しました" # 各値を抽出 APP_BASE_URL=$(echo "$OUTPUTS" | jq -r '.appBaseUrl.value') STATIC_WEB_APP_NAME=$(echo "$OUTPUTS" | jq -r '.resourceName.value') REPOSITORY_URL=$(echo "$OUTPUTS" | jq -r '.repositoryUrl.value') APP_LOCATION=$(echo "$OUTPUTS" | jq -r '.appLocation.value') API_LOCATION=$(echo "$OUTPUTS" | jq -r '.apiLocation.value') OUTPUT_LOCATION=$(echo "$OUTPUTS" | jq -r '.outputLocation.value') # 結果を表示 log_success "" log_success "Azure Static Web Appのデプロイが完了しました!" log_success "アプリのベースURL: $APP_BASE_URL" log_success "リソース名: $STATIC_WEB_APP_NAME" log_success "リポジトリURL: $REPOSITORY_URL" log_success "アプリの場所: $APP_LOCATION" log_success "APIの場所: $API_LOCATION" log_success "出力の場所: $OUTPUT_LOCATION" # === 新機能: デプロイトークンの取得とGitHub Environment設定 === log_info "" log_info "🔑 Azure Static Web Appsのデプロイトークンを取得中..." # デプロイトークンを取得 DEPLOY_TOKEN=$(az staticwebapp secrets list \ --name "$STATIC_WEB_APP_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --query "properties.apiKey" -o tsv) if [ -z "$DEPLOY_TOKEN" ]; then log_error "デプロイトークンの取得に失敗しました" exit 1 fi log_success "✅ デプロイトークンを取得しました" # GitHub環境の作成と設定 log_info "" log_info "🌍 GitHub環境 '$GITHUB_ENVIRONMENT' を設定中..." # 注意: GitHub CLIは現在のGitリポジトリを自動認識します # このスクリプトはGitリポジトリ内で実行する必要があります # Environment変数の設定 log_info "" log_info "📝 Environment変数を設定中..." # app_location if gh variable set APP_LOCATION --body "$APP_LOCATION" --env "$GITHUB_ENVIRONMENT" 2>/dev/null; then log_success "✅ 変数 'app_location' を設定: $APP_LOCATION" else log_error "❌ 変数 'app_location' の設定に失敗しました" fi # api_location if gh variable set API_LOCATION --body "$API_LOCATION" --env "$GITHUB_ENVIRONMENT" 2>/dev/null; then log_success "✅ 変数 'api_location' を設定: $API_LOCATION" else log_error "❌ 変数 'api_location' の設定に失敗しました" fi # output_location if gh variable set OUTPUT_LOCATION --body "$OUTPUT_LOCATION" --env "$GITHUB_ENVIRONMENT" 2>/dev/null; then log_success "✅ 変数 'output_location' を設定: $OUTPUT_LOCATION" else log_error "❌ 変数 'output_location' の設定に失敗しました" fi # デプロイトークンをシークレットに設定 log_info "" log_info "🔐 デプロイトークンをシークレットに設定中..." # 注意: gh secret setは現在のGitリポジトリの指定した環境にシークレットを設定します if gh secret set AZURE_STATIC_WEB_APPS_API_TOKEN --body "$DEPLOY_TOKEN" --env "$GITHUB_ENVIRONMENT" 2>/dev/null; then log_success "✅ シークレット 'deploy_token' を設定しました" else log_error "❌ シークレット 'deploy_token' の設定に失敗しました" log_warning "⚠ 手動で設定してください:" log_warning " リポジトリ設定 > Environments > $GITHUB_ENVIRONMENT > Secrets" log_warning " シークレット名: deploy_token" log_warning " 値: [デプロイトークン]" fi # 最終結果サマリー log_success "" log_success "🎉 すべての設定が完了しました!" log_success "" log_success "📋 設定内容サマリー:" log_success " - Azure Static Web App: $STATIC_WEB_APP_NAME" log_success " - アプリURL: $APP_BASE_URL" log_success " - GitHub環境: $GITHUB_ENVIRONMENT" log_success " - 設定された変数:" log_success " • app_location: $APP_LOCATION" log_success " • api_location: $API_LOCATION" log_success " • output_location: $OUTPUT_LOCATION" log_success " - 設定されたシークレット:" log_success " • deploy_token: [設定済み]" log_success "" log_success "🚀 GitHub Actionsでのデプロイが可能になりました!" 長いbashファイルですが、以下のステップを順次実行しています。 環境変数の取得 各種環境の確認 Bicepを使用したデプロイ検証・実行 Azureリソースから情報取得 GitHub CLIを経由したSecret・Valiableの設定 リザルト表示 各コマンドに関しての詳細な説明はこちら で行っています。権限を振って実行してみてください。 chmod +x ./infra/scripts/deploy.sh ./infra/scripts/deploy.sh # リザルトで以下が出たら成功!! #📋 設定内容サマリー: # - Azure Static Web App: swa-api-deploy-bicep # - アプリURL: https://blue-bay-01b32fb00.2.azurestaticapps.net # - GitHub環境: production # - 設定された変数: # • app_location: deploy/frontend # • api_location: api # • output_location: . # - 設定されたシークレット: # • deploy_token: [設定済み] 1分ぐらいで実行されると思います。リザルトが表示されたら、「アプリURL」にアクセスしてみてください。Welcomeページが表示されれば成功です。 環境作成 無事デプロイが確認出来たら、次は認証プロバイダーの設定を行いましょう。 GitHub > Settings > Developer Settingsにアクセス してください。 リポジトリのSettingsではなくUser Settingsのほうにアクセスしてね! OAuth Appsの作成を行いましょう。 設定項目としては、 設定項目 値 Application name 自由に決めてください Homepage URL アプリURL Authorization callback URL アプリURL/.auth/login/github/callback Enable Device Flow True 設定をするとClient IDとClient Secretが表示されます。そちらの値を .env ファイルに入れて再度 deploy.sh を起動させましょう。 ./infra/scripts/deploy.sh # リザルトで以下が出たら成功!! #📋 設定内容サマリー: # - Azure Static Web App: swa-api-deploy-bicep # - アプリURL: https://blue-bay-01b32fb00.2.azurestaticapps.net # - GitHub環境: production # - 設定された変数: # • app_location: deploy/frontend # • api_location: api # • output_location: . # - 設定されたシークレット: # • deploy_token: [設定済み] これで、SWAの環境変数に GITHUB_CLIENT_ID / GITHUB_CLIENT_SECRET が設定されました。もし不安な方はAzure Portalから確認しましょう。 CI/CD構築(GitHub Actions) ここでは、GitHub Actionsの設定を行っています。前提条件として、GitHubリポジトリのEnviroment(production)に値が設定済みである必要があります。 Name 説明 AZURE_STATIC_WEB_APPS_API_TOKEN SWAのデプロイトークン API_LOCATION APIのディレクトリ APP_LOCATION アプリのディレクトリ OUTPUT_LOCATION ビルドファイルの位置 今回作成するファイルのディレクトリ構成は以下になります。 ./.github └── workflows └── deploy.yaml ワークフロー設計・アーキテクチャ ワークフローでは、フロントビルドとデプロイを分離しています。フロントのビルドと並行してAPIのテストを実行するように設計しています。 build-frontend : フロントエンドの並列ビルド test-api : APIの並列ビルド deploy-swa : アーティファクトを使用したデプロイ 視覚化した図を以下に示します。 全体ワークフロー こちらのファイルをコピー&ペーストしてGitHub >Actionsのタブから手動実行しましょう。設定が完璧であれば、無事デプロイされるかと思います。 name: Build and Deploy to Azure Static Web Apps on: workflow_dispatch: jobs: # フロントエンドビルドジョブ build-frontend: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Setup Node.js for Frontend uses: actions/setup-node@v4 with: node-version: "18" cache: "npm" cache-dependency-path: "frontend/package-lock.json" - name: Install Frontend Dependencies working-directory: ./frontend run: npm ci - name: Build Frontend working-directory: ./frontend run: npm run build - name: Upload Frontend Build Artifacts uses: actions/upload-artifact@v4 with: name: frontend-build path: | frontend/out/ retention-days: 1 # APIビルドジョブ test-api: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Setup Node.js for API uses: actions/setup-node@v4 with: node-version: "18" cache: "npm" cache-dependency-path: "api/package-lock.json" - name: Install API Dependencies working-directory: ./api run: npm install - name: Build API working-directory: ./api run: npm run build - name: Run API Tests working-directory: ./api run: npm run test # Azure Static Web Appsデプロイジョブ deploy-swa: needs: [build-frontend, test-api] runs-on: ubuntu-latest environment: production env: APP_LOCATION: ${{ vars.APP_LOCATION || 'deploy/frontend' }} OUTPUT_LOCATION: ${{ vars.OUTPUT_LOCATION || '.' }} API_LOCATION: ${{ vars.API_LOCATION || 'api' }} steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Download Frontend Artifacts uses: actions/download-artifact@v4 with: name: frontend-build path: ./deploy/frontend - name: Copy Static Web App Configuration run: | cp frontend/config/prod/staticwebapp.config.json deploy/frontend/ - name: check working-directory: ./deploy/frontend/ run: | ls -la cat staticwebapp.config.json - name: Deploy to Azure Static Web Apps uses: Azure/static-web-apps-deploy@v1 with: azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }} action: "upload" app_location: ${{ env.APP_LOCATION }} api_location: ${{ env.API_LOCATION }} output_location: "${{ env.OUTPUT_LOCATION }}" skip_app_build: true skip_api_build: false フロントエンドビルドジョブ こちらでは、フロントエンドのビルドを行い結果をArtifactとして保存しています。 actions/setup-node@v4 を利用してnode_modulesをキャッシュしています。 build-frontend: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Setup Node.js for Frontend uses: actions/setup-node@v4 with: node-version: "18" cache: "npm" cache-dependency-path: "frontend/package-lock.json" - name: Install Frontend Dependencies working-directory: ./frontend run: npm ci - name: Build Frontend working-directory: ./frontend run: npm run build - name: Upload Frontend Build Artifacts uses: actions/upload-artifact@v4 with: name: frontend-build path: | frontend/out/ retention-days: 1 APIテストジョブ こちらでは、バックエンドのビルドを行いテストを実行しています。今回はテストが失敗したら通知を行う等の対応を行っていませんが、本格運用ではここにテスト関連の処理をまとめて切り出すことができます。 actions/setup-node@v4 を利用してnode_modulesをキャッシュしています。 # APIビルドジョブ test-api: runs-on: ubuntu-latest steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Setup Node.js for API uses: actions/setup-node@v4 with: node-version: "18" cache: "npm" cache-dependency-path: "api/package-lock.json" - name: Install API Dependencies working-directory: ./api run: npm install - name: Build API working-directory: ./api run: npm run build - name: Run API Tests working-directory: ./api run: npm run test SWAデプロイジョブ実装 こちらでは、以下の手順を順次実行しています。 「フロントエンドビルドジョブ」で作成した静的ファイルを復元 内部に本番環境用ルート定義を埋め込み Azure/static-web-apps-deploy@v1 を使用してデプロイ フロントエンドはビルド済みファイルをデプロイのみ任せて実行 バックエンドはビルドとデプロイを任せて実行 # Azure Static Web Appsデプロイジョブ deploy-swa: needs: [build-frontend, test-api] runs-on: ubuntu-latest environment: production env: APP_LOCATION: ${{ vars.APP_LOCATION || 'deploy/frontend' }} OUTPUT_LOCATION: ${{ vars.OUTPUT_LOCATION || '.' }} API_LOCATION: ${{ vars.API_LOCATION || 'api' }} steps: - name: Checkout Repository uses: actions/checkout@v4 - name: Download Frontend Artifacts uses: actions/download-artifact@v4 with: name: frontend-build path: ./deploy/frontend - name: Copy Static Web App Configuration run: | cp frontend/config/prod/staticwebapp.config.json deploy/frontend/ - name: check working-directory: ./deploy/frontend/ run: | ls -la cat staticwebapp.config.json - name: Deploy to Azure Static Web Apps uses: Azure/static-web-apps-deploy@v1 with: azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }} action: "upload" app_location: ${{ env.APP_LOCATION }} api_location: ${{ env.API_LOCATION }} output_location: "${{ env.OUTPUT_LOCATION }}" skip_app_build: true skip_api_build: false Copy Static Web App Configuration では、本番環境用の staticwebapp.config.json を復元したフロントのビルド済みディレクトリにコピーしています。 Azure/static-web-apps-deploy@v1 では、 skip_app_build と skip_api_build でアクション内のビルドプロセスを制御しています。 そもそもこのアクションは、アプリの構成を自動で読み取りビルドとデプロイを行っています。フロントエンドでルーティングファイルの制御を行うためにフロントエンドはビルドをスキップしてデプロイだけで使用しています。 GitHub Actions効率化紹介 今回のワークフローでは、ビルド時間の短縮とリソース効率化を重視した設計を行いました。特に並列処理とキャッシュ戦略により、従来の逐次処理と比較して大幅な時間短縮を実現できます。 独立したNodeキャッシュ戦略 各ジョブで異なる cache-dependency-path を指定することで、フロントエンドとAPIで独立したキャッシュを管理できます。これにより、一方の依存関係が変更されても他方のキャッシュが無効化されることを防げます。 # フロントエンドジョブでのキャッシュ設定 - name: Setup Node.js for Frontend uses: actions/setup-node@v4 with: node-version: "18" cache: "npm" cache-dependency-path: "frontend/package-lock.json" # APIジョブでのキャッシュ設定 - name: Setup Node.js for API uses: actions/setup-node@v4 with: node-version: "18" cache: "npm" cache-dependency-path: "api/package-lock.json" cache-dependency-path を明示的に指定することで、GitHub Actionsはそのファイルのハッシュをキャッシュキーとして使用します。フロントエンドとAPIで異なるpackage-lock.jsonを参照することで、以下のメリットがあります: フロントエンドの依存関係のみ変更時:APIのキャッシュはそのまま利用 APIの依存関係のみ変更時:フロントエンドのキャッシュはそのまま利用 両方変更時:それぞれ個別にキャッシュを再構築 フロントエンドジョブでは npm ci を使用しています。これはCI環境向けの最適化されたコマンドで、package-lock.jsonを厳密に遵守し、高速インストールを実現します。 アーティファクト活用による並列処理最適化 アーティファクトを使用することで、ビルドジョブとデプロイジョブを完全に分離できます。これにより、デプロイ時にビルドを再実行する必要がなくなり、処理時間を大幅に短縮できます。 # ビルドジョブでのアーティファクト保存 - name: Upload Frontend Build Artifacts uses: actions/upload-artifact@v4 with: name: frontend-build path: | frontend/out/ retention-days: 1 # デプロイジョブでのアーティファクト復元 - name: Download Frontend Artifacts uses: actions/download-artifact@v4 with: name: frontend-build path: ./deploy/frontend 今回の構成では、 build-frontend と test-api が並列で実行されます。これにより、従来の逐次処理と比較して大幅な時間短縮が期待できます。 将来的なテストへの拡張性 段階的テスト導入への対応 現在のAPIテストジョブは、将来的なテスト拡張を見据えた設計になっています。 - name: Build API working-directory: ./api run: npm run build - name: Run API Tests working-directory: ./api run: npm run test テスト失敗時の処理拡張 本格運用時には、テスト失敗時の通知機能を追加できます。以下のような拡張が可能です: - name: Run API Tests working-directory: ./api run: npm run test continue-on-error: false # テスト失敗時にワークフローを停止 # 将来的な拡張例 - name: Notify Test Failure if: failure() uses: actions/github-script@v6 with: script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: 'APIテストが失敗しました。詳細はワークフローログを確認してください。' }) 並列テスト実行の利点 フロントエンドビルドとAPIテストを並列実行することで、以下のメリットがあります: 時間効率 : 最も時間のかかる処理に全体時間が依存 早期発見 : APIテストの失敗を早期に検出 リソース効率 : GitHub Actionsの同時実行枠を有効活用 シークレット・環境変数の効率的管理 GitHub Actionsでは、Secretsと環境変数を適切に分離して管理することが重要です。 environment: production env: APP_LOCATION: ${{ vars.APP_LOCATION || 'deploy/frontend' }} OUTPUT_LOCATION: ${{ vars.OUTPUT_LOCATION || '.' }} API_LOCATION: ${{ vars.API_LOCATION || 'api' }} Secrets(機密情報)には AZURE_STATIC_WEB_APPS_API_TOKEN などのデプロイトークンを、Variables(環境固有設定)には APP_LOCATION などのディレクトリ設定を使い分けます。 本番環境での動作確認 お疲れさまでした!ここまでで無事にAzure SWAのデプロイが完了しましたね。でも、ここからが本当の勝負です。実際に本番環境で全ての機能が期待通りに動作するかを確認していきましょう。 私も最初の頃は「ローカルで動いてるから大丈夫だろう」と思っていましたが、本番環境では思わぬ設定の違いでハマることがよくありました。特に認証周りは環境による差が出やすい部分なので、しっかりと検証していきますね。 もし何かうまく動作しない部分があった場合は、まずはブラウザの開発者ツールでエラーがないか確認して、必要に応じてAzure Portalでのログ確認も行ってみてください。認証周りは環境設定に依存する部分が多いので、OAuth Appの設定やSWAの環境変数も再度チェックしてみると良いでしょう。 未認証状態での動作確認 シークレットモードやプライベートブラウジングで本番URLにアクセスしてみてください。まずはトップページ( / )から確認します。 期待される動作: ページ上部に「未認証状態」と表示される 「GitHubでログイン」ボタンが表示される 「サーバーからユーザー情報を取得」ボタンをクリックしても、ユーザー情報は取得できない( isAuthenticated: false が返される) 次に、保護されたページへの直接アクセスを試してみます。URLバーに /protected を入力してアクセスしてください。 期待される動作: 自動的にGitHub認証画面にリダイレクトされる エラー画面が表示されることはない これがAzure SWAの素晴らしいところですね。設定ファイル( staticwebapp.config.json )で定義したルート保護が自動的に働いて、未認証ユーザーを認証フローに誘導してくれます。 GitHub認証の実行 「GitHubでログイン」ボタンをクリックして、実際の認証フローを確認します。 認証フローの流れ: ボタンクリックで /.auth/login/github にリダイレクト GitHub認証画面が表示される GitHubでの認証完了後、元のページにリダイレクト GitHub認証画面では、作成したOAuth Appの情報が表示されます。App nameやAuthorization callback URLが正しく設定されていることを確認してください。 認証完了後は、トップページに戻ってきて以下のような変化が確認できるはずです: ページ上部に「認証済み」と表示される ユーザー情報(GitHubのユーザー名など)が表示される 「保護されたページへ」「ログアウト」ボタンが表示される セッション管理の確認 認証状態でページをリロードしても、認証状態が維持されることを確認してください。これはAzure SWAが内部的にセッション管理を行っているためです。 また、別タブで同じサイトを開いても、認証状態が共有されることも確認してみましょう。 API動作テスト 認証が正しく動作することが確認できたら、次はAPIの動作を確認していきます。 user-infoエンドポイントのテスト 認証済み状態で「サーバーからユーザー情報を取得」ボタンをクリックしてください。 期待される結果: { "userId": "github|あなたのGitHubユーザーID", "name": "あなたのGitHubユーザー名", "email": "あなたのGitHubユーザー名", "provider": "github", "roles": ["authenticated"], "isAuthenticated": true } これで、Azure SWAからAzure Functionsに認証ヘッダー( x-ms-client-principal )が正しく送信されて、API側で適切にデコードされていることが確認できます。 ブラウザの開発者ツールのネットワークタブでも確認してみてください。 /api/user-info へのリクエストが200で成功していて、上記のようなレスポンスが返っていることが確認できるはずです。 未認証状態でのuser-infoテスト シークレットモードで未認証状態でも同じボタンをクリックしてみてください。 期待される結果: { "userId": null, "name": null, "email": null, "provider": null, "roles": [], "isAuthenticated": false } このAPIは認証状態を問わずアクセス可能な設計になっているので、未認証でもエラーにならずに適切なレスポンスが返ることを確認できます。 protected-dataエンドポイントのテスト 保護されたページ( /protected )にアクセスして、「保護されたデータを取得」ボタンをクリックしてください。 期待される結果: { "userId": "github|あなたのGitHubユーザーID", "message": "こんにちは、ユーザーgithub|あなたのGitHubユーザーIDさん!", "timestamp": "2024-12-15T15:45:30.000Z", "userNumber": 数値 } userNumber は、ユーザーIDをベースにした簡単なシード値から生成される0-999の範囲の数値です。同じユーザーなら常に同じ値が返されることも確認してみてください。 未認証でのprotected-dataアクセステスト これは少し技術的なテストですが、直接APIエンドポイントにアクセスしてみましょう。 シークレットモードで https://あなたのサイトURL/api/protected-data に直接アクセスしてください。 期待される動作: 401エラーが返される または、Azure SWAのルート保護により認証画面にリダイレクトされる 実際には、SWAの設定によってはAPI直接アクセスも認証フローに誘導される可能性があります。どちらの動作でも、未認証ユーザーがデータにアクセスできないことが重要です。 セキュリティチェック 最後に、セキュリティ面での動作を確認していきます。 ルート保護の確認 未認証状態で以下のURLに直接アクセスしてみてください: /protected /protected/任意のパス どちらも認証画面にリダイレクトされることを確認してください。これは staticwebapp.config.json で設定した以下のルールが動作しているためです: { "route": "/protected/*", "allowedRoles": ["authenticated"] } 認証情報の適切な伝達 開発者ツールのネットワークタブで、認証済み状態でのAPI呼び出しを確認してください。 重要なポイント: API呼び出し時に、フロントエンド側で特別な認証ヘッダーを設定していない Azure SWAが自動的に x-ms-client-principal ヘッダーを付与している このヘッダーは開発者ツールでは見えないが、サーバー側では受信できている これがAzure SWAの大きなメリットの一つですね。フロントエンド側で複雑な認証処理を書く必要がなく、SWAが自動的に認証情報をバックエンドに伝達してくれます。 セッション継続性の確認 認証済み状態で以下を試してみてください: ページのリロード 別タブでの同サイトアクセス ブラウザを閉じて再度開く(セッションストレージのテスト) 通常のWebアプリケーションと同様に、ブラウザを閉じるまでは認証状態が維持されることを確認してください。 ログアウト機能の確認 「ログアウト」ボタンをクリックして、正常にログアウトできることを確認してください。 期待される動作: /.auth/logout にリダイレクト 認証状態がクリアされる トップページに戻り、未認証状態の表示になる お疲れさまでした!これで本番環境での動作確認は完了です。すべての機能が期待通りに動作していれば、Azure SWA + Next.js + Azure Functionsの認証統合システムが正常に稼働していることが確認できました。 リソースのクリーナップ 今回はAzure SWAをStandardプランで使用しています。こちらは動かしていれば、課金が走るようになっています。検証が確認できたら、心苦しいですがクリーナップしましょう。 Azure Portalからの削除でもよいのですが、せっかくなのでクリーナップ用のbashスクリプトを作成しました。bashスクリプト化しておくことのメリットはコマンド一つで環境の作成・削除ができる点です。GitHubのOAuth Appsの設定とEnvironmentの作成は手動ですが、完全自動化ではないですけどね… クリーナップスクリプト #!/bin/bash # エラー時に停止 set -e log_info() { echo -e "\033[1;34m$1\033[0m"; } log_success() { echo -e "\033[1;32m$1\033[0m"; } log_warning() { echo -e "\033[1;33m$1\033[0m"; } log_error() { echo -e "\033[1;31m$1\033[0m"; } CONFIG_FILE="$(git rev-parse --show-toplevel)/.env" 2>/dev/null || CONFIG_FILE=".env" if [ -f "$CONFIG_FILE" ]; then log_info "📁 設定ファイル読み込み: $CONFIG_FILE" source "$CONFIG_FILE" else log_error "⚠ 設定ファイルが見つかりません: $CONFIG_FILE" log_error " .env.example をコピーして設定してください" exit 1 fi # 必要な環境変数の確認 required_vars=("RESOURCE_GROUP_NAME" "STATIC_WEB_APP_NAME") for var in "${required_vars[@]}"; do if [ -z "${!var}" ]; then log_error "エラー: 環境変数 $var が設定されていません" exit 1 fi done log_info "🎯 削除対象: $STATIC_WEB_APP_NAME (リソースグループ: $RESOURCE_GROUP_NAME)" # Azure CLIがインストールされているかチェック if ! command -v az &> /dev/null; then log_error "Azure CLI (az) がインストールされていません" log_error "インストール方法: https://docs.microsoft.com/cli/azure/install-azure-cli" exit 1 fi # Azure CLIの認証チェック if ! az account show &> /dev/null; then log_error "Azure CLIが認証されていません" log_error "認証方法: az login" exit 1 fi # リソースグループの存在確認 log_info "" log_info "📦 リソースグループの確認..." if ! az group show --name $RESOURCE_GROUP_NAME &> /dev/null; then log_warning "⚠ リソースグループ '$RESOURCE_GROUP_NAME' が見つかりません" log_warning " 削除する必要がありません" exit 0 else log_success "✅ リソースグループ確認: $RESOURCE_GROUP_NAME" fi # Static Web Appリソースの存在確認 log_info "" log_info "🔍 Azure Static Web App リソースを確認中..." # 指定されたリソースの存在確認 if az staticwebapp show \ --name "$STATIC_WEB_APP_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --output none 2>/dev/null; then # リソース詳細の取得 SWA_DETAILS=$(az staticwebapp show \ --name "$STATIC_WEB_APP_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --query "{name:name, defaultHostname:defaultHostname, sku:sku.name, location:location}" \ -o json) log_success "✅ 削除対象リソースが見つかりました:" echo "$SWA_DETAILS" | jq -r '" • 名前: \(.name)"' echo "$SWA_DETAILS" | jq -r '" • URL: \(.defaultHostname)"' echo "$SWA_DETAILS" | jq -r '" • プラン: \(.sku)"' echo "$SWA_DETAILS" | jq -r '" • リージョン: \(.location)"' RESOURCE_EXISTS=true else log_warning "⚠ 指定されたAzure Static Web App '$STATIC_WEB_APP_NAME' が見つかりません" log_warning " リソースグループ: $RESOURCE_GROUP_NAME" log_info "削除する必要がありません" exit 0 fi # 確認プロンプト log_warning "" log_warning "⚠ 以下の操作を実行します:" log_warning " 1. Azure Static Web App '$STATIC_WEB_APP_NAME' の削除" log_warning " 2. リソースグループ '$RESOURCE_GROUP_NAME' は保持します" log_warning " 3. GitHub環境設定は保持します" log_warning "" read -p "続行しますか? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then log_info "操作をキャンセルしました" exit 0 fi # === Azure Static Web App リソースの削除 === log_info "" log_info "🗑 Azure Static Web App リソースを削除中..." log_info "🔧 削除中: $STATIC_WEB_APP_NAME" if az staticwebapp delete \ --name "$STATIC_WEB_APP_NAME" \ --resource-group "$RESOURCE_GROUP_NAME" \ --yes 2>/dev/null; then log_success "✅ 削除完了: $STATIC_WEB_APP_NAME" else log_error "❌ 削除失敗: $STATIC_WEB_APP_NAME" exit 1 fi log_success "✅ Azure Static Web App リソースの削除が完了しました" # === 削除完了サマリー === log_success "" log_success "🎉 クリーンアップが完了しました!" log_success "" log_success "📋 実行内容サマリー:" log_success " ✅ Azure Static Web App削除: $STATIC_WEB_APP_NAME" log_success " ✅ リソースグループ保持: $RESOURCE_GROUP_NAME" log_success " ✅ GitHub環境設定保持: 変更なし" log_success "" log_success "📌 注意事項:" log_success " • リソースグループ '$RESOURCE_GROUP_NAME' は保持されています" log_success " • GitHub環境設定はそのまま残っているので、再デプロイ可能です" log_success "" log_success "🚀 Azure リソースのクリーンアップ完了!" クリーナップスクリプトの実行 作成時と同じく、環境変数を .env ファイルから読み込んで削除処理を実行します。既にdeployスクリプト実行時に .env ファイルは設定済みのはずなので、そのまま使用できます。 chmod +x ./infra/scripts/cleanup.sh ./infra/scripts/cleanup.sh 実行すると、以下のような確認が表示されます: 🎯 削除対象: swa-api-deploy-bicep (リソースグループ: swa-bicep-test) ⚠ 以下の操作を実行します: 1. Azure Static Web App 'swa-api-deploy-bicep' の削除 2. リソースグループ 'swa-bicep-test' は保持します 3. GitHub環境設定は保持します 続行しますか? (y/N): y を入力すると削除処理が開始されます。 削除される内容と保持される内容 削除されるもの: Azure Static Web Appリソース 関連するManaged Functions カスタム認証プロバイダー設定 保持されるもの: リソースグループ(他のリソースがある可能性を考慮) GitHub Environment設定(再デプロイ時に再利用可能) GitHub OAuth App(再利用可能) 完全削除を希望する場合 もし完全にクリーンな状態に戻したい場合は、以下も手動で削除してください: Azure側: リソースグループ全体(他にリソースがない場合) 必要に応じて Azure CLI の認証情報もクリア GitHub側: OAuth App(Settings > Developer settings > OAuth Apps から削除) Environment設定(リポジトリの Settings > Environments から削除) これで、課金を気にすることなく安心して検証を終了できますね。再度環境を作りたい場合は、deployスクリプトを実行すれば簡単に復旧できるのも、Infrastructure as Codeのメリットです! まとめ 今回は Azure Static Web Apps と Azure Functions を使った Next.js アプリケーションの認証 API 統合について、DevContainer での開発環境構築から本番デプロイまでの完全なワークフローを詳しく見てきました。 技術統合のポイント この構成の最大の魅力は、 フロントエンドとバックエンドを統一プラットフォームで管理できる点 にあります。Azure SWA が提供する認証機能と Azure Functions の API を組み合わせることで、複雑な認証フローを比較的シンプルに実装できました。特に x-ms-client-principal ヘッダーを使った認証情報の受け渡しは、SWA 独自の仕組みとして理解しておくと実装がスムーズになります。 開発体験の向上 DevContainer を使った開発環境は、チーム開発での環境差異を解消する大きなメリットがありました。Azure CLI や SWA CLI、Functions Core Tools がすべて統一環境で利用でき、ローカルでの統合テストも swa start コマンド一つで実行できる点は、開発効率を大幅に向上させます。 運用面での実用性 GitHub Actions を使った CI/CD パイプラインでは、 フロントエンドビルドと API テストを並列実行 することで、デプロイ時間の短縮を実現しました。また、Bicep を使った IaC により、インフラの再現性と管理性も確保できています。Standard プランは課金が発生しますが、カスタム認証プロバイダーや SLA が必要な本格的なプロジェクトには必須の選択肢です。 コメント ここまで出来たら、Azure Static Web Appsの基本は一通り理解できたかともいます。あとは運用の要望に合わせてチューニングするエンタープライズの領域に入っていきます。Bicepを使用して人間の手が入る可能性を減らせば減らすほど、問題が少なくなると思うので積極的にスクリプト化できるものはしていきましょう! 長々とした記事ですがお疲れ様でした!! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post Azure SWA×Next.js認証API統合を実践解説【DevContainer〜本番まで】 first appeared on SIOS Tech. Lab .
はじめに こんにちはPSの佐々木です GitHub Copilot、ChatGPT、Claude等の生成AIツールは、現代のソフトウェア開発において不可欠な存在となっています。しかし、これらの生成AIが提案するコードには、意図せずライセンス違反を引き起こす可能性が潜んでいます。本記事では、生成AI開発におけるライセンス違反リスクと、SCANOSSを活用した効果的な対策について解説します。 生成AI開発におけるライセンス違反リスク 生成AIが学習するオープンソースコード 生成AIは膨大なオープンソースコードから学習しており、その中には様々なライセンスが適用されたコードが含まれています。AI が学習データから再現するコードには、以下のようなリスクが存在します: 意図しないライセンス違反 生成AIが提案したコードが、GPLやAGPLライセンスの既存コードと酷似している可能性 開発者が気づかないうちに、商用製品にコピーレフトライセンスのコードを組み込んでしまうリスク 従来のライブラリ依存関係では発見できない、コードスニペットレベルでの違反 企業が直面する課題 ライセンス違反リスクを恐れて、生成AIツールの導入を躊躇する企業が増加 開発効率向上のメリットと、法的リスクの間でのジレンマ 既存のライセンス検査ツールでは検出できない新しいタイプのリスク 具体的なリスクシナリオ ケース1: コードスニペットの意図しない複製 # 生成AIが提案したコードの例 def quicksort(arr): if len(arr) <= 1: return arr pivot = arr[len(arr) // 2] left = [x for x in arr if x < pivot] middle = [x for x in arr if x == pivot] right = [x for x in arr if x > pivot] return quicksort(left) + middle + quicksort(right) このようなコードが、実際にはGPLライセンスの既存プロジェクトから学習されている可能性があります。 ケース2: アルゴリズムの実装パターン 特定のアルゴリズムの実装において、生成AIが学習したコピーレフトライセンスのコードと同じパターンを再現してしまうケース。 主要なOSSライセンスと違反リスク パーミッシブライセンス(低リスク) MIT License 商用利用可能、制限が最小限 著作権表示のみ必要 生成AIで再現されても比較的リスクは低い Apache License 2.0 特許権の明確な規定あり 変更箇所の明記が必要 企業利用において安全性が高い BSD License シンプルで制限が少ない 商用利用に適している コピーレフトライセンス(高リスク) GPL (GNU General Public License) 最も注意が必要なライセンス 派生物もGPLライセンスでの配布が必要 商用製品での利用時、全ソースコードの開示義務 生成AIが無意識に組み込むと重大なリスク AGPL (GNU Affero General Public License) GPLよりもさらに厳しい条件 SaaS提供時でもソースコード開示が必要 クラウドサービス開発で特に危険 LGPL (GNU Lesser General Public License) ライブラリ自体の変更時のみソースコード開示 動的リンクの場合は比較的安全 弱いコピーレフトライセンス(中リスク) MPL (Mozilla Public License) ファイルレベルでのコピーレフト 変更したファイルのみソースコード開示 他のライセンスコードとの組み合わせは可能 SCANOSSによる生成AIコードの検知と対策 SCANOSSの独自技術 コードスニペットレベルでの検出 SCANOSSは、従来のライセンス検査ツールでは困難だった、わずか数行のコードスニペットからでも元のOSSコンポーネントとライセンスを特定できます: フィンガープリンティング技術 : コードの構造的特徴を分析し、部分的なコードからも元のライセンスを特定 機械学習による高精度マッチング : 大量のオープンソースコードデータベースから学習したパターンマッチング リアルタイム分析 : 開発中にリアルタイムでライセンス確認が可能 生成AIコードの包括的スキャン 開発ワークフロー統合 # 生成AIで作成されたコードの包括的スキャン scanoss scan --ai-generated-code --include-snippets /path/to/project # 特定のライセンスポリシーを適用したスキャン scanoss scan /path/to/project --policy-file corporate-policy.json --fail-on-license GPL-3.0 # CI/CDパイプラインでの自動スキャン scanoss scan . --output-format spdx --include-ai-analysis 検出できる違反パターン 生成AIによる既存コードの部分的な複製 Stack Overflowなどからコピーしたコードスニペット 依存関係に含まれないコードレベルでのライセンス違反 バイナリファイルに埋め込まれたコードの検出 実践的な生成AI開発でのライセンス管理 開発プロセスへの組み込み 1. コード生成時の即座チェック # 開発者のワークフロー例 # 1. 生成AIでコードを生成 # 2. SCANOSSで即座にライセンスチェック # 3. 問題があれば代替案を検討 # 4. 安全なコードのみを採用 2. CI/CDパイプライン統合 プルリクエスト時の自動スキャン 本番デプロイ前の包括的チェック ライセンス違反の検出時の自動アラート 3. 継続的な監視 新しい依存関係の追加時の自動チェック 既存コードの定期的な再スキャン ライセンスポリシーの更新時の全体チェック 企業でのライセンスポリシー策定 リスクレベル別の対応 高リスク(GPL、AGPL) : 原則使用禁止、例外時は法務承認必須 中リスク(LGPL、MPL) : 使用条件の明確化、適切な分離 低リスク(MIT、Apache、BSD) : 適切な著作権表示で使用可能 生成AIコードの承認プロセス 開発者による初期スキャン 自動化ツールによる検証 法務部門による最終承認(高リスクライセンス検出時) まとめ 生成AI時代のソフトウェア開発において、ライセンス管理は新たな複雑さを増しています。従来のライブラリレベルでの検出では捉えきれない、コードスニペットレベルでの違反リスクが現実のものとなっています。 SCANOSSのような専門的なツールを活用することで、以下のメリットを実現できます: 技術的メリット コードスニペットレベルでの高精度検出 生成AIコードの包括的な分析 開発ワークフローへのシームレスな統合 ビジネスメリット 法的リスクの大幅な軽減 生成AI導入による開発効率向上の安全な実現 コンプライアンス要件の確実な遵守 今後、生成AIがさらに普及する中で、適切なライセンス管理体制の構築は、企業の競争優位性を維持するための必須要件となるでしょう。早期にこれらの仕組みを整備し、安全に生成AIのメリットを享受できる環境を構築することが重要です。 サイオステクノロジーではSCAN OSSをサポートしています。 https://sios.jp/products/oss/scanoss/ ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post 生成AI時代のライセンス管理:SCANOSSによる違反リスクの検知と対策 first appeared on SIOS Tech. Lab .
はじめに こんにちは!重要な機能開発を任されて最近まで業務で手一杯だったなーがです。Windowsで開発環境を構築する際にGitやSSHなど、さまざまなツールの設定に時間を取られていませんか?PCを買い替えたり、突然PCが動かなくなったりしたときに新しい環境をセットアップするたびに同じ設定を繰り返すのは大変ですよね。 今回はそうした設定ファイルをGithubリポジトリとして管理することで、いつでも同じ開発環境を再現できる「 dotfiles 」という仕組みと、Windows環境に特化した具体的な構成をご紹介します。私も業務で使用しているPCのリプレース作業が近づいていることもあり、せっかくなので書いてみることにしました。スクリプトファイルは最後に付録として載せておきます! 📁 dotfilesリポジトリの紹介 今回ご紹介するのは、Windows環境向けに作成されたdotfilesリポジトリです。このリポジトリは、設定ファイルを一元管理し、簡単なコマンド一つで環境のセットアップを完了させることを目的としています。GitHubで「dotfiles」と検索してみると、多くの人が自身の設定ファイルを公開しています。 https://github.com/search?q=dotfiles&type=repositories リポジトリは以下のような構成になっています。ここでは比較的一般的な設定ファイルに絞って紹介します。 dotfiles/ ├── README.md # このファイル ├── setup.ps1 # セットアップスクリプト ├── ssh/ # SSH設定 │ └── config ├── VSCode/ # Visual Studio Code設定 │ ├── settings.json │ └── keybindings.json └── WindowsTerminal/ # Windows Terminal設定 └── settings.json VS CodeやWindows Terminalといった主要な開発ツールの設定ファイルが、すべてこのリポジトリで管理されているのがわかりますね。 ここでキモとなっているのが setup.ps1 で、Ubuntu等のLinux環境の方は XXXX.sh を用意されていると思います。私はメイン業務がWindowsなので、今回はPowerShellのスクリプトファイルしか記載していません。このスクリプトはこのリポジトリ内で管理している各設定ファイルと各設定ファイルが本来配置される箇所とのシンボリックリンクを作成する役割があります。シンボリックリンクを張ることで、各設定ファイルをわざわざ配置することなく参照でき、各設定ファイルに行った変更は全てこのリポジトリ内で管理できるというメリットがあります。 このスクリプトファイルですが、一昔前であればPowerShellやシェルスクリプトの知識が必要で少しハードルが高かったですが、現在はClaudeやGitHub CopilotをはじめとしたViveコーディングが可能なため「この設定ファイルのシンボリックリンクを作成するスクリプトを作成して」とお願いして、少し手直しするだけで簡単に実現できます。いい時代になりましたね…かく言うこのスクリプトも大枠はAIに作成してもらいました! 🚀 セットアップ方法 セットアップは非常にシンプルです。PowerShellスクリプトを実行するだけで、必要な設定ファイルへのシンボリックリンクが自動的に作成されます。 1. シンボリックリンクの作成 まず、管理者権限でPowerShellを開き、リポジトリ内にある setup.ps1 スクリプトを実行します。 # PowerShellを管理者として実行 .\\setup.ps1 このスクリプトを実行すると、以下の場所に設定ファイルのシンボリックリンクが作成され、リポジトリでの変更が即座に反映されるようになります。 VS Code設定 : %APPDATA%\\Code\\User\\ Windows Terminal設定 : %LOCALAPPDATA%\\Packages\\Microsoft.WindowsTerminal_8wekyb3d8bbwe\\LocalState\\ SSH設定 : %USERPROFILE%\\.ssh\\ Git設定 : %USERPROFILE%\\.gitconfig その他のshell設定 : .bashrc , .bash_profile , .vimrc など ⚙️ オプション セットアップスクリプトには、便利なオプションも用意されています。 強制上書きモード すでに設定ファイルが存在する場合でも強制的に上書きしてシンボリックリンクを作成します。 .\\setup.ps1 -Force カスタムパス指定 dotfilesを異なる場所にクローンした場合でも、パスを指定して実行できます。 .\\setup.ps1 -DotfilesPath "C:\\path\\to\\your\\dotfiles" さいごに 今回は、Windows環境における設定ファイルの管理と自動化を行うdotfilesリポジトリを紹介しました。このような仕組みを活用することで、環境構築の手間を大幅に削減し、より本質的な開発作業に集中できます。 ぜひ、このリポジトリを参考に、ご自身の最強の開発環境を構築してみてはいかがでしょうか。 付録 <# .SYNOPSIS dotfilesフォルダ内のファイルのシンボリックリンクを作成するスクリプト .DESCRIPTION dotfilesフォルダ内にある設定ファイルを、適切な場所にシンボリックリンクとして作成します。 管理者権限が必要です。 .PARAMETER DotfilesPath dotfilesフォルダのパス(デフォルト: .) .PARAMETER Force 既存のファイルを上書きするかどうか .EXAMPLE .\Create-DotfilesSymlinks.ps1 .EXAMPLE .\Create-DotfilesSymlinks.ps1 -DotfilesPath "C:\dotfiles" -Force #> param( [Parameter(Mandatory = $false)] [string]$DotfilesPath = ".", [Parameter(Mandatory = $false)] [switch]$Force ) # 管理者権限チェック function Test-Administrator { $currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent()) return $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } # シンボリックリンク作成関数 function New-SymbolicLink { param( [string]$LinkPath, [string]$TargetPath ) try { # リンク先のディレクトリが存在しない場合は作成 $linkDir = Split-Path -Parent $LinkPath if (-not (Test-Path $linkDir)) { New-Item -ItemType Directory -Path $linkDir -Force | Out-Null Write-Host "ディレクトリを作成しました: $linkDir" -ForegroundColor Green } # 既存のファイル/リンクをチェック if (Test-Path $LinkPath) { # 既存のパスがシンボリックリンクかどうか確認 $item = Get-Item -Path $LinkPath -Force $isSymLink = $item.Attributes -band [System.IO.FileAttributes]::ReparsePoint if ($isSymLink) { # シンボリックリンクの場合はスキップ Write-Warning "シンボリックリンクが既に存在します: $LinkPath (スキップ)" return $false } else { # 既存ファイルは削除 Remove-Item $LinkPath -Force Write-Host "既存のファイルを削除しました: $LinkPath" -ForegroundColor Yellow } } # シンボリックリンクを作成 New-Item -ItemType SymbolicLink -Path $LinkPath -Target $TargetPath -Force | Out-Null Write-Host "シンボリックリンクを作成しました: $LinkPath -> $TargetPath" -ForegroundColor Green return $true } catch { Write-Error "シンボリックリンクの作成に失敗しました: $LinkPath -> $TargetPath" Write-Error $_.Exception.Message return $false } } # メイン処理 function Main { Write-Host "=== Dotfiles シンボリックリンク作成スクリプト ===" -ForegroundColor Cyan # 管理者権限チェック if (-not (Test-Administrator)) { Write-Error "このスクリプトは管理者権限で実行する必要があります。" Write-Host "PowerShellを管理者として実行してから再度お試しください。" -ForegroundColor Yellow exit 1 } # dotfilesフォルダの存在チェック if (-not (Test-Path $DotfilesPath)) { Write-Error "dotfilesフォルダが見つかりません: $DotfilesPath" exit 1 } $DotfilesPath = Resolve-Path $DotfilesPath Write-Host "dotfilesフォルダ: $DotfilesPath" -ForegroundColor Cyan # ファイルマッピング定義 # 必要に応じて追加・変更してください $fileMappings = @{ # Git設定 ".gitconfig" = "$env:USERPROFILE\.gitconfig" # VS Code設定 "VSCode\settings.json" = "$env:APPDATA\Code\User\settings.json" "VSCode\keybindings.json" = "$env:APPDATA\Code\User\keybindings.json" # Windows Terminal設定 "WindowsTerminal\settings.json" = "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json" # SSH設定 "ssh\config" = "$env:USERPROFILE\.ssh\config" # その他の設定 ".bashrc" = "$env:USERPROFILE\.bashrc" ".bash_profile" = "$env:USERPROFILE\.bash_profile" ".vimrc" = "$env:USERPROFILE\.vimrc" ".minttyrc" = "$env:USERPROFILE\.minttyrc" ".wslconfig" = "$env:USERPROFILE\.wslconfig" } $successCount = 0 $skipCount = 0 $errorCount = 0 Write-Host "`n--- シンボリックリンクの作成を開始します ---" -ForegroundColor Cyan foreach ($mapping in $fileMappings.GetEnumerator()) { $sourcePath = Join-Path $DotfilesPath $mapping.Key $targetPath = $mapping.Value # ソースファイルの存在チェック if (-not (Test-Path $sourcePath)) { Write-Warning "ソースファイルが見つかりません: $sourcePath (スキップ)" $skipCount++ continue } Write-Host "`n処理中: $($mapping.Key)" $result = New-SymbolicLink -LinkPath $targetPath -TargetPath $sourcePath -ForceOverwrite $Force if ($result) { $successCount++ } else { if ((Test-Path $targetPath) -and -not $Force) { $skipCount++ } else { $errorCount++ } } } # 結果サマリー Write-Host "`n=== 処理結果 ===" -ForegroundColor Cyan Write-Host "成功: $successCount" -ForegroundColor Green Write-Host "スキップ: $skipCount" -ForegroundColor Yellow Write-Host "エラー: $errorCount" -ForegroundColor Red if ($errorCount -eq 0) { Write-Host "`nシンボリックリンクの作成が完了しました!" -ForegroundColor Green } else { Write-Host "`n一部のシンボリックリンクの作成に失敗しました。上記のエラーメッセージを確認してください。" -ForegroundColor Yellow } } # スクリプト実行 Main ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post PCの環境構築を迅速かつ簡単に!dotfilesで設定管理を始めよう first appeared on SIOS Tech. Lab .