2026/04/15

Android PDF 作成と表示【完全ガイド】PdfDocument vs PdfRenderer【2026年版】

はじめに

Android アプリで PDF を作成・表示することは、レポート生成、請求書作成、ドキュメント共有など、実務的なアプリケーション開発で頻繁に必要になります。

この記事では、Android で PDF を扱う 2 つの主要な方法(PdfDocumentPdfRenderer)を完全解説します。2026 年時点での最新の API、Kotlin での実装、トラブルシューティングまでをカバーしています。

PdfDocument とは

概要

PdfDocument は、Android 5.0(API 21)で導入された PDF 生成用のクラスです。アプリケーション内でプログラマティックに PDF を作成・レンダリングできます。

  • 用途:PDF ファイルの作成(ジェネレーション)
  • 対応 API:API 19 以上
  • クラスandroid.graphics.pdf.PdfDocument

メリット・デメリット

項目 メリット デメリット
柔軟性 完全なカスタマイズが可能 実装が複雑
依存性 外部ライブラリ不要 Android Framework に依存
パフォーマンス 中程度(小~中規模 PDF) 大規模 PDF では遅延の可能性
用途 レポート、請求書、領収書作成 複雑なレイアウトは困難

PdfRenderer とは

概要

PdfRenderer は、Android 5.0(API 21)で導入された PDF 表示・レンダリング用のクラスです。既存の PDF ファイルを画像としてレンダリングして表示します。

  • 用途:PDF ファイルの表示・レンダリング
  • 対応 API:API 21 以上
  • クラスandroid.graphics.pdf.PdfRenderer

メリット・デメリット

項目 メリット デメリット
シンプル 実装が簡潔 カスタマイズが限定的
パフォーマンス 高速(ネイティブレンダリング) メモリ使用量が多い可能性
出力形式 高品質な画像として表示 PDF ファイル自体の編集は不可
用途 PDF ファイルの表示・閲覧 PDF 生成には不適

PdfDocument vs PdfRenderer 比較表

項目 PdfDocument PdfRenderer
目的 PDF 生成 PDF 表示
対応 API API 19+ API 21+
入力 アプリケーションコード PDF ファイル
出力 PDF ファイル ビットマップ画像
使用難度 中程度(複雑) 簡単
カスタマイズ性 高い 低い
パフォーマンス 中速 高速
主な用途 レポート、請求書、データエクスポート PDF ビューア、ドキュメント表示

実装方法

前提条件(環境セットアップ)

// build.gradle.kts (Module: app)
android {
    compileSdk = 34  // 2026年推奨:API 34-35

    defaultConfig {
        minSdk = 21    // PdfDocument/PdfRenderer対応の最小値
        targetSdk = 34
    }
}

dependencies {
    // Kotlin stdlib
    implementation("org.jetbrains.kotlin:kotlin-stdlib:1.9.0")
}

PdfDocument での実装例

用途:レポート、請求書、ドキュメント作成

// Kotlin での実装例(2026年版)
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.pdf.PdfDocument
import android.os.Environment
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

class PdfReportGenerator(private val context: Context) {

    suspend fun generatePdfReport(): File = withContext(Dispatchers.IO) {
        // PDF ドキュメント作成
        val pdfDocument = PdfDocument()
        val pageInfo = PdfDocument.PageInfo.Builder(595, 842, 1).create()
        val page = pdfDocument.startPage(pageInfo)
        val canvas = page.canvas

        // ページに描画
        val paint = Paint().apply {
            textSize = 16f
        }

        canvas.drawText("サンプル PDF レポート", 50f, 50f, paint)
        canvas.drawText("生成日時: 2026年4月15日", 50f, 100f, paint)
        canvas.drawText("内容: Android PDF 生成のデモンストレーション", 50f, 150f, paint)

        pdfDocument.finishPage(page)

        // ファイル保存
        val pdfFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "report.pdf")
        pdfDocument.writeTo(pdfFile.outputStream())
        pdfDocument.close()

        return@withContext pdfFile
    }
}

// 使用例
// val generator = PdfReportGenerator(context)
// val pdfFile = generator.generatePdfReport()

PdfRenderer での実装例

用途:PDF ファイルの表示・閲覧

// Kotlin での実装例(2026年版)
import android.content.Context
import android.graphics.Bitmap
import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor
import java.io.File

class PdfViewerUtil(private val context: Context) {

    fun renderPdfPage(pdfFile: File, pageNumber: Int): Bitmap? {
        return try {
            val fileDescriptor = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
            val pdfRenderer = PdfRenderer(fileDescriptor)

            // ページ数確認
            if (pageNumber >= pdfRenderer.pageCount) {
                return null
            }

            // ページをビットマップとしてレンダリング
            val page = pdfRenderer.openPage(pageNumber)
            val bitmap = Bitmap.createBitmap(page.width, page.height, Bitmap.Config.ARGB_8888)
            page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)

            page.close()
            pdfRenderer.close()

            bitmap
        } catch (e: Exception) {
            e.printStackTrace()
            null
        }
    }
}

// 使用例
// val viewer = PdfViewerUtil(context)
// val bitmap = viewer.renderPdfPage(pdfFile, 0)
// imageView.setImageBitmap(bitmap)

ライブラリを使った簡単実装

より高度な PDF 操作が必要な場合、以下のライブラリを検討してください。

iText(商用・オープンソース)

dependencies {
    // iText 5 (無料版)
    implementation("com.itextpdf:itextg:5.5.13.3")
}

PDFBox(Apache オープンソース)

dependencies {
    // Apache PDFBox
    implementation("org.apache.pdfbox:pdfbox-android:2.0.27.0")
}

権限設定(AndroidManifest.xml)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <!-- ファイル読み書き権限(Android 13以前)-->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

    <!-- Android 12+: Scoped Storage自動対応 -->

</manifest>

Kotlin での実装(2026年版 - モダン Android)

coroutine を使った非同期 PDF 生成

// Kotlin Coroutine での実装例
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch

class PdfGeneratorViewModel(private val generator: PdfReportGenerator) : ViewModel() {

    fun generatePdfAsync() {
        viewModelScope.launch {
            try {
                val pdfFile = generator.generatePdfReport()
                // UI 更新
                println("PDF 生成完了: ${pdfFile.absolutePath}")
            } catch (e: Exception) {
                // エラーハンドリング
                println("PDF 生成エラー: ${e.message}")
            }
        }
    }
}

// UI 層での使用例(Jetpack Compose)
@Composable
fun PdfGeneratorScreen(viewModel: PdfGeneratorViewModel) {
    Button(onClick = { viewModel.generatePdfAsync() }) {
        Text("PDF を生成")
    }
}

Android 12+ での変更点

Scoped Storage

Android 12 以降、外部ストレージへのアクセス方法が変わりました。

  • 変更点WRITE_EXTERNAL_STORAGE 権限が無視される
  • 推奨方法getExternalFilesDir() を使用(アプリ固有ディレクトリ)
  • 代替手段FileProvider または Intent.ACTION_CREATE_DOCUMENT
💡 推奨実装パターン:Android 12+ では getExternalFilesDir() を使用してアプリ固有ディレクトリにファイルを保存することが標準実装です。
// Android 12+ での推奨実装
val pdfDir = context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS)
val pdfFile = File(pdfDir, "report_${System.currentTimeMillis()}.pdf")

// ファイルの保存・共有
val fileUri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", pdfFile)

// 共有インテント
val shareIntent = Intent(Intent.ACTION_SEND).apply {
    type = "application/pdf"
    putExtra(Intent.EXTRA_STREAM, fileUri)
    addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}

