CIDockerGitWindows

インフラCI実践ガイドを試してみる⑪【より品質の高い成果物を作る(後半)】

CI

購入した書籍「インフラCI実践ガイド」を試した際のメモの続きです。

第9章の「より品質の高い成果物を作る」です。第9章の後半になります。

システムテストを行う演習を通じて、運用に関連するノウハウを習得していくという内容ですが、かなり難易度が高めの内容になっていると思います。

これをすんなり理解できればインフラCIを理解したといってもいいのではないでしょうか。

9.3 コンテナイメージ化した成果物

イメージ化した成果物を再利用する一例として、演習で使用したKetchupとnginxをコンテナイメージの成果物とする方法を紹介します。

まずは、パイプラインの全体像を確認したうえで、コンテナのビルド、そしてイメージのデプロイとテストという流れを確認します。

9.3.1 パイプラインの概要

コンテナイメージを成果物とする場合は、コンテナイメージをビルドするためのDockerfileと、それを環境にデプロイするためのPlaybookが必要になります。

ポイントは以下となります。
・Dockerfileに対しても規約チェックが必要となる
・Commitしてパイプラインが起動して、規約/構文チェック
・イメージがビルドされる
・テスト前のイメージにはコンテナレジストリへ「devel」というタグをつけて登録しておく、併せてCommitIDを関連付けしておく
・ユニットテストではNginxとKetchupイメージからそれぞれのコンテナを起動させて実施
・インテグレーションテストでは2つのコンテナを連携させてシステムを起動してテストを実施
・テストを確認できたら、最後にコンテナレジストリに登録されているタグを「devel」から「latest」として登録し、メンバーとシステムに「このバージョンがテストを確認した最新版」だと通知します。

スケールを想定した環境でコンテナを起動する場合は、Kubernetsなどの活用も検討する必要があります。その場合はコンテナをどのようのサービスとして稼働させるか、というコンテナオーケストレーター用の構成定義ファイルが開発項目に加わります

9.3.2 Dockerfileを利用したコンテナのビルド

KetchupコンテナイメージのDockerfile

これまでは、それぞれが展開すべきサーバー(test-ketchup、test-ketchup-nginx)に対してPlaybookを実行していましたが、今度はコンテナイメージをビルド化する時点でこれらを実行します。

つまりDockerfile内でPlaybookの実行を行い、Ketchup/Nginx用のコンテナイメージを作成します。

まずはKetchupのDockerfileで行われている内容を確認します。

[root@infraci infraci]# cd ~/ketchup-vagrant-ansible/
[root@infraci ketchup-vagrant-ansible]# cat ./flexible_artifacts/ketchup/Dockerfile
FROM centos:7

EXPOSE 80/tcp
ENV ANSIBLE_VERSION 2.4.2.0
ENV TARGET_APP ketchup
ENV GITLAB_IP 192.168.33.10
ENV GITLAB_REPO ketchup-vagrant-ansible

RUN rm -f "/lib/systemd/system/multi-user.target.wants/*"; \
    rm -f "/etc/systemd/system/*.wants/*"; \
    rm -f "/lib/systemd/system/local-fs.target.wants/*"; \
    rm -f "/lib/systemd/system/sockets.target.wants/*udev*"; \
    rm -f "/lib/systemd/system/sockets.target.wants/*initctl*"; \
    rm -f "/lib/systemd/system/basic.target.wants/*"; \
    rm -f "/lib/systemd/system/anaconda.target.wants/*"; \
    rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7; \
    yum install -y epel-release; \
    yum install -y git; \
    yum install -y "ansible-${ANSIBLE_VERSION:?}"; \
    git clone "http://${GITLAB_IP}/root/${GITLAB_REPO}.git";

WORKDIR /${GITLAB_REPO}
RUN ansible-playbook -e 'artifact=containered' -i ./flexible_artifacts/hosts/ketchup/inventory -l ${TARGET_APP} ./${TARGET_APP}.yml

WORKDIR /
RUN rm -fr /"${GITLAB_REPO:?}"

VOLUME ["/sys/fs/cgroup","/bin"]
WORKDIR /opt/ketchup
ENTRYPOINT ["/opt/ketchup/ketchup","start"]

前半ではAnsibleやGitなどの必要なものをインストールしており、後半でketchupの設定をPlyabook経由で行っています。

注目すべき点はansible-playbookコマンド実行時の変数としてartifact=containerdを指定している点です。

このartifact変数はコンテナイメージを成果物にする際に不要なタスクの条件判定に使用されます。実際にこの条件が指定されているタスクは、ハンドラーに含まれる次のタスクです。

