2026年3月28日土曜日

Ubuntu で pam 認証を有効にする

Ubuntu で pam 認証を有効にする

概要

PAM (Pluggable Authentication Modules) は通常の sshd 認証と違い外部の認証機構などを使って認証する仕組みです
sshd と組み合わせることで OTP などを実現できます
今回は Ubuntu 24.04 で pam を設定する方法を紹介します

環境

  • macOS 26.3.1
  • docker 29.3.0
    • Ubuntu 24.04

pam.d/sshd

# PAM構成の基本
auth       required     pam_unix.so nullok_secure
account    required     pam_unix.so
session    required     pam_unix.so
session    required     pam_loginuid.so

Dockerfile

FROM ubuntu:24.04

RUN apt-get update && \
    apt-get install -y openssh-server && \
    mkdir /var/run/sshd

# ユーザー作成
RUN useradd -m -s /bin/bash testuser && echo "testuser:testpass" | chpasswd

# 既存設定をリセットし、必要な設定だけを新規作成
RUN rm -rf /etc/ssh/sshd_config.d/* && \
    echo "Port 22" > /etc/ssh/sshd_config && \
    echo "ListenAddress 0.0.0.0" >> /etc/ssh/sshd_config && \
    echo "PermitRootLogin no" >> /etc/ssh/sshd_config && \
    echo "KbdInteractiveAuthentication yes" >> /etc/ssh/sshd_config && \
    echo "PasswordAuthentication no" >> /etc/ssh/sshd_config && \
    echo "UsePAM yes" >> /etc/ssh/sshd_config

# PAM設定コピー
COPY pam.d/sshd /etc/pam.d/sshd

EXPOSE 22

CMD ["/usr/sbin/sshd", "-D"]

compose.yaml

services:
  ssh:
    build: .
    container_name: ssh-challenge
    ports:
      - "2222:22"

動作確認

  • docker compose up -d --build
  • ssh -o PreferredAuthentications=keyboard-interactive testuser@localhost -p 2222

最後に

Ubuntu24.04 の sshd の場合設定が少し特殊なので注意しましょう
基本はデフォルトを削除し新規に作成するのが確実です

次回は pam を使ってチャレンジレスポンス認証を試します

2026年3月17日火曜日

AWS Certified Solutions Architect - Associate 03 合格メモ

AWS Certified Solutions Architect - Associate 03 合格メモ

概要

一応残しておきます
GCP の ACE に比べて難しい印象です

環境

  • AWS SAA (03) (2026/03/16時点)
  • テストセンターにて受験

料金

  • 150ドル (約20,000円)

対策

完全に過去問ゲーというわけにはいかない感じです
過去問からそのまま採用されるケースはほぼない印象です

教科書

kindle で 0 円だったのでこれにしましたが他のやつの方がいいかもです

模擬試験

全部で804問です

解き方のポイント

  • 時間がかなり無い印象
    • 自分は時間ギリギリまで使った
    • わからない問題は飛ばすか適当に回答もあり
  • 15問はダミー
    • これがよくわからない
    • 正解でも誤答でも無関係らしい
    • しかもどれかわからない
    • これがあるから先程の適当回答も時間がなければワンチャンありという感じ
  • 過去問がそのまま出るケースはほぼなかった
    • 模擬試験を暗記していれば回答先読みで即回答が使えるが SAA03 はほぼできなさそう
    • 模擬試験と全く同じ問題はなかったが同じようなやつで回答項目が違ったり問題を少しひねったやつは出てくる
    • 全体的に意地悪な問題が多かった印象
  • 問題の日本語がおかしい
    • 説明が少なすぎて頭で補完しなければならない
    • これを実現するためにこのコンポーネントが足りないがそれは使う前提になっていたりとか
    • このケースは実はコストも関係しているから暗黙的に安いものを選択するとか
  • 実務関係なく考えななければならない
    • これは GCP も同様だった
    • 資格モードにならないとダメ
  • 消去法
    • これは結構使えるかもしれない
    • 明らかに違うだろうというやつが何個かあったので2択まで絞れる問題が多かった印象

実際に出題された問題の記憶

  • NoETL
  • LakeFormation + 認証
  • NATゲートウェイの固定IP化
  • DR関連の問題 (これが結構多かった)
  • 最適な構成の問題 (これも結構多かった)
  • STS + DMS + コードの内のSQL変換方法
  • Global Accelerator
  • AI 関連 (Comprehend, Rekognition, SageMaker など)

全体的にいやらしい問題が多かった印象です
単純な EC2 や EBS の構成の問題は少なかったです

スコアレポートはでない

詳細なのは出ませんが点数などの結果は certmetrics で確認できます
ちなみに試験後画面ですぐに出たりすることはないので合否もスコアレポートも後で確認になります

暫定合格を確認する方法もない

ないです

合否通知はいつくるか

当日は20時50分くらいに certmetrics にログインしたら合格結果が出ていました
ちなみにバッジでおなじみの credly からのメールは 21時50分くらいに来ていました

最後に

GCP に比べると難しい印象でした
ただ GCP に比べて結果がその日に出るのは嬉しいです
GCP は結果が出るまでに数日かかるのでイライラします

模擬試験ゲーというわけにもいかないので実務 or 教科書による各種サービスの詳細な仕様までわかっていたほうがいいです
しかも基本的なサービスと言うよりかはわりとニッチなサービスも知っておく必要がありそうです

2026年2月26日木曜日

stable-diffusion で新しいモデルを追加し古いモデルを削除した場合はキャッシュも削除しなければいけないっぽい

stable-diffusion で新しいモデルを追加し古いモデルを削除した場合はキャッシュも削除しなければいけないっぽい

概要

そうしないと古いモデルを参照し続けてしまうっぽい

環境

  • macOS 26.3

現象

waiNSFWIllustrious_v140.safetensors を models/Stable-diffusion から削除したが API 経由でなぜか参照する事案が発生

ログ

Checkpoint waiNSFWIllustrious_v140.safetensors not found; loading fallback waiIllustriousSDXL_v160.safetensors [a5f58eb1c3]

対処方法

一旦 webui.sh を停止します
そして以下のキャッシュを削除します

  • rm cache/hashes/cache.db
  • rm cache/safetensors-metadata/cache.db

画像にもモデル情報が含まれているケースがあるので削除します

  • rm -rf outputs/txt2img-images/2025-09-30/*

これで再度 webui.sh を起動しましょう
ログが出ていなければ OK です

最後に

モデルを削除するだけではダメらしい

2026年2月22日日曜日

npm でパッケージをアップグレードする方法

npm でパッケージをアップグレードする方法

概要

いちいち package.json を編集するのは面倒です

環境

  • macOS 26.3
  • nodejs 22.21.0

方法

  • npx npm-check-updates -u
  • npm install

最後に

outdated とか使う方法があるらしいですがややこしいのでそういうのをまとめてやってくれる npm-check-updates を使うのが簡単です

参考サイト

2026年2月16日月曜日

Github actions で kitchen test をする場合は buildkit を無効にしなければ動かなくなった

Github actions で kitchen test をする場合は buildkit を無効にしなければ動かなくなった

概要

おそらく本当に最近仕様変更があった模様

環境

  • Github actions (2026/02/13時点)
  • Ruby 4.0.1
    • test-kitchen 3.9.1
    • kitchen-docker 3.0.0

Github actions の設定

name: Kitchen test

on:
  push:
    branches:
      - "master"

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
        with:
          platforms: linux/arm64

      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      - name: Install dependencies
        run: bundle install

      - name: Run Test Kitchen
        run: |
          bundle exec kitchen test
        env:
          CHEF_LICENSE: accept
          DOCKER_BUILDKIT: 0

デバッグログを表示する場合

name: Kitchen test

on:
  push:
    branches:
      - "master"

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3
        with:
          platforms: linux/arm64

      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          bundler-cache: true

      - name: Install dependencies
        run: bundle install

      - name: Run Test Kitchen
        run: |
          bundle exec kitchen test --log-level debug || true
        env:
          CHEF_LICENSE: accept
          DOCKER_BUILDKIT: 0

      - name: Show Kitchen logs
        if: always()
        run: |
          echo "===== kitchen.log ====="
          cat .kitchen/logs/kitchen.log || true
          echo "===== instance log ====="
          cat .kitchen/logs/default-ubuntu-2204.log || true

最後に

内部的に buildkit を使うようになっており docker の出力形式が変更されたために起きていたエラーでした

2026年2月14日土曜日

スマホ版の Chrome ではちゃんと DOM を定義してから値を設定したりするほうがいい

スマホ版の Chrome ではちゃんと DOM を定義してから値を設定したりするほうがいい

概要

動的に appendChild とかするとリソースを消費しすぎと判断して描画を強制的に停止するようです

環境

  • iOS 26.2.1
  • Chrome 144.07559.95

ダメなコード

function addMessage(msg) {
  // avoid duplicates; update indicator if already present
  const existing = document.querySelector(`#log li[data-msg-id="${msg.id}"]`);
  if (existing) {
    const ind = existing.querySelector(".read-indicator");
    if (ind) {
      updateIndicator(ind, existing, msg.reads || []);
    }
    return;
  }
  // 新着メッセージ(相手からのもの)に対する通知・未読処理
  if (msg.message && msg.client_id !== CLIENT_ID) {
    // ブラウザ通知の呼び出し
    showBrowserNotification(msg);
    // タブが裏側にある場合、未読カウントを増やす
    if (document.visibilityState !== "visible") {
      unreadCount++;
      updateTabTitle();
    }
  }
  // new message
  const li = document.createElement("li");
  li.className = msg.client_id === CLIENT_ID ? "me" : "other";
  li.dataset.msgId = msg.id;

  const textSpan = document.createElement("span");
  textSpan.className = "msg-text";
  textSpan.textContent = msg.message;

  const ind = document.createElement("span");
  ind.className = "read-indicator";

  if (msg.client_id === CLIENT_ID) {
    // 自分のメッセージ: サーバーの reads 情報を使って表示
    updateIndicator(ind, li, msg.reads || []);
    li.appendChild(textSpan);
    li.appendChild(ind);
  } else {
    // 相手のメッセージ
    updateIndicator(ind, li, msg.reads || []);

    li.addEventListener("click", () => {
      if (isRead(msg.id)) return;
      setRead(msg.id);
      ind.classList.remove("unread");
      ind.classList.add("read");
      ind.title = "既読";
      // notify server
      markRead(msg.id);
    });

    li.appendChild(textSpan);
    li.appendChild(ind);
  }

  document.getElementById("log").appendChild(li);
}

良いコード

function addMessage(msg) {
  // 1. 基本チェック
  if (!msg || !msg.id) return;
  // read_event は applyReadEvent で処理されるため、ここではメッセージ本体がないものを除外
  if (msg.type === "read_event" || !msg.message) return;

  // 2. 重複チェック
  const log = document.getElementById("log");
  if (!log) return;
  const existing = document.querySelector(`#log li[data-msg-id="${msg.id}"]`);
  if (existing) return;

  // 3. 要素の作成
  const li = document.createElement("li");

  // 安全な ID チェック
  const currentId =
    typeof CLIENT_ID !== "undefined" ? CLIENT_ID : getClientId();

  li.className = msg.client_id === currentId ? "me" : "other";
  li.setAttribute("data-msg-id", msg.id); // dataset ではなく setAttribute を使用(古いブラウザ対策)

  const textSpan = document.createElement("span");
  textSpan.className = "msg-text";
  textSpan.textContent = String(msg.message); // 明示的に文字列変換

  const ind = document.createElement("span");
  ind.className = "read-indicator unread"; // デフォルトは unread

  // 4. 要素の組み立て(先に DOM に追加してから細部を調整するのがスマホでは安定します)
  li.appendChild(textSpan);
  li.appendChild(ind);
  log.appendChild(li);

  // 5. 既読インジケーターの更新(エラーが起きても表示自体は維持する)
  try {
    updateIndicator(ind, li, msg.reads || []);
  } catch (e) {
    console.warn("Indicator error ignored for safety", e);
  }

  // 6. イベントリスナー(相手のメッセージのみ)
  if (msg.client_id !== currentId) {
    li.addEventListener("click", function () {
      try {
        if (isRead(msg.id)) return;
        setRead(msg.id);
        ind.classList.remove("unread");
        ind.classList.add("read");
        markRead(msg.id);
      } catch (err) {
        console.error(err);
      }
    });

    // 通知処理(エラー回避のため try-catch)
    try {
      showBrowserNotification(msg);
    } catch (e) {}
  }

  // 7. スクロール
  log.scrollTop = log.scrollHeight;
}

比較

堅牢性とエラーハンドリング

「良いコード」は、外部要因でプログラムが止まらないよう工夫されています。

項目 ダメなコード 良いコード
ガード句 データの存在チェックが甘く、msg.idが欠落しているとエラーになる可能性がある。 冒頭で `!msg
例外処理 updateIndicator 等でエラーが起きると、後続の処理(DOM追加等)が止まる。 try-catch を活用し、一部の表示エラーが起きてもアプリ全体が止まらない。
変数の安全性 CLIENT_ID がグローバルにある前提。 typeof チェックやフォールバック(getClientId())を用意している。

責務の明確化と可読性

コードが整理されているため、後からの修正が容易です。

  • 単一責任の原則:
    • ダメ: addMessage 内で通知、未読カウント、DOM操作、重複チェックが混ざり、条件分岐(if (msg.client_id === CLIENT_ID) … else …)で同じような appendChild が重複している。
    • 良い: メッセージの型判定(read_event かどうか)を fetchMessages 側で仕分け、addMessage は「表示」に専念している。
  • DRY (Don’t Repeat Yourself):
    • ダメ: 自分のメッセージと相手のメッセージで、インジケーターの追加処理を2回書いている。
    • 良い: 共通の組み立て(li への追加)を先に行い、差分(イベントリスナー)だけを条件分岐で書いている。

ユーザー体験 (UX) と実用性

ブラウザやネットワークの特性を考慮した実装になっています。

  • スクロール処理: 「良いコード」では追加後に log.scrollTop = log.scrollHeight を行い、常に最新メッセージが見えるように配慮されています。
  • 通信の最適化: fetchMessages でキャッシュ回避のためのタイムスタンプを追加しており、古いデータを掴まされるリスクを減らしています。
  • 暗黙の型変換への対処: String(msg.message) とすることで、数値や null が送られてきても textContent で安全に表示できます。

改善のポイント(まとめ)

  • ❌ ダメな点
    • 重複コード: li.appendChild(textSpan) などが if/else 両方にあり、修正漏れの原因になる。
    • 不親切なエラー: ネットワークエラーやデータ不備で JS が停止し、画面が真っ白になるリスクがある。
    • 状態管理の欠如: スクロール位置の調整がないため、ユーザーが手動でスクロールする必要がある。
  • ✅ 良い点
    • 徹底したバリデーション: 「データはあるか?」「型は正しいか?」を常に疑っている。
    • 保守性の高い構造: コメントで工程(1〜7)が区切られており、どこで何をしているか一目でわかる。
    • API設計への配慮: lastId の更新により、重複してメッセージを取得しない仕組みが整っている。

最後に

スマホ版の Chrome も意識して JavaScript を書かないとダメなようです

2026年2月13日金曜日

サーバから Chrome ブラウザプッシュする実装の流れ

サーバから Chrome ブラウザプッシュする実装の流れ

概要

実装の流れを紹介します

環境

  • macOS 26.2
  • Python 3.12.11

鍵の作成

こちらを参考に作成します
鍵ファイルと JavaScript 用の鍵文字列を作成すれば OK です

サブスクリプション情報の保存 (Python)

JavaScrpt 側で通知の許可をした場合に通知先の情報をサーバ側に保存しておきます
今回は Redis で紹介しますが永続化できるものであれば何でも OK です

@app.route("/subscribe", methods=["POST"])
def subscribe():
    """フロントから送られてくる Subscription 情報を Redis に保存"""
    data = request.get_json()
    client_id = data.get("client_id")
    subscription_info = data.get("subscription")

    redis_conn = get_kv()
    # client_id ごとに購読情報を保存
    redis_conn.set(f"push_sub:{client_id}", json.dumps(subscription_info))
    return jsonify({"ok": True})

プッシュ送信処理 (Python)

あとで使いますが実際にプッシュを送信する処理です
メールアドレス部分はダミーでも問題ないですが一応ちゃんと設定した方がいいです (公開アプリの場合)

VAPID_PRIVATE_KEY_PATH = os.getenv("VAPID_PRIVATE_KEY_PATH", "./private_key.pem")
VAPID_CLAIMS = {"sub": "mailto:user@mail"}

def send_web_push(subscription_info, message_body):
    try:
        webpush(
            subscription_info=json.loads(subscription_info),
            data=json.dumps(message_body),
            vapid_private_key=VAPID_PRIVATE_KEY_PATH,
            vapid_claims=VAPID_CLAIMS,  # type: ignore
        )
    except WebPushException as ex:
        print("Push failed:", repr(ex))

ServiceWorker (JavaScript)

リモートプッシュはこの ServiceWorker がないと動きません
Python からのプッシュをこの ServiceWorker が受け取り通知を表示する感じです

通知のスタイルなどはシンプルにしているので必要であればアイコンなど設定してください

self.addEventListener("push", function (event) {
  console.log("Push received");
  let data = { title: "新着", body: "メッセージがあります" };

  try {
    if (event.data) {
      data = event.data.json(); // ここでエラーが起きると通知が出ない
    }
  } catch (e) {
    console.error("JSON parse error:", e);
  }

  const options = {
    body: data.body,
  };

  event.waitUntil(self.registration.showNotification(data.title, options));
});

ブラウザ側のサブスクリプション登録 (JavaScript)

ブラウザで特定のサイトなどにアクセスすると通知を許可しますかなど出るサイトがありますがそれを実装します
許可するをクリックしたら必要な情報を Python 側に送ります

urlBase64ToUint8Array に鍵文字列は冒頭で作成したものを使ってください

function urlBase64ToUint8Array(base64String) {
  const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
  const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
  const rawData = window.atob(base64);
  const outputArray = new Uint8Array(rawData.length);
  for (let i = 0; i < rawData.length; ++i) {
    outputArray[i] = rawData.charCodeAt(i);
  }
  return outputArray;
}

async function setupPush() {
  const register = await navigator.serviceWorker.register("/static/js/sw.js");

  // すでに購読済みかチェック
  let subscription = await register.pushManager.getSubscription();

  // 未購読なら新規作成
  if (!subscription) {
    subscription = await register.pushManager.subscribe({
      userVisibleOnly: true,
      applicationServerKey: urlBase64ToUint8Array(
        "xxx",
      ),
    });
  }

  // サーバーへ送信(常に送ることで client_id との紐付けを確実に更新する)
  await fetch("/subscribe", {
    method: "POST",
    body: JSON.stringify({
      client_id: CLIENT_ID,
      subscription: subscription,
    }),
    headers: { "Content-Type": "application/json" },
  });
}

通知許可ダイアログ表示 (JavaScript)

ページが読み込まれた場合に自動で出してもいいですが嫌がられるケースもあるので特定のボタンを押したら許可する流れにします

document.addEventListener("DOMContentLoaded", () => {
  // Click title to set client_id and request notification permission
  const title = document.getElementById("notification");
  if (title) {
    title.addEventListener("click", async () => {
      // 通知の許可を求める
      if ("Notification" in window) {
        const permission = await Notification.requestPermission();
        if (permission === "granted") {
          // ここで実行!
          await setupPush();
        }
      }
    });
  }
  // 追加: すでに許可済みなら、ページ読み込み時に購読情報を同期する
  if ("Notification" in window && Notification.permission === "granted") {
    setupPush().catch((err) => console.error("Auto-setup failed:", err));
  }
});

プッシュ通知送信タイミング (Python)

あとは任意のタイミングでサーバ側で送信すれば OK です
当然ですが送信しまくると大変なことになるので条件など設けて適切なタイミングで送るようにしましょう

@app.route("/send", methods=["POST"])
def redis_chat_send():
    # 1... 既存の処理
    # 2. プッシュ通知の送信 (自分以外の購読者へ)
    try:
        # push_sub: で始まるキーをすべて取得
        # ※ ユーザー数が多い場合はメンバーリストを別途管理するのが理想ですが、
        # 現状のチャットルーム運用であれば keys で十分です
        sub_keys = redis_conn.keys("push_sub:*")

        for sub_key in sub_keys:  # type: ignore
            target_id = sub_key.decode().split(":")[-1]

            # 送信者(自分)以外にのみ送る
            if target_id != client_id:
                sub_info = redis_conn.get(sub_key)
                if sub_info:
                    logger.info(f"Sending push to {target_id}")
                    # 非同期処理にするのが理想ですが、まずは直列で実装
                    send_web_push(
                        sub_info,
                        {"title": "新着メッセージ", "body": f"{client_id}: {msg}"},
                    )
    except Exception as e:
        logger.error(f"Push notification error: {e}")

    return jsonify({"ok": True})

サブスクリプション確認

  • Redis 側にキーが登録されていること
  • 開発者コンソールで ServiceWorker が登録されていること

プッシュが届かない場合

  • Mac であれば通知の設定を確認しましょう
    • Mac の Chrome はデフォルトでオフになっている
    • Chrome で許可していても Mac 側で拒否していることが多い
  • サイトが https 化されていない
    • localhost ならテストできるようです
    • IP ベースのサイトや http のサイトでは動作しません

スマホ or タブレット版でプッシュ通知が届かない場合

  • ラップトップ版と違いかなり作りが違う
  • 単純に通知を許可することができない
  • iOS 側で当然許可する必要があるがそれでもサブスクリプションできないことがある
  • サイトを PWA 化して上げる必要などがあるかも

最後に

参考サイト