よくあるエラーと対処法

FileNotFoundException

【エラーメッセージ】
java.io.FileNotFoundException: /storage/emulated/0/Documents/report.pdf (Permission denied)
【原因】
- ファイルの保存先に権限がない
- ディレクトリが存在しない
【対処法】
下記のように、Android 12+ 推奨の方法でファイルを保存してください。
// ❌ 間違い
val pdfFile = File("/sdcard/Documents/report.pdf")

// ✓ 正解(Android 12+ 推奨)
val pdfFile = File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "report.pdf")
// または
val pdfFile = File(context.cacheDir, "report.pdf")

SecurityException(Android 12+)

【エラーメッセージ】
java.lang.SecurityException: Permission Denial: opening provider android.content.ContentProvider
【原因】
- 外部ストレージへのアクセス権限不足
- FileProvider の設定不備
【対処法】
1. FileProvider を res/xml/file_paths.xml で設定
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-files-path name="documents" path="Documents/" />
</paths>

2. AndroidManifest.xml で宣言
<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="${applicationId}.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths" />
</provider>

OutOfMemoryError(大規模 PDF)

【エラーメッセージ】
java.lang.OutOfMemoryError: Failed to allocate [size] bytes
【原因】
- 複数ページの PDF を一度にメモリに読み込み
- 高解像度でレンダリング
【対処法】
// ページごとにレンダリング(メモリ効率化)
fun renderPdfPageSafely(pdfFile: File, pageNumber: Int): Bitmap? {
    return try {
        val fileDescriptor = ParcelFileDescriptor.open(pdfFile, ParcelFileDescriptor.MODE_READ_ONLY)
        val pdfRenderer = PdfRenderer(fileDescriptor)

        val page = pdfRenderer.openPage(pageNumber)

        // 低解像度でレンダリング(メモリ節約)
        val scale = 1.5f
        val bitmap = Bitmap.createBitmap(
            (page.width * scale).toInt(),
            (page.height * scale).toInt(),
            Bitmap.Config.RGB_565  // ARGB_8888 より軽い
        )
        page.render(bitmap, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)

        page.close()
        pdfRenderer.close()

        bitmap
    } catch (e: OutOfMemoryError) {
        e.printStackTrace()
        null
    }
}

実装例のリソース

関連記事

まとめ

Android で PDF を扱う場合、以下の判断基準で実装方法を選択してください。

  • PDF を生成したい場合PdfDocument を使用(または iText などのライブラリ)
  • PDF を表示・閲覧したい場合PdfRenderer を使用
  • 複雑な PDF 操作が必要な場合iTextPDFBox などのライブラリを導入

2026 年時点では、Kotlin + Coroutine での非同期処理が標準的な実装パターンです。また、Android 12 以降の Scoped Storage への対応は必須です。

この記事で紹介したコード例は、実際のプロジェクトに適応させて使用してください。不明な点や実装時のトラブルは、コメント欄でお気軽にお尋ねください。

2026/04/14

iOS UIViewController ライフサイクル完全ガイド【2026年UIKit + SwiftUI対応】

iOS UIViewControllerライフサイクル【2026年UIKit + SwiftUI 対応】

📌 関連記事: この記事は 【iOS】UIViewControllerのライフサイクル (Swift)(2019年)の2026年版アップデートです。UIKit に加えて SwiftUI 対応を追加しました。

2019年の記事では UIViewControlller のライフサイクルを紹介していた。2026年は状況が変わった。SwiftUI が標準になりつつあり、UIKit は「レガシー」扱いになりつつある。

ただ、既存プロジェクトや複雑な UI は UIKit が必須。両方理解すべき。


2019年 vs 2026年:フレームワークの立場

【2019年】

  • UIKit が主流
  • SwiftUI は登場したばかり(iOS 13)
  • ほぼ全員が UIKit を使っていた

【2026年】

  • SwiftUI が標準(iOS 16+対応アプリが主流)
  • UIKit は「既存プロジェクト対応」用途へ
  • 新規プロジェクト:SwiftUI 推奨
  • 既存プロジェクト:UIKit を理解する必要

UIKit:UIViewControllerのライフサイクル

全ライフサイクルメソッド(実行順序)

ビュー表示時

1. init(coder:) または init(nibName:bundle:)
   ↓
2. viewDidLoad()
   ↓
3. viewWillAppear(_:)
   ↓
4. viewWillLayoutSubviews()
   ↓
5. viewDidLayoutSubviews()
   ↓
6. viewDidAppear(_:)

ビュー非表示時

1. viewWillDisappear(_:)
   ↓
2. viewDidDisappear(_:)
   ↓
3. deinit (メモリから削除)

各メソッドの役割

class MyViewController: UIViewController {

    // ① 初期化時(1回のみ)
    override func viewDidLoad() {
        super.viewDidLoad()
        // UI の初期化、データの読み込み
        // 重い処理はここで実行 OK
    }

    // ② ビュー表示直前(毎回呼ばれる)
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // データの更新、画面リフレッシュ
        // 他の ViewController からの復帰時
    }

    // ③ レイアウト計算前
    override func viewWillLayoutSubviews() {
        super.viewWillLayoutSubviews()
        // Auto Layout の前処理
        // ビューのサイズが確定する前
    }

    // ④ レイアウト計算完了
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        // サイズに依存した処理
        // フレーム値が確定した後
    }

    // ⑤ ビュー表示完了
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // アニメーション開始
        // ネットワーク通信開始
        // センサー (GPS など) の監視開始
    }

    // ⑥ ビュー非表示直前
    override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        // タイマー停止
        // ネットワーク通信キャンセル
    }

    // ⑦ ビュー非表示完了
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // センサーの監視停止
        // リソース解放
    }

    // ⑧ メモリから削除
    deinit {
        // 最終クリーンアップ
        print("MyViewController が削除されました")
    }
}

SwiftUI:View のライフサイクル

SwiftUI では全く異なる

SwiftUI は 宣言型 UI で、UIViewControlller のような「ライフサイクルメソッド」は存在しない。

代わりに onAppear / onDisappear を使う。

struct ContentView: View {
    @State var count = 0

    var body: some View {
        VStack {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
        .onAppear {
            // ビュー表示時
            print("View appeared")
            // データ読み込み、API 呼び出しはここ
        }
        .onDisappear {
            // ビュー非表示時
            print("View disappeared")
            // リソース解放
        }
    }
}

SwiftUI のライフサイクル(概念的)

【初期化・表示】

  1. View 作成(body 計算)
  2. onAppear 実行
  3. ビュー表示

【状態変更】

  1. @State 更新
  2. body 再計算
  3. ビュー更新(自動)

【終了】

  1. onDisappear 実行
  2. View 削除

UIKit vs SwiftUI:ライフサイクル比較表

フェーズ UIKit SwiftUI
初期化 init(coder:) View 作成
初回ロード viewDidLoad() onAppear
表示前 viewWillAppear() なし(自動)
レイアウト viewWillLayoutSubviews() 自動(Combine)
表示完了 viewDidAppear() onAppear
非表示前 viewWillDisappear() なし(自動)
非表示完了 viewDidDisappear() onDisappear
終了 deinit View 削除

実装パターン:よくある用途別

UIKit版パターン 1:データの初期化

class UserProfileViewController: UIViewController {
    var user: User?

    override func viewDidLoad() {
        super.viewDidLoad()
        // ❌ ここで API 呼び出しは避ける
        // loadUser()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        // ✅ ここで最新データを取得(毎回)
        loadUser()
    }

