Proxy環境下で使うKubernetes

2022年03月03日 木曜日


【この記事を書いた人】
田口 景介

社会人生活の半分をフリーランス、半分をIIJで過ごすエンジニア。元々はアプリケーション屋だったはずが、クラウドと出会ったばかりに半身をインフラ屋に売り渡す羽目に。現在はコンテナ技術に傾倒中だが語りだすと長いので割愛。タグをつけるならコンテナ、クラウド、ロードバイク、うどん。

「Proxy環境下で使うKubernetes」のイメージ

皆さんがdocker, kubernetesを使う環境にはhttp proxyがありますか? 多くのエンタープライズネットワークがそうであるように、IIJのオフィスでもProxyを通らないとInternetへアクセスすることはできません。今回はそんなProxy環境下でKubernetesを使う話です。

Proxyの存在がどれほど生産性を低下させていることか。多くのエンジニアが一度は思ったことがあるのではないでしょうか。昨今はリモートワークの機会が多く、VPNとの合わせ技で以前にも増してProxyに苦しめられる場面が増えているのではないかと思います。そうした環境ではDocker, Kubernetesを利用するのも一苦労です。そこで、Proxyを乗り越えてDocker, Kubernetesを利用するために何を設定すべきかまとめます。

1 コンテナランタイムのProxy

まず、dockerd, containerdといったコンテナランタイムにProxyの設定が必要です。この辺りは公式ドキュメントでも詳細に解説されていますから、すでに解決されている方も多いことでしょうが念のため。

コンテナランタイムがInternetへアクセスするのは、主にレジストリ(主にはDocker Hub)からコンテナイメージをダウンロードするためです。「docker pull hello-world」としたとき、正常にダウンロードできなければProxyが正しく設定されていない可能性があります。

dockerdへProxyを設定する方法はごく一般的なもので、環境変数HTTPS_PROXY, HTTP_PROXY, NO_PROXYを設定するというものです。どこで環境変数を設定するかはOSやディストリビューションに依存しますが、dockerをsystemdから起動している場合は、例えば/etc/systemd/system/docker.service.dに以下のようなdrop-inファイルを作成してリスタートします(daemon-reloadもお忘れなく)。

/etc/systemd/system/docker.service.d/override.conf

[Service]
Environment="HTTP_PROXY=<ProxyのURL>"
Environment="HTTPS_PROXY=<ProxyのURL>"
Environment="NO_PROXY=127.0.0.1,localhost"

正常に設定できていれば、docker infoコマンドで確認できます。

$ sudo docker info | grep -i proxy
 HTTP Proxy: <ProxyのURL>
 HTTPS Proxy: <ProxyのURL>
 No Proxy: localhost,127.0.0.1

2 コンテナイメージのビルドに必要なProxy

また、Proxy環境下でDockerを利用する場合、~/.docker/config.jsonに下記の設定を用意すべしとされています。

~/.docker/config.json

{
        "proxies": {
                "default": {
                        "httpProxy": "<ProxyのURL>",
                        "httpsProxy": "<ProxyのURL>",
                        "noProxy": "localhost,127.0.0.1"
                }
        }
}

少しわかりにくいかもしれませんが、これはdockerdではなくコンテナとして起動されるプロセスへProxy用環境変数を設定するための設定です。docker runコマンドの–envオプションを自動的に付加してくれる機能と考えてよいでしょう。以下のように実際にコンテナを起動して環境変数をリストアップしてみると、実際の影響がよくわかると思います。大文字、小文字の環境変数が両方とも設定されるあたり気が利いていますね。

$ docker run alpine env | grep -i proxy  # 都度指定しなくてもProxy用環境変数が設定されている
HTTPS_PROXY=<ProxyのURL>
no_proxy=localhost,127.0.0.1
NO_PROXY=localhost,127.0.0.1
https_proxy=<ProxyのURL>
http_proxy=<ProxyのURL>
HTTP_PROXY=<ProxyのURL>

この設定がありがたいのは、こうしてdocker runコマンドを実行するときよりも、むしろdocker buildコマンドでコンテナイメージをビルドするときです。コンテナイメージをビルドするとき、多くの場合Linuxのパッケージやプログラミング言語のライブラリの類をダウンロードするはずです。この処理は、ビルド用のコンテナが起動され(以下の場合Ubuntu:20.04)、その中でDockerfileの指示に従ってコマンド(以下ではaptコマンド)が実行されるため、コンテナランタイムではなくコンテナの中にProxyの設定が必要なのです。Proxyの設定をDockerfileの中に記述したり、docker buildコマンドのコマンドラインで指定することも可能ですが、前者は環境依存の情報をDockerfileに記載するのは好ましくありませんし、後者はただ面倒です。素直にconfig.jsonに設定しておくとよいでしょう。

