こんにちは。西日本テクノロジー&イノベーション室の藤田です。

年末、帰省シーズンですね。

私は実家に帰省する度、3歳の甥っ子が毎朝6時に枕元にやってきて、朝ですよーと言いながら路上で拾ったであろうどんぐりを食べさせてくる苦行に耐え忍んでいます。

さてこの記事では、Dockerコンテナ上のJenkinsでコンテナをたててその中でテストを実行する方法について説明します。

背景

AntでビルドするJavaプロジェクトのCI環境を構築する機会がありました。CIツールはJenkinsで、これはプロジェクトの制約上Dockerコンテナ上で動いています。

ここで問題となったのがデータベースです。このプロジェクトではOracle Databaseを使用しており、テストのセットアップ時のデータベース接続でSQL*Plusを実行するようになっていました。

ビルドファイルを書き換えずにテストを実行するためには、テストの実行主体にクライアント(SQL*Plus)が必要です。

ここでJDBCドライバを使用するようビルドファイルを書き換えるという選択肢が出てくるのですが、現行のビルドファイルはすでに他のプロジェクトでも使用されているため、影響範囲を勘案すると書き換えは現実的ではありませんでした。

ビルドファイルの書き換え以外には以下のような選択肢があり、4番目のDockerを使う案を採用しました。

  1. Jenkinsのコンテナにデータベースを入れる
  2. JenkinsのコンテナにSQL*Plusのみ入れ、データベースを別のコンテナとして立てる
  3. JenkinsのコンテナにSQL*Plusのみ入れ、データベースはホストに入れる
  4. JenkinsのコンテナにDockerを入れ、データベース込みのイメージを作成しておいてコンテナ上でテストを実行する

いずれの選択肢もJenkinsコンテナのイメージをビルドし直す必要があり、せっかくJenkinsのイメージをビルドし直すのであれば、Dockerをいれて毎回テスト環境用のコンテナ内でテストを実行する構成にしてしまおうと考えたためです。

前置きが長くなりましたが、この記事ではDockerコンテナ上のJenkinsで更にコンテナをたててテストを実行する環境の構築方法を記載します。

概要

構成

