Oracleに接続可能ななるべく小さいpython実行環境のDocker imageをビルドする

今回のお題

接続先がOracleなシステムでPythonが実行可能なコンテナイメージを作りたい。
小さいイメージは正義。

tl;dr

  • Oracle公式のイメージを利用する。
  • Docker multi stage buildを活用する。
  • alpineは苦行。

完成形は最後のほうにあります。

要件

MUST

  • 接続先は既存システムのOracle。変更不可。
  • 実行環境はパブリッククラウド内。
  • コードはgit管理。
  • 開発言語は任意。(今回はData Classなどの機能を使ってみたかったのでpython3.7とした)

WANT

  • 環境構築は手軽にやりたい。
  • デプロイは高速にしたい。

構築までの道のり

まずは素朴に

ローカル確認

Dockernizeする前に、ローカルでの動作を確認します。

pythonからOracleに接続するための公式のライブラリは cx_Oracleです。 *1*2
pipenvで入れましょう。*3

$ pipenv install
$ pipenv install cx_Oracle
$ cat Pipfile
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true

[dev-packages]

[packages]
cx-oracle = "*"

[requires]
python_version = "3.7"

Oracleインスタンスの構築

接続先のOracleインスタンスもDockerで立てておきます。*4

macOSでOracle Database使いたい - Qiita

上記に従い本体のバイナリを入手します。バージョンは 19.3.0 Standard Edition 2 としました。 *5

$ git clone git@github.com:oracle/docker-images.git
$ cd docker-images/OracleDatabase/SingleInstance/dockerfiles
$ mv /your/download/path/to/LINUX.X64_193000_db_home.zip 19.3.0/
$ ./buildDockerImage.sh -i -s -v 19.3.0 # 完了まで10分ぐらいかかる
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
oracle/database     19.3.0-se2          91da406c90ef        23 seconds ago      6.65GB
oraclelinux         7-slim              874477adb545        6 weeks ago         118MB

無事イメージが作成されました。
とくにカスタマイズはせず起動します。

$ mkdir -p $HOME/tmp/oradata
$ docker run --name docker_oracle \
    -p 1521:1521 -p 5500:5500 \
    -v $HOME/tmp/oradata:/opt/oracle/oradata \
    oracle/database:19.3.0-se2
# ..snip..
#########################
DATABASE IS READY TO USE!
#########################

起動しました。コンテナが立ち上がっていることが確認できます。

$ docker ps
CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS                    PORTS                                            NAMES
6c7d9db8a2bb        oracle/database:19.3.0-se2   "/bin/sh -c 'exec $O"   16 minutes ago      Up 16 minutes (healthy)   0.0.0.0:1521->1521/tcp, 0.0.0.0:5500->5500/tcp   docker_oracle

せっかくなのでコンテナ内からOracleに接続してみましょう。

$ docker exec -it docker_oracle /bin/sh
$ sqlplus /nolog
$ SQL> connect sys/cO11MX9C5Bk=1@ORCLCDB as sysdba
Connected.
SQL> select SYSDATE from DUAL;

SYSDATE
---------
26-SEP-19

SQL> 

Oracleサーバの構築はここまでです。*6

実行

さて先程構築したOracleに接続してSYSDATEをSELECTする単純なpythonを書きます。

#!/usr/bin/env python3

import cx_Oracle

print(cx_Oracle.clientversion())

# パスワードは勝手に払い出される
conn = cx_Oracle.connect(user='sys', password='cO11MX9C5Bk=1',
                            dsn='localhost:1521/ORCLCDB', mode=cx_Oracle.SYSDBA)
curs = conn.cursor()
curs.execute('select SYSDATE from DUAL')
row = curs.fetchone()
print(f'Hello Oracle ! {row[0]}')

このまま実行するとクライアントライブラリがないエラーがでます。

$ pipenv run ./main.py 
Traceback (most recent call last):
  File "./main.py", line 5, in <module>
    print(cx_Oracle.clientversion())
cx_Oracle.DatabaseError: DPI-1047: Cannot locate a 64-bit Oracle Client library: "dlopen(libclntsh.dylib, 1): image not found". See https://oracle.github.io/odpi/doc/installation.html#macos for help

Oracleの公式*7からライブラリをダウンロードして任意の場所に展開します。

  • instantclient-basic-macos.x64-19.3.0.0.0dbru.zip
  • instantclient-sdk-macos.x64-19.3.0.0.0dbru.zip

