最近「DockerCon」イベントで、ほとんど手間をかけずにCI/CDパイプラインをコードベースに実装する方法を中心に講演しました。このブログでは、講演で使用したデモコードと CircleCI のコンフィグについて説明し、CI/CD パイプラインをコードベースに実装する方法をデモします。

このブログでは以下について説明します。

  • Python Flask アプリケーションの簡単な単体テスト
  • プロジェクトで CircleCI のコンフィグファイルを使用してコードベースに CI/CD パイプラインを実装する方法
  • Docker イメージのビルド
  • Docker Hubへの Docker イメージのプッシュ
  • Digital Ocean サーバーの Docker コンテナでアプリケーションを実行するデプロイスクリプトの開始

前提条件

このブログの操作を始める前に、以下の設定が必要です。

  • Docker Hub アカウント
  • CircleCI ダッシュボードで Docker Hub のユーザー名とパスワードを指定する プロジェクト環境変数 の設定します。
  • クラウドサーバーへの SSH アクセス。CircleCI ポータルから自 分のアカウントに SSH キーを追加 できます。このブログでは、Digital Ocean のサーバーを使用しますが、お好きなサーバー/クラウドプロバイダーを自由に使用できます。
  • また、このアプリケーションのデプロイに使用されるデプロイスクリプトをホストサーバーに配置する必要があります。ここで使用する サンプルのデプロイスクリプト は、deploy_app.sh です。

すべての前提条件を満たしたら、次のセクションに進みましょう。

アプリ

このブログでは、シンプルな Python Flask を使用します。このプロジェクトの完全なソースコードは ここから 入手でき、git clone コマンドを使用してローカルに展開できます。このアプリは、リクエストが送信されたときに html を返却する単純な Web サーバーです。Flask アプリケーションは hello_world.py ファイルに存在します。

from flask import Flask

app = Flask(__name__)

def wrap_html(message):
    html = """
        <html>
        <body>
            <div style='font-size:120px;'>
            <center>
                <image height="200" width="800" src="https://infosiftr.com/wp-content/uploads/2018/01/unnamed-2.png">
                <br>
                {0}<br>
            </center>
            </div>
        </body>
        </html>""".format(message)
    return html

@app.route('/')
def hello_world():
    message = 'Hello DockerCon 2018!'
    html = wrap_html(message)
    return html

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

このコードの重要なポイントは、hello_world() 関数内の message 変数です。この変数により文字列値が指定され、この変数値が一致するかどうかを単体テストでテストします。

テストコード

すべてのコードをテストして、高品質で安定したコードがリリースされているかを確認する必要があります。Python には unittest という名前のテストフレームワークが付属しており、このブログではこのフレームワークを使用します。完全な Flask アプリケーションを準備できましたので次はこのアプリケーションをテストし、設計どおりに機能することを確認するための単体テストが必要です。単体テストファイル test_hello_world.py は、この hello_world.py アプリのための単体テストです。では、コードを見ていきましょう。

import hello_world
import unittest

class TestHelloWorld(unittest.TestCase):

    def setUp(self):
        self.app = hello_world.app.test_client()
        self.app.testing = True

    def test_status_code(self):
        response = self.app.get('/')
        self.assertEqual(response.status_code, 200)
    
    def test_message(self):
        response = self.app.get('/')
        message = hello_world.wrap_html('Hello DockerCon 2018!')
        self.assertEqual(response.data, message)

if __name__ == '__main__':
    unittest.main()
import hello_world
import unittest

import ステートメントを使用して hello_world アプリケーションをインポートします。これによりテストで、hello_world.py のコードにアクセスできます。次に、unittest モジュールをインポートし、アプリケーションのテストカバレッジの定義を開始します。

class TestHelloWorld(unittest.TestCase): TestHelloWorld は、テストの最小単位である基本クラス unittest.Test からインスタンス化されます。これは、特定の入力セットに対する特定の応答をチェックします。 unittest は、新しいテストケースの作成に使用できる基本クラスである TestCase を提供します。

def setUp(self):
        self.app = hello_world.app.test_client()
        self.app.testing = True

setUp() は、テストフィクスチャを準備するために呼び出されるクラスレベルのメソッドです。これは、テストメソッドを呼び出す直前に呼び出されます。この例では、app という変数を作成して定義し、hello_world.py コードの app.test_client() オブジェクトとしてインスタンス化します。

def test_status_code(self):
    response = self.app.get('/')
    self.assertEqual(response.status_code, 200)

test_status_code() はメソッドであり、テストケースをコードで記述します。。このテストケースは、Flask アプリケーションに対する get リクエストを作成し、アプリケーションの応答をキャプチャして、response 変数に入れます。 self.assertEqual(response.status_code, 200) は、response.status_code の結果の値を、GET リクエストが成功したことを示す期待値 200 と比較します。サーバーが 200 以外の status_codeで 応答した場合、テストは失敗します。

