はじめに
今年、GMOメディア株式会社に入社した新卒1年目のエンジニアの水崎です。
入社してまだ数ヶ月ですが、Anthropic社のClaude Codeというツールにコントリビュートしました。
会社の新卒研修で学んだDockerを含めたコンテナとネットワークの知識が直接活きた瞬間でした。
今回は、新卒でもOSSに貢献できるという実体験を共有します。
Claude Codeとは?
まず、Claude Codeについて簡単に説明します。
Claude Code / Github
Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows — all through natural language commands. Use it in your terminal, IDE, or tag @claude on Github.
とあるようにターミナルから操作できるAIエージェントコーディングツールです。
ClaudeCode公式のgif(サイズ圧縮しています)
Claudeがコードを生成し、ファイルの作成・編集を自動で行ってくれます。プロジェクト全体のコンテキストを理解して作業してくれるのが特徴です。
問題の発見 – Dockerコンテナ間で通信ができない
起こったこと
全社でClaude Codeの導入が始まったが、Claude Codeに実際のコードやファイルを触らせるため、なるべくセキュアな環境を用意したいとなりました。
そこで、公式ドキュメントにあるように、コンテナの例が示されていたので参考にし開発環境を作ることにしました。
init-firewall.shをコンテナに対して実行して、ネットワークのセキュリティもしっかりさせました。
しかし、バックエンドを立ち上げるとさっきまで出来ていたDBに接続できなくなりました。
原因調査
新卒研修で学んだネットワーク知識を思い出しながら調査しました。
init-firewall.shでネットワークに対するセキュリティを強化した後だったので、問題はネットワークっぽいと判断しました。
$ nslookup google.com
;; connection timed out; no servers could be reached
$ nslookup google.com 8.8.8.8
Name: google.com
Address: 142.251.222.14
-> 繋がる
$ cat /etc/resolv.conf
nameserver 127.0.0.11
- DockerはコンテナにデフォルトでDNSサーバー(127.0.0.11)を提供
- NATルールが壊れると、DNS解決ができなくなる
原因の特定 – firewall.shの落とし穴
なぜこれが問題なのか
Dockerは以下のようなNATルールでDNSを実現しています:
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
:DOCKER_OUTPUT - [0:0]
:DOCKER_POSTROUTING - [0:0]
-A OUTPUT -d 127.0.0.11/32 -j DOCKER_OUTPUT
-A POSTROUTING -d 127.0.0.11/32 -j DOCKER_POSTROUTING
-A DOCKER_OUTPUT -d 127.0.0.11/32 -p tcp -m tcp --dport 53 -j DNAT --to-destination 127.0.0.11:34877
-A DOCKER_OUTPUT -d 127.0.0.11/32 -p udp -m udp --dport 53 -j DNAT --to-destination 127.0.0.11:53633
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p tcp -m tcp --sport 34877 -j SNAT --to-source :53
-A DOCKER_POSTROUTING -s 127.0.0.11/32 -p udp -m udp --sport 53633 -j SNAT --to-source :53
iptables -t nat -F
を実行すると、これらのルールも削除されてしまい、コンテナ内からDNS解決ができなくなります。
解決策の実装
最終的な修正内容
DockerのDNS NATルールを保護する機能を追加しました:
DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11" || true)
iptables -t nat -F
...
if [ -n "$DOCKER_DNS_RULES" ]; then
echo "Restoring Docker DNS rules..."
iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true
iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true
echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat
else
echo "No Docker DNS rules to restore"
fi
PRがマージされるまで
1. 下調べ
PRを出すと言っても、むやみやたらに出してもmergeされないと思いました。新卒研修時に、技術にはその技術の思想があるという話を思い出して、ClaudeCodeのPJの思想に沿ったものでないとコントリビュートできないと思い、以下のことを実行しました。
- CONTRIBUTING.mdなどのcontributeに対しての詳細な説明・ルールがないか確認
- 過去のPRやissueを確認して、「自分がやろうとしている修正は、既に他の誰かが着手していないか?、却下されていないか」「その上でどんなコメントがあるか」などを確認
調べたところ、数ヶ月前にiptableに関しての議論があったPRも存在したのですが、途中で終わっており、PRタブのところに”もしissueを修正したらPR出してね”って書いていたのでせっかくなら提出してみることにしました。
2. 検証
世界中で使われているClaude Codeに対してPRを出すのが恐れ多く、検証をちゃんとしてから出そうと思い、以下の手順で検証しました。
-
テスト用のコンテナ環境を作成
docker-compose.yml
```yaml services: test-workspace: build: context: . dockerfile: Dockerfile privileged: true # Required for iptables volumes: - .:/workspace - /var/run/docker.sock:/var/run/docker.sock working_dir: /workspace command: sleep infinity networks: - test-network depends_on: - redis redis: image: redis:7-alpine networks: - test-network networks: test-network: driver: bridge ```
-
修正前と修正後で名前解決のテストを実行し、どのようにnatルールが変わるかで出力できるようなシェルスクリプトを作成
run-dns-tests.sh (修正前・修正後・レビュー案の3パターンでDNS解決の動作を比較します)
```bash #!/bin/bash set -euo pipefail # Runner script for DNS firewall tests SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Function to test original script in clean environment test_original_script() { echo "🧪 Testing ORIGINAL script in clean environment..." cd "$SCRIPT_DIR" # Start fresh environment docker compose up -d --build sleep 10 echo "📝 Running ORIGINAL firewall script..." docker compose exec -T --user root test-workspace bash -c " echo '=== ORIGINAL SCRIPT TEST ===' > /tmp/original-test.log echo 'Testing original init-firewall.sh' >> /tmp/original-test.log echo >> /tmp/original-test.log echo 'Before script execution:' >> /tmp/original-test.log echo 'Docker DNS test:' >> /tmp/original-test.log timeout 5 dig @127.0.0.11 +short google.com >> /tmp/original-test.log 2>&1 || echo 'Docker DNS failed' >> /tmp/original-test.log echo >> /tmp/original-test.log echo 'Executing original script...' >> /tmp/original-test.log timeout 60 bash /workspace/init-firewall.sh >> /tmp/original-test.log 2>&1 || echo 'Script execution failed' >> /tmp/original-test.log echo >> /tmp/original-test.log echo 'After script execution:' >> /tmp/original-test.log echo 'iptables policies:' >> /tmp/original-test.log iptables -L | grep policy >> /tmp/original-test.log 2>&1 echo 'Docker DNS test:' >> /tmp/original-test.log timeout 5 dig @127.0.0.11 +short google.com >> /tmp/original-test.log 2>&1 || echo 'Docker DNS failed' >> /tmp/original-test.log cat /tmp/original-test.log " # Copy results and cleanup docker compose exec -T test-workspace cat /tmp/original-test.log > original-test-results.log docker compose down echo "✅ ORIGINAL test completed!" } # Function to test modified script in clean environment test_modified_script() { echo "🧪 Testing MODIFIED script in clean environment..." cd "$SCRIPT_DIR" # Start fresh environment docker compose up -d --build sleep 10 echo "📝 Running MODIFIED firewall script..." docker compose exec -T --user root test-workspace bash -c " echo '=== MODIFIED SCRIPT TEST ===' > /tmp/modified-test.log echo 'Testing modified init-firewall-modified.sh' >> /tmp/modified-test.log echo >> /tmp/modified-test.log echo 'Before script execution:' >> /tmp/modified-test.log echo 'Docker DNS test:' >> /tmp/modified-test.log timeout 5 dig @127.0.0.11 +short google.com >> /tmp/modified-test.log 2>&1 || echo 'Docker DNS failed' >> /tmp/modified-test.log echo >> /tmp/modified-test.log echo 'Executing modified script...' >> /tmp/modified-test.log timeout 60 bash /workspace/init-firewall-modified.sh >> /tmp/modified-test.log 2>&1 || echo 'Script execution failed' >> /tmp/modified-test.log echo >> /tmp/modified-test.log echo 'After script execution:' >> /tmp/modified-test.log echo 'iptables policies:' >> /tmp/modified-test.log iptables -L | grep policy >> /tmp/modified-test.log 2>&1 echo 'Docker DNS test:' >> /tmp/modified-test.log timeout 5 dig @127.0.0.11 +short google.com >> /tmp/modified-test.log 2>&1 || echo 'Docker DNS failed' >> /tmp/modified-test.log cat /tmp/modified-test.log " # Copy results and cleanup docker compose exec -T test-workspace cat /tmp/modified-test.log > modified-test-results.log docker compose down echo "✅ MODIFIED test completed!" } # Function to test reviewer script in clean environment test_reviewer_script() { echo "🧪 Testing REVIEWER script in clean environment..." cd "$SCRIPT_DIR" # Start fresh environment docker compose up -d --build sleep 10 echo "📝 Running REVIEWER firewall script..." docker compose exec -T --user root test-workspace bash -c " echo '=== REVIEWER SCRIPT TEST ===' > /tmp/reviewer-test.log echo 'Testing reviewer init-firewall-reviewer.sh' >> /tmp/reviewer-test.log echo >> /tmp/reviewer-test.log echo 'Before script execution:' >> /tmp/reviewer-test.log echo 'Docker DNS test:' >> /tmp/reviewer-test.log timeout 5 dig @127.0.0.11 +short google.com >> /tmp/reviewer-test.log 2>&1 || echo 'Docker DNS failed' >> /tmp/reviewer-test.log echo >> /tmp/reviewer-test.log echo 'Executing reviewer script...' >> /tmp/reviewer-test.log timeout 60 bash /workspace/init-firewall-reviewer.sh >> /tmp/reviewer-test.log 2>&1 || echo 'Script execution failed' >> /tmp/reviewer-test.log echo >> /tmp/reviewer-test.log echo 'After script execution:' >> /tmp/reviewer-test.log echo 'iptables policies:' >> /tmp/reviewer-test.log iptables -L | grep policy >> /tmp/reviewer-test.log 2>&1 echo 'Docker DNS test:' >> /tmp/reviewer-test.log timeout 5 dig @127.0.0.11 +short google.com >> /tmp/reviewer-test.log 2>&1 || echo 'Docker DNS failed' >> /tmp/reviewer-test.log cat /tmp/reviewer-test.log " # Copy results and cleanup docker compose exec -T test-workspace cat /tmp/reviewer-test.log > reviewer-test-results.log docker compose down echo "✅ REVIEWER test completed!" } # Function to run all three tests and compare run_tests_in_container() { echo "🚀 Starting clean environment comparison tests..." # Test original script test_original_script echo "" # Test modified script test_modified_script echo "" # Test reviewer script test_reviewer_script echo "" # Compare results echo "📊 Comparison Results:" echo "=====================" echo "" echo "ORIGINAL Script Results:" echo "------------------------" cat original-test-results.log echo "" echo "MODIFIED Script Results:" echo "------------------------" cat modified-test-results.log echo "" echo "REVIEWER Script Results:" echo "------------------------" cat reviewer-test-results.log echo "" echo "✅ All three tests completed! Check *-test-results.log files for details" } # Main execution case "${1:-run}" in "run") run_tests_in_container ;; esac ```
-
実際に検証し、修正前は名前解決ができず、自分のコードだと名前解決が問題なくできることを確認しました。
3. PR提出からマージまで
ClaudeCodeにPRを書かせて提出しました笑
TCP_PORT=$(iptables -t nat -L DOCKER_OUTPUT -n 2>/dev/null | grep 'tcp.*to:127.0.0.11:' | sed 's/.*127\.0\.0\.11://g' | cut -d' ' -f1 || echo "")
UDP_PORT=$(iptables -t nat -L DOCKER_OUTPUT -n 2>/dev/null | grep 'udp.*to:127.0.0.11:' | sed 's/.*127\.0\.0\.11://g' | cut -d' ' -f1 || echo "")
...
iptables -t nat -F
...
if [ -n "$TCP_PORT" ] && [ -n "$UDP_PORT" ]; then
echo "Restoring Docker DNS with TCP:$TCP_PORT, UDP:$UDP_PORT"
iptables -t nat -N DOCKER_OUTPUT
iptables -t nat -N DOCKER_POSTROUTING
iptables -t nat -A OUTPUT -d 127.0.0.11/32 -j DOCKER_OUTPUT
iptables -t nat -A POSTROUTING -d 127.0.0.11/32 -j DOCKER_POSTROUTING
iptables -t nat -A DOCKER_OUTPUT -d 127.0.0.11/32 -p tcp -j DNAT --to-destination 127.0.0.11:$TCP_PORT
iptables -t nat -A DOCKER_OUTPUT -d 127.0.0.11/32 -p udp -j DNAT --to-destination 127.0.0.11:$UDP_PORT
iptables -t nat -A DOCKER_POSTROUTING -s 127.0.0.11/32 -p tcp -j SNAT --to-source :53
iptables -t nat -A DOCKER_POSTROUTING -s 127.0.0.11/32 -p udp -j SNAT --to-source :53
fi
以上のように
- TCPとUDPのポートがエフェメラルポートなので先に取得しておく
- そのあと、各行を挿入していく
PR出してみると
@shota-0129 thanks for opening this PR. How would you feel about something like this?
1. Extract Docker DNS info BEFORE any flushing DOCKER_DNS_RULES=$(iptables-save -t nat | grep "127\.0\.0\.11") 2. Perform security lockdown (flush + rebuild allowlist) iptables -t nat -F ... rebuild strict allowlist rules ... 3. Selectively restore ONLY internal Docker DNS resolution if [ -n "$DOCKER_DNS_RULES" ]; then echo "$DOCKER_DNS_RULES" | iptables-restore --noflush fi
とより簡潔な表現できない?というフィードバックが飛んできました。
フィードバックが飛んでくる=即時却下ではないと思ったので、大変嬉しかったです。
フィードバックの内容を含めて検証し、
- ルールを追加する前にチェーンを作成する
-
iptables-restore
でなくxargs
を使用して各行を保存すること
if [ -n "$DOCKER_DNS_RULES" ]; then echo "Restoring Docker DNS rules..." iptables -t nat -N DOCKER_OUTPUT 2>/dev/null || true iptables -t nat -N DOCKER_POSTROUTING 2>/dev/null || true echo "$DOCKER_DNS_RULES" | xargs -L 1 iptables -t nat else echo "No Docker DNS rules to restore" fi
に修正し再度pushしたところ、少し修正される形でマージされました🎉
マージされたメールが嬉しくて目が覚めて、いつもより会社に早く行けました笑
学んだこと「3つの大切さ」
1. 基礎知識
新卒研修で学んだ内容
- Dockerのネットワーク: ブリッジ、NAT、内部DNS
- DNS: 名前解決の流れ
これらが全部繋がって、問題を解決できました。
インフラ・コンテナ研修をしていただいた先輩方ありがとうございました🙇
2. 公式ドキュメント・コードを読むこと
まず、今回の問題発見は、ClaudeCodeの公式ドキュメントを読みながら作成し、なおかつその中で公式のコードを紐解いて問題を発見しました。
学生の頃は、よくネット記事で同じ事象を探すことばっかりしていましたが、自分のエラーがどこにあるのかを実際のコードを読んで理解する重要さを学びました。
3. 挑戦すること
弊社で大事にしている3つの要素の中に「挑戦」というのがあるのですが、今回はその「挑戦」だったなと思います。
自分は学生の頃、優秀なエンジニアではなくて、入社した同期とも比べると未熟な部分が多いなと思う日々ですが、それでも今回、自分の中で研修中に成長したことを糧に挑戦できたこと、そしてその結果、コントリビュートできて自分の中で1つ誇りに思えることができました。
挑戦したからこそ、こんな記事も書けているので1つの成功体験として大事にしながら、これからも挑戦したいと思います💪
まとめ
Claude Codeへの初めてのコントリビュートは、自分にとって大きな自信になりました。
- 基礎知識の積み重ねが実際の問題解決に直結した
- 挑戦したからこそ、自分にとっての成功体験が生まれた
これからも日々の学びを大切にしながら、エンジニアとして成長できればと思います。
そして、この記事を読んで「自分にもできるかも」「なんか挑戦しようかな」と思ってくれる新卒エンジニアが一人でも増えれば嬉しいです!
余談
新卒(25卒)のエンジニアで交流会を2025年秋頃に開こうかなと思っているので、もし興味ある方は以下のプロフィールのSNSから連絡ください〜
参考リンク
Views: 0