[root@infraci ketchup-vagrant-ansible]# cat ./roles/ketchup/handlers/main.yml
---
- name: restart ketchup
  systemd:
    name: "{{ ketchup_init_name }}"
    enabled: yes
    state: restarted
  when: artifact|default("playbook") != "containered"
  become: True

[root@infraci ketchup-vagrant-ansible]# cat ./roles/nginx/handlers/main.yml
---
- name: restart nginx
  systemd:
    name: "{{ nginx_init_name }}"
    state: restarted
  when: artifact|default("playbook") != "containered"
  become: True

構成定義ファイルを成果物とした場合に、構築と起動を同時に行うためのタスクです。

全章まではこれらのプロセス起動タスクのおかげで、構成定義ファイルを実行すると同時にKetchupやNginxが起動され、アプリケーションにアクセスすることができました。

しかし、コンテナイメージを作成する場合は、プロセスの起動タスクまで実施してはいけません。これは、成果物をイメージとした場合の特徴のひとつである「構築と起動の分業」です

実際にketchupの構築はイメージ内で行われますが、プロセスの起動に関しては、イメージをデプロイするDocker Engine側から任意のタイミングで実行できる必要があります。

「ENTRYPOINT [“/opt/ketchup/ketchup”,”start”]」でDocker Engine上からイメージが実行されることを確認しておきます。

NginxコンテナイメージのDockerfile

[root@infraci ketchup-vagrant-ansible]# cat ./flexible_artifacts/ketchup_nginx/Dockerfile
FROM centos:7

ENV ANSIBLE_VERSION 2.4.2.0
ENV TARGET_APP ketchup_nginx
ENV GITLAB_IP 192.168.33.10
ENV GITLAB_REPO ketchup-vagrant-ansible

RUN rm -f "/lib/systemd/system/multi-user.target.wants/*"; \
    rm -f "/etc/systemd/system/*.wants/*"; \
    rm -f "/lib/systemd/system/local-fs.target.wants/*"; \
    rm -f "/lib/systemd/system/sockets.target.wants/*udev*"; \
    rm -f "/lib/systemd/system/sockets.target.wants/*initctl*"; \
    rm -f "/lib/systemd/system/basic.target.wants/*"; \
    rm -f "/lib/systemd/system/anaconda.target.wants/*"; \
    rpm --import /etc/pki/rpm-gpg/RPM-GPG-KEY-CentOS-7; \
    yum install -y epel-release; \
    yum install -y git; \
    yum install -y ansible-"${ANSIBLE_VERSION:?}"; \
    git clone "http://${GITLAB_IP}/root/${GITLAB_REPO}.git";

WORKDIR /${GITLAB_REPO}
RUN ansible-playbook -e 'artifact=containered' -i ./flexible_artifacts/hosts/ketchup/inventory -l "${TARGET_APP}" ./"${TARGET_APP}".yml

WORKDIR /
RUN rm -fr /"${GITLAB_REPO:?}"

# forward request and error logs to docker log collector
RUN ln -sf /dev/stdout /var/log/nginx/access.log \
        && ln -sf /dev/stderr /var/log/nginx/error.log

EXPOSE 80/tcp
STOPSIGNAL SIGTERM
CMD ["nginx", "-g", "daemon off;"]

インベントリファイル内でketchup_host=ketchupという変数をしていています。

[root@infraci ketchup-vagrant-ansible]# cat ./flexible_artifacts/hosts/ketchup/inventory
(略)

[all:vars]
ketchup_host=ketchup
ketchup_nginx_host=ketchup_nginx
ketchup_port=80

(略)

[root@infraci ketchup-vagrant-ansible]# cat ./roles/nginx/templates/ketchup.conf.j2
server {
    listen       {{ nginx_http_port }};
    server_name  localhost;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;

    location / {
        proxy_pass    http://{{ ketchup_host }}/;
    }
}

これまではインベントリを利用して静的にIPアドレスを割り当てていたため問題はありませんでした。

しかし、コンテナのように起動するまでIPアドレスが特定できない環境では、動的にIPアドレスを取得できるように、IPアドレスではなくホスト名に変更しなければなりません。

つまりこのketchup_hostにどのような仕組みで動的に変更されるIPアドレスを指定できるかによって、コンテナイメージの柔軟性が決まります。

今回の演習ではDockerのLink機能を利用して、この動的なアドレスに対する設定を行います

Link機能とは、起動するコンテナに対して指定したコンテナ名の環境変数と/etc/hostsを注入できる機能です。

ここではketchup_host変数に対して、Link機能で指定するKetchupのホスト名をあらかじめ設定しておきます。さらにketchupというホスト名で、ketchupコンテナのIPアドレスが取得できるようにNginx起動時に指定します。