def test_message(self):
    response = self.app.get('/')
    message = hello_world.wrap_html('Hello DockerCon 2018!')
    self.assertEqual(response.data, message)

test_message() は、異なるテストケースを記述している別のメソッドです。このテストケースは、hello_world.py コードの hello_world() メソッドで定義されている message 変数の値をチェックするように設計されています。前のテストと同様に、アプリに対して GETリクエストが行われ、結果がキャプチャされ response 変数に入れられます。次の行を見てみましょう。

message = hello_world.wrap_html('Hello DockerCon 2018!')

この message 変数には、hello_world アプリで定義されている hello_world.wrap_html() ヘルパーメソッドで生成された html が割り当てられます。Hello DockerCon 2018 という文字列が wrap_html() メソッドに提供され、html に挿入されて返されます。test_message() は、アプリのメッセージ変数がこのテストケースで期待される文字列と一致するかどうかを確認します。文字列が一致しない場合、テストは失敗します。

CI/CD パイプライン

アプリケーションと単体テストについて説明しましたので、次は、継続的インテグレーション/継続的デプロイメント(CI/CD)パイプラインをコードベースに実装しましょう。CircleCI を使用すると、CI/CD パイプラインを簡単に実装できます。続行する前に、以下の操作を行っていることを確認してください。

CI/CD パイプラインの実装

プロジェクトを CircleCI プラットフォームでセットアップすると、アップストリームにプッシュされたコミットが検出され、CircleCI は config.yml ファイルで定義されたジョブを自動的に実行します。

まず、リポジトリのルートに新しいディレクトリを作成し、このディレクトリ内に yaml ファイルを作成する必要があります。新しいアセットは、プロジェクトの git リポジトリ内で、ディレクトリ:.circleci/ ファイル: config.ymlの命名スキーマに従う必要があります。このディレクトリとファイルは、基本的に CircleCI プラットフォームの CI/CD パイプラインとコンフィグを定義します。

config.yml ファイル

config.yml は、CI/CD のすべての高度な機能が取り込まれています。以下は、このサンプルファイルの使用例です。この構文でどのような処理が行われているかを簡単に説明します。

version: 2
jobs:
  build:
    docker:
      - image: circleci/python:2.7.14
        environment:
          FLASK_CONFIG: testing
    steps:
      - checkout
      - run:
          name: Setup VirtualEnv
          command: |
            echo 'export TAG=0.1.${CIRCLE_BUILD_NUM}' >> $BASH_ENV
            echo 'export IMAGE_NAME=python-circleci-docker' >> $BASH_ENV 
            virtualenv helloworld
            . helloworld/bin/activate
            pip install --no-cache-dir -r requirements.txt
      - run:
          name: Run Tests
          command: |
            . helloworld/bin/activate
            python test_hello_world.py
      - setup_remote_docker:
          docker_layer_caching: true
      - run:
          name: Build and push Docker image
          command: |
            . helloworld/bin/activate
            pyinstaller -F hello_world.py
            docker build -t ariv3ra/$IMAGE_NAME:$TAG .
            echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
            docker push ariv3ra/$IMAGE_NAME:$TAG
      - run:
          name: Deploy app to Digital Ocean Server via Docker
          command: |
            ssh -o StrictHostKeyChecking=no root@hello.dpunks.org "/bin/bash ./deploy_app.sh ariv3ra/$IMAGE_NAME:$TAG"

jobs: キーは、実行されるジョブのリストを表します。ジョブは、実行されるアクションをカプセル化したものです。実行するジョブが 1 つしかない場合は、ジョブにキー名 build: を付ける必要があります。ジョブとビルドの詳細については、 ここを参照してください。

build: キーは、いくつかの要素から構成されます。

  • docker:
  • steps:

docker: キーは、CircleCI に Docker executorを使用するように指示します。つまり、Docker コンテナを使用してビルドが実行されます。

image: circleci/python:2.7.14 は、ビルドが使用する Docker イメージを指定します。

steps:

steps: キーは、このビルドで実行されるすべてのコマンドを指定するコレクションです。最初のアクションは、基本的にビルド環境に自分のコードの git clone を実行する - checkout コマンドです。

- run: キーは、ビルド内で実行するコマンドを指定します。Run キーには name: パラメーターがあり、コマンドグループにラベルを付けることができます。たとえば、name:Run Tests は、テスト関連のアクションをグループ化し、CircleCI ダッシュボード内でビルドデータを整理および表示するのに役立ちます。

重要な注記:run ブロックは、個々のシェルまたはターミナルに相当します。構成または実行されたコマンドは、後の run ブロックでは保持されません。 ヒントとコツのセクションで説明している $BASH_ENV のワークアラウンドを使用します

