ラクスル事業本部のサーバーサイドエンジニアの杉山です。2023年4月に新卒で入社しました。現在は、ラクスルで取り扱う商品を追加するための開発や、運用・保守を行っています。
今回の記事ではラクスルでの CircleCI の運用改善について紹介します。
ラクスルのサービス事情
ラクスルのサービス( raksul.com )は Ruby on Rails と PHP で構築されています。この Rails と PHP のアプリケーションは別々のリポジトリで管理されています。データベースのマイグレーションは Rails アプリケーション側で管理しています。また、フロントエンドの開発には Node.js を使用しています。
このような構成のため、 PHP のアプリケーションでのデータベースが関わるテストのために CI では Rails アプリケーションでマイグレーションを実行する必要があります。
これまでは、 PHP のアプリケーションの CI の実行時間を短縮するために、特定のバージョンの PHP,Ruby,Node.js をインストールした Docker イメージを AWS ECR に配置し CircleCI で使用していました。
課題
しかし、この方法には ひとつの言語のバージョンを上げるだけでもイメージの再ビルドが必要で手間がかかりました。 また、イメージのビルドは頻繁に行う作業ではないため、作業環境の構築に時間がかかるという問題もありました。
これらの問題を解決するために、新しい方法を検討しました。
解決策: CircleCIでのasdfの活用
この問題を解決するため、ローカル開発環境で使われることが多い asdf を CircleCI 上で使うことにしました。
asdf は複数の言語ランタイムを管理できる CLI ツールです。 Ruby は rbenv 、 Node.js は nvm というように言語ごとにあるバージョン管理ツールをひとまとめにできます。それぞれの言語はプラグインとして追加します。
asdf は以下のようなコマンドで使います。
asdf plugin add nodejs # nodejs プラグインを追加 asdf install nodejs latest # nodejs の最新バージョンをインストール asdf global nodejs latest # nodejs の最新バージョンをグローバルに設定
新たな方法では、 asdf の PHP,Ruby,Node.js のプラグインをインストールしたイメージをビルドし、 AWS ECR にプッシュしておきます。.circleci/config.yml でそれぞれの言語のバージョンを指定し、 CI 実行時に asdf を使って言語のインストールします。
参考までに簡略版の Dockerfile と .circleci/config.yml を掲載します。
FROM cimg/base:current-20.04 USER root RUN apt-get update && apt-get -y upgrade # Install dev library RUN apt-get install -y bison gettext libgd-dev libedit-dev libicu-dev libjpeg-dev libonig-dev libreadline-dev libxml2-dev libzip-dev re2c # for asdf-php RUN apt-get install -y patch rustc libssl-dev libyaml-dev zlib1g-dev libgmp-dev libncurses5-dev libffi-dev libgdbm6 libgdbm-dev libdb-dev uuid-dev # for asdf-ruby RUN apt-get install -y python3 g++ make python3-pip # for asdf-nodejs USER circleci ENV ASDF_VERSION=v0.11.3 RUN git config --global http.sslverify false # avoid git ssl error RUN git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch $ASDF_VERSION --depth=1 ENV PATH /home/circleci/.asdf/bin:/home/circleci/.asdf/shims:$PATH RUN asdf plugin-add php RUN asdf plugin-add ruby RUN asdf plugin-add nodejs RUN git config --global http.sslverify true
version: 2.1
versions:
ruby: &ruby_version 3.2.2
node: &node_version 18.16.0
php: &php_version 8.2.7
bundler: &bundler_version 2.4.13
executors:
base:
docker:
- image: ************.dkr.ecr.ap-northeast-1.amazonaws.com/circleci/base:v1
aws_auth:
aws_access_key_id: $AWS_ACCESS_KEY_ID
aws_secret_access_key: $AWS_SECRET_ACCESS_KEY
environment:
TZ: Asia/Tokyo
commands:
install-php:
description: "Install PHP"
parameters:
version:
type: string
default: *php_version
steps:
- run: asdf install php <<parameters.version>>
- use-php
- save_cache:
key: asdf-php-v1-<<parameters.version>>
paths:
- ~/.asdf/installs/php/<<parameters.version>>
restore-php-cache:
description: "Restore PHP Build Cache"
parameters:
version:
type: string
default: *php_version
steps:
- restore_cache:
keys:
- asdf-php-v1-<<parameters.version>>
use-php:
description: "Use PHP"
parameters:
version:
type: string
default: *php_version
steps:
- run: asdf global php <<parameters.version>>
- run: asdf reshim php
install-node:
description: "Install Node.js"
parameters:
version:
type: string
default: *node_version
steps:
- restore_cache:
keys:
- asdf-nodejs-v1-<<parameters.version>>
- run: asdf install nodejs <<parameters.version>>
- run: asdf global nodejs <<parameters.version>>
- run: asdf reshim nodejs
- save_cache:
key: asdf-nodejs-v1-<<parameters.version>>
paths:
- ~/.asdf/installs/nodejs/<<parameters.version>>
install-ruby:
description: "Install Ruby"
parameters:
version:
type: string
default: *ruby_version
bundler-version:
type: string
default: *bundler_version
steps:
- restore_cache:
keys:
- asdf-ruby-v1-<<parameters.version>>-<<parameters.bundler-version>>
- asdf-ruby-v1-<<parameters.version>>-
- run: asdf install ruby <<parameters.version>>
- run: asdf global ruby <<parameters.version>>
- run: asdf reshim ruby
- run: gem install bundler -v <<parameters.bundler-version>> -N
- save_cache:
key: asdf-ruby-v1-<<parameters.version>>-<<parameters.bundler-version>>
paths:
- ~/.asdf/installs/ruby/<<parameters.version>>
jobs:
build-php:
executor: base
resource_class: "large"
steps:
- restore-php-cache
- install-php
test:
executor: base
steps:
- checkout
- install-ruby
# Ruby を使う処理
- install-node
# Node.js を使う処理
- restore-php-cache
- use-php
# PHP を使う処理
workflows:
version: 2
test:
jobs:
- build-php
- test:
requires:
- build-php
ポイントは、 asdf install でインストールした言語のバージョンをキャッシュすることです。インストール済みのバージョンの場合は、キャッシュが使われます。これにより、同じバージョンのインストールは一度だけ行えばよくなります。また、 PHP はビルドに時間がかかるため、 build-php ジョブに切り出しています。 test ジョブでは build-php ジョブでビルドした PHP を使うため、 requires で依存関係を指定しています。
これにより、 PHP,Ruby,Node.js のバージョンを変更する場合は、 .circleci/config.yml で指定するバージョンを変更するだけで済み、イメージのビルドは不要になりました。なお、キャッシュを使うことで、以前の方法と同程度の速度で CI を実行できています。
まとめ
CI の改善は、ドメイン知識が浅くてもチームの生産性を向上させることができるため、新卒エンジニアにも取り組みやすい課題だと思っています。今後も、 CircleCI の運用をさらに改善して、ラクスルで働くエンジニアをよりラクにしていきたいです。