📖

Copierを使ってテンプレートを管理する

2024/11/08に公開

Copierを使ってテンプレートを管理する

4月に中途で入社したデジタルイノベーション本部の田中です。配属されたチームでは銀行全体で使用するシステムの共通リソースの開発を担当しています。入社してからはsemantic-release、.gitignoreなどの汎用的な機能を準備してきました。配属されたチームはこれらの必要な機能を備えたテンプレートにして公開しています。

既存のプロジェクトにもテンプレートを適用したい

社内にはサブシステムの単位でソースコード、テストなどがまとまった開発プロジェクト(以下プロジェクトと略します)が多数存在しています。テンプレートというと、プロジェクト必要なファイル配置されたプロジェクトがあり、テンプレートをベースに開発していき、1からの開発に適用するとき使われる印象がある人もいるかもしれません。私たちが機能を提供している開発チームは1から作るよりも既存のシステムに機能を含めたテンプレートを適用する使い方が多く、1からの開発は多くありません。そのため、既存のプロジェクトにテンプレートを適用できることが必要でした。また、導入後にもテンプレートの機能を改善していく中で、利用者側にも継続的に取り込める仕組みが必要でした。

その中で、使い始めたツールがCopierです。以下がCopierの公式リファレンスになります。各機能の詳細はリファレンスを参照してください。
Copier公式リファレンス

Copierとは

Copierとはテンプレート内のファイルを各プロジェクトに配布、更新を管理してくれるライブラリになります。同じようなことができるツールにテンプレを作る(cookiecutter)と更新を取り込む(cruft)を組み合わせた実現方式がありますが、1つのツールでテンプレートの更新が可能になることがCopierのメリットです。cookiecutter+cruftとCopierの比較記事が参考にさせて頂きました。
cookiecutterとcruftとcopier

Pythonで作られていて、Gitで変更を管理しているため、PythonとGitが前提になります。詳細は公式リファレンスを参照して下さい。

前提を満たしていれば、以下のコマンドでCopierをインストールするだけで利用できます。
pip install copier

Copierができることは三段階あります。

  • テンプレートを作る
  • プロジェクトにテンプレートを適用する
  • (更新されたら)プロジェクトにテンプレートの変更を取り込む

Copierのメリット

Copierを使うことによって、テンプレートの適用後にテンプレートが変更された場合、利用者が追従して取り込めるメリットがあります。また、変更の取り込みタイミングを利用者が選べるため、利用者側が開発状況に併せて、テンプレートを適用するスケジュールを組めます。

このCopierを使って、semantic-release、.gitignoreなどの汎用機能を含めたJavaのプロジェクトのテンプレートを準備してきました。以降で公式リファレンスを元にテンプレートを構築した中で、工夫した点をテンプレートを作り、適用する流れに合わせて紹介していきます。

テンプレートを作る

配布したいテンプレートを作ります。以下がCopierで作るテンプレート例です。

my_copier_template
├── copier.yml  
└──  generate_root
    │── template1.json
    │── template2.yml
    │── template3.yml.jinja
    └── copier-answers
    └── copier-answers.my_copier_template.yml.jinja 

copier.yml 、copier-answers.my_copier_template.yml.jinja がCopierの設定ファイルになります。Copierの設定ファイル以外がテンプレートとして配布したいファイルになります。generate_root以下のファイルがテンプレートを適用するプロジェクト配下にディレクトリ構成を維持したまま展開されます。

展開されると以下の構成になります。後述しますが、jinjaファイルは展開されるとjinjaの拡張子が外れ、展開されます。

my_project 
├── template1.json
├── template2.yml
├── template3.yml
└── copier-answers
    └── copier-answers.my_copier_template.yml

次にこの構成にしている理由や、Copierの構成を紹介します。

Copierの定義ファイル

定義が必要なファイルは copier.yml だけです。copier-answers.my_copier_template.yml.jinja もCopierの定義ファイルの1つですが、これはテンプレートを適用した側で質問の回答結果を保持するファイルになります。定義は不要で、以下の定義を入れておけば、Copierの機能で更新してくれます。

