🐍

PythonとCircleCIで自動テストとHerokuへの自動デプロイに入門する

2022/03/08に公開

はじめに

この記事はpipenvの環境で、pythonのテストモジュールであるpytestを、circleCI上で実行し、その後Herokuへの自動デプロイを行う方法を具体例を用いてまとめた記事です。
なお、WSL環境(ubuntu) + pipenvは導入済みであるものとします。
導入方法についてはこちらの記事などを参考にされてください。
また、恐らくDocker環境を使いますので、Docker for Windowsも必要かもしれません。

CircleCIについて

CircleCIは継続的インテグレーション(CI)や継続的デリバリー(CD)を実現するためのサービスです。
GithubなどのVCSと連携することで、開発したコードをプッシュするたびにビルドやテスト、あるいはデプロイといった一連の処理を自動化することができます。
これによって開発に付随する処理の属人化を防いだり、開発サイクルを素早く実行することができます。

今回はこのCircleCIに簡単なPythonアプリケーションで入門してみましょう。

前準備

まず初めに、今回の作業用のディレクトリを作って、pytestをインストールしておきましょう。

mkdir python_circleci # (適切なフォルダで)作業ディレクトリを作成する
cd python_circleci # 移動
pipenv --python 3 # pythonの環境を初期化する
pipenv install pytest # pytestをインストールする
code . # VScodeで開く

次に、適当な関数を用意して、その関数をテストする関数を作ります。

ソースコードの作成

テストやデプロイを行うための対象となるコードを作成します。
srcフォルダを作り、その中にcalc.pyを作ってください。単純に四則演算の関数でも書きましょう。

calc.py
def plus(a, b):
    return a + b

def sub(a, b):
    return a - b

def mul(a, b):
    return a * b

def div(a,b):
    return a // b

また、testフォルダを作り、その中にtest_calc.pyを作ってください。

test_calc.py
from src.calc import * # 関数を読み込む

def test_mul():
    assert 6 == mul(3, 2)

def test_div():
    assert 6 == div(12,2)

また、それぞれのフォルダに__init__.pyというファイルを追加してください。これはモジュールのインポートの解決に必要となります。

__init__.py
# 空でOK

ちなみに現時点ではフォルダ構成はこうなります。

.
├── Pipfile
├── Pipfile.lock
├── src
│   ├── __init__.py
│   └── calc.py
└── test
    ├── __init__.py
    └── test_calc.py

自動テストの作成

まず最初にローカルでpytestを実行してみましょう。

# 現在の環境にインストールされたpytestを実行する
# pipenv run pytest test_app.pyだとtest_app.pyを実行するが、
# 引数なしだと自動でtest_と名の付くスクリプトを実行する
pipenv run pytest 

ちゃんとテストが実行されていますね。(画像はテストコードが一つのものです)

CircleCIの設定ファイルの作成

次にcircleCIの設定ファイルを作成しましょう。
参考: https://circleci.com/docs/ja/2.0/language-python/

.circleci というフォルダを作って、その中にconfig.ymlを作ります。

config.yml
version: 2
jobs: # jobは処理のまとまり、実行環境やいくつかの処理(step)で構成される
  build: # job名
    docker: # circleci上でのdockerによる実行環境の指定
      - image: cimg/python:3.10.2 # 自分の実行環境に合わせる cimgのpythonであればpipenvがインストール済み
    steps:
      - checkout # ソースコードをgitからcheckoutする
      - run: python --version
      - run:
          name: install
          command: pipenv install # Pipfileを用いてインストールする
      - run:
          name: run test
          command: pipenv run pytest --junitxml=test-reports/junit.xml # pytest実行の際にテスト結果のレポートを作成する
      - store_test_results: # テスト結果をCircleCIにアップロード
          path: test-reports

ローカルでCircleCIを動かす

この時点でローカルでcircleCIを動かしてみましょう。そのためのCLIが提供されているのでインストールします。

curl -fLSs https://raw.githubusercontent.com/CircleCI-Public/circleci-cli/master/install.sh | bash

また、checkoutのステップを実行するために、この時点でgitで管理しておきましょう。

git init
git branch -M main
git add .
git commit -m "first commit"

設定ファイルを確認しましょう

circleci config validate .circleci/config.yml

次はjobを実行してみましょう

circleci local execute --job build

最後の結果を保存するところについてはパスを用意していないのでうまく動かないかと思いますが、他の部分は動いてますね。

GithubとCircleCIの連携

Githubにレポジトリを作ってpushする

それでは次はこのソースファイルをgithub上におき、そのレポジトリをCicleCIと連携しましょう。
github上でレポジトリを作成してから、以下のコマンドでpushしましょう。

git remote add origin git@github.com:ユーザー名/レポジトリ名.git
git push -u origin main