今回は $HOME/opt/oracle/instantclient_19_3 としました。 LD_LIBRARY_PATH も通しておきましょう。

$ pipenv run ./main.py
(19, 3, 0, 0, 0)
Hello Oracle ! 2019-09-26 02:16:43

無事動きました。ここまで前フリでした。

Dockerfileを書く

ようやくDockerに手を付けます。

上記のpython環境をDockerfileで記述します。

まずはpython3.7およびpipenvの入ったイメージを作成します。
ベースイメージは python:3.7-slim-buster としました。*8*9

0版

##
# Building runtime image.
##
FROM python:3.7-slim-buster

WORKDIR /usr/local/app

ADD Pipfile .
ADD Pipfile.lock .
ADD main.py .

RUN apt-get update -y \
    && apt-get -y install \
    && pip install --no-cache-dir pipenv \
    && pipenv install --deploy

ENTRYPOINT ["pipenv", "run"]
$ docker build . -t cx_oracle-slim # 出力は省略
$ docker images
REPOSITORY          TAG                 IMAGE ID            CREATED              SIZE
cx_oracle-slim      latest              e7b3dc4bafad        About a minute ago   239MB

この時点でのイメージサイズは 239MB です。

課題と対応

さてこの状態ではinstantclientが入っていないため、当然実行時にエラーとなります。

docker run -it  cx_oracle-slim ./main.py
Traceback (most recent call last):
  File "./main.py", line 5, in <module>
    print(cx_Oracle.clientversion())
cx_Oracle.DatabaseError: DPI-1047: Cannot locate a 64-bit Oracle Client library: "libclntsh.so: cannot open shared object file: No such file or directory". See https://oracle.github.io/odpi/doc/installation.html#linux for help

ではDockerfileでinstantclientを取得できるようにしたいところですが、Oracleのリソース取得にはライセンスの同意が必要です。
つまり、wgetcurlでの取得が面倒です。

対処としては以下のような方策が考えられます。

  1. instantclientのzipをgitにコミットしておきイメージビルド時に展開する。一番手軽ではありますが、gitに大きなバイナリ*10をコミットするのは望ましくありません。

  2. instantclientのzipをコードとは異なるgitリポジトリにコミットしておく。コードとは異なるリポジトリにzipのみをコミットしておき、ビルド時にcloneしておく方策です。 コードのリポジトリはキレイになりますが、どこにリポジトリを作るのか、バージョンアップはどうするのか、という将来の課題を残します。*11

  3. Oracle公式イメージをベースにしてpython環境を構築する。Oracle公式がgithubで公開しているイメージ oraclelinux:x-slim *12を利用すると、ライセンスの同意などを回避できる模様。素のイメージは368MBです。

$ docker images
REPOSITORY             TAG                 IMAGE ID            CREATED             SIZE
oracle/instantclient   19                  bdf41080d895        4 seconds ago       368MB

1版

運用保守を鑑みて、1,2は捨て、3の方針としました。
このときのDockerfileは以下です。

FROM oraclelinux:7-slim

ARG release=19
ARG update=3

RUN yum -y install oracle-release-el7 \
    && yum-config-manager --enable ol7_oracle_instantclient \
    && yum -y install \
        oracle-instantclient${release}.${update}-basic \
        oracle-instantclient${release}.${update}-devel \
    && rm -rf /var/cache/yum

WORKDIR /usr/local/app

ADD Pipfile .
ADD Pipfile.lock .

ENV LC_ALL=en_US.UTF-8

