エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

cruft実践入門 ~cookiecutter templateの変更に追従する~

Overview

エムスリーエンジニアリンググループ AI・機械学習チームでソフトウェアエンジニアをしている中村(po3rin) です。検索とGoが好きです。

AI・機械学習チームでは開発の効率化のため、プロジェクトの雛形を自動的に生成するcookiecutterのプロジェクトtemplateを利用しています。下記はAPI開発で利用しているcookiecutterプロジェクトtemplateについての記事です。

www.m3tech.blog

しかし、templateから作成したプロジェクトはtemplateが新しくなった場合に、その変更に追従するのが困難になります。そこで、今回はcruftというツールを導入して、最新cookiecutter templateへの追従を楽にできるようにしました。今回はcruftの紹介と、それをどのように導入したかをお話しします。

cookiecutterの課題

cookiecutterを使ったプロジェクト作成した後に、template側に重要な更新が入った場合、そのtemplateから作ったプロジェクトにその変更を加えたい時があります。例えばツールのバージョンアップや新しいセキュリティチェックをCIに導入した場合、templateの更新はもちろん、そのtemplateから作った全てのプロジェクトにその変更を加えていく必要があります。

どのプロジェクトに変更を追加したかを管理するのは非常に困難で、弊チームでは重要な変更のときはスプレッドシートを使って変更を適用したプロジェクトにチェックを入れてもらい、まだ変更を取り込んでいないプロジェクトを管理するなどの手作業が発生していました。

また、重要でない改善の反映はプロジェクトまかせになって反映されないことも多く、プロジェクトごとにベストプラクティスに追従できていない状態になっていました。

そこでcruftの登場です。

cruftとは

cruftを使用すると、templateの最新状態への追従や、変更のチェックなどを簡単に行えます。cookiecutter template機能と完全に互換性があるので、cookiecutterを使っているチームはすぐに取り入れることができます。

リポジトリはこちらです。

github.com

このツールを導入することで、cookiecutterで作った既存のプロジェクトが最新のtemplateへ追従するのを楽にしました。

既存プロジェクトへのcruft導入

cookiecutter templateから作った既存プロジェクトにcruftを導入する方法を解説します。まずは下記を実行します。

cruft link https://github.com/m3dev/cookiecutter-gokart

上記コマンドで、cookiecutterからプロジェクトを作る時と同じように、templateの値についての質問されるので、プロジェクトを作った時と同じ値を回答します。そうすると下記のようにcruft.jsonが作成されます。

{
  "template": "https://github.com/m3dev/cookiecutter-gokart",
  "commit": "25b2ea60fd1b3145908b750fc0e42e130913c7c5",
  "checkout": null,
  "context": {
    "cookiecutter": {
      "project_name": "xxx",
      "package_name": "xxx",
      // ...
      "_template": "https://github.com/m3dev/cookiecutter-gokart"
    }
  },
  "directory": null,
}

確実にdiffが出てしまうファイルに関してはcruft.jsonskipフィールドを追加できます。

{
  "template": "https://github.com/m3dev/cookiecutter-gokart",
  ...
  "skip": [
    "xxx",
    ".venv",
    "poetry.lock",
    "pyproject.toml"
  ]
}

この状態で下記を実行するとcruft.jsoncommitが最新のcookiecutterのcommitに追従していることを確認できます。

cruft check

もし、上記コマンドに失敗した場合は、cruft.jsoncommitとcookiecutterの最新コミットと差分があるので、その差分をチェックし、可能ならそのまま取り込みます。

cruft update

cruft checkに失敗している場合、CUIでどのアクションを行うかを問われます。cruft.jsoncommitから最新のコミットまでの変更を確認したい場合は[v]、変更を適用したい場合は[y]を選択します。適用したくない場合は[s]を選択します。これで簡単にcookiecutterの最新のcommitに追従できます。

cruft.jsoncommitから最新のコミットまでの変更ではなく、最新cookiecutterと現在のプロジェクトの差分を丸々見たい場合は下記を実行します。

cruft diff

古い既存プロジェクトに導入する場合は複数のdiffが出るので、diff単位で確認しながら適用したいdiffだけを適用する下記のコマンドが便利です。

cruft diff | git apply - && git add -p

CIの設定

cruftを使って常に最新の状態を確認して必要なdiffを取り込んだかをチェックするために、弊チームではcruft checkを行うCIを設定しました。弊社ではGitLabを利用しているので、GitLabでの設定方法を主に説明します。

CIでcruft checkするための設定を.gitlab-ci.yml追加します。この際にマージリクエストにコメントを残したい場合はgitlab-commentが便利です。このツールは弊社SREのyuyabanさんが開発しています。

github.com

## `GITLAB_ACCESS_TOKEN`を用意しておく。

