くらげになりたい。

くらげのようにふわふわ生きたい日曜プログラマなブログ。趣味の備忘録です。

VSCodeのDevContainer入門&Claude Codeのリファレンス実装を見てみる

この記事を読んで、Cline / RooCodeなどを使うなら、
ちゃんと使えるようにしたいなと思い、いろいろ調べてみたときの備忘録(*´ω`*)

環境は、macOSOrbStack

DevContainerとは

Dev Containerは、Dockerコンテナ上で開発するための拡張機能
環境構築をコンテナとして用意でき、ローカル環境も汚さないですむ

また、開いたフォルダをコンテナ上にマウントして、コンテナ内で実行するので、
それ以外のファイルにアクセスできなくすることができるので、AIエージェントを使うときにも安全

コンテナへの接続方法はいくつかあり、それに応じて、必要な拡張機能をインストールする

DevContainerを試してみる

必要なファイルとしては、こんな感じ。

.devcontainer/
├── devcontainer.json ... 必須: 設定ファイル
└── Dockerfile        ... 任意: 自分でイメージを作るなら

左下の「><」のアイコンから、Dev Containerのセットアップや開き直すことができる
初回だと、用意されているテンプレートを選択できる感じ

start dev container

テンプレートだと、こんなのを作成してくれる

// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/debian
{
 "name": "Debian",
 // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
 "image": "mcr.microsoft.com/devcontainers/base:bullseye",
 "features": {
  "ghcr.io/devcontainers/features/github-cli:1": {}
 }

 // Features to add to the dev container. More info: https://containers.dev/features.
 // "features": {},

 // Use 'forwardPorts' to make a list of ports inside the container available locally.
 // "forwardPorts": [],

 // Configure tool-specific properties.
 // "customizations": {},

 // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
 // "remoteUser": "root"
}

用意されているイメージ

microsoft自体が、dev container用のdocker imageを用意してくれているので、
これで十分であれば、devcontainer.jsonだけでOK

github-cliのようなfeaturesという仕組みがあって、
必要なのを指定すれば、自前でDockerfileを用意しなくても、ある程度カスタマイズができる

devcontainer.jsonの中身

リファレンスはこのあたり。ガイドをみると、実際の設定方法が書かれている

よく使うものだと、こんな感じ

{
  // コンテナの名前
  "name": "Debian",
  // コンテナのイメージ
  "image": "mcr.microsoft.com/devcontainers/base:bullseye",
  // コンテナのイメージに追加する機能
  "features": {
    "ghcr.io/devcontainers/features/github-cli:1": {}
  },
  "customizations": {
    "vscode": {
      "extensions": [
        // コンテナ上の各条機能
      ],
      "settings": {
        // コンテナ上のVSCodeの設定
      }
    }
  },
  // コンテナ上のユーザ名
  "remoteUser": "node",
  "mounts": [
    // コンテナにマウントするファイルの設定
  ],
  "remoteEnv": {
    // コンテナ上の環境変数
  },
  // コンテナ上のワークスペースのフォルダ
  "workspaceFolder": "/workspace",
  // コンテナ作成時に実行するコマンド
  "postCreateCommand": "..."
}

ローカルとは別環境なので、拡張機能や設定は、ここに追加しないといけない
Dev Containerは実行だけで、コーディングはローカルのVSCodeでするとかであれば不要

Tips

コンテナ上でGitにアクセスしたい

Dev Contaier内は、別環境なので、ローカルのSSH秘密鍵にアクセスできない
そのため、pushとかもコンテナからはできない。。。

クレデンシャルを共有する方法も、このガイドに書かれている感じ

ssh-agentは、秘密鍵を登録しておくと、コンテナなどリモート上で必要になった際に、
登録した秘密鍵をリモート側から利用できるようにしてくれるエージェントサービスらしい

ssh-agentでの設定

ホスト側(ローカル側)で設定などが必要

# ssh-agentの起動。macの場合はeval不要
$ ssh-agent

# エージェント経由で提供する秘密鍵の登録
$ ssh-add --apple-use-keychain ~/.ssh/id_rsa
# or 
$ ssh-add --apple-use-keychain

# 登録状況の確認
$ ssh-add -L

shell起動ごとに実行する必要があるので、
.bashrcなどに以下を追加して、起動するようにしておく

# .bashrc
ssh-agent > /dev/null
ssh-add --apple-use-keychain 2> /dev/null

エージェントに追加する秘密鍵は、
~/.ssh/configAddKeysToAgentをつけて指定できる

# ~/.ssh/config
Host *
  AddKeysToAgent yes
  UseKeychain yes
  IdentityFile ~/.ssh/id_rsa

最後に、コンテナを起動して、
コンテナ上から接続できればOK

ssh -T git@github.com

これで、ローカルのsshの設置をDevContainer上から利用することができる

Claude Code提供のサンプル構成

Claude Codeでもサンプル構成が公開されていて、
AIエージェント向けのDevContainerとして参考になる

Node20ベースで、zshなどが用意されているDockerfile
iptablesを使った独自のファイアーウォール設定もあるので、
指定したURL以外はアクセスできないようになっている

これをベースにいろいろ設定していくと、ある程度安全に利用できそう
ただ、絶対に安全ではないので、注意しつつカスタマイズしていくとよさそう

作ってみたDevContainer

基本は、Claude Codeをベースにしつつ、
pnpmを利用できるようにしてみた

Dockerfile

# .devcontainer/Dockerfile
# ref: https://github.com/anthropics/claude-code/blob/main/.devcontainer/Dockerfile
FROM node:23.5.0

ARG TZ
ENV TZ="$TZ"

# Install basic development tools and iptables/ipset
RUN apt update && apt install -y less \
  git \
  procps \
  sudo \
  fzf \
  zsh \
  man-db \
  unzip \
  gnupg2 \
  gh \
  iptables \
  ipset \
  iproute2 \
  dnsutils \
  aggregate \
  jq

# Ensure default node user has access to /usr/local/share
RUN mkdir -p /usr/local/share/npm-global && \
  chown -R node:node /usr/local/share

ARG USERNAME=node

# Persist bash history.
RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
  && mkdir /commandhistory \
  && touch /commandhistory/.bash_history \
  && chown -R $USERNAME /commandhistory

# Set `DEVCONTAINER` environment variable to help with orientation
ENV DEVCONTAINER=true

# Create workspace and config directories and set permissions
RUN mkdir -p /workspace /home/node/.claude && \
  chown -R node:node /workspace /home/node/.claude

WORKDIR /workspace

RUN ARCH=$(dpkg --print-architecture) && \
  wget "https://github.com/dandavison/delta/releases/download/0.18.2/git-delta_0.18.2_${ARCH}.deb" && \
  sudo dpkg -i "git-delta_0.18.2_${ARCH}.deb" && \
  rm "git-delta_0.18.2_${ARCH}.deb"

# Set up non-root user
USER node

# Install global packages
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
ENV PATH=$PATH:/usr/local/share/npm-global/bin

# Set the default shell to bash rather than sh
ENV SHELL /bin/zsh

# Default powerline10k theme
RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v1.2.0/zsh-in-docker.sh)" -- \
  -p git \
  -p fzf \
  -a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \
  -a "source /usr/share/doc/fzf/examples/completion.zsh" \
  -a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
  -x

# Install Claude and pnpm
RUN npm install -g @anthropic-ai/claude-code pnpm

# Copy and set up firewall script
COPY init-firewall.sh /usr/local/bin/
USER root
RUN chmod +x /usr/local/bin/init-firewall.sh && \
  echo "node ALL=(root) NOPASSWD: /usr/local/bin/init-firewall.sh" > /etc/sudoers.d/node-firewall && \
  chmod 0440 /etc/sudoers.d/node-firewall
USER node

devcontainer.json

devcontainer.jsonは、extensionsを好きなのを追加
roo-clineを追加したり、などなど

// .devcontainer/devcontainer.json
{
  "name": "my-devcontainer",
  "build": {
    "dockerfile": "Dockerfile",
    "args": {
      "TZ": "${localEnv:TZ:Asia/Tokyo}"
    }
  },
  "runArgs": [
    "--cap-add=NET_ADMIN",
    "--cap-add=NET_RAW"
  ],
  // Configure tool-specific properties.
  "customizations": {
    "vscode": {
      "extensions": [
        "dbaeumer.vscode-eslint",
        "connor4312.esbuild-problem-matchers",
        "ms-vscode.extension-test-runner",
        "rooveterinaryinc.roo-cline",
        "DavidAnson.vscode-markdownlint",
        "corschenzi.mermaid-graphical-editor"
      ],
      "settings": {
        "terminal.integrated.defaultProfile.linux": "zsh",
        "terminal.integrated.profiles.linux": {
          "bash": {
            "path": "bash",
            "icon": "terminal-bash"
          },
          "zsh": {
            "path": "zsh"
          }
        }
      }
    }
  },
  "remoteUser": "node",
  "mounts": [
    "source=claude-code-bashhistory,target=/commandhistory,type=volume",
    "source=claude-code-config,target=/home/node/.claude,type=volume"
  ],
  "remoteEnv": {
    "CLAUDE_CONFIG_DIR": "/home/node/.claude",
    "POWERLEVEL9K_DISABLE_GITSTATUS": "true"
  },
  "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
  "workspaceFolder": "/workspace",
  "postCreateCommand": "sudo /usr/local/bin/init-firewall.sh"
}

init-firewall.sh

ファイアーウォールの設定は、そのまま
必要に応じて、許可するドメインを調整する感じ

#!/bin/bash
# devcontainer/init-firewall.sh
set -euo pipefail  # Exit on error, undefined vars, and pipeline failures
IFS=$'\n\t'        # Stricter word splitting

# Flush existing rules and delete existing ipsets
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X
ipset destroy allowed-domains 2>/dev/null || true

# First allow DNS and localhost before any restrictions
# Allow outbound DNS
iptables -A OUTPUT -p udp --dport 53 -j ACCEPT
# Allow inbound DNS responses
iptables -A INPUT -p udp --sport 53 -j ACCEPT
# Allow outbound SSH
iptables -A OUTPUT -p tcp --dport 22 -j ACCEPT
# Allow inbound SSH responses
iptables -A INPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
# Allow localhost
iptables -A INPUT -i lo -j ACCEPT
iptables -A OUTPUT -o lo -j ACCEPT

# Create ipset with CIDR support
ipset create allowed-domains hash:net

# Fetch GitHub meta information and aggregate + add their IP ranges
echo "Fetching GitHub IP ranges..."
gh_ranges=$(curl -s https://api.github.com/meta)
if [ -z "$gh_ranges" ]; then
    echo "ERROR: Failed to fetch GitHub IP ranges"
    exit 1
fi

if ! echo "$gh_ranges" | jq -e '.web and .api and .git' >/dev/null; then
    echo "ERROR: GitHub API response missing required fields"
    exit 1
fi

echo "Processing GitHub IPs..."
while read -r cidr; do
    if [[ ! "$cidr" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/[0-9]{1,2}$ ]]; then
        echo "ERROR: Invalid CIDR range from GitHub meta: $cidr"
        exit 1
    fi
    echo "Adding GitHub range $cidr"
    ipset add allowed-domains "$cidr"
done < <(echo "$gh_ranges" | jq -r '(.web + .api + .git)[]' | aggregate -q)

# Resolve and add other allowed domains
for domain in \
    "registry.npmjs.org" \
    "api.anthropic.com" \
    "sentry.io" \
    "statsig.anthropic.com" \
    "statsig.com"; do
    echo "Resolving $domain..."
    ips=$(dig +short A "$domain")
    if [ -z "$ips" ]; then
        echo "ERROR: Failed to resolve $domain"
        exit 1
    fi
    
    while read -r ip; do
        if [[ ! "$ip" =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then
            echo "ERROR: Invalid IP from DNS for $domain: $ip"
            exit 1
        fi
        echo "Adding $ip for $domain"
        ipset add allowed-domains "$ip"
    done < <(echo "$ips")
done

# Get host IP from default route
HOST_IP=$(ip route | grep default | cut -d" " -f3)
if [ -z "$HOST_IP" ]; then
    echo "ERROR: Failed to detect host IP"
    exit 1
fi

HOST_NETWORK=$(echo "$HOST_IP" | sed "s/\.[0-9]*$/.0\/24/")
echo "Host network detected as: $HOST_NETWORK"

# Set up remaining iptables rules
iptables -A INPUT -s "$HOST_NETWORK" -j ACCEPT
iptables -A OUTPUT -d "$HOST_NETWORK" -j ACCEPT

# Set default policies to DROP first
# Set default policies to DROP first
iptables -P INPUT DROP
iptables -P FORWARD DROP
iptables -P OUTPUT DROP

# First allow established connections for already approved traffic
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
iptables -A OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

# Then allow only specific outbound traffic to allowed domains
iptables -A OUTPUT -m set --match-set allowed-domains dst -j ACCEPT

echo "Firewall configuration complete"
echo "Verifying firewall rules..."
if curl --connect-timeout 5 https://example.com >/dev/null 2>&1; then
    echo "ERROR: Firewall verification failed - was able to reach https://example.com"
    exit 1
else
    echo "Firewall verification passed - unable to reach https://example.com as expected"
fi

# Verify GitHub API access
if ! curl --connect-timeout 5 https://api.github.com/zen >/dev/null 2>&1; then
    echo "ERROR: Firewall verification failed - unable to reach https://api.github.com"
    exit 1
else
    echo "Firewall verification passed - able to reach https://api.github.com as expected"
fi

以上!! これで安全にAIエージェントと使えるように(*´ω`*)

参考にしたサイト様