購入した書籍「インフラ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 イメージ化した成果物のテスト
順序 | ステージ | 実行されるジョブ | 実行される内容 |
1 | Lint | Lint_Chack | Dockerfileの規約準拠チェック |
2 | build | Build_Ketchup_Image Build_Ketchup_Nginx_Image | コンテナイメージのビルド |
3 | unit_test | Unit_test_Ketchup | ユニットテスト |
4 | int_deploy | Init_Deploy_Ketchup | テスト環境にコンテナをデプロイ |
5 | int_test | Init_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は利用したことがなかったのでもう少し勉強してみたいと思います。
いよいよ最後になりそうです。長かった・・・
今回は以上となります。
コメント