2025年11月6日木曜日

code-server では Python の拡張だけでは import などを自動でしてくれない

code-server では Python の拡張だけでは import などを自動でしてくれない

概要

VSCode では Pylance の languageServer が使えるのですが code-server では使えません
その影響でメソッドやモジュールの自動インポートはしてくれません

今回はそんな場合の対処方法を紹介します

環境

  • macOS 15.7.1
  • code-server 4.105.1

インストール

  • brew install code-server

起動

  • brew services run code-server

Python 拡張のインストール

  • 左メニュー拡張
  • 検索バーに「Python」
  • ms-python 製の Python 拡張をインストールします

BasedPyright のインストール

  • 左メニュー拡張
  • 検索バーに「basedpyright」
  • detachhead 製の BasedPyright 拡張をインストールします

設定

  • settings.json
{
    "workbench.colorTheme": "Default Dark Modern",
     "basedpyright.analysis.diagnosticMode": "openFilesOnly"
}

動作確認

これでメソッド名の保管時に自動で import してくれるようになります
また型チェックなどもしてくれますが純正の pyright とは違うので注意してください

もし pyright 基準にしたい場合は以下の設定を入れると多少は近くなります

{
    "basedpyright.analysis.typeCheckingMode": "standard"
}

最後に

VSCode では普通にできても code-server ではできないことが多くあるので注意しましょう

参考サイト

2025年11月4日火曜日

OpenSpec 超入門

OpenSpec 超入門

概要

OpenSpec は簡単に言えば Markdown (仕様書) を作成するだけでアプリが開発できてしまうツールです
今までは特にフォーマットもなくプロンプトに命令していましたがそれを定型化したツールになります
今回は簡単なブラウザのタイマーアプリを OpenSpec を使って作ってみました

環境

  • Ubuntu 24.04
  • node 22.21.0
  • openspec 0.13.0
  • code-server v4.104.3

インストール

  • npm install -g @fission-ai/openspec@latest

初期化

まずはプロジェクトを初期化します

  • openspec init

 ????   ?????   ??????  ??  ??   ?????  ?????   ??????   ?????
??  ??  ??  ??  ??      ??? ??  ??      ??  ??  ??      ??
??  ??  ?????   ?????   ?? ???   ????   ?????   ?????   ??
??  ??  ??      ??      ??  ??      ??  ??      ??      ??
 ????   ??      ??????  ??  ??  ?????   ??      ??????   ?????

Welcome to OpenSpec!

Step 1/3

Configure your OpenSpec tooling
Let's get your AI assistants connected so they understand OpenSpec.

Press Enter to continue.

次に使用するLLMを選択します

Step 2/3

Which natively supported AI tools do you use?
Use ↑/↓ to move ・ Space to toggle ・ Enter selects highlighted tool and reviews.

    Natively supported providers (? OpenSpec custom slash commands available)
? ○ Auggie (Augment CLI)
  ○ Claude Code
  ○ Cline
  ○ CodeBuddy Code (CLI)
  ○ Crush
  ○ Cursor
  ○ Factory Droid
  ○ OpenCode
  ○ Kilo Code
  ○ Windsurf
  ○ Codex
  ○ GitHub Copilot
  ○ Amazon Q Developer

    Other tools (use Universal AGENTS.md for Amp, VS Code, GitHub Copilot, …)
  ○ Universal AGENTS.md (always available)

Selected configuration:
  - No natively supported providers selected

今回は Github Copilot を使います

Step 3/3

Review selections
Press Enter to confirm or Backspace to adjust.

? GitHub Copilot

これで初期化が完了です

? GitHub Copilot
? OpenSpec structure created
? AI tools configured

? OpenSpec initialized successfully!

Tool summary:
? Root AGENTS.md stub created for other assistants
? Created: GitHub Copilot
? Skipped: Auggie, Claude Code, Cline, CodeBuddy Code, Crush, Cursor, Factory Droid, OpenCode, Kilo Code, Windsurf, Codex, and Amazon Q Developer

