2025年10月31日金曜日

Gitlab CI で service を使って Web アプリを起動し簡単なテストを行う

Gitlab CI で service を使って Web アプリを起動し簡単なテストを行う

概要

イメージをビルドしビルドしたイメージからアプリを起動しそのアプリに対してテストするみたいな流れを GItlab CI でやってみます

今回 Web アプリは Python を使っていますが好きな言語のアプリで OK です

環境

  • Gitlab 18.2.6
  • Gitlab Runner 18.2.2
  • Python 3.12.11

Pipfile

[[source]]
url = "https://pypi.org/simple"
verify_ssl = true
name = "pypi"

[packages]
flask = "*"

[dev-packages]

[requires]
python_version = "3.12"

app.py

from flask import Flask

app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello, World!"

if __name__ == '__main__':
    app.run(debug=True, host="0.0.0.0")

Dockerfile

FROM python:3.12.11-slim-bookworm

# 作業ディレクトリを設定
WORKDIR /app

# 必要なファイルをコンテナにコピー
COPY Pipfile Pipfile
COPY Pipfile.lock Pipfile.lock
COPY app.py app.py

# 必要なPythonパッケージをインストール
RUN pip install pipenv
RUN pipenv install

# アプリケーションを起動
CMD ["pipenv", "run", "python", "app.py"]

.gitlab-ci.yml

stages:
  - build  # ビルドステージ
  - test   # テストステージ

build:
  stage: build
  image:
    name: moby/buildkit:rootless  # BuildKit を使用するための Docker イメージ
    entrypoint: [""]  # デフォルトのエントリーポイントを無効化
  variables:
    BUILDKITD_FLAGS: --oci-worker-no-process-sandbox  # BuildKit の設定
  before_script:
    # Docker 認証情報を設定
    - mkdir -p ~/.docker
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > ~/.docker/config.json
  script:
    # BuildKit を使用して Docker イメージをビルドし、GitLab Container Registry にプッシュ
    - |
      buildctl-daemonless.sh build \
        --frontend dockerfile.v0 \
        --local context=. \
        --local dockerfile=. \
        --output type=image,name=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA,push=true

app:
  stage: test
  services:
    # ビルドした Docker イメージをサービスとして起動
    - name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
      alias: testapp  # サービスに "testapp" というホスト名を割り当て
  script:
    # サービスが起動するまで待機
    - echo "Waiting for the service to be ready..."
    - >
      for i in {1..30}; do
        curl -s http://testapp:5000 && break || sleep 1;  # サービスが応答するまで最大 30 秒間リトライ
      done
    - echo "Service is ready."  # サービスが起動したことを確認