コンテナイメージのビルド

実際にコンテナをbuildしますが、前と同様にansibleのバージョンがなくてエラーになってしまったので、バージョン指定をなくしてます。
→最新バージョン(ansible2.9)だとketchup_nginx作成でエラーになってしまいました。dどうもPlaybookの記載ルール変更が原因っぽいです。そのため書籍で指定されているansible2.4系を使うように変更しました。

やり方は以下にまとめていますので参考にどうぞ

[root@gitlab-runner root]# git clone http://192.168.33.10/root/ketchup-vagrant-ansible.git
[root@gitlab-runner ketchup-vagrant-ansible]# cd ~/ketchup-vagrant-ansible/
[root@gitlab-runner ketchup-vagrant-ansible]# docker build --force-rm=true -t test/ketchup:0.1 ./flexible_artifacts/ketchup/

いろいろありましたが、なんとかコンテナイメージが作成できました。

[root@gitlab-runner ketchup-vagrant-ansible]# docker images
REPOSITORY                                                   TAG                 IMAGE ID            CREATED             SIZE
test/ketchup_nginx                                           0.1                 2aed70a83703        19 minutes ago      603MB
test/ketchup                                                 0.1                 07beb2e9b225        2 hours ago         673MB
[root@infraci infraci]# cd ~/ketchup-vagrant-ansible/
[root@infraci ketchup-vagrant-ansible]# cat ./flexible_artifacts/.gitlab-ci_containered.yml
---
stages:
  - lint
  - build
  - unit_test
  - int_deploy
  - int_test

variables:
  GL_KETCHUP_NAME: ketchup
  GL_KETCHUP_IMAGE: ${CI_REGISTRY}/${CI_PROJECT_PATH}/${GL_KETCHUP_NAME}
  GL_KETCHUP_NGINX_NAME: ketchup_nginx
  GL_KETCHUP_NGINX_IMAGE: ${CI_REGISTRY}/${CI_PROJECT_PATH}/${GL_KETCHUP_NGINX_NAME}

before_script:
  - export BUILD_TAG="devel-$(echo $CI_COMMIT_SHA | cut -c1-8)"

Lint_Check:
  stage: lint
  image:
    name: hadolint/hadolint
  script:
    - hadolint --ignore="SC2086" - < ./flexible_artifacts/${GL_KETCHUP_NAME}/Dockerfile
    - hadolint - < ./flexible_artifacts/${GL_KETCHUP_NGINX_NAME}/Dockerfile
  tags:
    - docker

Build_Ketchup_Image:
  stage: build
  image: docker:stable
  script:
    - docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
    - docker build --force-rm=true -f ./flexible_artifacts/${GL_KETCHUP_NAME}/Dockerfile -t ${GL_KETCHUP_IMAGE}:${BUILD_TAG} .
    - docker push ${GL_KETCHUP_IMAGE}
  tags:
    - docker

Build_Ketchup_Nginx_Image:
  stage: build
  image: docker:stable
  script:
    - docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
    - docker build --force-rm=true -f ./flexible_artifacts/${GL_KETCHUP_NGINX_NAME}/Dockerfile -t ${GL_KETCHUP_NGINX_IMAGE}:${BUILD_TAG} .
    - docker push ${GL_KETCHUP_NGINX_IMAGE}
  tags:
    - docker

パイプラインでのビルドステージでは、コンテナイメージにタグを付け、Gitlab Container Registoryに直接保存しています。Gitlabのパイプラインを利用している場合は、Gitlab Container Registoryを利用することによって、接続トークンやレジストリの指定が簡単に行えます。

構築と起動の分離

コンテナを含め、イメージ化した成果物を取り扱う場合は、即座に実行可能な状態で保存しておく必要があります。ここで言う実行可能な状態とは「サービスの起動を任意で行える状態」です。つまり、アプリケーション実行環境の標準化やバックアップを木庭としたイメージ化ではなく、構築と起動を分離し、サービス起動を任意のタイミングで行うことにより、すばやくスケールアウトできる成果物を提供することを目的としています。

9.3.3 Playbookによるコンテナのデプロイ

デプロイ方法が環境ごとに異なってしまう場合は、標準的なデプロイ方法を事前に確立しておくことが重要です。

これを解決する手段のひとつがPlaybookによるデプロイの標準化です。

環境に適したデプロイ

今回のデプロイ用のPlaybookでは、タグによってコンテナの起動と削除が管理できるように設定しています。

