every Tech Blog

株式会社エブリーのTech Blogです。

Dev Containerを使ってみる

はじめに

エブリーでデリッシュキッチンの開発をしている本丸です。
4月に新卒の開発研修を行ったのですが、配属されたチームが異なることなどもあり開発環境が揃っていないという問題がありました。 anyenvやgoenvなどを設定してもらうという手もあったのですが、それ自体の設定にも手間がかかってしまうためDev Containerの設定を用意してその場は乗り切りました。
本記事では、Dev Containerの基本的な使い方から、Docker Composeとの連携、拡張機能の設定まで、実際の開発現場で役立つ知識をお届けできればと思っています。

Dev Containerとは

Dev Containerは、開発環境をコンテナ化する仕組みです。Visual Studio CodeやCursorなどで利用することができ、開発に必要なツールやライブラリ、設定をすべてコンテナに封じ込めることができます。

主な利点

  • 環境の統一: チーム全員が同じ開発環境で作業できる
  • 簡単に開発環境を作成できる: 新しいメンバーも数分で開発環境を構築可能
  • 隔離性: プロジェクトごとに独立した環境を持てる、AIエージェントを使う上で意図しない環境の変更を防げる

使い方

基本的な使い方

1. Dev Container設定ファイルの作成

プロジェクトルートに.devcontainer/devcontainer.jsonを作成します。

{
    "name": "Go",
    // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
    "image": "mcr.microsoft.com/devcontainers/go:1-1.24-bookworm",

  ...

    // Configure tool-specific properties.
    "customizations": {
        // Configure properties specific to VS Code.
        "vscode": {
            "settings": {},
            "extensions": [
                "streetsidesoftware.code-spell-checker"
            ]
        }
    },

  ...

    // Use 'portsAttributes' to set default properties for specific forwarded ports. 
    // More info: https://containers.dev/implementors/json_reference/#port-attributes
    "portsAttributes": {
        "9000": {
            "label": "Hello Remote World",
            "onAutoForward": "notify"
        }
    }

  ...
}

2. 開発環境の起動

devcontainer.jsonの作成が終わったらあとは開発環境のコンテナを起動するだけです。

  1. VS CodeでDev Containers拡張機能をインストール
  2. コマンドパレット(Cmd+Shift+P)で「Dev Containers: Reopen in Container」を実行

これだけで、統一された開発環境が起動します。

サンプルコードで試すことができる

基本的な設定に関してはサンプルコードを参考にすることもできます。 サンプルコードを試す方法は簡単で

  1. コマンドパレット(Cmd+Shift+P)で「Dev Containers: Try a Dev Container Sample...」を実行
  2. 開発したい言語を指定する

これだけで、簡単なサーバーを実行するためのサンプルコードを試すことができます。
ちなみに、「Dev Container設定ファイルの作成」で例示したものはサンプルコードのdevcontainer.jsonを引用したものです。

開発環境と同時にローカルの実行環境も立ち上げる

基本的な使い方では、イメージを指定して実行を行っていましたがDocker Composeファイルを指定して実行することもできます。 今回は、開発環境とローカルでの実行環境を同時に立ち上げるという形で説明させていただければと思います。
Dev Containerの対応前は下記で動いていたとします。

  • Go
  • Docker Compose

1. Dev Container用のDocker Composeファイルを作成

今回は、元々のDocker Composeファイルをできるだけ変更することなく進めたかったため、Dev Containerの時だけ使用するDocker Composeファイルを用意することにしました。

プロジェクトルートのdevcontainerディレクトリにcompose-devcontainer.ymlを作成します。(命名はわかりやすいように変更しましたが、任意の名前で大丈夫なはずです。)

services:
  dev-env:
    image: mcr.microsoft.com/devcontainers/go:1-1.24-bookworm
    volumes:
      - .:/workspace:cached
    ports:
      - "9000:9000"
    command: sleep infinity

ここでは元々のDocker Composeで動かしていたserviceに加えて新しくdev-envを動作させています。 このdev-envがDev Containerでの開発環境にあたります。

2. devcontainer.jsonをDocker Composeに対応させる

devcontainer.jsonをDocker Composeを使用するように変更していきます。

基本版からDocker Compose版への変更点

  1. イメージ指定の変更
   - "image": "mcr.microsoft.com/devcontainers/go:1-1.24-bookworm",
   + "dockerComposeFile": [
   +     "../docker-compose.yml",
   +     "compose-devcontainer.yml"
   + ],

