TECH PLAY

株式会社豆蔵

株式会社豆蔵 の技術ブログ

100

これは 豆蔵デベロッパーサイトアドベントカレンダー2025 第1日目の記事です。 はじめに # 昨年9月に Tauri 2.0 の RC を触って記事を書いていました。 /blogs/2024/09/22/try-tauri-v2-rc/ 当時も2年ぶりぐらいに Tauri を触ったのですが、この時からまた1年以上が経ってしまいました。月日が経つの早いですね。 Tauri 2.0 は昨年10月に正式リリースされ、現在のバージョンは 2.9.3 です。そろそろ熟成されてきた頃ではないかと考え、Electron から移植してみる PoC を思いついた次第です。 移植するアプリ # これまでは、ちょっとした SPA 的なアプリを動かす程度のことしかやってこなかったので、もう少し実用的なアプリで試そうと思いました。 例によって拙作の Electron 製の野良 Cosense(Scrapbox) アプリを題材にさせていただきます。 https://github.com/kondoumh/sbe sbe の操作イメージのスクリーンショットです。タブ UI が特徴で、Cosense のページをタブで開いて表示・編集できるのと、独自の管理画面やプロジェクトのページ一覧などの UI も利用可能です。 移植といっても長年メンテしているアプリなので意外とコードベースも大きく機能も多いのでピンポイントでフィーチャーを実装してみて Electron との違いを噛み締めてみるという試みです。 今回の PoC での移植結果の出来上がりのスクリーンショットです。 最初のタブで開いたページの履歴とお気に入りが表示され、次のタブでページ一覧が開きます。ここでは任意の Cosense プロジェクトのページ一覧を表示可能です。履歴やお気に入りのリンクをクリックすると別ウィンドウでページを開きます。 後述しますが、タブ内での Cosense ページ表示はできなかったので PoC では妥協して別ウィンドウ表示としました。お気に入りへの追加はコンテキストメニューから可能です。 作成したコード全体をご紹介すると膨大になってしまうため、リポジトリは記事の終わりに掲載します。記事中のコードスニペットで雰囲気を掴んでいただければと思います。 使用したソフトウェアのバージョンなど # 今回は、以下のような構成で PoC を行いました。 Rust 1.19.1 Tauri 2.9.3 Vite 6.0.3 Vue 3.5.13 sbe では Vuetify を使っていましたが、シンプルにするため、Vue と CSS だけで作成しました。最初に vanilla テンプレートを使ってプロジェクトを作成し、後から必要なものをインストールしました。 mkdir sbe-tauri-poc && cd sbe-tauri-poc npm create tauri-app@latest . --template vanilla-ts マルチビュー、タブ UI # sbe では、複数の Cosense ページや独自の画面 をタブ UI で表示しています。以下のように、Electron の WebContentsView で Scrapbox のページを表示し、複数の WebContentsView を Vue(Vuetify) で実装したタブで切り替えるようにしています。 WebContentsView は BaseWindow に埋め込まれます。複数の WebContentsView を重ねて表示やタイル表示もできますし、API により Z 軸上の順序を入れ替え可能です。レンダラープロセスの Vuetiry のタブクリックイベントをメインプロセスに通知して WebContentsView の Z order を入れ替えることでタブ切り替えを実現しています。 --> Information Electron の WebContentsView の簡単なサンプルを GitHub の mamezou-tech オーガニゼーションで公開しています。 https://github.com/mamezou-tech/electron-example-browserview WebContentsView を使用するアプリの構造については以下の記事を参照してください。 /blogs/2024/08/28/electron-webcontentsview-app-structure/ 実際には sbe は BaseWindow + WebContentsView ではなく、BrowserWindow + BrowserView で実装しています。BrowserView は現在 WebContentsView の SIM として提供されているため、実質 WebContentsView による実装となっています。 一方、Tauri の WebView は埋め込みをサポートしておらず、独立ウィンドウとして表示する方法しかありません。sbe のような UI を実装するには、単独の WebView 内に iframe を使ってサイトを表示する方法が考えられます。 Cosense サイトを iframe で表示しようとすると以下のようなエラーになります。 Refused to load https://scrapbox.io/ because it does not appear in the frame-ancestors directive of the Content Security Policy. Cosense は Content Security Policy (CSP) によって iframe 内での表示を制限しているようです。 そこで、Cosense 自体のタブ内表示は諦め、ページ毎に独立したウィンドウを WebView で起動することにしました。ただし、sbe で実装している管理画面やプロジェクトのページ一覧のような UI はタブで本体の WebView で表示することとしました。 Rust 側で WebView をウィンドウ表示するコマンドを作成しました。 src-tauri/src/lib.rs(抜粋) #[tauri::command] async fn create_webview_window(app: tauri::AppHandle, url: String, label: String) -> Result<(), String> { let webview_url = WebviewUrl::External(url.parse().map_err(|e| format!("Invalid URL: {}", e))?); let window = WebviewWindowBuilder::new(&app, &label, webview_url) .title("Scrapbox") .inner_size(1200.0, 800.0) .min_inner_size(800.0, 600.0) .center() .resizable(true) .visible(false) .build() .map_err(|e| e.to_string())?; // Show window after it's fully initialized window.show().map_err(|e| e.to_string())?; Ok(()) } これを Vue の UI から invoke で呼び出します。 App.vue(抜粋) const reopenWindow = async (window: RecentWindow) => { try { const windowId = `reopen-${Date.now()}`; await invoke('create_webview_window', { url: window.url, label: windowId }); errorMessage.value = ""; } catch (error) { console.error('Failed to reopen window:', error); errorMessage.value = `ウィンドウの再起動に失敗しました: ${error}`; } }; デスクトップがウィンドウだらけになってしまいますが、ひとまずマルチビューアプリの土台はできました。 WebView でのナビゲーションの検出と Rust → フロントエンド通知 # sbe では閲覧した Cosense ページの履歴を記録していますが、これは Electron の webContents のイベントを捕捉して実装しています。Cosense サイト内での遷移は did-navigate-in-page イベントで捕捉できます。 Electron のコード - main.mjs(抜粋) function handleLinkEvent(view) { view.webContents.on('will-navigate', (e, url) => { // リンクを開く処理 }); view.webContents.on('did-start-navigation', async (e, url, isInPlace) => { const currentUrl = view.webContents.getURL(); // 遷移開始時の処理 }); view.webContents.on('did-navigate-in-page', async (e, url) => { // サイト内遷移の処理(ヒストリへの保存など) }); view.webContents.on('update-target-url', (e, url) => { // リンクのマウスオーバー時の処理 }); } Tauri の Rust 用 API では on_navigation や on_page_load というメソッドがあり、Web ページの取得開始やロード完了を捕捉できます。しかしこのハンドラーでは同一サイト内のページ遷移は検出できないようです。Cosense サイト内でのページ遷移をリアルタイムに捕捉するには、JavaScript を WebView に埋め込んでイベントをトラッキングする必要があります。そのため WebView を起動する際に initialization_script でトラッキング用のスクリプトを埋め込みます。 Tauri WebView でのスクリプト注入 - src-tauri/src/lib.rs(抜粋) #[tauri::command] async fn create_webview_window(app: tauri::AppHandle, url: String, label: String) -> Result<(), String> { let webview_url = WebviewUrl::External(url.parse().map_err(|e| format!("Invalid URL: {}", e))?); let window = WebviewWindowBuilder::new(&app, &label, webview_url) .title("Scrapbox") .inner_size(1200.0, 800.0) .min_inner_size(800.0, 600.0) .center() .resizable(true) .visible(false) .initialization_script(include_str!("../scripts/navigation-tracker.js")) .build() .map_err(|e| e.to_string())?; window.show().map_err(|e| e.to_string())?; Ok(()) } 短いスクリプトは initialization_script の中にインラインで書けますが、可読性や IDE での作業効率化のために別ファイルで作成しロードする方がよいでしょう。 以下のスクリプトでは、trackNavigation 関数を用意し、変更を検出したら Tauri の invoke コマンドを通じて Rust 側に送信しています。ブラウザの進む・戻るイベントをリッスンして通知。history.pushState / history.replaceState をキャプチャーして SPA のナビゲーションを通知しています。また、MutationObserver を使ってタイトル変更を検知するようにしています。これにより、Cosense のようなモダンな SPA の画面遷移をトラッキングできるようになります。 注入するスクリプト - navigation-tracker.js let currentUrl = window.location.href; let currentTitle = document.title || window.location.hostname || 'Untitled'; // Function to track navigation function trackNavigation(source = 'unknown') { const url = window.location.href; const title = document.title || window.location.hostname || 'Untitled'; // Skip if no change if (url === currentUrl && title === currentTitle) return; console.log('Navigation tracked (' + source + '):', title, '→', url); // Update state currentUrl = url; currentTitle = title; // invoke Tauri command if (window.__TAURI__ && window.__TAURI__.core) { window.__TAURI__.core.invoke('track_navigation', { windowLabel: window.navigationTrackerLabel, url: url, title: title }).then(result => { console.log('Track navigation success:', result); }).catch(err => { console.error('Failed to track navigation:', err); }); } else { console.error('Tauri API not available'); } } // Track initial page load trackNavigation('initialization'); // Listen for forward/back event window.addEventListener('popstate', () => trackNavigation('popstate')); window.addEventListener('hashchange', () => trackNavigation('hashchange')); // Handle SPA navigation const originalPushState = history.pushState; const originalReplaceState = history.replaceState; history.pushState = function(...args) { originalPushState.apply(this, args); trackNavigation('pushState'); }; history.replaceState = function(...args) { originalReplaceState.apply(this, args); trackNavigation('replaceState'); }; // Monitor title changes (for dynamic title updates) let titleObserver; if (document.querySelector('title')) { titleObserver = new MutationObserver(() => trackNavigation('titleChange')); titleObserver.observe(document.querySelector('title'), { childList: true }); } WebView から invoke された Rust の track_navigation では、Vue 側に add-to-recent イベントを発行します。 注入スクリプトから invoke される Tauri コマンド #[tauri::command] async fn track_navigation(app: tauri::AppHandle, window_label: String, url: String, title: String) -> Result<(), String> { println!("Navigation tracked: {} -> {} ({})", window_label, url, title); // Emit event to main window for history tracking app.emit("add-to-recent", NavigationEvent { window_label, url, title, }).map_err(|e| e.to_string())?; Ok(()) } Vue 側では add-to-recent イベントを受けてリストを更新し、重複を排除するなどの処理をしてローカルストレージに書き込みます。 Vue 側の処理 // Listen for navigation events from WebView windows navigationUnlisten = await listen('add-to-recent', (event: any) => { const { window_label, url, title } = event.payload; addToRecent({ id: `${window_label}-${Date.now()}`, title: title || new URL(url).hostname, url, lastAccessed: new Date() }); console.log(`Navigation tracked: ${title} (${url})`); }); // Recent windows functions const addToRecent = (window: RecentWindow) => { recentWindows.value = recentWindows.value.filter(w => w.id !== window.id); recentWindows.value.unshift(window); saveToStorage(); }; // Data persistence const saveToStorage = () => { localStorage.setItem('sbe-recent', JSON.stringify(recentWindows.value.map(w => ({ ...w, lastAccessed: w.lastAccessed.toISOString() })))); localStorage.setItem('sbe-favorites', JSON.stringify(favorites.value)); }; --> Information 今回はフロントエンド側で LocalStorage に保存しましたが、Rust 側で JSON ファイルとしてセーブ・ロードするように実装すれば、マシンが変わっても履歴を持っていけるので便利かもしれません。 Electron がページ内遷移の細やかなイベントを提供してくれていたので、Tauri の方式はかなり面倒に感じる部分でした。Electron が Chrome を内包していることで開発者はきめ細かいイベントの捕捉を簡単にできていましたが、Tauri は OS にインストールされた WebView を使用しているのでそこまで WebView 実装に入り込んだイベントの提供はできないようです。そこで、initialization_script スクリプトを注入するという、ややハッキーなやり方が必要でした。 これは Tauri と WebView が疎結合であるためであり、このおかげで Tauri のアプリは軽量で省メモリになっているとも言えます。 Cosense ページ一覧画面のための API 呼び出しと JSON Parse # Cosense プロジェクトのページ一覧を Vue で作成しタブ内で表示します。このためには Cosense の API で該当するプロジェクトのページリストを取得する必要があります。sbe では、およそ以下のような感じでメインプロセス側で Cosense API を使用してページ一覧を取得しています。 Electron での API 呼び出し - main.mjs async function fetchPageInfo(url) { const sid = await getSid(); const res = await fetch(url, { headers: { cookie: sid } }); const data = await res.json(); return data; } async function getSid() { const cookies = await session.defaultSession.cookies.get({ name: 'connect.sid' }); return cookies[0].value; } プライベートな Cosense プロジェクトからも取得できるよう、Cookie をセッションから取得して、リクエストヘッダーに埋め込んでいます。 Tauri でもデータのフェッチは Rust 側でやるのが推奨です。特に API キーなどはフロントエンドに晒さない方がよいでしょう。 Rust 側で API 呼び出しを実装するので、Electron のメインプロセス(の JavaScript) ではするっと実装できていたレスポンスの処理はやや面倒になります。API のレスポンスを解析して以下のように型情報を定義しました。 Rust でのレスポンス型定義 // API Response #[derive(Serialize, Deserialize)] struct ScrapboxPagesResponse { #[serde(rename = "projectName")] project_name: String, skip: i32, limit: i32, count: i32, pages: Vec<ScrapboxPage>, } // Cosense page #[derive(Serialize, Deserialize, Clone)] struct ScrapboxPage { id: String, title: String, image: Option<String>, descriptions: Vec<String>, #[serde(rename = "lastUpdateUser")] last_update_user: Option<ScrapboxUser>, // 中略 #[serde(rename = "charsCount")] chars_count: Option<i32>, helpfeels: Option<Vec<String>>, } // Cosense user #[derive(Serialize, Deserialize, Clone)] struct ScrapboxUser { id: String, } Cosense API を呼び出す fetch_scrapbox_pages コマンドです。Cosense API のページングのためのパラメータを処理しているため少し長ったらしくなっていますが、 cookies_for_url メソッドでウィンドウから Cookie を取得し、ヘッダーに埋め込むところは Electron と同様の流れです。上記で定義した ScrapboxPagesResponse に API のレスポンスを格納しています。 Rust での Cosense API 呼び出し // Command to fetch Scrapbox pages with authentication (supports both public and private projects) #[tauri::command] async fn fetch_scrapbox_pages( app: tauri::AppHandle, project: String, skip: Option<i32>, limit: Option<i32>, sort: Option<String> ) -> Result<ScrapboxPagesResponse, String> { let skip = skip.unwrap_or(0); let limit = limit.unwrap_or(20); let sort = sort.unwrap_or_else(|| "updated".to_string()); let api_url = format!( "https://scrapbox.io/api/pages/{}?skip={}&limit={}&sort={}", project, skip, limit, sort ); let scrapbox_url = Url::parse("https://scrapbox.io").map_err(|e| format!("Invalid URL: {}", e))?; // Try to get cookies from main window's webview let cookies = if let Some(main_window) = app.get_webview_window("main") { main_window.cookies_for_url(scrapbox_url.clone()) .map_err(|e| format!("Failed to get cookies: {}", e))? }; let client = reqwest::Client::new(); let mut request_builder = client.get(&api_url); // Add cookies if available if !cookies.is_empty() { let cookie_header = build_cookie_header(cookies); println!("Using cookies for authentication: {} cookies", cookie_header.matches(';').count() + 1); request_builder = request_builder.header("Cookie", cookie_header); } let response = request_builder .send() .await .map_err(|e| format!("Failed to fetch pages: {}", e))?; if !response.status().is_success() { return Err(format!("API request failed with status: {} - This might be a private project requiring authentication", response.status())); } let pages_data: ScrapboxPagesResponse = response .json() .await .map_err(|e| format!("Failed to parse JSON: {}", e))?; Ok(pages_data) } Vue 側では Rust の fetch_scrapbox_pages を invoke して取得したリストを表示します。 Vue 側の処理 // Scrapbox pages functions const fetchScrapboxPages = async () => { scrapboxLoading.value = true; scrapboxError.value = ''; try { const result = await invoke('fetch_scrapbox_pages', { project: scrapboxProject.value, skip: scrapboxSkip.value, limit: scrapboxLimit.value, sort: scrapboxSort.value }) as { pages: ScrapboxPage[], count: number, skip: number }; scrapboxPages.value = result.pages; console.log(`Fetched ${result.pages.length} pages from ${scrapboxProject.value}`); } catch (error) { console.error('Failed to fetch Scrapbox pages:', error); scrapboxError.value = `ページの取得に失敗しました: ${error}`; } finally { scrapboxLoading.value = false; } }; Electron はメインプロセスも JavaScript で書けるので JSON の処理は楽でした。Tauri(Rust) では実行時ではなくコンパイル時のエラー検出など型安全性によるメリットもありますし、TypeScript でも同様です。大規模な開発では、この辺はコードジェネレータの仕事なんだと思います。 コンテキストメニューのハンドリング # WebView ウィンドウに表示している Cosense ページをお気に入りに追加するための実装を行います。WebView 上でコンテキストメニューを表示して追加してもらうのが自然でしょう。 以前の記事 では、SPA をアプリ化していたので Tauri の JavaScript API で簡単にコンテキストメニューを実装していました。今回のように WebView に Web サイトを表示する場合、コンテキストメニューの処理はやはりスクリプトを注入する必要があります。Tauri API によるコンテキストメニューのコードを注入してもいいのですが、今回は DOM 操作でコンテキストメニューを追加しました。Tauri API で追加するコンテキストメニューは OS ネイティブなものなので、WebView で表示しているサイトのルックアンドフィールに合わせたい場合は、DOM 操作で近い雰囲気のメニューを作るのも選択肢です。 WebView に注入するコンテキストメニュー用スクリプト function showContextMenu(x, y) { // Remove existing context menu if any const existingMenu = document.getElementById('tauri-context-menu'); if (existingMenu) { existingMenu.remove(); } // Create context menu const menu = document.createElement('div'); menu.id = 'tauri-context-menu'; menu.style.cssText = ` position: fixed; left: ${x}px; top: ${y}px; box-shadow: 0 2px 8px rgba(0,0,0,0.15); z-index: 10000; min-width: 180px; font-size: 14px; `; // Add menu item const menuItem = document.createElement('div'); menuItem.textContent = '⭐ お気に入りに追加'; menuItem.style.cssText = ` padding: 8px 16px; cursor: pointer; border-radius: 4px; transition: background-color 0.2s; `; menuItem.addEventListener('click', () => { addToFavorites(); menu.remove(); }); menu.appendChild(menuItem); document.body.appendChild(menu); document.addEventListener('click', function removeMenu() { menu.remove(); document.removeEventListener('click', removeMenu); }); } このスクリプトを先ほどの、navigation-tracker.js と同様 WebView に注入します。 コンテキストメニューのクリックで addToFavorites 関数を呼び出しており、この中で、 add_to_favorites_from_webview を invoke しています。Rust 側で add_to_favorites_from_webview コマンドが実行され、Vue 側に add-to-favorites イベントが発行されます。 コンテキストメニューから呼び出される Tauri コマンド - lib.rs // Command to add to favorites from WebView #[tauri::command] async fn add_to_favorites_from_webview(app: tauri::AppHandle, url: String, title: String) -> Result<(), String> { // Emit event to main window to add to favorites app.emit("add-to-favorites", FavoriteEvent { url, title, }).map_err(|e| e.to_string())?; Ok(()) } Vue側では、Rust から送信されたイベントを元にお気に入り追加の処理を行います。 Vue 側の処理 const addFavoriteFromWebView = async (url: string, title: string) => { try { // Check if already exists const existingFavorite = favorites.value.find(f => f.url === url); if (existingFavorite) { errorMessage.value = "すでにお気に入りに登録されています"; setTimeout(() => { errorMessage.value = ""; }, 2000); return; } const favorite: Favorite = { id: `fav-${Date.now()}`, title, url }; favorites.value.unshift(favorite); saveToStorage(); errorMessage.value = `お気に入りに追加しました: ${title}`; setTimeout(() => { errorMessage.value = ""; }, 3000); } catch (error) { console.error('Failed to add favorite from WebView:', error); errorMessage.value = `お気に入りの追加に失敗しました: ${error}`; } }; GitHub Actions ワークフローでプラットフォーム毎のインストーラーを生成 # 一通り動作する Tauri 版の Cosense アプリができたので、macOS や Windows 向けのインストーラを CI で作成するようにしてみます。 Tauri は OS の WebView を使用するため、クロスコンパイルはできません。OS ごとにビルド環境を用意する必要があります。Electron でも OS 毎の Chrome を同梱させるため、OS ごとのビルド環境が必要になるのでそこは変わりません。 GitHub Actions の Strategy Matrix を使って、macOS と Windows のインストーラを作成して成果物として保存するワークフローを定義しました。 .github/workflows/build-installers.yml name: Build Installers on: workflow_dispatch: jobs: build: strategy: matrix: include: - os: macos-latest name: macos-installer path: | src-tauri/target/release/bundle/dmg/*.dmg src-tauri/target/release/bundle/macos/*.app - os: windows-latest name: windows-installer path: | src-tauri/target/release/bundle/msi/*.msi src-tauri/target/release/bundle/nsis/*.exe runs-on: ${{ matrix.os }} steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm' - name: Setup Rust uses: dtolnay/rust-toolchain@stable - name: Install dependencies run: npm install - name: Build Tauri app run: npm run tauri build - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: ${{ matrix.name }} path: ${{ matrix.path }} retention-days: 30 このワークフローを実行して、生成された Tauri のアプリのインストーラは 5-7MB 程度、インストールされるバイナリは 3-4MB 程度です。 sbe のインストーラは 100MB 前後、macOS のユニバーサルインストーラは200MB近くあります。 Tauri アプリのフットプリントの軽さは魅力的ですね。起動が速くてアプリのレスポンスも軽快です。 ソースコードのリポジトリ # 今回の PoC の結果は以下のリポジトリに置いています。 https://github.com/kondoumh/sbe-tauri-poc Copilot に README を書いてもらったので表現がやや大袈裟になってしまっている点はご了承ください😅。 さいごに # 以上、Electron のアプリを Tauri 2.0 に移植してみる PoC のご紹介でした。今回の題材だと Electron の機能性や利便性が逆に強調される感じでしたが、軽量で高速なバイナリが生成される点や、Rust/Tauri のエコシステム、型安全性による開発体験は魅力ですね。 --> Information Tauri では、.NET の Blazor もサポートされています。 https://v2.tauri.app/ja/start/create-project/#%E6%96%B0%E3%81%97%E3%81%84%E3%83%97%E3%83%AD%E3%82%B8%E3%82%A7%E3%82%AF%E3%83%88%E3%82%92%E6%BA%96%E5%82%99%E3%81%99%E3%82%8B Blazor については昨年のアドベントカレンダーで紹介されています。 /blogs/2024/12/20/asp-dotnet-core-blazor/ Tauri 2.0 では OS の WebView を利用していますが、Servo ベースのクロスプラットフォームな WebView を開発するプロジェクト Verso があります。 NLnet; Servo improvements for Tauri Verso により 各 OS の WebView 間の差異が吸収され、主要なデスクトップおよびモバイルプラットフォームで一貫性のある体験がもたらされます。今回面倒に感じた Navigation 用の API なども利用しやすくなるかもしれません。 将来 Tauri に Verso プロジェクトの成果が取り込まれれば WebView を腹持ちする構造になるため、バイナリサイズは大きくなるでしょう。そのため、従来の OS の WebView と切り替えるようなオプションが提供されるかもしれませんね。
本記事は、以下イベントで講演した内容の文字起こし版です。 超実践 AI駆動開発 ~生成AIを活用したシステム開発の効率化・品質向上の最前線~ --> Information 2025年11月17日より、Q Developer CLIは正式名称「Kiro CLI」となりました。本記事では執筆時点の名称「Q Developer CLI」で説明していますが、コマンドや機能は同じです。今後のアップデートで q コマンドが kiro コマンドに変更される可能性があります。 目次 # はじめに 背景知識:なぜSDD+TDDなのか 環境構築:最小限の準備で始める 実践①:仕様策定 - specify → clarify の反復 実践②:実装 - TDDサイクルの実践 実践③:デプロイ - 自然言語だけでAWS構築 よくある質問とトラブルシューティング まとめと参考資料 はじめに # 対象読者 # 本記事は以下のような方を対象としています。 生成AIを使った開発に興味がある開発者 AIコード生成で「意図しない実装」に困った経験がある方 品質を保ちながらAIを活用したい方 Amazon Q Developer(Kiro CLI)やSpec Kitを試したい方 前提知識 # 以下の基礎知識があることを前提としています。 Git、GitHubの基本操作 Docker、Docker Composeの基本概念 Java/Spring Boot、React/TypeScriptの基礎(サンプルプロジェクトで使用) AWSの基本知識(デプロイセクション) 本記事で得られること # 本記事を読むことで、以下ができるようになります。 環境構築からデプロイまでの工程を理解できる コピペで動く実践的なプロンプト例を入手できる AI駆動開発における品質確保の方法を学べる 実際に動くサンプルプロジェクトを手に入れられる 背景知識:なぜSDD+TDDなのか # 生成AI駆動開発の現状 # 2022年末のChatGPT登場以降、AIを活用した開発は急速に普及しました。現在、GitHub Copilot、Cursor、Claude Code、Windsurf、Amazon Q Developer [1] 、Kiroなど、多くのコーディングアシスタントが利用可能です。 AIの進化は「モデル中心(GPT時代)」から「エージェントネットワーク中心」へと移行しつつあり、単なるコーディング支援から自律実行型エージェントへと発展しています。 AIコード生成の「影」 # しかし、実際の開発現場では以下のような課題に直面することがあります。 1. 過剰実装 要求していない機能を勝手に追加してしまう。 例 : ログイン機能だけ依頼したのに、パスワードリセット機能まで実装される シンプルなCRUDを依頼したのに、検索機能や並び替えまで追加される 2. 仮定 要求仕様の曖昧な部分を勝手に補完し、意図しない設計になる。 例 : 「ユーザー情報を保存」→ AIが勝手にメールアドレスを必須項目にする 「データを表示」→ AIが勝手にページングを10件単位で実装する 3. 成功宣言 ビルドやテストが失敗しているのに「完了しました」と報告する。 例 : コンパイルエラーがあるのに「実装完了」 テストが落ちているのに「全てのテストが成功」と報告 4. 保守コストの増大 最近の研究 [2] [3] によると、AI支援ツールで開発スピードを上げても、保守やレビュー、品質管理コストは上昇する傾向にあります。また、経験豊富な開発者が本来の新規開発業務から外れ、保守業務に偏るパターンも観察されています。 SDD+TDDによる解決アプローチ # これらの課題を解決するために、以下2つの手法を組み合わせます。 SDD(Specification-Driven Development:仕様駆動開発) コードを書く前に、期待する動作・要件・制約を明確に仕様化し、それをもとに開発を進める手法。 効果 :明確な仕様があると、AIが曖昧さなく正確にコードを生成可能になる [4] 。 TDD(Test-Driven Development:テスト駆動開発) コードを書く前にテストを定義し、失敗を起点に修正・改善を繰り返す手法。 効果 :LLMによるコード生成にTDDの枠組みを導入すると生成成功率が向上 [5] 。テストを「仕様・制約」として明示することで、AIの生成精度が改善 [6] 。 SDDで仕様を固め、TDDで正しい振る舞いを基準化することで、AIの制御可能性を高めます。 環境構築:最小限の準備で始める # 前提条件チェックリスト # 以下がインストール済みであることを確認してください。 Windows + WSL2(Linuxでも可) Git VSCode Docker Desktop(または Docker Engine) AWS Builder ID または AWS IAM Identity Center アカウント(Q Developer用) ステップ1:GitHubリポジトリのクローン # 今回講演用に作成した公開サンプルリポジトリをクローンします。 # 作業ディレクトリに移動 cd ~/workspace # リポジトリをクローン git clone https://github.com/mamezou-tech/aidd-demo.git # ディレクトリに移動 cd aidd-demo https://github.com/mamezou-tech/aidd-demo リポジトリの内容 : backend/ : Spring Boot バックエンド(Java 17, Spring Boot 3.x) frontend/ : React フロントエンド(React 18, TypeScript, Vite) specs/ : 仕様書ドキュメント .devcontainer/ : DevContainer設定 .amazonq/ : Amazon Q Developer設定 docker-compose.yml : 開発環境構成 ステップ2:VSCodeでDevContainerを起動 # # VSCodeで開く code . VSCodeが起動したら、以下の手順を実行します。 左下の緑色のボタン(リモートエクスプローラー)をクリック 「Reopen in Container」を選択 初回は数十分かかります(Dockerイメージのビルド) コンテナ内のターミナルが自動的に開く DevContainerに含まれるもの : Node.js、Java 17、Gradle AWS CLI Q Developer CLI 1.19.7 各種開発ツール ステップ3:Q Developer CLI(Kiro CLI)のログイン # Free プランの場合(AWS Builder ID) # ログインコマンド q login # ブラウザが開くので、AWS Builder IDでログイン # ログイン完了後、ターミナルに戻る Pro プランの場合(IAM Identity Center) [7] # ログインコマンド q login # プロンプトに従ってIAM Identity Centerの情報を入力 # Start URL: https://[your-domain].awsapps.com/start # Region: ap-northeast-1 など # ブラウザが開くので、認証を完了 対話モード開始 : q chat --> Information 2025年11月17日以降、Q Developer CLIは正式名称「Kiro CLI」となりました。本記事では旧名称「Q Developer CLI」で説明していますが、機能は同じです。今後のアップデートで q コマンドが kiro コマンドに変更される可能性があります。 ステップ4:アプリケーションの起動と動作確認 # 1. データベースとバックエンドの起動 Dev Container内のターミナルで以下を実行します。 docker compose up -d このコマンドで以下が起動します。 MySQLデータベース(ポート3306) Spring Bootアプリケーション(ポート8080) コンテナが起動していることを確認します。 docker compose ps # 期待される出力: # mysqldbとappコンテナのSTATUSがUp (healthy) 2. フロントエンドの起動 以下を実行します。 cd frontend npm run dev ※依存パッケージはDev Container起動時に自動インストールされます。 フロントエンドは http://localhost:3000 で起動します。 動作確認 : ブラウザで以下にアクセスします。 フロントエンド: http://localhost:3000 バックエンドAPI: http://localhost:8080/api/health トラブルシューティング(環境構築) # Q1: DevContainerが起動しない 原因 : Dockerが起動していない、またはリソース不足。 解決策 : # Dockerが起動しているか確認 docker ps # リソース設定を確認(Docker Desktopの場合、Settings > Resources) Q2: q コマンドが見つからない 原因 : DevContainerのビルドが不完全。 解決策 : # コンテナを再ビルド # VSCodeで Ctrl+Shift+P → "Dev Containers: Rebuild Container" # または、コマンドラインから docker compose down docker compose up -d --build Q3: ポート3000や8080が既に使用されている 原因 : 他のアプリケーションがポートを使用中。 解決策 : # 使用中のプロセスを確認 lsof -i :3000 lsof -i :8080 # プロセスを終了するか、docker-compose.ymlのポートを変更 # ports: # - "3001:3000" # 3000 → 3001に変更 実践①:仕様策定 - specify → clarify の反復 # Spec Kitコマンド一覧 # Spec Kitは以下のコマンドを提供します。 コマンド 説明 主な用途 @speckit.constitution プロジェクト原則を作成・更新 TDDルールなどの定義 @speckit.specify 機能の要件・ユーザーストーリーを定義 仕様書作成 @speckit.clarify 曖昧な仕様を明確化 仕様の精緻化 @speckit.plan 技術的な実装計画を作成 アーキテクチャ設計 @speckit.analyze 仕様・計画・タスク間の整合性分析 矛盾チェック @speckit.tasks 実装タスク一覧を生成 タスク分解 @speckit.implement 計画とタスクに従って実装 コード生成 @speckit.checklist 要件チェックリスト生成 進捗管理 --> Caution Q Developer CLIでは、スラッシュ( / )ではなくアットマーク( @ )でプロンプトを呼び出します。引数を正しく渡すため、コマンドの先頭に引用符( ' )を付けます [8] 。 対話型セッションの開始 # まず、Q Developer CLIの対話型セッションを開始します。以降のコマンドはすべてこのセッション内で実行します。 q chat セッションが開始されると、プロンプトが表示され、コマンドを入力できるようになります。 ステップ1:プロジェクト原則の確認 # プロジェクトにTDDなどの原則が定義されているか確認します。 追加が必要な場合は、以下のコマンドで追加が可能です。 '@speckit.constitution <プロジェクト原則に追加したい内容>' 記載例 : # Project Constitution ## Test-First Imperative コードの前に必ずテストを書く。 - ユニットテストを先に作成 - テストが失敗すること(Red phase)を確認してから実装 - 非交渉事項として厳格に適用 ## Library-First Principle すべての機能は独立したライブラリとして実装する。 ## Simplicity Gate 過度なエンジニアリングを防ぐ。 - 初期実装は最大3プロジェクトまで 公開リポジトリのconstitution.mdには、Spec Kitで提示されている6つの原則を記述しています。 ステップ2:仕様を策定する(@speckit.specify) # 初回のspecify 社員検索システムMVPの仕様を作成します。 プロンプト例 : '@speckit.specify 社員検索システム(MVP)の仕様を作成してください。 【目的】 社員情報を一元管理し、人事が「誰がどのスキルを持ち、どの組織に所属しているか」をすばやく把握できるようにすること。 【主な利用者】 - 人事:全社の社員・スキル・組織情報を俯瞰し、配置検討や採用計画のインプットに利用する。 【想定ユースケース】 - 人事が、特定スキル(例:Java、AWS)を持つ社員を検索し、部署横断で候補者リストを作成する。 - 顔写真付きの社員一覧・詳細画面で、人物を視覚的に識別できる。 【MVPで提供したい機能範囲】 - ログイン機能(シンプルな認証) - 社員情報の登録・閲覧・検索 - 基本属性(氏名、社員ID、所属組織、役職、雇用区分) - 顔写真の登録・表示 - スキル情報の閲覧 - スキルマスタの管理 - 社員ごとの保有スキルの紐づけ - 組織の階層管理(親子関係のみのシンプルなツリー構造) 【MVP対象外】 - 詳細な権限管理 - 監査ログ - 高度なスキル分析 - 組織改編の履歴管理 - 外部システム連携 【アウトプットの期待】 - ユースケース一覧と簡単なフロー - 画面・APIの概要 - データモデルの概要 - 非機能要件(MVP に必要な最低限) - MVP対象外機能の明示 ' ポイント : 目的を明確に記述する 利用者のペルソナを定義する MVPの範囲を明確にする(特に「対象外」を明示) 曖昧な表現を避ける 出力例 : AIが spec.md ファイルを生成します。内容には以下のような内容が含まれます。 # 社員検索システムMVP 仕様書 ## 1. 概要 ... ## 2. ユースケース ### UC-001: ログイン - アクター: 人事担当者 - 前提条件: ユーザーアカウントが登録されている - 基本フロー: 1. ユーザーがログイン画面でユーザー名とパスワードを入力 2. システムが認証を実行 3. 認証成功時、ホーム画面にリダイレクト ... ## 3. 画面設計 ### 3.1 ログイン画面 - 入力項目: ユーザー名、パスワード - ボタン: ログイン ... ## 4. データモデル ### 4.1 社員(Employee) - employee_id (PK): 社員ID - name: 氏名 - organization_id (FK): 所属組織 - photo_url: 顔写真URL ... ステップ3:曖昧な点を明確化する(@speckit.clarify) # 生成された仕様書には、まだ曖昧な点が残っています。clarifyコマンドで明確化します。 プロンプト例 : '@speckit.clarify 社員検索システムMVP仕様書に対して、以下の点について曖昧な点を洗い出し、質問リストを作成してください: - ログイン/認証方式(MVPでの具体的な前提) - スキル検索と表示方法 - 顔写真の登録方法 - 一覧画面におけるページング/ソートの扱い ' 出力例 : AIが以下のような質問リストを生成します。 # 仕様の曖昧点に関する質問 ## ログイン/認証方式 Q1: パスワードの暗号化方式は何を使用しますか?(bcrypt、PBKDF2など) Q2: セッション管理はどうしますか?(Cookie、JWT、サーバーサイドセッションなど) Q3: ログアウト機能は必要ですか? Q4: パスワードリセット機能はMVPに含まれますか? ## スキル検索と表示方法 Q5: スキル検索は完全一致ですか、部分一致ですか? Q6: 複数スキルのAND検索/OR検索は必要ですか? Q7: スキルレベル(初級/中級/上級など)の扱いはどうしますか? ## 顔写真の登録方法 Q8: 顔写真のファイルサイズ上限は? Q9: 許可する画像形式は?(JPEG、PNG、GIFなど) Q10: 顔写真がない場合、デフォルト画像を表示しますか? ## ページング/ソート Q11: 一覧画面の1ページあたりの表示件数は? Q12: ソート項目は何ですか?(氏名、社員ID、組織など) Q13: デフォルトのソート順は? ステップ4:質問に回答して仕様を修正(@speckit.specify) # AIからの質問に回答し、仕様を修正します。 プロンプト例 : '@speckit.specify 社員検索システムMVP仕様書を、以下の回答に基づいて更新してください。 【ログイン/認証方式】 A1: bcryptを使用 A2: JWTトークンをCookieに保存 A3: ログアウト機能は必要 A4: パスワードリセット機能はMVP対象外 【スキル検索と表示方法】 A5: 部分一致(前方一致) A6: 複数スキルのAND検索のみ A7: スキルレベルは3段階(初級/中級/上級) 【顔写真の登録方法】 A8: 5MB以下 A9: JPEG、PNGのみ A10: デフォルトアバター画像を表示 【ページング/ソート】 A11: 20件/ページ A12: 氏名、社員ID、所属組織でソート可能 A13: 氏名の昇順 ' ステップ5:反復 # specify → clarify → specify のサイクルを繰り返すと、曖昧な点がかなり減少します。 反復のポイント : 仕様が曖昧だと困る点を指定すると効果的 AIに推奨案を提示してもらうことで高速化できる 高速化の例 : '@speckit.clarify 社員情報の登録と更新について、曖昧な点を洗い出し、 あなたの推奨案も合わせて提示してください。 ' 成果物:spec.md # 最終的に以下のような完成度の高い仕様書が得られます。 ユースケースが具体的に記述されている 画面項目・API仕様が明確 データモデルが詳細に定義されている 曖昧な点が解消されている MVP対象外が明確 実践②:実装 - TDDサイクルの実践 # ステップ1:実装計画を作成(@speckit.plan) # @speckit.plan 出力例 : # 実装計画 ## 技術スタック - Backend: Java 17, Spring Boot 3.2.0, Spring Security, Spring Data JPA - Frontend: React 18, TypeScript, Tailwind CSS - Database: MySQL 8.0 - Test: JUnit 5, Testcontainers, Playwright ## アーキテクチャ ### バックエンド - レイヤードアーキテクチャ - Controller層: REST APIエンドポイント - Service層: ビジネスロジック - Repository層: データアクセス ### フロントエンド - コンポーネントベースアーキテクチャ - React Router for SPA - Context API for 状態管理 ## セキュリティ - JWT認証 - bcryptによるパスワードハッシュ化 - CORS設定 この内容が plan.md として保存されます。 ステップ2:タスク一覧を生成(@speckit.tasks) # '@speckit.tasks タレントマネジメントシステムMVPを、2〜3日の粒度の実装タスクに分解してください。 ' 出力例 : # 実装タスク一覧 ## Phase 1: 基盤構築 - Task 1.1: プロジェクト構造のセットアップ - Task 1.2: データベーススキーマの作成 ## Phase 2: 認証機能 - Task 2.1: ユーザーエンティティとRepository - Red: UserRepositoryTestを作成し、失敗を確認 - Green: User.java, UserRepository.javaを実装し、テスト成功 - Refactor: コードの整理 - Task 2.2: JWT認証の実装 - Red: JwtUtilTestを作成し、失敗を確認 - Green: JwtUtil.java, JwtAuthenticationFilter.javaを実装 - Refactor: コードの整理 - Task 2.3: ログインAPIの実装 - Red: AuthControllerTestを作成し、失敗を確認 - Green: AuthController.java, AuthService.javaを実装 - Refactor: コードの整理 ## Phase 3: 社員管理機能 - Task 3.1: 社員エンティティとRepository - Task 3.2: 社員登録API - Task 3.3: 社員検索API - Task 3.4: 社員詳細API ... この内容が tasks.md として保存されます。 ステップ3:実装(@speckit.implement) # いよいよ実装です。Spec Kitのテストファースト原則に基づき、TDDサイクルを使って実装を進めます。 Task 2.1の実装例:ユーザーエンティティとRepository プロンプト例 : '@speckit.implement Task 2.1' AIの動作例 : まずテストを生成 (Red phase) UserRepositoryTest.java : package com.example.demo.repository; import com.example.demo.entity.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.testcontainers.containers.MySQLContainer; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @DataJpaTest @Testcontainers @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) class UserRepositoryTest { @Container static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0") .withDatabaseName("testdb"); @Autowired private UserRepository userRepository; @Test void ユーザー名でユーザーを検索できる() { // Arrange User user = new User(); user.setUsername("testuser"); user.setPassword("hashedPassword"); userRepository.save(user); // Act Optional<User> found = userRepository.findByUsername("testuser"); // Assert assertThat(found).isPresent(); assertThat(found.get().getUsername()).isEqualTo("testuser"); } @Test void 存在しないユーザー名で検索すると空が返る() { // Act Optional<User> found = userRepository.findByUsername("nonexistent"); // Assert assertThat(found).isEmpty(); } } テストを実行して失敗を確認 # AIが自動的に実行 ./gradlew test --tests UserRepositoryTest # 結果:FAILED(User.javaやUserRepository.javaがまだ存在しないため) 実装を生成 (Green phase) User.java : package com.example.demo.entity; import jakarta.persistence.*; import lombok.Data; @Entity @Table(name = "users") @Data public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true, length = 50) private String username; @Column(nullable = false) private String password; @Column(length = 100) private String email; } UserRepository.java : package com.example.demo.repository; import com.example.demo.entity.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import java.util.Optional; @Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); } テストを実行して成功を確認 ./gradlew test --tests UserRepositoryTest # 結果:PASSED リファクタリング (Refactor phase) テストが通った状態で、コードを改善します。AIが以下のようなリファクタリングを実行します。 変数名・メソッド名の改善 重複コードの削除 可読性の向上 テストが通り続けることを確認しながら、安全にコードを改善します。 TDDサイクルの反復ポイント # Red → Green → Refactor のサイクル : Red(失敗するテストを書く) 期待する振る舞いをテストで表現 実装がないため、テストは失敗する Green(テストが通る最小限の実装) テストを通過させるコードを書く この段階では「美しさ」より「動作」を優先 Refactor(リファクタリング) テストが通った状態で、コードを改善 重複を削除、可読性を向上 実践③:デプロイ - 自然言語だけでAWS構築 # 前提条件 # AWS CLIがインストール済み(DevContainerに含まれる) AWSプロファイル(認証情報)が設定済み EC2を起動する権限があるIAMユーザー/ロール 今回のデプロイはテスト用途 のため、以下は簡略化しています。 HTTPSではなくHTTP 単一EC2インスタンス ロードバランサーなし データベースはコンテナ内 ステップ1:EC2インスタンスの作成 # プロンプト例 : Amazon Linux 2023、t3.smallでEC2インスタンスを作成してください。 セキュリティグループは、22番ポート(SSH)と3000番ポート(アプリ)を 自分のIPアドレスからのみ許可してください。 キーペアは "aidd-demo-key" という名前で作成してください。 AIの動作例 : AIが以下を自動実行します。 現在のIPアドレスを取得 セキュリティグループを作成(SSH: 22、App: 3000) キーペアを作成 EC2インスタンスを起動 インスタンスIDとパブリックIPを出力 ステップ2:DockerとDocker Composeのインストール # プロンプト例 : EC2インスタンス((パブリックIP))にSSHで接続し、 Docker と Docker Compose をインストールしてください。 ユーザーをdockerグループに追加し、再ログインせずに使えるようにしてください。 AIの動作例 : AIが以下を自動実行します。 EC2インスタンスにSSH接続 システムパッケージを更新 Dockerをインストール・起動・自動起動設定 ユーザーをdockerグループに追加 Docker Composeをインストール インストール完了を確認 ステップ3:アプリ一式をEC2へ転送 # プロンプト例 : カレントディレクトリのdocker-compose.ymlと、 backend、frontend、dbディレクトリを、 EC2インスタンス((パブリックIP))の /home/ec2-user/aidd-demo へ転送してください。 AIの動作例 : AIが以下を自動実行します。 EC2インスタンス上に転送先ディレクトリを作成 docker-compose.yml、backend、frontend、dbディレクトリをSCPで転送 ステップ4:Docker Composeでアプリ起動 # プロンプト例 : EC2インスタンスで、転送したディレクトリに移動し、 docker compose up -d を実行してください。 全コンテナが Up(healthy) で起動していることを確認してください。 AIの動作例 : AIが以下を自動実行します。 EC2インスタンスにSSH接続 転送したディレクトリに移動 docker compose up -d でコンテナを起動 全コンテナのステータスを確認し、Up (healthy) であることを報告 ステップ5:アプリ動作確認 # ブラウザでアクセス : http://(パブリックIP):3000 API疎通確認 : curl http://(パブリックIP):8080/api/health # 出力例: # {"status":"UP"} ログイン確認 : ブラウザでログイン画面にアクセスし、以下のテストユーザー情報でログインします。 ID: test@example.com PW: aiddTest ログイン後TOP画面が表示され、そのリンクから社員検索システムに遷移します。 ※登録データおよび顔写真はすべてAI生成によるものです。 CORS問題の修正(必要に応じて) # もしCORSエラーが発生した場合は以下を実行します。 プロンプト例 : バックエンドのCORS設定を修正してください。 フロントエンドのオリジン(http://(パブリックIP):3000)からのリクエストを許可してください。 修正後、EC2上でアプリを再起動してください。 まとめと参考資料 # 本記事のポイント # 本記事では、Amazon Q Developer × Spec Kitを使ったAI駆動開発の全工程を解説しました。 環境構築 : GitHubリポジトリのクローンだけで開始可能 DevContainerで依存関係を自動解決 仕様策定 : specify → clarify の反復で仕様を精緻化 曖昧さを排除することがAI活用の鍵 実装 : TDDサイクル(Red → Green → Refactor)の実践 テストファーストの原則でAIの生成精度が向上 デプロイ : 自然言語でAWSリソースを構築 重要な学び # 人間の役割は不可欠 :上流工程と品質確保は現段階では人間が担う 仕様とテストが資産 :高品質な成果物は次のAI駆動開発で再利用可能 Amazon Web Services. Amazon Q Developer . ↩︎ Xu et al. AI-assisted Programming and Maintenance Burden . arXiv, 2025. ↩︎ Amasanti & Jahić. The Impact of Generative AI-Generated Solutions on Software Maintainability . arXiv, 2025. ↩︎ GitHub. Spec-driven development with AI: Get started with a new open source toolkit . 2024. ↩︎ Mathews et al. Test-Driven Development for Code Generation . arXiv, 2024. ↩︎ Chen et al. TENET: Leveraging Tests Beyond Validation for Code Generation . arXiv, 2025. ↩︎ Classmethod. Amazon Q Developer Pro をメンバーアカウントでサブスクライブ利用してみた . 2024. ↩︎ Ahanoff. Amazon Q Developer を使用した Spec Kit:発見事項と癖 . 2024. ↩︎
はじめに # 前回 では、 仕様から実装・テスト生成まで の流れを体験し、AIがどのようにソフトウェア開発を支援できるかを確認しました。 今回(Day 3)は、その延長として 品質保証 に焦点を当てます。 AIによるコード生成が一般化する中で、重要なのは「 どう品質を保証し続けるか 」。 Q Developerの レビュー支援機能とメトリクス可視化 を使いながら、 AIと人間のハイブリッドによる品質保証サイクル を具体的に見ていきましょう。 1. どこを見るか ― 品質観点の棚卸し # まずは「レビューで何を見るのか」を整理します。 これはQ Developerが自動チェックする項目にも関わる部分です。 観点 チェック項目 目的 例外処理 例外の握り潰し・再スロー方針の一貫性 障害解析性を確保 命名規約 クラス名・変数名・ディレクトリ構造の整合性 読解コスト削減 依存関係 循環依存・層違反の有無 構造的健全性の担保 ログ設計 粒度・相関ID・監査性 運用時トレーサビリティの向上 テストの意図 境界条件・オラクルの有無 妥当性と網羅性の保証 2. Q Developerでレビューを回す # コードレビューをAIに依頼する場合、次のようなコマンドで開始します。 サンプルは前回作成したToDoアプリを使用します。 Q Developerを起動します。 # 対話モードを起動 q レビュー観点をレビューのカテゴリにまとめ、カテゴリ単位でQ Developerに指示を出します。 レビュー観点 レビュー・カテゴリ 説明 例外処理 設計/可読性 例外方針の整合性・例外伝播の扱いは設計扱い 命名規約 可読性 命名揺れ・責務不一致は可読性として指摘される 依存関係 設計 層違反・循環依存などアーキテクチャ的問題 ログ設計 可読性/設計 ログ粒度・相関ID は設計・運用設計の観点 テストの意図 設計/品質(テスト品質) AI では「テストの妥当性」=設計・品質問題として扱われる また、上記表のカテゴリにはありませんが、全体横断的にセキュリティは重要な観点のため、Q Developerへの指示に追加します。 起動後のプロンプトで以下のように入力します。 ※ツール実行提案が出た場合、セッション中のみ許可する場合は y 、以後も許可する場合は t を選択します。 main.py と test_api.py のコードをレビューしてください。 セキュリティ、可読性、設計の観点で指摘を分類し、修正提案も示してください。 応答: 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/todo-fastapi/main.py, all lines ✓ Successfully read 1684 bytes from /home/ubuntu/todo-fastapi/main.py ⋮ ● Completed in 0.0s 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/todo-fastapi/test_api.py, all lines ✓ Successfully read 3155 bytes from /home/ubuntu/todo-fastapi/test_api.py ⋮ ● Completed in 0.0s > ## コードレビュー結果 ### 🔒 セキュリティの問題 main.py: 1. 入力値制限なし python # 問題: title/descriptionの長さ制限なし title: str # DoS攻撃の可能性 2. レート制限なし python # 問題: API呼び出し制限なし @app.post("/todos") # 大量リクエスト可能 3. CORS設定なし python # 問題: クロスオリジン制御なし app = FastAPI() # CORS未設定 test_api.py: 4. ハードコードされたURL python BASE_URL = "http://localhost:8000" # 設定ファイル化推奨 ### 📖 可読性の問題 main.py: 1. グローバル変数 python # 問題: グローバル状態 todos = [] todo_id_counter = 1 2. マジックナンバー python # 問題: ポート番号ハードコード uvicorn.run(app, host="0.0.0.0", port=8000) test_api.py: 3. 重複コード python # 問題: 同じパターンの繰り返し print(f"Create Todo: {response.status_code}") print(json.dumps(response.json(), indent=2)) ### 🏗️ 設計の問題 main.py: 1. 単一責任原則違反 python # 問題: ビジネスロジックとAPI層が混在 def create_todo(todo: TodoCreate): global todo_id_counter # データ操作がAPI層に 2. スレッドセーフティ python # 問題: 競合状態の可能性 todo_id_counter += 1 # 非原子的操作 test_api.py: 3. テスト独立性なし python # 問題: テスト間の依存関係 todo_id = test_create_todo() # 前のテスト結果に依存 ## 🔧 修正提案 ### セキュリティ改善 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: /home/ubuntu/todo-fastapi/main_improved.py + 1: from fastapi import FastAPI, HTTPException + 2: from fastapi.middleware.cors import CORSMiddleware + 3: from pydantic import BaseModel, Field, validator + 4: from typing import List, Optional + 5: from datetime import datetime, timezone ~~~(中略)~~~ + 84: @app.delete("/todos/{todo_id}", status_code=204) + 85: def delete_todo(todo_id: int = Field(..., gt=0)): + 86: if not repo.delete(todo_id): + 87: raise HTTPException(status_code=404, detail="Todo not found") + 88: + 89: if __name__ == "__main__": + 90: import uvicorn + 91: uvicorn.run(app, host="127.0.0.1", port=8000) ⋮ ↳ Purpose: Create improved main.py with security enhancements Creating: /home/ubuntu/todo-fastapi/main_improved.py ⋮ ● Completed in 0.1s > ### テスト改善 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: /home/ubuntu/todo-fastapi/test_improved.py + 1: import requests + 2: import json + 3: from datetime import datetime, timedelta + 4: from typing import Dict, Any + 5: ~~~(中略)~~~ + 121: # クリーンアップ + 122: self.cleanup() + 123: self.test_get_todos() + 124: + 125: if __name__ == "__main__": + 126: tester = TodoAPITester() + 127: tester.run_all_tests() ⋮ ↳ Purpose: Create improved test file with better structure Creating: /home/ubuntu/todo-fastapi/test_improved.py ⋮ ● Completed in 0.3s > ## 📋 改善点まとめ ### セキュリティ改善 • **入力値制限**: Field()でmin_length/max_length設定 • **CORS設定**: 特定オリジンのみ許可 • **スレッドセーフ**: threading.Lockで排他制御 • **バインドアドレス**: 127.0.0.1に変更(外部アクセス制限) ### 可読性改善 • **責任分離**: TodoRepositoryクラスでデータ操作を分離 • **設定外部化**: 環境変数やクラス初期化で設定管理 • **重複排除**: テストクラスで共通処理をメソッド化 ### 設計改善 • **依存性注入**: リポジトリパターン採用 • **テスト独立性**: クリーンアップ機能追加 • **エラーハンドリング**: 適切なHTTPステータスコード使用 • **型安全性**: Field()バリデーション強化 改善版では本格運用に近い品質になっています。 指示したように、AIレビュー結果では以下のように分類されて表示されました。 カテゴリ 例 対応方針 セキュリティ 未処理の例外、ハードコードされた認証情報 優先対応 可読性 命名のばらつき、コメント欠如 対応を検討 設計 クラス肥大化、循環依存 チーム内で合意形成が必要 このようにAIを相手にレビューを実施することが可能です。 ただし、AIの自動修正提案(ツール実行)は “提案”レベル です。 必ず人間が承認する運用にしましょう。 3. メトリクスで「見える化」する # レビューは定性的な活動ですが、Q Developerでは数値指標による定量的管理も可能です。 ここでは、 なぜメトリクスが必要なのか 、そして どのように活用すべきか を、より実務的な観点で詳しく説明します。 3.1 メトリクスの役割とは? # メトリクスとは、コード品質や開発プロセスを「数値」で客観的に評価する道具です。 属人的な「なんとなく良さそう」「たぶん問題ない」を排除し、以下の3つを実現します: 現状の可視化(見える化) :改善すべき箇所を定量的に把握できる 改善効果の測定 :前回との比較で良くなったかどうかがわかる 基準の共有 :チーム全員が同じ尺度で議論できる 特にAIと共存する開発では、生成コードの質にばらつきがあるため、メトリクスは「品質の安定化」に欠かせません。 3.2 主要メトリクスの詳細 # ここでは、コード品質を評価する際に特に重要となるメトリクスを整理します。 これらは単なる“数字”ではなく、 品質改善の優先順位付け や チーム内での合意形成 に欠かせない指標です。 メトリクスの目的や意味を理解しておくことで、レビュー結果をより実務的に活用できます。 指標 目的 目安 詳細説明 静的解析スコア 構文・構造の健全性 90点以上 循環依存、未使用コード、例外処理、セキュリティ問題などを総合評価。点数化することで改善の優先度を決めやすい。 テストカバレッジ 機能網羅率 80%以上 単に数値を見るのではなく、 どの関数が未カバーか まで確認するのが重要。重要ロジックは100%を目指す。 コメント率 可読性・ドキュメント性 10〜20% コメントが「多ければ良い」ではない。コードの意図や設計判断を書き残す“質”が重要。 命名一貫率 品質基準遵守度 95%以上 関数・変数の命名パターンが揃っているか。レビュー時間削減につながる。 変更影響深度 リファクタ容易性 低いほど良い 変更がどれだけ他モジュールへ影響するかを測る。深度が高い=保守コストが高いコード。 3.3 メトリクスを実際に計測する # 前回作成した todo-fastapi/ はモジュール構成が小さいため、まずは テストカバレッジ を例にします。 pytestを使用したカバレッジ計測を実施します。 指示: python main.py pytest --cov=. --cov-report=xml 応答: 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: python main.py & ⋮ ↳ Purpose: Start FastAPI server in background ⋮ ● Completed in 0.1s 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: sleep 3 && pytest --cov=. --cov-report=xml ⋮ ↳ Purpose: Wait for server to start and run pytest with coverage ============================= test session starts ============================== platform linux -- Python 3.12.3, pytest-9.0.1, pluggy-1.6.0 rootdir: /home/ubuntu/todo-fastapi plugins: anyio-4.11.0, cov-7.0.0 collected 8 items test_api.py ..E..... [100%] ==================================== ERRORS ==================================== ______________________ ERROR at setup of test_delete_todo ______________________ file /home/ubuntu/todo-fastapi/test_api.py, line 24 def test_delete_todo(todo_id): ~~~(中略)~~~ 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: ps aux | grep "python main.py" ⋮ ↳ Purpose: Check if server is running ubuntu 222074 0.2 0.6 231596 49596 pts/7 Sl+ 21:55 0:00 python main.py ubuntu 224348 0.0 0.0 4756 3248 pts/5 S+ 22:02 0:00 bash -c ps aux | grep "python main.py" ubuntu 224350 0.0 0.0 4100 1992 pts/5 S+ 22:02 0:00 grep python main.py ⋮ ● Completed in 0.28s 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: curl -s http://localhost:8000/todos || echo "Server not responding" ⋮ ↳ Purpose: Test server connectivity [{"id":1,"title":"Test Todo","description":"Test description","priority":"high","due_date":"2025-11-17T21:55:47.625131","created_at":"2025-11-16T21:55:47.632922"},{"id":2,"title":"Minimal Todo","description":null,"priority":"low","due_date":null,"created_at":"2025-11-16T21:55:47.654090"},{"id":5,"title":"Test Todo","description":"Test description","priority":"high","due_date":"2025-11-17T22:02:54.546964","created_at":"2025-11-16T22:02:54.550647"},{"id":6,"title":"Minimal Todo","description":null,"priority":"low","due_date":null,"created_at":"2025-11-16T22:02:54.575022"}] ⋮ ● Completed in 0.16s > ✅ サーバーが起動し、テストが実行されました 結果: • **サーバー**: 正常起動中(PID 222074) • **XMLレポート**: Coverage XML written to file coverage.xml - 成功 • **テスト**: 7つ成功、1つエラー(fixture問題) • **API**: 正常動作(Todoデータが存在) 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: pkill -f "python main.py" ⋮ ↳ Purpose: Stop the server ⋮ ● Completed in 0.15s > サーバーを停止しました。XMLカバレッジレポートが正常に生成されています。 これにより coverage.xml が生成されます。 3.4 coverage.xml を Q Developer に解析させる(対話) # 対話モードで、次のように依頼します: coverage.xml を読み込み、 ・関数ごとのカバレッジ ・未テスト箇所の一覧 ・優先的に追加すべきテストケースの提案 ・複雑度の高い関数ランキング を出してください。 Q Developer は内部ツール(fs_read / analyze_code)を使い、以下を返します: どの関数が未カバーか 条件分岐のテスト不足 異常系・境界テストの欠落 テスト追加の具体例(パラメータ例まで出ることが多い) 応答: 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/todo-fastapi/coverage.xml, all lines ✓ Successfully read 4891 bytes from /home/ubuntu/todo-fastapi/coverage.xml ⋮ ● Completed in 0.0s ~~~(中略)~~~ 問題: サーバーが起動していたにも関わらず、main.pyのカバレッジが0%のままです。これは、テストがHTTPリクエストを送信しているだけで、pytestがmain.pyのコードを直接実行していないためです。TestClientを使用することで解決できます。 どうやら、カバレッジが正しく収集できないようです。 Q Developerは単に「できません」と言うだけでなく、解決案の提示もしてくれます。 Q Developerの進言に従って、TestClientを使用するように依頼します。 指示: TestClientを使用するように変更してください 応答: 🛠️ Using tool: fs_write ⋮ ● Path: test_api.py - 0 : import requests + 0: from fastapi.testclient import TestClient 1, 1: import pytest 2, 2: from datetime import datetime + 3: from main import app 3, 4: - 4 : BASE_URL = "http://localhost:8000" + 5: client = TestClient(app) 5, 6: 6, 7: def test_create_todo(): 7, 8: todo_data = { 8, 9: "title": "Test Todo", 9, 10: "description": "Test description", 10, 11: "priority": "high", 11, 12: "due_date": "2025-11-17T12:00:00" 12, 13: } ~~~(中略)~~~ Coverage XML written to file coverage.xml ========================= 8 passed, 1 warning in 1.17s ========================= ⋮ ● Completed in 1.557s > ✅ TestClientを使用したテストが成功しました! 結果: • **httpx**: インストール完了 • **テスト**: 8つ全て成功 • **XMLレポート**: Coverage XML written to file coverage.xml - 成功 • **実行時間**: 1.17秒 • **警告**: Pydantic V1 validator使用の警告のみ TestClientを使用することで、サーバー起動なしでAPIテストが実行でき、main.pyのコードカバレッジも測定されるようになりました カバレッジが収集されたので、再度以下のように依頼します。 coverage.xml を読み込み、 ・関数ごとのカバレッジ ・未テスト箇所の一覧 ・優先的に追加すべきテストケースの提案 ・複雑度の高い関数ランキング を出してください。 応答: 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/todo-fastapi/coverage.xml, all lines ✓ Successfully read 4181 bytes from /home/ubuntu/todo-fastapi/coverage.xml ⋮ ● Completed in 0.0s > ## カバレッジ分析結果 ### 📊 全体カバレッジ • **総合カバレッジ**: 97.87% (92/94行) - 大幅改善! • **main.py**: 95.83% (46/48行) • **test_api.py**: 100% (46/46行) ### 🔍 関数ごとのカバレッジ #### main.py (95.83%カバレッジ) • create_todo() (38-48行): ✅ 完全テスト済み • get_todos() (49-51行): ✅ 完全テスト済み • delete_todo() (57-63行): ✅ 完全テスト済み • validate_due_date() (24-28行): ✅ 完全テスト済み #### test_api.py (100%カバレッジ) • 全テスト関数が実行済み ### ❌ 未テスト箇所の一覧 #### main.py (わずか2行未テスト) 1. 66行: import uvicorn 2. 67行: uvicorn.run(app, host="0.0.0.0", port=8000) これらはif __name__ == "__main__"ブロック内のサーバー起動コードで、テスト時には実行されません。 ### 🎯 優先的に追加すべきテストケース #### 低優先度(既に高カバレッジのため) 1. エッジケースの追加 • 空文字列タイトル • 極端に長いタイトル/説明 • 境界値テスト 2. エラーハンドリングの詳細テスト • 不正なJSON形式 • 型変換エラー 3. パフォーマンステスト • 大量データでの動作確認 ### 📈 複雑度の高い関数ランキング #### 1位: create_todo() (main.py 38-48行) • **複雑度**: 高 • **カバレッジ**: ✅ 100% • **理由**: グローバル変数操作、オブジェクト生成、リスト操作、カウンター管理 • **行数**: 11行 #### 2位: delete_todo() (main.py 57-63行) • **複雑度**: 中高 • **カバレッジ**: ✅ 100% • **理由**: ループ処理、条件分岐、例外処理、リスト操作 • **行数**: 7行 #### 3位: validate_due_date() (main.py 24-28行) • **複雑度**: 中 • **カバレッジ**: ✅ 100% • **理由**: 条件分岐、日時比較、例外発生 • **行数**: 5行 #### 4位: get_todos() (main.py 49-51行) • **複雑度**: 低 • **カバレッジ**: ✅ 100% • **理由**: 単純なリスト返却 • **行数**: 3行 ### 🎉 成果 TestClient導入により劇的改善: • **カバレッジ**: 35.96% → 97.87% (+61.91%) • **main.py**: 0% → 95.83% (+95.83%) • **全テスト**: 成功(8/8) • **実行時間**: 高速化(サーバー起動不要) ### 💡 推奨アクション 1. 現状維持 - 既に優秀なカバレッジ 2. コード品質向上 • Pydantic V2への移行(警告解消) • 型ヒントの追加 3. 統合テストの追加 • 複数操作の組み合わせテスト 4. ドキュメント整備 • API仕様書の作成 結論: TestClientの導入により、ほぼ完璧なテストカバレッジを達成しました。残りの2行は実行時コードのため、実質的に100%のカバレッジです。 無事にカバレッジを収集することができました。 3.5 メトリクスをどう改善に活かすか # メトリクスは“数値を見ること”が目的ではありません。 むしろ、メトリクスは 「どこから改善すれば最も効果が高いのか」 を示す“改善ナビゲーション”です。 数値が示す意味を正しく理解し、次のアクションへつなげることで、品質改善サイクルが加速します。 以下では、代表的なメトリクスからどのように改善につなげるかを、より具体的に説明します。 改善のために以下のように使います: 低カバレッジ関数 → 追加テストを作成 カバレッジが低いということは、挙動を保証するテストが不足している状態です。特にビジネスロジックやバリデーション処理など、バグが入りやすい部分は優先的にテストを追加します。 例:create_todo() が0% → POST /todos の正常系・異常系テストを追加 複雑度の高い関数 → まずは分割(リファクタ) 複雑度が高い関数は読みづらく、バグが入りやすい上にテストもしづらい傾向があります。 責務を分割したり、共通処理をメソッド化することで可読性が向上し、結果的にテストもしやすくなります。 例:create_todo() に状態管理・バリデーション・登録処理が混在 → 役割ごとに関数分離 命名揺れ → コード規約をAIに与えて揃える 命名規約のばらつきは理解コストを上げ、レビュー時間を増大させます。 Q Developer に命名規約(例:snake_case / camelCase、略語ルール)を伝えておけば、一貫性のない命名を自動で指摘させることも可能です。 また、プロジェクトに合わせて命名変更案をAIが自動生成してくれます。 変更影響深度が高い箇所 → 設計見直しを検討 「この関数を変えると他の10ファイルが壊れる」という状態は、保守性の低さを表します。 依存関係の整理、責務分離、アーキテクチャ層の明確化などを検討し、影響範囲を意図的に小さくしていきます。 また、メトリクスは改善結果を“数字で説明できる”ため、「なぜその改善が必要なのか?」をチームに説明しやすくなります。 定量化により、改善ポイントが明確になり、チーム内での合意形成が容易になります。 このように、どれだけのコードがカバーできているのかを「定量的」に見極めながら、品質を維持しつつ機能を拡張・検証していくことが可能になります。 4. メトリクス駆動のハイブリッドレビュー運用モデル # AI と人間、それぞれの強みを活かしつつ、 定量的メトリクスを軸に品質保証サイクルを回すための運用モデル です。 従来のレビューでは人の経験や勘に依存していましたが、メトリクスを導入することで「観点の統一」「効果測定」「改善の優先順位付け」が可能になり、AI との組み合わせによって品質保証を持続的に行えるようになります。 AI レビューは網羅性と速度に優れ、一方でメトリクスは改善の指針として機能します。 最終判断は人間が行い、 AI × メトリクス × 人間の判断 によって再現性と持続性のある品質保証が成立します。 まとめ # 今回の最大の成果は、 メトリクスによって品質を“定量的に”評価できるようになったこと です。 これにより、これまで感覚的・属人的だったレビュー活動が、数字を用いた客観的な改善サイクルへと進化しました。 特に以下の点が大きな前進です: 静的解析スコア、カバレッジ、複雑度、命名一貫率といった指標により、レビューの“抜け漏れ”が可視化された 数値をもとに「どこを優先して直すべきか」が判断できるようになり、改善活動の効率が向上した AIによるレビュー提案と人間の判断を組み合わせることで、品質改善プロセスを継続的に回せる基盤ができた これらにより、レビュー精度のばらつきが減り、 品質改善のPDCAがデータに基づいて回る状態 へと近づきました。 皆さまの生成AI活用の参考になれば幸いです。 img { border: 1px gray solid; }
はじめに # 最近、Ubuntu Desktop 用にノートPCを購入しました。自宅で Web アプリケーションをホストしてみたいと思ったのがきっかけです。 購入後、早速 Apache Web サーバーをインストールし、 /var/www/html/index.html を直接編集してブラウザからアクセスしてみました。思った通りに表示されるのを見て、「これは楽しい!」と感じたのを覚えています。 しかし、このままでは問題があります。 この状態で外部にページを公開すると、編集内容が即座に本番環境へ反映されてしまう ということです。つまり、開発中の未完成なコードや、テスト用のデータが訪問者に見えてしまう可能性があります。 「開発環境と本番環境を分けたい...!」 そこで、Apache Web サーバーの公開ディレクトリとは別に開発用のディレクトリを作成し、そちらで開発を進めることにしました。同時に Git/GitHub を使ったバージョン管理も導入し、一般的な dev ブランチから main ブランチへのプルリクエスト・マージという開発フローも採用しました。 しかし、新たな問題が発生しました。この開発フローを取り入れたことで、開発が完了するたびに 開発ディレクトリから公開ディレクトリへの手動コピー&ペースト作業 が必要になったのです。GitHub 上でプルリクエストをマージさせつつ、手元のローカルリポジトリの main ブランチを基準にして、開発ディレクトリから公開ディレクトリへの手動コピー&ペーストを行う必要があります。流石にこれは面倒です。 「プルリクエストをマージしたら、更新内容が自動的に Apache Web サーバーの公開ディレクトリに反映されてほしい...!」 この面倒な手作業をなんとか自動化できないかと考えた結果、GitHub Actions による CD パイプラインを構築することにしました。 本記事では、GitHub Actions を用いて自宅 LAN 内の Ubuntu Desktop に対する継続的デプロイメント(CD)パイプラインの構築経験を、具体的な設定手順も併せて解説したいと思います。 構築するパイプライン # 本記事で構築する CD パイプラインは、以下のような仕組みで動作します。 実現される動作フロー # graph TD A[コード編集] --> B[dev ブランチにコミット] B --> C[GitHub で PR 作成] C --> D[PR を main にマージ] D --> E[GitHub Actions 自動起動] E --> F[自宅サーバーに自動デプロイ] F --> G[本番環境に反映完了] style A fill:#e1f5fe style D fill:#fff3e0 style G fill:#c8e6c9 手動で行っていた「開発ディレクトリから公開ディレクトリへのコピー&ペースト」が、 プルリクエストのマージをトリガーに完全自動化 されます。 システム構成 # graph TD A[GitHub Actions Runner] --> B[Cloudflare Edge] B --> C[Cloudflare Tunnel] C --> D[自宅 Ubuntu Desktop] D --> E[Apache Web Server] style A fill:#e1f5fe style B fill:#fff3e0 style C fill:#fff3e0 style D fill:#c8e6c9 style E fill:#c8e6c9 コンポーネント 役割 GitHub Actions マージをトリガーにデプロイワークフローを実行 Cloudflare Tunnel 自宅 IP を公開せずに安全な通信経路を提供 Cloudflare Access Service Token による自動認証を実現 SSH + rsync 変更されたファイルのみを効率的に同期 完成後にできること # ✅ プルリクエストをマージするだけで本番環境に自動反映 ✅ 自宅のパブリック IP アドレスを公開せずに安全に運用 ✅ デプロイ履歴が GitHub Actions のログに自動記録 ✅ 手動デプロイ作業から完全に解放 この記事の前提条件 # 本記事は以下の知識・環境をお持ちの方を対象としています。 必要な知識 # GitHub Actions の基本 : workflow ファイルの書き方や基本的なアクションの使い方 Linux の基本操作 : コマンドライン操作、ファイル権限、SSH 接続の概念 Apache Web サーバーの基本 : DocumentRoot の概念、基本的な設定 Git の基本 : ブランチ、プルリクエスト、マージの概念 必要な環境 # GitHub リポジトリ : プライベート・パブリックは問いません Ubuntu Desktop 環境 : 今回は Ubuntu 24.04.3 LTS を想定 Apache Web サーバー : インストール・設定済み Cloudflare アカウント : Cloudflare Zero Trust の利用を想定 独自ドメイン : ネームサーバーが Cloudflare に設定されているドメインであること 必要な技術概念と知識 # パイプラインを構築する前に、関連する技術概念を整理します。 GitHub Actions Workflows # GitHub Actions は、GitHub リポジトリ内で CI/CD パイプラインを実行するためのプラットフォームです。 https://docs.github.com/ja/actions/get-started/understand-github-actions 重要な概念 ワークフロー : .github/workflows/ 内のYAMLファイルで定義された1つ以上のジョブを実行するプロセス イベント : ワークフローをトリガーする、リポジトリ内の特定のアクティビティ(例えばプルリクエストのマージ) ジョブ : 同じランナーで実行される、ワークフロー内の一連のステップ アクション : ワークフロー内で特定のタスクを実行するコンポーネント (GitHub Actions で提供されるものや、自作のものを使用できる) ランナー : ワークフローを実行する仮想環境。各ランナーは一度に1つのジョブを実行できる。(今回は ubuntu-latest を使用します) SSH 接続とセキュリティ # SSH(Secure Shell)の基本 SSH は暗号化された通信路を使ってリモートサーバーに安全に接続するプロトコルです。 ホスト認証 SSH クライアント側で、接続先が 本物の SSH サーバー であることを確認する。 ホスト認証の仕組み SSH サーバーには ホスト鍵(host key) という固有の秘密鍵/公開鍵ペアが設定されている。 SSH クライアント側でこの公開鍵を保存する先が、 ~/.ssh/known_hosts ファイル。 初回接続時に、SSH サーバーのホスト鍵 (公開鍵) を SSH クライアント側の ~/.ssh/known_hosts ファイルに追加する。 その後の接続時に、このファイルに登録されているホスト鍵 (公開鍵) と SSH サーバーのホスト鍵 (秘密鍵) を比較して、接続先が本物の SSH サーバーであることを確認する。 ユーザー認証 SSH サーバー側で、接続してきた SSH クライアントのユーザーが 本物のユーザー であることを確認する。 ユーザー認証の仕組み 公開鍵認証方式を使用する。 ざっくりなイメージは以下のとおり。 SSH サーバーは、許可された SSH クライアントの公開鍵を ~/.ssh/authorized_keys ファイルに保持している。 SSH クライアントは、自身の秘密鍵を使用して署名を生成する。 SSH サーバーは、SSH クライアントの公開鍵を使用して署名を検証する。 署名が検証された場合、SSH サーバーは SSH クライアントのユーザーとして接続を許可する。 https://qiita.com/whoami_priv/items/9f165f8dfd95edb169b7 https://qiita.com/pyon_kiti_jp/items/f89b8fa9f5b7f8abac23 --> Information 今回の SSH 接続における役割 SSH クライアント : GitHub Actions のランナー環境 (ubuntu-latest) SSH サーバー : 自宅の Ubuntu Desktop なぜこの関係になるのか? GitHub Actions のワークフローは GitHub のクラウド環境で実行されます。 したがって、クラウド環境からみて外部のサーバー(自宅の Ubuntu Desktop)にファイルをデプロイするには、GitHub Actions ランナーから自宅サーバーに向けて SSH 接続を開始する必要があります。 つまり、「クラウド → 自宅」という方向での接続となり、GitHub Actions ランナーが接続を開始する側(クライアント)、自宅 Ubuntu Desktop が接続を受ける側(サーバー)となります。 Cloudflare Tunnel # Cloudflare Tunnel とは Cloudflare Tunnel は、 パブリック IP アドレスを公開することなく、ローカルサーバーを安全にインターネットに公開できるサービス です。 https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/ https://qiita.com/keke21/items/efaa2b2c35dfb646a43e 従来の自宅サーバー公開方法との違い 従来の方法(ポートフォワーディング): インターネット → ルーター(ポート22開放) → 自宅 Ubuntu Desktop ↑ セキュリティリスク ❌️ ルーターで SSH ポート(22番)を開放する必要 ❌ パブリック IP アドレスが直接露出 ❌ DDoS 攻撃やブルートフォース攻撃の標的になりやすい ❌ 自宅の IP アドレスが特定される可能性 Cloudflare Tunnel を使った方法: GitHub Actions → Cloudflare Edge → Cloudflare Tunnel → 自宅 Ubuntu Desktop ↑ 暗号化されたトンネル ✅️ ルーターでポートを開放する必要がない ✅️ パブリック IP アドレスを隠匿 ✅️ Cloudflare のセキュリティ機能を活用 ✅️ トラフィックが Cloudflare エッジを経由してフィルタリング 今回の構成での役割 graph TD A[GitHub Actions Runner] --> B[Cloudflare Edge] B --> C[Cloudflare Tunnel] C --> D[自宅 Ubuntu Desktop] style A fill:#e1f5fe style D fill:#c8e6c9 style C fill:#fff3e0 自宅 Ubuntu Desktop 側 : cloudflared デーモンが常時 Cloudflare に接続 GitHub Actions 側 : cloudflared access ssh コマンドでプロキシ経由接続 Cloudflare :両者の間でセキュアなトンネルを提供 セキュリティ上のメリット IP 隠匿 :自宅のパブリック IP アドレスが外部に露出しない DDoS 保護 :Cloudflare のセキュリティインフラを活用 アクセス制御 :Cloudflare Access による詳細な制御 Cloudflare Access # Cloudflare Access とは Cloudflare Access は、 Zero Trust Network Access(ZTNA)を実現する Cloudflare のセキュリティサービス です。従来の VPN に代わって、アプリケーションレベルでの認証・認可を提供します。 Zero Trust Network Access(ZTNA)の概念 https://www.cloudflare.com/ja-jp/learning/access-management/what-is-ztna/ 従来の境界セキュリティモデル: 外部(危険) | ファイアウォール | 内部(安全) 社内ネットワークとインターネットの間に境界を引き、ファイアウォールで外部からの脅威をブロックする 「内部ネットワークは信頼できる」という前提 一度内部に侵入されると横展開のリスク Zero Trust アプローチ: すべてのアクセスを検証 → 認証 → 認可 → アクセス許可 「信頼しない、常に検証する」(Never Trust, Always Verify) ネットワークの場所に関係なく、すべての接続を認証・認可 今回の構成での Cloudflare Access の役割 sequenceDiagram participant GA as GitHub Actions participant CF as Cloudflare Access participant CT as Cloudflare Tunnel participant UD as Ubuntu Desktop GA->>CF: 1. SSH 接続リクエスト + 認証情報 CF->>CF: 2. Access Policy 確認 CF->>GA: 3. 認証成功 GA->>CT: 4. cloudflared プロキシ経由で SSH 接続 CT->>UD: 5. セキュアトンネル経由で SSH 接続 UD->>GA: 6. SSH 接続確立 セキュリティ上のメリット 細かいアクセス制御 :リソースごと、ユーザーごとの詳細な権限設定 監査ログ :すべてのアクセス試行が記録される MFA 対応 :多要素認証の強制が可能(人間のユーザーの場合) リアルタイム制御 :ポリシー変更が即座に反映 セッション管理 :接続セッションの詳細な管理・監視 従来の VPN との違い 項目 従来の VPN Cloudflare Access 接続範囲 ネットワーク全体 アプリケーション単位 認証 接続時のみ アクセス毎に検証 設定複雑さ 複雑 比較的シンプル スケーラビリティ 制限あり 高い 監査ログ 限定的 詳細 Service Token による自動認証 Service Token とは Service Token は、 自動化システムやマシン間通信のための認証メカニズム です。人間のユーザーによるインタラクティブな認証(OAuth、SAMLなど)とは異なり、完全にプログラマティックなアクセスを可能にします。 従来のユーザー認証との違い: 項目 ユーザー認証 (OAuth/SAML) Service Token 対象 人間のユーザー 自動化システム・アプリケーション 認証フロー ブラウザベース・インタラクティブ API ベース・プログラマティック 認証情報 ユーザー名・パスワード・MFA Client ID・Client Secret 有効期限 セッション単位 (数時間) 長期間 GitHub Actions での役割 1. 自動化された SSH 接続の実現 GitHub Actions のワークフローでは、人間による認証操作ができないため、Service Token が不可欠です。 GitHub Actions Runner → Service Token 認証 → Cloudflare Access → SSH 接続 2. CD パイプラインに最適な理由 無人実行 : 人間の介入なしで24時間実行可能 スケーラビリティ : 複数のワークフローから同時利用可能 信頼性 : セッション切れやタイムアウトの心配がない セキュリティ : GitHub Secrets での安全な管理 3. 自動化ワークフローでのセキュリティ利点 最小権限の原則 : 特定のアプリケーションのみへのアクセス 監査可能性 : すべてのアクセスがログに記録 中央管理 : Cloudflare ダッシュボードでの一元管理 認証フローの概念 Service Token 認証プロセス: sequenceDiagram participant GA as GitHub Actions participant CF as Cloudflare Access participant App as Protected Application GA->>CF: 1. Service Token (Client ID + Secret) CF->>CF: 2. Token 有効性確認 CF->>CF: 3. Access Policy 評価 CF->>GA: 4. 認証成功 (Access Token) GA->>App: 5. アプリケーションアクセス (Access Token) App->>GA: 6. レスポンス Cloudflare Access による検証プロセス: Token 有効性確認 : 提供された Client ID と Secret の検証 Policy 評価 : 該当する Access Policy の条件確認 アクセス許可判定 : すべての条件を満たした場合のみアクセス許可 監査ログ記録 : 認証試行と結果の詳細ログ この Service Token による認証機構により、セキュリティを損なうことなく、完全に自動化された CD パイプラインを実現できます。 完成形のパイプライン全体像 # まずは最終的に完成したCDパイプラインの全体像を紹介します。 deploy.yml ファイルの全容 # name: Deploy to Server on: pull_request: types: [closed] branches: - main jobs: deploy: runs-on: ubuntu-latest if: github.event.pull_request.merged == true env: REMOTE_HOST: ssh.your-domain.com REMOTE_USER: deploy-user REMOTE_DIR: /var/www/html SSH_PROXY_COMMAND: /tmp/cloudflared/cloudflared access ssh --id ${{ secrets.CLOUDFLARED_SSH_ID }} --secret ${{ secrets.CLOUDFLARED_SSH_SECRET }} --hostname %h steps: - name: Install cloudflared run: | latest_version=$(curl -s $GITHUB_API_URL/repos/cloudflare/cloudflared/releases/latest | jq -r '.tag_name') mkdir -p /tmp/cloudflared curl -sL -o /tmp/cloudflared/cloudflared $GITHUB_SERVER_URL/cloudflare/cloudflared/releases/download/$latest_version/cloudflared-linux-amd64 chmod +x /tmp/cloudflared/cloudflared /tmp/cloudflared/cloudflared --version - name: Prepare .ssh/known_hosts from secrets run: | mkdir -p $HOME/.ssh chmod 700 $HOME/.ssh echo "${{ secrets.SSH_KNOWN_HOSTS }}" > $HOME/.ssh/known_hosts chmod 644 $HOME/.ssh/known_hosts - name: Set up SSH key run: | SSH_KEY_PATH=$HOME/.ssh/id_ed25519_github_actions echo "${{ secrets.SSH_PRIVATE_KEY }}" > "$SSH_KEY_PATH" chmod 600 "$SSH_KEY_PATH" - name: Checkout repository uses: actions/checkout@v4 - name: Deploy application via rsync run: | SSH_KEY_PATH=$HOME/.ssh/id_ed25519_github_actions rsync -rvz --no-group --no-perms --omit-dir-times \ -e "ssh -i \"$SSH_KEY_PATH\" -o StrictHostKeyChecking=yes -o ProxyCommand='$SSH_PROXY_COMMAND'" \ --include='public/***' \ --include='src/***' \ --include='views/***' \ --include='composer.json' \ --include='composer.lock' \ --exclude='*' \ ./ \ $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ - name: Run composer install on remote run: | echo "Installing composer dependencies..." SSH_KEY_PATH=$HOME/.ssh/id_ed25519_github_actions ssh -i "$SSH_KEY_PATH" \ -o StrictHostKeyChecking=yes \ -o ProxyCommand="$SSH_PROXY_COMMAND" \ $REMOTE_USER@$REMOTE_HOST " set -euo pipefail cd \"$REMOTE_DIR\" if command -v composer >/dev/null 2>&1; then COMPOSER_NO_INTERACTION=1 \ composer install \ --no-dev \ --prefer-dist \ --no-interaction \ --no-progress \ --optimize-autoloader if sudo -n true 2>/dev/null; then sudo chown -R www-data:www-data vendor sudo chmod -R 755 vendor else echo '⚠️ sudo権限がないため vendor の所有権/権限変更をスキップしました' fi echo '✅ Composer install completed.' else echo '❌ composer not found on remote host.' exit 1 fi " パイプラインの動作フロー # graph TD A[プルリクエストが main ブランチにマージ] --> B[GitHub Actions 起動] B --> C[リポジトリのコードをチェックアウト] C --> D[cloudflared のインストール] D --> E[SSH 設定とホスト鍵登録] E --> F[rsync によるファイル同期] F --> G[デプロイ完了] F -.-> H[Composer による依存パッケージのインストール<br/>※ PHP 環境のみ] H -.-> G style A fill:#e1f5fe style G fill:#c8e6c9 style H fill:#fff2cc 各ステップの詳細 # 1. トリガー条件 on: pull_request: types: [closed] branches: [main] main ブランチへのプルリクエストがクローズされた際に実行 github.event.pull_request.merged == true でマージされた場合のみ処理 2. cloudflared のインストール latest_version=$(curl -s $GITHUB_API_URL/repos/cloudflare/cloudflared/releases/latest | jq -r '.tag_name') mkdir -p /tmp/cloudflared curl -sL -o /tmp/cloudflared/cloudflared $GITHUB_SERVER_URL/cloudflare/cloudflared/releases/download/$latest_version/cloudflared-linux-amd64 chmod +x /tmp/cloudflared/cloudflared /tmp/cloudflared/cloudflared --version GitHub APIから最新バージョンを動的に取得 3. SSH 接続におけるホスト鍵の事前登録 mkdir -p $HOME/.ssh chmod 700 $HOME/.ssh echo "${{ secrets.SSH_KNOWN_HOSTS }}" > $HOME/.ssh/known_hosts chmod 644 $HOME/.ssh/known_hosts 事前に登録したホスト鍵を使用して安全な SSH 接続を確立 --> Caution セキュリティを重視した設計判断 自動化システムで SSH 接続を行う際、 StrictHostKeyChecking=no オプションを使用することで初回接続時のホスト確認を省略し、設定の簡素化も可能です。 # 簡単だがセキュリティリスクのあるアプローチ ssh -o StrictHostKeyChecking=no user@hostname "command" しかし、このアプローチは以下のセキュリティリスクを抱えています: ❌️ 中間者攻撃(MITM)の危険性 : 悪意のあるサーバーが正規のサーバーになりすますことが可能 ❌️ 接続先の真正性確認不可 : 意図した正しいサーバーに接続しているかの保証がない ❌️ セキュリティポリシーの妥協 : 自動化のために基本的なセキュリティチェックを無効化 事前ホスト鍵登録によるメリット: # セキュアなアプローチ echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts ssh -o StrictHostKeyChecking=yes user@hostname "command" # デフォルト動作 ✅️ 接続先の検証 : 事前に登録した正規のホスト鍵との照合により接続先を確実に検証 ✅️ 中間者攻撃の防止 : 不正なサーバーへの接続を自動的に拒否 ✅️ 自動化とセキュリティの両立 : 人間の介入なしに安全な接続を実現 ✅️ 監査適合性 : セキュリティ基準を満たした自動化システムの構築 このパイプラインでは、 自動化の利便性よりもセキュリティを優先 し、事前にホスト鍵を取得・管理する手間を承知で、より安全なアプローチを採用しました。 https://io.cyberdefense.jp/entry/dangerous_ssh_sftp_usage/ 4. ソースコードのチェックアウト - name: Checkout repository uses: actions/checkout@v4 actions/checkout@v4 : GitHub Actions の公式アクションを使用してリポジトリのソースコードを取得 デプロイ対象ファイルの準備 : rsync での転送前に、最新のマージされたコードをランナー環境に配置 5. ファイル同期 SSH_KEY_PATH=$HOME/.ssh/id_ed25519_github_actions rsync -rvz --no-group --no-perms --omit-dir-times \ -e "ssh -i \"$SSH_KEY_PATH\" -o StrictHostKeyChecking=yes -o ProxyCommand='$SSH_PROXY_COMMAND'" \ --include='public/***' \ --include='src/***' \ --include='views/***' \ --include='composer.json' \ --include='composer.lock' \ --exclude='*' \ ./ \ $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ rsync コマンドの詳細解説 SSH_KEY_PATH 変数 : SSH 秘密鍵のパスを変数化して可読性と保守性を向上 ed25519 鍵タイプ : RSA より高速で安全な楕円曲線暗号を使用 GitHub Actions 専用鍵 : デプロイ専用の識別しやすい鍵名 rsync オプションの解説 rsync -rvz --no-group --no-perms --omit-dir-times オプション 説明 理由 -r 再帰的コピー ディレクトリ構造を保持して全ファイルを転送 -v 詳細出力 転送状況をログで確認可能 -z 圧縮転送 ネットワーク帯域を効率的に利用 --no-group グループ変更スキップ 権限エラーを回避 --no-perms 権限変更スキップ 既存のサーバー権限を保持 --omit-dir-times ディレクトリタイムスタンプスキップ タイムスタンプ関連エラーを防止 https://log.dot-co.co.jp/rsync/ SSH 接続設定 -e "ssh -i \"$SSH_KEY_PATH\" -o StrictHostKeyChecking=yes -o ProxyCommand='$SSH_PROXY_COMMAND'" -e フラグ : rsync が使用する SSH コマンドを明示的に指定 -i "$SSH_KEY_PATH" : 指定した秘密鍵ファイルを使用してユーザー認証 StrictHostKeyChecking=yes : ホスト鍵検証を強制(セキュリティ確保) ProxyCommand='$SSH_PROXY_COMMAND' : Cloudflare Access 経由で SSH 接続を確立 SSH_PROXY_COMMAND の詳細 SSH_PROXY_COMMAND: /tmp/cloudflared/cloudflared access ssh --id ${{ secrets.CLOUDFLARED_SSH_ID }} --secret ${{ secrets.CLOUDFLARED_SSH_SECRET }} --hostname %h パラメータ 説明 役割 /tmp/cloudflared/cloudflared cloudflared バイナリパス 事前にインストールした cloudflared の実行ファイル access ssh SSH アクセスモード Cloudflare Access 経由での SSH 接続を指定 --id ${{ secrets.CLOUDFLARED_SSH_ID }} Service Token ID Cloudflare Access での認証に使用するクライアント ID --secret ${{ secrets.CLOUDFLARED_SSH_SECRET }} Service Token Secret Service Token のクライアントシークレット(GitHub Secrets で管理) --hostname %h 動的ホスト名指定 SSH 接続時のホスト名を動的に取得( %h は SSH の置換変数) なぜこのコマンドが必要なのか? GitHub Actions ランナー(SSH クライアント)は、人間のようにブラウザでログインして ID/パスワードや MFA を入力できません。Cloudflare Access のゲートを通過するためには、プログラマティックな認証メカニズムが必要です。 このコマンドの役割: このコマンドは、SSH 接続の前に Cloudflare Access による認証を自動的に行い、必要な認証情報を付与する ProxyCommand として機能します。具体的には、Service Token を使用して Cloudflare Access の認証を通過し、Cloudflare Tunnel 経由で自宅サーバーへの SSH 接続を確立します。 SSH プロキシコマンドの動作フロー: sequenceDiagram participant SSH as SSH Client participant CF as cloudflared participant CA as Cloudflare Access participant CT as Cloudflare Tunnel participant Server as Ubuntu Server SSH->>CF: ProxyCommand 実行 CF->>CA: Service Token で認証 CA->>CF: 認証成功 CF->>CT: Tunnel 経由接続要求 CT->>Server: SSH 接続転送 Server->>SSH: SSH セッション確立 このプロキシコマンドにより、GitHub Actions ランナー(SSH クライアント)は Cloudflare Access の認証とトンネルを経由して安全に自宅サーバー(SSH サーバー)に接続できます。 ファイル選択ロジック 包含パターン(Include): --include='public/***' # Web 公開ファイル --include='src/***' # アプリケーションソースコード --include='views/***' # テンプレートファイル --include='composer.json' # PHP 依存関係定義 --include='composer.lock' # 依存関係ロックファイル 除外パターン(Exclude): --exclude='*' # デフォルトですべて除外 選択的同期の利点: セキュリティ : 機密ファイル( .env , .git 等)の転送を防止 効率性 : 不要なファイル( node_modules , vendor 等)を除外して高速転送 安全性 : 本番環境に影響を与える可能性のあるファイルを制御 転送元・転送先の指定 ./ $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/ ./ : カレントディレクトリ(チェックアウトされたリポジトリのルート) $REMOTE_USER : リモートサーバーのユーザー名(例: deploy-user ) $REMOTE_HOST : 接続先ホスト名(例: ssh.your-domain.com ) $REMOTE_DIR : デプロイ先ディレクトリ(例: /var/www/html ) この設定により、 セキュアで効率的、かつ制御されたファイル同期 を実現しています。 この基本的な4ステップで、GitHub Actions から Cloudflare Access 経由で自宅サーバーへの安全なファイル同期が実現できます。 詳細な設定手順 # 実際に同じ環境を構築するための詳細な手順を説明します。 1. Cloudflare の設定 # 参考 Cloudflare login https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/create-remote-tunnel/ https://zenn.dev/z4ck_key/articles/github-actions-to-cloudflare-tunnnel https://zenn.dev/greendrop/articles/2024-04-25-aacf4debe469e8 https://dev.classmethod.jp/articles/use-service-tokens-to-authenticate-cloudflare-access-from-my-application/ https://zenn.dev/takajun/articles/fbd783e459c722 1.1 Cloudflare Tunnel の作成 Zero Trust > ネットワーク > Tunnels に移動 トンネルを作成する をクリック Cloudflared をクリック トンネル名を入力 トンネルを保存 をクリック 表示されたインストールコマンドをコピー 下記項目を入力し、 セットアップを完了する をクリック 項目 説明 サブドメイン (任意) ssh ドメイン 取得済みの独自ドメイン タイプ SSH URL localhost:22 1.3 Cloudflare Access の設定 1.3.1 Service Token の作成 Zero Trust > Access > サービス認証 に移動 サービストークンを作成する をクリック 下記項目を入力し、 トークンを作成する をクリック 項目 説明 サービストークン名 サービストークンの名前 サービストークンの有効期間 任意の選択肢 作成されたサービストークンの クライアント ID と クライアントシークレット をコピー (追って GitHub の Secrets に設定します) 保存 をクリック 1.3.2 Policy の作成 Zero Trust > Access > ポリシー に移動 ポリシーを追加する をクリック 下記項目を入力し、 保存 をクリック 項目 説明 ポリシー名 任意の名前 アクション Service Auth セッション時間 任意の選択肢 セレクター Service Auth 値 1.3.1 Service Token の作成 で作成したサービストークン 1.3.3 Application の作成 Zero Trust > Access > アプリケーション に移動 アプリケーションを追加する をクリック セルフホスト をクリック 下記項目を入力 項目 説明 アプリケーション名 任意の名前 セッション時間 任意の選択肢 サブドメイン (任意) ssh ドメイン 取得済みの独自ドメイン パブリックホスト名 の入力欄は パブリックホスト名を追加 をクリックすると表示されます。 既存のポリシーを選択 をクリックし、 1.3.2 Policy の作成 で作成したポリシーを選択 次へ をクリック エクスペリエンス設定 と 詳細設定 はデフォルトのまま、各自の環境に合わせて設定し、 次へ / 保存 をクリック 2. Ubuntu Desktop 側の設定 # 2.1 SSH サーバーのインストールと設定 # OpenSSH Serverのインストール sudo apt update sudo apt install openssh-server # SSH設定の編集 sudo nano /etc/ssh/sshd_config 設定項目: # パスワード認証を無効化 PasswordAuthentication no # 公開鍵認証を有効化 PubkeyAuthentication yes # rootログインを無効化 PermitRootLogin no # 空のパスワードを持つアカウントのログインを禁止 PermitEmptyPasswords no # 最大認証試行回数 MaxAuthTries 6 # ホスト鍵の指定 HostKey /etc/ssh/ssh_host_rsa_key HostKey /etc/ssh/ssh_host_ecdsa_key HostKey /etc/ssh/ssh_host_ed25519_key 設定後の再起動: sudo systemctl restart ssh.socket sudo systemctl enable ssh.socket 2.2 ファイアウォールの設定 # ファイアウォールの有効化 sudo ufw enable # デフォルトポリシーで受信 (Incoming) を拒否 sudo ufw default deny incoming # デフォルトポリシーで送信 (Outgoing) を許可 sudo ufw default allow outgoing # SSHポートの開放 sudo ufw allow ssh # SSH ブルートフォース攻撃の防止 sudo ufw limit ssh # ファイアウォールの状態を確認 sudo ufw status セキュアなSSHサーバの構築と運用ガイド(Ubuntu上) 2.3 デプロイ用ユーザーの作成 # デプロイ専用ユーザーを作成 sudo adduser deploy-user # デプロイ用ユーザー用に公開鍵を配置するディレクトリを作成する sudo mkdir -p /home/deploy-user/.ssh # 公開鍵ファイルを作成する (今は空ファイル) sudo touch /home/deploy-user/.ssh/authorized_keys # ディレクトリの権限を設定する sudo chmod 700 /home/deploy-user/.ssh # 公開鍵ファイルの権限を設定する sudo chmod 600 /home/deploy-user/.ssh/authorized_keys # ディレクトリの所有者を設定する sudo chown -R deploy-user:deploy-user /home/deploy-user/.ssh 2.4 Apache Web サーバーの設定 # Apacheのインストール sudo apt install apache2 # DocumentRootの権限設定 find /var/www/html -type d -exec chmod 750 {} \; find /var/www/html -type f -exec chmod 640 {} \; sudo chown -R www-data:www-data /var/www/html # deploy-userがwww-dataグループで書き込み可能に sudo usermod -aG www-data deploy-user 2.5 パッケージマネージャーのインストール --> Information この手順について パッケージマネージャーのインストールは今回の主題(CD パイプライン構築)とは別の内容のため、詳細は省略します。 今回は PHP 環境のため Composer をインストールしますが、開発言語に応じて適切なパッケージマネージャーを選択してください。 PHP : Composer Node.js : npm / yarn / pnpm Python : pip / poetry / pipenv Ruby : gem / bundler Go : go mod Rust : cargo Java : Maven / Gradle 各言語の公式ドキュメントを参照してインストールを行ってください。 2.6 cloudflared のインストールと設定 #1.1 Cloudflare Tunnel の作成 でコピーしたインストールコマンドを実行します。 # cloudflaredのインストール # Add cloudflare gpg key sudo mkdir -p --mode=0755 /usr/share/keyrings curl -fsSL https://pkg.cloudflare.com/cloudflare-public-v2.gpg | sudo tee /usr/share/keyrings/cloudflare-public-v2.gpg >/dev/null # Add this repo to your apt repositories echo 'deb [signed-by=/usr/share/keyrings/cloudflare-public-v2.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list # install cloudflared sudo apt-get update && sudo apt-get install cloudflared サービス化: sudo cloudflared service install [TOKEN] sudo systemctl start cloudflared sudo systemctl enable cloudflared 3. GitHub Secrets の設定 # 3.1 SSH鍵ペアの生成 --> Caution この手順について SSH 鍵の生成と公開鍵の配置は、 2.3 デプロイ用ユーザーの作成 で作成したユーザーのホームディレクトリで実施します。 # ユーザー名を確認 $ whoami deploy-user # カレントディレクトリを確認 $ pwd /home/deploy-user # GitHub Actions用のSSH鍵を生成 ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519_github_actions -C "GitHub Actions" # 公開鍵をサーバーに配置 cat ~/.ssh/id_ed25519_github_actions.pub >> ~/.ssh/authorized_keys 3.2 ホスト鍵の取得 Ubuntu Desktop の SSH サーバーに接続して、ホスト鍵を取得します。 下記コマンドの出力をコピーしておきます。 ssh-keyscan localhost 2>/dev/null | sed 's/localhost/ssh.your-domain.com/' ssh-keyscan コマンドで取得したホスト鍵に含まれる文字列 localhost を Cloudflare Tunnel のホスト名に置き換えるため、 sed コマンドの your-domain.com を各自の環境に合わせて置き換えてください。 3.3 GitHub Repository Settings での設定 Settings → Secrets and variables → Actions で New repository secret をクリックし、以下を設定する。 Name Value SSH_PRIVATE_KEY ~/.ssh/id_ed25519_github_actions の内容 SSH_KNOWN_HOSTS 3.2 ホスト鍵の取得 でコピーしたホスト鍵の内容 CLOUDFLARED_SSH_ID 1.3.1 Service Token の作成 で作成したサービストークンの クライアント ID (ヘッダー部分を除いたものをそのまま貼り付け) CLOUDFLARED_SSH_SECRET 1.3.1 Service Token の作成 で作成したサービストークンの クライアントシークレット (ヘッダー部分を除いたものをそのまま貼り付け) 4. ワークフローファイルの配置 # リポジトリに .github/workflows/deploy.yml を作成し、先ほど紹介した完成版のYAMLを配置します。 5. 動作確認 # 5.1 プルリクエストでのテスト 機能ブランチを作成 何らかの変更をコミット main ブランチに対するプルリクエストを作成 プルリクエストをマージ GitHub Actions の実行ログを確認 まとめ # 本記事では、GitHub Actions を使って自宅の Ubuntu Desktop への継続的デプロイメント(CD)パイプラインを構築する方法を解説しました。 達成したこと # 手動デプロイ作業の完全自動化 : プルリクエストのマージをトリガーに、開発ディレクトリから公開ディレクトリへのファイル同期を自動化 セキュアな接続 : Cloudflare Tunnel と Cloudflare Access を活用し、自宅のパブリック IP を公開せずに安全な SSH 接続を実現 Zero Trust アーキテクチャの実装 : Service Token による認証とアクセス制御により、セキュリティを損なわない自動化を構築 重要なポイント # Cloudflare Tunnel の活用 : ルーターのポート開放不要で、セキュアに自宅サーバーを外部に公開 Service Token 認証 : 人間の介入なしに、GitHub Actions からの自動アクセスを安全に実現 ホスト鍵の事前登録 : StrictHostKeyChecking=no の安易な使用を避け、セキュリティを優先した設計 このアプローチの利点 # 開発効率の向上 : デプロイ作業が自動化され、開発に集中できる セキュリティの確保 : 複数のセキュリティレイヤーにより、自宅サーバーを安全に運用 スケーラビリティ : 同じ仕組みを複数のプロジェクトやサーバーに適用可能 監査可能性 : すべてのデプロイが GitHub Actions と Cloudflare のログに記録 今後の発展 # このパイプラインを基盤として、以下のような機能拡張も検討できます。 デプロイ前の自動テスト実行 複数環境(ステージング/本番)への対応 デプロイ失敗時の自動ロールバック Slack/Discord への通知連携 自宅サーバーでの Web ホスティングは、学習や実験に最適な環境だなぁと感じました。大変楽しかったです☺️
はじめに # 前回 は、Q Developerを使って簡単なアプリケーションをインタラクティブモードで作成しました。 今回の記事では、Q Developerをさらに一歩掘り下げ、 仕様 → 実装 → テスト生成 → 修正 の一連の工程を通じて、AIがどのようにソフトウェア開発を支援するのかを確認します。 とくに焦点を当てるのは、 テスト可能な仕様(バリデーション) の考え方です。 AIが生成するテストがどこまで自動化できるのか、そして人間の開発者がどの段階で介入し、品質を保証すべきなのかを、実際のプロセスを通して検証していきます。 この記事を読むことで、次のような理解が得られるでしょう。 Q Developerが仕様からテストまで自動化できる範囲 AIによる自動テスト生成の利点と限界 テスト可能な仕様を設計する際の実務的なポイント 人が介在すべき判断ポイントと品質保証の勘どころ 使用するQ Developerのバージョンは 「 1.13.0 」 を想定しています。 それでは、AIが関わるテスト自動化を具体的に見ていきましょう。 1. シンプルなToDoアプリを作る # 1.1 Q Developer を対話モードで起動 # 作業ディレクトリを作成し、 q を起動します。 mkdir todo-fastapi && cd todo-fastapi q 起動後のプロンプトに、以下のメッセージを貼り付けます。 指示: FastAPIを使ってシンプルなToDoアプリケーション (登録・一覧・削除 API) を作成するためのPythonスクリプトを生成してください。 ファイル名は main.py と test_api.py です。 DBはメモリ、Pydanticでバリデーションを行い、 priorityは low/medium/high、 due_date は未来日チェックを含めてください。 応答: 🛠️ Using tool: fs_write ⋮ ● Path: /home/ubuntu/todo-fastapi/main.py + 1: from fastapi import FastAPI, HTTPException + 2: from pydantic import BaseModel, validator + 3: from typing import List, Optional + 4: from datetime import datetime + 5: from enum import Enum + 6: + 7: app = FastAPI() + 8: + 9: # In-memory database + 10: todos = [] ~~~(中略)~~~ + 57: @app.delete("/todos/{todo_id}") + 58: def delete_todo(todo_id: int): + 59: for i, todo in enumerate(todos): + 60: if todo.id == todo_id: + 61: del todos[i] + 62: return {"message": "Todo deleted successfully"} + 63: raise HTTPException(status_code=404, detail="Todo not found") + 64: + 65: if __name__ == "__main__": + 66: import uvicorn + 67: uvicorn.run(app, host="0.0.0.0", port=8000) ⋮ ↳ Purpose: Create FastAPI ToDo application with registration, listing, and deletion APIs Allow this action? Use 't' to trust (always allow) this tool for the session. [y/n/t]: > t Creating: /home/ubuntu/todo-fastapi/main.py ⋮ ● Completed in 0.0s 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: /home/ubuntu/todo-fastapi/test_api.py + 1: import requests + 2: import json + 3: from datetime import datetime, timedelta + 4: + 5: BASE_URL = "http://localhost:8000" + 6: + 7: def test_create_todo(): + 8: todo_data = { + 9: "title": "Test Todo", + 10: "description": "Test description", ~~~(中略)~~~ + 50: + 51: # Test invalid due date + 52: test_invalid_due_date() + 53: print() + 54: + 55: # Test deleting a todo + 56: test_delete_todo(todo_id) + 57: print() + 58: + 59: # Verify deletion + 60: test_get_todos() ⋮ ↳ Purpose: Create test file for the FastAPI ToDo application Creating: /home/ubuntu/todo-fastapi/test_api.py ⋮ ● Completed in 0.0s > FastAPIを使ったシンプルなToDoアプリケーションを作成しました。 main.py の主な機能: • POST /todos: ToDo登録 (タイトル、説明、優先度、期限日) • GET /todos: ToDo一覧取得 • DELETE /todos/{todo_id}: ToDo削除 • Pydanticでバリデーション (優先度: low/medium/high、期限日: 未来日チェック) • メモリ内データベース test_api.py でAPIテストが可能です。 アプリケーションを起動するには: bash python main.py テストを実行するには (別ターミナルで): bash python test_api.py 指示通りにPythonファイル main.py と test_api.py が作成されました。 1.2 生成コードの確認と適用 # 結果を確認します。 Q を起動しているフォルダ(作業ディレクトリ)上に main.py と test_api.py が保存されています。 ※もし Q Developer が「ツール実行(ファイル作成)」を提案した場合は、 yes で許可します。 依存ライブラリをインストールします。 これも Q Developer に実施してもらいます。 指示: アプリケーションを動かすのに必要なPythonライブラリをインストールしてください 応答: 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: python main.py ⋮ ↳ Purpose: Start the FastAPI ToDo application Traceback (most recent call last): File "/home/ubuntu/todo-fastapi/main.py", line 1, in <module> from fastapi import FastAPI, HTTPException ModuleNotFoundError: No module named 'fastapi' ⋮ ● Completed in 0.11s > FastAPIがインストールされていないようです。インストールします: 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: pip install fastapi uvicorn requests ⋮ ↳ Purpose: Install FastAPI and required dependencies ~~~(中略)~~~ Using cached uvicorn-0.38.0-py3-none-any.whl (68 kB) Downloading typing_extensions-4.15.0-py3-none-any.whl (44 kB) Downloading typing_inspection-0.4.2-py3-none-any.whl (14 kB) Installing collected packages: uvicorn, typing-extensions, typing-inspection, pydantic-core, pydantic, fastapi Attempting uninstall: typing-extensions Found existing installation: typing_extensions 4.13.2 Uninstalling typing_extensions-4.13.2: Successfully uninstalled typing_extensions-4.13.2 Attempting uninstall: typing-inspection Found existing installation: typing-inspection 0.4.0 Uninstalling typing-inspection-0.4.0: Successfully uninstalled typing-inspection-0.4.0 Attempting uninstall: pydantic-core Found existing installation: pydantic_core 2.33.2 Uninstalling pydantic_core-2.33.2: Successfully uninstalled pydantic_core-2.33.2 Successfully installed fastapi-0.121.1 pydantic-2.12.4 pydantic-core-2.41.5 typing-extensions-4.15.0 typing-inspection-0.4.2 uvicorn-0.38.0 [notice] A new release of pip is available: 25.1.1 -> 25.3 [notice] To update, run: pip install --upgrade pip ⋮ ● Completed in 7.388s > ライブラリのインストールが完了しました。アプリケーションを起動できます: bash python main.py 動作確認を行います。 アプリケーションを別コンソールから実行します。 python main.py ブラウザまたは curl で次を確認します。 curl http://127.0.0.1:8000/todos まだデータは空なので結果として「 [] 」(空データ)が出力されるはずです。 1章では「AIに自然言語で仕様を伝え、コードを自動生成させる」までを実施しました。 次に コードを前提に、仕様を改めて明文化し、テストで検証できるように します。 2. バリデーション強化 ― テスト可能な仕様にする # 次の2つの仕様を改めて明文化します。 due_date は「未来日」でなければならない priority は low , medium , high のいずれかである AIに対しては、以下のように日本語で伝えます。 指示: Taskモデルにバリデーションを追加してください。due_dateは未来日でなければならず、priorityはlow・medium・highのいずれかのみ許可してください。 応答: > 既存のコードにはバリデーションが実装されていますが、より明確にするために確認・改善します: 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/todo-fastapi/main.py, from line 10 to 30 ✓ Successfully read 469 bytes from /home/ubuntu/todo-fastapi/main.py ⋮ ● Completed in 0.0s ~~~(中略)~~~ 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: pkill -f "python main.py" ⋮ ↳ Purpose: Stop the background FastAPI process ⋮ ● Completed in 0.48s > バリデーションは既に正しく実装されており、動作しています: 1. priority: Priority Enumにより low、medium、high のみ許可 2. due_date: 過去日を指定すると422エラーで「due_date must be in the future」メッセージが返される テスト結果で確認できるように: • 有効なデータ: 正常に作成 (200) • 無効な過去日: バリデーションエラー (422) 現在のコードで要求されたバリデーションは完全に実装されています。 このように、コードを前提に、仕様を改めて明文化し、テストで検証できるようになりました。 これまで1章、2章で実施した内容をまとめると以下のようになります。 章 フェーズ 意図 役割 1章 実装生成 AIに自然言語で仕様を伝え、コードを自動生成させる 「生成の正確さ」を観察 2章 仕様検証 コードを前提に、仕様を改めて明文化・テストで検証 「仕様のテスト化」を学ぶ 3. サーバ実行とテスト # では、実際にアプリケーションを動かして、テストを実施してみましょう。 (テストの内容については、この後の章で説明します) アプリケーションをローカルで実行します python main.py 次にテストを実行します。 python test_api.py 実行結果は以下のようになりました。 Testing FastAPI Todo App ============================== Create Todo: 200 { "id": 1, "title": "Test Todo", "description": "Test description", "priority": "high", "due_date": "2025-11-10T22:59:37.092516", "created_at": "2025-11-09T22:59:37.099295" } Get Todos: 200 [ { "id": 1, "title": "Test Todo", "description": "Test description", "priority": "high", "due_date": "2025-11-10T22:59:37.092516", "created_at": "2025-11-09T22:59:37.099295" } ] Invalid Due Date: 422 { "detail": [ { "type": "value_error", "loc": [ "body", "due_date" ], "msg": "Value error, due_date must be in the future", "input": "2025-11-08T22:59:37.104030", "ctx": { "error": {} } } ] } Delete Todo: 200 { "message": "Todo deleted successfully" } Get Todos: 200 [] テストの戻り値「 200 」「 422 」が確認できていることがわかります。 4. AI生成テストを読み解く # AIは仕様文からテストを推論します。 今回作成されたテストケースは以下の通りです。 def test_create_todo(): todo_data = { "title": "Test Todo", "description": "Test description", "priority": "high", "due_date": (datetime.now() + timedelta(days=1)).isoformat() } response = requests.post(f"{BASE_URL}/todos", json=todo_data) print(f"Create Todo: {response.status_code}") print(json.dumps(response.json(), indent=2)) return response.json()["id"] def test_get_todos(): response = requests.get(f"{BASE_URL}/todos") print(f"Get Todos: {response.status_code}") print(json.dumps(response.json(), indent=2)) def test_delete_todo(todo_id): response = requests.delete(f"{BASE_URL}/todos/{todo_id}") print(f"Delete Todo: {response.status_code}") print(json.dumps(response.json(), indent=2)) def test_invalid_due_date(): todo_data = { "title": "Invalid Todo", "priority": "low", "due_date": (datetime.now() - timedelta(days=1)).isoformat() } response = requests.post(f"{BASE_URL}/todos", json=todo_data) print(f"Invalid Due Date: {response.status_code}") print(json.dumps(response.json(), indent=2)) これらのテストは基本的には正しく動作しますが、 AIが自動生成したものを鵜呑みにせず、人間の観点でレビューすることが極めて重要 です。 AIの推論は仕様文から妥当なロジックを導き出しますが、 暗黙的な前提や境界条件 までは十分に理解していないことが多いためです。 以下の観点でレビューを行うと、テストの品質を一段高めることができます。 観点 チェック内容 対応方針 仕様整合性 未来日境界(今日の日付は有効か?)、入力制約は明確か? 境界テストを追加する 可読性 テスト名が仕様を説明しているか、期待結果が明示されているか 名前やprint内容を改善する 網羅性 low , medium , high の正常系が揃っているか、異常系が網羅されているか 不足分を追加生成させる 独立性 各テストが他テストの結果に依存していないか 前提データ作成・削除処理を分離する 再現性 実行順序や時刻依存で結果が変わらないか 固定日時やID管理の仕組みを導入 AIが生成するテストは、仕様記述をもとに 「最も一般的なケース」 を推論する傾向があります。 そのため、 境界条件(当日や閾値など) エラー系(不正値、欠損値、異常なリクエスト) 並行動作や排他制御の確認 といった “仕様の周縁部” を十分に網羅できていないケースが多いです。 AIに「境界テストも追加して」と明示すれば生成されますが、重要なのは「何を境界とみなすか」を人が定義することです。 AIは仕様書の文章を解析してロジックを作りますが、その仕様書自体が不完全であれば、テストも不完全なままになります。 5. テスト補完(人手で対応) # AIが生成したテストコードは、いわば “骨格” です。 そこに 仕様の解釈・リスクベース思考・品質観点 を肉付けするのが人間の役割です。 とくに、次の3点を意識すると品質が安定します。 仕様理解の再確認 :AIが実装した内容と意図した仕様が一致しているか。 異常系の強化 :予期しない入力や外れ値をどう扱うかを明示する。 テスト設計の再利用性 :後続の機能追加でも使える汎用構造に整理する。 FastAPIのToDoアプリが動作し、基本テストが通ったら、次は 不足しているテストケース(境界・異常系) を補います。 ここでは、AIの提案に頼るのではなく、人が意図をもってテストを追加する手順を示します。 テスト対象: due_date が「今日」→ 無効(未来日限定) priority に不正値 → 無効( low / medium / high のみ許可) 正常系3種(low, medium, high)を網羅 def test_due_date_today_invalid(): """今日の日付を指定した場合の検証(422想定)""" todo_data = { "title": "Today Todo", "priority": "low", "due_date": datetime.now().isoformat() } response = requests.post(f"{BASE_URL}/todos", json=todo_data) print(f"Due Date Today Invalid: {response.status_code}") try: print(json.dumps(response.json(), indent=2)) except Exception: print(response.text) def test_priority_invalid_value(): """priority に不正値を指定した場合の検証(422想定)""" todo_data = { "title": "Invalid Priority Todo", "priority": "urgent", # 許可値: low/medium/high のみ "due_date": (datetime.now() + timedelta(days=1)).isoformat() } response = requests.post(f"{BASE_URL}/todos", json=todo_data) print(f"Invalid Priority: {response.status_code}") try: print(json.dumps(response.json(), indent=2)) except Exception: print(response.text) 6. テスト補完(AIに依頼) # 5章では人手でテストケースを拡充しましたが、AIにテストケースの補完が可能かどうかを確認してみます。 指示: テストケースが十分かどうかを確認してください 応答: > 現在のテストケースを確認し、不足している部分を特定します: 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/todo-fastapi/test_api.py, all lines ✓ Successfully read 1661 bytes from /home/ubuntu/todo-fastapi/test_api.py ⋮ ● Completed in 0.0s > 現在のテストケースは基本的な機能のみをカバーしています。不足しているテストケースを追加します: 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: test_api.py 29, 29: def test_invalid_due_date(): 30, 30: todo_data = { 31, 31: "title": "Invalid Todo", 32, 32: "priority": "low", 33, 33: "due_date": (datetime.now() - timedelta(days=1)).isoformat() ~~~(中略)~~~ + 107: # Delete minimal todo if created + 108: if minimal_id: + 109: test_delete_todo(minimal_id) + 110: print() + 111: 91, 112: # Verify deletion 92, 113: test_get_todos() ⋮ ↳ Purpose: Update main test execution to include all test cases Updating: test_api.py ⋮ ● Completed in 0.0s > 追加したテストケース: バリデーションテスト: • test_invalid_priority(): 無効な優先度 ("urgent") をテスト • test_missing_title(): 必須フィールド (title) の欠如をテスト • test_minimal_todo(): 最小限の必須フィールドのみでの作成をテスト エラーハンドリングテスト: • test_delete_nonexistent(): 存在しないToDo削除時の404エラーをテスト テストカバレッジ: • 正常系: 完全なToDo作成、最小限のToDo作成、一覧取得、削除 • 異常系: 過去日、無効な優先度、必須フィールド欠如、存在しないID削除 これで主要なバリデーションとエラーケースがカバーされます。 追加されたテストを実行してみましょう。 指示: テストを実行してください 応答: 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: python main.py & ⋮ ↳ Purpose: Start FastAPI application in background ⋮ ● Completed in 0.3s ~~~(中略)~~~ 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: pkill -f "python main.py" ⋮ ↳ Purpose: Stop the background FastAPI process ⋮ ● Completed in 0.15s > テスト結果の分析: ✅ 正常系テスト: • 完全なToDo作成: 200 OK • 最小限のToDo作成: 200 OK (description, due_dateはnull) • 一覧取得: 200 OK • ToDo削除: 200 OK ✅ バリデーションテスト: • 過去日指定: 422エラー "due_date must be in the future" • 無効な優先度: 422エラー "Input should be 'low', 'medium' or 'high'" • 必須フィールド欠如: 422エラー "Field required" ✅ エラーハンドリングテスト: • 存在しないToDo削除: 404エラー "Todo not found" すべてのテストケースが期待通りに動作しており、バリデーションとエラーハンドリングが正しく実装されています。テストカバレッ ジは十分です。 このようにAIと対話を進めながら、テストケースを拡充していくこともできます。 AIとペアでプログラミングできる日が来るとは、すごい時代になりました。 まとめ # AIが提案するテストはあくまで「たたき台」です。 人が仕様理解と品質観点でレビューし、採用・修正・削除の判断を行うことが重要です。 観点 採用すべき提案 却下すべき提案 仕様整合性 未来日・優先度の要件を正確に守る 曖昧な条件を含む(例:今日を許可) 可読性 テスト名・変数名が直感的 意味不明な略語や複雑なロジック 保守性 簡潔で再利用可能 重複コードや不要な依存関係を含む 実行容易性 python test_api.py で完結 外部サーバ起動が必要 これがQ Developerによる“品質管理の自動化”を成立させる鍵です。 皆さまの生成AI活用の参考になれば幸いです。 img { border: 1px gray solid; }
はじめに # 先月 Immutable releases が GA になりました。 https://github.blog/changelog/2025-10-28-immutable-releases-are-now-generally-available/ これによりリリースが公開後に変更されていないことを確認でき、改ざんや偶発的な変更を回避できるようになります。 変更不可リリースの特徴 # ドキュメントは以下で参照できます。 https://docs.github.com/ja/code-security/supply-chain-security/understanding-your-software-supply-chain/immutable-releases 変更不可リリースは以下のような特徴があります。 Git タグは移動または削除できない :リリースに関連付けられている Git タグは特定のコミットにロックされ、変更または削除することはできなくなります。 リリースアセットを変更または削除することはできない :リリースにアタッチされているすべてのファイルは、変更または削除から保護されます。 リリース構成証明が自動的に生成され、リリース タグ、コミット SHA、アセットなどの検証が可能になります。 変更不可リリースが有効な場合、そのリポジトリを削除し、同じ名前の新しいリポジトリを作成した場合でも、元のリポジトリの変更不可リリースに関連付けられたタグを再利用することはできなくなります。これは強力な保護機能ですね。 使ってみる # 筆者がメンテナンスしている野良 Cosense アプリ sbe のリポジトリで設定してみました。 --> Information Immutable releases は、リポジトリ単位、オーガニゼーション単位で設定可能です。 一度リリースしてしまうとリポジトリのコミッタでも変更はできないため、以下のようにドラフトリリースで作業する手順が推奨されています。 ドラフトリリースを作成 全てのアセットをドラフトリリースにアタッチ ドラフトリリースを正式リリースにする ちょうど sbe の変更が溜まっていたので、ベストプラクティスに従ってリリースドラフトを作成します。 保存されたドラフトリリースはまだ非公開で変更可能です。 --> Information このアプリのリリース用ワークフローではタグ作成を契機にリリースを作るようにしています。 softprops/action-gh-release という Action を使っています。リリース成果物のアタッチもやってくれます。 prerelease を true にすることで、ドラフトリリースを作成してくれます。 - name: Publish uses: softprops/action-gh-release@v2 with: files: | dist/**/*.exe dist/**/*.deb dist/**/*.AppImage dist/**/*.dmg prerelease: true 添付ファイルや差分を指差し確認してリリースを発行します。 Immutable release を有効にしていると確認のダイアログが出ます。 発行されたリリースには Immutable マークが付きました。 アセットには、リリースの attestation (証明) の JSON ファイルも追加されています。 編集画面では、リリースの説明などは編集可能ですが、タグやアセットは編集できない旨のメッセージが表示されます。 リリースを検証する # 利用者は Immutable release で作成された変更不可リリースを GitHub CLI で検証できます。 https://docs.github.com/ja/code-security/supply-chain-security/understanding-your-software-supply-chain/verifying-the-integrity-of-a-release?utm_medium=changelog&utm_campaign=universe25 リリースが存在し、かつ不変であることを検証するには、クローンしたリポジトリのディレクトリ内で release verify コマンドを実行します。 gh release verify RELEASE-TAG sbe の v3.8.0 リリースを検証すると GitHub API を使って証明を読み取り表示してくれます。 $ gh release verify v3.8.0 Resolved tag v3.8.0 to sha1:1f3f380d33f022230046a3200a67950ea027c8a1 Loaded attestation from GitHub API ✓ Release v3.8.0 verified! Assets NAME DIGEST sbe-3.8.0-universal.dmg sha256:ab1c2595601136bf82aa7594d48bc764fe6f226ed1071c52441eb531f34e0252 sbe-3.8.0.AppImage sha256:de1797b12152531df71e78519d660e32e1a79dca203bc3201d85b2facfe4b5a9 sbe-Setup-3.8.0.exe sha256:a4a8d6fe8ddde6e1a2005a29d7e2759511cb537427e4a0ff03440c5e9f48fb94 ローカルにある成果物がリリース成果物と完全に一致していることを検証するには release verify-asset コマンドを使用します。 gh release verify-asset RELEASE-TAG ARTIFACT-PATH リリースのアセットから macOS 用ユニバーサルインストーラのバイナリをダウンロードして検証してみました。 $ gh release verify-asset v3.8.0 ~/Downloads/sbe-3.8.0-universal.dmg Calculated digest for sbe-3.8.0-universal.dmg: sha256:ab1c2595601136bf82aa7594d48bc764fe6f226ed1071c52441eb531f34e0252 Resolved tag v3.8.0 to sha1:1f3f380d33f022230046a3200a67950ea027c8a1 Loaded attestation from GitHub API ✓ Verification succeeded! sbe-3.8.0-universal.dmg is present in release v3.8.0 --> Information アセットの証明には Sigstore の署名技術が利用されています。ソフトウェアの出所情報を検証可能し、ソフトウェアサプライチェーンの安全性を高めるための技術です。かなり前の記事ですが以下で紹介しています。 https://developer.mamezou-tech.com/blogs/2022/08/17/github-actions-workflows-for-software-supply-chain-security/ さいごに # 以上、Immutable releases の紹介でした。一定数ユーザーがいる OSS では変更不可リリースを採用する方がいいでしょう。 GitHub Actions でもサードパーティの Action を利用する際は、不意な変更の影響を受けないためにバージョンだけでなくコミットハッシュまで指定して固定することもあります。変更不可リリースを採用してくれる Action が増えれば使う側も安心ですね。
Q Developerとは # Q Developer は、AWSが提供する 生成AIによる開発支援ツール です。 ChatGPT や Copilot のように「コードを補完するAI」ではなく、 設計・実装・テスト・レビュー・ドキュメント化までを支援する開発プラットフォーム として設計されています。 特徴を一言で言うと、 AWSの開発を、AIと一緒に進められる統合環境 です。 主な機能と特徴 # 項目 概要 自然言語での開発支援 「Lambda関数を作って」「DynamoDBと連携して」と自然言語で指示できる コード生成と補完 Python, TypeScript, Javaなど主要言語をサポート テストコード自動生成 既存コードからテストを推論して自動生成 レビューと改善提案 コード品質・例外処理・命名規約などをAIが自動レビュー AWS統合 Lambda / API Gateway / DynamoDB / CDK などのAWSリソースを直接扱える VS Code統合 拡張機能からインラインでAIに指示・生成・修正が可能 CLIとVS Codeの2つの使い方 # Q Developerには2つの利用スタイルがあります。 スタイル 概要 コマンド例 CLI(コマンドライン) ターミナル上でAIに直接指示を出す $ q generate lambda --name hello-world VS Code拡張 エディタ内で自然言語プロンプトを入力し、コード生成・修正を行う コマンドパレットで「Q Developer: Generate Code」 Pro版を利用していれば、CLIとVS Codeの両方からAWSサービスを直接操作できるようになります。 “Q”の由来と目的 # 「Q」は “Quality(品質)” と “Question(問い)” の両方を意味します。 このツールの思想は、「AIがコードを書く」のではなく、 開発者がAIと会話しながら品質を作り込む という協働開発にあります。 したがって、Q Developerは単なる補助ツールではなく、 設計レビューから実装・改善までを一貫して支援する“AI開発パートナー” です。 今回は主に Q CLI を使って簡単なアプリケーションを作っていきます。 1. Q Developer CLIの概要 # CLI(コマンドラインインターフェース) では高度な操作が可能になります。 CLIでは、ターミナル上でAIと対話(インタラクティブモード)できるだけでなく、以下のようなコマンドで自動処理や統合が行えます。 機能 説明 コード生成 Lambda・API Gateway・CDK構成などをAIが生成 レビュー 既存コードの品質を分析し、改善案を提示 修正と再生成 問題箇所をAIが自動修正 テスト生成 単体テストコードを自動で生成・実行 ドキュメント化 READMEや設計書を自動出力 動作確認例 # 以下が表示されれば、環境は正しく構成されています。 コマンドラインですべて完結させることができますが、初回の試みということもあるので、 今後はすべて対話形式のインタラクティブモードで実行 します。 $ q --version q 1.13.0 $ q help # 利用可能なサブコマンド一覧を表示 # generate / review / test / fix / metrics / deploy など 2. Q CLIの起動とインタラクティブモード # 以下のコマンド「 q 」を実行します。 $ q 以下のような画面が表示されます。 ⢠⣶⣶⣦⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⢀⣤⣶⣿⣿⣿⣶⣦⡀⠀ ⠀⠀⠀⣾⡿⢻⣿⡆⠀⠀⠀⢀⣄⡄⢀⣠⣤⣤⡀⢀⣠⣤⣤⡀⠀⠀⣠⣤⣤⣤⣄⠀⠀⢀⣤⣤⣤⣤⣤⣤⡀⠀⠀⣀⣤⣤⣤⣀⠀⠀ ⠀⢠⣤⡀⣀⣤⣤⣄⡀⠀⠀⠀⠀⠀⠀⢠⣿⣿⠋⠀⠀⠀⠙⣿⣿⡆ ⠀⠀⣼⣿⠇⠀⣿⣿⡄⠀⠀⢸⣿⣿⠛⠉⠻⣿⣿⠛⠉⠛⣿⣿⠀⠀⠛⠉⠉⠻⣿⣧⠀⠈⠛⠛⠛⣻⣿⡿⠀⢀⣾⣿⠛⠉⠻⣿⣷⡀⠀ ⢸⣿⡟⠛⠉⢻⣿⣷⠀⠀⠀⠀⠀⣼⣿⡏⠀⠀⠀⠀⠀ ⢸⣿⣿ ⠀⢰⣿⣿⣤⣤⣼⣿⣷⠀⠀⢸⣿⣿⠀⠀⠀⣿⣿⠀⠀⠀⣿⣿⠀⠀⢀⣴⣶⣶⣶⣿⣿⠀⠀⠀⣠⣾⡿⠋⠀⠀⢸⣿⣿⠀⠀⠀ ⣿⣿⡇⠀⢸⣿⡇⠀⠀⢸⣿⣿⠀⠀⠀⠀⠀⠀⢹⣿⣇⠀⠀⠀⠀ ⠀⢸⣿⡿ ⢀⣿⣿⠋⠉⠉⠉⢻⣿⣇⠀⢸⣿⣿⠀⠀⠀⣿⣿⠀⠀⠀⣿⣿⠀⠀⣿⣿⡀⠀⣠⣿⣿⠀⢀⣴⣿⣋⣀⣀⣀⡀⠘⣿⣿⣄⣀⣠⣿⣿⠃⠀⢸⣿⡇⠀⠀⢸⣿⣿⠀⠀⠀⠀⠀⠀⠈⢿⣿⣦⣀⣀⣀⣴⣿⡿⠃ ⠚⠛⠋⠀⠀⠀⠀ ⠘⠛⠛⠀⠘⠛⠛⠀⠀⠀⠛⠛⠀⠀⠀⠛⠛⠀⠀⠙⠻⠿⠟⠋⠛⠛⠀⠘⠛⠛⠛⠛⠛⠛⠃⠀⠈⠛⠿⠿⠿⠛⠁⠀⠀⠘⠛⠃⠀ ⠘⠛⠛⠀⠀⠀⠀⠀⠀⠀⠀⠙⠛⠿⢿⣿⣿⣋⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀     ⠈⠛⠿⢿⡧ ╭─────────────────────────────── Did you know? ────────────────────────────────╮ │ │ │ If you want to file an issue to the Q CLI team, just tell me, or run q │ │ issue │ │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ /help all commands • ctrl + j new lines • ctrl + s fuzzy search ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 🤖 You are chatting with claude-4-sonnet この状態が「 インタラクティブモード 」です。 自然言語で直接話しかけて、AIと一緒に設計・実装を進めることができます。 対話例 # 以下の文章を入力します。 > DynamoDB にデータを保存する Lambda 関数を作成して 応答は次のようになりました。 > DynamoDBにデータを保存するLambda関数を作成します。 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: /home/ubuntu/q/lambda_dynamodb_jp.py + 1: import json + 2: import boto3 + 3: + 4: dynamodb = boto3.resource('dynamodb') + 5: table = dynamodb.Table('your-table-name') + 6: + 7: def lambda_handler(event, context): + 8: try: + 9: # 入力データの検証 + 10: if not event or not isinstance(event, dict): + 11: return { + 12: 'statusCode': 400, + 13: 'body': json.dumps('無効な入力: イベントは空でないオブジェクトである必要があります', ensure_ascii=False) + 14: } + 15: + 16: # DynamoDBにデータを保存 + 17: table.put_item(Item=event) + 18: + 19: return { + 20: 'statusCode': 200, + 21: 'body': json.dumps('データが正常に保存されました', ensure_ascii=False) + 22: } + 23: except Exception as e: + 24: return { + 25: 'statusCode': 500, + 26: 'body': json.dumps(f'エラー: {str(e)}', ensure_ascii=False) + 27: } ⋮ ↳ Purpose: DynamoDBにデータを保存するLambda関数を作成 Creating: /home/ubuntu/q/lambda_dynamodb_jp.py ⋮ ● Completed in 0.1s > Lambda関数を作成しました。主な機能: • boto3を使用してDynamoDBと連携 • イベントデータの入力検証 • DynamoDBテーブルへのデータ保存 • 日本語でのエラーメッセージ • 適切なHTTPステータスコードを返却 使用方法: 1. 'your-table-name'を実際のDynamoDBテーブル名に変更 2. Lambda実行ロールにdynamodb:PutItem権限を付与 3. 関数はイベントで渡されたJSONデータをDynamoDBに保存します 3. 対話で進めるプログラム開発体験 # Q Developer CLI( q )を使って、AIと自然言語で会話しながら、シンプルなアプリケーション(ここでは「テトリス」ゲーム)を一緒に作り上げる流れを紹介します。 $ q 🤖 You are chatting with claude-4-sonnet 3.1 シナリオ設定 # 今回作成するテトリスの要件は以下のとおりです。 ①:通常のテトリスが備えている操作は一通り持っている ②:単なるブロックではなく、動物のアイコン(例えば猫)を使う ③:猫アイコンの表情も一通りではなく、ブロックが積まれてくると、猫アイコンの表情を変える ④:格納場所はAWSのS3として、グローバルでアクセスできるように設定する 3.2 Q Developerとの対話開始 # 指示:(概要+詳細(上記の要件の①と②)) テトリスを作ってください。 テトリスが備えているであろう普通の機能(ブロックの回転、位置決め、落下など)を備えてください。 ブロックは通常のブロックではなく、猫のアイコン画像(画像の選定は任せます)を使ってください。 応答: > 猫のアイコンを使ったテトリスゲームを作成します: 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: /home/ubuntu/q/cat-tetris.html + 1: <!DOCTYPE html> + 2: <html> + 3: <head> + 4: <meta charset="UTF-8"> + 5: <title>猫テトリス</title> + 6: <style> + 7: body { margin: 0; padding: 20px; background: #222; color: white; font-family: Arial; text-align: center; } + 8: canvas { border: 2px solid #fff; background: #000; } + 9: .score { font-size: 20px; margin: 10px; } + 10: .controls { margin: 10px; font-size: 14px; } + 11: </style> + 12: </head> ~~~(中略)~~~ + 239: // Start game + 240: updateScore(); + 241: requestAnimationFrame(gameLoop); + 242: </script> + 243: </body> + 244: </html> ⋮ ↳ Purpose: Create cat tetris game with standard tetris features Creating: /home/ubuntu/q/cat-tetris.html ⋮ ● Completed in 0.1s > 猫のアイコンを使ったテトリスゲームを作成しました! 主な機能: • **ブロック回転**: ↑キーで回転 • **左右移動**: ←→キーで移動 • **高速落下**: ↓キーで高速落下 • **ハードドロップ**: スペースキーで瞬間落下 • **ライン消去**: 横一列が揃うと消去 • **スコアシステム**: ライン消去でスコア獲得 • **レベルアップ**: スコアに応じて落下速度が上がる • **猫のアイコン**: 7種類の猫の絵文字をランダム使用 操作方法: • ←→: 左右移動 • ↓: 高速落下 • ↑: 回転 • スペース: ハードドロップ ファイルを開いてブラウザでプレイできます! どうやら「 /home/ubuntu/q/cat-tetris.html 」が成果物のようです。 (出力を明確に指定していなかったので、Qが任意の保存場所とファイル名を用意したようです) HTMLファイルを開いてみます。 猫テトリスが動いています。 猫アイコンもそれぞれ表情が違っていて面白いです。 しかし、最初から猫が泣いていたりして、法則性が無いように思います。 どのように猫の表情を変化させているのかを確認してみます。 指示: > 猫アイコンはどういうロジックで変化しているのですか? 応答: > 猫アイコンの変化ロジックは以下の通りです: 1. ピース生成時にランダム決定: javascript this.catType = Math.floor(Math.random() * CAT_EMOJIS.length); 新しいテトリスピースが生成される際に、7種類の猫絵文字(🐱😺😸😹😻🙀😿)からランダムに1つ選択されます。 2. ピース全体で同じ猫: 各テトリスピース(I、O、T、S、Z、J、L)は生成時に決まった1種類の猫アイコンで統一されます。 ~~~(中略)~~~ 4. 表示時の変換: javascript drawBlock(x, y, board[y][x] - 1); 描画時に-1して元の猫タイプインデックスに戻します。 つまり: 各テトリスピースごとに猫の種類がランダムに決まり、そのピース内のすべてのブロックは同じ猫アイコンになります。ピー スが異なれば猫の種類も変わる可能性があります。 質問を投げかけると、このように応答します。 ただ、ランダムだと面白くないので、さらに制約(詳細)を追加していきます。 指示:(要件:③) ゲームの序盤では余裕の猫で、ブロックが高く積みあがってくるにしたがって、余裕のない表情の猫にしてください。 応答: > ゲームの状況に応じて猫の表情が変わるように修正します: 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: cat-tetris.html - 29 : // 猫の絵文字を使用 - 30 : const CAT_EMOJIS = ['🐱', '😺', '😸', '😹', '😻', '🙀', '😿']; + 29: // 猫の絵文字を使用(余裕→緊張の順) + 30: const CAT_EMOJIS = ['😻', '😺', '😸', '🐱', '😹', '🙀', '😿']; ~~~(中略)~~~ > 修正完了しました! 新しい猫の表情システム: • **😻 (余裕)**: ブロックが低い位置にある時 • **😺 (まだ余裕)**: 少し積み上がってきた時 • **😸 (少し心配)**: 中程度の高さ • **🐱 (普通)**: 半分くらいの高さ • **😹 (緊張)**: かなり高く積み上がった時 • **🙀 (焦り)**: 危険な高さ • **😿 (絶望)**: ゲームオーバー寸前 ブロックが高く積み上がるほど、猫の表情が余裕から絶望へと変化します。ゲームの緊張感が猫の表情で表現されるようになりました! 実行して制約を確認します。 以下が実行結果です。 今度は余裕が無くなってくる(ブロックが積み上がる)と、猫アイコンの表情が(余裕→絶望)に変わっていくのがわかります。 ただ、ゲームを少しプレイしていると以下のバグが見つかりました。 横4つつながったブロックが上↑キーで回転しない。 2回目以降のゲーム再開で、最初から絶望や焦りの表情をした猫ブロックが登場する。 バグの修正を「日本語」で指示します。 指示:(バグ修正依頼) バグがあります。修正してください。バグは以下です。 1.横4つつながったブロックが上↑キーで回転しない。 2.2回目のゲームで、最初から絶望や焦りの猫の表情のブロックが登場する。 応答: > バグを修正します: 1. I字ピース(横4つ)の回転バグ修正: 🛠️ Using tool: fs_write (trusted) ⋮ ● Path: cat-tetris.html 39, 39: const PIECES = [ 40, 40: // I piece - 41 : [[[1,1,1,1]]], + 41: [[[1,1,1,1]], [[1],[1],[1],[1]]], ⋮ ↳ Purpose: Fix I-piece rotation by adding vertical rotation state Updating: cat-tetris.html ⋮ ● Completed in 0.0s > 2. ゲームリセット時のボード初期化バグ修正: 🛠️ Using tool: fs_read (trusted) ⋮ ● Searching: /home/ubuntu/q/cat-tetris.html for pattern: ゲームオーバー ✓ Found 1 matches for pattern 'ゲームオーバー' in /home/ubuntu/q/cat-tetris.html ⋮ ● Completed in 0.0s 🛠️ Using tool: fs_read (trusted) ⋮ ● Reading file: /home/ubuntu/q/cat-tetris.html, from line 224 to 234 ✓ Successfully read 464 bytes from /home/ubuntu/q/cat-tetris.html ⋮ ~~~(中略)~~~ ⋮ ↳ Purpose: Fix board reset to ensure proper initialization Updating: cat-tetris.html ⋮ ● Completed in 0.0s > 修正完了しました! 修正内容: 1. I字ピースの回転: 横4つの状態と縦4つの状態を追加し、↑キーで回転できるようになりました 2. ゲームリセット: ボードを完全に0で初期化し、新しいピースを生成するようにしたので、2回目のゲームでも正しく余裕のある猫から始まります これでバグが解消され、正常にゲームが動作するはずです! 修正版のゲームを再度実行します。 回転できなかった横一列のブロックも回転できるようになっています。 また、ゲーム再開時の猫アイコンも正しく表示されるようになっていました。 3.3 AWS S3にアップロード # 出来上がったテトリスアプリケーションはHTMLファイルが一つだけの単純なものです。 このアプリケーションをAWS S3にアップロードしてみます。 (AWS S3へのアクセス権を持っていることが条件です) 指示:(要件:④) ゲームをAWS S3にアップロードして。 アップロード先は新しいバケットを作成して。 応答: > 新しいバケットを作成して猫テトリスをアップロードしますね。 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: aws s3 mb s3://cat-tetris-game-$(date +%s) ⋮ ↳ Purpose: 新しいS3バケットを作成 make_bucket: cat-tetris-game-<バケットID> ⋮ ● Completed in 1.573s 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: aws s3 cp /home/ubuntu/cat-tetris.html s3://cat-tetris-game-<バケットID>/cat-tetris.html --content-type text/html ⋮ ↳ Purpose: 猫テトリスファイルを新しいバケットにアップロード upload: ./cat-tetris.html to s3://cat-tetris-game-<バケットID>/cat-tetris.html ⋮ ● Completed in 0.655s > 完了しました!新しいバケット cat-tetris-game-<バケットID> を作成して、猫テトリスファイルをアップロードしました。 アップロードが完了したようですが、アクセス方法がわかりません。 最終的にどのようにアクセスしたいかを伝えていませんでした。 Q Developerに<最終成果物のイメージ>を伝えます。 指示: URLでアクセスしたいので、URLを教えて 応答:(バケットIDは実際に出力されたものから変更しています) > 現在はS3バケットがプライベートなので、Webからアクセスできません。パブリックアクセスを有効にする必要があります。 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: aws s3api put-public-access-block --bucket cat-tetris-game-<バケットID> --public-access-block-configuration "BlockPublicAcls=false,IgnorePublicAcls=false,BlockPublicPolicy=false,RestrictPublicBuckets=false" ⋮ ↳ Purpose: バケットのパブリックアクセスブロックを無効化 ⋮ ● Completed in 0.989s 🛠️ Using tool: execute_bash (trusted) ⋮ ● I will run the following shell command: aws s3api put-bucket-policy --bucket cat-tetris-game-<バケットID> --policy '{ "Version": "2012-10-17", "Statement": [ { "Sid": "PublicReadGetObject", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::cat-tetris-game-<バケットID>/*" } ] }' ⋮ ↳ Purpose: バケットポリシーでパブリック読み取りを許可 ⋮ ● Completed in 1.112s > これで以下のURLでアクセスできます: https://cat-tetris-game-<バケットID>.s3.amazonaws.com/cat-tetris.html ブラウザでこのURLを開けば猫テトリスがプレイできます! 指定されたURLを開きます。 ローカルで実行したものと同じテトリスを、AWS S3上で実行することが出来ました。 4. 指示の出し方の基本パターン # AIとの対話で成果を出すには、 「どのように伝えるか」 が最も重要です。 Q Developerは自然言語を理解しますが、 明確で構造的な指示 ほど精度の高いコードを生成します。 4.1 指示の基本構造 # AIにタスクを伝えるときは、次の3段階を意識します。 段階 内容 例 ① ゴール 何を達成したいか 「DynamoDBにデータを保存するAPIを作りたい」 ② 制約 技術的な条件・使用技術 「Python 3.11で、Lambda関数として実装して」 ③ 期待結果 出力してほしい成果物の形 「ハンドラー関数とテストコードを生成して」 この3要素を順番に伝えるだけで、出力の再現性と品質が大きく向上します。 4.2 指示の粒度を使い分ける # Q Developerへの指示は、大きく「 部分指示 」と「 全体指示 」の2種類があります。 種類 対象範囲 使い方の例 部分指示 関数・モジュールなど局所的な修正 「この関数をリファクタして、例外処理を標準化して」 全体指示 プロジェクト全体や設計方針 「全APIのレスポンス形式を統一して、共通エラーハンドラを追加して」 部分指示では即時的な改善を狙い、全体指示では設計方針の整合性を図ります。 この2つを組み合わせることで、AIに 意図の上下関係 を理解してもらうことができます。 4.3 フィードバックを循環させる # AIが出した結果に対して「そのまま使う」のではなく、 誤解 → 修正 → 再生成 のサイクルを回すことが重要です。 例: AI:Lambda関数を作成しました。DynamoDBに書き込みます。 ↓ 開発者:このコードではテーブル名が固定なので、環境変数から取得するようにして。 ↓ AI:了解しました。修正したコードを以下に示します。 このように、AIとの対話を「レビューの往復」として扱うと、より高品質なコードを一緒に育てることができます。 4.4 良い指示と悪い指示の比較 # 指示の例 AIの反応 評価 「DynamoDB対応のLambda作って」 動くが、スキーマや例外処理が曖昧 ❌ 不明確 「DynamoDBにタスクを登録するLambda関数を作成。titleとstatusを受け取り、statusの初期値はpendingにして」 適切なハンドラーとバリデーションを自動生成 ✅ 明確 「関数を改善して」 どこをどう直すか分からない ❌ 抽象的 「この関数のエラーハンドリングを追加し、ログに例外内容を出力して」 コードの修正意図を正確に反映 ✅ 具体的 「短い指示」よりも「明確な文脈を含む指示」が結果を良くします。 4.5 応用:AIをレビューアとして使う # AIに “設計者” や “品質管理者” の視点を持たせることも可能です。 たとえば次のような指示が有効です。 このコードの保守性とテスト容易性の観点から、改善点を3つ挙げて。 または、 このAPI設計をREST原則に照らしてレビューして。 これにより、Q Developerは単なる生成エンジンから「レビューAI」として機能し、開発者の品質意識を高めるパートナーになります。 まとめ # 指示は「 ゴール → 制約 → 期待結果 」の順に伝える 粒度(部分/全体)を意識して構造的に指示する フィードバックを繰り返して精度を上げる 最小構成で動かしながら、 AIとの会話 を通じて開発プロセスを理解する Q Developerは「AIがコードを書くツール」ではなく、「 AIと共に設計・実装・品質を磨くためのプラットフォーム 」です。 皆さまの生成AI活用の参考になれば幸いです。 img { border: 1px gray solid; }
はじめに # 社内プロジェクトの 営業支援システム(Sales Support System、以下、SSS) 開発で導入した Webhook のイベントキューイングの Terraform での構築手順を紹介します。 背景 # SSS ではワークフローを提供する SaaS と稟議の進捗イベントを Webhook 連携することでデータのステータス管理をしています。 初期の段階では優先度や工数の制約により、直接呼び出しで運用が開始されました。 しかし、以下にあげる事情により、イベントのキューイングを導入することにしました。 想定していた機能開発が完了し、先送りしていた機能改善に着手する工数ができた。 優先度や頻度の多い他のエラーが解消されて優先度が上位になった。 運用リカバリで、ただでさえ少ない工数なのに手動データパッチの手間やワークフローの再申請などの利用者の負担となることも。 [1] 運用保守向けの補足的な機能で、技術的な選択の自由度が高い。 キューイング機能に対する要件 # 実際に SaaS 連携イベントのキューイングを導入するに当たり、以下のような要件を満たすべく、いくつかの AWS サービスを比較検討しました。 メッセージを取りこぼさないでほしい。 順番を保証してほしい。 順序を保証してほしいイベントは状態が遷移しないと次のイベントが出せないので実質的な問題は発生しないが仕組みとして保証できればしたい。 受信失敗したときにメッセージが残っていてほしい。 失敗したメッセージを簡単に再送出来るとなお良し。 ECS の SSS サービスとは独立させたい。 独立していないとリプレースで ECS サービス停止中に同じ問題が発生してしまう。 既存の SSS サービスへの修正ができるだけ少ない方が良い。 追加機能だけが依存するのがベスト。 どうせならサーバレスなサービスを利用したい。 機能比較/検討 # 以上の要件を踏まえて機能比較表を作成して評価しました。 [2] 本当は重みがありそうですが、ポイントは単純に〇(2)、△ と?(1)、×(0)で換算しています。 案 サービス タイプ 順序 exactly-once サーバレス API GW 統合 [3] 送信失敗時 振り分け ポイント 備考 1 SQS 標準 × × 〇 〇 DLQ Lambda 4 2 SQS FIFO 〇 〇 〇 〇 DLQ Lambda 8 3 SNS 標準 × × 〇 ×(Lambda) ? SNS 4(3-5) 4 SNS FIFO 〇 〇 〇 ×(Lambda) ? SNS 6(5-7) 5 Kinesis DataStream 〇 ? 〇 〇 ? Lambda 7(5-9) 6 SNS+SQS FIFO+FIFO 〇 〇 〇 ×(Lambda) DLQ SNS 6 7 SQS+SNS FIFO+FIFO 〇 〇 〇 〇 DLQ Lambda 8 なお、DLQ(Dead Letter Queue)は正常に処理できなかったメッセージを一時的に保存するための特別なメッセージキューのことです。 --> SNS の送信失敗 当時は見つけられなかったのか、比較表では SNS の送信失敗時は「?」となっていますが、SNS も DLQ があるようです。 Amazon SNS デッドレターキュー - Amazon Simple Notification Service 実体は SQS の DLQ に連携するらしいですが。 比較表からポイントで単純に絞り込んで案 2 か案 7 のいずれか。 1 ポイント差の案 5 も惹かれるけど「イベントストリーム」というほどデータは来ないので廃案。 案 7 は振り分けに SNS が使えないかと考えたがキュー自体を分けるか結局 Lambda を利用する必要があったので組み合わせのメリットがなくなったため廃案。 大したデータ量と頻度もないのに複数のキューに分けて管理とかしたくないのも理由。 1 つのキューにすると結局は Lambda で振り分けになる。 これだと SNS 意味がないのでは? 案 2 に無駄に SNS が追加されただけになる。 案 2 はメッセージグループ ID で Lambda が振り分け。 メッセージグループ ID は API Gateway との統合で設定可能(ルート(URL パス)ごとにできる)。 以上の検討の結果、以下の AWS サービス構成と呼び出しフローとすることになりました。 補足事項 # SQS のイベント監視の Lambda のポーリングは実体がそうなっているだけで実装するわけではない。 イベントソースとして SQS を指定するだけ。 直接 CloudMap を呼び出したかったが上手くいかなかった。 サービスディスカバリで CloudMap の登録サービスの取得まではいけたが、呼び出しが戻ってこないでタイムアウトする。 同じ URL で踏み台サーバから curl で呼び出したら出来たのに AWS Lambda からだとうまくいかなかった。 設定とかいろいろやれば行けるのかもしれないが、後日の課題とした。 当たり前だが、Amazon API Gateway 経由では行けたのでこちらの方式で対応することにした。 結局 Amazon API Gateway のパスがさらされたままだから、SSS サービスを直接呼べるように将来はしたいところ。 構築の前提事項 # 外部システムから既存システムの Webhook の呼び出しの間にキューを差し込む形になるため、以下が前提となっています。 IaC で Sales Support System のインフラ構築 で紹介した API Gateway 経由で ECS サービスを呼び出すシステムが既に構築されていること。 ECS サービスは Webhook 用の API が公開されていること。 この記事では 2 つ目の前提の代替として AWS Lambda の統合を利用するものとします。 次章から具体的な実装について説明していきます。 メッセージキュー # まずは 以下の 2 つの AWS SQS の作成をしていきます。 Webhook 用メッセージキュー DLQ Webhook 用メッセージキュー # メインとなる Webhook 用のメッセージキューの作成です。 AWS SQS では 2 種類のキューがありますが、今回は FIFO キューを利用します。 fifo_queue を true にしていますが FIFO の場合はキュー名のサフィックスが .filo でなければなりません。 また、DLQ を利用するため、関連付け( deadLetterTargetArn )が必要となります。 他にはメッセージの重複判定をコンテンツベースにするのと可視性タイムアウト(処理中に他からメッセージが見えなくなる時間)を設定しています。 main.tf resource "aws_sqs_queue" "webhook_queue" { name = "${local.webhook_queue_name}.fifo" fifo_queue = true content_based_deduplication = true visibility_timeout_seconds = local.processing_timeout redrive_policy = jsonencode({ deadLetterTargetArn = aws_sqs_queue.webhook_dlq.arn maxReceiveCount = var.webhook.max_receive_count }) } DLQ # 次に DLQ の作成になります。 メインのメッセージキューよりシンプルに定義できます。 キュー名についてはメインのメッセージキューと同様に .fifo サフィックスが必要です。 リカバリ処理で失敗したメッセージ内容の確認をするまでの時間を調整するため、保持期間( message_retention_seconds 、デフォルトは 4 日間)を外部変数で指定しています。 main.tf # DLQ resource "aws_sqs_queue" "webhook_dlq" { name = "${local.webhook_queue_name}-dlq.fifo" fifo_queue = true message_retention_seconds = var.webhook.dlq_retention_second } キューイング用 Webhook API # 今回は SSS 同様に API Gateway は既存のものがある前提となるため、新たに SQS が受けるための設定を API Gateway へ追加することになります。 具体的には以下のものになります。 ルート 統合 なお、API Gateway 自体の構築については IaC で Sales Support System のインフラ構築 の記事を参照ください。 キューイング Webhook API に対するルート # 今回は JWT 認証しないため、以前のアプリケーション用のルートよりもシンプルになります。 ルートキーのパスは /sqs-hook としています。 HTTP メソッドは SSS で利用している SaaS の指定( POST )に合わせています。 なお、API Gateway の ID については既存の参照としてデータソースを利用しています。 API Gateway 自体も新規に作成する場合は通常の AWS リソースへの参照となります。 integration.tf resource "aws_apigatewayv2_route" "webhook_event_route" { api_id = data.aws_apigatewayv2_api.this.id route_key = "POST /sqs-hook" target = "integrations/${aws_apigatewayv2_integration.webhook_event_producer.id}" } SQS との統合 # 続いて API Gateway と SQS を関連付けるための統合を作成します。 integration_subtype として SQS-SendMessage を指定しています。 これによって SQS への送信用として統合されます。 更に request_parameters で以下の設定をします。 [4] キューイング Webhook API に対する URL(必須) メッセージグループ ID メッセージボディ(必須) integration.tf resource "aws_apigatewayv2_integration" "webhook_event_producer" { description = "Queue of Webhook Event" api_id = data.aws_apigatewayv2_api.this.id integration_type = "AWS_PROXY" integration_subtype = "SQS-SendMessage" credentials_arn = aws_iam_role.webhook_event_producer_role.arn request_parameters = { "QueueUrl" = aws_sqs_queue.webhook_queue.url "MessageGroupId" = local.message_group_id "MessageBody" = "$request.body" } } SQS 送信のための IAM ロール # API Gateway が SQS にメッセージ送信するための権限を付与するための IAM ロールを作成します。 API Gateway に対するロールなので信頼ポリシー( apigateway_assume_role )の principals に API Gateway を指定します。 付与するポリシーは SQS への送信のみのため actions として sqs:SendMessage のみを指定します。 これらを API Gateway 統合用の IAM ロールに関連付けます。 念の為ですが、 aws_iam_role_policies_exclusive も指定しておきます。 integration.tf data "aws_iam_policy_document" "apigateway_assume_role" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = ["apigateway.amazonaws.com"] } } } data "aws_iam_policy_document" "sqs_send_only_policy" { statement { actions = ["sqs:SendMessage"] resources = ["${aws_sqs_queue.webhook_queue.arn}"] } } resource "aws_iam_role" "webhook_event_producer_role" { name = "${local.prefix}-webhook-event-producer-role" assume_role_policy = data.aws_iam_policy_document.apigateway_assume_role.json } resource "aws_iam_role_policy" "sqs_integration_access_policy" { name = "sqs-integration-access-policy" role = aws_iam_role.webhook_event_producer_role.id policy = data.aws_iam_policy_document.sqs_send_only_policy.json } resource "aws_iam_role_policies_exclusive" "webhook_event_producer_role_policies" { role_name = aws_iam_role.webhook_event_producer_role.name policy_names = [ aws_iam_role_policy.sqs_integration_access_policy.name ] } SQS Labmda トリガー # SQS の準備ができたので、SQS からメッセージを受け取ってアプリケーションの Webhook に送信するための Lambda トリガーを作成します。 SQS Lambda トリガー用 Lambda 関数 # メッセージを受け取ったらアプリケーションの Webhook に送信するための AWS Lambda 関数を作成します。 SQS のトリガーとして AWS Lambda を関連付けるためにはキューの URL の環境変数と aws_lambda_event_source_mapping の定義が必要となります。 キューへの URL 指定は aws_lambda_function の環境変数で設定し、環境変数名は QUEUE_URL になります。 archive_file データソースなどの他の設定は通常の AWS Lambda と同様に行います。 設定の詳細は 本記事のリポジトリ のコードや Terraform のドキュメントを参照してください。 続いて aws_lambda_event_source_mapping を定義します。 イベントソースは当然ながら Webhook 用メッセージキューを指定します。 Lambda 関数も今回定義したトリガ用のものを指定します。 他にバッチサイズ(SSS は 1 つずつなので 1 )と同時処理最大数を設定しています。 main.tf resource "aws_lambda_function" "webhook_event_producer" { description = "Webhook Event Producer" function_name = local.webhook_event_producer_function_name handler = "${local.webhook_event_producer_module_name}.lambda_handler" filename = data.archive_file.webhook_event_producer.output_path source_code_hash = data.archive_file.webhook_event_producer.output_base64sha256 role = aws_iam_role.webhook_event_producer_execution_role.arn runtime = var.webhook.runtime architectures = ["arm64"] timeout = local.processing_timeout environment { variables = { QUEUE_URL = aws_sqs_queue.webhook_queue.url } } depends_on = [ aws_iam_role_policy_attachment.webhook_event_producer_basic_execution_role_attach, aws_cloudwatch_log_group.webhook_event_producer, ] } resource "aws_lambda_event_source_mapping" "webhook_event_producer_mapping" { event_source_arn = aws_sqs_queue.webhook_queue.arn function_name = aws_lambda_function.webhook_event_producer.function_name batch_size = 1 # 1つのメッセージごとに Lambda 関数を呼び出します scaling_config { maximum_concurrency = var.webhook.max_concurrency } } SQS Lambda トリガーのための IAM ロール # SQS Lambda トリガーは SQS からのメッセージを受信する権限のみを付与します。 システム構成図で SQS のイベント監視の AWS Lambda が SQS をポーリングしていましたが、ここにイベントソース処理実装の影響が出ています。 AWS Lambda のロジックには SQS のメッセージ受信処理はないのに、キューの確認やメッセージ受信、受信後のキューからのメッセージ削除などの権限が必要になっています。 main.tf data "aws_iam_policy_document" "sqs_receive_message_policy" { statement { actions = [ "sqs:ReceiveMessage", "sqs:ChangeMessageVisibility", "sqs:DeleteMessage", "sqs:GetQueueAttributes" ] resources = ["${aws_sqs_queue.webhook_queue.arn}"] } } resource "aws_iam_role" "webhook_event_producer_execution_role" { name = local.webhook_event_producer_execution_role_name assume_role_policy = data.aws_iam_policy_document.lambda_assume_role_policy.json } resource "aws_iam_role_policy" "sqs_receive_message_policy" { name = "sqs-receive-message-policy" role = aws_iam_role.webhook_event_producer_execution_role.id policy = data.aws_iam_policy_document.sqs_receive_message_policy.json } resource "aws_iam_role_policies_exclusive" "webhook_event_producer_execution_role_policies" { role_name = aws_iam_role.webhook_event_producer_execution_role.name policy_names = [ aws_iam_role_policy.sqs_receive_message_policy.name, ] } resource "aws_iam_role_policy_attachment" "webhook_event_producer_basic_execution_role_attach" { role = aws_iam_role.webhook_event_producer_execution_role.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } SQS Lambda トリガー関数 # Lambda 関数は Python で実装しています。 Python ファイル内には 3 つの関数が定義されています。 lambda_handler extract_data_from_event send_request lambda_handler 関数は AWS Lambda のエントリポイント関数で主処理になります。 まず、第 1 引数で渡されたイベントから extract_data_from_event で宛先とメッセージを取り出します。 次に、 send_request で取得した宛先とメッセージを API Gateway のアプリケーションの Webhook API に転送します。 extract_data_from_event はデータ構造 [5] をチェックしながら、メッセージグループ ID とメッセージ自体を取り出します。 イベントは Python では dict として扱うことができます。 メッセージグループ ID からアプリケーションの URL に変換して、その URL とメッセージ内容を返します。 send_request は引数で渡された元のアプリケーションの Webhook API の URL に対してメッセージを HTTP の POST メソッド で呼び出します。 API Gateway の呼び出しは普通に HTTP 通信すれば大丈夫です。 Python の Lambda 関数の実装に際して以下の点に注意してください。 データはエンコードする必要がある。 処理に失敗した(DLQ に入れる)場合は例外にする。 お行儀よく 4xx や 5xx のコードを返して正常終了にしていたら、DLQ にメッセージが転送されませんでした。 AWS Lambda 関数のコード詳細は 本記事のリポジトリ を参照ください。 データソース # API Gateway は IaC で Sales Support System のインフラ構築 で構築したものを取得します。 統合の定義などに API Gateway の ID が必要ですが、 aws_apigatewayv2_api データソースを直接使うと ID が必要になってしまうのでひと工夫しています。 data.tf data "aws_apigatewayv2_apis" "this" { protocol_type = "HTTP" name = var.apigw_name } data "aws_apigatewayv2_api" "this" { api_id = one(data.aws_apigatewayv2_apis.this.ids) } 最後に # SSS では定期リリースのおりに、作業前に通知しているにもかかわらず、SaaS からのメッセージを送る操作をしてしまうユーザがいました。 しかし、これで開発者もユーザもリリースのことを気にせずに作業できるようになりました。 キューイング機能をリリースしてから、実際に何度かリリース中に操作が行われてしまうことがありました。 ですが、DLQ にメッセージが保持されていたため、リリース後に再送することで、後続業務が支障なく進められました。 SSS アプリケーションサービスとは独立したキューとして作成することで、SSS と外部の SaaS との結合度を軽減することが出来ました。 更に、既存のシステムへの改修もなかったため、短期間での導入もできました。 また、DLQ からの再送も AWS 管理コンソールや AWS CLI の機能が使えたため、保守ツールの開発コストも抑えることも出来ました。 今回紹介した内容は IaC で Webhook イベントのキューイングを構築のリポジトリ からコードを入手可能です。 また、 IaC で Sales Support System のインフラ構築 のリポジトリコードと合わせることで、実際に動作させて確認することが出来ます。 運用工数削減に関しては他のエラー対応の改善(半自動化やチェック強化など)の一環でもあります。 ↩︎ 参考として AWS のメッセージングサービスの決定木を紹介している Decision Tree: choose the right AWS messaging service | Better Dev も参照。 ↩︎ AWS API Gateway V2 を使う場合(ECS 統合で利用しているため)。利用できない場合は Lambda 経由となるため Lambda の開発が追加になる。 ↩︎ request_paramters の項目は integration_subtype の値によって変わります。詳細は Integration subtype reference - Amazon API Gateway を参照。 ↩︎ イベントの具体的な構造は FIFO キューメッセージイベントの例 を参照。 ↩︎
はじめに # ちょっと時間が経ってしまいましたが、先月末 GitHub Copilot CLI がパブリックプレビューになりました。 https://github.blog/changelog/2025-09-25-github-copilot-cli-is-now-in-public-preview/ VS Code の拡張から始まった GitHub Copilot も Claude Code や Gemini と同様 CLI としても動作するようになりました。CLI として提供されることで、 IDE 縛りがなくなる パイプを使って他の CLI ツールとの連携が可能 という強みが追加されます。ターミナルだけで作業が完結できるという点にも魅力を感じる人は多いでしょう。 さらに GitHub にログイン状態で利用するため自分が関わっているリポジトリの操作もできますし、GitHub の MCP サーバーもすぐ利用ます。 日本語のドキュメントは以下から参照できます。 GitHub Copilot CLI の使用 - GitHub Docs --> Caution 従来 GitHub CLI の拡張として提供されていた gh-cpilot は Copilot CLI のパブリックプレビューに伴って非推奨となり、Copilot CLI に置き換えられます。 https://github.blog/changelog/2025-09-24-deprecate-github-copilot-extensions-github-apps/ gh-copilot については昨年2月紹介してました。 https://developer.mamezou-tech.com/blogs/2024/02/28/github-copilot-in-cli/ --> Information GitHub Copilot CLI は、GitHub Copilot Pro、GitHub Copilot Pro+、GitHub Copilot Business、GitHub Copilot Enterprise プランで使用できます。 インストールと起動 # ドキュメントに従ってインストールします。 https://docs.github.com/ja/copilot/how-tos/set-up/install-copilot-cli npm でグローバルインストール可能です。 npm install -g @github/copilot GitHub CLI を起動します。 copilot GitHub CLI の TUI が起動し GitHub へのログイン状態、GitHub MCP サーバへの接続状況が表示されます。 作業するフォルダの信頼について聞かれています。いつも $HOME/dev で作業してるので2番の Yes, and remember this folder for future sessions を選択しました。 --> Information 筆者の環境では GitHub CLI (Copilot CLI じゃない GitHub 操作用 CLI) で事前に GitHub にログインしているため、その認証トークンでログイン状態になっているのだと思います。 使ってみる # 使い方を理解する上では以下のページが役立ちます。 https://docs.github.com/ja/copilot/concepts/agents/about-copilot-cli 対話型モード # TUI 上でプロンプトを入力して対話的に作業を進めるモードです。 GitHub から clone した筆者が作っている Electron アプリのリポジトリ( sbe )のディレクトリに移動してから Copilot CLI を起動しました。 cd sbe copilot @ でファイルをメンションできます。 @ に続いてパスの一部を入力すると候補が列挙されます。 ソースコードの説明をしてもらいました。 日本語で質問すると日本語で回答してくれます。 対話モードを抜けると利用実績やコードの変更行数などが表示されます。 Total usage est: 2 Premium requests Total duration (API): 15.1s Total duration (wall): 12m 33.3s Total code changes: 0 lines added, 0 lines removed Usage by model: claude-sonnet-4.5 24.9k input, 414 output, 0 cache read, 0 cache write (Est. 2 Premium requests) Shutting down... プログラムモード # Copilot CLI に引数やパイプでプロンプトを与えて直接実行するモードです。 copilot -p "explain src/favs.js" TUI は起動せず直接プロンプトの結果が出力されます。 I'll read the src/favs.js file to explain it to you. ✓ Read src/favs.js (57 lines) This is a Vue.js 3 app that manages favorites in an Electron application. It creates a UI for displaying and deleting favorite items with these key features: **Core functionality**: Loads favorites from the Electron backend via `window.favsApi`, displays them in a list, and provides delete functionality with a confirmation dialog. It listens for window focus events to refresh the favorites list. **Theme support**: Automatically detects and applies light/dark mode based on system preferences using Vuetify's theming system. Total usage est: 1 Premium request Total duration (API): 11.5s Total duration (wall): 15.2s Total code changes: 0 lines added, 0 lines removed Usage by model: claude-sonnet-4.5 23.6k input, 239 output, 0 cache read, 0 cache write (Est. 1 Premium request) 結果は標準出力に出力され、続いて利用実績も表示されます。 ローカルタスク # ローカルにあるコードを変更するように指示できます。 まず、1つのソースコードのファイルについてのレビューを依頼し、改善ポイントを挙げてもらいました。 Review @src/about.js 良い点として、適切なライフサイクルフックを持つクリーンなコンポーネント構造とか、IPC による関心事の適切な分離などを誉めてくれています。 いくつか問題点を挙げてくれていますが、使用している Vue の beforeUnmount でフォーカス関連のリスナーを削除してないので、メモリリークの懸念があるというのが気になりました。 そこで、この問題を修正するように依頼。 Fix No cleanup problem beforeUnmount のフックメソッドと Listener 削除のコードが追加されました。リスナーを off で削除する API は存在しないため受け入れる変更ではありません。ですが、今は Copilot CLI の機能を試しているため、他のファイルにも同様に適用をお願いしてみました。ファイルごとに変更していいか聞かれます。 Apply this fix to other files too 全てのファイルに適用が終わりました。 --> Information この例では使用しませんでしたが、ローカルタスクにおいて sed とか chmod などの外部コマンドを使用する場合は、使用許可を聞いてきます。 --allow-tool で実行時に予め許可を与えることもできます。 GitHub タスク(issue 一覧取得) # GitHub の操作に関するタスクも実行できます。手始めにリポジトリの issue 一覧を表示させてみました。 list my open issues GitHub タスク(PR 作成) # 先ほど試した Electron アプリへのリスナー削除の変更が手元にあるので、そこから PR を作ってもらいます。 create a pull request from this changes ブランチを作って push までやってくれました。これをもとに PR を作るか確認が入りました。 Yes を選択すると PR が作成されました。 PR の作者は筆者自身となっています。 GitHub タスク(Actions ワークフロー実行) # GitHub タスクでは GitHub Actions ワークフローの操作も可能です。まず、このリポジトリのワークフローを列挙させてみました。 列挙されたワークフローのうち OS Matrix は、クロスプラットフォームで Electron アプリのテストを実行するワークフローです。手動実行時に beta というパラメータを true に設定して実行すると Electron の最新ベータ版をインストールしてテストします。ワークフローファイルは こちら から参照してください。 このワークフローの実行を指示してみました。 Run OS Matrix with input value "beta" to true (GitHub Copilot CLI ではなく) GitHub CLI を使って実行するプランを提示、実行についてのオプションを提示してきました。 1を選択すると、1回限りの実行を許可、2を選択するとこのセッションを通しての許可を与えることになります。1を選択すると無事に実行できたようです。 実際にちゃんと Electron のベータ版をインストールしてテストが実行されていました。 Web UI を使わなくても GitHUb CLI によるワークフロー実行方法を知らなくても、自然言語で指示すればいいので助かりますね。 さいごに # GitHub Copilot CLI は予想以上に強力な Copilot 協調環境を提供してくれていました。 プロンプトの複数行入力にも対応するなど、GA に向けて改善が進んでいます。 https://github.blog/changelog/2025-10-17-copilot-cli-multiline-input-new-mcp-enhancements-and-haiku-4-5/ MCP サーバーとの連携なども使いこなせれば、ターミナルだけで多くの複雑なタスクがこなせそうですね。
C#から7年ほど遠ざかり、久々にデベロッパーサイト向けにC#をやり出しました。そこでふと疑問が出てきました。最近のC#ではDIコンテナはどんなのがあるんだろうと。 以前やっていたときは、Castle WindsorやUnity(ゲーム制作ツールのUnityとは別物)、Seasarなどがありました(実は.NET用のSeasarなんてものがかつては存在しました)。 Castle Windsorのページ https://www.castleproject.org/projects/windsor/ .NET CoreになってからMicrosoft製のDIコンテナも登場しているようです。他にはAUTOFACというものやNinjectというものもあるようです。 AUTOFACのページ https://autofac.org/ Ninjectのページ http://www.ninject.org/ 色々ありますが、Microsoft製のものが一番とっつきやすいかなと思って使ってみたところ、本当にとっつきやすかったです。 そのためこの記事ではMicrosoft製のMicrosoft.Extensions.DependencyInjectionの使い方について、サンプルコード付きで解説します。 DIコンテナとは # あらためてDIコンテナとは何かについて確認します。 DI(Dependency Injection)コンテナはオブジェクトの生成、ライフサイクルの管理、依存関係の注入を自動化するライブラリです。コードを疎結合化し、修正やテストをしやすくします。 DIコンテナを使うメリットは主に以下です。 仕様変更などが発生しても、修正の手間を減らせる。 テスト時にはテスト用のクラス(モックと呼ばれる)に差し替えることで、テストをしやすくできる。 どの部品が他のどの部品を必要としているかが分かりやすいため、システムの構造が見通しやすい。 例えば以下のサンプルコードのように、オブジェクトの生成がハードコードされているとします。 public class Sample { private readonly SampleWriter _sampleWriter = new(); protected override SampleResult ExecuteSample() { return _sampleWriter.Write($"Execute sample at: {DateTimeOffset.Now}"); } } シンプルなコードなので気にならないと思いますが、 SampleWriter クラスを別のクラスで置き換えることを考えてみましょう。 すると SampleWriter クラスのオブジェクトを使っている個所を見直さなければいけなくなります(見直す範囲は仕様次第ですが)。 そこで登場するのがDIコンテナです。DIコンテナは例えるなら「必要なオブジェクトをまとめて提供してくれる万能な倉庫」です。 このインターフェイスにはこのクラスを代入してくださいという設定をDIコンテナに教えます。するとDIコンテナはその設定に基づいて必要なオブジェクトを自動的に作成し、渡してくれます。 例えば先ほどのサンプルコードは次のように修正できます。 public class Sample() { public Sample(ISampleWriter sampleWriter) { _sampleWriter = sampleWriter; } private readonly ISampleWriter _sampleWriter; protected override SampleResult ExecuteSample() { return _sampleWriter.Write($"Execute sample at: {DateTimeOffset.Now}"); } } コンストラクタの引数としてDIコンテナからオブジェクトを受け取ります。そして SampleWrite の型をインターフェイスとしています。 SampleWriter クラスを別のクラスで置き換えるにしても、インターフェイスを使って ISampleWriter としているので、DIコンテナの設定だけ変えればよくなります。 Microsoft.Extensions.DependencyInjectionとは # Microsoft.Extensions.DependencyInjectionはMicrosoft製のDIコンテナです。NuGetからインストールするだけですぐ使えます。またMicrosoft公式の記事も充実しています。 必要十分な機能を備えており、軽量でシンプルです。そして何より実際にやってみてコーディングが簡単でした。プロジェクト作成時に自動生成される Program.cs に少し追加するだけなのです。 Microsoft.Extensions.DependencyInjectionはC#で使うには導入のハードルが低いDIコンテナと言ってよいでしょう。 WebAppSample var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); builder.Services.AddTransient<ISampleProc, SamplePoc>(); var app = builder.Build(); 主要な概念 # サービス # サービスは依存関係として注入するインスタンスのことです。 例えば IsampleProc というインターフェイスに SampleProc というクラスのインスタンスを注入するよう設定した場合、 SampleProc というクラスのインスタンスがサービスに該当します。 サービスには以下の3つのライフサイクルがあります。 種類 概要 Transient 一時的という意味。 サービスが要求されるたびに、新しいインスタンスが生成される。 一時的な操作を行うサービスや状態を持つべきでないサービスに適している。 Scoped 特定のスコープ内でインスタンスが1つだけ生成される。 例えばWebアプリにおけるHTTPリクエスト(リクエストスコープ)や アプリケーション全体(アプリケーションスコープ)。 Singleton アプリケーション全体でインスタンスが1つだけ生成される。 コンテナ # ISampleProc が要求されたら SampleProc のインスタンスを渡すというインターフェイスとオブジェクトの紐付けや、そのスコープを登録しておくものがコンテナです。 サービスプロバイダー # サービスプロバイダーはコンテナに登録された内容に基づいて依存関係の解決を行います。 あるクラスのオブジェクトが生成されるとき、そのクラスに依存関係の注入が必要なインターフェイスがあったら、オブジェクトを生成して注入します。 言葉だと抽象的なので、コードで見てみましょう。 DiSample というクラスのオブジェクトをサービスプロバイダーが生成するケースを考えます。 このクラスには ISampleProc があります。サービスプロバイダーが DiSample を生成したとき、コンストラクタに ISampleProc があるのを見て SampleProc も生成してくれるのです。 DiSample public class DiSample { private ISampleProc _sampleProc; // コンストラクタでインジェクション public DiSample(ISampleProc sampleProc) { _sampleProc = sampleProc; } } さらにサービスプロバイダーは依存関係を連鎖解決してくれます。 SampleProc に IDbConnection がある場合を考えてみましょう。 DiSample public class SampleProc { private IDbConnection _dbConnection; // コンストラクタでインジェクション public SampleProc(IDbConnection dbConnection) { _dbConnection = dbConnection; } } サービスプロバイダーが DiSample を生成すると、先ほど書いた通り ISampleProc があるのを見て SampleProc も生成が必要だと判断します。 すると次は SampleProc に IDbConnection があるのを見て、 DbConnection の生成が必要だと判断します。 こうしてサービスプロバイダーは連鎖解決してオブジェクトを生成してくれます。なんて便利なのでしょう。 サービスの登録方法 # AddSingleton # ライフサイクルをSingletonにしてサービスを登録するには、下記のように記述します。 SingletonSample services.AddSingleton<ISampleProc, SampleProc>(); AddScoped # ライフサイクルをScopedにしてサービスを登録するには、下記のように記述します。 ScopedSample services.AddScoped<ISampleProc, SampleProc>(); AddTransient # ライフサイクルをTransientにしてサービスを登録するには、下記のように記述します。 TransientSample services.AddTransient<ISampleProc, SampleProc>(); 複数のオブジェクトを登録する方法 # Microsoft.Extensions.DependencyInjectionは1つのインターフェイスに対して複数のオブジェクトを登録できます。その場合は後から追加した設定で上書きされ、最後に追加された設定が使われます。 ただし IEnumerable<{SERVICE}> を使って解決すれば、登録したオブジェクトすべてを生成できます。 サンプルコードを見てみましょう。まずは ISampleProc に注入するオブジェクトを2つ登録します。 DiEnumerable var builder = WebApplication.CreateBuilder(args); builder.Services.AddControllersWithViews(); builder.Services.AddTransient<ISampleProc, SampleProc>(); builder.Services.AddTransient<ISampleProc, SampleProcess>(); var app = builder.Build(); ResolveSample public class ResolveSample { public ResolveSample(ISampleProc sampleProc) { // この場合はSampleProcessのオブジェクトが渡される } } ResolveSampleEnumerable public class ResolveSample { public ResolveSample(IEnumerable<ISampleProc> sampleProcs) { // この場合はIEnumerableにSampleProcとSampleProcessのオブジェクトが入って渡される // つまり値が2つあるコレクションとして渡される } } サンプルコードで実践しつつ解説 # サービスとして登録するインターフェイスとクラス # まずはサービスとして登録するインターフェイスとクラスのサンプルコードを提示します。 1つのインターフェイスに対して、インジェクションするオブジェクトのクラスを変えることで、Hello WorldとMorning Worldの表示を切り替えます。また IEnumerable を使って1つのインターフェイスに複数のクラスを登録し、利用するサンプルも掲載します。 以下はHello WorldとMorning Worldを表示するためのインターフェイスとクラスのサンプルコードです。 namespace が DIConsoleApp になっていますが、プロジェクト名やディレクトリ名に合わせてください。 IMessageCreator.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DIConsoleApp { public interface IMessageCreator { string CreateMessage(); } } MessageCreatorHello.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DIConsoleApp { internal class MessageCreatorHello : IMessageCreator { public string CreateMessage() { return "Hello, World!"; } } } MessageCreatorMorning.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DIConsoleApp { internal class MessageCreatorMorning : IMessageCreator { public string CreateMessage() { return "Morning, World!"; } } } 続いてこれらのクラスをコンストラクタからインジェクションするサンプルコードを掲載します。 ISampleProc.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DIConsoleApp { internal interface ISampleProc { void DisplayMessage(); } } SampleProc.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DIConsoleApp { internal class SampleProc : ISampleProc { private IMessageCreator _messageCreator; public SampleProc(IMessageCreator messageCreator) { _messageCreator = messageCreator; } public void DisplayMessage() { Console.WriteLine(_messageCreator.CreateMessage()); } } } こちらは1つのインターフェイスに複数のクラスが登録されている場合に、複数のクラスのオブジェクトを取得するサンプルコードです。 SampleProcEnumerable.cs using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace DIConsoleApp { internal class SampleProcEnumerable : ISampleProc { private IMessageCreator _messageCreator; public SampleProcEnumerable(IEnumerable<IMessageCreator> messageCreators) { _messageCreator = messageCreators.ToArray()[0]; } public void DisplayMessage() { Console.WriteLine(_messageCreator.CreateMessage()); } } } コンソールアプリ # まずはコンソールアプリでDIを試してみましょう。理由はシンプルなものから見ていった方が理解しやすいからです。 コンソールアプリプロジェクトを作ってください。そしてインターフェイスやクラスを作成し、先ほど掲載したサンプルコードをコピペしてください。 それができたら Program.cs にDI設定を記述します。 Program.cs using Microsoft.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; using DIConsoleApp; // ビルダーの作成 HostApplicationBuilder builder = Host.CreateApplicationBuilder(args); // サービスの登録 builder.Services.AddTransient<ISampleProc, SampleProc>(); builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>(); // ホストの構築 IHost host = builder.Build(); // サービスの取得と使用 ISampleProc sampleProc = host.Services.GetRequiredService<ISampleProc>(); sampleProc.DisplayMessage(); サービス登録の個所を以下のように、 IEnumerable を使わない方のクラスにして実行してみましょう。 Program.cs(一部抜粋) // サービスの登録 builder.Services.AddTransient<ISampleProc, SampleProc>(); //builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>(); //builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>(); コンソールにHello Worldが表示されればOKです。 これを以下のようにMorningWorld用のクラスに変えて実行してみましょう。 Program.cs(一部抜粋) // サービスの登録 builder.Services.AddTransient<ISampleProc, SampleProc>(); //builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>(); //builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>(); 今度はMorning Worldが表示されます。 それではHello WorldのクラスもMorning Worldのクラスも両方とも登録してみましょう。 Program.cs(一部抜粋) // サービスの登録 builder.Services.AddTransient<ISampleProc, SampleProc>(); //builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>(); この場合は後勝ちとなってMorning Worldが表示されます。 その次は IEnumerable を試してみましょう。コードを次のように変えて実行します。 Program.cs(一部抜粋) // サービスの登録 //builder.Services.AddTransient<ISampleProc, SampleProc>(); builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>(); SampleProcEnumerable では、以下のようにインデックスが0のオブジェクトを使うようになっています。そのため先に登録された MessageCreatorHello のオブジェクトがインジェクションされます。インデックスを1にすれば2番目に登録されたクラスのオブジェクトがインジェクションされます。 SampleProcEnumerable.cs(一部抜粋) public SampleProcEnumerable(IEnumerable<IMessageCreator> messageCreators) { _messageCreator = messageCreators.ToArray()[0]; } Webアプリ # WebアプリのDI設定 今度はWebアプリで試してみましょう。やっぱり現実的にはWebアプリのプロジェクトが多いでしょうから、Webアプリでの使い方を知っておきたいところです。 この記事ではRazorページを使って解説していきます。Razorとは何かについてはこちらの記事を参照してください。 C#とRazorで始める効率的なWeb開発!サンプルコード付きで徹底解説 Razorページアプリプロジェクトを作ってください。そしてインターフェイスやクラスを作成し、先ほど掲載したサンプルコードをコピペしてください。 そしたら Program.cs を開いてみてください。次のようになっています。 Program.cs var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorPages(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.Run(); なんとRazorページアプリプロジェクトの Program.cs には、最初からMicrosoft.Extensions.DependencyInjectionで使うビルダーが記述されているのです。RazorページがDI設定同様にサービスとして登録されているのです。 ここがMicrosoft.Extensions.DependencyInjectionの導入のしやすさなのでしょう。仕組みがRazorページのようなC#でよく使う技術と共通化されているわけですね。 Razorページの登録前にDI設定を記述します。サンプルコードは以下です。コメントで「サービスの登録」と記述した個所が該当します。 Program.cs using DIConsoleApp; var builder = WebApplication.CreateBuilder(args); // サービスの登録 builder.Services.AddTransient<ISampleProc, SampleProc>(); builder.Services.AddTransient<ISampleProc, SampleProcEnumerable>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorHello>(); builder.Services.AddTransient<IMessageCreator, MessageCreatorMorning>(); // Add services to the container. builder.Services.AddRazorPages(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthorization(); app.MapRazorPages(); app.Run(); Webアプリの動作確認 Webアプリの場合は確認用の画面を作る必要もあります。 Index を修正し、 SampleProc と SampleProcEnumerable というRazorページを作ってください。ページ遷移のイメージはこの画像のようになります。 Webアプリのサンプルのページ遷移 サンプルコードを掲載します。まずは Index.cshtml に以下のようにアンカータグを2ページ分追加します。 Index.cshtml <div class="text-center"> <h1 class="display-4">Welcome</h1> <p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p> <p><a href="/SampleProc">SampleProc</a></p> <p><a href="/SampleProcEnumerable">SampleProcEnumerable</a></p> </div> そしたら SampleProc (後勝ち用のページ)と SampleProcEnumerable ( IEnumerable 用のページ)を作ります。まずは SampleProc のサンプルコードを掲載します。 SampleProc.cshtml @page @model DIWebApp.Pages.SampleProcModel @{ } <h2>SampleProc</h2> <p>@Model.DisplayMessage()</p> SampleProc.cshtml.cs using DIWebApp; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace DIWebApp.Pages { public class SampleProcModel : PageModel { private IMessageCreator _messageCreator; public SampleProcModel(IMessageCreator messageCreator) { _messageCreator = messageCreator; } public string DisplayMessage() { return _messageCreator.CreateMessage(); } public void OnGet() { } } } 続いて SampleProcEnumerable ( IEnumerable を使うページ)のサンプルコードを掲載します。 SampleProcEnumerable.cshtml @page @model DIWebApp.Pages.SampleProcEnumerableModel @{ } <h2>SampleProcEnumerable</h2> <p>@Model.DisplayMessage()</p> SampleProcEnumerable.cshtml.cs using DIWebApp; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace DIWebApp.Pages { public class SampleProcEnumerableModel : PageModel { private IMessageCreator _messageCreator; public SampleProcEnumerableModel(IEnumerable<IMessageCreator> messageCreatora) { _messageCreator = messageCreatora.ToArray()[0]; } public string DisplayMessage() { return _messageCreator.CreateMessage(); } public void OnGet() { } } } デバッグ起動すると最初にIndex画面が表示されます。 そしたら SampleProc と SampleProcEnumerable にアクセスして、先ほどのコンソールアプリ同様に、後勝ちであることや IEnumerable について実行して確認してみてください。 おわりに # 私はC#にはLINQやRazorなどとても便利な技術があるのに、DIコンテナはいまいちだなぁと感じていました。 しかし今回Microsoft.Extensions.DependencyInjectionを使ってみて、C#にも簡単に扱えるDIコンテナがあるんだと知りました。 かつて私がCastle Windsorを使ったときは、.NET MVCで今の Program.cs に該当するクラスにもっと複雑なコードを書いていました。そしてXMLに冗長なDI設定を書いていました。 それと比べるとMicrosoft.Extensions.DependencyInjectionは書くべき個所が明確ですし、書き方も簡単ですね。これなら導入のハードルは低いです。 もしC#で開発する際のDIコンテナに迷っているようでしたら、Microsoft.Extensions.DependencyInjectionを使ってみてはいかがでしょうか。その際にこの記事が参考になれば幸いです。
はじめに # 前回 の記事では、 1つのテキストファイル(桃太郎物語) を対象にした単純なRAG(検索+生成)環境を構築しました。 今回はその拡張として、 複数のドキュメントを読み込み・保持・削除できる永続化対応のローカルRAGアプリ を構築します。 全体構成 # ディレクトリ構造 # project/ ├─ app2.py # 本体アプリケーション ├─ vectorstore/ # ベクトルストアの永続化ディレクトリ │ ├─ faiss_index/ # FAISSのインデックスファイル │ ├─ metadata.json # 読み込まれたファイル一覧 │ └─ temp_docs/ # アップロードされたドキュメントの一時保存場所 新しいアプリ( app2.py )では次のような進化があります。 機能 内容 マルチドキュメント対応 PDF, Word, PowerPoint, テキストなど複数ファイルを同時に学習可能 永続化ストレージ ベクトルDB(FAISS)をローカル保存し、再起動後も再構築不要 個別ファイル削除 特定のファイルだけを削除し、DBを再構築 UI強化 Streamlitのサイドバーでファイル一覧・削除・全削除操作が可能 精度向上 MultiQueryRetrieverで質問を多角的に変換して検索精度を改善 アプリを起動すると、 vectorstore/ 以下に自動で必要なフォルダが作成されます。 ファイルを追加すると、ベクトルDBとメタデータがディスクに永続化されます。 必要なライブラリ # 必要なライブラリを以下のコマンドでインストールします。 pip install langchain langchain-openai langchain-community langchain-huggingface sentence-transformers streamlit faiss-cpu pypdf python-docx python-pptx pydantic cryptography unstructured docx2txt プログラム全体 # 本体ソースコードは以下です。 ソースコード中のコメントに処理内容を記載しています。 かなり行数が多いので、プログラムの主要な部分についてはこの後解説します。 import streamlit as st import os import tempfile from pathlib import Path import shutil import json import uuid # --- LangChain関連ライブラリのインポート --- # LLM(大規模言語モデル)および埋め込みモデルを利用したRAG(Retrieval-Augmented Generation)構成に使用 from langchain_openai import ChatOpenAI # OpenAI互換LLM(例:LM Studio/Ollama)との接続用 from langchain_huggingface import HuggingFaceEmbeddings # HuggingFaceの埋め込みモデル(ベクトル化用) from langchain.text_splitter import RecursiveCharacterTextSplitter # 文書をチャンク単位に分割 from langchain_community.vectorstores import FAISS # 高速ベクトル検索エンジン(ローカル永続化対応) from langchain.chains import RetrievalQA # 検索と回答生成を結合したRAGチェーン from langchain_community.document_loaders import ( PyPDFLoader, Docx2txtLoader, TextLoader, UnstructuredPowerPointLoader ) # 各種ファイル形式のローダー from langchain.prompts import PromptTemplate # LLMへのプロンプトテンプレート from langchain.retrievers.multi_query import MultiQueryRetriever # 複数クエリ拡張による検索精度向上 # ============================================================ # 永続化関連のパス設定 # ============================================================ DB_DIR = "vectorstore" # ベクトルストア保存ディレクトリ DB_FAISS_PATH = Path(DB_DIR) / "faiss_index" # FAISSベクトルインデックスファイル DB_METADATA_PATH = Path(DB_DIR) / "metadata.json" # メタデータ保存ファイル TEMP_DOCS_DIR = Path(DB_DIR) / "temp_docs" # 一時的なアップロードファイルの保存先 # ============================================================ # file_uploader のキーを初期化 # ============================================================ # file_uploaderを再初期化するためのユニークキーを設定 if 'file_uploader_key' not in st.session_state: st.session_state['file_uploader_key'] = str(uuid.uuid4()) # ============================================================ # ファイル読み込み関数 # ============================================================ def load_document(file_path): """拡張子に応じて適切なLangChainローダーで文書を読み込む""" ext = os.path.splitext(file_path)[1].lower() if ext == ".pdf": loader = PyPDFLoader(str(file_path)) elif ext == ".docx": loader = Docx2txtLoader(str(file_path)) elif ext == ".pptx": loader = UnstructuredPowerPointLoader(str(file_path)) else: loader = TextLoader(str(file_path), encoding="utf-8") return loader.load() # LangChain Document形式で返却 # ============================================================ # 埋め込みモデル初期化(キャッシュ利用) # ============================================================ # 埋め込みモデルをキャッシュして再利用する関数 @st.cache_resource(show_spinner=False) def get_embeddings(): """HuggingFaceのSentence-BERTモデルを一度だけロードしキャッシュ""" return HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2") # ============================================================ # ベクトルDB構築関数 # ============================================================ # ベクトルDBを構築し、ファイルリストと共にディスクに保存する関数 @st.cache_resource(show_spinner=False) def build_and_save_db(documents, file_metadata_list): """文書群からFAISSベクトルDBを構築し、メタデータと共に保存""" if not documents: # 文書が空の場合は既存DBを削除 if Path(DB_DIR).is_dir(): shutil.rmtree(DB_DIR) return None # 文書を小さなチャンクに分割(500文字単位で100文字重複) text_splitter = RecursiveCharacterTextSplitter( chunk_size=500, chunk_overlap=100, length_function=len ) docs = text_splitter.split_documents(documents) # 分割実行 # 埋め込みモデルの初期化 (Sentence-BERTベースのモデルを使用) embeddings = get_embeddings() # 埋め込みモデルの取得 # FAISSベクトルストアの新規構築 db = FAISS.from_documents(docs, embeddings) # 永続化ディレクトリ作成と保存 Path(DB_FAISS_PATH).parent.mkdir(parents=True, exist_ok=True) db.save_local(str(DB_FAISS_PATH)) # メタデータをJSONとして保存 with open(DB_METADATA_PATH, "w", encoding="utf-8") as f: json.dump(file_metadata_list, f, ensure_ascii=False, indent=4) return db # ============================================================ # RAGチェーン構築関数 # ============================================================ # 保存されたDBをロードし、RAGチェーンを作成する関数 def create_rag_chain_from_db(db): """既存DBからRAGチェーン(Retrieval+LLM)を構築""" llm = ChatOpenAI( model_name="local-model", # LM Stduioに読み込まれているモデルを指定(モデル名は特定していない) openai_api_base="http://localhost:1234/v1", # LM StduioサーバのURL openai_api_key="not-needed", # API-keyは無し temperature=0.1, # 高い確率の単語を優先的に選ぶ max_tokens=512 # 最大512トークンに制限 ) # 検索精度向上のため、質問を複数クエリに拡張するRetriever # MultiQueryRetrieverを設定: 質問をLLMに渡し、複数の質問に言い換えて検索精度を向上させる # search_kwargs={"k": 2} で取得する文書チャンク数を2つに制限し、応答速度を改善 retriever = MultiQueryRetriever.from_llm( retriever=db.as_retriever(search_kwargs={"k": 2}), llm=llm ) # RAGプロンプト定義(回答方針) prompt_template = """ 以下の参考文章を元に、質問に日本語で回答してください。 参考文章に答えが見つからない場合は、「分かりません」と回答してください。 参考文章: {context} 質問: {question} """ PROMPT = PromptTemplate( template=prompt_template, input_variables=["context", "question"] ) # RetrievalQAチェーンを生成(検索→回答生成の一連の流れ) qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", # 検索結果を全てプロンプトに詰め込む方式 retriever=retriever, return_source_documents=True, # 回答の根拠となった文書(ソース)を返す設定 chain_type_kwargs={"prompt": PROMPT} # カスタムプロンプトを適用 ) return qa_chain # ============================================================ # 個別ファイル削除とDB再構築 # ============================================================ # 個別ファイル削除とDB再構築のロジック def delete_single_file(file_id_to_delete): """指定されたファイルIDを削除し、残りの文書でDBを再構築""" if not DB_METADATA_PATH.exists(): st.error("メタデータが見つかりません。全体削除を推奨します。") return with open(DB_METADATA_PATH, "r", encoding="utf-8") as f: current_metadata = json.load(f) # 削除対象を特定 file_to_remove = next((item for item in current_metadata if item["id"] == file_id_to_delete), None) if not file_to_remove: st.error("削除対象のファイルIDが見つかりませんでした。") return # 一時ファイルを削除 temp_path = Path(file_to_remove["temp_path"]) if temp_path.exists(): os.remove(temp_path) # 残りのファイルでDBを再構築 new_metadata = [item for item in current_metadata if item["id"] != file_id_to_delete] all_documents = [] with st.spinner(f"ファイルを削除し、残りのデータでAIを再構築中..."): # 残ったすべてのファイルを再ロード for item in new_metadata: doc_path = Path(item["temp_path"]) if doc_path.exists(): documents = load_document(doc_path) all_documents.extend(documents) # 既存キャッシュをクリアしてDB再構築 if "rag_chain" in st.session_state: del st.session_state["rag_chain"] build_and_save_db.clear() # 残りの文書でDBを再構築し、保存 db = build_and_save_db(all_documents, new_metadata) # 再構築後の状態更新 if db: st.session_state.rag_chain = create_rag_chain_from_db(db) st.session_state.current_files = new_metadata st.toast(f"✅ ファイル '{file_to_remove['name']}' を削除し、DBを更新しました。", icon="🗑️") else: # 全削除された場合の後処理 keys_to_delete = ["rag_chain", "messages", "current_files", "file_identifiers"] for key in keys_to_delete: if key in st.session_state: del st.session_state[key] st.toast("✅ 全てのファイルを削除しました。", icon="🗑️") st.session_state['file_uploader_key'] = str(uuid.uuid4()) st.rerun() # Streamlit再実行でUI更新 # ============================================================ # 全体削除処理 # ============================================================ def delete_all_data(): """DBディレクトリとセッション変数を全削除""" if Path(DB_DIR).is_dir(): try: shutil.rmtree(DB_DIR) st.toast("✅ 全ての読み込みデータを削除しました。", icon="🗑️") except OSError as e: st.error(f"データの削除中にエラーが発生しました: {e}") return # セッション変数をクリア keys_to_delete = ["rag_chain", "messages", "current_files", "file_identifiers"] for key in keys_to_delete: if key in st.session_state: del st.session_state[key] # file_uploader のキーをリセット st.session_state['file_uploader_key'] = str(uuid.uuid4()) st.rerun() # ============================================================ # Streamlit UI 構築 # ============================================================ st.title("📄 ドキュメント Chatbot") st.write("複数のPDF, DOCX, PPTX, テキストファイルをアップロードして、内容について質問してください。") # --- サイドバー: ファイルアップロード --- uploaded_files = st.sidebar.file_uploader( "ファイルをアップロード(既存DBに追加・上書き)", type=["pdf", "docx", "pptx", "txt", "md"], accept_multiple_files=True, key=st.session_state['file_uploader_key'] ) # --- DB存在チェック --- db_exists = DB_FAISS_PATH.exists() metadata_exists = DB_METADATA_PATH.exists() current_file_identifiers = [(f.name, f.size) for f in uploaded_files] # ---------------------------------------------------------------------- # 初期化ロジック (既存データと新規データの結合処理) # ---------------------------------------------------------------------- # 1. 新しいファイルがアップロードされた場合 (追加/新規構築&保存) if uploaded_files: # 既存のメタデータ(ファイルリスト)をロード existing_metadata = [] if metadata_exists: with open(DB_METADATA_PATH, "r", encoding="utf-8") as f: existing_metadata = json.load(f) # 既存のファイル名(name)のセットを作成(重複チェック用) existing_names = {meta['name'] for meta in existing_metadata} # ---------------------------------------------------- # 新規アップロードファイルの処理 # ---------------------------------------------------- newly_uploaded_files = [] # アップロードされたファイルの中から、既存ファイル名と重複しないものだけを選択 for uploaded_file in uploaded_files: if uploaded_file.name not in existing_names: newly_uploaded_files.append(uploaded_file) else: # 既存ファイルと同じ名前の場合はスキップ(上書きは行わない) pass if newly_uploaded_files: all_documents = [] new_metadata_list = [] TEMP_DOCS_DIR.mkdir(parents=True, exist_ok=True) with st.spinner("新しいファイルを読み込んでいます..."): for uploaded_file in newly_uploaded_files: unique_id = str(uuid.uuid4()) # 新しい一時ファイルとして保存 temp_path = TEMP_DOCS_DIR / f"{unique_id}_{uploaded_file.name}" temp_path.write_bytes(uploaded_file.getvalue()) # 新しいファイルのメタデータを生成 new_metadata_list.append({ "id": unique_id, # 一意なID "name": uploaded_file.name, "temp_path": str(temp_path) }) documents = load_document(temp_path) all_documents.extend(documents) # ドキュメントリストに追加 # ---------------------------------------------------- # 既存データと新規データの結合 # ---------------------------------------------------- # 既存のファイルを再度ロードし、全ドキュメントリストに結合 for meta in existing_metadata: doc_path = Path(meta["temp_path"]) if doc_path.exists(): documents = load_document(doc_path) all_documents.extend(documents) # メタデータリストを結合 combined_metadata = existing_metadata + new_metadata_list with st.spinner("AIのデータ統合と再構築をしています..."): # 古いキャッシュをクリア if "rag_chain" in st.session_state: del st.session_state["rag_chain"] build_and_save_db.clear() # 全文書と全メタデータでDBを再構築し、保存 db = build_and_save_db(all_documents, combined_metadata) st.session_state.rag_chain = create_rag_chain_from_db(db) st.session_state.current_files = combined_metadata # セッションに新しいファイルリストを保存 st.session_state.messages = [] st.info("新しいデータが既存のデータに追加され、AIの準備が完了しました。") # DB構築成功後、アップローダーのリスト(赤枠部分)をクリアするためにリセット st.session_state['file_uploader_key'] = str(uuid.uuid4()) st.rerun() else: # アップロードはされたが、すべて既存ファイル名と同じだった場合 st.info("アップロードされたファイルはすべて既に読み込まれているファイル名と同じだったため、処理をスキップしました。") # スキップされた場合も、手動での削除を防ぐためにアップローダーをリセット st.session_state['file_uploader_key'] = str(uuid.uuid4()) st.rerun() # 2. アップロードがなく、既存DBファイルが存在する場合 (高速ロード) elif not uploaded_files and db_exists and "rag_chain" not in st.session_state: with st.spinner("既存のAIデータ(DB)を読み込んでいます..."): # 埋め込みモデルをロード embeddings = get_embeddings() # 既存のFAISS DBをディスクからロード db = FAISS.load_local(str(DB_FAISS_PATH), embeddings, allow_dangerous_deserialization=True) st.session_state.rag_chain = create_rag_chain_from_db(db) # 既存のメタデータ(ファイルリスト)をロード if metadata_exists: with open(DB_METADATA_PATH, "r", encoding="utf-8") as f: loaded_metadata = json.load(f) st.session_state.current_files = loaded_metadata st.success("既存のドキュメントでチャット可能です。") else: st.session_state.current_files = [] st.warning("既存のドキュメントでチャット可能ですが、元のファイル名リストが見つかりませんでした。") st.session_state.messages = [] # 3. 初期メッセージの表示 if "rag_chain" not in st.session_state and not db_exists: st.info("ファイルをアップロードするか、過去に保存したデータが存在すれば自動的にロードされます。") # ---------------------------------------------------------------------- # 表示ロジック: 現在読み込まれているファイル名の表示と削除ボタン # ---------------------------------------------------------------------- if "current_files" in st.session_state: st.sidebar.markdown("---") st.sidebar.subheader("現在読み込まれているファイル") if st.session_state.current_files: for file_meta in st.session_state.current_files: col1, col2 = st.sidebar.columns([0.8, 0.2]) # ファイル名を表示 col1.markdown(f"- **{file_meta['name']}**") # 個別削除ボタンのUI (ポップオーバーで確認) with col2.popover("🗑️", help="このファイルを削除します"): st.write(f"ファイル **{file_meta['name']}** を削除し、DBを再構築しますか?") if st.button("削除を確定", key=f"delete_{file_meta['id']}", type="secondary"): delete_single_file(file_meta['id']) else: st.sidebar.markdown("- ファイルがありません。") st.sidebar.markdown("---") if Path(DB_DIR).is_dir(): # 全体削除ボタン if st.sidebar.button("🗑️ 全ての読み込みデータを削除", type="secondary"): delete_all_data() # ============================================================ # チャット処理 # ============================================================ if "rag_chain" in st.session_state: # 履歴表示 if "messages" in st.session_state: for message in st.session_state.messages: with st.chat_message(message["role"]): st.markdown(message["content"]) # ユーザー入力受付 if prompt := st.chat_input("ドキュメントについて質問をどうぞ"): with st.chat_message("user"): st.markdown(prompt) st.session_state.messages.append({"role": "user", "content": prompt}) rag_chain = st.session_state.rag_chain with st.spinner("考え中..."): # RAG実行(検索+回答生成) response = rag_chain.invoke({"query": prompt}) answer = response["result"] # 回答表示と参照ソース展開 with st.chat_message("assistant"): st.markdown(answer) with st.expander("参考にした文章"): for doc in response["source_documents"]: st.markdown(f"--- \n {doc.page_content}") st.session_state.messages.append({"role": "assistant", "content": answer}) コード全体の構造 # app2.py の構成を俯瞰すると以下のようになります。 1. ドキュメントローダー関数群 2. ベクトルDB構築・保存・ロード関数 3. RAGチェーン生成関数(MultiQueryRetrieverによる検索精度向上) 4. ファイル削除ロジック 5. Streamlit UI構成 - サイドバー: アップロード・削除・一覧表示 - メイン画面: チャットUI この分離構造により、RAG処理とUIが明確に分かれ、拡張性が高い設計になっています。 主要コンポーネントの説明 # この章では、 app2.py の内部構造をさらに掘り下げ、各関数やモジュールがどのように連携して RAG (Retrieval-Augmented Generation) 環境を構築しているのかを詳しく解説します。 1. ドキュメントローダー(Document Loader) # この部分はシステムの「入力ゲート」として機能します。アップロードされたファイルを受け取り、LangChain が扱える Document オブジェクトに変換します。これにより、後続のベクトル化処理や検索が統一的に実施できます。 内部では拡張子を基に適切なローダークラスを選択し、PDF・Word・PowerPoint・テキストの各形式に対応しています。 ローダーは単にテキストを抽出するだけでなく、ページ情報などのメタデータも付与します。 load_document() 関数で、PDF・DOCX・PPTX・TXTなどのファイル形式を自動判別して読み込みます。 LangChainの各種ローダーを活用しています。 from langchain_community.document_loaders import ( PyPDFLoader, Docx2txtLoader, TextLoader, UnstructuredPowerPointLoader ) 2. ベクトルDB(FAISS)永続化 # RAG の根幹を担うのがこの部分です。分割・埋め込み・保存の3工程で構成され、アップロードした複数の文書を検索可能な形に変換します。 分割 : RecursiveCharacterTextSplitter により、文脈の自然な区切りを保ったままテキストをチャンク化。 埋め込み : HuggingFaceEmbeddings によるSentence-BERTモデル( all-MiniLM-L6-v2 )を使用し、文意を数値ベクトルに変換。 保存 :FAISSによってベクトル空間を構築し、インデックスファイルとしてローカル保存。 これらを組み合わせることで、アプリを再起動しても同一の知識ベースを即座に再利用できる「永続化RAG」が実現しています。 分割したドキュメントチャンクを埋め込み(ベクトル化)し、FAISSで保存します。 保存先は vectorstore/faiss_index/ です。 また、読み込んだファイル情報(UUID、ファイル名、保存パス)は metadata.json に保持します。 これにより、アプリを再起動しても 同じ知識ベースを即座に再利用 できます。 また、アプリの中で特に処理時間が長くなりやすいのが、 埋め込みモデルのロード や FAISSベクトルDBの構築 です。 これらを毎回ゼロから実行すると、ユーザー体験が大きく損なわれます。 そこで Streamlit の @st.cache_resource デコレータを利用しています。 これは、関数の戻り値(モデルやデータベースなどの「リソース」)をキャッシュし、再利用可能にする仕組みです。 以前の @st.cache の後継であり、リソース指向のキャッシュをより安全かつ効率的に扱うことができます。 3. MultiQueryRetrieverによる検索精度向上 # 標準的なRAGでは、ユーザーの質問を1つのクエリに変換して検索しますが、このプログラムでは LangChain の MultiQueryRetriever を採用し、LLMが質問を複数の言い換えに自動変換します。 たとえば「品質管理とは何か?」という質問に対して、LLMは内部で以下のような複数クエリを生成します: 品質管理の定義とは? ソフトウェア品質保証との違いは? 品質を維持・改善する方法とは? これにより、文書中の言い換え表現や別の文脈にもマッチしやすくなり、検索精度が大幅に向上します。 単一の質問に対し、LLMが複数の検索クエリを自動生成し、類似文書を多角的に探索します。 これにより、 言い換えや表現の揺れ に強いRAGが実現できます。 from langchain.retrievers.multi_query import MultiQueryRetriever retriever = MultiQueryRetriever.from_llm( retriever=db.as_retriever(search_kwargs={"k": 2}), llm=llm ) search_kwargs={"k": 2} の 「k」 は、Retriever が検索時に 取得する類似文書チャンクの件数(トップK件) を表します。 つまり、「最も関連度の高い文書を上位2件だけ取得する」という設定です。 LangChain の Retriever は、ユーザーの質問と文書をベクトル空間上で比較し、 コサイン類似度が高い順に K 件の文書チャンクを返します 。 パラメータ 意味 k 類似度上位 K 件の文書を取得する search_kwargs 検索時の動作パラメータをまとめた辞書 db.as_retriever() ベクトルDB(FAISSなど)を検索エンジンとして利用する設定 なぜ「2」を選んでいるのかは、 検索精度・処理速度・トークン消費 のバランスを最適化するためです。 観点 kが大きい場合 kが小さい場合(例:2) 検索精度 多くの文書を参照できるが、関係ない文も混ざりやすい 関連度の高い文脈に限定できる 処理速度 応答が遅くなる(特にローカル実行時) 高速で軽量に動作する トークン消費 多文書入力により増加 少なく済むため効率的 適用場面 大規模RAGや多分野文書 小規模・単一テーマのRAG(本アプリに最適) 本アプリでは「ローカル実行」「FAISS永続化」「中〜小規模ドキュメント」を前提としているため、 過剰な文脈を含めずに回答の精度と速度を両立させる ことが重要です。 LLM(LM Studio / Ollamaなど)のコンテキスト長に収まるよう調整 StreamlitのUI上で応答をスムーズに返すため、処理時間を短縮 ノイズ文書を除去して、より一貫性のある回答を生成 チューニングの目安は以下です。 k値 特徴 想定用途 1 最速・最小トークン。文脈が単純な場合に最適。 短文中心・単一テーマ 2〜3 精度と速度のバランスが良い。 通常のRAG(本アプリ推奨) 5以上 網羅的だが遅い。長文・百科事典的な用途に適する。 多分野・長文RAG このように、 k=2 は「精度・速度・軽量性の最適点」 を狙った実装上の設計判断です。 4. ファイル削除操作 # 永続化されたデータを扱う場合、部分的な削除や再構築の制御が不可欠です。 このアプリでは2段階の削除ロジックを実装しています: 個別削除 :特定のファイルIDを基に、そのファイルに対応する一時保存データとメタ情報を削除し、残りのデータからDBを再構築します。 全削除 : vectorstore/ ディレクトリ全体を削除し、完全な初期化を行います。 削除処理はすべてStreamlitのセッション状態と連動しており、削除後に st.rerun() でアプリを再描画することで、UIが即時更新されます。 アプリは、ファイルアップロード時に自動でベクトルDBを再構築します。 また、以下の2種類の削除機能を備えています。 操作 動作 個別削除 特定ファイルを削除し、残りのデータでDBを再構築 全削除 vectorstore/ ディレクトリ全体を削除して完全初期化 削除後は自動でUIがリフレッシュされ、状態が更新されます。 5. Streamlit UI の特徴 # アプリケーションの操作性を担うフロントエンド部分です。Streamlitのコンポーネントを駆使し、シンプルながら実用的なチャットUIを構築しています。特筆すべき点は次の通りです。 サイドバー :ファイル管理・削除・リスト表示の制御を集中化。 チャットエリア : st.chat_message を使い、ユーザーとAIの会話履歴を対話形式で可視化。 再現性 :Streamlitの session_state を活用することで、状態保持と再初期化を両立しています。 さらに、チャットの背後ではRAGチェーン( RetrievalQA )が動作しており、質問ごとに検索→生成の2段階推論を行います。 サイドバー ファイルアップロード(複数対応) 現在読み込まれているファイル一覧 各ファイルの削除ボタン 「全削除」ボタン メイン画面 チャット履歴の表示 「ドキュメントについて質問をどうぞ」入力欄 AI回答と「参考にした文章」の展開パネル 実行方法 # LM Studioで http://localhost:1234 サーバを起動(事前にLLMをロードしておく) ターミナルで以下を実行: streamlit run app2.py ブラウザ(通常は http://localhost:8501 )でアプリが開きます。 初回起動時は「ファイルをアップロードしてください」というメッセージが表示されます。 PDFやテキストをアップロードすると、自動的に学習・永続化が行われます。 例:複数ドキュメントを活用したRAG # 前回の「桃太郎」のほかに「かぐや姫」の物語をRAGに読み込ませます。 かぐや姫の物語は以下のようにしました。 むかしむかし、竹を取って暮らす翁(おきな)と、その妻が住んでいました。 ある日、翁が山で光る竹を見つけ、その竹を割ると中から小さな女の子が出てきました。 翁はその子を家に連れ帰り、妻とともに「かぐや姫」と名付けて大切に育てました。 かぐや姫は美しく成長し、そのうつくしさは都にまで広まりました。 多くの貴族が求婚しましたが、かぐや姫は誰の申し出も受けず、難しい宝を求めて試しました。 誰一人として成功する者はいませんでした。 やがて帝もかぐや姫を愛しましたが、彼女の心は月の国にありました。 十五夜の夜、月の使者が迎えに来て、かぐや姫は涙を流しながら月へ帰っていきました。 翁と妻は深く悲しみ、いつまでも夜空を見上げてかぐや姫を思いました。 「桃太郎」と「かぐや姫」の2つのテキストをアップロードした場合、両者の内容を組み合わせた質問にも正確に回答できます。 それぞれを読み込ませます。 質問例:「桃太郎とかぐや姫にはどんな共通点がありますか?」 RAGはそれぞれの物語から類似する文脈を抽出し、両者を比較した回答を生成します。 回答として以下のような文章が出力されました。 回答の中の「どちらも最終的に元の場所へ帰っていくという結末」は、双方が元居た場所に戻るというところを関連付けているのが面白いです。 (桃太郎はおじいさん、おばあさんの元へ。かぐや姫は月へ) まとめ # 今回の拡張版では、ローカルRAG環境をより実践的な形に進化させました。 改善点 効果 FAISS永続化 再起動後もDB再構築不要で高速起動 MultiQueryRetriever 検索精度の向上 ファイル管理UI アップロード・削除を視覚的に操作可能 複数ファイル形式対応 PDF/DOCX/PPTX/TXTを混在学習可能 ローカル環境で完結しながらも、実用的な知識アシスタントを実現できるようになりました。 次回は、 文書の要約・分類機能の追加 や、 OpenAI互換API以外のモデル(例:Ollama, Llama.cpp)対応 にも拡張していく予定です。 img { border: 1px solid gray; }
はじめに # 前回 は、LM Studio+Gemmaでクラウドに頼らないAI環境を構築しました。 本記事では、 LM Studio を使ってローカルでLLM(例:Gemma 3 4B)を動かし、さらに LangChain と Streamlit を組み合わせて、クラウドに頼らずに動作する RAG(Retrieval-Augmented Generation) 環境を構築します。 題材として、誰もが知っている「桃太郎」の物語を使い、自分で用意した知識ベースを読み込む ローカルAIチャットボット を作っていきます。 今回のゴールは以下です。 LM Studio でローカルLLMをAPIサーバーとして動かす LangChain でRAG(検索+生成)パイプラインを構築する Streamlit でチャットUIを作成する すべて自分のPC上で完結し、インターネット接続がなくても動作します。 まさに「自分だけのAI」を作る第一歩です。 LangChainとは # LangChain は、 大規模言語モデル(LLM)を外部データと統合して活用するためのフレームワーク です。 特に、 RAG(Retrieval-Augmented Generation) の構築を容易にする仕組みを提供します。 今回作成するRAG環境では次の4つの処理を組み合わせて、より正確で文脈に基づいた回答を生成します。 ステップ 処理内容 LangChainの機能 ① テキスト分割 ドキュメントを小さなチャンクに分割 TextSplitter ② 埋め込み生成 テキストをベクトル化 Embeddings ③ 検索 質問と類似したチャンクを検索 VectorStore (FAISS, Chromaなど) ④ 回答生成 検索結果+質問をLLMに入力 RetrievalQA や Chain Streamlitとは # Streamlit(ストリームリット) は、 Pythonコードだけで簡単にWebアプリを作成できるフレームワーク です。 データ分析・機械学習・LLMアプリ(例:RAGチャットボット)などのUI構築に広く利用されています。 特徴 説明 簡単な構文 HTMLやJavaScriptを使わず、PythonのみでUIが書ける 即時実行型 コードを保存すると自動でWeb画面が更新される データ可視化に強い matplotlib 、 plotly 、 pandas などと連携可能 インタラクティブUI テキスト入力・スライダー・ボタン・チャットUIなどを簡単に実装できる ローカル or クラウド両対応 streamlit run app.py でローカル実行、または共有用にクラウド公開可能 LM Studio サーバの起動 # 最初のステップとして、モデルを動かすための「APIサーバー」をLM Studioで起動します。 これによって、PythonプログラムがLLMと会話できるようになります。 手順は以下です。 LM Studioを開きます。 左側のメニューから、「開発者」タブを選びます。 画面の上部にあるドロップダウンメニューで、gemma-3 4B モデルを選択し、読み込みます。 (モデルは前回も使用したものです) 「Start Server」 ボタンをクリックします。 サーバーが起動します。 ログに以下のようなメッセージが出力されます。 サーバが「 http://localhost:1234 」で動作していることがわかります。 以下のAPIが確認できます。 (OpenAI互換APIです) RAGパイプラインの構築 # 次は、 RAG(Retrieval-Augmented Generation) という仕組みを作ります。 これは、 LLMが外部の知識(今回はテキストファイル)を参照しながら回答を生成するための技術 です。 身近な例で言うと、「教科書を見ながらテスト問題を解く」 に似ています。 教科書 = 事前に用意するデータ(今回はテキストファイルを用います) テスト問題 = ユーザーからの質問 問題を解く人 = LLM この仕組みを作るために、まずはPythonの環境を整える必要があります。 いくつか専門のライブラリ(便利な道具セットのようなもの)をインストールします。 ターミナル(WindowsならコマンドプロンプトやPowerShell)を開いて、以下のコマンドを実行します。 pip install langchain langchain-community langchain-openai langchain-huggingface streamlit faiss-cpu RAGの教科書 # まず、LLMが読み込む「教科書」(知識ベース)を作成します。(単純なテキストファイルとします) Python スクリプトを作成する予定のフォルダーと同じフォルダーに、 knowledge.txt という名前の新しいテキストファイルを作成します。 教科書データとして、桃太郎に関する次の短編小説をコピーしてテキストファイルに貼り付け、保存します。 このファイルが ローカル ナレッジ ベース になります。 むかしむかし、あるところにおじいさんとおばあさんが住んでいました。 おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました。 おばあさんが川で洗濯をしていると、大きな桃がどんぶらこ、どんぶらこと流れてきました。 おばあさんはその桃を拾い上げて、家に持ち帰りました。 家に帰って桃を割ってみると、中から元気な男の子の赤ちゃんが出てきました。 桃から生まれたので、その子を「桃太郎」と名付けました。 桃太郎はすくすくと育ち、やがて鬼ヶ島へ鬼退治に行くと言い出しました。 おばあさんからきびだんごをもらい、桃太郎は旅に出ます。 旅の途中で、犬、猿、雉を家来にしました。 そして、みんなで力を合わせて鬼を退治し、宝物を持って家に帰りました。 Streamlitアプリを作成する # 本体のプログラムを作成します。 先ほど作った knowledge.txt と同じフォルダに、 app.py という名前で新しいファイルを作成します。 そして、以下のコードをすべてコピーして、 app.py ファイルに貼り付けます。 import streamlit as st from langchain_openai import ChatOpenAI from langchain.text_splitter import CharacterTextSplitter from langchain_huggingface import HuggingFaceEmbeddings from langchain_community.vectorstores import FAISS from langchain.chains import RetrievalQA # ============================= # --- LLMとRAGのセットアップ --- # ============================= # ドキュメントを読み込んでRAGパイプラインを作成する関数 def create_rag_chain(document_path): # ドキュメントを読み込む with open(document_path, 'r', encoding='utf-8') as f: document_text = f.read() # 1. ドキュメントを小さな「チャンク」に分割する # これにより、モデルが関連情報を見つけやすくなります。 text_splitter = CharacterTextSplitter( separator="\n", chunk_size=200, # 各チャンクのサイズ(文字数) chunk_overlap=50, # チャンク同士の重なり length_function=len ) docs = text_splitter.split_text(document_text) # 2. 各チャンクの「埋め込みベクトル」を作成する # 埋め込み(Embeddings)は、テキストをコンピュータが意味を理解できる数値のベクトルに変換する技術です。 # all-MiniLM-L6-v2は、文章をコンピュータが理解できる数値(ベクトル)に変換することに特化した、小型で高速なモデルです。 embeddings = HuggingFaceEmbeddings(model_name="all-MiniLM-L6-v2") # 3. ベクトルストア(FAISS)を作成し、埋め込みを保存・検索できるようにする # これは、私たちの「教科書」に検索可能な索引を作るようなものです。 db = FAISS.from_texts(docs, embeddings) # 4. ローカルLLMサーバー(LM Studio)への接続を設定する llm = ChatOpenAI( # ↓↓↓ LM Studioの「API Identifier」をここに貼り付けてください ↓↓↓ model_name="local-model", # ローカルモデルを使用するように指定 base_url="http://localhost:1234/v1", # LM Studioサーバーのアドレス api_key="not-needed", # ローカルサーバーなのでAPIキーは不要 temperature=0.1 # temperatureを低く設定して、「参考文章から外れず、最も確実な回答をしなさい」とAIに指示している ) # 5. RetrievalQAチェーンを作成する # このチェーンは、検索役(FAISSの索引)とLLMを組み合わせます。 # 質問をすると、まず最も関連性の高いテキストチャンクを見つけ出し、 # それを質問と一緒にLLMに渡して、回答を生成させます。 retriever = db.as_retriever() qa_chain = RetrievalQA.from_chain_type( llm=llm, chain_type="stuff", # "stuff"は、関連するチャンクをすべてプロンプトに「詰め込む」方式 retriever=retriever, return_source_documents=True ) return qa_chain # knowledge.txtを使ってRAGチェーンを作成 rag_chain = create_rag_chain("knowledge.txt") # ============================= # --- Streamlit UI --- # ============================= st.title("🍑 桃太郎チャットボット") st.write("桃太郎の物語について、質問してください!") # チャット履歴を初期化する if "messages" not in st.session_state: st.session_state.messages = [] # 履歴にあるメッセージを再表示する for message in st.session_state.messages: with st.chat_message(message["role"]): st.markdown(message["content"]) # ユーザーの入力に反応する if prompt := st.chat_input("質問をどうぞ"): # ユーザーのメッセージを表示 with st.chat_message("user"): st.markdown(prompt) # ユーザーのメッセージを履歴に追加 st.session_state.messages.append({"role": "user", "content": prompt}) # LLMの応答を取得 response = rag_chain.invoke({"query": prompt}) answer = response["result"] # アシスタントの応答を表示 with st.chat_message("assistant"): st.markdown(answer) # アシスタントの応答を履歴に追加 st.session_state.messages.append({"role": "assistant", "content": answer}) ソースコード中のコメントでプログラムの解説をしていますが、概略を以下にまとめます。 前半:頭脳の準備パート ( create_rag_chain 関数) # ここは、LLMが 「賢い司書」 になるための準備をする部分です。 本を読む: まず、教科書ファイル「桃太郎の物語」 ( knowledge.txt ) の内容をすべて読み込みます。 付箋を貼る: 物語を短い文章(チャンク)に区切って、内容ごとにたくさんの付箋を貼っていくようなイメージです。( CharacterTextSplitter ) 索引を作る: コンピュータが「どの付箋にどんな内容が書いてあるか」をすぐに見つけられるように、特殊な索引(ベクトルストア)を作ります。( Embeddings , FAISS ) LLMと連携: 最後に、「質問が来たら、まず索引を使って関連する付箋を探し、その内容を参考にして答える」という ルール ( RetrievalQA )を決め、LM StudioのLLMと連携させます。 この準備によって、LLMはただの物知りではなく、 資料(教科書データ)に基づいて 回答できる専門家になります。 --> Information all-MiniLM-L6-v2 はどこから来るのか? 上記のモデルは、初めてプログラムを実行したときに、インターネット上にある 「Hugging Face Hub」 という巨大なAIモデルの保管庫から、自動的にダウンロードされます。 一度ダウンロードされると、PC内の特別なフォルダ(キャッシュと呼ばれます)に保存されます。 2回目以降にプログラムを実行するときは、もう一度ダウンロードするのではなく、PCに保存されたそのファイルから直接読み込まれます。 後半:アプリ画面パート (Streamlit UI) # ここは、ユーザーが実際に触るチャット画面を作る部分です。 画面の表示: 「🍑 桃太郎チャットボット」というタイトルを表示します。 入力欄の用意: ユーザーが質問を入力するためのチャットボックスを用意します。 応答の処理: ユーザーが質問を入力すると、その質問を 前半で作った「賢い司書」 に渡します。 司書(RAG)が資料を調べて作った回答を受け取ります。 その回答をチャット画面に表示します。 ざっくり言うと、 「前半で資料を読み込んで賢くなったAIを用意し、後半でそのAIと会話するためのチャット画面を作る」 という2段構成になっています。 アプリケーションを実行 # アプリケーション(チャットボット)を起動します。 LM Studio のサーバーが起動していることを確認します。 app.py と knowledge.txt を保存したフォルダを、ターミナル(コマンドプロンプト)で開きます。 ターミナルで、以下のコマンドを入力して実行します。 streamlit run app.py 上記コマンドを実行すると、自動的にWebブラウザで新しいタブが開きます。 (デフォルトでは「 http://localhost:8501/ 」でアプリケーションが起動しています) ブラウザの右上にRAGを用意していることを示す進捗が数秒間表示されます。 少しの時間の後「🍑 桃太郎チャットボット」の画面が表示されるはずです。 質問をする # チャットボットに質問をしてみます。 質問は以下です。 「桃太郎とは何者ですか?」 「桃太郎は何をしましたか?」 教科書データに載っている内容を回答していることがわかります。 教科書データの内容を一部変えてみる # 教科書データの内容を一部変えてみます。 以下の 旅の途中で、犬、猿、雉を家来にしました。 の部分を 旅の途中で、猫、亀、鶴を家来にしました。 に変えて実行してみましょう。 結果は以下のようになりました。 正しく 家来が入れ替わって解釈されている ことがわかります。 教科書データに無いことを聞いてみる # 教科書データに無いことを聞いてみます。 AIが勝手に物語を拡張していないことを確認します。 用意した教科書データ以外の内容については答えられないことがわかります。 参考にした元の文章の表示 # 回答と一緒に「参考にした元の文章」も表示するように機能を追加します。 ソースコードの以下の部分 with st.chat_message("assistant"): st.markdown(answer) に機能を追加します。 変更後のソースコードは以下です。 with st.chat_message("assistant"): st.markdown(answer) # --- ここから追加 --- with st.expander("参考にした文章"): for doc in response["source_documents"]: st.markdown(f"--- \n {doc.page_content}") # --- ここまで追加 --- アプリケーションを実行すると、以下のように参考にした文章も一緒に表示されるようになりました。 まとめ # 今回紹介した手順では、 LM Studio × LangChain × Streamlit を組み合わせることで、 クラウドに依存しないローカルRAG環境 を構築しました。 ポイントを振り返ると次の通りです。 LM Studio でローカルLLMをOpenAI互換APIとして動作 LangChain で文書を分割・ベクトル化・検索・回答生成を自動化 Streamlit で対話的なUIを構築し、ブラウザから手軽に利用可能 教科書データ を変更すると、AIの回答内容も動的に変化 この仕組みにより、「自分の持つ知識ファイル」をAIが読み込み、AIが “自分専用の知識アシスタント” のように振る舞います。 より複雑なナレッジや複数ファイル対応、さらには検索精度向上やUI改善などに発展させることもできます。 自分のデータを自分の環境で活かす、新しいAI活用の形を試すことができました。 img { border: 1px gray solid; }
はじめに # 私は普段は業務アプリの開発に従事しております。開発言語はほぼJavaであり、Spring Framework/Spring Bootを使用することが多いです。 業務以外でプログラムを書く機会や趣味はほとんどなかったのですが、最近インディー系の2Dアクションゲームにハマっており(ホロウナイト、カップヘッド、オリシリーズなどが好きです。)自分でも簡単なもので良いからミニゲーム開発をしてみたい!と思い立ってやってみることにしました。 今回は私のミニゲーム開発に使用したPythonのライブラリのことや開発の様子についてお伝えしたいと思い記事を書くことにしました。読んでいただけますと幸いです。 開発の進め方 # まずは本を読んで体系的に技術の知識を身に着けたいと思い、以下の書籍を参考にすることにしました。 Pythonでつくる ゲーム開発 入門講座 実践編 / 廣瀬 豪 この本を読めば書いてある通りのミニゲームが作成できますが、オリジナルの要素や機能の追加をしたくなることもあるかと思い、その際はChatGPTを頼ることにしました。本と生成AIのハイブリッド開発ですね。 使用するライブラリ「tkinter」について # 本では、Pythonのライブラリ tkinter と Pygame が紹介されています。 Pygame は高機能なゲーム用ライブラリですが、今回はシンプルなミニゲームを作るので tkinter のみを使用することとしました。 ChatGPTに聞いてみたところ、以下のようなライブラリとのことです。 tkinter(ティーケーインター)は、PythonでGUI(グラフィカルユーザーインターフェース)アプリを作るための標準ライブラリです。 Pythonだけで「ウィンドウを出したり、ボタンやラベルを配置したり、画像を表示したり」といった デスクトップアプリを作れる便利なツールです。 ゲームに限らずシンプルなGUIツールを作成するのにも使えそうですね。Pythonに標準で入っているのが嬉しい点だと思いました。 ミニゲームの要件 # 以下のような要件を満たす2Dのミニゲームを作ります。 ヘビからウサギが逃げるというコンセプトのゲーム ウサギがヘビに接触するとゲームオーバー ヘビは画面内をランダムな方向に移動する ウサギはプレイヤーが操作し、マウスのカーソルに追従して移動する ゲーム開発に関しては初心者なのでまずはこれくらいで良いでしょう。 tkinterの基本 # 本を読みながらコードを書いてゆきます。tkinterのミニゲームアプリは以下のコードが必須だそうです。 # メインウィンドウとキャンバスの設定 root = tkinter.Tk() root.title("ウサギのミニゲーム") # ---------------------- # 画像を描画するキャンバスの設定を行う。 # (縦横比の設定や背景画像素材の設定、マウスムーブのイベントとキャンバスの紐づけ等) # ---------------------- root.mainloop() 本には root = tkinter.Tk() が「ウィンドウの部品を作る」、 root.mainloop() が「ウィンドウを表示」の処理だと書かれていますが、もう少し詳しく知りたいのでChatGPTに聞いてみます。 root = tkinter.Tk() 何をしているか tkinter.Tk() は アプリ全体のメインウィンドウ(トップレベルウィンドウ)を生成 しています。つまり「ウィンドウを1つ作る」という命令です。 具体的な動作 OS上で、新しい空のウィンドウを作ります。このウィンドウは、後でボタンやキャンバス、ラベルなどのウィジェットを配置する「土台」になります。 root はこのウィンドウを操作するための変数名(慣習的に root や window と書かれます)。 root.mainloop() 何をしているか イベントループ(main roop)を開始します。これは、ウィンドウを開いたままにして、ユーザー操作(クリック・キー入力など)を待ち続ける仕組みです。 具体的な動作 この行を呼ぶと、プログラムは「ループ状態」に入ります。 tkinterが内部的に「イベント(クリック・キー入力など)」を監視して、対応する処理を呼び出します。 mainloop を呼ばないと、ウィンドウは一瞬で開いてすぐ閉じてしまいます。 より詳しく知ることができました。 tkinter でウィンドウを作成し、表示・維持する最も基本的なコードだということが分かりました。GUIアプリはユーザの操作を待つ必要があるので内部的にループ状態を管理する仕組みがあるんですね。 作ってみる # 一旦できました。拙いですが一応ちゃんとミニゲームとして動いていて感動です。 細かい部分は全て書ききれないので、このミニゲームのキモとなる当たり判定のロジックについてピックアップして書いておきます。 当たり判定のロジック # 本では「円同士の当たり判定」と「長方形同士の当たり判定」の2種類が紹介されていました。等身の高いキャラクターなんかは長方形の方が適していそうですが、今回は円同士の当たり判定を実装してみました。 ウサギとヘビそれぞれのx、y座標の値と半径の長さrを使用して計算します。座標同士の距離を求め、それが半径の合計以下の長さになっているかを判定しています。 def hit_check(self): dis = math.sqrt((self.rabbit.x - self.snake.x) ** 2 + (self.rabbit.y - self.snake.y) ** 2) return dis <= self.rabbit.r + self.snake.r マウスムーブ時に実行する処理の中でhit_checkメソッドを呼び、Trueが返されたときはゲームオーバー画面を表示するようにしています。 改善点 # 一応形にはなりましたが改善したい部分も出てきました。ヘビは0.05秒間隔でランダムな方向に一定距離移動するように実装していたのですが、目で見ると動きがかなりぎこちないです。ChatGPTに相談しつつ、ゆっくりとウサギの位置に追従するような動きになるよう処理内容を修正してみます。 以下が、ヘビを管理するSnakeクラスに実装した修正後のヘビ移動メソッドです。target_x、target_yはウサギの座標を受け取り、on_move_doneはこの移動処理を繰り返し呼ぶためのコールバックです。 def move_toward(self, target_x, target_y, on_move_done): # ウサギに向かって移動 dx = target_x - self.x dy = target_y - self.y dist = math.sqrt(dx**2 + dy**2) # 距離が0でないときだけ移動方向を正規化 if dist != 0: dx /= dist dy /= dist new_x = self.x + dx * self.speed new_y = self.y + dy * self.speed # 画面範囲チェック 範囲内なら更新 if 0 < new_x < 1200 and 0 < new_y < 676: self.x, self.y = new_x, new_y self.draw() # 50msごとに再実行 self.job = self.canvas.after(50, on_move_done) 修正後の動きがこんな感じになりました。 自然な動きでウサギを追いかけるようになりました。他にも工夫すれば、追従とランダムな方向を組み合わせたり、一定時間おきにスピードアップしたりできそうですね。 おわりに # ゲームを作るのは初めてでしたが、やってみると結構簡単にちょっとしたミニゲームが作れて面白かったです。今は本だけではなく生成AIに相談しながら開発を進めることができるのがかなり便利ですね。本で基礎を学んで、ChatGPTと相談しながらオリジナル要素を実装してゆくという進め方が楽しいです。 まだまだ改善したい点はたくさんあります。 ウサギをマウスムーブに追従ではなくボタン操作できるようにする ジャンプを実装する 障害物を配置する ヘビとバトルして倒せるようにする(攻撃アクションとHPの導入) などなど。 また、今回は素材画像をいらすとやさんからダウンロードして使用しましたが、素材も全て生成AIで作るのも良さそうだと思いました。 引き続き趣味でゲーム開発をやっていき、次は Pygame を使って何かしら作り記事にできたらと考えています。 読んでいただいてありがとうございました。
はじめに # 時代の流れは速いもので、 「2024年版!VS Code で Java 開発環境を構築する」 で、VS CodeのJava環境構築が紹介されてからのいくつかの改善がなされました。今回はそれらを紹介します。 Extension Pack for Java Auto Configの利用 # 今回も結論から言ってしまうと 「Extension Pack for Java Auto Config を入れましょう」で終わりです。 Extension Pack for Java Auto Config - Visual Studio Marketplace この拡張パックの内訳は以下のようになっています。 JDKの自動構成 Extension Pack for Java - Visual Studio Marketplace Spring Boot Extension Pack - Visual Studio Marketplace 追加の拡張 素のVS Codeに「Extension Pack for Java Auto Config」を入れるだけで、Javaアプリor SpringBootアプリを作るための準備はほぼ完了です。言ってしまえばVS Code版「Pleiades All in One」という内容になっています(実際にこの拡張はPleiadesチームによって開発されています)。 「Extension Pack for Java」と「Spring Boot Extension Pack」は 2024年版 で解説されているので説明は割愛します。 JDKの自動構成 # この拡張のメインの機能です。この拡張は内部に複数のJDK(少なくとも 3 つの LTS バージョンと最新バージョン)を含んでいます。フォルダを開いたときにMavenプロジェクトなどが含まれている場合は、最適なJDKを使うように自動的に構成されます。またmavenやgradleも含まれているので、これらをインストールしなくても開発を始めることができます。 VS Codeからターミナルを起動するときも、各JDK環境に合わせたターミナルを立ち上げることができます。 Windows環境での日本語文字化け対策 # Windows環境でJavaアプリを実行するとターミナルへのログの出力が文字化けすることがあるので、対策をします。 JDK18以降を使っている場合 # JDK18以降のデフォルトの文字コードはUTF-8です。一方でターミナルのデフォルトの文字コードはMS932のため文字化けが起こることがあります。 これを解消するには、ターミナルの文字コードを強制的にUTF-8にします。 レジストリエディタを立ち上げて \HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Command Processor を開きます。 ここに以下のような値を作成します。 値の名前:Autorun 値のデータ:chcp 65001 > nul 最後の部分は「null」ではなく「nul」であることに気を付けてください。 JDK17以前を使う場合 # 上記の設定をしたままでJDK17以前を使うと、JDKの文字コードはMS932で、ターミナルはUTF-8なので文字化けが起こります。 そこで以下のような環境変数を設定してJDKの文字コードをUTF-8にします。 JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8 追加の拡張 # Extension Pack for Java Auto Configが追加するその他の拡張について見ていきます。 XML - Visual Studio Marketplace # XMLの入力支援をしてくれます。例えば、タグの上にマウスカーソルをホバーさせるとスキーマにかかれたドキュメンテーションを表示するなどの機能があります。 Code Spell Checker - Visual Studio Marketplace # コードやコメントのスペルチェックをしてくれます。 TODO Tree - Visual Studio Marketplace # ソースコード中のTODOやFIXMEを一覧表示してくれます。 Live Server - Visual Studio Marketplace # HTMLやCSSなどの確認に便利な簡易サーバーです。 HTMLファイルを開いた状態で、画面右下の「Go Live」を押すと、ブラウザでHTMLを表示してくれます。 Live Reload機能によりHTMLを書き換えるとリロードなしでブラウザに修正が繁栄されます。 Trailing Spaces - Visual Studio Marketplace # 末尾空白のハイライト表示と削除をしてくれます。 indent-rainbow - Visual Studio Marketplace # インデントのハイライト表示をします。 Rainbow CSV - Visual Studio Marketplace # CSVファイルのハイライト表示をします。 さいごに # Extension Pack for Java Auto Configによって、これ1つインストールするだけで、Javaアプリ/SpringBootアプリの開発環境が整うのは便利になったと思います。 EclipseやIntelliJ IDEAなどの統合環境に比べると機能面で劣るところもあります。一方、VS Codeは無料で動作が軽く、しかもAI対応が統合環境よりも早いことを考えると、Java開発環境としてVS Codeを使うのもありだろうと思います。
本記事は、「TwinCATで始めるソフトウェアPLC開発」シリーズの第2回目です。 他の章も併せてご覧ください。 第1回:環境構築編 第2回:ST言語でのプログラミング(1/2)(今回) 第3回:ST言語でのプログラミング(2/2)← 絶賛作成中! 0. はじめに # 前回の記事 はTwinCATの開発環境(XAE)・実行環境(XAR)の構築方法について説明しました。 今回は基本的なPLCプログラムの実装方法についてご紹介します。 1. ST言語とは? # IEC61131-3規格で定められた5種類のプログラム言語のうちの1つです。 テキスト形式による実装が可能な言語であり,Pascalライクな文法で記述します。 本記事ではこの言語を使用します。 ST言語によって記述したプログラムの例 FOR i:=0 TO 10 DO // メソッド実施,引数によるデータの入出力 fbHogeHoge.FugaFuga(i, outData => tmpData) END_FOR 2. PLCプログラムの作成・実装 # 開発環境を起動し,ST言語でのPLCプログラムを実装していきます。 2.1 ソリューション作成 # Visual Studio もしくは XAE Shellを開きます。 (今回はVisual Studioを選択しました) 「新しいプロジェクトの作成」を選択します。 プロジェクトテンプレートには「TwinCAT XAE Project (XML format)」を選択します。 プロジェクト名とソリューション名を指定します。 「ソリューションとプロジェクトを同じディレクトリに配置する」にチェックを入れます。 プロジェクト名・ソリューション名は「TwinCAT-Tutorial」とします。 2.2 PLCプロジェクト作成 # ソリューションエクスプローラーにて「PLC」を右クリックして,「新しい項目の追加」をクリックします。 --> ソリューションエクスプローラーの開き方 XAEShellもしくはVisualStudioの画面左側にソリューションエクスプローラーが表示されない場合は,下記の項目をクリックしてください。 「表示」>「ソリューションエクスプローラー」 「Standard PLC Project」を選択して,プロジェクト名を指定します。 今回は「PlcTutorialProject」として追加ボタンをクリックします。 2.3 MAINプログラムを編集してみる # PLCプログラムを新規作成すると,ソリューションエクスプローラー内の「PLC」配下に項目が追加されます。 「POUs」フォルダ内にある「MAIN(PRG)」をクリックして編集画面を開きます。 編集画面の上半分は変数を定義するためのスペース,下半分はプログラムの処理を記述するためのスペースです。 (C++で例えるなら,上半分がヘッダファイル,下半分がソースファイルを記述するスペースとなります) 定義スペース(上半分)には下記のように記述します。今回はDINT型(符号付き32bit整数)の変数を定義します。 変数定義時は「変数名 : 型」のように記述します。 MAINプログラム 定義スペース PROGRAM MAIN VAR /// プログラム呼び出し回数 CycleCount : DINT; END_VAR 実装スペース(下半分)は下記のように記述します。 今回は処理1回ごとに変数「CycleCount」をインクリメントしています。 MAINプログラム 実装スペース // 変数をインクリメントする CycleCount := CycleCount + 1; --> 代入時の記号 代入では「:=」を使用します。「=」は**同値評価(値が等しいかどうか)**である点に注意してください。 --> TwinCATで使用可能なプリミティブ型 使用可能なプリミティブ型の一覧は こちら を参照してください。 --> 自動補完機能 「Ctrl+Space」キーを入力すると,補完候補のウィンドウが表示されます。コーディングの時間短縮におすすめです。 プログラムの編集が完了したら,ビルドしてエラーが発生しないことを確認します。 IDE上部の「ビルド」タブ>「ソリューションのビルド」をクリックします。 IDE下部に表示される「出力」タブ内で,失敗の数が0となっていることを確認します。 3. プロジェクトの実行と動作確認 # 3.1 デプロイ前の確認事項 # プログラムを書き込むために,まずはXAR環境(=実行環境)にアクセスできるかを確認します。 システムトレイに表示されている歯車アイコンを右クリックして 「Router」>「Edit Routes」を選択します。 --> システムトレイにアイコンが表示されない場合 システムトレイに歯車アイコンが表示されない場合は,下記のexeファイルを起動してください。 C:\Program Files (x86)\Beckhoff\TwinCAT\3.1\System\TcAmsRemoteMgr.exe (※TwinCATのインストール場所を変更した場合は,上記と異なる場合があります) 「TwinCAT Static Routes」ウィンドウが表示されるため,下記のように緑色となっていれば接続が行えています。 もし緑色の項目が存在しない場合は, 前回記事の3章・4章 から設定を見直してください。 3.2 プロジェクトのデプロイ # XARとの通信が確立していることを確認したら,IDEからターゲットを指定します。 IDEを開き,「表示」タブ>「ツールバー」>「TwinCAT XAE Base」をクリックしてチェックを入れます。 これにより,IDEの上部にTwinCATに関する表示が増えます。 【変更前】 【変更後】 追加された項目のうち,「ローカル」と表示されているコンボボックスをクリックして,XAR環境をターゲットとして指定します。 ターゲット指定後,青色の階段のアイコンをクリックします。 「構成のアクティブ化」ウィンドウが表示されるので,OKボタンを押します。 初回書き込み時は評価ライセンスの生成を促されるため,「はい」を選択します。 表示されたものと同じ文字列をテキストボックスに入力し,OKボタンを押します。 これにより,評価用ライセンスが生成され,プログラムが実行可能な状態となります。 --> TwinCATのランタイムライセンスについて TwinCATの各パッケージをXAR環境で使用する場合はライセンスが必要です。 ライセンスを所持していない場合、無償で使用するための評価用ライセンスを生成して使用することが出来ます。 ただし、この評価版ライセンスは有効期限が7日間であり、期限を過ぎた場合は再度生成しなおす必要があります。 評価版ライセンスは正式なライセンスを使用した場合に比べて、機能に制限がかかりますが、基本的な動作を確認するだけであれば十分に使用可能です。 TwinCATを再起動するか尋ねられるため,「OK」を押して再起動します。 IDEの右下に表示されている歯車アイコンが下図のように緑色かつ回転していれば,プログラムが正常に実施されています。 3.3 ログインによる動作確認 # TwinCATでは,XARにログインすることで変数の値をリアルタイムで確認することができます。 このログイン機能を使用して,先ほど書き込んだプログラムが正常に動作しているかを確認してみます。 「拡張機能」タブ>「PLC」>「ログイン」を選択してログインします。 このボタンが無効状態となっている場合は,ターゲットを指定するコンボボックスに正しいターゲットが指定されているかを確認してください。 ログインした状態でMAINプログラムを開くと,CycleCount変数の値がリアルタイムで確認できます。 1秒間におよそ100だけ加算されていく様子が確認できます。 これは,TwinCATプロジェクトを作成したときに生成されるタスクの実行周期が10msであるためです。 4. タスクの実行周期を変えてみる # デフォルトで生成されるタスクの周期は10msですが,これを変更してみましょう。 プロジェクト生成時に自動で追加されたタスクを消してみます。 新しいタスクを作成します。「SYSTEM」>「タスク」を右クリックして「新しい項目の追加」をクリックしてください。 タイプは「TwinCATタスク」を選択し,名前を「MainTask」として「OK」をクリックします。 作成したタスクの詳細設定画面が開かれるので,「サイクルティック」を10 → 100に変更します。 これによりタスクの実行周期が100msとなります。 --> Information サイクルティック1つ当たりの時間はデフォルトでは1msですが,CPUのコア設定で変更可能です。 詳細についてはこちらをご覧ください。 https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_system/5210414219.html&id= タスクを作成したら,どのプログラムを呼び出すかを設定します。 「PLCプロジェクト」を右クリック>「追加」>「参照されるタスク」を選択します。 割り当て可能なタスクが表示されるので,先ほど作成した「MainTask」を指定して「Open」をクリックします。 生成した「タスク参照」を右クリックして,「追加」>「既存の項目」を選択します。 タスクから呼び出すプログラムを選択します。先ほどコードを修正した「MAIN」プログラムを選択してOKを押します。 先程と同様に,ログインして変数の様子を見てみましょう。 1秒間に10だけ値が増えていくことが確認できると思います。 これは,先ほど作成したタスクがMAINプログラムを100msごとに呼び出しているからです。 5. 同一タスクに複数のプログラムを登録する # 1つのタスクには複数個のプログラムを登録できます。 例えば実行周期を10msに設定したタスクにプログラムAとプログラムBの2つを登録した場合,10msごとにプログラムAとプログラムBが実施されます。 ただし,プログラムAとBは並列で実行されるのではなく, どちらかのプログラムが完了した後にもう一方のプログラムが実施される 点に注意してください。 概念構造を下図に示します。 --> Warning 両プログラムの処理時間の合計がタスク実行周期を上回る(タスクオーバーラン)と,システムがハングする可能性があります。 プログラムの実行時間とタスクの実行周期には十分ご注意ください。 実際に複数のプログラムを同一のタスクに割り当ててみます。 「POUs」フォルダを右クリックして,「追加」>「POU」を選択します。 プログラム名は「MAIN2」とし,タイプは「プログラム」を,実装言語は「構造化テキスト」(ST)を選択してOpenをクリックします。 MAIN2プログラムでは,MAINプログラムと同じように変数をカウントアップする処理を記述します。 (MAINプログラムと区別するために,インクリメント量を2倍にしておきます) MAIN2プログラム 定義スペース PROGRAM MAIN2 VAR /// プログラム呼び出し回数の2倍値 CycleDoubleCount : DINT; END_VAR MAIN2プログラム 実装スペース CycleDoubleCount := CycleDoubleCount + 2; MAINプログラムの時と同様に,MAIN2プログラムをMainTaskにアサインします。 MainTask参照アイテムの子要素に「MAIN」と「MAIN2」の両方があることを確認してください。 この内容を書き込んで動作を確認してみます。 MAIN2プログラムはMAINプログラムと同じ周期(100ms)で実行され,CycleDoubleCount変数値が1秒間で20だけ値が増えることが確認できます。 6. プログラム間でデータを共有する # あるプログラムで計算した値を別のプログラムで使用したい場合が多々あります。 このような場合は,プログラムもしくはタスク間で共通のデータ(グローバル変数)を定義します。 グローバル変数はすべてのタスクが参照できる共有リソースとして定義されるため,これを用いることでタスクを跨いだデータ共有が可能です。 実際にMAINプログラム内での値をMAIN2プログラムで参照してみます。 「GVLs」フォルダを右クリックして,「追加」>「グローバル変数一覧」をクリックします。 変数リスト名は「GVL_Var」として「Open」をクリックします。 ソリューションエクスプローラー上の「GVL_Var」をクリックして編集画面を開き,下図のようにグローバル変数を定義します。 GVL_Var {attribute 'qualified_only'} VAR_GLOBAL /// プログラム間共有データ SharedData : DINT; END_VAR --> Information 先頭行に記載されている波括弧の部分は,グローバル変数に対する属性(Attribute)です。 Attributeの詳細については下記のリンク先をご覧ください。 https://infosys.beckhoff.com/english.php?content=../content/1033/tc3_plc_intro/2529567115.html&id= MAINプログラムでこのグローバル変数(SharedData)に,CycleCount変数の値を代入するようにします。 MAINプログラム 実装スペース CycleCount := CycleCount + 1; // 共有データに値を書き込む(追記部分) GVL_Var.SharedData := CycleCount; この値を,MAIN2プログラム内で別変数として受け取ってみます。 MAIN2プログラム 定義スペース PROGRAM MAIN2 VAR CycleDoubleCount : DINT; /// MAINプログラムのデータ MainProgramData : DINT; END_VAR MAIN2プログラム 定義スペース CycleDoubleCount := CycleDoubleCount + 1; // 共有データの値をローカル変数に格納する MainProgramData := GVL_Var.SharedData; このTwinCATプロジェクトを書き込み動作を確認してみましょう。 MAIN2プログラムのMainProgramData変数に,MAINプログラムのCycleCount変数の値が格納されていることが確認できます。 7. おわりに # 今回はST言語による基本的なPLCプログラムを作成してみました。 タスク・プログラムの使用方法が理解できたと思います。 ここまでのプロジェクトを こちら で共有しています。補助資料としてご活用ください。 次回は,ファンクションブロック(Function Block)を用いたPLCプログラムについて説明します。
やっと涼しくなってきました。弊社も新体制となり、Web サイトがリニューアルされました [1] 。🎊 https://mamezo.tech/ 今後ともよろしくお願いいたします。 それでは、2025年度第2四半期のサマリーです。 記事数・執筆者数 # この3ヶ月で40本の記事が投稿され、記事総数は809になりました。800本超えです。新たに5名が執筆デビューし、累計71名になりました。 テーマ別の記事 # プロジェクトマネージメント # 1Qで始まったプロジェクトマネージメントシリーズ。現場のプロセス改善の話題にも踏み込んだ記事が公開されています。 チェックリストの形骸化を防ぐ|デキるPMの再構築術と7つの改善策 形骸化しない定例会議の進め方|デキるPMの7つの改善ステップ 課題が消化されるリスト運用|デキるPMの脱・形骸化テクニック12選 因果関係図を活用した問題解決手法|現場改善に効くデキるPMの実践ステップ プロセス改善の実践ステップ|デキるPMが使うIDEALモデルと成功の秘訣 未来実現ツリー活用の中間目標で現場を動かす|デキるPMの改善計画術 変更管理の成功ガイド|デキるPMが実践する要件管理・構成管理・トレーサビリティ活用法 目的・目標・手段を区別する力 ─ 新人プロジェクトマネージャーが指揮官から学ぶ計画思考 .NET 系 # .NET C# 系の記事が増えました。豆蔵にも .NET 好きがけっこういます。 VS Codeで始める!わかる&できるC#開発環境の構築【2025年版マニュアル】 現場で迷わない!C#のLINQをサンプルコード付きで徹底攻略 C#とRazorで始める効率的なWeb開発!サンプルコード付きで徹底解説 【C#】WPFとMVVM「はじめの一歩」から現場Tipsまで! 〜デスクトップアプリ開発の実践メモ〜 C#とEntity Frameworkで生産性アップ!基本から実践まで徹底解説 ロボット # ロボットシステム開発の基盤技術が詳しく解説された記事が公開されました。 産業用ロボットの教示方法とその応用 生成AIを開発に活かす # コード生成だけでなく要件定義から設計書作成にもAIを活かす記事が公開されてます。 KiroでAI開発革命!? アルバムアプリをゼロから作ってみた【その1:要件定義・設計・実装計画】 最新LLMで“バイブコーディング”を実践(要件定義〜機能実装①) Kiroでアルバムアプリを作成する記事は全6本の連載となっており、 こちら から全部の記事を読めます。 LLM 関係 # LLM をローカルでホストする記事、LLM の仕組みに踏み込んだ記事などが公開されました。 「アテンションが全て」ではなかった?GPT2 small(124M)から学ぶLLMの仕組み クラウドに頼らないAI体験:LM Studioで始めるローカルLLM入門(Gemma 3) AWSで自分だけのLLM環境を!EC2 GPUインスタンスとOllamaでAIを動かす実践ガイド 夏のリレー連載2025開催 # 今年も夏のリレー連載が無事完了し10本の記事が公開されました。やはり生成AIの記事が多かったです。 夏のリレー連載2025 機械学習ページをリニューアル # 生成AIの記事が増えてきたので、従来の「機械学習」のページを「機械学習・生成AI」としてリニューアルし最新の記事を紹介しています。 機械学習・生成AI さいごに # 以上、2025年度第2四半期のサマリーでした。 よかったら フィード の購読、 X や Bluesky でのフォローもお願いします。 Facebook でも本サイトの注目記事をはじめ豆蔵に関するイベントを紹介しています。 note にも時々本サイト関連の記事が掲載されています。 社名の英字表記も微妙に変わりました。 ↩︎
はじめに # 豆蔵の GitHub オーガニゼーションもメンバーが増えて、多くのリポジトリを把握するのが困難になってきました。 新規のリポジトリ作成の内容をチェックすることも必要になってきました。機密性の高い情報を扱う場合もあるため、リポジトリの可視性が public になっていないかを確認することは重要です。 この記事では、オーガニゼーションのリポジトリ作成を通知する仕組みを構築しようと試行錯誤した内容をお届けします。 --> Information 豆蔵では Team プランで契約していますが、Enterprise プランならメンバーのリポジトリの作成を制限し、オーガニゼーションの管理者が依頼ベースで作成する運用も可能です。 開発者の自発的活動を阻害してしまうことにも繋がるため、個人的にはあまりこのような制約はかけたくはありませんが。 GitHub のイベント通知を Slack の Incoming Webhook で受ける(ダメ) # Slack には Incoming Webhook というアプリで Webhook 経由の通知を受け取る汎用的な仕組みがあります。 最初 GitHub から Incoming Webgook でイベント通知すればいいのでと考えました。そこで、通知したい Slack チャンネルに Incoming Webhook を導入。 GitHub の オーガニゼーションの Settings で Webhooks > Add webhook で設定します。 通知するイベントを選択するオプションを指定。 「Repositories」を指定すると、リポジトリの作成・アーカイブ・可視性変更といったイベントを通知できます。 Slack の Incoming Webhook の URL を指定して、設定を完了しました。 この設定をしてからしばらくして、同僚の人がリポジトリを作ったことを知りましたが、チャンネルには通知が来ていませんでした。 GitHub 側では、イベントを通知しようとしていましたが、失敗していました。ステータス400ということで、リクエストのデータが不正だったようです。 Response には missing_text_orfallback_or_attachments というメッセージが格納されています。 リクエストを見ると確かに text などのフィールドはありません。Incoming Webhook 用のメッセージに変換する中継サービスがないとダメそうです。ということで、GitHub の通知と Slack の Incoming Webhook を直接繋ぐのは無理でした。 GitHub の Slack アプリはどうか(ダメ) # Slack には GitHub 公式のアプリもあるので、これが使えないかと考えました。 GitHub アプリのリポジトリは以下です。 https://github.com/integrations/slack この README.md を読んだところ、リポジトリ単位ではなく、オーガニゼーション単位のサブスクライブも可能なようです。試しに、チャンネルから、オーガニゼーションにサブスクライブしてみました。 通知されるイベントはスクリーンショットで列挙されているものだけのようで、オーガニゼーション内のリポジトリの Issue や PR などに関するイベントしか通知されません。ということでこの方法も NG でした。 --> Information GitHub App を使ったリポジトリイベントの通知に関しては以下の記事で紹介しています。 /blogs/2022/12/12/notify-github-actions-workflow-to-slack/ GitHub Actions でリポジトリの作成日時から検出する # 最後の手段は、GitHub API と GitHub Actions ワークフローで定期的にチェックして Slack に通知を飛ばす方法です。リアルタイム性はないですが、1日1回程度通知されれば実用上は十分でしょう。 以前、オーガニゼーションのメンバーを把握するためのワークフローを設置したリポジトリに新たにワークフローを追加することにしました。 /blogs/2024/10/04/build-simple-github-org-admin-site/ この記事の時と同様、ランタイムは Bun、スクリプトは TypeScript を採用します。 ワークフローを JST で0時に起動して、GitHub の GraphQL でリポジトリの名前・URL・作成日時・可視性を取得し、作成日時が前日になっているものでフィルターするのがよさそうです。 以下のようなワークフローファイルを用意しました。 name: Notify New Repos to Slack on: schedule: - cron: '0 15 * * *' #1 workflow_dispatch: jobs: notify: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Bun uses: oven-sh/setup-bun@v2 #2 - name: Install dependencies run: bun install --no-save #3 - name: Notify new repos to Slack env: GH_PAT: ${{ secrets.ORG_REPO_PAT }}. #4 SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} #5 GITHUB_ORG: ${{ vars.ORG_NAME }} #6 run: bun run src/notify-new-repos.ts #7 各ステップの処理は以下のようになっています。 UTC の15時(JST の0時)に起動 Bun 環境をセットアップ bun install で octokit/graphql をインストール オーガニゼーションの参照権限を付与した PAT をシークレットから設定 Slack チャンネルの Incoming Webhook の URL をシークレットから設定 オーガニゼーション名を設定 Bun スクリプトを実行 実行される Bun スクリプトを抜粋します。 最初に環境変数を読み込んでおきます。 const org = process.env.GITHUB_ORG; const token = process.env.GH_PAT; const slackWebhook = process.env.SLACK_WEBHOOK_URL; GraphQL 部分。リポジトリを50件、作成日時(CREATED_AT)の降順(DESC)で取得するクエリーです。フィールドとして、リポジトリ名、URL、作成日時、可視性を取得しています。 const query = ` query($org: String!) { organization(login: $org) { repositories(first: 50, orderBy: {field: CREATED_AT, direction: DESC}) { nodes { name url createdAt visibility } } } } `; Octokit を使って GraphQL でリポジトリのリストを取得する処理です。 let repos: { name: string; url: string; createdAt: string; visibility: string }[] = []; try { const data = await graphql<{ organization: { repositories: { nodes: typeof repos } } }>(query, { org, headers: { authorization: `token ${token}` } }); repos = data.organization.repositories.nodes; } catch (err) { console.error('GitHub GraphQL API error:', err); process.exit(1); } 1日前に作られたリポジトリを取得するため、JST の前日の日付を作成し、GraphQL で取得したリポジトリのリストの作成日時が前日になっているものを抽出します。 const now = new Date(); const JST_OFFSET = 9 * 60; const jstNow = new Date(now.getTime() + (JST_OFFSET - now.getTimezoneOffset()) * 60000); const yesterday = new Date(jstNow); yesterday.setDate(jstNow.getDate() - 1); const ymd = (d: Date) => d.toISOString().slice(0, 10); const yesterdayJSTDate = ymd(yesterday); const newRepos = repos.filter(r => { const created = new Date(r.createdAt); const jstCreated = new Date(created.getTime() + JST_OFFSET * 60000); return ymd(jstCreated) === yesterdayJSTDate; }); 最後に Slack の Webhook URL 向けにメッセージを作成し、送信します。 リポジトリが作成された場合、Slack への投稿に気づけるように @here メンションをつけています。これはメッセージに <!here> を含めることで実現できます。 let message: string; if (newRepos.length) { message = "<!here>\nList of newly created repositories:\n" + newRepos.map(r => `• <${r.url}|${r.name}> (created at ${r.createdAt.slice(0,10)} ${r.visibility})`).join('\n'); } else { message = "No new repositories were created yesterday."; } try { const resp = await fetch(slackWebhook, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text: message }), }); if (!resp.ok) { console.error('Slack notification failed:', await resp.text()); process.exit(1); } console.log('Slack notification succeeded:', message); } catch (err) { console.error('Slack notification failed:', err); process.exit(1); } 以下のような感じで、リポジトリ作成通知がチャンネルに届きます(手動実行したため、時刻は午前10時30分ごろになっています)。 さいごに # 以上、オーガニゼーション内のリポジトリ作成を検知するために実施した方法の紹介でした。 やはり、Slack の GitHub アプリでリポジトリのライフサイクルイベントを通知してほしいところですね。
はじめに:SimulinkとArduinoで始めるS-Functionブロックの自作 # Arduinoで利用可能なデバイスは多岐にわたりますが、Simulinkで直接サポートされていないセンサーやディスプレイも数多く存在します。 そこで有効なのが S-Function を使った自作ブロックです。 本記事では、 OLEDディスプレイ SSD1306 を例に、Simulink用のS-Functionブロックを自作し、Arduinoで動作させる手順を紹介します。 開発環境の準備 # ソフトウェア MATLAB(バージョン:R2025a) Simulink(バージョン:25.1) アプリ(for Simulink) Simulink Support Package for Arduino Hardware(バージョン:25.1.0) ハードウェア Arduino Uno(または互換機) USBケーブル(PCとArduinoの通信用) HC-SR04(超音波距離センサ) OLED SSD1306(I2C接続ディスプレイ) 環境構築の詳細手順: # 1. MATLAB/Simulink と 基本パッケージの導入 MATLAB と Simulink、Arduino Support Package の導入については、 前回の記事 を参照してください。 2. Rensselaer Arduino Support Package Library を導入 MATLAB を起動 → メニューから 「アドオン」 → 「ハードウェアサポートパッケージの入手」 を選択します(アドオンエクスプローラーが起動します) 検索ボックスで 「Arduino」 と入力し、 Rensselaer Arduino Support Package Library (RASPLib) を選択します Install をクリックし、パッケージを導入します 導入完了後、Simulinkライブラリに「Rensselaer Arduino Support Package Library」のブロックが追加されます ↓ その中に「超音波距離センサ HC-SR04」用のブロックがあります。HC-SR04を動かすために、このブロックを使用します。 回路の接続 # HC-SR04 # VCC → Arduino 5V GND → Arduino GND Trig → Arduino デジタルピン 7(例) Echo → Arduino デジタルピン 8(例) OLED SSD1306 (I2C) # VCC → Arduino 3.3V または 5V GND → Arduino GND SCL → Arduino A5 (Unoの場合) SDA → Arduino A4 (Unoの場合) SSD1306用のS-Functionブロックの作成 # HC-SR04はRASPLibでサポートされていますが、SSD1306については専用ブロックが見つからなかったため、S-Functionブロックで自作します。 ここで重要なのは、 Simulinkで直接サポートされていない機能はArduinoライブラリを呼び出して補う という点です。 ArduinoのライブラリはC/C++で書かれたデバイス制御用関数群であり、S-Functionを介してSimulinkモデルから呼び出すことが可能です。 これにより、Simulinkでサポート外のデバイスでも、 Arduinoの豊富なライブラリエコシステムを活用して動作させることができる のです。 ArduinoのOLED SSD1306用ライブラリ選定 # Arduino向けOLEDライブラリとして有名なのは以下です。 Adafruit_SSD1306 Adafruit-GFX-Library ただし、UnoのFlashに収まらない場合があるため、より軽量な U8g2_Arduino を採用します。 U8g2は多機能ですが、Unoでのメモリ制約を考慮し、今回は U8x8テキストモードのみ を利用します。 プロジェクトの作成 # S-Function Builderは単体でも使えますが、複数のS-Functionを扱う場合は Simulinkプロジェクト機能 を利用すると便利です。 新しいプロジェクトを作成 MATLABの「新規 → プロジェクト → 空のプロジェクト」を選択します 保存先フォルダが「プロジェクトフォルダ」となります この方法を取れば、複数のデバイスを扱う場合でも整理しやすくなり、再利用性も向上します。 ライブラリの作成 # 新しいSimulinkライブラリを作成 MATLABを起動し、 simulink コマンドでSimulinkライブラリブラウザを開く メニューから「新規 → ライブラリ」を選択し、空のライブラリファイルを作成 ここに自作ブロックを追加していきます S-Function Builderブロックの配置 新しいライブラリに「S-Function Builder」ブロックを配置 生成される .cpp ファイルや .tlc ファイルがライブラリと連動するように管理されます。 外部ライブラリの準備 Arduinoで利用する Wire.h や U8x8lib.h がインクルードできるように準備 U8g2_Arduino を C:\ProgramData\MATLAB\thirdpartylibs\U8g2_Arduino などに配置 環境に応じて Support Package のパスも確認しておきます S-Function Builder でブロックを自作 # 今回は、仕様を簡単にし、少ないメモリでも動作する設計にします。 U8g2 の U8x8 テキストモードで、SSD1306 128×64 (I²C) に1行のASCII文字列(最大16文字)を表示する仕様です。 プロジェクト名: sfun_ssd1306_u8x8_display_block S-Function名: sfun_ssd1306_u8x8_display 言語:C++ ソースコード: /* Includes_BEGIN */ #ifndef MATLAB_MEX_FILE #include <Arduino.h> #include <Wire.h> #include <U8x8lib.h> // SSD1306 128x64 via hardware I2C, no reset pin (UNO: SCL=A5, SDA=A4) static U8X8_SSD1306_128X64_NONAME_HW_I2C u8x8(/* reset = */ U8X8_PIN_NONE); static bool isDisplayInitialized_u8x8 = false; #endif /* Includes_END */ /* Externs_BEGIN */ /* extern double func(double a); */ /* Externs_END */ void sfun_ssd1306_u8x8_display_Start_wrapper(void) { /* Start_BEGIN */ #if !defined(MATLAB_MEX_FILE) if (!isDisplayInitialized_u8x8) { u8x8.begin(); u8x8.setPowerSave(0); // 文字フォント(ASCII用の軽量フォント) u8x8.setFont(u8x8_font_chroma48medium8_r); // 必要ならI2Cアドレス指定(一般的な0x3Cを8bit表現で) // u8x8.setI2CAddress(0x3C << 1); // 何も出ない時だけ試す u8x8.clearDisplay(); isDisplayInitialized_u8x8 = true; } #endif /* Start_END */ } void sfun_ssd1306_u8x8_display_Outputs_wrapper(const uint8_T *u) { /* Output_BEGIN */ #if !defined(MATLAB_MEX_FILE) if (!isDisplayInitialized_u8x8) return; // 入力ベクトル長に合わせて調整 const uint8_t MAX_STR = 16; char buf[MAX_STR + 1]; uint8_t i = 0; for (; i < MAX_STR; ++i) { uint8_t b = u[i]; buf[i] = (char)b; if (b == 0) break; } buf[(i < MAX_STR) ? i : MAX_STR] = '\0'; // 1行目(行=0)にASCII文字列を表示 u8x8.clearLine(0); u8x8.drawString(0, 0, buf); #endif /* Output_END */ } void sfun_ssd1306_u8x8_display_Terminate_wrapper(void) { /* Terminate_BEGIN */ #if !defined(MATLAB_MEX_FILE) // nothing #endif /* Terminate_END */ } 端子とパラメーター: 外部コード: 必要な「U8g2_Arduino」を以下のパスにGitでCloneしておきます。 C:\ProgramData\MATLAB\thirdpartylibs\U8g2_Arduino Arduino Support Package のパスは以下になっていました。(MATLABをインストールした環境に依存しますので、皆さんの環境では、Support Packageのパスを確認してください) C:\ProgramData\MATLAB\SupportPackages\R2025a\aCLI\data\packages\arduino ビルドとライブラリ登録 # S-Functionをビルドすると、以下のファイルが生成されます: .cpp (本体ソース) _wrapper.cpp (ラッパコード) .tlc (ターゲット言語コンパイラ用) .mexw64 (Windows用バイナリ) ### Output folder is 'C:\Users\<ユーザ名>\Documents\MATLAB\sfun_ssd1306_u8x8_display_block' ### 'sfun_ssd1306_u8x8_display.cpp' は正常に作成されました ### 'sfun_ssd1306_u8x8_display_wrapper.cpp' は正常に作成されました ### 'sfun_ssd1306_u8x8_display.tlc' は正常に作成されました ### S-Function 'sfun_ssd1306_u8x8_display.mexw64' が正常に作成にされました さらに、ライブラリブラウザに登録するには以下のファイルを用意します: slblocks.m (必須) setup.m (推奨) INSTALL.m (任意)※プロジェクトを配布するときに便利です slblocks.m function blkStruct = slblocks % この関数は、指定したライブラリを % Simulinkライブラリブラウザに表示するために定義します。 % --- ライブラリの登録情報 --- % Browser.Library には、ライブラリのファイル名(拡張子なし)を指定します。 Browser.Library = 'ssd1306_u8x8_display_lib'; % Browser.Name には、ライブラリブラウザに表示したい名前を指定します。 Browser.Name = 'Arduino SSD1306 U8x8 display library'; % --- 構造体にまとめる --- blkStruct.Browser = Browser; end setup.m function setup addpath(fileparts(mfilename('fullpath'))); % #ok<MCAP> 自フォルダをPATHへ try lb = LibraryBrowser.LibraryBrowser2; refresh(lb); catch sl_refresh_customizations; end % 依存チェック(例:Arduinoサポート) % assert(exist('arduino','file')~=0, 'Install MATLAB Support Package for Arduino'); end INSTALL.m %% Add library to path addpath(pwd); savepath; %% Refresh library browser lb = LibraryBrowser.LibraryBrowser2; refresh(lb); パスの設定でパスを登録します。 登録が成功すると、Simulinkライブラリに自作のSSD1306用ブロックが追加されます。 メインのSimulinkモデルの作成 # 新規モデルを作成 # Simulink を起動し、新しい「空のモデル」を作成します ブロックライブラリから以下を配置します: HC-SR04ブロック(RASPLib) SSD1306表示用S-Functionブロック(自作) 文字列処理ブロック(定数文字列、数値⇒文字列変換、文字列結合、文字列⇒ASCII変換) 表示用ブロック(デバッグ確認) パラメータ設定 # 「ハードウェア設定」-「ハードウェア実行」を以下のように設定します。 ハードウェア実行 Arduinoへの書き込みと実行 # プログラムをArduinoにアップロードし、自動実行できるようにします。 Simulink の「ビルド、展開起動」を実行します コンパイル → Arduinoへ転送  転送が成功すると、以下のログが出力されます。 超音波距離センサで計測した物体との距離がOLED上に表示されるようになりました。 (少し数値が読み取りづらいですが、7(cm)くらいの距離に障害物を置いています) 実行結果と考察 # HC-SR04で取得した距離を即座にOLEDに表示でき、センサとディスプレイをSimulink経由で統合できました Unoのメモリ制約によりU8g2のフルバッファ機能は利用困難ですが、U8x8モードは軽量かつ実用的です 今回の構成で センサ入力 → 文字列変換 → ディスプレイ出力 を直感的に構築できました 今後は 行位置・列位置の可変表示 や フォント切替 、 I²Cアドレス指定 などのパラメータ化を進めることで、より汎用的なブロックに発展させられます まとめ # HC-SR04 は RASPLib を利用すればSimulink上で簡単に利用可能です。 SSD1306 は自作S-Functionを作ることで表示機能を拡張できます。 U8g2ライブラリ を活用し、メモリ制約に配慮してU8x8モードを利用するのが実用的です。 プロジェクト機能+slblocks/setup/INSTALLを組み合わせれば、ライブラリとして管理・配布も容易です。 今回の取り組みにより、Arduinoを用いた 複数デバイス統合の一例 を示すことができました。 将来的には他のセンサやアクチュエータにも同様の方法を展開し、ライブラリを充実させることで、モデルベース開発の適用範囲をさらに広げられます。 img { border: 1px gray solid; }
はじめに # 近年、大規模言語モデル(LLM)をローカル環境で動作させるツールが充実してきました。 その中でも LM Studio は、ユーザーが手軽にLLMを試せるアプリケーションとして注目されています。 今回は、LM Studio を使って Gemma LLM を動作させる手順と、基本的な使い方を紹介します。 LM Studio とは # LM Studio は、ローカル環境で大規模言語モデル(LLM)を手軽に動かせるように設計されたアプリケーションです。 専門的な設定やコマンドライン操作を必要とせず、 インストール後すぐにモデルを実行できる手軽さ が特徴です。 また、クロスプラットフォームに対応しており、Windows / macOS / Linux いずれの環境でも利用でき、研究開発から個人学習まで幅広く使われています。 代表的な特徴は以下の通りです。 クロスプラットフォームで動作するLLM実行環境 GUIベースで簡単にモデルを切り替え・実行可能 Chat UI とコード生成支援の両方に対応 Hugging Face や独自モデルをインポートして利用できる つまり、LM Studio は「LLMを試すための実験場」であると同時に、「日常的に使える対話AIの実行環境」としても利用できます。 Gemma LLM とは # Gemma は、Google DeepMind が開発した最新の大規模言語モデルです。 研究者や開発者がローカル環境で安全に利用できるように設計されており、特に「軽量で効率的に動作する」という点に大きな特徴があります。 Gemma はクラウド環境に依存せず、自分のPC上で直接実行できるため、 データプライバシーを確保しながらAIを活用できる のも魅力です。 また、オープンモデルとして公開されているため、誰でも自由に試したり改良したりでき、コミュニティによる拡張も期待されています。 主な特徴は以下の通りです。 Google DeepMind が開発した大規模言語モデル 軽量でローカル実行に最適化された設計 オープンモデルとして Hugging Face に公開されている 自然言語理解やコード補完など幅広い用途に利用可能 ただのテキストLLMではなく、画像理解もできるマルチモーダルモデル Gemma は単なる「軽いLLM」ではなく、 最新の研究成果を活かしつつ、開発者が自由に使える実験環境 として位置付けられています。 これにより、研究用のプロトタイピングから個人開発まで、幅広いユースケースに適用できます。 なぜローカルLLMの需要が高まっているのか? # クラウドベースのAIサービスが普及する一方で、 ローカル環境でLLMを実行するニーズが急速に高まっています 。その背景には、以下のような要因が挙げられます。 データプライバシーの確保 機密情報や個人データを外部サーバーに送信せずに済むため、安心して利用できる。 オフライン環境での利用 インターネット接続が不安定な場所でも、ローカルLLMなら安定して動作可能。 低コストでの実行 API利用料を気にせず、PCのリソースを活かして繰り返し実験できる。 カスタマイズ性の高さ 特定のドメインデータで再学習やファインチューニングが可能。 レイテンシの低減 サーバー通信を挟まないため、応答速度が向上する。 これらの理由から、 研究用途だけでなく、個人開発・教育現場・企業内利用に至るまで、ローカルLLMの導入が加速している のです。 環境構築の手順 # LM Studio を 公式サイト からダウンロード・インストールします。 2025-09-20現在、最新版は「0.3.26」でした。 実行ファイルをダウンロードして、インストーラを起動します。 「Get Started」をクリックします。 LM Studio のインストール時に表示される「Choose your level」は、ユーザーの経験や利用目的に合わせて、UIの見せ方や初期設定の範囲を調整するための選択肢です。 「Choose your level」では「Power User」を選択し、Continueを押します。 --> Information LM Studio のユーザーレベルは以下のように分類されているようです。 レベル 想定ユーザー 特徴 User 初めてAI/LLMを使う人 - 最低限の設定だけで利用可能 - UIはシンプル - 余計なパラメータは非表示 Power User LLMに慣れてきた中級者 - モデル切り替えや生成設定が可能 - UIはシンプルさを保ちつつ調整機能あり - 細かい設定もある程度可能 Developer 開発者・研究者 - 全ての設定・機能にアクセス可能 - API連携やログ詳細、カスタムモデル管理も利用可能 - ツール開発や高度な利用に最適 最初のモデルをダウンロードします(モデルサイズが20Bと大きいので、ローカルPCのメモリに余裕がないと動作しないため、ご自身の環境に合わせてダウンロードしてください) ダウンロードが始まります(サイズが大きいの時間がかかります) ダウンロード終了後、「Start New Chat」を押します。 LM Studio が起動します。(この時点ではまだモデルは読み込まれていません) 環境を「日本語」に設定します。 アプリケーション右上の「外観」をクリックし、選択項目から「View more settings」を選択します。 「Language」を「日本語」に設定します。 UIが日本語に変更されました。 Gemma LLM を探索・ダウンロード 「探索」-「Model Search」で「Gemma 3 4B」を検索し、ダウンロードします。(ご自身のPCスペックに合わせてサイズを選択してください) --> Information モデルサイズとPCメモリの目安は以下です。 モデルサイズ 推奨RAM (CPU実行) 推奨VRAM (GPU実行) 備考 1B (~1Bパラメータ) 8GB以上 4GB以上 軽量。ノートPCでも動作可能 2B 16GB以上 6〜8GB以上 ローカル実行の入門サイズ 4B 16〜24GB以上 10〜12GB以上 実用的な精度と軽さのバランス 9B 32GB以上 16GB以上 高精度。ハイエンドGPUが望ましい 12B 48GB以上 24GB以上 研究用途向け。個人PCでは厳しい場合あり 27B 64GB以上 48GB以上 本格的な開発・研究用。専用サーバー推奨 E4B (Efficient 4B) 16GB以上 8〜10GB以上 省メモリ版。軽量化技術で4B相当より動かしやすい ダウンロード完了後に、モデルを読み込みます。 モデルが読み込まれました。 実際に使ってみる # Chatで簡単な質問をしてみます。 LLMに「日本の昔話の桃太郎について教えてください」と問いかけます。 出力結果は以下のようなものでした。 桃太郎は、日本で最も有名な昔話の一つです。その物語には様々なバージョンがありますが、基本的な流れは以下の通りです。 **物語の概要** * **始まり:** 桃太郎は、ある村に住む貧しい少年でした。ある日、大きな桃を拾って家に持ち帰ると、桃を開けるとそこには元気な男の子がいました。 * **修行:** 桃太郎は、その子と二人で様々な場所を旅して修行をしました。熊や猿、雉といった動物たちに芸を教えたり、おじいさんやおばあさんを助けたりします。 * **鬼ヶ島へ:** 修行が終わった後、桃太郎は鬼ヶ島(鬼が住んでいる島)に行って、鬼退治をする決意をします。 * **準備:** 桃太郎は、犬、猿、雉を家来にして、船を作って鬼ヶ島へと向かいます。犬は「マー坊」、猿は「モンキー」、雉は「コケコッコー」という名前が付けられています。 * **鬼退治:** 鬼ヶ島に到着した桃太郎は、鬼たちと戦い、鬼を全て倒してしまいます。鬼の首を切り取って持ち帰ります。 * **宝物:** 鬼ヶ島には、金銀財宝や美しい宝石などがたくさん隠されていました。桃太郎は、その宝物を持ち帰り、村に貢献します。 **物語のテーマと象徴** * **勇気と正義感:** 困難に立ち向かい、悪を打ち倒す桃太郎の姿は、勇気と正義感の象徴として描かれています。 * **成長と自己実現:** 貧しい少年から、家来を率いる英雄へと成長していく桃太郎の物語は、自己実現や夢の実現といったテーマを示唆しています。 * **多様性の尊重:** 犬、猿、雉といった異なる動物たちを家来にするという点は、多様性を尊重し、協力することの大切さを伝えています。 **様々なバージョン** 桃太郎の物語には、地域や時代によって様々なバリエーションがあります。例えば、鬼ヶ島までの道のりや、鬼との戦い方、宝物の種類などが異なる場合があります。また、桃太郎の性格や家来たちの役割も、物語によって異なっています。 **桃太郎の起源** 桃太郎の物語は、中国の伝説「桃木爺」に由来すると言われています。「桃木爺」は、桃の木を育てた老人が、鬼退治をするという物語です。この物語が日本に伝来し、日本の昔話として発展したと考えられています。 **その他** * 桃太郎は、子供たちの間で人気のあるキャラクターであり、アニメや漫画、映画など様々な形で表現されています。 * 桃太郎の物語は、日本の文化や価値観を反映しており、現代においても多くの人々に愛され続けています。 より詳しく知りたい場合は、以下のキーワードで検索してみてください。 * 桃太郎 昔話 * 桃太郎 中国伝説 * 桃太郎 キャラクター また、以下のようなサイトも参考になるでしょう。 * [https://kotobazaka.jp/kitaro-story/mototarou/](https://kotobazaka.jp/kitaro-story/mototarou/) * [https://www.city.fukuoka.lg.jp/culture/bunka/shumon/shomon_06_03.html](https://www.city.fukuoka.lg.jp/culture/bunka/shumon/shomon_06_03.html) 何か他に知りたいことや、特定の質問があれば、お気軽にお尋ねください。 言いたいことはわかりますが、時々変な解釈や拡張が見られますね。 桃太郎が桃を持ち帰ってくるなどの派生版も存在したのでしょうか(笑) (他のLLMも色々と試してみましたが、猫が桃太郎の家来になっていたバージョンもありました) コード生成を試してみます(Pythonスニペットなど) 簡単な例です。 少し高度にしてみましょう。 期待以上に賢く応答しています。 画像をインプットしてみます 我が家の愛猫について まずまずな評価のようです。(愛猫の模様は”キジ白”なのですが、単一の写真だけでは正確には判断が難しいのでしょう) まとめ # 今回、LM Studioで Gemma を試した結果、次のような知見を得ました。 LM Studio は、LLMをローカル環境で動かすためのハードルを大幅に下げ、初心者から上級者まで使いやすい実行環境を提供しています。 Gemma LLM は、軽量ながらも幅広いタスクに対応でき、研究・学習からプロトタイピングまで十分に活用可能です。 ローカルLLMの利点(プライバシー確保・低コスト・オフライン利用・高速応答)を体感でき、クラウド依存では難しいユースケースに有効です。 さらに、今後の展望としては以下が挙げられます。 モデルのパーソナライズ 個人や組織ごとのデータで調整することで、より実用的な応用が期待できます。 開発環境への統合 エディタやIDEとの連携を強化すれば、コード補完やデバッグ支援ツールとしての価値が高まります。  (LM Studio には「ローカルAPIモード」があり、HTTP経由でリクエストを送れます) 複数モデルの比較活用 Gemma 以外のLLMと並行利用することで、用途ごとに最適な選択が可能になります。 img { border: 1px gray solid; }
はじめに:SimulinkとArduinoで始める「Lチカ」 # 「Lチカ」(LEDの点滅) は、ハードウェア制御の入門として最も基本的な実験です。 本記事では、 MATLAB/SimulinkとArduinoを連携させ 、LEDを点滅させるプログラムの作成方法を解説します。 開発環境の準備 # ソフトウェア MATLAB(バージョン:R2025a) Simulink(バージョン:25.1) アプリ(for Simulink) Simulink Support Package for Arduino Hardware(バージョン:25.1.0) ハードウェア Arduino Uno/Nano(または互換機) USBケーブル(PCとArduinoの通信用) (オプション) LED + 抵抗(330Ω程度) ブレッドボード、ジャンパワイヤ 環境構築の詳細手順: # 1. MATLAB と Simulink をインストール MathWorks の公式サイトからインストーラをダウンロードします ライセンス認証を行い、MATLAB と Simulink をインストールします インストール時に「Simulink」コンポーネントを忘れずにチェックします 2. Arduino Support Package を導入 MATLAB を起動 → メニューから 「アドオン」 → 「ハードウェアサポートパッケージの入手」 を選択します(アドオンエクスプローラーが起動します) 検索ボックスで 「Arduino」 と入力し、 Simulink Support Package for Arduino Hardware を選択します Install をクリックし、パッケージを導入します 導入完了後、Simulinkライブラリに「Simulink Support Package for Arduino Hardware」のブロックが追加されます 3. Arduino のシリアル通信確認 ArduinoボードをUSBでPCに接続すると、自動的にドライバが認識されます 認識されない場合は、デバイスマネージャ(Windowsの場合)や ls /dev/tty* (Mac/Linuxの場合)でポートを確認します(※下図はCOM7に接続した例) 回路の接続 # Arduino の 13番ピン と GND に LED + 抵抗を接続します。 (内蔵LEDを使う場合は外部配線は不要です) 回路図イメージ: (Arduino 13) ----[抵抗330Ω]----|>|(LED)---- (GND) Simulinkモデルの作成 # 新規モデルを作成 # Simulink を起動し、新しい「空のモデル」を作成します ブロックライブラリから以下を配置します: Pulse Generator(パルス波形を生成) Digital Output(Arduinoのピン出力):このブロックは「Simulink Support Package for Arduino Hardware」に含まれています。 配置したブロックを接続します: パラメータ設定 # 先ほど配置したブロックのパラメータを設定します。 Pulse Generator パルスタイプ:「サンプルベース」 時間:「シミュレーション時間を使用」 振幅:1 周期(サンプル数):1000 パルス幅(サンプル数):500 位相遅延(サンプル数):0 サンプル時間:0.001 ベクトルパラメータを1次元として解釈:チェックON Digital Output Pin番号:13 --> Information サンプル時間が「0.001」(秒)で、サンプル周期が「1000」なので、周期(時間)は「1(秒)」になります。 パルス幅を「500」にしているので、この設定では500ミリ秒毎にLEDがON/OFFを繰り返します。 モデル設定 # ハードウェア実行を以下のように設定します。 (今回、Arduino Nano互換機を使用しました。互換機のブートローダーが旧版だったため、アプリケーションダウンロードのボーレートが低くなっています) ハードウェア実行 パルスジェネレータの出力確認 # Arduinoにアプリケーションをアップロードする前に、パルスが正しく出力されているか確認します。 信号のログを設定します(接続線をクリックし、「信号のログ」を設定) ↓ 信号のログが設定されたことをアイコンで確認できます シミュレーションを実行します データインスペクタでパルス波形を確認できます 監視と調整(USBポート経由での実行確認) # USB経由でArduino Nanoにプログラムを転送し、正しくプログラムが動くか確認します。 ハードウェアタブから「監視と調整」を実行します(終了時間には「inf」を設定します。停止操作をするまで実行を続けます) Arduino の内蔵LEDが1秒周期で点滅すればモデルは正しく実行されています Arduinoへの書き込みと実行 # プログラムをArduinoにアップロードし、自動実行できるようにします。 Simulink の「ビルド、展開起動」を実行します コンパイル → Arduinoへ転送  転送が成功すると、以下のログが出力されます。  LEDが1秒ごとに点滅すれば、転送は成功です。 実行結果と考察 # わずか2つのブロックを接続するだけで、簡単にLED点滅プログラムを作成できました。 周期を短くすると「高速点滅」します(例えば、周期を100、パルス幅を50 など)。 パルス幅(Duty比)を変更することで、 明るさの制御(PWMの基本) にも応用できます。 まとめ # MATLAB/SimulinkとArduinoを組み合わせることで、 ブロック線図ベースで直感的に制御プログラムを開発できる ことがわかりました。 LED点滅は単純な例ですが、PWM制御・センサ入力・モータ制御などへ拡張可能です。 ただLEDを点滅させるだけだったら、Arduino IDEやPlatformIOなどの開発環境でプログラミングした方が早いと思いますが、今後複雑なプログラミングをしていく上で、MATLAB/SimulinkはMBD開発の強力なツールになると感じました。 今後は、自作ライブラリを作ったり、高度な周辺機器をつないで細かいプログラミングに挑戦していきたいと思います。 img { border: 1px gray solid; }