Oracleを使うGoアプリのコンテナ化メモ（挫折編）


https://qiita.com/yuuman/items/09f6d3e43e96de0a5096

続きます。

# 4. アプリのDockerイメージ化
前回作成したサンプルのGoアプリをコンテナ化して実行できることを確認します。

## 4.1 Dockerfileの準備
まずはアプリビルド用のDockerfileをベースイメージ`golang:1.18.5`で作成します。

```dockerfile:Dockerfile
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](https://qiita.com/yuuman/items/09f6d3e43e96de0a5096#23-oci8pc%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E4%BD%9C%E6%88%90)を`Dockerfile`と同じディレクトリに配置します。
```:oci.pc
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`に修正します。

:::note info
**接続設定の環境変数化**
本来であれば実行する環境(コンテナ)が変わると接続設定も変更となります。
そのため接続設定部分は環境変数化しコンテナ起動時に指定できるようにすることを推奨します。
今回はハードコートされているので直接アプリ本体をイジるイケてない実装です。
:::



```go:main.go
func main() {
	log.Printf("Version:%s/Revision:%s", VERSION, REVISION)
	connectStr := "gouser/pwd@oracle19c:1521/GOPDB"
    // 省略
}
```

## 4.2 ビルドと実行
次にDockerfileをビルドします。

```shell
$ docker build -t go-sample .
… ビルドログ省略

$ docker images
REPOSITORY        TAG         IMAGE ID       CREATED          SIZE
go-sample         latest      fdca9c0d76b7   2 seconds ago   1.43GB
```

ビルドが完了したのでコンテナを起動しますが、すでに`oracle19c`コンテナが起動しておりコンテナ間で通信するためにネットワークを合わせる必要があります。
なので現在のネットワークを調べてみることに。

```shell
$ 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`に繋がっている事が判明。

```shell
$ 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`イメージにビルドしたバイナリファイルを移して軽量化します。

https://zenn.dev/tatsurom/articles/golang-docker-environment

```dockerfile:Dockerfile
# ===================== #
# 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を参照している](https://github.com/mattn/go-oci8/blob/master/globals.go#L7)ためCGOが必要となります。
ですが、クロスコンパイルの場合CGOは自動で無効化されるため`CGO_ENABLED=1`でビルド時にCGOの有効化を行う必要があります。

これをビルドして実行すると。。。

```shell
$ 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」となったみたいです。

https://qiita.com/hrkt/items/6a2169b021e756eb32e2

実際に依存関係を中に入って確認してみようと思いましたが、`scratch`イメージだとコンテナの中に入れないので検証のため`prd`をUbuntuイメージに変更して再度ビルドしてコンテナの中に入ります。

コンテナの中を確認するとちゃんとビルドしたファイルが存在しています。

```bash
/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
```

次にファイルの依存関係を確認すると。。。

```bash
/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がありました。

https://github.com/mattn/go-oci8/issues/102

ビルド時のコマンドに静的リンクオプションをつけたり、`oci8.pc`を書き換えたりといろいろとやり取りがあったので色々試したのですが上手くいかず。
Issueの起票者も結局うまく行ったのかわからないままフェードアウトしてるし。。。

Oracle Clientの静的ライブラリがあれば静的リンクでビルドできるかもとのコメントもあります。
下記の通り、Oracle Instant Clientをインストールしただけでは静的ライブラリは生成されず、自分で作る必要があるそうです。
ただ私がOracleに詳しくないのと、もうこれ以上Oracle Instant Clientに振り回されたくないので別の方法に切り替えることにしました。
（もう`mattn/go-oci8`を使うのは疲れたよ。。。）

> 6.1.6 クライアントの静的ライブラリの生成
> クライアントの静的ライブラリ(libclntst12.a)は、Oracle Databaseのインストール時に生成されません。クライアントの静的ライブラリにアプリケーションをリンクする場合は、最初に静的ライブラリを生成する必要があります。

https://docs.oracle.com/cd/F19136_01/unxar/using-oracle-precompilers-and-oracle-call-interface.html#GUID-0A71F60A-3765-477C-AC3D-B146E21936C4

ちなみに下記ブログでも同じ問題に直面しており「`mattn/go-oci8`だとマルチステージビルドで動かないね」とのこと。

https://umemak.github.io/blog/posts/2022/06/12_oracle_docker_go3/

# 挫折編のまとめ
**結局`mattn/go-oci8`を利用したGoアプリのマルチステージビルドはあきらめることにし、Pure Goで実装された`sijms/go-ora`を利用する方針に切り替えたいと思います。**
**もし`mattn/go-oci8`を使ってマルチステージビルドでうまく実行できた方がいればぜひ教えていただきたいです！！！！**

あと余談ですが、`mattn/go-oci8`も最終リリースは数年前で、あまり積極的にメンテされていない様子。


https://github.com/mattn/go-oci8/issues/433
> 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.


https://qiita.com/yuuman/items/dab16bdf5777b096f62b

に続く・・・