imageプロパティを削除してdockerComposeFileプロパティを追加します。この際、複数のYAMLファイルを選択することができます。

  1. サービスとワークスペースの指定を追加
   + "service": "dev-env",
   + "workspaceFolder": "/workspace",

ここでは、どのサービスを開発環境として使うかなどの指定を行なっています。

変更後の完全なdevcontainer.json

{
    "name": "Go",
    // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
     "dockerComposeFile": [
        "../docker-compose.yml",
        "compose-devcontainer.yml"
    ],

  ...

    // Configure tool-specific properties.
    "customizations": {
        // Configure properties specific to VS Code.
        "vscode": {
            "settings": {},
            "extensions": [
                "streetsidesoftware.code-spell-checker"
            ]
        }
    },

  "service": "dev-env",
    "workspaceFolder": "/workspace",

  ...

    // Use 'portsAttributes' to set default properties for specific forwarded ports. 
    // More info: https://containers.dev/implementors/json_reference/#port-attributes
    "portsAttributes": {
        "9000": {
            "label": "Hello Remote World",
            "onAutoForward": "notify"
        }
    }

  ...
}

拡張機能に関して

Dev Containerでの拡張機能の指定の方法は2つあります。 複数のユーザーで共通で使いたい場合はdevcontainer.json

   "customizations": {
        // Configure properties specific to VS Code.
        "vscode": {
            "settings": {},
            "extensions": [
                "streetsidesoftware.code-spell-checker",
        ...,
         ]
        }
    },

を追加し、extensionsに任意の拡張機能を指定すれば大丈夫です。

一人のユーザーが複数のDev Containerの環境で同じ拡張機能を使いたい場合はsettings.json

"dev.containers.defaultExtensions": [
  "streetsidesoftware.code-spell-checker",
  ...,
],

を追加し、dev.containers.defaultExtensionsに任意の拡張機能を指定すれば大丈夫です。

まとめ

Dev Containerについて調べたり、触ってみる中でDev Containerをどのように使うのかの具体的なイメージを掴むことができました。 まだ実際の開発の現場で利用はできていないため、実際のプロダクトを開発する際にも取り入れてみたいと思いました。

参考

クロスクラウド環境で AWS SSM を利用して SSH の開放範囲を絞る

エブリーで小売業界向き合いの開発を行っている @kosukeohmura です。

エブリーでは全社的に SSH を使ったサーバーへのログインから、AWS Systems Manager Session Manager ( 以下 Session Manager ) を使った運用に切り替えました。

tech.every.tv

これは私達のチームで管理している他社クラウド (AWS 以外という意味で他社です) 上に存在するサーバについても対象ですが、Session Manager を直接利用することはできません。そこで Session Manager を使った SSH 踏み台サーバーを構築し、それ経由で SSH 接続することで、他社クラウド上のマシンの 22 番ポートを開放する範囲を限定することができました。

構成

簡単に前提とする構成を説明します。

AWS VPC と他社クラウドのネットワーク同士が Site-to-Site VPN で接続されている構成です。開発者や CI/CD は、AWS SSM を通して踏み台サーバーに SSH 接続し、更に踏み台サーバーから他社クラウド上の Target Server へ SSH でアクセスします。

この構成を取ることによって、目的のサーバーの SSH ポートの開放範囲を AWS VPC の CIDR ブロックのみに限定することができます。

Session Manager を通した SSH 接続

セッション開始時には aws ssm start-session というコマンドを使いますが、その際 AWS-StartSSHSession というドキュメントを指定することによって、SSH セッションが開始できます*1

なおその際の接続対象 (今回の場合、踏み台サーバー) では SSH 接続をサポートするように設定する必要があります*2。つまり SSH サーバーである必要があります。

踏み台サーバーの構築

sshd を動かすシンプルなサーバーを作ります。今回 ECS タスクとして動作させますが、環境に合わせて EC2 や EKS などでも良いかと思います。

Dockerfile は下記のようになりました。シンプルですね。

FROM debian:stable-20250520-slim