[root@infraci ketchup-vagrant-ansible]# ansible-playbook --list-tasks ./flexible_artifacts/manage_ketchup_app.yml
 [WARNING]: Could not match supplied host pattern, ignoring: gitlab-runner


playbook: ./flexible_artifacts/manage_ketchup_app.yml

  play #1 (gitlab-runner): Manage Ketchup Containers    TAGS: []
    tasks:
      Cleanup running conatainer        TAGS: [absent, develop]
      Run ketchup container     TAGS: [develop, started]
      Run ketchup_nginx container       TAGS: [develop, started]

[root@infraci ketchup-vagrant-ansible]# cat  ./flexible_artifacts/.gitlab-ci_containered.yml
Init_Deploy_Kethup:
  stage: int_deploy
  image:
    name: irixjp/lint-rules:latest
    entrypoint: [""]
  script:
    - cd ./flexible_artifacts/
    - ansible-playbook -i ./hosts/ketchup/inventory -t develop -e "KETCHUP_IMAGE=${GL_KETCHUP_IMAGE}" -e "KETCHUP_NGINX_IMAGE=${GL_KETCHUP_NGINX_IMAGE}" -e "IMAGE_VER=${BUILD_TAG}" ./manage_ketchup_app.yml -vv
  after_script:
    - sleep 10
  tags:
    - docker

ここであえてstartedやabsentタグを指定していないのは、テスト環境上に同じ名前のコンテナが複数立ち上がり、エラーを起こすことがないように削除と作成を順番に行っているためです。

パイプライン上でのデプロイ

事前にansibleのバージョンも2.4でインストールできるように変更しておきます。

[root@infraci ketchup-vagrant-ansible]# vi ./flexible_artifacts/ketchup/Dockerfile
[root@infraci ketchup-vagrant-ansible]# vi ./flexible_artifacts/ketchup_nginx/Dockerfile
    yum install -y https://releases.ansible.com/ansible/rpm/release/epel-7-x86_64/ansible-2.4.6.0-1.el7.ans.noarch.rpm; \
#    yum install -y ansible-"${ANSIBLE_VERSION:?}"; \
    git clone "http://${GITLAB_IP}/root/${GITLAB_REPO}.git";

.gitkab-ci.ymlを置き換えてpushしてパイプラインを回してみます。

[root@infraci ketchup-vagrant-ansible]# mv -v .gitlab-ci.yml .gitlab-ci_practice.yml
[root@infraci ketchup-vagrant-ansible]# cp -v ./flexible_artifacts/.gitlab-ci_containered.yml  .gitlab-ci.yml
[root@infraci ketchup-vagrant-ansible]# git add .gitlab-ci.yml
[root@infraci ketchup-vagrant-ansible]# git commit -a -m "ketchup container build and deploy practice"
[root@infraci ketchup-vagrant-ansible]# git push

Gitlab Container Resistoryにコンテナが登録されていることを確認します。

さらにgitlab-runner上にコンテナがデプロイされて起動していることを確認します。

[root@gitlab-runner ~]# docker ps -a
CONTAINER ID        IMAGE                                                                          COMMAND                  CREATED             STATUS                  PORTS                NAMES
4f5bb0cd71ee        192.168.33.10:4567/root/ketchup-vagrant-ansible/ketchup_nginx:devel-c3af1d9a   "nginx -g 'daemon of…"   15 minutes ago      Up 15 minutes           0.0.0.0:80->80/tcp   ketchup_nginx
ed63e2ad5078        192.168.33.10:4567/root/ketchup-vagrant-ansible/ketchup:devel-c3af1d9a         "/opt/ketchup/ketchu…"   15 minutes ago      Up 15 minutes           80/tcp               ketchup

デプロイ時の動的な状態取得

Link機能を使ってデプロイしているのでnginxサーバ単独ではketchupのホスト名が解決できないため起動できません。

[root@gitlab-runner ~]# docker images
REPOSITORY                                                      TAG                 IMAGE ID            CREATED             SIZE
192.168.33.10:4567/root/ketchup-vagrant-ansible/ketchup         devel-c3af1d9a      4806914b16d2        28 minutes ago      610MB
192.168.33.10:4567/root/ketchup-vagrant-ansible/ketchup_nginx   devel-c3af1d9a      2aed70a83703        2 hours ago         603MB

[root@gitlab-runner ~]# docker run --rm --name test_ketchup_nginx 192.168.33.10:4567/root/ketchup-vagrant-ansible/ketchup_nginx:devel-c3af1d9a
2020/06/30 22:14:54 [emerg] 1#0: host not found in upstream "ketchup" in /etc/nginx/conf.d/ketchup.conf:9
nginx: [emerg] host not found in upstream "ketchup" in /etc/nginx/conf.d/ketchup.conf:9