    func loadUser() {
        API.fetchUser { [weak self] user in
            self?.user = user
            self?.updateUI()
        }
    }
}

SwiftUI版パターン 1:データの初期化

struct UserProfileView: View {
    @State var user: User?
    @State var isLoading = false

    var body: some View {
        VStack {
            if let user = user {
                Text(user.name)
            }
        }
        .onAppear {
            loadUser()
        }
    }

    func loadUser() {
        isLoading = true
        API.fetchUser { user in
            self.user = user
            isLoading = false
        }
    }
}

UIKit版パターン 2:センサーの監視開始・停止

class MapViewController: UIViewController {
    var locationManager: CLLocationManager?

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        // ✅ ビュー表示時に監視開始
        locationManager = CLLocationManager()
        locationManager?.startUpdatingLocation()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        // ✅ ビュー非表示時に監視停止
        locationManager?.stopUpdatingLocation()
    }
}

SwiftUI版パターン 2:センサーの監視開始・停止

struct MapView: View {
    @StateObject var locationManager = LocationManager()

    var body: some View {
        VStack {
            Text("Latitude: \(locationManager.latitude)")
        }
        .onAppear {
            locationManager.startUpdating()
        }
        .onDisappear {
            locationManager.stopUpdating()
        }
    }
}

class LocationManager: NSObject, ObservableObject {
    @Published var latitude = 0.0
    let manager = CLLocationManager()

    func startUpdating() {
        manager.startUpdatingLocation()
    }

    func stopUpdating() {
        manager.stopUpdatingLocation()
    }
}

UIKit版パターン 3:タイマー

class CountdownViewController: UIViewController {
    var timer: Timer?

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
            self.updateCountdown()
        }
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        timer?.invalidate()  // ❌ これを忘れるとメモリリーク
        timer = nil
    }
}

SwiftUI版パターン 3:タイマー

struct CountdownView: View {
    @State var count = 10
    @State var timer: Timer?

    var body: some View {
        Text("\(count)")
            .onAppear {
                timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
                    count -= 1
                }
            }
            .onDisappear {
                timer?.invalidate()  // 自動クリーンアップ
            }
    }
}

メモリリークを避けるためのベストプラクティス

UIKit

❌ メモリリーク:self をキャプチャしすぎ
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    API.fetchData { data in
        self.updateUI(data)  // self が保持され続ける
    }
}
✅ weak self で回避
override func viewDidAppear(_ animated: Bool) {
    super.viewDidAppear(animated)
    API.fetchData { [weak self] data in
        self?.updateUI(data)  // self が解放される
    }
}

SwiftUI

// SwiftUI は自動的に管理してくれる場合が多い
struct MyView: View {
    @StateObject var viewModel = MyViewModel()

    var body: some View {
        Text(viewModel.data)
            .onAppear {
                viewModel.load()  // 自動的に適切にハンドル
            }
    }
}

2019年 vs 2026年:推奨される実装方針

状況 2019年の推奨 2026年の推奨
新規プロジェクト UIKit SwiftUI
既存 UIKit プロジェクト UIKit 継続 UIKit 継続(段階的に SwiftUI 導入)
複雑な UI UIKit UIKit(SwiftUI の限界回避)
学習用 UIKit SwiftUI

まとめ

【2019年】

UIViewController のライフサイクルを理解 = iOS 開発の基本

【2026年】

  • 新規プロジェクト:SwiftUI を使い、onAppear/onDisappear で対応
  • 既存プロジェクト:UIViewController ライフサイクルは依然重要
  • 両方の理解が必須(業界の過渡期)

実装判断:

  • iOS 16+ のみ対応 → SwiftUI
  • iOS 15 以下対応が必要 → UIKit
  • iOS 14 以下対応が必要 → 必ず UIKit

参考資料

2026/04/13

Ollama + Open WebUI:ローカルLLM構築【2026年完全ガイド】

ローカルで LLM を動かすなら Ollama + Open WebUI が標準。2025年の古い記事では断片的だったが、2026年版は実装から運用まで全て網羅。


なぜローカル LLM なのか

【クラウド API(ChatGPT など)】

  • 毎回データが外に出る(セキュリティ問題)
  • API 代がかさむ(月数千~数万円)
  • 速度が遅い場合がある(ネットワーク遅延)

【ローカル LLM(Ollama)】

  • データが自分のサーバーだけ(セキュアー)
  • 無料(初期構築後、ランニング費用ほぼゼロ)
  • 速度が速い(ネットワーク遅延ゼロ)

必要な環境

ハードウェア

【最小構成】

  • CPU:4コア以上(Ryzen 5 相当)
  • メモリ:8GB(推奨 16GB)
  • ストレージ:30GB(モデルサイズ次第)

【GPU あると大幅に高速化】

  • Nvidia GPU:RTX 3060 以上推奨
  • AMD GPU:RX 6700 XT 以上推奨
  • Mac M1/M2:統合 GPU で十分

【GPU なし場合】

  • CPU のみでも動作(ただし遅い)
  • 推論速度:1トークン/秒程度
  • GPU あり場合:10~20倍高速

ソフトウェア

【必須】

  • Docker
  • Docker Compose

【推奨(GPU使用時)】

  • Nvidia GPU ドライバー
  • CUDA Toolkit 12.0+

インストール手順

ステップ 1:Docker & Docker Compose インストール

# Ubuntu/Debian
sudo apt-get update
sudo apt-get install docker.io docker-compose

# 権限設定(sudo なしで実行可能に)
sudo usermod -aG docker $USER
newgrp docker

ステップ 2:docker-compose.yml 作成

version: '3.8'

services:
  ollama:
    image: ollama/ollama:latest
    container_name: ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama
    environment:
      - NVIDIA_VISIBLE_DEVICES=all
    gpus:
      - driver: nvidia
        all: true  # GPU 全て使用
    restart: always

  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    container_name: open-webui
    ports:
      - "8080:8080"
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
    depends_on:
      - ollama
    restart: always
    volumes:
      - webui_data:/app/backend/data

volumes:
  ollama_data:
  webui_data:

ステップ 3:起動

docker-compose up -d

# ログ確認
docker-compose logs -f

ステップ 4:モデルのダウンロード

# Ollama コンテナに入る
docker exec -it ollama bash

# モデルをダウンロード
ollama pull llama2:7b
# または
ollama pull mistral:7b
# または
ollama pull neural-chat:7b

ステップ 5:Open WebUI にアクセス

ブラウザで http://localhost:8080 を開く
→ ユーザー登録(初回のみ)
→ モデル選択して利用開始

GPU 設定(高速化のため)

Nvidia GPU の場合

# CUDA Toolkit インストール
sudo apt-get install cuda-toolkit-12-0

# docker-compose.yml で GPU を有効化(上記の例参照)
services:
  ollama:
    gpus:
      - driver: nvidia
        all: true

GPU が認識されているか確認

# コンテナ内で確認
docker exec -it ollama bash
ollama run llama2:7b

# プロンプトで GPU 使用状況を確認
# "GPU acceleration enabled" と表示されたら OK

推奨モデル一覧(2026年)

モデル サイズ 速度 品質 用途
Llama 2 7B 4GB 速い 標準 汎用・最初の1択
Mistral 7B 4GB 高速 高い 日本語・コード
Neural Chat 7B 4GB 速い 高い 会話型・日本語
Llama 2 13B 8GB 中程度 高い より精度重視
Code Llama 7B 4GB 速い コード特化 プログラミング
Mistral Large 34GB 遅い 非常に高い 重い処理用

初心者推奨:Mistral 7B(バランス型)