Use `openspec update` to refresh shared OpenSpec instructions in the future.

Next steps - Copy these prompts to GitHub Copilot:
────────────────────────────────────────────────────────────
1. Populate your project context:
   "Please read openspec/project.md and help me fill it out
    with details about my project, tech stack, and conventions"

2. Create your first change proposal:
   "I want to add [YOUR FEATURE HERE]. Please create an
    OpenSpec change proposal for this feature"

3. Learn the OpenSpec workflow:
   "Please explain the OpenSpec workflow from openspec/AGENTS.md
    and how I should work with you on this project"
────────────────────────────────────────────────────────────

project.md
の編集

ここにどんなアプリを作成したいか概要を記載ます とりあえず参考サイトにもあるブラウザで動かくタイマアプリを作成してみます Project Conventions の欄はとりあえず不要なので削除しています

  • vim openspec/project.md
# Project Context

## Purpose
家庭や業務用キッチンで使用できるシンプルで視認性の高いタイマーを、Web ブラウザー経由で提供します。

- 1分、3分、5分を設定するためのボタンを配置します。ボタンを押すと、その時間からカウントダウンが開始されます。
- カウントダウン中は、残り時間を目立つように表示します。
- カウントダウン中に再度ボタンを押すと、タイマーがリセットされ、新たなカウントダウンが開始されます。

## Tech Stack
- HTML5
- JavaScript(ES6)
- CSS3

提案書の作成

code-server の Github Copilot のチャット欄に /openspec-proposal コマンドを入力します

  • /openspec-proposal UIを作成する

すると必要な Markdown ファイルを changes 配下に作成してくれます

ls -lR openspec/changes/ui-timer/
openspec/changes/ui-timer/:
total 12
-rw-r--r-- 1 devops docker  374 Nov  4 13:57 proposal.md
drwxr-xr-x 3 devops docker 4096 Nov  4 13:57 specs
-rw-r--r-- 1 devops docker  341 Nov  4 13:57 tasks.md

openspec/changes/ui-timer/specs:
total 4
drwxr-xr-x 2 devops docker 4096 Nov  4 13:57 timer-ui

openspec/changes/ui-timer/specs/timer-ui:
total 4
-rw-r--r-- 1 devops docker 810 Nov  4 13:57 spec.md

作成されたファイルの中身を確認して仕様に間違いがないか確認しましょう 変更は Copilot Chat にお願いしてもいいですし手動で書き換えても OK です 問題なければ Keep してファイルを保存しましょう

検証する

チャット欄に「検証して」と入力すると勝手に openspec validate コマンドを実行してくれます 内部的には openspec validate ui-timer --strict を実行しています

生成された Markdown に不備があると以下のようにエラーが表示されます ちなみにエラーがよくわからなくても「修正してください」と命令し再度「検証してください」と命令すればそれだけで修正できます

code-server が openspec コマンドを実行できない場合は PATH を確認するか PATH に通っているけど実行できない場合は sudo systemctl restart code-server@USER で再起動しましょう

コードを生成する

あとは /openspec-apply を Copilot Chat に入力すればコードを生成してくれます 各種 html や js が作成されたのが確認できます 内容を確認し問題なければ Keep しましょう

動作確認

これもチャットに「動作確認して」と依頼すればブラウザでアプリを開いて動作確認してくれます

Web アプリがない場合は Python で簡易用のアプリも起動してくれます

アーカイブする

アプリの挙動が問題なければアーカイブします チャット欄に /openspec-archive を入力しましょう すると生成されたコードは archive 配下に移動します

以後アプリに修正を入れたい場合は再度提案を作成し修正するようにしましょう そうすることで changes/archive 配下に日付ごとにディレクトリが作成され提案ごとに履歴管理することができるようになります

最後に

OpenSpec に入門してみました 基本は Markdown を書くだけで開発していく感じです 今まではプロンプトに自分なりの質問やテンプレートを使って質問していたのが完全に形式化したのが OpenSpec かなと思います

