~CircleCIでTerraformリリースのサーバーレス化~ インフラの独自リリースを継続的リリースへ

adachin|2021年10月25日
SRE

SREチームの安達(@adachin0817)です。以前エンジニアブログにてこんなことを言っていたのを皆様覚えておりますでしょうか。

それ以外のプロジェクトであるLancers CreativeProsheetLancers AgentMENTAはECS/Fargateに移行しているので、すべてTerraformでコード管理しています。また、CircleCIでTerraform CI(validate,plan)を実行しているということもあり、今後terraform applyもCircleCIで行う予定なので、Deployサーバーでの運用は廃止していく予定です。

  • 今までのTerraform管理方法

そうです。以下のようにTerraformでの運用について課題がありました。

  • わざわざサーバーを用意したくないのと運用コストが上がる
  • Deployサーバーで管理するのはやめましょうといってもローカルPCで反映したくない
  • Terraformの適用(apply)はCircleCIのみでリリース(サーバーレス)ができればベスト

ちなみにDeployサーバーというのはランサーズで利用している各サーバー側のデプロイ(Ansible)やアプリケーションのデプロイと様々な用途として利用しています。またランサーズで運用しているEC2はAmazon Linux2のarmに移行後、性能評価を比較して、ECS/Fargateに移行する予定です。なのでCircleCIのみでのリリースに統一となると、Deployサーバーは廃止する方向になります。そこで今回は上記をどのように改善していったのかブログしていきたいと思います。

背景

  • Deployサーバーで適用してしまうとブランチの切り替えを忘れて他のリリースに影響が出る
  • Deployサーバーに依存してしまうので複数人でのTerraform開発スピードが遅れる
  • Terraformのバージョンアップ対応中でも急に依頼が来て、バージョンアップの切り替えをしなければならない
  • ECS Scheduled Taskによる頻繁に開発メンバーからの更新とそれに伴いリリースの際SREチームの工数を取ってしまう

上記の4つが課題となります。まず問題なのが、Deployサーバーに依存してしまうということです。もちろんTerraformでの開発はterraform planだけでは判断できないことも多く、コード書く際にその場で適用し、デバッグをして修正といったことも多々あります。そこでローカルPCで管理してしまうとクレデンシャルを設定しなければならなく、非常に強い権限が必要なため、万が一、漏洩となることも考えると、なるべくローカルにはクレデンシャルを設置したくないという思いがありました。またECS Scheduled TaskによるバッチはTerraform化しているので、頻繁に開発メンバーからはレビューとマージ後にSREチームの誰かが適用しなければなりません。そういった運用工数も取られてしまうので自動化できればと考えておりました。そして考慮した結果、Terraform専用の開発環境を構築し、CircleCIで継続的にリリースをするという方向になりました。

Terraform開発環境

  • ディレクトリ構成
tree .
.
├── Dockerfile
├── README.md
├── docker-compose.yml
├── root
│   └── dev-credentials
└── setting_dev.sh
  • docker-compose.yml
version: '3'
services:
  app:
    container_name: menta-terraform-dev
    build:
      context: .
      dockerfile: Dockerfile
    image: menta-terraform-dev
    volumes:
      - ~/hoge:/var/www/hoge/:delegated
    tty: true
  • Dockerfile
FROM hashicorp/terraform:1.0.5
ENV APP_ROOT /var/www/hoge
WORKDIR $APP_ROOT

