Webエンジニアの日常とリーグオブレジェンド

Webエンジニアとして働いている猫のブログ。EmacsとMySQLとリーグオブレジェンド(LoL)が好物。主に技術的な記事かLoLの記事を書く。

プライベートプロジェクトでもDocker&AWS ECRでも無料で使えるCircleCIの設定をしてみた

f:id:yoshiki_utakata:20191213111706p:plain

はじめに

この記事は CircleCI Advend Calendar 2019 の 15日目の記事です。

qiita.com

CircleCIってプライベートプロジェクトでも無料で使えるんだ!って知って早速設定したので書きます。

CIって何?

CI は Continuous Integration の略です。日本語だと「継続的インテグレーション」と言われたりします。インテグレーションは「改善」みたいなニュアンスでしょうか。継続的な改善のことです。

例えば、

ソースコードがGitHubにpushされたタイミングで自動的ユニットテストなどを実行してくれる

みたいな仕組みを CI と呼びます。

  • GitHubにpushされたタイミングで自動的に → 継続的に実行される
  • ユニットテストを実行してくれる → アプリケーションの品質を改善(担保)してくれる

ということでこれも CI になります。

CIについては下記書籍の5章にいい感じの説明が乗っています。CIに限らずDevOpsという開発と運用に関する手法がまとめられていて非常に良い本です。

なぜCIが必要なのか

CIが無い場合、ユニットテストが継続的に実行されないので、「誰かがユニットテストを実行した時に、たまたまテストが落ちていることに気づく」ことになります。これが本番デプロイ直前だった場合、非常に困ります。

また、テストは実行するのが面倒なので、次第にみんな実行しなくなっていきます。

例えば、クラスAを修正したとします。対応したクラスAのテストを修正して問題ないことを確認します。この時、もしかしたらこの変更でクラスBも影響を受けているかもしれませんが、クラスBのテストは実行しません。面倒だからです。

いやいや、自分はちゃんと実行するから。そう思うかもしれませんが、CIで実行されていないテストは通ることが担保されていないも同様と思ってもいいと思います。

無料で使えるCIは

無料で使えるCIで有名どころは2つあります。ビルド時間はテストを実行している時間の長さと考えてください。

OSS はほとんどが Travis CI を採用していると思います。商用プロダクトは CircleCI が多いです。小規模なプロダクトであれば250分もあれば十分です。*1

CircleCIの設定を書く

方針

GitHubにプッシュされた時点でDockerのビルド→それを使ってユニットテストの一連の流れを実行します。

WorkflowとJob

CircleCI には WorkflowJob という概念があります。

Job は、一連の流れみたいなものです。今回は「DockerビルドするJob」と「テストをするJob」を作成します。前者を build, 後者を test と呼ぶことにします

  • build: DockerをビルドするJob
  • test: テストを実行するJob

これら2ジョブを分ける理由ですが、

  • build: Linuxマシン上でコマンドを実行
  • test: Docker上でコマンドを実行

と、実行環境が違うので、これら2つの Job は分ける必要があります。*2

Workflow は複数の Job を組み合わせたものです。Jobを並列につなぎ合わせたり直列につなぎ合わせたりすることができます。実際の設定を見るとわかりやすいです。

Workflowの設定

CircleCIの .circleci/config.yml は以下のようになります。

version: 2.1
workflows:
  version: 2.1
  build_and_test:
    jobs:
      - build
      - test:
          # test は build が終わったあとに実行する
          requires:
            - build
jobs:
  # build はテスト用の Docker image のビルドをするジョブ
  build:
    # build の中身は後述
  test:
    # test の中身は後述

DockerビルドのJob

事前にプロジェクトの設定から AWS Permissions を設定しておく必要があります。

  build:
    # CircleCI の machine を使ってジョブを実行する
    # https://circleci.com/docs/ja/2.0/executor-types/
    machine: true
    steps:
      # ソースコードをpull
      - checkout 
      - run:
          # AWS ECS にログインする
          # AWS Permissions で設定した AWS の情報を使ってログインする
          # $(...) は ... の実行結果をそのままコマンドとして実行するもので、
          # get-loginの実行結果をそのまま叩くとログインが完了する
          name: "AWS ECSにログイン"
          command: $(aws ecr get-login --no-include-email --region ap-northeast-1)
      - run:
          # はじめに ECS からイメージを pull しておいて、
          # 差分ビルドをすることでビルド時間を短縮する
          # ただし、初回はimageがないのでpullに失敗してテストが落ちる
          # なので予め適当なイメージをpushしておく
          # コマンドがコケても無視する、という設定は面倒なので妥協でこうしている
          name: "ECSからイメージをpullしてくる"
          command: docker pull XXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/app:test
      - run:
          name: "Dockerイメージをビルドする"
          command: docker build --file="Dockerfile" --tag="XXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/app:test" --cache-from="XXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/app:test" .
      - run:
          name: "ビルドしたイメージをECRにpushする"
          command: docker push XXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/app:test

テストのJob

  test:
    # 先程はmachineを指定したが、今回はdockerを指定
    docker:
      - image: XXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/app:test
        environment:
          # pythonの方で、環境変数を呼んでDB接続するようにしている
          # circleci/mysql のイメージを使うので、その設定をする
          RDS_DB_NAME: 'circle_test'
          RDS_USERNAME: 'root'
          RDS_PASSWORD: ''
          RDS_HOSTNAME: '127.0.0.1'
          RDS_PORT: '3306'
      - image: circleci/mysql:5.7
    steps:
      - run:
          # dockerize というツールを使って mysql の起動を待たないと、
          # mysql が立ち上がる前にテストが実行されてしまい失敗する
          name: 'docker の起動を待つための dockerize コマンドをインストール'
          command: |
            apt install wget -y \
            && wget https://github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
            && tar -C /usr/local/bin -xzvf dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz \
            && rm dockerize-linux-amd64-$DOCKERIZE_VERSION.tar.gz
          environment:
            DOCKERIZE_VERSION: v0.6.1
      - run:
          name: 'DBコンテナの起動を待つ'
          command: dockerize -wait tcp://localhost:3306 -timeout 1m
      - run:
          name: "テストを実行する"
          command: ./manage.py test

今後

  • 実はよく見ると AWS Permissions に「These project settings are no longer available. Please use the aws-cli orb instead.」と書いてありました。なので今回のやつはちょっと書き換える必要があります。。。やってるときに気づけよ
  • まだテスト結果の収集ができてないのでやる必要があります

余談

この記事書くために Travis CI で調べたら CircleCI の広告出てきて

f:id:yoshiki_utakata:20191213165139p:plain

*1:もし1週間あたり250分以上のビルドをするのであれば、有料を使ってもいいレベルのプロダクトだと思います。それか1回のビルドにかかる時間をがんばって短くしましょう。

*2:他にも例えば、Job A と Job B があり、AとBは依存関係が無いので、可能であれば並列で実行したい、といった場合もJobを分ける必要があります。普通に上からコマンドを実行していけばいい場合は1つのJobにしておいたほうが、データのやり取り(Dockerイメージのやり取りのような)も簡単になってデメリットが少ないです。