copier-answers.my_copier_template.yml.jinja

{{ _copier_answers|to_nice_yaml -}}

次に定義が必要なcopier.ymlを紹介します。Copierはテンプレートを適用する際に、CI機能を使うか。などの質問ができます。回答はテンプレートの可変部分を置換するために使います。

以下はCI機能を持ったテンプレートにて、Copierで定義している質問の例です。

copier.yml

# questions
use_user_ci:
  type: bool
  help: Does this project use own custom GitLab-CI?
  default: yes

custom_gitlab_ci_path:
  type: str
  help: Path to own custom GitLab-CI configuration file
  when: "{{use_user_ci}}"
  default: "gitlab-ci/.gitlab-ci.user.yml"

それぞれの質問項目の意味は以下です。

  • use_user_ci
    プロジェクト固有のGitLab-ciを定義するかを質問しています
  • custom_gitlab_ci_path
    プロジェクト固有のGitLab-ciを使う場合、固有のGitLab-ciのパスを質問しています
    この質問は use_user_ci がYesの時のみ必要なため、Noの場合は質問しないように条件をつけています

サブディレクトリに分けた理由

テンプレートのプロジェクト直下に置くことも可能ですが、サブディレクトリ(上記の例のgenerate_root)を配下に配布したいリソースを置くようにしています。プロジェクトの直下には.gitignoreファイルだったり、配布したいリソース以外にプロジェクトのメタデータが含まれます。そのため、配布したいリソースとテンプレートを適用するプロジェクトのメタデータが混同してしまいます。そのため、サブディレクトリを使用して、配布するリソースとテンプレートのメタデータを分けています。サブディレクトリを使うことによって、サブディレクトリ配下をテンプレートのルートとして扱ってくれます。

設定の適用には以下を入れるだけです。

copier.yml

_subdirectory: "generate_root"

Jinja2テンプレート

CopierにはJinja2テンプレートエンジンが使用されています。Jinja2テンプレートエンジンは変数やIF文などの制御構文を用いて、テキストベースのフォーマットを動的に生成するツールです。Jinja2はJinjaの後継であり、ファイル拡張子はjinjaになります。

Copierはテンプレートの適用時に質問できることを利用し、質問の回答はJinja2が使用できる変数になります。これを用いて、質問の回答によって、配布するリソースを切り分けることができます。たとえば、ユーザー側でCIを独自実装するかどうかをCopierで質問し、する場合のみ、独自実装用のファイルをincludeさせることができます。

.gitlab-ci.yml.jinja

include:
  - local: 'gitlab-ci/.gitlab-ci.semantic-release.yml'
  {% if use_user_ci -%}
  - local: '{{ custom_gitlab_ci_path }}'
  {%- endif %}

use_user_ciがTrue(Yes)の場合、custom_gitlab_ci_pathに指定した回答結果をinclude属性に追加します。jinjaファイルは変数や制御文を変換したうえでjinja拡張子が外れた状態のファイルを展開します。テンプレートの.gitlab-ci.yml.jinjaは適用したプロジェクトには.gitlab-ci.ymlで展開されます。

.gitlab-ci.yml

include:
  - local: 'gitlab-ci/.gitlab-ci.semantic-release.yml'
  - local: 'gitlab-ci/.gitlab-ci.user.yml'

copier-answersのディレクトリにまとめる

Copierの公式リファレンスはcopier_answerファイルをディレクトリに切ることはせず、プロジェクトルートに配置しています。これに対して、copier-answerファイル用のディレクトリを切り、その配下にcopier-answerファイルをまとめています。これは複数のテンプレートを導入する前提だったため、プロジェクトルートにcopier-answerファイルが増え続けてしまうことが懸念したからです。

copier-answerファイル用のディレクトリを切り、複数のテンプレートを適用すると以下のようになります。

my_project 
├── <template1のルート直下に配布したいファイル>
├── <template2のルート直下に配布したいファイル>
└── copier-answers
    └── copier-answers.template1.yml
    └── copier-answers.template2.yml