# Setup UTC+9
RUN apk --update add tzdata && \
cp /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
apk del tzdata && \
rm -rf /var/cache/apk/*

# install packages
RUN apk update && \
apk upgrade && \
apk add --update --no-cache \
bash \
coreutils \
vim \
python3 \
py3-pip

# install aws-cli
RUN pip3 install --upgrade pip
RUN pip3 install awscli && rm -rf /var/cache/apk/*

# copy empty credentials
RUN mkdir /root/.aws
COPY root/dev-credentials /root/.aws/credentials
COPY root/.bashrc /root/.bashrc

ENTRYPOINT ["/bin/bash"]
  • setting_dev.sh
#!/bin/bash

aws --profile=terraform-kms s3 cp s3://hoge/dev.key /root/.aws/

aws --profile=hoge kms decrypt \
--ciphertext-blob fileb:///root/.aws/dev.key \
--output text \
--query Plaintext \
| base64 --decode > /root/.aws/newcredentials

mv /root/.aws/newcredentials /root/.aws/credentials

まず各プロジェクトにTerraformの開発環境を準備しました。TerraformはDocker hub公式でコンテナが用意されているので現状のv1.0.5を指定し、API経由ということもあってDockerでのPortも特に開放する必要がありません。

そしてクレデンシャルをどう管理するのかと考慮したところ、AWS KMSを利用してKMS専用のクレデンシャルから暗号化されたkeyをコピーし、新たにTerraformで利用するクレデンシャルを上書きで復号するようにシェルスクリプトで作成しました。コンテナ内にクレデンシャルを配置することで、セキュリティレベルが二重になり、リスクも軽減されるようになります。もちろんdocker-compose downをすればクレデンシャルは初期化されるので漏洩してしまう恐れも減ります。この開発環境の構築により、各々ローカルPCでTerraformのデバッグやバージョンアップがしやすい環境となりました。次はCircleCIでのリリースフローを説明していきましょう。

CircleCIでのリリースフロー(MENTA)

  • .circleci/config.yml
version: 2.1

setup: true

orbs:
  aws-ecr: circleci/aws-ecr@7.0.0
  aws-ecs: circleci/aws-ecs@2.2.0
  aws-cli: circleci/aws-cli@2.0.0
  path-filtering: circleci/path-filtering@0.0.3
  slack: circleci/slack@3.4.2

parameters:
deploy_app_stg:
type: boolean
default: false
deploy_app_private:
type: boolean
default: false

~省略~

workflows:
  version: 2
  ci:
    jobs:
      - laravel_test
      - stg_terraform_fmt_validate
      - stg_terraform_plan:
          requires:
            - stg_terraform_fmt_validate
      - prd_terraform_fmt_validate
      - prd_terraform_plan:
          requires:
      - prd_terraform_fmt_validate
      - path-filtering/filter:
          base-revision: origin/master
          config-path: .circleci/deploy.yml
          mapping: |
            terraform/stg/.* stg_terraform_apply true
            terraform/prd/.* prd_terraform_apply true

MENTAの例ですが、TerraformのCIはvalidate,planをpush時に実行しています。以前masterマージでterraform applyという運用をしていましたが、SREチームがTerraformのデバッグをして、そのままapplyをしたままマージせずに他のプルリクをマージすることで元に戻ってしまうということがあり、更に他の部分で差分が出ているのに気づかず、applyされて変更されてしまったりと、危うくインシデントになりそうだったのでapplyの部分は一旦断念しました。またCircleCIのAPIを利用してシェルでリリースをするという方法も考えましたが、権限的に開発メンバーが全員リリース出来てしまうという懸念点もありました。

そこで解決してくれるのが、CircleCIのpath-filteringというOrbsを利用して、terraformディレクトリにmasterとの差分があればapplyされるように実装しました。

https://circleci.com/developer/orbs/orb/circleci/path-filtering

path-filteringはsetup: true が付いていることによって複数のworkflowが無効化になります。その場合デプロイ系は全て別ymlに移行(deploy.yml)して、single workflowに変更する必要があります。single workflowになったことでCIが20秒ほど早くなりました。Terraform CIに関しては私の個人ブログを参考にしてみてください。

[AWS]CircleCIでTerraformのCI/CD環境を実装してみた

  • .circleci/deploy.yml
version: 2.1
orbs:
  aws-ecr: circleci/aws-ecr@7.0.0
  aws-ecs: circleci/aws-ecs@2.2.0
  aws-cli: circleci/aws-cli@2.0.0
  slack: circleci/slack@3.4.2

parameters:
stg_terraform_apply:
type: boolean
default: false
prd_terraform_apply:
type: boolean
default: false
deploy_app_stg:
type: boolean
default: false
deploy_app_private:
type: boolean
default: false

~省略~
jobs:
  check_terraform_difference:
    <<: *default_config
    steps:
      - run:
         name: install package
         command: |
           apk add bash curl jq
      - run: echo "No terraform difference ok !!"
      - slack/status:
          fail_only: true
          mentions: 'here'
          failure_message: 'Error check terraform difference 🚨 \n :innocent: ${CIRCLE_USERNAME} :branch: ${CIRCLE_BRANCH}'
          webhook: ${SLACK_WEBHOOK}
      - slack/notify:
          title: 👍
          color: '#42f486'
          message: 'No terraform difference OK !! ✨ \n :grin: ${CIRCLE_USERNAME} :branch: ${CIRCLE_BRANCH}'
          webhook: ${SLACK_WEBHOOK}
~省略~

workflows:
  version: 2.1
  check_terraform_difference:
    jobs:
     - check_terraform_difference
    when:
      and:
        - not: << pipeline.parameters.stg_terraform_apply >>
        - not: << pipeline.parameters.prd_terraform_apply >>
  stg_terraform_apply:
    when: << pipeline.parameters.stg_terraform_apply >>
    jobs:
      - stg_terraform_apply:
          filters:
            branches:
              only: master
  prd_terraform_apply:
    when: << pipeline.parameters.prd_terraform_apply >>
    jobs:
      - prd_terraform_apply:
          filters:
            branches:
              only: master
  deploy_app_stg:
    when: << pipeline.parameters.deploy_app_stg >>
    jobs:
      - deploy_app_stg
  deploy_app_private:
    when: << pipeline.parameters.deploy_app_private >>
    jobs:
      - deploy_app_private
  deploy_app_prd:
    jobs:
      - deploy_app_prd:
          filters:
            branches:
              only: master

新しいjobとしてcheck_terraform_differenceを作りましたが、CircleCIの仕様で差分がないときにmappingに検知されず、全てのパラメータはdefaultのfalseが返ってきてしまうという動作になってしまうため、実行するworkflowがない場合はエラーとなります。なのでechoでtrueを返すように作りました。ここはCircleCIさんぜひデフォでtrueを返すようにしてほしい!以下動作確認ですが、terraformディレクトリに差分がなければcheck_terraform_differenceが動作してそのままCIが動き、差分があればterraform applyされるようになりました。

  • 動作確認

まとめ

これでようやくDeployサーバーからの管理を脱却することができ、開発環境でのデバッグとTerraformのリリースを安全にサーバーレスでの実現や改善することができました。またLancers本体も完全Terraform化するための準備も整うことができました。CircleCIのpath-filteringはモノレポでもパス単位で自動リリースすることができるので、Lambdaなどにも応用ができることでしょう。またpath-filteringの今後のバージョンにも期待です!また課題等出てきたらブログしようと思います。

SRE募集しているので気になる方はぜひTwitterでDMお待ちしています!

※CircleCIユーザーコミュニティでLTしてきました!