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_installbundle install を実行するジョブでは circleci/ruby:2.5.0 を使います。
実行した後のvendorはCircleCIの save_cache を使ってキャッシュしました。
そのキャッシュしたvendorはterraform applyを行うための hashicorp/terraform:0.11.14 で restore_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.0 と hashicorm/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/rubyでbundle 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無しでイベントから直で動かせるようにしてください。