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を参考にしました。

コマンド

  1. docker exec -it (Gitlab runnerのコンテナ名) gitlab-runner register -n \
  2. --url (登録先URL) --registration-token (トークン) \
  3. --name=my-runner --tag-list="(タグ)" \
  4. --executor=docker --docker-image "docker:latest" \
  5. --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

  1. include:
  2. - local: "deploy/.gitlab-ci-deploy.yml"
  3.  
  4. stages:
  5. - other
  6. - lint
  7. - test
  8. - build:step1
  9. - build:step2
  10. - deploy:web
  11. - deploy:app
  12.  
  13. checkenviroment:
  14. stage: other
  15. tags:
  16. - docker
  17. - gitlab-runner01
  18. script:
  19. - uname -a
  20. - pwd
  21. - ls -al
  22. only:
  23. variables:
  24. - $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

  1. variables:
  2. WEB_CONTAINER_NAME: nginx
  3. CONTAINER_NAME: ${CI_PROJECT_NAME}_${APP_NAME}
  4. CONTAINER_IMAGE: ${CI_REGISTRY}/${CI_PROJECT_NAMESPACE}/${CI_PROJECT_NAME}/${APP_NAME}
  5.  
  6.  
  7. .pythoninitenv:
  8. image: python:latest
  9. before_script:
  10. - pip install --upgrade pip
  11. - pip install --no-cache-dir -r requirements.txt
  12.  
  13.  
  14. pythonfilelinting:
  15. image: pipelinecomponents/flake8
  16. stage: lint
  17. tags:
  18. - docker
  19. - gitlab-runner01
  20. script:
  21. - flake8
  22. rules:
  23. - if: $JOBTORUN == "pythonfilelinting"
  24. - changes:
  25. - "*.py"
  26. - "**/*.py"
  27. allow_failure: true
  28.  
  29.  
  30. unittest:
  31. extends: .pythoninitenv
  32. stage: test
  33. tags:
  34. - docker
  35. - gitlab-runner01
  36. script:
  37. - coverage run -m pytest tests
  38. - coverage html
  39. artifacts:
  40. paths:
  41. - htmlcov/
  42. expire_in: 2 days
  43. only:
  44. variables:
  45. - $JOBTORUN == "unittest"
  46.  
  47. 続く...

ビルドジョブ

ビルドのジョブは、配布パッケージを作る’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

  1. ...続き
  2. createwheel:
  3. extends: .pythoninitenv
  4. stage: build:step1
  5. tags:
  6. - docker
  7. - gitlab-runner01
  8. script:
  9. - python setup.py bdist_wheel
  10. artifacts:
  11. paths:
  12. - dist/*.whl
  13. expire_in: 2 days
  14. only:
  15. variables:
  16. - $JOBTORUN == "build"
  17.  
  18.  
  19. dockerimagebuild:
  20. stage: build:step2
  21. tags:
  22. - docker
  23. - gitlab-runner01
  24. variables:
  25. DOCKER_DRIVER: overlay2
  26. DOCKER_TLS_CERTDIR: "/certs"
  27. services:
  28. - docker:dind
  29. before_script:
  30. - docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
  31. script:
  32. - docker build -t ${CONTAINER_IMAGE} -f ./deploy/Dockerfile .
  33. - docker push ${CONTAINER_IMAGE}
  34. only:
  35. variables:
  36. - $JOBTORUN == "build"
  37.  
  38. 続く...

デプロイジョブ

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

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

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

deploy/.gitlab-ci-deploy.yml

  1. ...続き
  2.  
  3. .deploytemplate:
  4. image: ubuntu
  5. before_script:
  6. - 'which ssh-agent || ( apt -y update && apt install -y openssh-client )'
  7. - eval $(ssh-agent -s)
  8. - mkdir -p ~/.ssh
  9. - chmod 700 ~/.ssh
  10. - echo 'echo ${SSH_PASSPHRASE}' > ~/.ssh/tmp && chmod 700 ~/.ssh/tmp
  11. - echo "${SSH_PRIVATE_KEY}" | tr -d '\r' | DISPLAY=None SSH_ASKPASS=~/.ssh/tmp ssh-add -
  12. - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
  13.  
  14.  
  15. deployweb:
  16. extends: .deploytemplate
  17. stage: deploy:web
  18. tags:
  19. - docker
  20. - gitlab-runner01
  21. script:
  22. - |
  23. ssh ${VPSUSERNAME}@${SRVIP} << EOC
  24. hostname
  25. docker container stop ${WEB_CONTAINER_NAME} || true
  26. docker container rm ${WEB_CONTAINER_NAME} || true
  27. docker image rm ${WEB_CONTAINER_NAME} || true
  28. docker pull ${WEB_CONTAINER_NAME}
  29. docker volume create socket
  30. docker run -d -v socket:/tmp --name ${WEB_CONTAINER_NAME} -p 80:80 ${WEB_CONTAINER_NAME}
  31. EOC
  32. only:
  33. variables:
  34. - $JOBTORUN == "deployweb"
  35.  
  36. deployapp:
  37. extends: .deploytemplate
  38. stage: deploy:app
  39. tags:
  40. - docker
  41. - gitlab-runner01
  42. script:
  43. - |
  44. ssh ${VPSUSERNAME}@${SRVIP} << EOC
  45. hostname
  46. docker container stop ${CONTAINER_NAME} || true
  47. docker container rm ${CONTAINER_NAME} || true
  48. docker image rm ${CONTAINER_IMAGE} || true
  49. docker login -u gitlab-ci-token -p ${CI_BUILD_TOKEN} ${CI_REGISTRY}
  50. docker pull ${CONTAINER_IMAGE}
  51. docker volume create socket
  52. docker run -d -v socket:/tmp --name ${CONTAINER_NAME} ${CONTAINER_IMAGE}
  53. EOC
  54. only:
  55. variables:
  56. - $JOBTORUN == "deployapp"

デプロイジョブの実行

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

デプロイジョブの実行

デプロイジョブの結果

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

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