validate:
  stage: test
  image: curlimages/curl:latest  # curl を使用するための軽量イメージ
  services:
    # ビルドした Docker イメージを再度サービスとして起動
    - name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
      alias: testapp  # サービスに "testapp" というホスト名を割り当て
  script:
    # サービスに対してレスポンスを検証
    - echo "Running validation tests..."
    - |
      RESPONSE=$(curl -s http://testapp:5000)  # サービスにリクエストを送信しレスポンスを取得
      if [ "$RESPONSE" = "Hello, World!" ]; then
        echo "Validation passed!"  # レスポンスが期待通りの場合
      else
        echo "Validation failed: Expected 'Hello, World!' but got '$RESPONSE'"  # レスポンスが期待と異なる場合
        exit 1  # ジョブを失敗させる
      fi

最後に

Gitlab には Ultimate ライセンスを使っていると DAST を使えるのですがそうじゃないライセンスだと使えません
そんな場合に自分で DAST 的なことをする必要があるので今回のような流れで実装できます

次回は nikto を組み合わせてセキュリティテストしてみます

2025年10月26日日曜日

LetsEncrypt で 0001 や 0002 などの証明書が取得できてしまった場合の解決方法

LetsEncrypt で 0001 や 0002 などの証明書が取得できてしまった場合の解決方法

概要

前の設定が残っている状態で新規に証明書を発行すると連番が付いてしまいます
こうなると /etc/letsencrypt/live/ 配下で証明書を管理するディレクトリにも 0001 などが付き色々と面倒なので削除する方法を紹介します

なお過去に紹介した docker compose バージョンの certbot を使います

環境

  • Ubuntu 22.04
  • certbot 5.1.0

削除する

サフィックスが付いている証明書および付いていないやつも削除しましょう

  • docker compose run --rm certbot delete --cert-name your-domain.com-0001
  • docker compose run --rm certbot delete --cert-name your-domain.com-0002
  • docker compose run --rm certbot delete --cert-name your-domain.com

これで再度取得すればサフィックスなし版が取得できます
なおサフィックス版があるかないかは certificates サブコマンドを使って確認しましょう

  • docker compose run --rm certbot certificates

最後に

ホスト側の証明書をコンテナにマウントして使う場合にサフィックスがあるといろいろ面倒なのでなし版で取得するようにしましょう

参考サイト

2025年10月25日土曜日

Microsoft Graph API を使って個人の OneDrive を操作する方法

Microsoft Graph API を使って個人の OneDrive を操作する方法

概要

前回 OneDrive にアクセスできるトークンの取得まで行いました
今回はそのトークンを使って実際に OneDrive にアクセスします

環境

  • macOS 15.7.1
  • Python 3.12.11

サンプルコード

import json
import os

import requests

TENANT_ID = "xxx"
CLIENT_ID = "xxx"
CLIENT_SECRET = "xxx"
REDIRECT_URI = "http://localhost:8080"
TOKEN_FILE = "onedrive_token.json"
GRAPH_API_BASE = "https://graph.microsoft.com/v1.0"


# =========================
# トークン管理
# =========================
def load_tokens():
    """保存済みトークンを読み込む"""
    if not os.path.exists(TOKEN_FILE):
        raise FileNotFoundError(f"{TOKEN_FILE} が存在しません。先にトークン取得を実行してください。")
    with open(TOKEN_FILE, "r", encoding="utf-8") as f:
        return json.load(f)


def save_tokens(tokens):
    """トークンを保存"""
    with open(TOKEN_FILE, "w", encoding="utf-8") as f:
        json.dump(tokens, f, indent=2)


def refresh_access_token(refresh_token, client_id, client_secret, redirect_uri):
    """リフレッシュトークンからアクセストークンを更新"""
    token_url = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
    data = {
        "client_id": client_id,
        "client_secret": client_secret,
        "redirect_uri": redirect_uri,
        "grant_type": "refresh_token",
        "refresh_token": refresh_token
    }
    res = requests.post(token_url, data=data)
    res.raise_for_status()
    return res.json()


# =========================
# OneDrive 操作
# =========================
def get_headers(access_token):
    return {"Authorization": f"Bearer {access_token}"}


def upload_file(access_token, local_path, remote_name):
    """小さなファイルをアップロード"""
    url = f"{GRAPH_API_BASE}/me/drive/root:/{remote_name}:/content"
    with open(local_path, "rb") as f:
        res = requests.put(url, headers=get_headers(access_token), data=f)
    if res.status_code in (200, 201):
        print(f"✅ アップロード成功: {remote_name}")
    else:
        print(f"❌ アップロード失敗 ({res.status_code}): {res.text}")


def list_files(access_token):
    """ルートディレクトリのファイル一覧取得"""
    url = f"{GRAPH_API_BASE}/me/drive/root/children"
    res = requests.get(url, headers=get_headers(access_token))
    if res.status_code == 200:
        items = res.json().get("value", [])
        print("📄 OneDrive ファイル一覧:")
        for item in items:
            print(f" - {item['name']}")
    else:
        print(f"❌ 取得失敗 ({res.status_code}): {res.text}")


def delete_file(access_token, remote_name):
    """ファイル削除"""
    url = f"{GRAPH_API_BASE}/me/drive/root:/{remote_name}"
    res = requests.delete(url, headers=get_headers(access_token))
    if res.status_code in (204, 200):
        print(f"🗑️ 削除成功: {remote_name}")
    else:
        print(f"❌ 削除失敗 ({res.status_code}): {res.text}")


# =========================
# メイン
# =========================
if __name__ == "__main__":
    tokens = load_tokens()
    access_token = tokens["access_token"]

    # 必要に応じてリフレッシュ
    tokens = refresh_access_token(tokens["refresh_token"], CLIENT_ID, CLIENT_SECRET, REDIRECT_URI)
    access_token = tokens["access_token"]
    save_tokens(tokens)

    # サンプルファイル作成
    sample_file = "test.txt"
    with open(sample_file, "w", encoding="utf-8") as f:
        f.write("Hello OneDrive via Graph API!")

    # アップロード
    upload_file(access_token, sample_file, "test.txt")

    # ファイル一覧
    list_files(access_token)

    # 削除(必要であればコメント解除)
    delete_file(access_token, "test.txt")

ポイント

  • トークンが切れた場合にリフレッシュトークを使って再度トークンを取得します
  • 毎回行っても問題ないので実際は毎回リフレッシュトークを使って問い合わせたほうがいいです
  • トークンをファイルで保存していますが漏洩すると大変なので管理には注意しましょう

最後に

Python で OneDrive の操作をしてみました
認証が面倒ですが一度認証してしまえばあとはスクリプトを実行するだけです

アプリのシークレットに期限があるのでその期限がすぎたら再度ブラウザを開いてトークンファイルを更新する必要があります

2025年10月24日金曜日

Microsoft Graph API を使って個人の OneDrive にアクセスする方法

Microsoft Graph API を使って個人の OneDrive にアクセスする方法

概要

前回 Graph API に入門しました
今回は個人用の OneDrive アクセスしてみます
なそアプリーケーション権限は使えず「委任されたアクセス許可」に変更する必要があるので注意してください

簡単に言うと API をコールするためのアクセストークンを一度ブラウザを開いて取得しなければなりません

環境

  • macOS 15.7.1
  • Python 3.12.11

サポートされているアカウントの種類

  • 任意の組織ディレクトリ内のアカウント (任意の Microsoft Entra ID テナント - マルチテナント) と個人用の Microsoft アカウント (Skype、Xbox など)

もしくは

  • 個人用 Microsoft アカウントのみ

権限の設定

  • 管理 -> API のアクセス許可 -> アクセス許可の追加 -> Microsoft Graph -> 委任されたアクセス許可
    • Files.ReadWrite.All
    • Sites.ReadWrite.All
    • User.Read
    • offline_access
  • 既定のディレクトリに管理者の権限を与えますを選択

コールバックURLの設定

  • 管理 -> 認証 -> URLの追加 -> http://localhost:8080

トークンの取得

これは一度だけ実行します
表示された URL にブラウザでアクセスしログインします

  • vim ./fetch_token.py
import http.server
import json
import socketserver
import urllib.parse
import webbrowser

import requests

# ======== 設定 ========
TENANT_ID = "xxx"
CLIENT_ID = "xxx"
CLIENT_SECRET = "xxx"
REDIRECT_URI = "http://localhost:8080"
SCOPES = ["Files.ReadWrite.All", "Sites.ReadWrite.All", "User.Read", "offline_access"]
TOKEN_FILE = "onedrive_token.json"
# =======================


def start_local_server():
    """localhostで認可コードを受け取るための簡易HTTPサーバー"""
    class Handler(http.server.SimpleHTTPRequestHandler):
        def do_GET(self):
            query = urllib.parse.urlparse(self.path).query
            params = urllib.parse.parse_qs(query)
            if "code" in params:
                setattr(self.server, "auth_code", params["code"][0])
                self.send_response(200)
                self.end_headers()
                self.wfile.write(b"Authorization successful. You can close this window.")
            else:
                self.send_response(400)
                self.end_headers()
                self.wfile.write(b"Missing authorization code.")

    with socketserver.TCPServer(("localhost", 8080), Handler) as httpd:
        setattr(httpd, "auth_code", None)
        print("🌐 ローカルサーバー起動中 (http://localhost:8080)")
        httpd.handle_request()
        return getattr(httpd, "auth_code", None)


def get_tokens(auth_code):
    """認可コードからトークンを取得"""
    token_url = "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
    data = {
        "client_id": CLIENT_ID,
        "redirect_uri": REDIRECT_URI,
        "client_secret": CLIENT_SECRET,
        "code": auth_code,
        "grant_type": "authorization_code"
    }
    res = requests.post(token_url, data=data)
    res.raise_for_status()
    return res.json()


def save_tokens(tokens):
    """トークンをローカルに保存"""
    with open(TOKEN_FILE, "w", encoding="utf-8") as f:
        json.dump(tokens, f, indent=2)
    print(f"💾 トークンを保存しました → {TOKEN_FILE}")


def main():
    # 1️⃣ 認可URLを生成
    params = {
        "client_id": CLIENT_ID,
        "response_type": "code",
        "redirect_uri": REDIRECT_URI,
        "response_mode": "query",
        "scope": " ".join(SCOPES)
    }
    auth_url = f"https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?{urllib.parse.urlencode(params)}"

    print("\n🔗 以下のURLを開いてMicrosoftアカウントでサインインしてください:")
    print(auth_url)
    webbrowser.open(auth_url)

    # 2️⃣ 認可コード受け取り
    auth_code = start_local_server()
    if not auth_code:
        print("❌ 認可コードを取得できませんでした。")
        return

    print(f"✅ 認可コード取得成功: {auth_code[:20]}...")

    # 3️⃣ トークン取得
    tokens = get_tokens(auth_code)
    print("✅ アクセストークン・リフレッシュトークン取得成功")
    save_tokens(tokens)

    print("\n🧾 トークン内容:")
    print(json.dumps(tokens, indent=2))


if __name__ == "__main__":
    main()

動作確認

  • pipenv run python ./app.py

でブラウザが開いたらMicrosoft アカウントでログインしアプリに許可を与えます
成功するとトークン情報が onedrive_token.json に保存されるので今後はこれを使って OneDrive にアクセスします

最後に

Python から OneDrive にアクセスするためのトークンの取得まで行いました
次回は実際にファイルのアップロードや取得などを行います

Onedrive for bussiness だとアプリケーションの許可の権限で操作できるようです

2025年10月23日木曜日

Microsoft Graph API を Python で使ってみる

Microsoft Graph API を Python で使ってみる

概要

Microsoft365 の API である Microsoft Graph API を Python から利用してみました

環境

  • macOS 15.7.1
  • Python 3.12.11

アプリの作成

  • https://portal.azure.com にログイン
  • 左メニューから「Microsoft Entra ID」を選択
  • 「アプリの登録」->「新規登録」をクリック
    • 名前 -> 何でも OK
    • サポートされているアカウントの種類 -> この組織ディレクトリのみに含まれるアカウント (既定のディレクトリ のみ - シングル テナント)
    • リダイレクト URI -> とりあえず空でOK
  • テナントID、クライアントID をコピーしておく

シークレットの登録

  • 「証明書とシークレット」->「新しいクライアント シークレット」から発行
  • 「値」をコピーしておく

API アクセスの許可

  • アプリ登録ページ左メニューから「API のアクセス許可」を選択
  • 「+ アクセス許可の追加」->「Microsoft Graph」->「アプリケーションの許可」を選択
    • Sites.ReadWrite.All
    • Files.ReadWrite.All
  • アクセス許可の追加」->「管理者の同意を与える」をクリック(管理者権限が必要)

ライブラリインストール

  • pipenv install msal requests

アクセストークンを取得するサンプルコード

とりあえず作成したアプリがアクセスできる認証情報だけ取得してみます

  • vim ./app.py
import pprint

from msal import ConfidentialClientApplication

TENANT_ID = "xxx"
CLIENT_ID = "xxx"
CLIENT_SECRET = "xxx"

app = ConfidentialClientApplication(
    CLIENT_ID,
    authority=f"https://login.microsoftonline.com/{TENANT_ID}",
    client_credential=CLIENT_SECRET,
)

result = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])