その後、CircleCI( https://circleci.com/ja/ )にログインしてください。アカウントがなければアカウントを作成してください。

githubと連携させると、レポジトリの一覧がでると思うので、今作ったレポジトリをsetupします。

すると、どのconfig.ymlを使うかという設定項目がでてきます。今回はすでにconfig.ymlを用意しているのでmainブランチのものを使いましょう。

連携した後で今セットアップしたプロジェクトを見に行くと、設定した一連の処理が実行されているのが分かります。

テストを失敗してみる

次にテストを失敗してみましょう。以下のようなテストを追加して、pushしてみてください。

test_calc.py
def test_plus():
    assert 4 == plus(1,1) # 間違えたテスト

その後、プロジェクトを確認すると、テストがちゃんと失敗していることがわかります。

自動デプロイ

これまでで、継続的にテストする方法は分かったので、次は継続的にデプロイする方法を見ていきましょう。

まずはサーバーを構築します。pythonには標準ライブラリとして軽量なhttpサーバーのモジュールがついていますので、そちらを使います。

src/server.py
# coding: utf-8

from http.server import HTTPServer
from http.server import BaseHTTPRequestHandler
from calc import *

class class1(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("User-Agent","test1")
        self.end_headers()
        # 折角なので計算もさせる
        html = "<h1>Answer is " + str(mul(6,7)) +  "</h1>"
        self.wfile.write(html.encode())

ip = '127.0.0.1'
port = 8765

server = HTTPServer((ip, port), class1)
print("access http://127.0.0.1:8765")
server.serve_forever()

Pipfileに実行コマンドを登録しましょう

Pipfile
[scripts]
serve = "python src/server.py"

すると以下のコマンドでサーバーを実行できます。ブラウザで( http://127.0.0.1:8765 )にアクセスしてみてください。

pipenv run serve


全ての宇宙の答え

Herokuにアプリをデプロイする

まずは手動でデプロイを行います。

Heroku側の設定をしましょう。herokuで新規アプリを作成してください。
アプリ名はなんでも構いません。

その後、少しソースコードを書き換えます。

herokuはポート番号を動的に割り当てるので、ポート番号を8765から下記のように書き換えましょう。

import os
# 中略

ip = '0.0.0.0'
# PORTという環境変数からherokuが割り当てたポート番号を取得する
port = int(os.environ.get('PORT', 8765)) 

server = HTTPServer((ip, port), class1)
print("Access http://0.0.0.0:" + str(port))

次に、Heroku上のアプリケーションを起動するためのコマンドの指定として、Procfileを作成します。下記のようにファイルを作成し、プロジェクトフォルダ直下に配置してください。

Procfile
web: pipenv run serve

Herokuに手動でデプロイを行うために、heroku cliをローカルにインストールします。

curl https://cli-assets.heroku.com/install-ubuntu.sh | sh

その後、herokuにログインしてください

heroku login # herokuにログインする
heroku apps # アプリ名の一覧を表示する
heroku git:remote -a アプリ名 # 先ほど作成したアプリ名へのデプロイ用レポジトリを追加する
git push heroku main # heroku上にデプロイする

すると、無事にデプロイされ、アクセスすると数字が出るはずです

CircleCIによるデプロイの自動化

これらの作業を自動化することで、最新版のソフトウェアをいつもユーザーが使うことができるようになります。
また、このデプロイプロセスを繰り返すことによって、アプリケーションをユーザーに提供できなくなるような破壊的な変更に早く気付くことが出来ます。

CircleCIからHerokuへのデプロイにはOrbを用いると便利です。
これはCircleCIが予め用意しているワークフローで、プリセットのようなものです。
Heroku用のOrbがあります(参考: https://circleci.com/developer/orbs/orb/circleci/heroku

まず、config.ymlを下記のように修正しましょう。

config.yml
version: 2.1
orbs: # heroku用のorbを使う
  heroku: circleci/heroku@1.2.6
jobs:
  deploy:
    executor: heroku/default
    steps:
      - checkout
      - heroku/install
      - run:
          command: >
            echo "The command above installs Heroku, the command below deploys.
            What you do inbetween is up to you!"
      - heroku/deploy-via-git
  build-test:
    docker: # circleci上でのdockerによる実行環境の指定
      - image: cimg/python:3.10.2 # 自分の実行環境に合わせる cimgのpythonであればpipenvがインストール済み
    steps:
      - checkout
      - run: python --version
      - run:
          name: install
          command: pipenv install # Pipfileを用いてインストールする
      - run:
          name: run test
          command: pipenv run pytest --junitxml=test-reports/junit.xml
      - store_test_results: # テスト結果をCircleCIにアップロード
          path: test-reports
workflows:
  version: 2.1
  test_and_deploy: # workflow名
    jobs:
      - build-test
      - deploy:
          requires: # buildが成功したら実行する
            - build-test

次に、HerokuへアクセスするためのAPI keyとアプリの名前をCircleCIのプロジェクト設定から設定しましょう。

設定するのはHEROKU_API_KEYHEROKU_APP_NAMEの二つの環境変数です。
HEROKU_API_KEYについては

# heroku login 後
heroku auth:token

で取得することができます。
また、HEROKU_APP_NAME はHeroku上のアプリ名です。

ではこれを使ってみようと思うので、少しだけget時の処理を書き換えて、pushしましょう。

server.py
# 中略
# 折角なので計算もさせる
- html = "<h1>Answer is " + str(mul(6,7)) +  "</h1>"
+ html = "<h1>Answer is " + str(mul(5,5)) +  "</h1>"
# コミットしてから
git push origin main

すると、デプロイが失敗したのではないでしょうか?

実は、まだ間違えているテストをそのままにしていたので、テストが通らなかったのです。下記のように、requiresを指定していることで、テストが通ったときだけデプロイ処理を行うように指定することができます。

config.yml
      - deploy:
          requires: # buildが成功したら実行する
            - build-test

間違えたテストを修正して、再度プッシュを行いましょう。

test_calc.py
def test_plus():
-    assert 4 == plus(1,1) # 間違えたテスト
+    assert 2 == plus(1,1) # 正しいテスト
# コミットしてから
git push origin main


build-testもdeployも正しく実行されていることが分かる

HerokuでOpen Appからアクセスしてみましょう。

これでデプロイを自動化することができました!

終わりに

以上までの方法で、Pythonのアプリケーションに対して、Githubと連携し自動テストと自動デプロイをさせることができるようになりました。

より学びたい方のために、追加課題を書いておきたいと思います。

といった方法にチャレンジしてみてください。

また、もしこの記事が役に立てば、食料品などを買っていただけると幸いです。

Discussion