RUN yum update -y \
    && yum -y install \
        libaio \
        python36 \
    && rm -rf /var/cache/yum/* \
    && yum clean all \
    && pip3 install --no-cache-dir pipenv \
    && pipenv install --deploy

ADD main.py .

ENTRYPOINT ["pipenv", "run"]

Dockerで立てたOracleサーバと通信するためにdocker run時にlinkしてやる必要があります。
main.pyの接続先名を localhost から ora_server に置き換えたのち実行します。

$ docker run -it --link docker_oracle:ora_server cx_oracle-slim ./main.py
(19, 3, 0, 0, 0)
Hello Oracle ! 2019-09-26 04:51:55

無事当初の目的である、Docker内からOracleと通信が可能になりました。

課題

しかしながら、oraclelinux自体がCentOSをベースにしているようで、yumでは2019-09-26現在python37は入りません。
python36を妥協して使うにしても出来上がったイメージのサイズは 685MB となかなかなサイズとなりました。
pythonのライブラリが増えてきた時を考えるともう少しダイエットさせたい気持ちがあります。

軽量化の取り組み

さてようやく本題です。

満たしたい事項を箇条書きにします。

  • python37を使いたい
  • instantclientのバイナリを自前で管理したくない
  • イメージサイズは小さくしたい

上記を解決するために、Dockerのmulti-stage buildsを利用します。

疲れてきたのでいきなり最終的なDockerを示します。

最終版

##
# Buidling intermediate image.
#  instantclientのみほしいが、wgetなどではユーザ認証が必要になるため、公式のイメージから拝借する
##
FROM oraclelinux:7-slim AS oracle-client

ARG release=19
ARG update=3

RUN yum -y install \
        oracle-release-el7 \
    && yum-config-manager --enable ol7_oracle_instantclient \
    && yum -y install \
        oracle-instantclient${release}.${update}-basic \
        oracle-instantclient${release}.${update}-devel \
    && rm -rf /var/cache/yum

##
# Building runtime image.
##
FROM python:3.7-slim-buster
COPY --from=oracle-client /usr/lib/oracle /usr/lib/oracle

## XXX: バージョン指定がバラけてる
ARG release=19
ARG update=3

ENV LD_LIBRARY_PATH=/usr/lib/oracle/${release}.${update}/client64/lib

WORKDIR /usr/local/app

ADD Pipfile .
ADD Pipfile.lock .

RUN apt-get update -y \
    && apt-get -y install \
        --no-install-recommends \
        libaio1 \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* \
    && pip install --no-cache-dir pipenv\
    && pipenv install --system --deploy \
    && pip uninstall -y \
        pipenv \
        virtualenv-clone \
        virtualenv

ADD main.py .

ENTRYPOINT ["./main.py"]

最終版のイメージサイズは426MBと、第1版の40%減となりました。

 docker images
REPOSITORY             TAG                 IMAGE ID            CREATED             SIZE
cx_oracle-slim         latest              bd5a907578db        6 seconds ago       426MB

トピック

以下最終版に至るまでのトピックです。

まず oraclelinux:7-slim から実行に必要な必要な資材を特定しました。
oracle-instantclient${release}.${update}-xxx/usr/lib/oracle 配下にyumでインストールされるようでした。
oraclelinux:7-slimAS oracle-client としておき、こちらを COPY --from=/usr/lib/oracle でpython37のベースイメージに持ってきます。

この時点でイメージサイズは459MBと、第1版よりも200MB以上小さくなりました。また、python3.7が使える環境です。

docker images
REPOSITORY             TAG                 IMAGE ID            CREATED             SIZE
cx_oracle-slim         latest              9377e5551843        6 seconds ago       459MB

/usr/lib/oracle 配下のファイルは全体で225MB、cx_Oracleにはほぼ必須のもので、削れるにしてもojdbc8.jarなどの4MBでしたのでDockerfileの可読性を踏まえてこれ以上は追求しないこととしました。

次にapt, pipenv周りを削ります。

apt-getでは --no-install-recommends を付与することで余計なパッケージのインストールを抑止できますが、今回は効果がありませんでした。*13

pipenvは仮想環境を構築しますが、今回はDocker内のためシステムにインストールされるPythonは単一のバージョンです。したがって仮想環境用のバイナリをコピーする必要はありません。
--system を付与することで、pipenvに書かれたパッケージをsystemにインストールしてくれます。 さらに、pipenvはinstall実行後に不要となりますので、pipからも削除可能です。これで更に30MB削減です。*14

以上です。

*1:https://oracle.github.io/python-cx_Oracle/

*2:プロダクションではSQLAlchemyを経由するのが一般的だと思うが今回は使わない。

*3:pipenvは各自入れておいてください。

*4:Oracle Cloudで楽に済ませようとしたらユーザ登録でカード決済がなぜか通らなくて泣いた。

*5:https://www.oracle.com/database/technologies/oracle-database-software-downloads.html

*6:小一時間かかった…。

*7:Instant Client for macOS (Intel x86)

*8:The best Docker base image for your Python application (July 2019)

*9:当初 `python:3.7-alpine` で試みましたがglibc周りで断念。

*10:zipは75MB程度ある

*11:経験として誰も管理せず腐る傾向にある。

*12:https://github.com/oracle/docker-images/blob/master/OracleInstantClient/dockerfiles/19/Dockerfile

*13:今後必要なパッケージが増える際に効いてくるはずです

*14:Pipenv と Docker を使った開発環境のベストプラクティス - kawasin73のブログ