pprint.pprint(result)
  • pipenv run python ./app.py

以下のようにトークンが取得できれば OK です

{'access_token': 'xxx',
 'expires_in': 3599,
 'ext_expires_in': 3599,
 'token_source': 'identity_provider',
 'token_type': 'Bearer'}

sharepoint のサイト一覧を取得する

  • vim ./app.py
import requests
from msal import ConfidentialClientApplication

TENANT_ID = "xxx"
CLIENT_ID = "xxx"
CLIENT_SECRET = "xxx"

app = ConfidentialClientApplication(
    CLIENT_ID,
    authority=f"https://login.microsoftonline.com/{TENANT_ID}",
    client_credential=CLIENT_SECRET,
)
token = app.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
if token is None:
    raise Exception("トークンの取得に失敗しました。")
headers = {"Authorization": f"Bearer {token['access_token']}"}

# サイト一覧を検索(すべて表示)
url = "https://graph.microsoft.com/v1.0/sites?search=*"
response = requests.get(url, headers=headers)
print(response.json())
sites = response.json()

for s in sites.get("value", []):
    print(f"名前: {s['name']}")
    print(f"URL:  {s['webUrl']}")
    print(f"ID:   {s['id']}")
    print("---")

sharepoint は当然ですがライセンスがないと使えないので「Tenant does not have a SPO license.」のエラーになります

最後に

Python から MicrosoftGraphAPIをコールしてみました
無料アカウントでもここまではいけます
次回は実際に Microsoft365 上にあるリソースにアクセスしてみます

参考サイト

2025年10月22日水曜日

journal ログをすべて削除する方法

journal ログをすべて削除する方法

概要

忘れるのでメモ

環境

  • Ubuntu 22.04

全削除

sudo du -sh /var/log/
3.3G    /var/log/
  • sudo journalctl --vacuum-time=1s

削除ログ

