続きます。
4. アプリのDockerイメージ化
前回作成したサンプルのGoアプリをコンテナ化して実行できることを確認します。
4.1 Dockerfileの準備
まずはアプリビルド用のDockerfileをベースイメージgolang:1.18.5
で作成します。
FROM golang:1.18.5
WORKDIR /workspace
ENV ORACLE_URL=https://download.oracle.com/otn_software/linux/instantclient/217000/
ENV BASIC_ZIP=instantclient-basic-linux.x64-21.7.0.0.0dbru.zip
ENV SDK_ZIP=instantclient-sdk-linux.x64-21.7.0.0.0dbru.zip
ENV PKG_CONFIG_PATH=/usr/lib/pkgconfig/
ENV LD_LIBRARY_PATH=/usr/local/instantclient_21_7
COPY ./ ./
RUN set -x && \
echo "==== install libs ====" && \
apt-get update && \
apt-get -y install pkg-config libaio1 unzip curl gcc g++ &&\
echo "==== oci install ====" && \
curl -sS -o ./${BASIC_ZIP} ${ORACLE_URL}${BASIC_ZIP} && \
curl -sS -o ./${SDK_ZIP} ${ORACLE_URL}${SDK_ZIP} && \
unzip -q ./${BASIC_ZIP} -d /usr/local && \
unzip -q ./${SDK_ZIP} -d /usr/local && \
cp ./oci8.pc /usr/lib/pkgconfig/oci8.pc && \
echo "==== go build ====" && \
go mod download && \
go build -o goapp
CMD ["./goapp"]
また、前回の記事と同じoci8.pcをDockerfile
と同じディレクトリに配置します。
prefix=/usr/local/instantclient_21_7
libdir=${prefix}
includedir=${prefix}/sdk/include/
glib_genmarshal=glib-genmarshal
gobject_query=gobject-query
glib_mkenums=glib-mkenums
Name: oci8
Description: oci8 library
Libs: -L${libdir} -lclntsh
Cflags: -I${includedir}
Version: 12.2
4.2 アプリの改修
次にアプリ本体に手を加えます。
コンテナ化にあたりmain.go
のOracle接続URLを改修します。
改修ポイントは@<host>:<port>
のホスト部分です。
コンテナ化したアプリとOracle19cのコンテナ間で通信するためにホスト部を修正します。
コンテナは同じネットワークにつながっている場合、コンテナ名で通信可能なのでホスト部をoracle19c
に修正します。
接続設定の環境変数化
本来であれば実行する環境(コンテナ)が変わると接続設定も変更となります。
そのため接続設定部分は環境変数化しコンテナ起動時に指定できるようにすることを推奨します。
今回はハードコートされているので直接アプリ本体をイジるイケてない実装です。
func main() {
log.Printf("Version:%s/Revision:%s", VERSION, REVISION)
connectStr := "gouser/pwd@oracle19c:1521/GOPDB"
// 省略
}
4.2 ビルドと実行
次にDockerfileをビルドします。
$ docker build -t go-sample .
… ビルドログ省略
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
go-sample latest fdca9c0d76b7 2 seconds ago 1.43GB
ビルドが完了したのでコンテナを起動しますが、すでにoracle19c
コンテナが起動しておりコンテナ間で通信するためにネットワークを合わせる必要があります。
なので現在のネットワークを調べてみることに。
$ docker network
NETWORK ID NAME DRIVER SCOPE
ac9ed5bc648d bridge bridge local
2fa21a38fc2d desktop_default bridge local
5de0591cb26b dockerfiles_default bridge local
bbe4185d26f3 host host local
7d902802ca33 none null local
以外に多かった。
oracle19c
コンテナがどのネットワークに繋がってるかわからない・・・
調べてみたところdesktop_default
に繋がっている事が判明。
$ docker ps --filter network=desktop_default
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
af04e002b55b oracle/database:19.3.0-ee "/bin/sh -c 'exec $O…" 2 days ago Up 27 minutes (healthy) 0.0.0.0:1521->1521/tcp, 0.0.0.0:5500->5500/tcp oracle19c
接続先ネットワークが判明したので--net
オプションでネットワークを指定してビルドしたイメージからコンテナを起動します。
$ docker run --name go-sample --net desktop_default go-sample:latest
2022/09/15 14:08:48 Version:default/Revision:dev
2022/09/15 14:08:48 ID: 1 Name: testuser01
2022/09/15 14:08:48 ID: 2 Name: testuser02
2022/09/15 14:08:48 ID: 3 Name: testuser03
go-sample
コンテナからoracle19c
コンテナへの通信が通り正常に処理が実行できました。
5. Dockerイメージのマルチステージビルド
現在のイメージは1.43GBとかなり大きめなので、ビルドしたバイナリだけを軽量のイメージに移した軽量な実行イメージを作りたいと思います。
この軽量な実行イメージの作成が今回の目標です。
5.1 マルチステージビルドとコンテナの実行
こちらのブログの内容を参考にscratch
イメージにビルドしたバイナリファイルを移して軽量化します。
# ===================== #
# Builder
# ===================== #
FROM golang:1.18.5 as builder
WORKDIR /workspace
ENV ORACLE_URL=https://download.oracle.com/otn_software/linux/instantclient/217000/
ENV BASIC_ZIP=instantclient-basic-linux.x64-21.7.0.0.0dbru.zip
ENV SDK_ZIP=instantclient-sdk-linux.x64-21.7.0.0.0dbru.zip
ENV PKG_CONFIG_PATH=/usr/lib/pkgconfig/
ENV LD_LIBRARY_PATH=/usr/local/instantclient_21_7
COPY ./ ./
RUN set -x && \
echo "==== install libs ====" && \
apt-get update && \
apt-get -y install pkg-config libaio1 unzip curl gcc g++ &&\
echo "==== oci install ====" && \
curl -sS -o ./${BASIC_ZIP} ${ORACLE_URL}${BASIC_ZIP} && \
curl -sS -o ./${SDK_ZIP} ${ORACLE_URL}${SDK_ZIP} && \
unzip -q ./${BASIC_ZIP} -d /usr/local && \
unzip -q ./${SDK_ZIP} -d /usr/local && \
cp ./oci8.pc /usr/lib/pkgconfig/oci8.pc && \
echo "==== go build ====" && \
go mod download && \
CGO_ENABLED=1 GOOS=linux go build -o goapp
# ===================== #
# Production
# ===================== #
FROM scratch as prod
ENV ROOT_PATH=/go/app
WORKDIR ${ROOT_PATH}
COPY --from=builder /workspace/goapp ${ROOT_PATH}
CMD ["/go/app/goapp"]
↑が今回マルチステージ用に修正したDockerfileです。
別の環境(scratch
イメージ)で動かすため、クロスコンパイルが必要となります。
クロスコンパイルのオプションとしてgo build
コマンドにCGO_ENABLED=1 GOOS=linux
を追加しました。
クロスコンパイルを実行する場合、GOOS
で実行するOSを指定します。
CGOはGoからCで実装したコード呼び出しを可能とする機能でimport "C"
とすることでcgoパッケージのインポートを行います。
mattn/go-oci8
ではC言語で実装されたOracle Instant Clientを参照しているためCGOが必要となります。
ですが、クロスコンパイルの場合CGOは自動で無効化されるためCGO_ENABLED=1
でビルド時にCGOの有効化を行う必要があります。
これをビルドして実行すると。。。
$ docker build -t go-sample .
//ビルドログ省略
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
go-sample latest 67967756567c 4 seconds ago 2.85MB
golang 1.18.5 06c366130191 1 weeks ago 965MB
oracle/database 19.3.0-ee b29bba5ef6ba 1 weeks ago 6.53GB
$ docker run --name go-sample --net desktop_default go-sample:latest
exec /go/app/goapp: no such file or directory
「no such file or directory」となりました。。。
5.2 実行バイナリの依存確認
どうやらsample-go
が依存している共有ライブラリがインストールされていないので「no such file or directory」となったみたいです。
実際に依存関係を中に入って確認してみようと思いましたが、scratch
イメージだとコンテナの中に入れないので検証のためprd
をUbuntuイメージに変更して再度ビルドしてコンテナの中に入ります。
コンテナの中を確認するとちゃんとビルドしたファイルが存在しています。
/go/app # ls -la
total 2792
drwxr-xr-x 1 root root 4096 Oct 3 13:54 .
drwxr-xr-x 1 root root 4096 Oct 3 05:05 ..
-rwxr-xr-x 1 root root 2845088 Oct 3 05:05 goapp
次にファイルの依存関係を確認すると。。。
/go/app # ldd goapp
linux-vdso.so.1 (0x00007ffdc1bff000)
libclntsh.so.21.1 => not found
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f637a223000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6379ffb000)
/lib64/ld-linux-x86-64.so.2 (0x00007f637a22c000)
OCI関連やその他ライブラリを動的リンクで参照していますね。
libclntsh.so.21.1
はインストールしていないためnot found
になっています。
ちなみに静的リンクの場合は「not a dynamic executable」となります。
当然ながら実行する先の環境には必要なライブラリがインストールされていないので起動にコケるというわけです。
builder
のようにOracle Instant Clientやその他ライブラリをインストールすれば問題なさそうですが、scratch
の場合はbash
も入っていないので簡単にはインストールできないためビルド時に依存するライブラリを静的リンクでバイナリに含める必要があります。
5.3 静的リンクによるビルド方法の調査
Goではビルド時にバイナリに依存するライブラリを含める静的リンクと、依存するライブラリをバイナリに含めず実行時に呼び出す動的リンクがあります。
標準ライブラリを利用していれば静的リンクとなりバイナリ単体で実行可能となるのですが、今回のように特殊なライブラリ(Oracle Instant Client)を利用するパターンだと動的リンクになってしまいます。
CGO_ENABLED=0
でCGOを無効化すると静的リンクになるのですが、今回はCGOを無効化するとそもそもビルドできません。
どうにかCGO有効化で静的リンクビルドできないか調べてたところ、mattn/go-oci8
のGitHubでCGO有効化した状態での静的リンクビルドについてIssueがありました。
ビルド時のコマンドに静的リンクオプションをつけたり、oci8.pc
を書き換えたりといろいろとやり取りがあったので色々試したのですが上手くいかず。
Issueの起票者も結局うまく行ったのかわからないままフェードアウトしてるし。。。
Oracle Clientの静的ライブラリがあれば静的リンクでビルドできるかもとのコメントもあります。
下記の通り、Oracle Instant Clientをインストールしただけでは静的ライブラリは生成されず、自分で作る必要があるそうです。
ただ私がOracleに詳しくないのと、もうこれ以上Oracle Instant Clientに振り回されたくないので別の方法に切り替えることにしました。
(もうmattn/go-oci8
を使うのは疲れたよ。。。)
6.1.6 クライアントの静的ライブラリの生成
クライアントの静的ライブラリ(libclntst12.a)は、Oracle Databaseのインストール時に生成されません。クライアントの静的ライブラリにアプリケーションをリンクする場合は、最初に静的ライブラリを生成する必要があります。
ちなみに下記ブログでも同じ問題に直面しており「mattn/go-oci8
だとマルチステージビルドで動かないね」とのこと。
挫折編のまとめ
結局mattn/go-oci8
を利用したGoアプリのマルチステージビルドはあきらめることにし、Pure Goで実装されたsijms/go-ora
を利用する方針に切り替えたいと思います。
もしmattn/go-oci8
を使ってマルチステージビルドでうまく実行できた方がいればぜひ教えていただきたいです!!!!
あと余談ですが、mattn/go-oci8
も最終リリースは数年前で、あまり積極的にメンテされていない様子。
Q:Basically the title. I dont see any commits after January of 2021. Is this library still maintained?
A:Yes. However, it is not aggressive.
に続く・・・