適用するテンプレートが増える場合もcopier-answersのディレクトリにcopier-answerファイルを増えるだけになり、ルート直下は配置したいリソースのみに抑えることができました。

copier-answerファイル名をテンプレート側で固定

copier-answerファイルは以下のようになっており、テンプレートのURLや最後に適用したバージョン、質問の最新の回答が保存されています。

# Changes here will be overwritten by Copier
_commit: v1.3.1
_src_path: https://gitlab.com/muit/copier/templates/semantic-release.git
name: semantic-release

更新に必要な情報がcopier-answerファイルに保存されているため、適用しているテンプレートごとに生成されます。Copierのデフォルトでは.copier-answers.ymlとして生成されるため、複数のテンプレートを適用する場合、他のテンプレートのcopier-answerファイルに上書きをしてしまう場合があります。複数テンプレートを導入する前提のため、命名ルールを.copier-answers.<テンプレートのプロジェクト名>.yml.jinjaとし、テンプレート側でファイル名を固定しました。

テンプレートを適用する

ここから作ったテンプレートをプロジェクトに適用していきます。

最低限のコマンドは以下です。
copier copy <テンプレートのGitリポジトリORテンプレートのパス> <テンプレートを適用するプロジェクトのパス>

実行すると以下のようにCopierに設定された質問を対話形式で回答すると、回答に従ってテンプレートが適用されます。

copier copy https://github.com/muit/copier/templates/semantic-release.git C:\workspace\test_project
🎤 A nice human-readable name
   test_project
🎤 A slug of the name
   test_project
🎤 プロジェクト固有で.gitignoreに追加したい除外するパターンを入力してください。複数行可
    (Finish with 'Alt+Enter' or 'Esc then Enter')
> *.class
🎤 Subdirectory to use as the template root when generating a project
   generate_root
🎤 Does this project semantic release?
   Yes
Next steps:

1. Change directory to the project root:

   $ cd C:\workspace\test_project

非対話形式も用意されています。

  • --data コマンドラインの引数で回答を指定
  • --data-file回答をファイルにまとめて取り込む

回答が完了すると、テンプレートのファイルが適用先に展開されます。テンプレートはGitリポジトリのデフォルトブランチのタグから最新のバージョンが取得されます。テンプレートにはコミットメッセージにもとづいて自動的にバージョン管理、およびパッケージの公開するツールであるsemantic-releaseを導入しておくと、テンプレートの運用が簡略化されます。

新しいプロジェクトだけでなく、既存のプロジェクトにテンプレートを構成するファイルを展開して適用できます。以下は既存のプロジェクトに対するテンプレートの適用前後の構成になります。

テンプレート適用前のプロジェクト構成

テンプレート適用後のプロジェクト構成

テンプレートの変更を取り込む

提供しているテンプレートも機能拡張や使用しているツールの更新によって、更新されていきます。Copierによるテンプレートの更新を反映する方法はテンプレートを適用しているプロジェクトが以下のコマンドを打つだけです。
copier update -a <<answer file name>>

answerfileには、最後に適用したバージョンが記録されているため、適用後から最新までを比較し、差分があれば更新の差分を取り込みしてくれます。最後に適用したバージョンが 1.0.0 だった場合、 1.0.0 から最新のバージョン 1.1.0 までに変更、追加されたファイルをプロジェクト側に更新してくれます。

適用はテンプレートのバージョンアップと非同期で行えるため、プロジェクト側が更新時期を決められるようにしています。

終わりに

Copierについては日本語の記事があまりなく、英語の公式のガイドやGitHubのIssueを読み、理解しながら構築してきました。実際に使ってみて、公式のガイドのとおり作るだけでは使いにくく感じてしまう点があったため、公式が掲示している形を少しアレンジしました。まだ導入始めたばかりで試行錯誤している状態ではありますが、便利なツールを実際の現場に適用していく際に、見つかった課題や対処法がCopierを使う人の導入の助けになれば幸いです。

参考資料

Discussion