CircleCIでRubyのvendorをWorkflow間で共有した時の失敗談

目次

はじめに

どうもお仕事でAWS触ることが多くなっているゲインです。

最近Rubyを動かすためのAWS LambdaをTerraform + CircleCIでDeployするという構成でちょっとハマったので失敗談と解決方法の一例を示します。

TL;DR

  • それぞれのコンテナでユーザーおよび$HOMEが違う
  • そのためCircleCIの working_directory~ を使うと異なるパスになることがある
  • 結果的に restore_cache が別のパスに保存されてしまうことがある

前提条件

  • CircleCI上でTerraformが実行されて反映される環境である。

ハマったこと

プロジェクトの都合でRubyを使ってLambdaを動かすことになりました。

CircleCI上でのCI/CD環境が既にあったのでWorkflowを使って以下ような事を考えて、フローを追加しました。

  • Terraformは既に hashicorp/terraform で動いているでそのままにする
  • Rubyのvendorは別のjobでコンテナイメージを変更して動かす

それを実際に反映したのが以下のconfig.ymlです。

# config.yml
version: 2
jobs:
  build: # Terraform apply
    docker:
      - image: hashicorp/terraform:0.11.14
    working_directory: ~/terraform
    environment:
      AWS_DEFAULT_REGION: ap-northeast-1
    steps:
      - checkout
      - restore_cache:
          key: source-v1-{{ .Branch }}-{{ .Revision }}-{{ checksum "lambda/src/myfunction/Gemfile.lock" }}
      - run:
          command: |
            terraform init -backend-config="bucket=${BUCKET_NAME}" -backend-config="key=terraform.tfstate" -backend-config="region=${REGION}"
            terraform apply -var-file=stg.tfvars 
 
  bundle_install: # bundle install for Lambda function
    docker:
      - image: circleci/ruby:2.5.0
    working_directory: ~/terraform
    steps:
      - checkout
      - run:
          name: bundle install
          command: |
            cd modules/sfn/lambda/src/myfunction
            bundle install --path vendor/bundle
      - save_cache:
          key: source-v1-{{ .Branch }}-{{ .Revision }}-{{ checksum "lambda/src/myfunction/Gemfile.lock" }}
          paths:
            - "lambda/src/myfunction/vendor"
 
 
workflows:
  version: 2
  deploy:
    jobs:
      - bundle_install
      - build:
          requires:
            - bundle_install

bundle install を実行するジョブでは circleci/ruby:2.5.0 を使います。

実行した後のvendorはCircleCIの save_cache を使ってキャッシュしました。

そのキャッシュしたvendorはterraform applyを行うための hashicorp/terraform:0.11.14restore_cache しました。

さてこれで bundle install でvendorが作成され、TerraformでそれらがzipにまとめられLambdaで使えるようになりましたとさ。

...とはなりませんでした。

バッチ処理のStep Function内で使われるLambdaだったので、朝起きるとCloudWatch Logsにはこんなエラーが。

Traceback (most recent call last):
main.rb:1:in `<main>': undefined local variable or method `GEM_NAME' for main:Object (NameError)

なぜかLambdaにはvendorがなく、使いたかったgemを使えないという問題でした。

原因

今回使っていた circleci/ruby:2.5.0hashicorm/terraform:0.11.14 ではユーザが異なっていました。

実際に手元でdockerコンテナ内に入って確認してみましょう。

まずは hashicorp/terraform のコンテナに入ってみます。

# hashicorp/terraform
docker run -it --entrypoint=ash hashicorp/terraform:0.11.14

# コンテナ内
circleci@3d233757b0cc:/$ whoami
root
circleci@3d233757b0cc:/$ echo $HOME
/root

ユーザーは circleci$HOME/root となりました。

次に circleci/ruby:2.5.0 に入ってみます。

# circleci:ruby
docker run -it circleci/ruby:2.5.0 /bin/bash

# コンテナ内
circleci@3d233757b0cc:/$ whoami
circleci

circleci@3d233757b0cc:/$ echo $HOME
/home/circleci

ユーザーは circleci$HOME/home/circleci となりました。

つまりは以下のようなことでした。

  • circleci/rubybundle install したときは /home/circleci/workdir/lambda/src/vendor のパスでキャッシュされる
  • hashicorp/terraform のジョブでは /root/workdir/lambda/src/vendor のパスを期待している
  • 実際には /home/circleci/workdir/lambda/src/vendor に実際にはキャッシュが復元される

同様の事例として、CircleCIのサポートにも似たような話があります。

解決策

上記のCircleCIのサポートページには以下の記述があります。

可能なら、複数のジョブ間で同じイメージを使用してください。 それが難しい場合は、2 つのイメージ間で共通の場所に作業ディレクトリを設定してください (/tmp が使用できることがあります)。 また、キャッシュを保存する前に、ファイルのアクセス許可を変更または設定する必要があります。

そのため bundle install のパスを /tmp 配下にして、そこから取り出すという手段をとってみました。

configは以下のようになります。

# config.yml
version: 2
jobs:
  build: # Terraform apply
    docker:
      - image: hashicorp/terraform:0.11.14
    working_directory: ~/terraform
    environment:
      AWS_DEFAULT_REGION: ap-northeast-1
    steps:
      - checkout
      - restore_cache:
          key: source-v1-{{ .Branch }}-{{ .Revision }}-{{ checksum "lambda/src/myfunction/Gemfile.lock" }}
					
      ## ここを追加!!
      - run:
          name: Move tmp to app dir from /tmp
          command: mv /tmp/vendor lambda/src/vendor
 
      - run:
          command: |
            terraform init -backend-config="bucket=${BUCKET_NAME}" -backend-config="key=terraform.tfstate" -backend-config="region=${REGION}"
            terraform apply -var-file=stg.tfvars 
 
  bundle_install: # bundle install for Lambda function
    docker:
      - image: circleci/ruby:2.5.0
    working_directory: ~/terraform
    steps:
      - checkout
      - run:
          name: bundle install
          command: |
            cd modules/sfn/lambda/src/myfunction
            ## ここを修正!!
            bundle install --path /tmp/vendor/bundle
      - save_cache:
          key: source-v1-{{ .Branch }}-{{ .Revision }}-{{ checksum "lambda/src/myfunction/Gemfile.lock" }}
          paths:
            - "lambda/src/myfunction/vendor"
 
 
workflows:
  version: 2
  deploy:
    jobs:
      - bundle_install
      - build:
          requires:
            - bundle_install

ただこちらの解決策も本来はいまいちなはずなので、実際には次のような対応を取ると良いでしょう。

  • circleci/rubyをterraformのジョブでも使う
  • terraform自体はgoで作らているのでBuild済みのものを公式から取得して使う

まとめ

CircleCIのworkflow間で外部ライブラリを save_cache で共有するときのハマったところと解決策を紹介しました。

ここまで想像力が働かずにバッチが失敗してしまったので今後は気をつけたいですね。

TerraformでLambdaを触るのは少し辛くなってきたのでなにかベストプラクティスが欲しいところです。

AWSさんはGlueのCralwerをLambda無しでイベントから直で動かせるようにしてください。