自動化と時短を目的にCircleCIで暫定のCICD環境を作成した記録

CircleCIにて暫定のCI/CDを行った際の設定過程です。常時実行が不要なbuildとdeployに対してManual Approvalを挟むことでリソース浪費防止を試みました。
2019.10.25

はじめに

手作業によるbuild・deployのヒューマンエラー防止として自動化を検討する際に、ぶつかる壁として考えられるものに

  • 手順をCI/CDに落とし込めるか
  • 処理時間が肥大化しないか
  • CI/CD設定作成が困難ではないか

の3点があります。

今回、それらの課題を抑えつつも「既に出来上がっている手順を大きくは崩さない」前提でCircleCIの設定へ落とし込みをしてみました。

前提

本運用にて用いるプラットフォームの選定が完了していないものの、プラットフォームを変える選択肢有りでとりあえず自動化を済ませたいという目論見があります。既にCircleCI上でpytestの実行だけは行っており、その延長としてCI/CDを実施することにしました。

CircleCIでのCI用設定について業務を通して組み立てた内容をまとめてみた

Dockerイメージのbuildと環境へのdeployで時間を要することが判っています。ですが、Dockerイメージ更新発生頻度は少なめで、環境へのdeployタイミングもそこまで多くはありません。そのため、buildとdeployを毎回実施する必要はありません

追加する手順の整理

大まかには以下の内訳となります。

  • Dockerイメージのビルド
  • ECRへイメージをアップロード
  • CloudFormation経由での環境への反映

更に、特に行っていなかったものの、追加候補となる手続きとしては以下辺りになります。

  • Slackへの更新通知

CircleCI環境を整理する

設定を書き始める前に、利用するOrbや環境変数設定を行います。

Contextsを用いた環境変数設定

手順にてTerminal上で環境変数の設定を行っている箇所について、事前にContextsに追加を行っています。今回はブランチ名で本番用job・開発用jobを切り替えつつ、切り替えに応じて利用するContextsも変更する想定です。

コンテキストの使用 - CircleCI

暫定で以下の内容を指定しています。

  • AWS_ACCESS_KEY_ID
  • AWS_DEFAULT_REGION
  • AWS_SECRET_ACCESS_KEY
  • ROLE_ARN
  • SLACK_WEBHOOK

Orbを用いた設定の簡易化

効率よく設定化するため、いくつかOrbを併用しています。

ECRへのアップロードにはaws-ecrのOrbを用いる方法もあるのですが、手順にてawscliを多く使っていたためそちらに合わせています。

その他

docker imageは全てcircleci/python:3.7.3としています。これはプロジェクトのベースがpythonコードによるためです。

config.ymlの作成

実際には一つのファイルにおさめていますが、各要素毎で触れるために区切った形で掲載しています。

version
orbs
jobs
workflows

の4つから成り立っています。各要素については以下の公式ドキュメントを参照してください。

CircleCI を設定する - CircleCI

DockerイメージのビルドとECRへのアップロード

.aws/credentialsへ追加を行い、その後buildとECRへのアップロードを行います。${AWS_ACCESS_KEY_ID}${AWS_SECRET_ACCESS_KEY}の指定がありますが、これはworkflowからの呼び出し時に指定されるcontextに沿って決定されます。

jobs:
  build:
    docker:
      - image: circleci/python:3.7.3
    parameters:
      python-version:
        type: string
    executor: aws-cli/default
    steps:
      - checkout
      - setup_remote_docker
      - run: 
          name: set aws-cli
          command: |
            mkdir -p ~/.aws
            echo "[default]" >> ~/.aws/credentials
            echo "aws_access_key_id = ${AWS_ACCESS_KEY_ID}" >> ~/.aws/credentials
            echo "aws_secret_access_key = ${AWS_SECRET_ACCESS_KEY}" >> ~/.aws/credentials
            echo "[assume_role]" >> ~/.aws/credentials
            echo "source_profile = default" >> ~/.aws/credentials
            echo "role_arn = ${ROLE_ARN}" >> ~/.aws/credentials
      - aws-cli/setup:
          profile-name: assume_role
      - run:
          name: build docket image
          command: |
            echo 'export AWS_PROFILE=profile' >> $BASH_ENV
            docker build -t project -f docker/Dockerfile .
            eval $(aws ecr get-login --no-include-email --profile=profile)
            docker tag project:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/project:latest
            docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/project:latest

echo 'export AWS_PROFILE=profile' >> $BASH_ENVとなっている箇所は、以下のリンク先に詳細があります。

環境変数の使い方 - CircleCI

環境への反映

./deploy.sh devにてawscliの呼び出しでCFnを実行しており、profileを設定するため事前にexportを行います。

parametersにpython-versionを渡しているのは、aws-cliのOrbへworkflow経由で3.7と指定することが目的です。

jobs:
  deploy:
    docker:
      - image: circleci/python:3.7.3
    parameters:
      python-version:
        type: string
    executor: aws-cli/default
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: deploy to environment
          command: |
            echo 'export AWS_PROFILE=assume_role' >> $BASH_ENV
            ./deploy.sh dev

Workflowの設定

requiresを指定することで順序建てています。

ジョブの実行を Workflow で制御する - CircleCI