Vacuuming done, freed 0B of archived journals from /run/log/journal.
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/user-1002@cb5cf46ac4024d6ab8ac0f7039ee62f0-000000000279f7c3-00063ffa5ec9cd0c.journal (8.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-00000000027b400b-00064004588b93b5.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-00000000027d6843-000640152308d284.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-00000000027f9176-00064025f94a8e93.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-000000000281b863-00064036e00660b8.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-000000000283e0cf-00064047f2873c97.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-00000000028609f5-00064058f8e381d0.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-00000000028834bc-0006406a1746446c.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-00000000028a5e91-0006407b42809cd3.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-00000000028c873b-0006408c87b6d808.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/user-1002@cb5cf46ac4024d6ab8ac0f7039ee62f0-00000000028e2463-0006409965aaafa8.journal (8.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-00000000028eb3bd-0006409dca5e037c.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-000000000290dc16-000640ae8e9c7381.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/user-1002@cb5cf46ac4024d6ab8ac0f7039ee62f0-0000000002912098-000640b0a2d36d3b.journal (8.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-0000000002930478-000640bf5eb5c522.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/user-1002@cb5cf46ac4024d6ab8ac0f7039ee62f0-0000000002936af1-000640c27aa7db32.journal (8.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-0000000002952cde-000640d027fee751.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-000000000297559b-000640e0f3a72cf5.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-0000000002997e37-000640f1d327fe5d.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-00000000029ba58e-00064102c1378bd0.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-00000000029dce0e-00064113bde6de0a.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-00000000029ff7c5-00064124cc9e6f43.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-0000000002a2215b-00064135ea1f4111.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-0000000002a44b4b-000641471cc25b72.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-0000000002a6736b-00064158657fee95.journal (128.0M).
Deleted archived journal /var/log/journal/95b2c59cdf7701096ea7d3386e92e124/system@6e6c307cc5634e74888ce601b9437fcf-0000000002a89d70-00064169c011da77.journal (128.0M).
Vacuuming done, freed 2.7G of archived journals from /var/log/journal/95b2c59cdf7701096ea7d3386e92e124.
Vacuuming done, freed 0B of archived journals from /var/log/journal.

sudo du -sh /var/log/
443M    /var/log/

微妙に残ります
インスタンス自体を再起動すると更に減ります

du 以外にも journalctl 専用のコマンドでログの使用量を確認することもできます

  • sudo journalctl --disk-usage
Archived and active journals take up 136.0M in the file system.

そもそも保存させる容量を調整する

  • sudo vim /etc/systemd/journald.conf
[Journal]
SystemMaxUse=100M
  • sudo systemctl restart systemd-journald

ちなみに SystemMaxUse のデフォルトはディスクサイズの10%なので30GBのディスクであれば3.0GBほど保存します

最後に

だいたい dockerd のログでうまります

参考サイト

2025年10月21日火曜日

buildah 超入門

buildah 超入門

概要

buildah を使ってみました
基本は docker のように使えますが rootless 実行するのがかなり面倒そうです

環境

  • Ubuntu 24.04
  • buildah 1.33.7
  • docker 28.5.1

インストール

  • sudo apt -y update
  • sudo apt -y install buildah

apt の公式リポジトリからインストールできますが最新版はインストールできません

イメージのビルド (rootモード)

  • sudo buildah build -f Dockerfile -t lab .

sudo 付きは root モードなのであまりお勧めできません

イメージのビルド (rootless モード)

不明

Error during unshare(CLONE_NEWUSER): Permission denied
ERRO[0000] parsing PID "": strconv.Atoi: parsing "": invalid syntax
ERRO[0000] (Unable to determine exit status)

になり実行できませんでした unshare --user --map-root-user --mount あたりが実行できないと rootless 実行は厳しいです

確認

  • sudo buildah images
REPOSITORY                 TAG                     IMAGE ID       CREATED          SIZE
localhost/lab              latest                  6b467d5adf55   31 seconds ago   129 MB
docker.io/library/python   3.12.11-slim-bookworm   688a685f6a1f   2 months ago     129 MB

イメージのプッシュ

  • sudo buildah login your-container-registry-address
  • sudo buildah tag localhost/lab your-container-registry-address/username/reponame
  • sudo buildah push your-container-registry-address/username/reponame

最後に

基本は docker コマンドと同様に使えます
rootless 実行が良い点なのですが rootless 実行できるようにするまでがかなり大変そうなのでそこが何ともといった感じです

当然ですが docker デーモンがなくても buildah を使ってビルドはできるのでそれだけでも kaniko の代用にはなりそうです

  • sudo systemctl stop docker
  • sudo systemctl stop docker.socket

参考サイト

2025年10月20日月曜日

GitlabCI で buildkit を使って docker イメージをビルドする

GitlabCI で buildkit を使って docker イメージをビルドする

概要

buildkit 単体で動かそうとするとデーモンが必要だったり runc が必要だったりするのですが GitlabCI で公式のイメージを使用すると簡単に使えたのでメモしておきます

環境

  • buildkit
  • Gitlab 18.2.6
  • Gitlab Runner 18.2.2

.gitlab-ci.yml

.gitlab-ci.yml と同じディレクトリに Dockerfile があれば以下でそのまま使えます
他に環境変数などの設定が必要な場合は variables などで設定してください

stages:
  - test

build:
  stage: test
  image:
    name: moby/buildkit:rootless
    entrypoint: [""]
  variables:
    BUILDKITD_FLAGS: --oci-worker-no-process-sandbox
  before_script:
    - mkdir -p ~/.docker
    - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > ~/.docker/config.json
  script:
    - |
      buildctl-daemonless.sh build \
        --frontend dockerfile.v0 \
        --local context=. \
        --local dockerfile=. \
        --output type=image,name=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA,push=true

最後に

kaniko からの代替としては一番近い感じはします
以下にもありますが公式にも kaniko からの移行ドキュメントがあります

参考サイト

2025年10月18日土曜日

GitlabCI と werf を組み合わせてイメージのビルドとプッシュをしてみる

GitlabCI と werf を組み合わせてイメージのビルドとプッシュをしてみる

概要

前回 werf に入門してみました
今回は GitlabCI と連携して werf を使ってみます
なお Gitlab Runner は docker executor なので werf の公式イメージを使ってビルドアンドプッシュを行います

環境

  • werf 2-stable
  • Gitlab 18.2.6
  • Gitlab Runner 18.2.2

Gitlab Runner 側設定

以下どちらかを有効にする必要があります

  • 特権モード有効にする
  • seccompとapparmorを無効にする

前者であれば以下のように設定します

[[runners]]
  name = "<name of the Runner you registered>"
  [runners.docker]
    privileged = true
    volumes = [
      "werf-cache:/home/build/.werf",
      "buildah-cache:/home/build/.local/share/containers"
    ]

後者であれば以下のように設定します

[[runners]]
  name = "<name of the Runner you registered>"
  [runners.docker]
    security_opt = ["seccomp:unconfined", "apparmor:unconfined"]
    volumes = [
      "werf-cache:/home/build/.werf",
      "buildah-cache:/home/build/.local/share/containers"
    ]

.gitlab-ci.yml

Gitlab のコンテナレジストリはオンにしておきましょう
以下は werf を使って build and push する最低限の設定になります

  • vim .gitlab-ci.yml
stages:
  - test

werf_build_and_push:
  stage: test
  image:
    name: "registry.werf.io/werf/werf:2-stable"
    pull_policy: always
  before_script:
    - cat $(werf ci-env gitlab --as-file)
    - source $(werf ci-env gitlab --as-file)
  script:
    - werf build --repo ${CI_REGISTRY_IMAGE} --add-custom-tag latest

実際にパイプラインを走らせるとコンテナレジストリにイメージがあることが確認できます
どうやら werf は指定のイメージだけではなくメタデータなどを管理するイメージもコンテナレジストリ上で管理するようです

ポイント

werf を Gitlab で使って便利な点は source $(werf ci-env gitlab --as-file) の部分です
レジストリの認証情報やリポジトリの基本設定を自動で読み込んでくれます
例えば CI_REGISTRY_IMAGE は werf 用の変数 WERF_REPO を使っても OK です

その他コマンド

  • werf converge
    • ビルドアンドプッシュと k8s 環境へのデプロイもしてくれます
  • werf cleanup --repo container-registry/username/reponame --without-kube
    • 使用されていないイメージを自動で削除してくれます
    • 基本は --without-kube なしで使用し k8s 環境で使用されていないイメージを自動で削除しコンテナレジストリのディスク容量を解放するのが目的です
  • werf purge --repo container-registry/username/reponame
    • cleanup に似ていますがこれはイメージが使用されていようがいまいが強制的にコンテナレジストリ上のイメージを削除します

その他各種コマンドは https://werf.io/docs/v2/reference/cli/overview.html を参照してください

最後に

GitlabCI と werf を組み合わせてイメージのビルドアンドプッシュをしてみました
kaniko 代替としてはこれで十分な気もします
そもそも werf は k8s 環境に特化したツールなのでデプロイまで含めてやりたい場合などは werf 一択かなと思います

参考サイト

2025年10月17日金曜日

werf 超入門

werf 超入門

概要

kaniko がメンテされなくなり代わりのイメージビルドツールに werf というツールがあったので試してみました

環境

  • Ubuntu 24.04
  • werf 2.45.1

インストール

  • curl -sSL https://werf.io/install.sh | bash -s -- --version 2 --channel stable

.bashrc などに設定が記載されるのでターミナルを開きなおせば werf コマンドが使えるようになります

$ werf version
v2.45.1

werf.yaml の作成

  • vim werf.yaml
project: lab
configVersion: 1
---
image: test
context: .
dockerfile: Dockerfile

Dockerfile はプロジェクトの直下にあるものとします

werf.yaml の追加

werf.yaml はコミットされていないと認識されないので先にコミットします
リモート側に push まではしなくて OK です

  • git add .
  • git commit -m “Add werf.yaml”

ビルド

  • werf build

以下のように実行されます

Version: v2.45.1
WARNING: unable to parse ssh key /home/devops/.ssh/id_dsa: error parsing private key /home/devops/.ssh/id_dsa: ssh: unhandled key type
Using werf config render file: /tmp/werf-config-render-2657037271

┌ ???   (1/1) image test
│ ┌ Building stage test/dockerfile
│ │ test/dockerfile  #0 building with "default" instance using docker driver
│ │ test/dockerfile
│ │ test/dockerfile  #1 [internal] load remote build context
│ │ test/dockerfile  #1 DONE 0.1s
│ │ test/dockerfile
│ │ test/dockerfile  #2 copy /context /
│ │ test/dockerfile  #2 DONE 0.0s
│ │ test/dockerfile
│ │ test/dockerfile  #3 [internal] load metadata for docker.io/library/python:3.12.11-slim-bookworm
│ │ test/dockerfile  #3 ...
│ │ test/dockerfile
│ │ test/dockerfile  #4 [auth] library/python:pull token for registry-1.docker.io
│ │ test/dockerfile  #4 DONE 0.0s
│ │ test/dockerfile
│ │ test/dockerfile  #3 [internal] load metadata for docker.io/library/python:3.12.11-slim-bookworm
│ │ test/dockerfile  #3 DONE 2.7s
│ │ test/dockerfile
│ │ test/dockerfile  #5 [1/2] FROM docker.io/library/python:3.12.11-slim-bookworm@sha256:519591d6871b7bc437060736b9f7456b8731f1499a57e22e6c285135ae657bf7
│ │ test/dockerfile  #5 resolve docker.io/library/python:3.12.11-slim-bookworm@sha256:519591d6871b7bc437060736b9f7456b8731f1499a57e22e6c285135ae657bf7 done
│ │ test/dockerfile  #5 sha256:519591d6871b7bc437060736b9f7456b8731f1499a57e22e6c285135ae657bf7 9.13kB / 9.13kB done
│ │ test/dockerfile  #5 sha256:c00fc7b44d844b6da22861ec24af43968a5200eac4ec607b4725d585165d6b49 1.75kB / 1.75kB done
│ │ test/dockerfile  #5 sha256:688a685f6a1fa9250d7c6cee916889cbca364e4b027520110e0fce80c64a13e0 5.60kB / 5.60kB done
│ │ test/dockerfile  #5 sha256:5c32499ab806884c5725c705c2bf528662d034ed99de13d3205309e0d9ef0375 0B / 28.23MB 0.1s
│ │ test/dockerfile  #5 sha256:229c2e83adbca32a7582f378ce5a103e5c327ffd945d3f451fe66d1886693f34 0B / 3.52MB 0.1s
│ │ test/dockerfile  #5 sha256:d296ae3a1b166ec8b25480749804c463ffde8dd56bd4a0d938c764b93565254e 0B / 13.66MB 0.1s
│ │ test/dockerfile  #5 sha256:229c2e83adbca32a7582f378ce5a103e5c327ffd945d3f451fe66d1886693f34 3.52MB / 3.52MB 0.6s done
│ │ test/dockerfile  #5 sha256:5ac70eb707d38d47fc69ca91acd4de7256ef894f5f446a9c344a1f4ad7628114 0B / 250B 0.6s
│ │ test/dockerfile  #5 sha256:5c32499ab806884c5725c705c2bf528662d034ed99de13d3205309e0d9ef0375 2.10MB / 28.23MB 0.9s
│ │ test/dockerfile  #5 sha256:5c32499ab806884c5725c705c2bf528662d034ed99de13d3205309e0d9ef0375 11.53MB / 28.23MB 1.1s
│ │ test/dockerfile  #5 sha256:d296ae3a1b166ec8b25480749804c463ffde8dd56bd4a0d938c764b93565254e 4.19MB / 13.66MB 1.1s
│ │ test/dockerfile  #5 sha256:5c32499ab806884c5725c705c2bf528662d034ed99de13d3205309e0d9ef0375 14.68MB / 28.23MB 1.2s
│ │ test/dockerfile  #5 sha256:d296ae3a1b166ec8b25480749804c463ffde8dd56bd4a0d938c764b93565254e 7.50MB / 13.66MB 1.2s
│ │ test/dockerfile  #5 sha256:5ac70eb707d38d47fc69ca91acd4de7256ef894f5f446a9c344a1f4ad7628114 250B / 250B 1.1s done
│ │ test/dockerfile  #5 sha256:5c32499ab806884c5725c705c2bf528662d034ed99de13d3205309e0d9ef0375 16.78MB / 28.23MB 1.4s
│ │ test/dockerfile  #5 sha256:d296ae3a1b166ec8b25480749804c463ffde8dd56bd4a0d938c764b93565254e 13.66MB / 13.66MB 1.3s done
│ │ test/dockerfile  #5 sha256:5c32499ab806884c5725c705c2bf528662d034ed99de13d3205309e0d9ef0375 23.07MB / 28.23MB 1.5s
│ │ test/dockerfile  #5 sha256:5c32499ab806884c5725c705c2bf528662d034ed99de13d3205309e0d9ef0375 28.23MB / 28.23MB 1.6s
│ │ test/dockerfile  #5 extracting sha256:5c32499ab806884c5725c705c2bf528662d034ed99de13d3205309e0d9ef0375
│ │ test/dockerfile  #5 sha256:5c32499ab806884c5725c705c2bf528662d034ed99de13d3205309e0d9ef0375 28.23MB / 28.23MB 1.6s done
│ │ test/dockerfile  #5 extracting sha256:5c32499ab806884c5725c705c2bf528662d034ed99de13d3205309e0d9ef0375 1.6s done
│ │ test/dockerfile  #5 extracting sha256:229c2e83adbca32a7582f378ce5a103e5c327ffd945d3f451fe66d1886693f34
│ │ test/dockerfile  #5 extracting sha256:229c2e83adbca32a7582f378ce5a103e5c327ffd945d3f451fe66d1886693f34 0.2s done
│ │ test/dockerfile  #5 extracting sha256:d296ae3a1b166ec8b25480749804c463ffde8dd56bd4a0d938c764b93565254e
│ │ test/dockerfile  #5 extracting sha256:d296ae3a1b166ec8b25480749804c463ffde8dd56bd4a0d938c764b93565254e 0.8s done
│ │ test/dockerfile  #5 extracting sha256:5ac70eb707d38d47fc69ca91acd4de7256ef894f5f446a9c344a1f4ad7628114 done
│ │ test/dockerfile  #5 DONE 4.6s
│ │ test/dockerfile
│ │ test/dockerfile  #6 [2/2] RUN python -V
│ │ test/dockerfile  #6 0.496 Python 3.12.11
│ │ test/dockerfile  #6 DONE 0.8s
│ │ test/dockerfile
│ │ test/dockerfile  #7 exporting to image
│ │ test/dockerfile  #7 exporting layers 0.0s done
│ │ test/dockerfile  #7 writing image sha256:9b378fddd07c7546217a3ec2dba912453034e050f7a642d2052aa472fe085408 done
│ │ test/dockerfile  #7 naming to docker.io/library/9107100b-cef7-4f0a-9a66-49d3dbfe2bed done
│ │ test/dockerfile  #7 DONE 0.1s
│ │ ┌ Store stage into :local
│ │ └ Store stage into :local (0.01 seconds)
│ ├ Info
│ │      name: lab:594be819ed90f0081179a4a268c6045413df7b83b21e1a225badccdd-1760658483401
│ │        id: 9b378fddd07c
│ │   created: 2025-10-17 08:48:03.293806991 +0900 JST
│ │      size: 118.5 MiB
│ └ Building stage test/dockerfile (8.66 seconds)
└ ???   (1/1) image test (9.05 seconds)

Running time 9.30 seconds

以下のようなイメージができれば OK です

docker images
REPOSITORY            TAG                                                                      IMAGE ID       CREATED              SIZE
lab                   594be819ed90f0081179a4a268c6045413df7b83b21e1a225badccdd-1760658483401   9b378fddd07c   About a minute ago   124MB

プッシュする

ビルドしたイメージをプッシュするには --repo オプションを使います
例えば gitlab であれば以下のようにします

  • werf build --repo container-registry.your-gitlab-address/username-or-groupname/repo-name

コンテナレジストリのアドレス/ユーザ名orグループ名/gitlabのリポジトリ名 で指定します
実行すると以下のように gitlab に push されていることが確認できます

Version: v2.45.1
WARNING: unable to parse ssh key /home/devops/.ssh/id_dsa: error parsing private key /home/devops/.ssh/id_dsa: ssh: unhandled key type
Using werf config render file: /tmp/werf-config-render-2317128975

┌ ???   (1/1) image test
│ ┌ Copy suitable stage from secondary :local
│ │ Use previously built image for test/dockerfile
│ │      name: container-registry.your-gitlab-address/username-or-groupname/repo-name:594be819ed90f0081179a4a268c6045413df7b83b21e1a225badccdd-1760658483401
│ │        id: 9b378fddd07c
│ │   created: 2025-10-17 08:48:03 +0900 JST
│ │      size: 44.2 MiB
│ └ Copy suitable stage from secondary :local (18.60 seconds)
└ ???   (1/1) image test (19.27 seconds)

Running time 28.24 seconds

docker images を見ると先ほどビルドしたイメージに gitlab にプッシュするためのタグが作成されてそれがプッシュされていることが確認できます

コンテナレジストリにログインしていない場合は

以下のようなエラーになります

Error: unable to init storage manager: error get synchronization: unable to init http synchronization: unable to get client id for the http synchronization server: unable to get repo container-registry.your-gitlab-address/username-or-groupname/repo-name tags: unable to fetch tags for repo "container-registry.your-gitlab-address/username-or-groupname/repo-name": reading tags for "container-registry.your-gitlab-address/username-or-groupname/repo-name": GET https://xxx/jwt/auth?scope=repository%3Ausername-or-group%2Frepo-name%3Apull&service=container_registry: DENIED: access forbidden

素直に docker login すれば OK です

  • docker login container-registry.your-gitlab-address

タグを設定するには

werf はデフォルトだと UUID のようなランダムな文字列をタグとして付与します
latest やバージョンなど独自のタグを付与して push したい場合には以下のようにします

  • werf build --repo container-registry.your-gitlab-address/username-or-groupname/repo-name --add-custom-tag latest --add-custom-tag hoge

--add-custom-tag を使います
--add-custom-tag は複数指定できます
以下のように複数のタグが push されていることが確認できます

Version: v2.45.1
WARNING: unable to parse ssh key /home/devops/.ssh/id_dsa: error parsing private key /home/devops/.ssh/id_dsa: ssh: unhandled key type
Using werf config render file: /tmp/werf-config-render-4112436980

┌ ???   (1/1) image test
│ ┌ Copy suitable stage from secondary :local
│ │ Use previously built image for test/dockerfile
│ │      name: container-registry.your-gitlab-address/username-or-groupname/repo-name:594be819ed90f0081179a4a268c6045413df7b83b21e1a225badccdd-1760658483401
│ │        id: 9b378fddd07c
│ │   created: 2025-10-17 08:48:03 +0900 JST
│ │      size: 44.2 MiB
│ └ Copy suitable stage from secondary :local (2.14 seconds)
└ ???   (1/1) image test (2.90 seconds)

┌ Adding custom tags
│ ┌ tag latest
│ │   name: container-registry.your-gitlab-address/username-or-groupname/repo-name:latest
│ └ tag latest (3.27 seconds)
│
│ ┌ tag hoge
│ │   name: container-registry.your-gitlab-address/username-or-groupname/repo-name:hoge
│ └ tag hoge (3.23 seconds)
└ Adding custom tags (6.51 seconds)

Running time 17.49 seconds

最後に

kaniko の代用として werf を使ってみました
kaniko のようにビルド -> プッシュを一回のコマンドでできるは良い点です
次回は GitlabCI で werf を使ってみます

参考サイト

2025年10月16日木曜日

go-acme lego をローカルでビルドする方法

go-acme lego をローカルでビルドする方法

概要

流れを紹介します

環境

  • Ubuntu 24.04
  • golang 1.24.0

リポジトリのクローン

  • mkdir -p $GOPATH/src/github.com/go-acme
  • cd $GOPATH/src/github.com/go-acme
  • git clone git@github.com:YOUR_USERNAME/lego.git
  • cd lego

ビルド準備

  • make fmt

これでビルドに必要な golang を自動でインストールしてくれます
/usr/bin/go にあります

  • curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0
  • make checks

lint してくれます
結構時間がかかります
またマシンリソースも必要になります

    PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
 591663 devops    20   0 4634332   3.3g  34048 S 118.0  42.9  12:40.54 golangci-lint
golangci-lint run
0 issues.

となれば OK です

  • make test

ユニットテストを実行します
これも結構時間がかかります

ビルド

  • make build

成功すると dist/lego が作成されます
リントやテスト同様これも結構時間がかかります

実行

NIFCLOUD_ACCESS_KEY_ID=xxx \
NIFCLOUD_SECRET_ACCESS_KEY=xxx \
NIFCLOUD_PROPAGATION_TIMEOUT=1800 \
NIFCLOUD_POLLING_INTERVAL=40 \
NIFCLOUD_TTL=40 \
dist/lego \
--path /tmp/lego_test \
--email your@mail \
--dns nifcloud \
--domains test.your.domain \
--accept-tos
run

最後に

あとはブランチを作成し自分のリポジトリにpushしRPを作成すればOKです

参考サイト

2025年10月15日水曜日

code-server をアップグレードする方法

code-server をアップグレードする方法

概要

拡張は自動で更新してくれるようなのですが本体は自動では更新してくれないようなので手動で行います

環境

  • Ubuntu 24.04
  • code-server 4.103.2 -> 4.104.3

アップデート

curl -fsSL https://code-server.dev/install.sh | sh

インストールスクリプトを再度実行するだけです

あとはプロセスを再起動しましょう

  • sudo systemctl daemon-reload
  • sudo systemctl restart code-server@$USER

ページをリロードして最新になっていることを確認します

設定をカスタムしている場合には

アップグレードすると設定ファイルも初期化されてしまうことがあるので変更されている場合は再度手動で変更しましょう

  • vim /home/devops/.config/code-server/config.yaml
bind-addr: 0.0.0.0:8081

また service ファイルも初期化されていることがあるので証明書などを設定している場合は再度書き換えましょう

https://hawksnowlog.blogspot.com/2025/08/code-server-must-b-setup-https-configuration.html

~/.config/code-server/config.yaml に cert と cert-key の設定を記載すれば service ファイルが上書きされても毎回変更する必要はないかもです

最後に

自動化する方法はないだろうか

参考サイト

2025年10月14日火曜日

Gitlab で renovate を使うときの基本設定

Gitlab で renovate を使うときの基本設定

概要

renovate-runner は使わずまずはローカルから実行する際の最低限の設定について紹介します

環境

  • Ubuntu 24.04.3
  • nodejs 22.14.0
  • renovate 41.144.1

renovate.json

このファイルは renovate を実行するターゲットのリポジトリ配下に配置します

{
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
  "extends": [
    "config:recommended"
  ],
  "automergeStrategy": "squash",
  "ignorePaths": [
    "**/archive/**"
  ]
}

renovate コマンド

GITHUB_COM_TOKEN=ghp_xxx LOG_LEVEL=info renovate --platform=gitlab --endpoint=https://your-gitlab-com-url/api/v4 --username=oauth2 --token=glpat-xxx your/repo

トラブルシューティング

curl 'https://index.docker.io/v2/library/pthon/tags/list?n=10000'
{"errors":[{"code":"UNAUTHORIZED","message":"authentication required","detail":[{"Type":"repository","Class":"","Name":"library/pthon","Action":"pull"}]}]}

リポジトリ名が間違っている場合に認証エラーになるようです

最後に

renovate を動かす場合には必ずターゲットのリポジトリに renovate.json を配置しましょう
配置しないでも動かすことはできますが Gitlab の場合などは MR などが作成されないので注意しましょう

2025年10月9日木曜日

Ruby OpenSSL::SSL::SSLError - SSL_connect returned=1 errno=0 対策

Ruby OpenSSL::SSL::SSLError - SSL_connect returned=1 errno=0 対策

概要

macOS + openssl@3.6.0 で発生します
回避策を紹介します

エラー全文

2025-10-09 10:48:58 - OpenSSL::SSL::SSLError - SSL_connect returned=1 errno=0 peeraddr=[2606:4700::6810:190e]:443 state=error: certificate verify failed (unable to get certificate CRL) (OpenSSL::SSL::SSLError):

環境

  • macOS 15.7.1
  • Ruby 3.4.4
  • openssl 3.6.0

回避策

  • vim libs/ssl_patch.rb
# frozen_string_literal: true

require 'openssl'

# 開発環境限定で SSL 検証をスキップする
if ENV['MODE'] == 'dev'
  s = OpenSSL::X509::Store.new.tap(&:set_default_paths)
  OpenSSL::SSL::SSLContext.send(:remove_const, :DEFAULT_CERT_STORE)
  OpenSSL::SSL::SSLContext.const_set(:DEFAULT_CERT_STORE, s.freeze)
end

あとはメインなどの先頭で

require './libs/ssl_patch'

パッチなので可能な限り先に読み込むようにしてください

最後に

SSL 関係はいろいろとハックしなければいけないケースが多いので辛いです

参考サイト

2025年10月7日火曜日

VSCode + Excel MCP を試す

VSCode + Excel MCP を試す

概要

VSCode で Excel MCP を試してみました
Excel の MCP はたくさんあるのですが今回は negokaz/excel-mcp-server を使います
なおクライアントは Github copilot chat ではなく前回同様 RooCode を使います

環境

  • Windows11
  • nodejs 22.20.0 (nvm for windows)
  • VSCode 1.104.3
  • RooCode 3.28.15

node のインストール

ここから最新の nvm-setup.exe をダウンロードして nvm をインストールします

nvm がインストールできたら Powershell を開いて以下を実行します

  • nvm install 22.20.0
  • nvm use 22.20.0

VSCode のターミナルを開いて node -v が実行できれば OK です

Excel MCP の追加

  1. 左メニューの RooCode のアイコンをクリック
  2. 右上の「・・・」のメニューをクリック
  3. 「MCP Servers」を選択
  4. グローバルMCPを編集

で以下を追記します

{
  "mcpServers": {
    "excel": {
      "command": "npx",
      "args": [
        "--yes",
        "@negokaz/excel-mcp-server"
      ],
      "env": {
        "EXCEL_MCP_PAGING_CELLS_LIMIT": "4000",
        "EXCEL_MCP_DEFAULT_FILE_PATH": "C:/path/to/your_excel_file.xlsx",
        "EXCEL_MCP_USE_EXCELIZE": "true"
      }
    }
  }
}

これで保存して Excel MCP サーバがグリーンランプになり起動することを確認します

動作確認

RooCode のチャットを使ってエクセルを操作していみましょう

以下のように入力してみてください

MCP を使って C:/path/to/your_excel_file.xlsx の内容を説明してください

すると MCP の excel_describe_sheetsexcel_read_sheet などの命令を使ってエクセルを読み取りエクセル内の情報を表示してくれるのが確認できると思います

自動承認に MCP を入れると楽です

最後に

Excel MCP を使ってみました
今回は読み取りだけ行ってみましたが書き込みなども可能です
ただ複雑なエクセルはどこまでできるか不明なのでその辺りの検証という MCP を使ってどこまでするかの線引は必要かなと思います

参考サイト

2025年10月4日土曜日

SQLAlchemy でテーブルスキーマと automap を共存させる方法

SQLAlchemy でテーブルスキーマと automap を共存させる方法

概要

過去にも紹介しましたがすでにテーブルがある際にわざわざテーブルのスキーマを定義しなくても Python からテーブルの情報を取得したりできる機能が automap です

今回は automap をすでに使っている環境で新規にテーブルを追加するスキーマを automap と共存させる方法を紹介します

同一 Metadata を使うのがポイントです

環境

  • macOS 15.7.1
  • MySQL 9.4.0
  • Python 3.12.11
    • mysqlclient 2.2.7
    • SQLAlchemy 2.0.43
    • alembic 1.16.5

テーブル準備

USE testdb;
CREATE TABLE user_legacy (
    id INT AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(100) NOT NULL,
    email VARCHAR(255) NOT NULL UNIQUE,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

このテーブルは automap 用のテーブルになります

lib/db.py

データベース接続用のファイルです

from sqlalchemy import create_engine

# MySQL で先に testdb というデータベースを作成しておくこと
# CREATE DATABASE testdb;
# MySQL ドライバは mysqlclient なので mysqldb を指定する
# 今回はテストなのでパスワード無しで root ユーザで接続
DATABASE_URL = "mysql+mysqldb://root:@localhost:3306/testdb"

engine = create_engine(DATABASE_URL, echo=True)


def get_engine():
    return engine

lib/models.py

モデルを定義するファイルです
automap の定義と追加するテーブルのスキーマ定義を共存させます
automap では既存のテーブルのみを管理するので reflection_options を追加します

reflection_options の設定がないと以下のエラーになります

sqlalchemy.exc.InvalidRequestError: Table 'user' is already defined for this MetaData instance.  Specify 'extend_existing=True' to redefine options and columns on an existing Table object.
from sqlalchemy import Integer, MetaData, String
from sqlalchemy.ext.automap import automap_base
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column

from lib.db import get_engine

# 共通の metadata を作る
metadata = MetaData()


# Declarative 用 Base
class Base(DeclarativeBase):
    metadata = metadata


# automap 用 Base
AutomapBase = automap_base(metadata=metadata)

# --- 既存DBをリフレクションしてクラス自動生成 ---
engine = get_engine()
# automap の対象となるテーブルを reflection_options で指定する
AutomapBase.prepare(
    engine,
    reflect=True,
    reflection_options={"only": ["user_legacy"]},  # automap で読み込むテーブルを限定
)

# 例: 既存のテーブル user_legacy を ORM クラスとして取得できる
# UserLegacy = AutomapBase.classes.user_legacy


# --- 新規テーブルは DeclarativeBase で定義 ---
class User(Base):
    __tablename__ = "user"

    id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
    name: Mapped[str] = mapped_column(String(255), nullable=False)
    age: Mapped[int] = mapped_column(Integer, nullable=False)

マイグレーション確認

今回新規で追加するのは user テーブルになります

  • pipenv run alembic revision --autogenerate -m "Create user table."

これでマイグレーションファイルが作成できたら upgrade します

  • pipenv run alembic upgrade head

これで user テーブルが作成されていれば OK です

mysql> show tables;
+------------------+
| Tables_in_testdb |
+------------------+
| alembic_version  |
| user             |
| user_legacy      |
+------------------+
3 rows in set (0.004 sec)

app.py
で動作確認

automap と手動で定義したスキーマを使って各種 CRUD ができるか確認します

  • vim app.py
from sqlalchemy import delete, select, update
from sqlalchemy.orm import Session

from lib.db import get_engine
from lib.models import AutomapBase, Base, User

# SQLite / MySQL どちらでもOK
engine = get_engine()

# Declarative で定義したテーブルを作成 (まだ存在しなければ)
Base.metadata.create_all(engine)

# automap で読み込んだ既存テーブル
UserLegacy = AutomapBase.classes.user_legacy

# セッション作成
with Session(engine) as session:
    # --- INSERT (user) ---
    new_user = User(name="Alice", age=25)
    session.add(new_user)
    session.commit()
    print(f"Inserted User ID: {new_user.id}")

    # --- SELECT (user) ---
    stmt = select(User).where(User.name == "Alice")
    result = session.scalars(stmt).all()
    for user in result:
        print(f"Selected User: id={user.id}, name={user.name}, age={user.age}")

    # --- UPDATE (user) ---
    stmt = update(User).where(User.name == "Alice").values(age=26)
    session.execute(stmt)
    session.commit()
    print("Updated Alice's age to 26")

    # --- DELETE (user) ---
    stmt = delete(User).where(User.name == "Alice")
    session.execute(stmt)
    session.commit()
    print("Deleted Alice")

    # -------------------------
    # automap: user_legacy 操作
    # -------------------------
    # --- INSERT (user_legacy) ---
    new_legacy = UserLegacy(username="bob", email="bob@example.com")
    session.add(new_legacy)
    session.commit()
    print(f"Inserted UserLegacy ID: {new_legacy.id}")

    # --- SELECT (user_legacy) ---
    stmt = select(UserLegacy).where(UserLegacy.username == "bob")
    result = session.scalars(stmt).all()
    for ul in result:
        print(f"Selected UserLegacy: id={ul.id}, username={ul.username}, email={ul.email}")

    # --- UPDATE (user_legacy) ---
    stmt = update(UserLegacy).where(UserLegacy.username == "bob").values(email="bob@newmail.com")
    session.execute(stmt)
    session.commit()
    print("Updated bob's email")

    # --- DELETE (user_legacy) ---
    stmt = delete(UserLegacy).where(UserLegacy.username == "bob")
    session.execute(stmt)
    session.commit()
    print("Deleted bob from user_legacy")
  • pipenv run python app.py

user_legacy も user もどちらも操作できることを確認しましょう

最後に

SQLAlchemy の automap と独自で定義したテーブルスキームを共存させる方法を紹介しました
automap はデフォルトだとすべてのテーブル情報を読み込んでしまうので reflection_options を使うのがポイントです
あとは Metadata を共有すれば OK です

すでにテーブルがあるサービスを automap で管理してる場合にどうしても新規でテーブルを追加しなければならないケースなどに使えるテクニックかなと思います

参考サイト