9.3.4 イメージ化した成果物のテスト

順序ステージ実行されるジョブ実行される内容
1LintLint_ChackDockerfileの規約準拠チェック
2buildBuild_Ketchup_Image
Build_Ketchup_Nginx_Image
コンテナイメージのビルド
3unit_testUnit_test_Ketchupユニットテスト
4int_deployInit_Deploy_Ketchupテスト環境にコンテナをデプロイ
5int_testInit_Test_Ketchup稼働しているKetchupコンテナのインテグレーションテスト

Dockerfileの規約準拠チェック

「Haskell Dockerfile Linter」というDockerfileの規約準拠チェックツールを利用します。

Lint_Check:
  stage: lint
  image:
    name: hadolint/hadolint
  script:
    - hadolint --ignore="SC2086" - < ./flexible_artifacts/${GL_KETCHUP_NAME}/Dockerfile
    - hadolint - < ./flexible_artifacts/${GL_KETCHUP_NGINX_NAME}/Dockerfile
  tags:
    - docker

hadolintコマンドに対してDockerfileの内容を流し込むだけで規約準拠のチェックができます。

「SC2086」というルールを除外してます。「SC2086」ではコマンド内に単語分割やグロブ(ワイルドカードを含むファイル指定)が含まれている場合は、ダブルクォーテーションで囲まなければいけないというルールです。

テストにおけるコンテナの利用

テスト環境としてコンテナを利用することのメリットのひとつは「毎回クリーンな環境でテストを実行できる」ことです。

ユニットテストの実装

Unit_Test_Ketchup:
  stage: unit_test
  image:
    name: irixjp/lint-rules:latest
    entrypoint: [""]
  script:
    - cd ./flexible_artifacts/
    - ansible-playbook -i ./hosts/ketchup/inventory -t develop -e "KETCHUP_IMAGE=${GL_KETCHUP_IMAGE}" -e "KETCHUP_NGINX_IMAGE=${GL_KETCHUP_NGINX_IMAGE}" -e "IMAGE_VER=${BUILD_TAG}" ./manage_ketchup_app.yml -vv
    - cd ../tests/
    - ansible-playbook -e 'artifact=containered' -e "ansible_become=false" -e "ansible_connection=docker" -i ../flexible_artifacts/hosts/docker.py -i ../flexible_artifacts/hosts/ketchup/inventory ./ketchup_test.yml -vv
    - ansible-playbook -e 'artifact=containered' -e "ansible_become=false" -e "ansible_connection=docker" -i ../flexible_artifacts/hosts/docker.py -i ../flexible_artifacts/hosts/ketchup/inventory ./ketchup_nginx_test.yml -vv
  after_script:
    - cd ./flexible_artifacts/
    - ansible-playbook -i ./hosts/ketchup/inventory -t absent ./manage_ketchup_app.yml -vv
  tags:
    - docker

実施している内容は以下です。

①テスト用のコンテナを作成
②「manage_ketchup_app.yml」でビルドしたコンテナを起動
③ユニットテストの実施(ketchup_test.ymlとketchup_nginx_test.yml)

起動したコンテナに対してどのように接続するのか。コンテナでは仮想マシンのようにssh接続はしません。

Ansibleの「Connection Plugin」を利用します。ユニットテスト内に記載のある「”ansible_connection=docker”」によって、テスト実行用のコンテナから、テスト対象のコンテナへの接続をAPI経由で行っています。

さらにインベントリを「../flexible_artifacts/hosts/docker.py」で指定してDynamic Inventoryで現在起動しているコンテナの最新情報を取得しています。

インテグレーションテストの実装

インテグレーションテストも初めにテスト用のコンテナを起動してから、テスト用の2つのコンテナを起動させて、それらをコンテナの外から接続する流れです。

Init_Test_Kethup:
  stage: int_test
  image:
    name: irixjp/lint-rules:latest
    entrypoint: [""]
  script:
    - cd ./tests/
    - ansible-playbook -i ../flexible_artifacts/hosts/ketchup/inventory -e "ketchup_nginx_host=192.168.33.11" ./int_test_ketchup_nginx.yml -vv
  tags:
    - docker

9.4 まとめ

成果物の品質を向上させる仕組みの活用は、インフラCIを実践するための必須作業です。

感想

かなり難しい内容でしたが、なんとか全体の流れとしては理解できました。Gitlab Container Registryは利用したことがなかったのでもう少し勉強してみたいと思います。

いよいよ最後になりそうです。長かった・・・

今回は以上となります。

コメント