各ソフトウェアのバージョンは以下の通りです。

  • Jenkins 2.190.2(イメージ:jenkins / jenkins : 2.190.2
  • Oracle Database Express Edition 11.2.0.2
  • CentOS Linux release 7.7.1908 (Core)
  • Docker version 19.03.4

次に構成図を示します。Jenkins上のDockerがホストのDockerソケットをマウントし、ホストのDockerからテスト用コンテナを立ち上げるようになっています。

JenkinsのコンテナとGitBucketのコンテナはDocker Composeで起動しています。

やったこと

今回の構成を実現するためにやったことは以下の通りです。

  1. JenkinsとDockerが入ったコンテナのイメージをビルドする
  2. docker-compose.ymlを書き換えてJenkinsのイメージを切り替える
  3. Oracle Database(テスト用データベース)のイメージをビルドする
  4. Java+Ant+SQL*Plus(テストの実行主体)のイメージをビルドする
  5. テスト実行用Jenkinsfileを書く

次項から詳細な内容をかいていきます。

具体的な構築手順

1. JenkinsとDockerが入ったコンテナのイメージをビルドする

まず、JenkinsのコンテナにDockerをいれるため、Dockerfileを書いてイメージをビルドします。

ここではdockerコマンドをインストール後、jenkinsユーザーでDockerが扱えるよう、dockerグループにjenkinsユーザーを追加しています。

FROM jenkins/jenkins:2.190.2

ENV DEBIAN_FRONTEND noninteractive

USER root

RUN apt-get update -y \
&& apt-get -y install sshpass \
&& curl -fL -o docker.tgz "https://download.docker.com/linux/static/test/x86_64/docker-19.03.4.tgz" \
&& tar --strip-components=1 -xvzf docker.tgz -C /usr/bin \
&& groupadd docker \
&& gpasswd -a jenkins docker

USER jenkins

2. docker-compose.ymlを書き換えてJenkinsのイメージを切り替える

次に、Jenkinsのイメージを先程作ったイメージに差し替えるため、docker-compose.ymlを修正します。

jenkinsの設定部分だけ抜粋すると以下の形となります。

jenkins:	
    container_name: jenkins	
    image: 【Jenkinsのイメージ】	
    restart: always		
    env_file: ./common.env	
    environment:	
        JENKINS_OPTS: --prefix=/jenkins	
        no_proxy: proxy,nexus.repository	
    volumes:	
        - /data/jenkins:/var/jenkins_home	
        # ホストのdocker.sockをマウント
        - /var/run/docker.sock:/var/run/docker.sock	

ここでのポイントはdocker.sockをマウントしているところです。

ホストのdocker.sockをマウントすると、コンテナ内のdockerコマンドをホストのDocker環境から実行できるようになります。

docker.sockをマウントして立ち上げたコンテナからホスト上の他のコンテナへの操作が可能になるという点でセキュリティリスクがありますが、今回は限られたメンバーしかJenkinsを見られないことから、ホストのdocker.sockをマウントしホストのDockerデーモンを使用してコンテナ操作する方法を採用しました。

コンテナからコンテナを操作する方法として、もう一つDocker in Dockerを実現するイメージdocker : dindを使用する方法があります。

このイメージを元にprivilegedオプションをつけてコンテナを作成すると、ホストへのシステムコールが許可され、当該コンテナからホストのデバイスへのアクセスが可能となります。

dindの開発者であるJérôme Petazzoni氏のブログ記事Using Docker-in-Docker for your CI or testing environment? Think twice.では、Docker in Dockerを利用した場合のデメリットについて言及しており、CIにDocker in Dockerを利用したいと考えているユーザーに向けて、Docker in Dockerの代替案としてdocker.sockをマウントする方法を挙げています。

3. Oracle Database(テスト用データベース)のイメージをビルドする

今度はテストで使用するコンテナのイメージをビルドします。 公式で用意されているOracle Database Express Edition 11.2.0.2のDockerfileはこちらにあります。

今回は、ビルドしたイメージを元に、タイムゾーンや文字コードの設定をしています。

FROM [データベースのイメージ]

RUN echo 'TZ="Asia/Tokyo"' > /etc/sysconfig/clock && \
  cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
  echo 'LANG="ja_JP.UTF-8"' > /etc/sysconfig/i18n && \
  echo 'LC_CTYPE="ja_JP.utf8"' >> /etc/sysconfig/i18n && \
  yum reinstall -y glibc-common && \
  yum reinstall -y glibc && \
  localedef -f UTF-8 -i ja_JP ja_JP.UTF-8

4. Java+Ant+SQL*Plus(テストの実行主体)のイメージをビルドする

続いて、テストの実行主体となるコンテナのイメージを作成します。

Dockerfileは以下の通りです。

FROM oraclelinux:7-slim

ENV BASE_FILE=oracle-instantclient11.2-basic-11.2.0.4.0-1.x86_64.rpm \
    CLIENT_FILE=oracle-instantclient11.2-sqlplus-11.2.0.4.0-1.x86_64.rpm

RUN mkdir -p /root/install

COPY ${BASE_FILE} ${CLIENT_FILE} /root/install/

RUN echo 'TZ="Asia/Tokyo"' > /etc/sysconfig/clock && \
    cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    echo 'LANG="ja_JP.UTF-8"' > /etc/sysconfig/i18n && \
    echo 'LC_CTYPE="ja_JP.utf8"' >> /etc/sysconfig/i18n && \
    yum reinstall -y glibc-common && \
    yum reinstall -y glibc

# install SQL*Plus
RUN yum -y install /root/install/${BASE_FILE}  && \
    yum -y install /root/install/${CLIENT_FILE} && \
    echo /usr/lib/oracle/11.2/client64/lib > /etc/ld.so.conf.d/oracle-instantclient11.2.conf && \
    ldconfig && \
    rm -rf /root/install

ENV PATH=$PATH:/usr/lib/oracle/11.2/client64/bin

# install Java
RUN mkdir -p /usr/share/man/man1 && \
    yum install -y java-1.8.0-openjdk-devel.x86_64

# install Ant
RUN cd /opt && \
    curl -O https://archive.apache.org/dist/ant/binaries/apache-ant-1.10.1-bin.tar.gz && \
    tar zxvf apache-ant-1.10.1-bin.tar.gz

5. テスト実行用Jenkinsfileを書く

Pipelineプロジェクトを作成し、Docker Pluginを使って実行します。

Jenkinsfileでは、以下のような形でデータベースのコンテナをサイドカーとして動かしています。

Declarative Pipelineの記法で書きたかったのですが、Docker Pluginを利用してサイドカーとしてコンテナを動かしたい場合にはまだ対応していないようだったので、Scripted Pipelineの記法で書いています。

node {
    docker.image('[データベースのイメージ]').withRun('--name ${BUILD_TAG}-db --shm-size 1g -p 1521 -p 8080 -e ORACLE_PWD=password -e NLS_LANG=JAPANESE_JAPAN.UTF8 -e TZ=Asia/Tokyo -e LANGUAGE=ja_JP.ja -e LANG=ja_JP.UTF-8') { c ->
        docker.image('[テストの実行主体のイメージ]').inside("--name ${BUILD_TAG} -e NLS_LANG=JAPANESE_JAPAN.UTF8 -e TZ=Asia/Tokyo -e LANGUAGE=ja_JP.ja -e LANG=ja_JP.UTF-8 --link ${c.id}:db") {
            stage('git clone') {
                git url: 'http://example/gitbucket/git/hoge.git'
            }
            ...
        }
    }
}

参考:Using Docker with Pipeline#running-sidecar-containers

ハマったところ

Docker PluginがCMDを上書きする

当初、テスト用のイメージはJava+Ant+Databaseの入ったコンテナを1つだけたてるつもりでした。

しかし、作成したコンテナをDocker Pluginで動かしてみると、データベースの初期化がされていないことが分かりました。

データベース初期化をしているのは、Dockerfileの下記の部分です。

CMD exec $ORACLE_BASE/$RUN_FILE

改めて実行したジョブのログを見ると

[Pipeline] withDockerContainer
Jenkins seems to be running inside container 134c3014329870b8e8d0be31a69cb50e0c58e0efb9e8a23eef22ff64e6ea6522
$ docker run -t -d -u 1000:1000 --name jenkins-JOB-6 -e NLS_LANG=JAPANESE_JAPAN.UTF8 -e TZ=Asia/Tokyo -e LANGUAGE=ja_JP.ja -e LANG=ja_JP.UTF-8 --link 8b3f7ccf6ceee62b6be188c53eb423054d34f12d2ae390f752fa588999bb32ef:db -w /var/jenkins_home/workspace/JOB --volumes-from 134c3014329870b8e8d0be31a69cb50e0c58e0efb9e8a23eef22ff64e6ea6522 -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** -e ******** [image] cat

CMDが cat で上書きされていました。

Docker PluginによってCMDが上書きされ、それが原因でデータベースの初期化がされていなかったようです。

このcatはどうあがいても取り除けないようなので、先に挙げたJenkinsfileの通り、サイドカーとしてDBコンテナを起動することで解決しました。

課題

ここまで詳細な構築の方法を書いておきましたが、今回構築した環境の課題について述べていきます。

Dockerイメージが意外と大きい

テスト用のDockerイメージが案外大きかったです。データベースとテストの実行主体とで分ければ大きさが抑えられると思っていたのですが、両イメージ共に1.5GB程度となっていました。

docker.sockをマウントするセキュリティリスクが高い

Dockerコンテナ上からコンテナを操作する方法としてホストのdocker.sockをマウントしましたが、この方法だと1つのコンテナから他のコンテナが操作できる状況なのでセキュリティリスクがあります。

今回はJenkinsにアクセスできるのが限られたPJメンバーのみだったため、docker.sockをマウントする方法を選択しましたが、Jenkinsにそもそもアクセスできる人を絞れない場合はやめておいた方がいいです。

感想

色々とDockerコンテナ上のJenkinsでDockerを扱う方法を書いてきましたがかなりハマったので、同じようにコンテナ内でテストを実行してくれるGitLab CI/CDを使いたかったなというのが正直な感想です。

コンテナからコンテナを操作する方法としてDocker in Dockerとホストのdocker.sockをマウントする方法を挙げましたが、どちらもセキュリティのリスクが高いので今後は極力やりたくないです。

とはいえ、こういった環境を一から構築する経験は初めてで個人的には非常に勉強になりました。

この経験を経て、コンテナをどこまで積み重ねられるのか試してみたりと遊びの幅が広がりましたし、コンテナ技術への興味に繋がりました。


本コンテンツはクリエイティブコモンズ(Creative Commons) 4.0 の「表示—継承」に準拠しています。