新規系は簡単に導入できそうですが既存のアプリにこれを導入するとなるとかなり大変そうです

参考サイト

2025年11月3日月曜日

GCP の Ops エージェントを完全にアンインストールする方法

GCP の Ops エージェントを完全にアンインストールする方法

概要

コンソールから Ops エージェントをインストールするとなぜかアンインストールしても勝手に再インストールしてしまうので完全にアンインストールする方法を紹介します

環境

  • Ops エージェント 2.60.0
  • Ubuntu 22.04

まずは purge

  • sudo apt purge google-cloud-ops-agent

設定ファイルの削除

purge した場合は基本ないはずですが一応確認します

  • sudo ./add-google-cloud-ops-agent-repo.sh --remove-repo
  • sudo rm /etc/apt/sources.list.d/google-cloud-monitoring.list

/opt 配下にも何も無いことを確認します

VM のカスタムメタデータの削除

これがあるとエージェントが勝手に再インストールしている可能性があるので削除します
コンソールでも OK です

  • gcloud compute instances remove-metadata YOUR_INSTANCE_NAME --zone=YOUR_ZONE --keys=enable-osconfig

VM のラベルの削除

  • VM インスタンス -> 選択 -> 編集 -> ラベルを管理 -> goog-ops-agent-policy のラベルを削除

デフォルト

以下のエージェントだけになっていればデフォルトの状態です

dpkg -l | grep google
ii  google-compute-engine            20230808.00-0ubuntu1~22.04.1            all          Google Compute Engine guest environment.
ii  google-compute-engine-oslogin    20231004.00-0ubuntu1~22.04.3            amd64        Google Compute Engine OS Login
ii  google-guest-agent               20231004.02-0ubuntu1~22.04.5            amd64        Google Compute Engine Guest Agent
ii  google-osconfig-agent            20240524.03-0ubuntu2~22.04.1            amd64        Google OS Config Agent

動作確認

最終的にはコンソールでオブザーバビリティを確認したい際に「Opsエージェントをインストール」が表示されていれば完全に削除できています
インストール中の保留中やメトリクスが送信できないような文言の場合はまだタグやラベルが残っている可能性があります

最後に

GCP の小さめのインスタンスに Ops エージェントをインストールすると CPU とメモリを食い尽くして他のプロセスが全く動かなくなるので安易にインストールするのはやめておきましょう

2025年11月1日土曜日

Gitlab CI で nikto を動かし DAST 的なことをする

Gitlab CI で nikto を動かし DAST 的なことをする

概要

前回 Gitlab CI のサービスを使って Web アプリを起動しそのアプリにテストする方法を紹介しました
今回は nikto を実行してみます

環境

  • Gitlab 18.2.6
  • Gitlab Runner 18.2.2
  • Python 3.12.11

.gitlab-ci.yml

stages:
  - build   # ビルドステージ
  - test    # テストステージ
  - pentest # ペネトレーションテストステージ

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

pentest:
  stage: pentest
  image:
    name: ghcr.io/sullo/nikto:latest
    entrypoint: [""]
  services:
    - name: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
      alias: testapp
  script:
    - echo "Running Nikto penetration test..."
    - mkdir -p reports  # レポートを保存するディレクトリを作成
    - nikto.pl -h http://testapp:5000 -F htm -o reports/report.html
    - echo "Nikto scan completed. Report saved to reports/report.html."
  artifacts:
    paths:
      - reports/report.html  # レポートをアーティファクトとして保存
    expire_in: 1 week  # レポートの保存期間を 1 週間に設定

最後に

Gitlab Ultimate に登録していない場合に DAST が使えないので自分で .gitlab-ci.yml にペネトレーションテスト的なことを記載する必要があります
またレポートも自分で保存する必要があるので artifact を使って保存するようにしましょう

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 の操作をしてみました
認証が面倒ですが一度認証してしまえばあとはスクリプトを実行するだけです

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