よくある落とし穴

1. GPU が認識されない

# 確認コマンド
docker run --rm --gpus all nvidia/cuda:12.0-base nvidia-smi

# 出力がなければ、nvidia-docker が必要
sudo apt-get install nvidia-docker2
sudo systemctl restart docker

2. メモリ不足エラー

"OOM Killed" または メモリ不足エラーが出た場合

対策 1:小さいモデルを使う

ollama pull llama2:7b  # 7B は 4GB
# ではなく
ollama pull phi:2.7b   # 2.7B は 2GB

対策 2:スワップメモリを増やす

sudo fallocate -l 16G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

3. Open WebUI が Ollama に接続できない

# docker-compose.yml で
environment:
  - OLLAMA_BASE_URL=http://ollama:11434

# これが正しくないと接続失敗
# ホスト名は「ollama」(service 名)
# ポートは 11434(Ollama のデフォルト)

4. 応答が遅い(GPU 未使用の場合)

# GPU が使われているか確認
docker exec -it ollama ollama ps

# "GPU acceleration disabled" が出たら GPU 未認識
→ 上記「GPU が認識されない」の対策を実施

実装時のベストプラクティス

1. モデルの永続化

volumes:
  ollama_data:/root/.ollama  # ← ここにモデルが保存される

# コンテナ削除後も モデルが残る
docker-compose down  # コンテナ削除
docker-compose up -d  # 再起動(モデルダウンロード不要)

2. Web UI のセキュリティ設定

open-webui:
  environment:
    - OLLAMA_BASE_URL=http://ollama:11434
    # ローカルホストのみアクセス許可
    - WEBUI_AUTH_REQUIRED=true  # ユーザー認証必須

3. 複数モデルの管理

# ダウンロード済みモデルを確認
docker exec -it ollama ollama list

# 不要なモデルを削除
docker exec -it ollama ollama rm llama2:7b

4. バックアップ

# モデルデータをバックアップ
docker cp ollama:/root/.ollama ./ollama_backup

# WebUI のデータ(会話履歴)をバックアップ
docker cp open-webui:/app/backend/data ./webui_backup

トラブルシューティング

ポート競合

"Address already in use" エラーが出た場合

docker ps  # 既存コンテナを確認
docker stop <コンテナ ID>
docker-compose up -d  # 再起動

メモリリーク

# コンテナのメモリ使用量を監視
docker stats

# メモリが増え続ける場合、モデルを再ロード
docker restart ollama

ネットワーク接続の問題

# コンテナ間の通信確認
docker exec -it open-webui ping ollama

# 失敗する場合、ネットワークドライバを確認
docker network ls
docker network inspect <network_name>

2025年版との変更点

項目 2025年 2026年
モデルサイズ Llama 2 のみ 複数選択肢
GPU 設定 簡潔 詳細なトラブルシューティング
セキュリティ 未記載 ユーザー認証設定
バックアップ 未記載 手順明記
トラブルシューティング なし 充実

次のステップ

【1日目】

  • Ollama + Open WebUI をセットアップ
  • モデル 1つをダウンロード・テスト

【1週間後】

  • GPU 最適化を試す
  • 複数モデルを試して相性を確認

【1ヶ月後】

  • 実運用に向けて、セキュリティ設定を強化
  • API 化(他アプリから Ollama を呼び出し)を検討

参考資料

Android Jetpack Compose LazyGrid で斜めスクロール実装【2026年版】

Android 斜めスクロールView実装 ~2026年Jetpack Compose版

📌 関連記事: この記事は 【android】ScrollViewで縦横斜めにスクロール(2015年)の2026年版アップデートです。当時のコンセプトが、今どう実装されているかをご紹介します。

かつて「Androidで斜めスクロールって実装できないのか?」という問い合わせがあった。2015年時点では標準APIになく、自作が必須だった。今は Jetpack Compose の登場で、状況が大きく変わった。

2026年なら、Compose + LazyGrid で 1行で解決できる。


昔の問題(2015年)vs 現在(2026年)

2015年:框組みで解決しようとしていた時代

ScrollView(縦)+ HorizontalScrollView(横)
  ↓
マージして「斜めスクロール」を実現
  ↓
カスタムViewGroupを自作

当時は効果測定も「コンセプト段階」で、実装されていなかった。

2026年:Jetpack Compose なら標準

LazyVerticalGrid / LazyHorizontalGrid
  ↓
フリングスクロール、慣性スクロール完備
  ↓
nested scroll も自動対応

標準APIs の充実で、カスタム実装が不要になった。


2026年の実装:Jetpack Compose

ステップ 1:依存関係の追加

dependencies {
    implementation "androidx.compose.foundation:foundation:1.7.0"
    implementation "androidx.compose.material3:material3:1.3.0"
    implementation "androidx.activity:activity-compose:1.9.0"
}

ステップ 2:LazyGrid で斜めスクロール実現

@Composable
fun DiagonalScrollView() {
    LazyVerticalGrid(
        columns = GridCells.Fixed(3),
        modifier = Modifier
            .fillMaxSize()
            .padding(8.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        items(100) { index ->
            GridItem(index)
        }
    }
}

@Composable
fun GridItem(index: Int) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .height(120.dp),
        colors = CardDefaults.cardColors(
            containerColor = Color(0xFF6200EE)
        )
    ) {
        Box(
            modifier = Modifier.fillMaxSize(),
            contentAlignment = Alignment.Center
        ) {
            Text(
                text = "Item $index",
                color = Color.White,
                fontSize = 16.sp,
                fontWeight = FontWeight.Bold
            )
        }
    }
}

ステップ 3:複数方向スクロール(横 + 縦)

単純な縦スクロール + 横スクロール両対応が必要なら:

@Composable
fun BiDirectionalScroll() {
    LazyVerticalGrid(
        columns = GridCells.Fixed(4),
        modifier = Modifier
            .fillMaxSize()
            .horizontalScroll(rememberScrollState())
    ) {
        items(200) { index ->
            GridItem(index)
        }
    }
}
注意:
  • horizontalScroll()LazyVerticalGrid でも使用可能
  • フリングスクロール、慣性スクロールは自動対応
  • パフォーマンスは Compose の仮想化により最適化済み

実装時の落とし穴

1. NestedScrollConnection の設定を忘れずに

親スクロールと子スクロールが両方ある場合:

val nestedScrollDispatcher = remember { NestedScrollDispatcher() }

LazyVerticalGrid(
    columns = GridCells.Fixed(3),
    modifier = Modifier
        .nestedScroll(nestedScrollDispatcher.asNestedScrollConnection())
) {
    // ... items
}

2. パフォーマンス:アイテム数が大きい場合

❌ 悪い例:すべてのアイテムを描画
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
    repeat(10000) { index ->
        GridItem(index)  // 全項目メモリに残る
    }
}
✅ 良い例:仮想化による遅延描画
LazyVerticalGrid(columns = GridCells.Fixed(3)) {
    items(10000) { index ->
        GridItem(index)  // 見える範囲だけ描画
    }
}

3. スクロール位置の保存

ユーザーが別の画面から戻ってきた時、スクロール位置を復元:

val listState = rememberLazyGridState()

LazyVerticalGrid(
    columns = GridCells.Fixed(3),
    state = listState
) {
    items(100) { index ->
        GridItem(index)
    }
}

// スクロール位置を保存
LaunchedEffect(listState) {
    snapshotFlow { listState.firstVisibleItemIndex }
        .collect { index ->
            // ローカルDBに保存
            saveScrollPosition(index)
        }
}

2015年実装 vs 2026年実装:比較