workflows:
  version: 2
  build_and_push_image:
    jobs:
      - test:
          python-version: "3.6"
          context: dev
      - slack/approval-notification:
          message: ${CIRCLE_BRANCH}をbuildするためにはApproveが必要です。
          requires:
            - test
          context: dev
      - approve_build:
          type: approval
          requires:
            - test
          context: dev
      - build:
          requires:
            - approve_build
          python-version: "3.6"
          context: dev
      - slack/approval-notification:
          message: ${CIRCLE_BRANCH}をdeployするためにはApproveが必要です。
          requires:
            - build
          context: dev
      - approve_deploy:
          type: approval
          requires:
            - build
          context: dev
      - deploy:
          requires:
            - approve_deploy
          python-version: "3.6"
          filters:
            branches:
              ignore:
                - master
          context: dev

buildとdeployが頻繁なバッチ系ファイル修正によるプッシュに伴うとリソースの浪費に繋がるため、type: approvalをbuildとdeployのjobに指定することで常時実行を防いでリソースの浪費防止を狙っています。

なお、slack/approval-notificationにもapprovalの文字が含まれていますが、実際にはSlackへ実行承認目的でのWorkflowページへのリンクが通知されるだけです。

Workflowページ上では以下のような表示となります。

config.yml全体

とりあえず動かすことを目的としているため、細かい部分で粗を含む可能性があります。

---
version: 2.1
orbs:
  aws-cli: circleci/aws-cli@0.1.16
  slack: circleci/slack@3.4.0
jobs:
  test:
    docker:
      - image: circleci/python:3.7.3
    parameters:
      python-version:
        type: string
    executor: aws-cli/default
    steps:
      - checkout
      - setup_remote_docker
      - run: sudo chown -R circleci:circleci /usr/local/bin
      - run: sudo chown -R circleci:circleci /usr/local/lib/python3.7/site-packages
      - restore_cache:  # このステップは依存関係をインストールする<em>前</em>に実行します
          key: deps9-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
      - run: echo "export latest_commit_log=$(git log --oneline -1)" >> $BASH_ENV
      - run:
          command: |
            pip install pipenv
            pipenv sync --dev
      - save_cache:
          key: deps9-{{ .Branch }}-{{ checksum "Pipfile.lock" }}
          paths:
            - ".venv"
            - "/usr/local/bin"
            - "/usr/local/lib/python3.7/site-packages"
      - run:
          name: run tests
          command: |
            export AWS_REGION_NAME='ap-northeast-1'
            export AWS_ACCESS_KEY_ID='test'
            export AWS_SECRET_ACCESS_KEY='test'
            export run_env=dev
            pipenv run pytest tests lambda --cov=project --cov-report=term-missing --cov-report=html --benchmark-skip
      - store_artifacts:
          path: htmlcov
          destination: htmlcov
      - run:
          name: run linting and metrics
          command: |
            pipenv run flake8 src/ tests/ lambda/
            pipenv run cfn-lint -i W3002 -t cfn/*.yml
  build:
    docker:
      - image: circleci/python:3.7.3
    parameters:
      python-version:
        type: string
    executor: aws-cli/default
    steps:
      - checkout
      - setup_remote_docker
      - run: 
          name: set aws-cli
          command: |
            mkdir -p ~/.aws
            echo "[default]" >> ~/.aws/credentials
            echo "aws_access_key_id = ${AWS_ACCESS_KEY_ID}" >> ~/.aws/credentials
            echo "aws_secret_access_key = ${AWS_SECRET_ACCESS_KEY}" >> ~/.aws/credentials
            echo "[assume_role]" >> ~/.aws/credentials
            echo "source_profile = default" >> ~/.aws/credentials
            echo "role_arn = ${ROLE_ARN}" >> ~/.aws/credentials
      - aws-cli/setup:
          profile-name: assume_role
      - run:
          name: build docket image
          command: |
            echo 'export AWS_PROFILE=assume_role' >> $BASH_ENV
            docker build -t project -f docker/Dockerfile .
            eval $(aws ecr get-login --no-include-email --profile=assume_role)
            docker tag project:latest XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/project:latest
            docker push XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/project:latest
  deploy_dev:
    docker:
      - image: circleci/python:3.7.3
    parameters:
      python-version:
        type: string
    executor: aws-cli/default
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: deploy to environment
          command: |
            echo 'export AWS_PROFILE=assume_role' >> $BASH_ENV
            ./deploy.sh dev
  deploy_prod:
    docker:
      - image: circleci/python:3.7.3
    parameters:
      python-version:
        type: string
    executor: aws-cli/default
    steps:
      - checkout
      - setup_remote_docker
      - run:
          name: deploy to environment
          command: |
            AWS_PROFILE=assume_role ./deploy.sh prod
workflows:
  version: 2
  build_and_push_image:
    jobs:
      - test:
          python-version: "3.6"
          context: dev
      - slack/approval-notification:
          message: ${CIRCLE_BRANCH}をbuildするためにはApproveが必要です。
          requires:
            - test
          context: dev
      - approve_build:
          type: approval
          requires:
            - test
          context: dev
      - build:
          requires:
            - approve_build
          python-version: "3.6"
          context: dev
      - slack/approval-notification:
          message: ${CIRCLE_BRANCH}をdeployするためにはApproveが必要です。
          requires:
            - build
          context: dev
      - approve_deploy:
          type: approval
          requires:
            - build
          context: dev
      - deploy:
          requires:
            - approve_deploy
          python-version: "3.6"
          context: dev

あとがき

取り敢えず自動化することを目的としているため、本運用時にはここから大幅に変わる可能性もあります。環境設営の速度を求める場合の参考になれば幸いです。