$ cat Dockerfile
FROM ubuntu:20.04
RUN apt update && DEBIAN_FRONTEND=noninteractive apt install -y dnsutils curl iputils-ping net-tools iproute2 procps git supervisor
 
ENTRYPOINT ["/usr/bin/supervisord", "-n"]
$ docker build -t ubuntu-tools:20.04 . # コマンドラインでは何も指定しなくても、Proxyを通じてapt installが実行される

もっとも、これはdockerコマンドを実行する際に参照されるコンフィグレーションなので、Kubernetesのコンテナランタイムとして使うDockerには不要な設定です。あくまでも、手元の作業環境のための設定ということで。

3 KubernetesのProxy

さて、それでは本題のKubernetesのProxy設定です。と言っても、実はkube-apiserver, kube-controller-manager, kube-schedulerといったKubernetesのコントロールプレーンにProxyの設定は不要です。Proxyの設定を必要とするのは、ユーザがデプロイしたInternetへアクセスするPod(コンテナ)だけです。

3.1 環境変数で設定する

PodへProxyを設定する方法は、何も特別なことはありません。いつものように環境変数を設定するだけです。例えば、以下のように環境変数に設定したい値をConfigMapリソースとして作成し、

proxy.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: proxy
  namespace: default
data:
  http_proxy: <ProxyのURL>
  https_proxy: <ProxyのURL>
  no_proxy: 127.0.0.1,localhost,kubernetes.default,10.96.0.1

PodにenvFromを使って読み込ませます。

pod.yaml

apiVersion: v1
kind: Pod
metadata:
  name: ubuntu-tools
spec:
  containers:
  - name: ubuntu-tools
    image: ubuntu-tools:20.04
    envFrom:
    - configMapRef:
        name: proxy

確認してみましょう。Kubernetes上のPodに環境変数が設定され、Internet上のサイトへアクセスできることがわかります。

$ kubectl exec ubuntu-tools -- env | grep -i proxy
http_proxy=<ProxyのURL>
https_proxy=<ProxyのURL>
no_proxy=127.0.0.1,localhost,kubernetes.default,10.96.0.1
$ kubectl exec ubuntu-tools -- curl https://www.iij.ad.jp # Internet上のサイトへアクセスできる

これで一件落着と言いたいところですが、話はそう単純ではありません。プロキシを通す設定はこれで十分なのですが、プロキシを通さずにアクセスしたい宛先をどうやってno_proxyに設定すればいいでしょう。

状況をまとめます。

  • Internetへ向かう通信はProxyへ、クラスタ内部の通信は宛先のPodへ直接届けたい
  • KubernetesにおいてPodのIPアドレスは動的に設定されるため、クラスタ内部の通信では宛先にほぼ例外なくホスト名が使われる
  • no_proxyにワイルドカードを利用できるかどうかはhttpクライアントの実装に依存し、利用できないクライアントが少なくない

つまり、no_proxyには想定されるクラスタ内の通信先ホスト名(サービス名。例: api-endpoint.default.svc.cluster.local)を列挙せざるを得ない状況が生まれるということです。力技で解決できなくはありませんが、さすがにちょっと現実的ではないと思いませんか?

3.2 透過プロキシを活用する

no_proxyの問題以前にすべてのPodにConfigMapをマウントする時点ですでに現実的とはいいがたいので、もっとスマートな方法で解決したいところです。そこで、Kubernetesのすべてのノードに透過プロキシを構成します。

一般的にProxyを経由してアクセスするためには、httpクライアントへ事前に設定しておく必要があります。ですが、ネットワークの経路上に透過プロキシを配置すると、httpあるいはhttpsのトラフィックを解析し、Proxyへ向けるべきリクエストはヘッダを書き換えてProxyへ向けなおし、Proxyが不要なリクエストはそのまま通すということが可能です。つまり、httpクライアントからはProxyがなくともInternetへアクセス可能であるかのように利用できるということです。

ちなみに、透過プロキシで検索すると「サーバ証明書を偽造する必要がある」との解説が見つかることがあると思いますが、これはSNIが普及する前のだいぶ古い情報ですのでご心配なく。実際にはサーバ証明書を偽造する必要はなく、また暗号化されたリクエストボディを復号化する必要もなく、httpsの透過プロキシは実現可能ですし、実際にIKEではそのようなシステムで透過プロキシが実現されています。