項目 2015年 2026年
フレームワーク View(XML + Java) Jetpack Compose(Kotlin)
コード行数 100~200行 30~50行
パフォーマンス メモリ負荷大(全アイテム保持) 軽量(仮想化)
実装難度 中~高(カスタムViewGroup必須) 低(標準APIで解決)
フリングスクロール 手実装 自動対応
nested scroll対応 複雑 シンプル
テスト容易性 高(Composable テスト用ツール充実)

代替案:特殊な要件がある場合

要件 1:斜め45度のカスタムスクロール

@Composable
fun DiagonalCustomScroll(
    angle: Float = 45f  // 45度斜めスクロール
) {
    LazyVerticalGrid(
        columns = GridCells.Fixed(3),
        modifier = Modifier
            .fillMaxSize()
            .rotate(angle)  // 斜めに回転
    ) {
        items(100) { index ->
            GridItem(index)
        }
    }
}

要件 2:スクロール速度をカスタマイズ

@Composable
fun CustomSpeedScroll() {
    val scrollState = rememberScrollState()

    Column(
        modifier = Modifier
            .verticalScroll(
                scrollState,
                flingBehavior = ScrollableDefaults.flingBehavior()
            )
    ) {
        repeat(100) { index ->
            Text("Item $index", modifier = Modifier.padding(16.dp))
        }
    }
}

まとめ

2015年:「カスタムViewGroup自作が必須」

2026年:「Compose の LazyGrid で 30行で完成」

Android 開発は急速に進化している。かつての「困難な実装」が「標準機能」に変わるのは珍しくない。

古い記事の実装方法に固執するのではなく、2026年のプラットフォーム能力を活用する方が圧倒的に効率的。


参考資料

最近、Excel で選択したセルの内容を Ollama で要約・添削できる Add-in を作ってみた

最近、Excel で選択したセルの内容を Ollama で要約・添削できる Add-in を作ってみた。

正直なところ、想定外の落とし穴がいっぱいあった。特に「ローカルネットワーク別 PC の Ollama に、Excel Add-in から安全にアクセスする」という部分が、予想以上に複雑だった。その試行錯誤プロセスを記録しておく。


背景:なぜこんなもの作ったのか

仕事で長めの文章を扱うことが多い。メール、レポート、提案書とか。

最初は ChatGPT API で要約・添削をやってみたけど、3 つの理由でやめた:

  1. データを外に出したくない - 機密文書を API に送るのは避けたい
  2. API コストが地味に積もる - 毎日使うと月額が結構かかる
  3. レスポンス待ちが遅い - 社内 LAN 上なら圧倒的に速い
そこで目を付けたのが Ollama。ローカルネットワーク別 PC で Ollama を建てて、そこに直接アクセスできれば、データも保護できるし、レスポンスも速い。

「Excel から直接使えたら最高だな」と思いついて、Add-in を作り始めたのが始まり。


技術構成

実装してみた構成は以下:

【Excel Add-in】
  ↓ HTTPS でロード
https://localhost:8888(React UI の配信)
  ↓ HTTP リクエスト
【Go バックエンド】(localhost:8888)
  ↓ ローカルネットワーク経由
【Ollama API】(http://172.19.10.10:11434)

バックエンド: Go(標準 net/http パッケージ)

  • シンプルに HTTP サーバーを立てるだけなので、フレームワークは不要だと判断
  • 自己署名証明書での HTTPS 対応

フロント: React

  • Office JavaScript API で Excel セルの読み書き
  • Go API への HTTP 通信

Ollama

  • ローカルネットワーク別 PC 上で実行
  • セルの内容を送信 → 要約・添削結果を取得

予想外の落とし穴:Excel Add-in の HTTPS 必須ポリシー

実装し始めてすぐにぶつかったのが、Excel Add-in は HTTP ではなく HTTPS が必須という制約。

「localhost で動かすんだから HTTP でいいか」と思ってた。甘かった。

さらに複雑だったのが、Ollama が ローカルネットワーク別 PC にあること。

構図:
  PC A(Excel を使ってる)
    ↓
  自分の PC(Go + React のサーバー)
    ↓ ローカルネットワーク
  PC B(172.19.10.10 に Ollama が動いてる)

やりたいこと:

  • Excel Add-in の UI(HTML/React)を自分の PC から HTTPS で配信
  • Add-in から Ollama(別 PC)に HTTP でアクセス

問題:

  • Excel Add-in は HTTPS 配信が必須だけど、自己署名証明書では「信頼されていない」と判定される
  • 別 PC の Ollama にはネットワーク経由でアクセスする必要がある

解決策 1:オレオレ証明書を localhost で動かす

まず自己署名証明書を生成した。

openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes \
  -subj "/CN=localhost"

この証明書と秘密鍵で、Go の net/http サーバーを立ち上げる。

package main

import (
    "net/http"
)

func main() {
    // ハンドラーを登録
    http.HandleFunc("/api/summarize", handleSummarize)
    http.HandleFunc("/api/proofread", handleProofread)

    // HTTPS で起動(自己署名証明書使用)
    http.ListenAndServeTLS(":8443", "server.crt", "server.key", nil)
}

これで https://localhost:8443 で HTTPS サーバーが起動。

ただし、ここまでではまだ不十分。Excel Add-in のコンテキストでは、この自己署名証明書が「信頼されていない」と判定される。


解決策:自己署名証明書 + ネットワークルーティング

ここが工夫の見せどころ。

2 つのポイントで解決した:

  1. localhost で自己署名証明書を使った HTTPS サーバー起動 - Excel Add-in の HTTPS 必須要件を満たす
  2. Go バックエンド - HTML/React を配信しつつ、Ollama への API リクエストを中継
【構図】
Excel Add-in
  ↓ HTTPS でロード
https://localhost:8888
  ↓ HTML/React の配信 + API エンドポイント
【Go サーバー】
  ├─ /(HTML/React UI を配信)
  └─ /api/summarize, /api/proofread(Ollama に転送)
    ↓ HTTP でローカルネットワーク経由
http://172.19.10.10:11434
  ↓
【Ollama API】

実装

まず自己署名証明書を生成。

openssl req -x509 -newkey rsa:4096 -keyout server.key -out server.crt -days 365 -nodes \
  -subj "/CN=localhost"

Go のサーバーコード:

package main

import (
    "net/http"
    "net/http/httputil"
    "net/url"
)

func main() {
    // Ollama API へのリバースプロキシを設定
    ollamaURL, _ := url.Parse("http://172.19.10.10:11434")
    reverseProxy := httputil.NewSingleHostReverseProxy(ollamaURL)

    // HTML/React UI を配信
    fs := http.FileServer(http.Dir("./public"))
    http.Handle("/", fs)

    // /api/generate は Ollama に中継
    http.HandleFunc("/api/generate", func(w http.ResponseWriter, r *http.Request) {
        reverseProxy.ServeHTTP(w, r)
    })

    // HTTPS で起動(自己署名証明書)
    http.ListenAndServeTLS(":8888", "server.crt", "server.key", nil)
}

React 側から Ollama API を呼び出すときは、相対パス /api/generate を使う。

// React コンポーネント
const handleSummarize = async (cellContent) => {
    const response = await fetch('/api/generate', {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            model: 'llama2:7b',
            prompt: `要約してください:\n\n${cellContent}`,
            stream: false
        })
    });

    const result = await response.json();
    return result.response;
};

