Webアプリケーション(Flask)のCI/CD環境構築 -Gitlabも用いたデプロイ編-

CI/CD,Docker,Flask,Gitlab,Python

コンテンツ

はじめに

前回Dockerを用いて簡単にデプロイ、更新できる環境を構築しました。さらに、GitlabサービスのGitlab CI/CDを使って自動的に行えるようにします。

GitlabサービスでCI/CD環境構築を発展

Gitlab とはオープンソースのGitリポジトリをホスティングするソフトウェアです。ConoHaでVPSを借りて、オンプレミスでGitlabを運用したかったのですが借りたスペックだと快適に動きそうになかった(金欠でアップグレードできない…哀)ので、サービス版のGitlabを利用します。無償でもプライベートリポジトリが作成できたりと便利です。借りたConoHaのVPSは、Webアプリのデプロイ先、Gitlab-runnerのオンプレミス運用に用いています。



VPSの準備

ConoHaにログインして、イメージを選択してVPSを立ち上げます。サーバと言ったらCentOSの方が多いかと思うのですが今回は使い慣れているUbuntuのイメージにしました。余談ですが、今までVPSを使ったことがなかったので知らなかったのですがゲームをはじめとした色んなイメージ(Gitlab, Mattermost等も)がありました。生活が安定したら、いろいろなイメージを使ってみたいと思います。

VPSを立ち上げたらコンソールからサーバにログインして、基本的な準備をします。以下が実施したことです。

  • ユーザの作成
    • 自分がリモートアクセスなどに使うユーザ
    • Gitlab CI/CDで使うユーザ
  • リモートアクセスの設定
    • 公開鍵の登録
    • SSHの設定
    • 参考
  • Docker Engine のインストール

Gitlab runnerの準備

Gitlab CI/CDでプロジェクトのルートディレクトリに’.gitlab-ci.yml’作成し、そこに自動的に行ってほしい処理などを記述していきます。その処理を実際に実行するのがGitlab runnerとソフトウェアになります。

サービス版のGitlabでは、Shared Runner(使用時間の上限がある)が提供されていますが、今回はDockerコンテナで自分用のものを用意しました(参考 : ドキュメント)。Dockerコンテナ以外にも、パッケージでインストールしたりできます。

Gitlab runner を起動したら、プロジェクトの Specific runner または、 Group runner として登録します。登録先URLとトークンは画像の場所で確認できます。タグは、実際にこのランナーに動いてほしいといったときに識別子になります。他の引数は、runnerの使い方によるので ドキュメント1ドキュメント2を参考にしました。

コマンド

docker exec -it (Gitlab runnerのコンテナ名) gitlab-runner register -n \
  --url (登録先URL) --registration-token (トークン) \
  --name=my-runner --tag-list="(タグ)" \
  --executor=docker --docker-image "docker:latest" \
  --docker-privileged=true --docker-volumes "/certs/client"
  

プロジェクトの設定 -> CI/CD -> Runner部分の展開(Specific runnerのトークン)

グループ -> CI/CD -> Register a group runnerのプルダウン(Group runnerのトークン)

CI/CD パイプラインを作成

プロジェクトのルートに’.gitlab-ci.yml’を作成し、パイプラインを作成しました。仕事で使っていたのですが理解せずに使っていたので、とりあえずどんな環境でコマンドが実行されているのか見てみました。定義の仕方などは、公式のドキュメントを確認してください。

checkenviroment:というジョブがどんな環境か見るものです。このジョブはonly:部分のvariables:にあるようにJOBTORUNとう変数が"checkenviroment"のときに実行されます。実行されるスクリプトは、script:部分に記載されています。tags:はこのジョブを実行してほしいrunnerを指定してます。実行はプロジェクトから’CI/CD -> パイプライン -> Run pipeline’で下記の画像のようにJOBTORUNとういう変数を設定すれば実行できます。

myproject
  ├── app/
  ├── deploy/
  │   ├── 
  │   └── .gitlab-ci-deploy.yml
  ├── docker-compose.yml
  ├── tests/
  └── .gitlab-ci.yml

.gitlab-ci.yml

include:
  - local: "deploy/.gitlab-ci-deploy.yml"

stages:
  - other
  - lint
  - test
  - build:step1
  - build:step2
  - deploy:web
  - deploy:app