- run:
    name: Setup VirtualEnv
    command: |
      echo 'export TAG=0.1.${CIRCLE_BUILD_NUM}' >> $BASH_ENV
      echo 'export IMAGE_NAME=python-circleci-docker' >> $BASH_ENV 
      virtualenv helloworld
      . helloworld/bin/activate
      pip install --no-cache-dir -r requirements.txt

この run ブロックの command: キーには、実行するコマンドのリストが含まれます。これらのコマンドにより、このビルド全体で使用されるカスタム環境変数 $TAG および IMAGE_NAME が設定されます。残りのコマンドは、python virtualenv をセットアップし、requirements.txt ファイルで指定されている必要な依存ライブラリをインストールします。

- run:
    name: Run Tests
    command: |
      . helloworld/bin/activate
      python test_hello_world.py

この run ブロックで、このコマンドがアプリケーションでテストを実行します。これらのテストが失敗すると、ビルド全体が失敗することになり、開発者はコードを修正して再コミットする必要があります。

- setup_remote_docker:
    docker_layer_caching: true

この run ブロックは、setup_remote_docker: キーを指定します。このキーの機能により、Docker Executor ジョブ内からの Docker イメージのビルド、実行、レジストリへのプッシュが可能になります。docker_layer_caching が true に設定されている場合、CircleCI は以前のジョブまたはワークフローでビルドされた Docker イメージ(レイヤー)の再利用を試みます。つまり、前のジョブでビルドしたすべてのレイヤーにリモート環境でアクセスできます。ただし、コンフィグで、docker_layer_caching: true が指定されている場合でも、クリーンな環境でジョブが実行される場合があります。

このアプリの Docker イメージをビルドし、イメージを Docker Hub にプッシュしているため、setup_remote_docker: が必要です。

- run:
    name: Build and push Docker image
    command: |
      . helloworld/bin/activate
      pyinstaller -F hello_world.py
      docker build -t ariv3ra/$IMAGE_NAME:$TAG .
      echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
      docker push ariv3ra/$IMAGE_NAME:$TAG

Build and push Docker image の run ブロックは、pyinstaller を使用してアプリケーションを単一のバイナリにパッケージ化するコマンドを指定した後に、Docker イメージのビルドプロセスに進みます。

docker build -t ariv3ra/$IMAGE_NAME:$TAG .
echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin
docker push ariv3ra/$IMAGE_NAME:$TAG

これらのコマンドは、リポジトリに含まれる Dockerfile に基づいて Docker イメージをビルドします。Dockerfile には、Docker イメージのビルド方法に関する指示が記述されています。

FROM python:2.7.14

RUN mkdir /opt/hello_word/
WORKDIR /opt/hello_word/

COPY requirements.txt .
COPY dist/hello_world /opt/hello_word/

EXPOSE 80

CMD [ "./hello_world" ]

echo $DOCKER_PWD | docker login -u $DOCKER_LOGIN --password-stdin コマンドは、CircleCI ダッシュボードで設定された $DOCKER_LOGIN および $DOCKER_PWD 環境変数を認証情報としてログインし、このイメージを Docker Hub にプッシュします。

- run:
    name: Deploy app to Digital Ocean Server via Docker
    command: |
      ssh -o StrictHostKeyChecking=no root@hello.dpunks.org "/bin/bash ./deploy_app.sh ariv3ra/$IMAGE_NAME:$TAG"

最後の実行ブロックでは、新しいコードを Digital Ocean プラットフォームのサーバーにデプロイします。リモートサーバーでデプロイスクリプトを作成していることを確認します。ssh コマンドでリモートサーバーにアクセスし、このサーバーで deploy_app.sh スクリプトを実行し、ariv3ra/$IMAGE_NAME:$TAG を指定します。これにより、Docker Hub からプルおよびデプロイするイメージが指定されます。

ジョブが正常に完了すると、config.yml で指定したターゲットサーバーで新しいアプリケーションが実行されます。

まとめ

このブログでは、CI/CD パイプラインをコードとして実装する方法について説明しました。このサンプルでは、Python テクノロジーを使用してビルドが行われていますが、一般的なビルド、テスト、およびデプロイのコンセプトをお好きな言語やフレームワークを使用して、簡単に実装できます。このブログのサンプルは簡単なものですが、このサンプルを拡張して、自分のパイプラインに合わせて調整することができます。CircleCI は有用なドキュメントを提供していますので、ドキュメントサイトをぜひご覧ください。操作方法が分からずに作業が中断してしまう場合には、https://discuss.circleci.com/ community/forum (英語) サイトから CircleCI コミュニティで対応策を聞いてみることもできます。