重要なポイント:

  • localhost:8888 で自己署名証明書を使った HTTPS サーバーを起動
  • Excel Add-in のセキュリティ要件(HTTPS 配信)を満たす
  • React は相対パス /api/generate で Ollama に通信(実際には Go バックエンドが仲介)
  • Go バックエンド内で、localhost から 172.19.10.10 へのネットワークルーティングを実現
  • シンプル:プロキシツール不要で、Go だけで実装完結

セルを選択→要約・添削:実装の流れ

実装が決まったら、次は実用機能。

Excel で文章が入ってるセルを選択して、「要約」「添削」ボタンを押すと、Ollama で処理してセルに結果を返す。

流れ:

  1. セル選択時のイベント取得
await Excel.run(async (context) => {
    const cell = context.application.getSelectedData();
    // セルの内容を取得
});
  1. Go API に送信
const summarized = await fetch('https://localhost:8888/api/summarize', {
    method: 'POST',
    body: JSON.stringify({ text: cellContent })
});
  1. Ollama から要約結果を取得
  • モデルはデフォルトで llama2:7b を使用
  • プロンプトエンジニアリングで「要約」「添削」を区別
  1. 結果をセルに書き込み
await Excel.run(async (context) => {
    context.worksheets.getActiveWorksheet()
        .getRange("A1")
        .values = [[result]];
});

実装時の工夫:

  • ローディング表示 - Ollama はレスポンスに数秒かかるので、「処理中...」を表示
  • エラーハンドリング - ネットワーク接続や Ollama が落ちてる場合の対応
  • タイムアウト設定 - 長すぎる文章は処理に時間がかかるので、上限を設ける
  • プロンプト最適化 - 同じモデルでも「要約」「添削」でプロンプトを変える
// 要約用プロンプト
func getSummarizePrompt(text string) string {
    return fmt.Sprintf(
        "以下のテキストを簡潔に要約してください。箇条書きで3点まで。\n\n%s",
        text,
    )
}

// 添削用プロンプト
func getProofreadPrompt(text string) string {
    return fmt.Sprintf(
        "以下のテキストの文法、表現、敬語を確認して、改善案を提示してください。\n\n%s",
        text,
    )
}

実際に使ってみて

完成して 1 ヶ月ほど使ってみた感想。

便利だったこと:

  1. データが外に出ない - 機密文書を扱うときは本当に安心。API に送ってる時間を気にしなくていい
  2. レスポンスが速い - ローカルネットワーク経由なので、ChatGPT API より圧倒的に高速。待ち時間がストレスにならない
  3. コストがゼロ - Ollama はローカル実行なので追加コストなし。PC のリソース(GPU)を使ってる分だけ

予想外だったこと:

  • 要約の質が思ったより良い - llama2:7b で十分実用的。わざわざ大型モデルに乗り換える必要がない
  • 添削より要約の方が使用頻度が高い - 最初は「添削機能メインで作ろう」と思ってたけど、実用では「要約」がメイン用途に
  • 複数人での利用が想定より難しい - 自分の PC でプロキシを立ててるので、他の PC からはアクセスできない。チーム展開するなら、プロキシを別サーバーに移す必要があった

今後の改善案:

  • Ollama の複数モデル対応 - llama2 以外の軽量モデル(mistral など)も選べるようにしたい
  • Web UI での設定画面 - 今は Go コード内でハードコードしてる設定を、UI で変更できるように
  • ローカルプロキシをサーバー化 - チーム利用を想定して、プロキシを別サーバーに移す

技術的な学び

このプロジェクトで学んだことが 3 つ。

1. Excel Add-in のセキュリティ要件は思ったより厳しい

Excel が HTTPS・自己署名証明書・CORS まで厳密にチェックする。SPA(Single Page Application)の世界と違って、企業向けアプリケーションの厳格さを感じた。

2. ローカル接続は「仲介役」として超有効

別ネットワークにある Ollama にアクセスする時、直接アクセスではなくローカルサーバー経由にすることで、セキュリティと利便性のバランスが取れた。

3. ローカル LLM の強さ

API の待ち時間がゼロに等しく、コストもかからない。確度は ChatGPT より落ちるかもしれないが、実務用途(要約・添削)には十分。「LLM の大型化よりローカル化の時代が来たのかも」と感じた。


まとめ

「Go + React + Ollama」という組み合わせで Excel Add-in を作った。

最大の工夫は「自己署名証明書 + ネットワークルーティング」という組み合わせで、ローカルネットワーク別 PC の Ollama に安全にアクセスする仕組み。

完成してみると、想定外の利便性があった。特に「データが絶対外に出ない」という安心感と「レスポンスの速さ」は、クラウド API では代替できない価値。

チーム展開や本番運用となると、プロキシのサーバー化やアーキテクチャの見直しが必要だけど、個人ツールレベルなら「自己署名証明書 + ネットワークルーティング」で十分実用的。

ローカル LLM の活用方法は、これからも増えていくんだろうなと思った。

2026/03/31

【緊急】axiosがサプライチェーン攻撃に遭遇:汚染バージョン(1.14.1 / 0.30.4)と今すぐ取るべき対策

2026年3月31日、npm パッケージ「axios」がサプライチェーン攻撃の被害に遭った。

単なる「脆弱性が見つかった」ではなく、axios のメンテナアカウントが侵害されて、悪意あるバージョンが npm に直接公開された。つまり、ダウンロードして npm install しただけで、マルウェアが仕込まれる可能性がある。

この記事は、「うちの環境で使ってるけど、どうしたらいい?」という判断フローを書いておく。


事件の概要

何が起きたのか

axios のメンテナアカウント(複数疑い)が侵害され、以下の 2 つのバージョンが悪意あるコードを含んで npm に公開された:

  • axios@1.14.1(最新安定版)
  • axios@0.30.4(レガシー版)

どちらも、同じ手口でマルウェアが混入していた。

どのくらい危険か

めっちゃ危険。理由:

  1. axios は超ポピュラー - JavaScript/Node.js 開発で最も使われている HTTP クライアントライブラリ
  2. 直接実行される - npm install 時に postinstall フック で自動実行
  3. 複数プラットフォーム対応 - Windows/Mac/Linux すべてに対応したマルウェア

実際に axios のダウンロード数は週 2000 万超。2~3 時間の公開期間でも、数千~数万の開発者が被害に遭った可能性がある。


技術的な詳細

マルウェアの動作

npm install 時に postinstall フック が実行され、以下の処理が起きる:

  1. プラットフォーム判定 - OS(Windows/Mac/Linux)を自動判定
  2. マルウェア配置 - 各 OS の隠蔽しやすい場所に RAT(リモートアクセストロージャン)を配置
  3. C2 サーバーへの接続 - sfrclak[.]com:8000 へ接続
  4. トレース消去 - node_modules から証拠を削除

各 OS でのマルウェア配置先

OS 配置先 特徴
macOS /Library/Caches/com.apple.act.mond キャッシュフォルダに偽装
Windows %PROGRAMDATA%\wt.exe, %TEMP%\6202033.vbs システムフォルダに混在
Linux /tmp/ld.py /tmp に配置(起動時消滅する可能性)

含まれていた悪意あるパッケージ

公式記述では「plain-crypto-js」という偽パッケージ。これが dependencies に追加されて、npm install 時に自動ダウンロード・実行される。


うちは大丈夫?:判断フロー

ステップ 1:package.json / package-lock.json を確認

# 正確な確認方法
grep -E "axios.*1\.14\.1|axios.*0\.30\.4" package-lock.json

もし上記コマンドで何か出たら要注意。

ステップ 2:実際にインストールされているバージョンを確認

npm list axios

出力例:

└── axios@1.14.0