checkenviroment:
  stage: other
  tags:
    - docker
    - gitlab-runner01
  script:
    - uname -a
    - pwd
    - ls -al
  only:
    variables:
      - $JOBTORUN == "checkenviroment"
  

実行結果は以下の画像のようになりました。プロジェクトをフェッチしてそのディレクトリをワーキングディレクトリとしてコマンドを実行しているようです。このジョブを実行したrunnerのexcutorはdockerなので、コンテナが作成されてその中で実行されていました(結果の下の画像)。ジョブのimageを指定していないので、runner登録時のデフォルトのdockerイメージのコンテナです。

以下のようなイメージでジョブが実行されている。

パイプラインがどんな感じで実行されているか分かったので、リンティング、テスト、ビルド、デプロイのジョブも作成しました。

いろんなジョブの作成

'.gitlab-ci.yml’にジョブを全て書くと見づらくなるため、ある程度まとまった範囲でファイルを分けました。デプロイ関連(テスト、ビルド、デプロイ)は’deploy/.gitlab-ci-deploy.yml’に記述して、’.gitlab-ci.yml’から’include:’でインクルードします。今回はFlaskアプリを開発していく上でのひな型を作成したかったので、ジョブが実行される条件が’$JOBTORUN == (文字列)’になっており、手動で実行でするようになっています。実際に開発してく適切に’rules:’などを設定する必要があります。

リンティング、テストジョブ

'pythonfilelintin:’がリンティングジョブで’unittest:’がテストジョブです。’.pythoninitenv:’ジョブは’unittest:’がextendsすジョブで、他のジョブでも共通する部分なので分けて書いておきました。’pip install’の時に用いている’requirements.txt’はpytestや配布パッケージ作成時に必要なパッケージです(Flask, pytest, wheel, covergae)。

'pythonfilelintin:’はdockerイメージの’pipelinecomponents/flake8’を使って、’flake8’コマンドを実行するだけです。’rules:’でジョブが実行される条件を記述しています。基本的には’.py’ファイルが変更されるコミットで実行されますが、手動で実行するために’if: $JOBTORUN == “pythonfilelinting"'を追加しました。とりあえずpythonの書き方でどこが悪いと分かればいいので’allow_failure: true’でリントエラーが出ても後続のジョブが動くようになっています。

'unittest:’はpythonのDockerイメージを使い必要なパッケージをインストールして、’pytest’コマンドを実行するだけです。covergaeを見れるhtmlを作成し、artifactsのpathを指定してジョブが終わった後も確認できるようにしました。

deploy/.gitlab-ci-deploy.yml

variables:
  WEB_CONTAINER_NAME: nginx
  CONTAINER_NAME: ${CI_PROJECT_NAME}_${APP_NAME}
  CONTAINER_IMAGE: ${CI_REGISTRY}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}/${APP_NAME}


.pythoninitenv:
  image: python:latest
  before_script:
    - pip install --upgrade pip
    - pip install --no-cache-dir -r requirements.txt


pythonfilelinting:
  image: pipelinecomponents/flake8
  stage: lint
  tags:
    - docker
    - gitlab-runner01
  script:
    - flake8
  rules:
    - if: $JOBTORUN == "pythonfilelinting"
    - changes:
      - "*.py"
      - "**/*.py"
  allow_failure: true


unittest:
  extends: .pythoninitenv
  stage: test
  tags:
    - docker
    - gitlab-runner01
  script:
    - coverage run -m pytest tests
    - coverage html
  artifacts:
    paths:
      - htmlcov/
    expire_in: 2 days
  only:
    variables:
      - $JOBTORUN == "unittest"

続く...
  

ビルドジョブ

ビルドのジョブは、配布パッケージを作る’createwheel:’ジョブとDockerイメージをビルドする’dockerimagebuild:’ジョブの二つからなります。

'createwheel:’ジョブは、pythonのDockerイメージを使って必要なものをインストールして’python setup.py bdist_wheel’実行するだけです。作成された配布パッケージを他のジョブでも使用できるように’artifacts:’の’paths:’に記載しておき、’dockerimagebuild:’ジョブでイメージに含めます。

'dockerimagebuild:’ジョブは、Webアプリのコンテナのイメージを作成し、Gitlabのコンテナレジストリにpushします。’docker build’コマンドを使うのにこのドキュメントを参照しました。

deploy/.gitlab-ci-deploy.yml