RUN apt-get update && \
    apt-get -y upgrade && \
    apt-get -y install openssh-server net-tools && \
    apt-get clean && \
    rm -rf /var/lib/apt/lists/* && \
    mkdir /var/run/sshd

COPY --chmod=755 entrypoint.sh /root/entrypoint.sh
EXPOSE 22
ENTRYPOINT ["/root/entrypoint.sh"]

次に Dockerfile 内で ENTRYPOINT としている起動スクリプトです。

# entrypoint.sh
#!/bin/bash

if [ -n "$SSH_PUBLIC_HOST_KEY_BASE64ENCODED" ]; then
    echo "$SSH_PUBLIC_HOST_KEY_BASE64ENCODED" | base64 -d > /etc/ssh/ssh_host_ed25519_key.pub
    chmod 644 /etc/ssh/ssh_host_ed25519_key.pub
fi
if [ -n "$SSH_PRIVATE_HOST_KEY_BASE64ENCODED" ]; then
    echo "$SSH_PRIVATE_HOST_KEY_BASE64ENCODED" | base64 -d > /etc/ssh/ssh_host_ed25519_key
    chmod 600 /etc/ssh/ssh_host_ed25519_key
fi
echo "$ROOT_PUBLIC_SSH_KEY_BASE64ENCODED" | base64 -d > /root/.ssh/authorized_keys
chmod 600 /root/.ssh/authorized_keys

exec /usr/sbin/sshd -D

ホストキーが起動毎に毎回生成されるのを避けるため、タスク起動時にあらかじめ生成した鍵でファイルを作成しています。なお各キーは AWS Secrets Manager に Base64 エンコードし保管してあるものを、そのまま ECS タスクの環境変数として注入している都合でデコードしています。

運用上の工夫

目的のサーバーへ踏み台サーバーを経由し SSH 接続をするためには、SSM セッションを確立後に SSH での多段接続という手順を踏む必要があり、毎回行うのは手間です。そこで簡単なスクリプトを使って、接続時の手間を軽減しています。

まず ECS タスクへと aws ssm start-session コマンドを実行する場合、--target オプションを <cluster_name>_<task_id>_<container_runtime_id> といった形式での指定が手間なので、下記 Make ターゲットで出力できるようにしています(中身はいささか力技ですが)。

CLUSTER_NAME := '<cluster_name>'
FAMILY_NAME := '<family_name>'

ACCOUNT_ID := `aws sts get-caller-identity --query "Account" --output text;`
TASK_ID := `aws ecs describe-tasks --cluster $(CLUSTER_NAME) --tasks \`aws ecs list-tasks --cluster $(CLUSTER_NAME) --family $(FAMILY_NAME) | jq -r '.taskArns[0]'\` | jq -r '.tasks[0].taskArn' | awk -F'/' '{print $$3}'`
CONTAINER_RUNTIME_ID := `aws ecs describe-tasks --cluster $(CLUSTER_NAME) --tasks \`aws ecs list-tasks --cluster $(CLUSTER_NAME) --family $(FAMILY_NAME) | jq -r '.taskArns[0]'\` | jq -r '.tasks[0].containers[0].runtimeId'`

.PHONY: echo-as-ssm-target
echo-as-ssm-target:
    @echo "ecs:$(CLUSTER_NAME)_$(TASK_ID)_$(CONTAINER_RUNTIME_ID)"

.ssh/config は下記のようにしています。少々トリッキーですが、make コマンドを .ssh/config 内で使用しています。ProxyJump オプションにより、bastion 経由で target_server への多段接続が自動的に行われます。結果的に ssh target_server を実行するだけで目的のサーバーへログインできます。

# .ssh/config

host bastion
  User root
  IdentityFile ~/.ssh/<bastion_key_name>
  ProxyCommand sh -c "aws ssm start-session --target `make -C <path_to_makefile_dir> echo-as-ssm-target` --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"

Host target_server
  ProxyJump bastion
  IdentityFile ~/.ssh/<target_key_name>
  HostName ...

GitHub Actions などの CI/CD でも同様に、適切な IAM Role を渡せば SSM セッションを開始し、SSH 経由でデプロイやコマンド実行が可能です。

まとめ

今回踏み台サーバーを構築し、Session Manager を通した SSH 接続を行うことで、他社クラウドのマシンにおいても SSH ポートの開放範囲を絞ることができ、また運用上の手間も減らすことができました。似たような環境の方のお役に立てばと思います。

人工知能学会(JSAI2025)に参加しました

はじめに

こんにちは。デリッシュキッチンでデータサイエンティストをしている古濵です。

2025年5月27日〜30日に開催された第39回人工知能学会全国大会(JSAI2025)に、プラチナスポンサーとして協賛いたしました。 今年は史上最多の参加者数を更新したようで、学会としての盛り上がりを肌で感じることができました。

tech.every.tv

エブリーとしても人工知能学会への参加は今回が初めてでした。 本記事では、スポンサーブースでのエブリーのAIプロダクト開発に関する紹介や、聴講した講演等についてまとめていきたいと思います。

スポンサーブース

弊社のスポンサーブースでは「AIエージェントによるレシピ提案の開発と応用事例」というタイトルで、AIプロダクトであるデリッシュAIについて事例紹介を行いました。 多くの学生、大学教員、企業の方々と交流することができ、デリッシュキッチンがどのようなサービスなのかという基本的な部分から、デリッシュAIのプロダクトの詳細や今後の展望まで、幅広くお話しすることができました。

ブースでは、実際にデモ機を用いてデリッシュAIを体験していただきました。 中には、デリッシュキッチンを普段からご利用いただいている方や、現役の管理栄養士の方ともお話しする機会がありました。 デリッシュキッチンに「こういう機能があると良い」といった具体的なご要望も含めてお聞かせいただき、非常に有意義な時間を過ごすことができました。 お立ち寄りいただいた皆様、ありがとうございました。

聴講

人工知能学会では多くの講演やセッションが開催されており、個人的に聴講して特に印象に残ったものをご紹介したいと思います。

自ら話しかけるチャットボット実現のための話題選択RAGの提案

JSAI2025/Proposal of a Topic Selection RAG for Developing Proactive Chatbots

ユーザーがチャットボットとの会話を継続する意欲を維持することの課題に焦点を当てた研究です。 既存の受動的なチャットボットとは異なり、ユーザーの興味や知識に基づいて能動的に新しい話題を提供できるチャットボットを提案しています。

ユーザーの興味を刺激する未知の情報を効率的に取得するために、話題をテーマごとにデータ化し、話題選択が可能なRAGシステムを構築している点が非常に興味深く感じました。 デリッシュAIでも、ユーザーのクエリに対してレシピを提案するだけでなく、今日何を作るか決まっていないユーザーとコミュニケーションを取りながらレシピ提案を行えるようになれば良いなと考えており、この研究のアプローチは大変参考になりました。

大規模視覚言語モデルチューニングを用いた非構造ドキュメント画像向け情報抽出

JSAI2025/Information Extraction from Unstructured Document Images with Tuned Large-Scale Vision-Language Model

非構造化文書画像に対する情報抽出手法について、複数のアプローチを比較検討した研究です。 OCR+LLMによる手法では、レイアウト要素が欠落してしまうため、視覚情報も考慮できるLVLM(Large Vision Language Model)を用いた比較検討を行っています。

具体的には、「LVLMによる手法におけるFew-shotとFine-tuningの比較」と「OCRの活用有無による比較」を実施していました。 結果として、LVLMをFine-tuningした手法が最も優れており、OCRを使用しない方が良い結果となったことは興味深い知見でした。 LVLMについては、別の講演で「大規模視覚言語モデルの開発」のチュートリアルも開催されており、画像とテキストの情報を統合的に処理できるLVLMの技術的発展を実感することができました。

LLMエージェントによるエルファロル・バー問題の解析

JSAI2025/Analysis of the El Farol Bar Problem by LLM Agents

エルファロル・バー問題は、バーに行きたがっている複数の人が同時に意思決定を行う状況を想定した、ゲーム理論における問題です。 この発表では、各エージェントがLLMによって発話・記憶・行動を生成することで、エルファロル・バー問題をシミュレーションし、その結果を解析した研究について紹介されていました。

ゲーム理論の問題をLLMでシミュレーションするというアプローチが研究として非常に興味深く感じました。 シミュレーション結果では、エージェントがバーに入る前に何人かで集合したり、発話内にハッシュタグが出現してエージェントのグループ内で情報が拡散されたりするなど、個人的には予想外な行動パターンが観察され驚きました。 集団インタラクションを通じて個性を分化させるという、ある種の社会的な振る舞いが、エージェントによって実現可能であることを示唆する結果は、大変興味深いものでした。

おわりに

今回、人工知能学会(JSAI2025)にプラチナスポンサーとして参加し、エブリーのAIプロダクトであるデリッシュAIについて多くの方々にご紹介することができました。 学生から研究者、企業の方々まで幅広い層の皆様と交流でき、デリッシュキッチンやデリッシュAIに対する貴重なフィードバックをいただくことができました。

個人的には、学生時代がコロナ禍だったこともあり、学会の雰囲気と盛り上がりを直接体感することができて非常に良い経験となりました。 聴講した研究発表から得られた知見を、今後のプロダクト開発にどのように応用できるか模索していきたいと思います。

最後に、学会でお話しいただいた皆様、運営スタッフの皆様に心より感謝申し上げます。 来年の人工知能学会にも、さらに発展したプロダクトとともに参加できることを楽しみにしています。

Cursor v1.0 で追加された Memories を試してみる

Cursor v1.0 で追加された Memories を試してみる

はじめに

こんにちは、TIMELINE 開発部 Service Development をしているhondです!
タイトルにある通り、Cursorの0.51から待望のMemoriesがBETA機能としてリリースされました!
リリースされたばかりということでChangeLogにも詳細はないので、自分なりに使ってみた感想や調査結果を共有できればと思います! このblog執筆時点の2025/06/02時点では、ChangeLogにv0.51やGenerate Memoriesに関しての記述はありませんでしたがv1.0リリースにてChangeLogにv1.0の内容とともにMemoriesの内容が追加されました。

注意

2025/06/02現在、BETA機能としてリリースされたMemoriesですが、privacy modeが無効でないと使うことができません。
privacy modeを無効にした場合、どのような情報がCursorに共有され、自分の環境で使う際には問題はないのか確認の上利用することをお勧めします。

Memories とは

以前のCursor Custom modes を利用した cursor-memory-bank のワークフローを試してみるでも取り上げましたが、メモリとはセッション間での命令内容やプロジェクトの知識を構造化したドキュメントとして保存する方法です。これによって新しいセッションを開始した時や、コンテキストウィンドウが肥大化した際にも、プロジェクトの詳細や技術スタック、アーキテクチャ、現在取り組んでいる内容やそれに関する進捗を繰り返し説明する必要をなくす一つの手段です。
ClineではBlog公式ドキュメントで、Memory Bankを導入する方法が紹介されていましたが、Cursorではなかったので、これらの方法を参考に運用していた人も多いのではないでしょうか。ちなみに、CursorのforumでもMemory Bank導入に関してはFeature Requestとして投稿されて、多くのユーザーが関心を寄せていました。

Generate Memoriesの設定

0.51で追加されたGenerate Memories機能はCursor Settings > Rulesから確認することができます。

Generate Memoriesの設定
冒頭でも説明した通り、privacy modeが無効でないと機能を有効化することはできません。
設定は以上になります!

実際に使ってみた

以降、実際に行った内容はこちらのRepositoryにアップロードしてあります。

検証環境

  • Cursor Version : 0.51.1
  • Model : claude-4-sonnet
  • User Rules : なし
  • Project Rules : なし
  • Repositoryの状態 : gitの設定のみ
  • Chat Mode : Agent
  • 目的 : Goを用いた時刻を返すMCP Serverを作成させる

実際のやりとり

下記はMCP Serverの作成とMemoryを作成するにあたり、実際に行ったやりとりです。

User

golang を用いて現在時刻や引数として渡された時間分経過した時の時刻を取得できる MCP server を作成して

Cursor

Golang を使用して時刻を取得できる MCP server を作成いたします。まず、プロジェクトの構造を確認してから実装を進めます。 Ran tool プロジェクトが初期状態のようですね。Golang MCP server を作成していきます。まず、必要なファイル構造を作成します。 MCP Go SDK が存在しないようですね。標準的な MCP プロトコルを実装するために、JSONRPC ベースの実装を作成します。 次に、README.md ファイルを作成して使用方法を説明します。 最後に、テスト用のサンプルスクリプトを作成します。 テストクライアントのファイル名を変更して、型エラーを修正します。 Ran tool 最後に、実際にサーバーが動作するかテストしてみましょう。

go mod tidy
go build -o time-mcp main.go
cd test && go run client.go

素晴らしい!MCP Time Server が正常に動作しています。最後にプロジェクトの構造を確認しましょう。

cd .. && find . -type f -name "*.go" -o -name "*.md" -o -name "go.mod" | sort

完成!

Golang を使用した時刻取得機能を持つ MCP server を正常に作成しました。以下が実装された機能です:

🎯 主な機能

  1. 現在時刻取得 (get_current_time)

    • 現在の時刻を様々なフォーマットで取得
    • デフォルトは RFC3339 形式
  2. 時間加算 (add_minutes_to_current_time)

    • 現在時刻に指定された分数を加算(負の値で過去の時刻も取得可能)
    • 様々なフォーマットに対応

📁 プロジェクト構成

  • main.go - MCP Time Server のメイン実装
  • go.mod - Go module 定義
  • README.md - 詳細な使用方法とドキュメント
  • test/client.go - 動作確認用のテストクライアント
  • time-mcp - ビルドされた実行可能ファイル

✨ 特徴

  • 標準ライブラリのみ使用 - 外部依存関係なし
  • 豊富な時刻フォーマット対応 - RFC3339、Kitchen、JST 等 20 種類以上
  • MCP 準拠 - Model Context Protocol 2024-11-05 に準拠
  • JSON-RPC 2.0 - 標準的なプロトコル実装
  • エラーハンドリング - 適切なエラーレスポンス
  • 日本語対応 - 日本語でのメッセージ表示

🚀 使用方法

# ビルド
go build -o time-mcp main.go

# 実行
./time-mcp

# テスト
cd test && go run client.go

テスト結果からも分かるように、サーバーは正常に動作し、現在時刻の取得や時間の加算機能が期待通りに動作しています。MCP プロトコルに準拠しているため、MCP 対応のクライアントから利用することができます。

User

ここまでの実装でメモリに保存する必要のある内容はある?

Cursor

はい、このプロジェクトの実装について、メモリに保存すべき重要な内容がいくつかあります。特に、MCP プロトコルの実装パターンや技術的な決定事項は今後の参考になります。 Ran tool Ran tool メモリに保存した内容は以下の通りです:

保存した知識

1. MCP Server 実装パターン(Golang)

  • 標準ライブラリのみでの MCP サーバー実装方法
  • 必須の MCP メソッド(initialize、tools/list、tools/call)
  • JSON-RPC 2.0 準拠のレスポンス構造
  • 適切なエラーハンドリング
  • Go の時刻フォーマット活用法

2. MCP ツール定義のベストプラクティス

  • JSON Schema を使った InputSchema の適切な定義
  • 日本語対応のアプローチ
  • 引数の柔軟性とデフォルト値設定
  • 型安全な引数解析の実装
  • E2E テストの戦略

これらの知識は、今後 MCP サーバーを実装する際や、既存のサーバーを拡張する際に非常に有用です。特に、標準ライブラリのみでの実装パターンや、適切なエラーハンドリング、テスト戦略などは再利用可能な重要な知見です。

生成されたMemory

下記は実際にGenerate Memories機能によって生成されたUser Ruleになります。

Golang で MCP(Model Context Protocol)サーバーを実装する際の重要なパターン:

1. **標準ライブラリのみでの実装**: 外部依存関係なしで JSON-RPC 2.0 ベースの MCP サーバーを実装可能。bufio.Scanner + os.Stdin/Stdout で標準入出力通信を実現。

2. **必須の MCP メソッド**: initialize(プロトコルバージョン 2024-11-05、capabilities、serverInfo)、tools/list(利用可能ツール一覧)、tools/call(ツール実行)の 3 つが基本。

3. **レスポンス構造**: MCPResponse は必ず jsonrpc:"2.0"、id、result/error フィールドを含む。ツール実行結果は content 配列内に type:"text"と text フィールドで返す。

4. **エラーハンドリング**: JSON-RPC 標準エラーコード(-32601: Method not found, -32602: Invalid params)を使用。

5. **時刻フォーマット対応**: Go の time 定数(time.RFC3339、time.Kitchen 等)を活用し、カスタムフォーマット(JST 等)も追加可能。

考察

一回目のプロンプトでMCP Server自体の実装は完了していたので、そのタイミングでMemoryが自動で作成されると思ったのですが、作成されなかったので「ここまでの実装でメモリに保存する必要のある内容はある?」のように明示的にメモリに保存するように指示することでMemoryが作成されました。
実際のやりとりではわかりづらいですが、「はい、このプロジェクトの実装について、メモリに保存すべき重要な内容がいくつかあります。特に、MCP プロトコルの実装パターンや技術的な決定事項は今後の参考になります。」の後の「Ran tool」でUser Rules updateが実行されて、User Rulesに書き込まれています。
ClineのMemory Bankの設定では大事だと思った部分をMemory Bankに保存するルールがあったため、一定明確な指示をしなくても保存してくれていたので、もしGenerate Memoriesでは明確な指示が必要な場合は少し使い勝手が悪そうだなと感じました。
また、設定の際に気づいた人もいるかと思いますが、今回のGenerate Memoriesで作成されるのはあくまでUser Rulesになります。そのため、今までプロジェクトレベルでカスタマイズしたルールを共有するなどのアプローチをとっていて、Generate Memoriesの結果を共有する場合は、一度自分で設定のUser Rulesを開いて、そこから今回取得された設定を別途ファイルに書き出す必要があります。

まとめ

まだBETA版ではありますが、Memory Bank機能は個人的にも期待していて、ずっとforumを追っていたので、公式で導入されたのは嬉しい限りです!
Memory Bankを実装したMCP Serverを使うアプローチもありますが、それを用いない場合、必要なプロジェクトで都度Memory Bankに関するルールを記述するかグローバルに設定する必要があったので、その設定がなくなるのは一つCursorの強みになるのかなと感じました。
また、Generate Memoriesが導入されたためか、User Rulesも複数設定できるようになり、管理が容易になった点も今回のアップデートの良い点だと思います。ルール設定などまだベストプラクティスが見つけられていないので、Generate Memoriesで作成されたものを採用しつつ、より開発効率を上げていきたいと思います。

トモニテで発生した SQL インジェクション攻撃の記録と教訓

はじめに

こんにちは、トモニテで開発を担当している吉田です。 サービスを運営する上で、セキュリティ対策は欠かせません。

本記事では、実際にトモニテが受けた攻撃の事例をもとに、 異常検知から調査の経緯、攻撃の詳細、そして発見された問題点や今後の対応についてまとめています。

セキュリティリスク

現代の Web サービスにおいて、セキュリティリスクは多岐にわたります。代表的なものとしては、クロスサイトスクリプティング(XSS)クロスサイトリクエストフォージェリ(CSRF)ブルートフォース攻撃などが挙げられます。これらのリスクは、サービスの信頼性やユーザーの安全を脅かす重大な問題となり得ます。 中でも、SQL インジェクションは古くから存在する攻撃手法の 1 つで、昨年にはある企業の会員制サイト内の過去に使用していたページのセキュリティ設定に不備があり、同サイトのデータベースから顧客情報などが漏洩したといった事案も発生しました。(参考

このように、SQL インジェクションは今なお多くのサービスで脅威となっています。ここからは、実際にトモニテで発生した SQL インジェクション攻撃の記録と、そこから得られた教訓について紹介します。

異常検知と調査の経緯

今回、調査のきっかけとなったのは、Web サービスのレスポンスタイムが長くなっているというアラート通知と、ほぼ同時にデータベースの CPU 使用率が高くなっているというアラート通知があったことでした。 実際にログを確認したところ、多くのリクエストに対しては HTTP ステータス 400 のエラーが返されていました。

しかし、「なぜ多くのリクエストが 400 エラーで弾かれているにもかかわらず、データベースの CPU 使用率が上がっているのか?」という点に疑問を持ちました。 このことから、「もしかしたら一部の攻撃的なリクエストがアプリケーションのバリデーションをすり抜けてデータベースまで到達しているのではないか」と考え、さらに詳細な調査を進めました。

その結果、一部のクエリパラメータに対してバリデーションが不十分であったことが判明し、これが攻撃リクエストに対して HTTP ステータス 200 を返してしまう原因となっていたことが分かりました。 では、実際にどのような攻撃が行われていたのか、具体的な内容を紹介します。

受けた攻撃の詳細

詳細な調査の結果、実際に受けた攻撃はクエリパラメータに SQL 文を埋め込むことで、データベースに対して不正な命令を実行させようとするものでした。 この攻撃の特徴は、パラメータに SQL インジェクションのペイロードを仕込むことで、データベースの動作を意図的に遅延させたり、条件分岐を利用して情報を引き出そうとする点です。 特に、DBMS_PIPE.RECEIVE_MESSAGE や sleep 関数など、タイムベースのブラインド SQL インジェクションが試みられていました。

実際にログに残っていたリクエスト例をいくつか挙げます。

1. tag_id にランダムな値を指定したリクエスト

/path?tag_id=dn8r1je0elkm.html&page=1&per_page=20

推測される意図: 一見普通のリクエストに見えますが、tag_id にランダムな値(dn8r1je0elkm.html)が入っています。 これは「存在しない ID」や「想定外の値」を入れることで、アプリケーションのエラーハンドリングやレスポンスの違いを観察し、脆弱性の有無や内部構造を探ろうとしている可能性があります。

2. page パラメータに OR 条件を含めたリクエスト

/path&page=-1 OR 2+138-138-1=0+0+0+1&per_page=20

推測される意図: page パラメータに「-1 OR 2+138-138-1=0+0+0+1」という SQL 文が埋め込まれています。 これは SQL インジェクションの典型的なテストで、「OR」条件を使って本来の条件式を無効化し、任意の条件でデータを取得できるかを試しています。 この場合、2+138-138-1=0+0+0+1 は常に真(1=1)になるため、全件取得や認証回避などが狙いと考えられます。 もしアプリケーションがこの注入を受け入れてクエリを実行した場合、通常はデータが返されないはずの状況(例: 存在しない page -1)でも、1=1 が真になることでデータが返されるといった挙動の変化が起こる可能性があります。 このリクエストは Web アプリケーションが SQL インジェクションに対して脆弱であるかどうかを探る、初期段階の偵察攻撃と考えられます。

3. Oracle の DBMS_PIPE.RECEIVE_MESSAGE を使ったリクエスト

/path?page=1'||DBMS_PIPE.RECEIVE_MESSAGE(CHR(98)||CHR(98)||CHR(98),15)||'&per_page=20

推測される意図: page パラメータに「'||DBMS_PIPE.RECEIVE_MESSAGE(CHR(98)||CHR(98)||CHR(98),15)||'」という Oracle 特有の関数が使われています。 これはDBMS_PIPE.RECEIVE_MESSAGEを使って、SQL の実行を 15 秒間遅延させる(タイムベースのブラインド SQL インジェクション)攻撃です。 レスポンスの遅延時間によって、SQL インジェクションが成立しているかどうかを判別しようとしています。

4. MySQL の sleep 関数を使ったリクエスト

/path?page=(select(0)from(select(sleep(15)))v)/'+(select(0)from(select(sleep(15)))v)+'"+(select(0)from(select(sleep(15)))v)+"/&per_page=20

推測される意図: page パラメータに「(select(0)from(select(sleep(15)))v)」など、MySQL 系の sleep 関数を使った SQL インジェクションが仕込まれています。スリープが正常に(中断なしで)復帰すると、0 が返されます。 これもタイムベースのブラインド SQL インジェクションで、SQL が実行されると 15 秒間レスポンスが遅れるため、脆弱性の有無を確認できます。

いずれも「SQL インジェクションが成立するかどうか」を探るためのテストや、タイムベースの手法で脆弱性の有無を確認しようとする攻撃です。 特に 3・4 番目は、データベースの種類(Oracle か MySQL か)によって使い分けている点も特徴的です。

これらのリクエストは、アプリケーションが入力値を適切に検証・サニタイズせずに SQL クエリを組み立てている場合、データベースに対して不正な操作を行うことができてしまう危険性を示しています。

攻撃の影響とシステムの挙動

調査の結果、基本的には不正なパラメータに対しては HTTP ステータス 400 を返していました。 しかし一部の攻撃リクエストに対して HTTP ステータス 200 が返っていたことがわかりました。 幸い、クエリのビルド時に正規のパラメータのみを受け付けていたため SQL インジェクション自体は成立していなかったことが判明しています。

ただし、攻撃者が仕込んだインジェクションペイロードがそのままアプリケーションに渡っていたことは事実です。

仮に SQL インジェクションが成立していた場合、データベース内の情報漏洩や、サービスの停止、データの改ざんなど重大な被害につながる恐れがありました。 特に、タイムベースのブラインド SQL インジェクションが成立していれば、攻撃者は時間をかけてデータベースの中身を窃取することも可能です。

発見された問題点と今後の対応

今回の調査で、一部のリクエストパラメータに対してバリデーションが適切に行われていなかったことが判明しました。 特に、攻撃リクエストの中には本来不正な値であるにもかかわらず、HTTP ステータス 200 を返してしまっていたものがありました。 これは、パラメータの値に対する十分な検証が行われていなかったことが原因です。 本来、許可された文字列のみを受け付けるべきですが、現状は任意の文字列がそのまま渡ってしまう状態でした。 この問題に対し、該当するパラメータに対して正規の値のみ受け付けるようホワイトリスト(許可リスト)によるバリデーションを追加し、不正な値が渡らないように修正を行っています。

まとめ

今回の事例を通じて、たとえ SQL インジェクションが実際に成立していなかったとしても、入力値のバリデーション不足は大きなリスクとなることを改めて認識しました。 また、調査の中で一部のパラメータに対するバリデーションが不十分だったことが、攻撃リクエストに対して HTTP ステータス 200 を返してしまう原因となっていたことも判明しました。 今後は、入力値に対して適切なバリデーションを徹底し、同様のリスクが他のパラメータにも潜んでいないか定期的に見直しを行っていきます。 セキュリティ対策は一度実施すれば終わりではなく、継続的な改善と運用が不可欠です。今後も気を緩めることなく、サービスの安全性向上に取り組んでいきます。