cruft_check:
  stage: test
  image: <<何かしら素敵なpython image>>
  before_script:
    # Gitのセットアップなど...
    # ...

    - pip install --upgrade pip
    - pip install cruft
    - wget -q https://github.com/yuyaban/gitlab-comment/releases/download/v0.2.3/gitlab-comment_0.2.3_linux_amd64.tar.gz
    - tar -zxvf gitlab-comment_0.2.3_linux_amd64.tar.gz
    - chmod +x gitlab-comment
    - mv gitlab-comment /usr/bin/gitlab-comment
    - rm gitlab-comment_0.2.3_linux_amd64.tar.gz
  script:
    - |
      export GITLAB_ACCESS_TOKEN=$CRUFT_GITLAB_TOKEN
      cruft check || exit_code=$?
      if [ $exit_code -ne 0 ]; then
        export GITLAB_ACCESS_TOKEN=$CRUFT_GITLAB_TOKEN
        gitlab-comment post -k cruft_check -u 'Comment.HasMeta && Comment.Meta.TemplateKey == "cruft_check"' --var target:"${CI_JOB_NAME}"
        exit 1
      fi

ここでGitのセットアップをしていますが、これはcruftが内部でcookiecutter templateをcloneしてdiffを確認するためです。cookiecutter templateがプライベートリポジトリにある場合はアクセスできるようにしておきましょう。

このCIの設定は既存プロジェクトにすぐに導入できるように、template化してチーム内で利用できるようにしてあります。詳しくはGitLab CIのtemplate基盤を構築した際に書いた下記のブログをご覧ください。

www.m3tech.blog

そして、check結果をMRのコメント欄に通知するために、gitlab-comment.yamlを追加します これでgitlab-commentがどのコメントを更新すれば良いかを判別できます。

---
post:
  default: |
    hello gitlab-comment !
  cruft_check:
    # update: 'Comment.HasMeta && Comment.Meta.TemplateKey == "cruft_check"'
    template: |
      最新のcookiecutterに追従できていません。ローカルで ```cruft update``` を実行して、最新のcookiecutterに追従しましょう。
      https://*****(ドキュメントへのリンクなど)

これでCIでcruft checkをして、もし最新版に追従していない場合はマージリクエストにコメントが残ります。cruftの運用ドキュメントなどへのリンクも置いておくと便利です。

弊チームの運用では、メインのロジックやミドルウェアがプロジェクトごとに違うので、全てのプロジェクトに確実に差分が出てしまいます。そうすると毎回マージリクエストのたびにコメントが投稿されてしまいます。そのため今回はCIにcruft diffの結果をアップロードする運用は見送りました。

cookiecutter template自体へのcruft導入

cookiecutter templateには上記で説明したCIの設定を置いておくだけでOKです。

さらに発展の運用として、templateでcruft.jsonの共通設定を置いておきたい場合などの対応も紹介します。例えばcruft.jsonskipフィールドをtemplateに設定として置いておきたい場合などが挙げられます。

そこで、templateにcruft.jsonを事前に追加しておくなどの対応ができます。しかし、ここでcruft.jsonに設定したcommitフィールドはどんどん古くなっていきます。その対応としてcookiecutterからプロジェクトを作成した段階で最新のコミットにするようにPost-Generate Hooksを設定できます。

Pre/Post-Generate Hooks機能に関するドキュメントはこちら

cookiecutter.readthedocs.io

プロジェクト作成時にcruft updateが走るようにするコード例は下記になります。cruft updateコマンドに-sオプションをつけることで、コミットIDの更新だけを行ってくれます。

import subprocess
from pathlib import Path
import shutil

# setup cruft 
subprocess.check_call(['poetry', 'run', 'cruft', 'update', '-s'])

これをドキュメントの通りhooksディレクトリに入れておけばcookiecutterからのプロジェクト作成時に新しいコミットIDに書き換えてくれます。弊社ではskipするディレクトリやファイルがプロジェクトごとにかなり変わるので、一旦はcruft.jsonをtemplateに置いておくのではなく、cruft createコマンドでできるcruft.jsonにプロジェクトごとに手動でskipなどの設定を追加する運用を取っています。

チームへの導入

cruftをチームへ導入する際の障壁はcookiecutterで作った既存の全てのプロジェクトに導入する必要があることです。ここをパッと自動化することは難しいため(何か素敵なアイデアがあれば教えてほしい)、チームのみんなに協力してもらう必要があります。

そのために僕が行った取り組みは下記です。

  • 既存プロジェクトへの導入のためのドキュメントを書く
  • 参考として導入事例のプロジェクトを一個作る
  • 簡単にCIをセットアップできるようにGitLab CI templateを用意
  • チームで週1で行っている技術共有会でcruftを紹介。運用方法について合意を取る。

このように大量のプロジェクトに変更する場合は、チームメンバーに協力してもらうための障壁を落とす必要があります。今までセキュリティチェックツールなどを導入する場合には同じようなことをしていましたが、cruft導入を突破してしまえば楽なのでがんばりましょう。

まとめ

cruftの導入によって、最新版cookiecutterへの追従や変更の確認を簡単に行う方法を紹介し、実際にチームに導入する方法なども紹介しました。cookiecutterからプロジェクトを作成してガンガン開発するようなチームであれば是非導入を検討してみてください。

We're hiring!

エムスリーではエンジニアの開発体験をゴリゴリ改善する仲間を募集しています。我こそはという方はぜひ以下からご応募ください!

jobs.m3.com