1.14.0 以下、または 0.30.3 以下なら OK。1.14.1 または 0.30.4 なら危険。

ステップ 3:「うちのチームはいつこれをインストールしたのか?」を確認

npm install したのが 3月31日の 2~3 時間の間 だったかどうかで、被害の可能性が変わる。

  • 3月31日より前にインストール: 被害なし
  • ⚠️ 3月31日 12:00~15:00 GMT 付近でインストール: 被害の可能性あり
  • 3月31日 15:00 以降でインストール: 被害なし(既にパッチ済み)

リスク評価:うちは被害に遭ったのか?

ケース 1:インストールしたけど実行環境が本番ではない(社内 PC のみ)

リスク度: 中~高

対応:

  1. 該当の PC のマルウェアをスキャン(下記参照)
  2. API キー・パスワードをすべてローテーション
  3. git commit / push のログをチェック(悪意あるコードがコミットされていないか)

ケース 2:本番環境で実行中

リスク度: 超高

すぐに以下を実行:

  1. サーバー再起動
  2. 認証情報のすべてをローテーション(API キー、DB パスワード、SSH キーなど)
  3. アクセスログ確認sfrclak[.]com への外部通信がなかったか)
  4. セキュリティ監査(外部セキュリティ企業に委託検討)

ケース 3:Docker / CI/CD パイプラインで npm install を実行

リスク度: 超高

  • Docker イメージが作成されて、本番環境にデプロイされている場合
  • GitHub Actions などの CI パイプラインで実行してた場合
  • → 該当期間の全デプロイを疑う

対応:

  1. 該当期間の全デプロイを一覧化
  2. 本番環境から当該バージョン削除
  3. 全認証情報ローテーション

すぐにやるべきこと:修復手順

ステップ 1:axios をダウンロードグレード

npm install axios@1.14.0 --ignore-scripts

なぜ --ignore-scripts が必要か

  • もし 1.14.1 がまだキャッシュに残ってたり、中途半端な状態だと危険
  • グレードダウン時も postinstall フック を無視して安全にする

ステップ 2:node_modules の手動確認

# 疑わしいファイルを探す
find node_modules -name "plain-crypto-js" -type d
find node_modules -name "*.vbs" -o -name "*.exe" -o -name "*.py"

見つかったら、即座に削除:

rm -rf node_modules/plain-crypto-js
npm ci  # package-lock.json から再インストール

ステップ 3:マルウェアスキャン(PC にインストールされている場合)

Windows:

Start-MpScan -ScanType FullScan

macOS:

# マルウェアバイトスキャン推奨
# https://www.malwarebytes.com/mac から Malwarebytes ダウンロード・実行

Linux:

sudo freshclam  # パターンアップデート
sudo clamscan -r /tmp /home /root --remove

ステップ 4:全認証情報のローテーション

感染可能性がある環境では、以下をすべてローテーション:


本番環境への影響判定

チェックリスト

すべて「Yes」に近いなら、セキュリティ監査推奨。


実装側の判断:パッケージアップデートはいつする?

axios の対応タイムライン:

バージョン リリース 判定
0.30.3 以下 2026/3/31 18:00 ✅ 安全
1.14.0 以下 2026/3/31 18:00 ✅ 安全
1.14.1 2026/3/31 12:00-15:00 ❌ 危険
0.30.4 2026/3/31 12:00-15:00 ❌ 危険
1.14.2 以上 2026/3/31 15:00 ✅ パッチ済み
0.30.5 以上 2026/3/31 15:00 ✅ パッチ済み

推奨

  • 本番環境: 1.14.2 以上 または 0.30.5 以上 に即座にアップデート
  • 開発環境: 余裕があれば同じく最新版へ

今後のためのセキュリティ対策

npm パッケージ監視ツールの導入

# npm audit で定期チェック
npm audit

# dependabot で自動 PR 生成(GitHub)
# Settings → Code security and analysis → Enable Dependabot

CI パイプラインでのセキュリティスキャン

# npm audit を CI に組み込む
npm audit --audit-level=moderate

package-lock.json の厳密管理

# 本番環境では必ず --frozen-lockfile を使う
npm ci --frozen-lockfile
# 決して npm install を使わない

最後に:この事件が教えてくれること

サプライチェーン攻撃は「あり得ない」ではなく「あり得る」。

axios は信頼性の高いパッケージだが、それでも被害に遭った。理由は、人気が高いがゆえに、攻撃者にとって「おいしいターゲット」だったから。

対策:

  1. すべてのパッケージを信用しない - npm audit を常態化
  2. 本番環境を隔離 - 必ず --frozen-lockfile を使う
  3. credentials の定期ローテーション - 3~6ヶ月ごと
  4. ネットワークログの監視 - 不審な外部接続を検出

2026 年も、サプライチェーン攻撃の脅威は続く。

2026/03/26

SSL証明書の有効期限短縮ショック…手動更新の限界と自動化へ

CA/Browser Forum の新ルール:2026年から始まる SSL 証明書の有効期限の段階的短縮化。

最終的には 2029年に 47 日 という極めて短い有効期間になる。これまで「年 1 回更新」で済んでたやつが、近い未来「月複数回更新」になる可能性がある。

正直、手動管理では確実に破綻する。この記事は、「うちのチーム、どう対応すればいい?」という実装判断を書いておく。


タイムラインの全貌

CA/Browser Forum が定めた段階的短縮化

時期 有効期限 管理頻度目安 影響度
現在(2026年3月まで) 398 日 年1回更新
2026年3月15日~ 200 日 年1~2回更新
2027年3月15日~ 100 日 年3~4回更新
2029年3月15日~ 47 日 月複数回更新 超高

2029年の恐怖:47日制限とは

47日という期間は、現在の管理方法では実質的に自動化が必須を意味する。

なぜなら:

  • 人間が「そろそろ更新しようか」と判断→実行 → 検証 → デプロイ
  • この一連が確実に完了するまでに、確実に 1~2週間かかる
  • 47日 - 2週間 = 残り約5週間の余裕
  • その間に他の作業が入ると、簡単に「あ、更新忘れた」になる

問題の本質:「手動管理の破綻」

ケース 1:小規模チーム(1~3人)

現状

  • 証明書が 1~3個
  • 年1回のリマインダーで更新

2029年以降

  • 証明書が同じ数でも、更新頻度が 約 9 倍
  • 月に複数回、毎回「更新→検証→デプロイ」の手作業
  • 誰か 1人が退職したら、知識が失われてヤバい

結果:確実に誰かが忘れる → SSL エラー → サービス停止 → 信用喪失

ケース 2:中規模チーム(複数サーバー、複数ドメイン)

現状

  • 証明書が 10~20 個
  • スプレッドシートで管理

2029年以降

  • 月に 10~20 個 × 複数回の更新作業
  • 手作業では追いつかない
  • ミスの確率が指数関数的に増加

結果:管理が混乱 → 本番環境で SSL エラー → 顧客影響

ケース 3:大規模企業(複数部門、複数国)

現状

  • 証明書が 100 個超
  • 既に何かしらの管理システムで対応

2029年以降

  • 手動ステップが多いと管理システム自体がボトルネック
  • 完全自動化への移行が必須

解決策:ACME プロトコルによる完全自動化

根本的な対応:自動更新の導入

2029年の 47 日制限に耐えられる唯一の方法は、ACME プロトコルを使った完全自動更新

ACME とは:

  • Automated Certificate Management Environment
  • Let's Encrypt が提唱した、証明書の自動更新プロトコル
  • OpenSSL コマンドで certbot を使うことが最も一般的

実装方法:Certbot を使った自動更新