Kubernetesに透過プロキシを組み込む方法はいくつか考えられますが、すべてのノードに透過プロキシをデプロイするのがもっとも効率が良さそうです。こうすると、ノード内に閉じるトラフィックには影響なく、ノードから外へ出ていくトラフィックのみ必要に応じてリクエストをProxy宛に書き換えることができます。

 

IIJ謹製ルータSEILにも透過プロキシの機能は備わっていますが、IIJのオフィスネットワークに構築されたKubernetes、IKE OfficeはUbuntu上に構築されていることもあり、ここではwadahiroさんのgo-transproxyを利用させていただいています。ノードから出ていくhttp, httpsのトラフィックは一旦すべてgo-transproxyを通し、宛先がプライベートアドレスであればそのままに、それ以外のトラフィックは透過的かつ強制的にProxyへ転送する処理を行います。

https://github.com/wadahiro/go-transproxy

実際に利用するには、例えば以下のようなunitファイルを用意して各ノードでsystemdからgo-transproxyを起動します。

trasproxy.service

[Unit]
Description=trasproxy
Documentation=https://github.com/wadahiro/go-transproxy
 
[Service]
Environment=http_proxy=<ProxyのURL>
Environment=https_proxy=<ProxyのURL>
Environment=no_proxy=192.168.0.0/16,10.0.0.0/8,172.16.0.0/12
 
ExecStartPre=-/sbin/iptables -t nat -N TRANSPROXY
ExecStartPre=/sbin/iptables -t nat -A TRANSPROXY -i eth1 -j RETURN
ExecStartPre=/sbin/iptables -t nat -A TRANSPROXY -i eth2 -j RETURN
ExecStartPre=/sbin/iptables -t nat -A TRANSPROXY -d 192.168.0.0/16 -j RETURN
ExecStartPre=/sbin/iptables -t nat -A TRANSPROXY -d 172.16.0.0/12 -j RETURN
ExecStartPre=/sbin/iptables -t nat -A TRANSPROXY -d 10.0.0.0/8 -j RETURN
ExecStartPre=/sbin/iptables -t nat -A TRANSPROXY -p tcp --dport 80 -j DNAT --to-destination <transproxyのIPアドレス>:3129
ExecStartPre=/sbin/iptables -t nat -A TRANSPROXY -p tcp --dport 443 -j DNAT --to-destination <transproxyのIPアドレス>:3130
ExecStartPre=/sbin/iptables -t nat -I PREROUTING -j TRANSPROXY
 
ExecStopPost=/sbin/iptables -t nat -D PREROUTING -j TRANSPROXY
ExecStopPost=/sbin/iptables -t nat -F TRANSPROXY
ExecStopPost=/sbin/iptables -t nat -X TRANSPROXY
 
Type=simple
ExecStart=/usr/sbin/transproxy -disable-iptables
Restart=always
RestartSec=10s
 
[Install]
WantedBy=multi-user.target

これによってKubernetesのユーザはProxyの要不要を気にすることなくPodをデプロイすることが可能になりました。no_proxyの問題が解決するだけでなく、いちいちProxy用環境変数を設定する手間も省けて一石二鳥というわけです。

4 docker desktopのProxy設定

こうしてProxyの環境下でも不自由なくKubernetesを動かすことができるようになりましたが、実はこれとまったく同じ設定をDocker Desktopではまるでブラウザのプロキシを設定するかのように簡易にやってのけます。dockerdの設定も、dockerコマンドの設定も、透過プロキシ(もしくはそれに類する設定)も、すべてです。Windows版でWSL2 backendモードを使う場合は、Windowsでも、WSL2でも同じように使えるように設定を整えてくれます。

Docker Desktopを使ってKubernetesを構築すると、Proxyの環境下であってもあまりにも自然に動いてしまうのでそのありがたみを感じることができないかもしれませんが、それなりに苦労してオフィスネットワークにKubernetesを構築した身としては、Docker Desktopを使えばこんなにも簡単に同等環境が手に入ると知った時には少しばかりやるせない気分になりましたが、それ以上にすばらしい製品を送り出してくれたDocker Inc.に感謝したものです。

その辺りの話は先日オンラインで開催したIIJ Technical Week 2021でも触れています。セミナーの内容はYouTubeで公開されていますので、よろしければそちらもご覧ください。

クラウドネイティブ最新動向

田口 景介

2022年03月03日 木曜日

社会人生活の半分をフリーランス、半分をIIJで過ごすエンジニア。元々はアプリケーション屋だったはずが、クラウドと出会ったばかりに半身をインフラ屋に売り渡す羽目に。現在はコンテナ技術に傾倒中だが語りだすと長いので割愛。タグをつけるならコンテナ、クラウド、ロードバイク、うどん。

Related
関連記事