...続き
createwheel:
  extends: .pythoninitenv
  stage: build:step1
  tags:
    - docker
    - gitlab-runner01
  script:
    - python setup.py bdist_wheel
  artifacts:
    paths:
      - dist/*.whl
    expire_in: 2 days
  only:
    variables:
      - $JOBTORUN == "build"
  


dockerimagebuild:
  stage: build:step2
  tags:
    - docker
    - gitlab-runner01
  variables:
    DOCKER_DRIVER: overlay2
    DOCKER_TLS_CERTDIR: "/certs"
  services:
    - docker:dind
  before_script:
    - docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
  script:
    - docker build -t ${CONTAINER_IMAGE} -f ./deploy/Dockerfile .
    - docker push ${CONTAINER_IMAGE}
  only:
    variables:
      - $JOBTORUN == "build"

続く...
  

デプロイジョブ

デプロイジョブは、Webサーバのコンテナをデプロイする’deployweb:’ジョブとWebアプリのコンテナをデプロイする’deployapp:’ジョブの二つから成ります。デプロイジョブは、すこし汎用的にしてデプロイ先のサーバにSSHで接続し、そこにWebサーバのコンテナとWebアプリのコンテナをデプロイします。’.deploytemplate:’はSSHが使えるようにするのと、パスワード付きの秘密鍵が簡単に使えるようにしています(参考)。

'deployweb:’はデプロイ先のサーバにSSHで接続して、dockerコマンドで、既存のWebサーバコンテナの削除、新しいイメージ取得、コンテナを起動します。

'deployapp:’も同様にデプロイ先のサーバにSSHで接続して、dockerコマンドで、既存のWebアプリコンテナの削除、新しいイメージ取得、コンテナを起動します。

deploy/.gitlab-ci-deploy.yml

...続き

.deploytemplate:
  image: ubuntu
  before_script:
    - 'which ssh-agent || ( apt -y update && apt install -y openssh-client )'
    - eval $(ssh-agent -s)
    - mkdir -p ~/.ssh
    - chmod 700 ~/.ssh
    - echo 'echo ${SSH_PASSPHRASE}' > ~/.ssh/tmp && chmod 700 ~/.ssh/tmp
    - echo "${SSH_PRIVATE_KEY}" | tr -d '\r' | DISPLAY=None SSH_ASKPASS=~/.ssh/tmp ssh-add -
    - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config


deployweb:
  extends: .deploytemplate
  stage: deploy:web
  tags:
    - docker
    - gitlab-runner01
  script:
    - | 
      ssh ${VPSUSERNAME}@${SRVIP} << EOC
      hostname
      docker container stop ${WEB_CONTAINER_NAME} || true
      docker container rm ${WEB_CONTAINER_NAME} || true
      docker image rm ${WEB_CONTAINER_NAME} || true
      docker pull ${WEB_CONTAINER_NAME}
      docker volume create socket
      docker run -d -v socket:/tmp --name ${WEB_CONTAINER_NAME} -p 80:80 ${WEB_CONTAINER_NAME}
      EOC
  only:
    variables:
      - $JOBTORUN == "deployweb"

deployapp:
  extends: .deploytemplate
  stage: deploy:app
  tags:
    - docker
    - gitlab-runner01
  script:
    - | 
      ssh ${VPSUSERNAME}@${SRVIP} << EOC
      hostname
      docker container stop ${CONTAINER_NAME} || true
      docker container rm ${CONTAINER_NAME} || true
      docker image rm ${CONTAINER_IMAGE} || true
      docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
      docker pull ${CONTAINER_IMAGE}
      docker volume create socket
      docker run -d -v socket:/tmp --name ${CONTAINER_NAME} ${CONTAINER_IMAGE}
      EOC
  only:
    variables:
      - $JOBTORUN == "deployapp"
  

デプロイジョブの実行

かなり甘い部分やシンプルすぎる部分はあるとは思いますが、デプロイまで(rules:を適切に設定すれば)自動的行えるパイプラインができました。特に、Webサーバのコンテナの設定は自動化できていないので早急にWebアプリとの連携ができるような設定に自動的に書き換えるようにしたいと思います。デプロイジョブでデプロイして、Webアプリとの連携の設定をWebサーバに加えた例が以下です。

デプロイジョブの実行

デプロイジョブの結果

実際にデプロイされているか確認

ブラウザで'IPアドレス:80/hello'にアクセス