RAKSUL TechBlog

ラクスルグループのエンジニアが技術トピックを発信するブログです

CircleCI で asdf を活用して言語のバージョンアップをラクにする

ラクスル事業本部のサーバーサイドエンジニアの杉山です。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 の運用をさらに改善して、ラクスルで働くエンジニアをよりラクにしていきたいです。