エラー内容:何が起きたか
コンパイル言語であるGoのコードや機能の改修を行いやすくするために、Dockerを用いてGoの環境構築を行っていたところコンテナ内部で謎のネットワークエラーが出た。
行った手順としてはまず、以下のようなDockerfileとdocker-compose.ymlを作成。
FROM golang:latest
WORKDIR /path/to/go/project
COPY go.mod ./
COPY go.sum ./
RUN go mod download
COPY path/to/main.go ./
EXPOSE 8080
version: '3'
services:
app:
container_name: go_container
image: golang:latest
tty: true
ports:
- "3000:3000"
volumes:
- /path/to/$GOPATH:/go
(※なお、今回の事象はこれと全く同じ構成のdocker-composeでなくとも発生する。 理由は後述)
docker-composeを実行
docker-compose up --build -d
すると、go mod downlodのところで永遠に処理が終わらない。
別のアプローチ
Dockerfileを以下のように変更
FROM golang:latest
WORKDIR /path/to/go/project
COPY go.mod ./
COPY go.sum ./
RUN go get -u github.com/go-sql-driver/mysql
COPY path/to/main.go ./
EXPOSE 8080
go getで取得するモジュールはなんでもOK
これを実行すると以下のようなエラーが返ってくる。
(前略) net/http: TLS handshake timeout
つまりネットワークが途絶えてしまっている。
後の調査(内容割愛。curlとか)の結果、奇妙なことにhttps通信の場合にだけこのような事象が発生することがわかった。
原因:答えはスタート地点に
原因がわからず数日にわたって調べまくったところ、公式の最新バージョン(20.10.8)リリースノートに以下のような記述を発見。
IMPORTANT
Due to net/http changes in Go 1.16, HTTP proxies configured through the ~~ $HTTP_PROXY environment variable are no longer used for TLS (https://) connections. Make sure you also set an $HTTPS_PROXY environment variable for handling requests to https:// URLs.
Refer to the HTTP/HTTPS proxy section to learn how to configure the Docker Daemon to use a proxy server.
go側のnet/httpの仕様変更に伴って環境変数 $HTTPS_PROXYも自分で設定しなきゃダメだよということ。(かなり大雑把)
灯台下暗しとはまさにこのこと。
これならどのようなDockerfileを書いたところで同じ事象に遭遇するはず
とはいえ、設定するようなプロキシサーバがなかったため、squidを使用してホスト側にプロキシサーバを建てることに。
原因はこのDockerの仕様ではなくOSのカーネル、あるいはクラウドサービス側のOS設定に原因がありそうだということがわかりました。誤情報すみません。
CentOSを使っているときにこういったネットワーク接続周り(iptablesやfirewalldなど)で動作がうまくいかないケースがあるようで、これらの内部での対応をあきらめてプロキシで無理やりネットワークを通していこうというのが以下の対策となります。よって対応策は同じです。
対策:環境変数の設定とプロキシサーバの設置
squidのインストール・設定
(僕の利用していた環境はcentos7なので、yumを利用して)squidをインストール
sudo yum install -y squid
/etc配下にある設定ファイルを編集
#
# Recommended minimum configuration:
#
# Example rule allowing access from your local networks.
# Adapt to list your (internal) IP networks from where browsing
# should be allowed
acl localnet src 10.0.0.0/8 # RFC1918 possible internal network
acl localnet src 172.16.0.0/12 # RFC1918 possible internal network
acl localnet src 192.168.0.0/16 # RFC1918 possible internal network
acl localnet src fc00::/7 # RFC 4193 local private network range
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
acl SSL_ports port 443
acl Safe_ports port 80 # http
acl Safe_ports port 21 # ftp
acl Safe_ports port 443 # https
acl Safe_ports port 70 # gopher
acl Safe_ports port 210 # wais
acl Safe_ports port 1025-65535 # unregistered ports
acl Safe_ports port 280 # http-mgmt
acl Safe_ports port 488 # gss-http
acl Safe_ports port 591 # filemaker
acl Safe_ports port 777 # multiling http
acl CONNECT method CONNECT
# 以下2行を追記(多分コンテナの方だけでいい)
acl name(任意の名前1) src (DockerホストのIP)
acl name2(任意の名前2) src (DockerコンテナのIP)
#
# Recommended minimum Access Permission configuration:
#
# Deny requests to certain unsafe ports
http_access deny !Safe_ports
# Deny CONNECT to other than secure SSL ports
http_access deny CONNECT !SSL_ports
# Only allow cachemgr access from localhost
http_access allow localhost manager
http_access deny manager
# We strongly recommend the following be uncommented to protect innocent
# web applications running on the proxy server who think the only
# one who can access services on "localhost" is a local user
#http_access deny to_localhost
#
# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS
#
# Example rule allowing access from your local networks.
# Adapt localnet in the ACL section to list your (internal) IP networks
# from where browsing should be allowed
# http_access allow localnet
http_access allow localhost
# 以下2行を追記(こちらも多分コンテナの方ry)
# (※name, name2はさきほど定義したもの)
http_access allow name
http_access allow name2
# http_access allow all
# And finally deny all other access to this proxy
http_access deny all
# Squid normally listens to port 3128
http_port 3128
# Uncomment and adjust the following to add a disk cache directory.
#cache_dir ufs /var/spool/squid 100 16 256
# Leave coredumps in the first cache dir
coredump_dir /var/spool/squid
#
# Add any of your own refresh_pattern entries above these.
#
refresh_pattern ^ftp: 1440 20% 10080
refresh_pattern ^gopher: 1440 0% 1440
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
refresh_pattern . 0 20% 4320
(squidのデフォルトのポート番号は3128。今回はとくに変更しない。)
コンテナのIDがわからない場合は、コンテナ内部に入り以下のコマンドで確認
hostname -i
開始
sudo systemctl start squid
次はdocker側の設定
Docker側、環境変数$HTTP(S)_PROXYの設定
Docker公式の設定ページに従って環境変数を設定。
sudo mkdir -p /etc/systemd/system/docker.service.d
Environment="HTTP_PROXY=(squidを設置したホストのIP):3128"
Environment="HTTPS_PROXY=(squidを設置したホストのIP):3128"
ついでに、dockerを利用するユーザーのホームディレクトにconfig.jsonを設置。
公式ページの設定ガイドラインはこちら
{
"proxies":
{
"default":
{
"httpProxy": "(squidホスト):3128",
"httpsProxy": "(squidホスト):3128"
}
}
}
書いたらDockerを再起動。
sudo systemctl restart docker
再起動するとコンテナが止まってしまうためコンテナをstartするのを忘れないように。
自分はそれで気づかずにホストでhttpsの検証してぬか喜びしてしまった
確認
コンテナ内部でcurlコマンドを使い検証。
alpine使ってる人とかでcurlできない人はインストールするか、apk updateでもかけてみるのがよし。
crul https://google.com
`
301 Moved301 Moved
The document has moved here. `HTMLが返ってきたらOK。
この状態になればgo getやgo mod downloadなどのコマンドも通る。
総括
最新版(20.10.8)のDockerはコンテナ内部からhttps通信を行うためにプロキシが必要。
20.10.7などの直前バージョンを使おうとしても、他モジュールが20.10.8でくっついてきてしまったり、そもそものgoの仕様変更だったりという事情で結局この事象から逃れられなかった。
また、最初からhttp通信はできていたが、$HTTP_PROXYのデフォルト値は見つけられなかった。(echoだと空行)
OS起因でのネットワークエラーなので素直にCentOSをやめて別のOSを使うのが最善。それができない事情がある場合は上記のように無理やりプロキシで帳尻を合わせて対応すれば使うことはできます。
Goの場合はセオリーとはやり方が異なりますが、プロジェクトごとvolumeしておいてdocker-compose.yml側でビルドすればproxy経由でモジュールを取得できます。Dockerfileで同じことをやろうとすると、proxy利用のための環境変数を設定する前にイメージビルドが走るためモジュールの取得に失敗します。
なお、CentOSであっても個人的に使っている環境ではこのような事象は起きず普通に使えることから提供しているサーバ会社の設定なども加味された場合に起こる特殊な現象であると考えられます。