ステップ 1:Certbot のインストール

# Ubuntu/Debian
sudo apt-get install certbot python3-certbot-nginx

# CentOS/RHEL
sudo yum install certbot python3-certbot-nginx

# macOS
brew install certbot

ステップ 2:証明書の自動更新設定

# 証明書を取得(初回)
sudo certbot certonly --nginx -d example.com -d www.example.com

# 自動更新の動作確認
sudo certbot renew --dry-run

ステップ 3:cron で定期実行

# root ユーザーで crontab を編集
sudo crontab -e

# 以下を追加(毎日 2:30 に更新試行)
30 2 * * * /usr/bin/certbot renew --quiet && systemctl reload nginx

この設定で:

  • 毎日自動で更新チェック
  • 更新が必要なら自動実行
  • Nginx をリロード
  • すべて無人で完結

メジャーな証明書発行者の ACME 対応

CA ACME サポート URL 推奨度
Let's Encrypt https://letsencrypt.org ⭐⭐⭐⭐⭐
GlobalSign ACME API あり ⭐⭐⭐⭐
DigiCert ACME API あり ⭐⭐⭐⭐
Sectigo ACME API あり ⭐⭐⭐⭐

推奨:Let's Encrypt でテストしてから、有料 CA に移行。


段階的な導入ロードマップ

Phase 1:現状把握(今月)

- ドメイン名 / 発行元 CA / 有効期限 / 更新予定日

- 誰が更新してるのか / どのサーバーが対象か / 更新後の検証手順は

Phase 2:Let's Encrypt で試験運用(1~2ヶ月)

- インストール / 自動更新の動作確認 / 運用手順の整理

- Let's Encrypt の証明書を取得 / 1ヶ月運用してみる / 特に問題ないか確認

Phase 3:段階的な本番環境への展開(2~4ヶ月)

- 更新スクリプトの改善 / 監視・アラート設定 / トラブル対応の経験値を貯める

- 100日制限への対応が必須になる前に

Phase 4:完全自動化(2027年まで)

- ACME ベースの統一管理 / 中央管理ダッシュボード / 異常時のアラート設定


実装時の注意点

1. Let's Encrypt のレート制限

Let's Encrypt は無料だが、制限がある:

  • 週あたり 50 個の新規証明書
  • 1 ドメインあたり 5 個/週

複数ドメインで同時に導入すると、レート制限に引っかかる可能性がある。段階的導入が大事。

2. DNS 検証 vs HTTP 検証

Certbot の検証方法は 2 種類:

方法 特徴 推奨度
HTTP 検証 .well-known フォルダで検証。シンプル ⭐⭐⭐⭐⭐
DNS 検証 DNS レコード追加で検証。ワイルドカード対応 ⭐⭐⭐⭐

通常は HTTP 検証で十分。DNS 検証が必要なのはワイルドカード証明書の場合。

3. 更新スクリプトのテスト

本番環境を止めないために:

sudo certbot renew --dry-run --non-interactive

# このコマンドを cron で定期実行して
# 「更新が失敗していないか」を事前に検知する

4. 監視・アラート設定

ACME 自動化しても「更新に失敗する」可能性はある。例:

  • DNS が一時的に応答しない
  • サーバーのディスク容量が満杯
  • スクリプトのバグ

対策:

30 2 * * * /usr/bin/certbot renew --quiet || \
  mail -s "SSL renewal failed" admin@example.com

コスト評価

導入前(手動管理)

項目 金額 説明
証明書購入費 月 0~10万円 CA による
人件費(更新作業) 月 2~5万円 年1回なら月割り
合計 月 2~15万円

導入後(ACME 自動化)

項目 金額 説明
証明書購入費 月 0~10万円 CA による(Let's Encrypt なら ¥0)
人件費(初期構築) 50~100万円 1 回限り
人件費(運用・監視) 月 1~3万円 異常時対応のみ
合計 初期 50~100万円 + 月 1~13万円

ROI

  • 初期投資が回収できるのは、約 3~6ヶ月
  • 5 年運用なら 年平均で 15~20万円削減

実装チェックリスト

今月やること

来月やること

3ヶ月以内にやること

2027年3月までにやること


最後に:なぜこんなことになるのか?

CA/Browser Forum が証明書有効期限を短縮化した背景:

  1. セキュリティ向上 - 秘密鍵の露出リスクを早期に低減
  2. 自動化の推進 - 手動管理では対応できないレベルに引き上げることで、業界全体の自動化を加速
  3. 脆弱性対応の高速化 - 有効期限が短ければ、脆弱性検出時に「全サーバーの置き換え」が現実的

つまり、業界全体を「手動管理」から「自動化」へ強制的にシフトさせるための施策。

2029年の 47日制限は、もはや「手動では対応不可」という業界の合意。

早めに自動化しておけば、慌てずに対応できる。逆に、2028年になって「あ、対応しなきゃ」って思っても、もう手遅れ。

今から準備しておこう。

2026/03/20

YouTube LiveのチャットだけをAndroidで別画面表示するアプリを作ってリリースした

YouTube LiveのチャットだけをAndroidで別画面表示するアプリを作ってリリースした

テレビやモニターでYouTube Liveを全画面再生していると、ライブチャットが見えなくなります。スマホでYouTubeアプリを開き直しても動画が再生されてしまう。「Live Chatだけをスマホで別画面表示したい」という需要、ライブ配信を見る人なら絶対あると思っていました。そのまま自分で作ってリリースしました。

作ろうと思ったきっかけ

大画面でライブ配信を見ているとき、Live Chatの流れをリアルタイムで追いたいことがよくあります。でもテレビのフルスクリーン表示ではチャット欄が隠れてしまう。スマホでYouTubeアプリを開き直すと動画が二重再生になって邪魔。

既存のアプリを探してみたのですが、ちょうどいいものが見つからなかったので、自分で作ることにしました。シンプルに「URLを登録したらLive Chatだけが表示される」、それだけのアプリです。

アプリでできること

  • YouTube LiveのURLを登録してLive Chatのみをリアルタイム表示する
  • YouTubeアプリのシェアボタンから直接URLを送って登録できる
  • 動画を再生せずチャット画面だけを表示する
  • テレビ・モニターでライブを見ながらスマホでチャットを追える
ポイント

YouTubeアプリのシェアボタン経由で登録できるのが地味に便利です。URLをコピペする手間がなく、見ているライブ配信をそのまま登録してチャット画面を開けます。

使い方

  1. テレビやモニターでYouTube Liveをフルスクリーン再生する
  2. スマホのYouTubeアプリで同じライブ配信を開き、シェアボタンをタップ
  3. 共有先から「ChatTube」を選択
  4. アプリにLive Chatがリアルタイムで表示される

もちろんURLを直接入力して登録することもできます。動画IDだけでも対応しています。

こんな場面で使える

  • ライブ配信をテレビで見ながら、スマホでLive Chatをリアルタイムに追う
  • PC作業中に別モニターでライブ再生しつつ、スマホでチャットだけ確認する
  • 家族と大画面でライブを見ながら、自分だけチャットを追いたいとき
  • 配信者がチャットの反応を別画面で確認しながら配信するとき
セカンドスクリーン活用

テレビのリモコンでYouTubeを操作しているとチャット欄へのアクセスが面倒です。このアプリを使えばスマホをLive Chat専用画面として使えます。

ダウンロード

Android向けに Google Play で公開しています。無料でダウンロードできます(広告あり)。

Google Play で「ChatTube」を見る

不具合・要望があればコメントかレビューで教えてもらえると助かります。使ってみた感想もぜひ。