本記事は、Ansible Advent Calendar 2020 の24日目です。
はじめに
ここではpre-commitとAnsible-lintを使って、コードをcommitする前に静的コード解析を行うための手法をご紹介します。
Ansibleを開発するにあたって、Ansible-lintや、yamllintを導入したことがある方は多いと思いますが、以下のような不満を目にしたことがあります。
- 開発メンバーにコードをpushする前にlintをかけてとお願いしたが徹底できない
- CI/CDを回してからSyntaxエラーが見つかる
他にも挙げればきりがないですが、よく聞く話ですとここら辺でしょうか。
特にCI/CDを回してからSyntaxエラーが見つかるって、やるせないですし、動かないコードが含まれたcommitがpushされること自体がストレスですよね。
そこで紹介したいのがpre-commitです。
pre-commit
まずはpre-commitのネームスペース衝突問題から解説しましょう。
「pre-commit」というキーワードで検索されると以下の二つが検索に引っかかります。
- gitの機能であるpre-commit hooksの略称
- 「pre-commit」と言うOSSソフトウェア
今回紹介するのは後者で、pre-commitと言うOSSソフトウェアのことを取り扱います。
このドキュメンテーションでは前者をgit pre-commit hooks、後者のことをpre-commitと表記することにします。
では次に、pre-commitとは何かをお話しさせてください。
pre-commitとはgitのpre-commit hooksの為の多言語パッケージマネージャーのことです、gitのpre-commit hooksを便利に管理してくれるフレームワークだと思ってください。
pre-commitの特徴
pre-commitが叶えてくれる事は以下のような事です。
- git pre-commit hooksのスクリプト管理
- プロジェクト事に異なるgit pre-commit hooksのconfiguration化
- commit前にコードの問題を静的解析で見つけてくれる
特に筆者が優れているなと思えるのは下記の部分になります。
- .pre-commit-config.yamlに記載するだけで、外部のレポジトリからlintにの実行に必要なライブラリをインストールしてくれる
- git pre-commit hooksに対する知識がなくても簡単に導入できる
- localのコマンドも設定でき、自作のlint toolも動かすことができる
では早速使い方を紹介していきます。
pre-commitのインストール
pre-commitのインストールはとても簡単で、以下の手順だけで済みます。
curl https://pre-commit.com/install-local.py | python3 -
pre-commitのconfiguration
pre-commitのconfigrationはgit repositoryのrootに**.pre-commit-config.yaml**と言う名前のファイルを作成して設定していきます。
また、pre-commit自体がsample-configを吐き出してくれるオプションも持っています。
❯ pre-commit sample-config
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
簡単にconfigurationを説明すると、「repos」の中にある「repo」が提供されるlintの一塊りだと思ってください。
その中にある「id」が単一のlint ruleのようなものです。
上記のconfigartionの中にある「id」がどう言った振る舞いをしているかに関しては、以下のドキュメンテーションが理解の助けになります。
Support hooks
ここで一つ注意が必要なのが、-fixerの名前で終わっているhooksに関しては修正まで行ってしまうことに気をつけてください。
一つ一つのconfigurationについては、pre-commitのドキュメンテーションをみてもらえればいいと思っています。
Creating new hooks
pre-commitの使い方
Step by Stepの手順ではありませんが、みなさんpre-commitのconfigurationまで作れたでしょうか。
それでは実際にpre-commitを使っていこうと思います。
pre-commitは設定ファイルを書いただけでは動作してくれません、それはなぜでしょう?
それは、git pre-commit hooksを利用して動くので、.git/hooks/pre-commitにpre-commit用のpythonスクリプトを配置しなければならないからです。
それでは早速、スクリプトの配置を行っていきましょう。
.pre-commit-config.yamlが配置されているカレントディレクトリで以下のコマンドを実施してください。
❯ pre-commit install
pre-commit installed at .git/hooks/pre-commit
pre-commit installed at .git/hooks/pre-commitと言うメッセージが出てくれば成功です。
もちろん、このスプリプトの中身も見ることができます。
どうやらpre-commitで必要なライブラリはHome directoryの.pre-commit-venvに格納するみたいですね。
virtual envを切って実装してくれるのはとてもありがたいですね。
❯ cat .git/hooks/pre-commit
#!/usr/bin/env python3
# File generated by pre-commit: https://pre-commit.com
# ID: 138fd403232d2ddd5efb44317e38bf03
import os
import sys
# we try our best, but the shebang of this script is difficult to determine:
# - macos doesn't ship with python3
# - windows executables are almost always `python.exe`
# therefore we continue to support python2 for this small script
if sys.version_info < (3, 3):
from distutils.spawn import find_executable as which
else:
from shutil import which
# work around https://github.com/Homebrew/homebrew-core/issues/30445
os.environ.pop('__PYVENV_LAUNCHER__', None)
# start templated
INSTALL_PYTHON = '/Users/yyamashi/.pre-commit-venv/bin/python3'
ARGS = ['hook-impl', '--config=.pre-commit-config.yaml', '--hook-type=pre-commit']
# end templated
ARGS.extend(('--hook-dir', os.path.realpath(os.path.dirname(__file__))))
ARGS.append('--')
ARGS.extend(sys.argv[1:])
DNE = '`pre-commit` not found. Did you forget to activate your virtualenv?'
if os.access(INSTALL_PYTHON, os.X_OK):
CMD = [INSTALL_PYTHON, '-mpre_commit']
elif which('pre-commit'):
CMD = ['pre-commit']
else:
raise SystemExit(DNE)
CMD.extend(ARGS)
if sys.platform == 'win32': # https://bugs.python.org/issue19124
import subprocess
if sys.version_info < (3, 7): # https://bugs.python.org/issue25942
raise SystemExit(subprocess.Popen(CMD).wait())
else:
raise SystemExit(subprocess.call(CMD))
else:
os.execvp(CMD[0], CMD)
それではpre-commitを実際に走らせてみましょう。
まずは、git pre-commit hooksを経由しないで直接pre-commitを実行してみましょう。
その場合はpre-commit runコマンドを利用します。
ただこの時注意なのが基本的にはgit addされてstagingに上がっているファイルのみをlintにかける振る舞いをします、なのでgit addしてlintにかけたいファイルをgit addしてから試してください。
❯ git add .pre-commit-config.yaml
❯ git add .
❯ pre-commit run
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed
問題なく通りましたね、では次にあえてエラーを発生させてみましょう。
以下のようにnginxを呼び出す為のsite.ymlの一部をkey value pareに変更してみました。
この記法はAnsibleでは問題なく通りますが、yamlのsyntaxでは認められていませんので、check-yaml hooksエラーを検知できることを期待しています。
❯ git diff
diff --git a/site.yml b/site.yml
index c839566..3221f74 100644
--- a/site.yml
+++ b/site.yml
@@ -3,7 +3,7 @@
name: 'Install ando configure nginx'
gather_facts: true
tasks:
- - name: 'Import nginx role'
+ - name='Import nginx role'
import_role:
name: 'nginx'
❯ git add site.yml
❯ pre-commit run
Fix End of Files.........................................................Passed
Check Yaml...............................................................Failed
- hook id: check-yaml
- exit code: 1
mapping values are not allowed in this context
in "site.yml", line 7, column 18
Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed
確かにLine7でエラーを検知できていますね!
では、最後にgit commitを実行してpre-commitがgit pre-commit hooksから呼び出されるか確認しましょう。
❯ git commit -m '[Add] Add nginx and haproxy roles'
Fix End of Files.........................................................Passed
Check Yaml...............................................................Failed
- hook id: check-yaml
- exit code: 1
mapping values are not allowed in this context
in "site.yml", line 7, column 18
Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed
実験成功ですね!これで、pre-commitの導入自体は完了です。
pre-commitとansible-lintのintegration
さて、ここからが本筋です、pre-commitにansible-lintをインテグレーションして、実践的で今すぐ使えるpre-commit環境を作っていきましょう。
まずは以下のようなconfigurationを作りましょう。
❯ cat .pre-commit-config.yaml
---
# yamllint disable rule:quoted-strings
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: end-of-file-fixer
- id: check-yaml
- id: mixed-line-ending
- id: check-case-conflict
- id: check-merge-conflict
- repo: https://github.com/ansible/ansible-lint.git
rev: v4.3.7
hooks:
- id: ansible-lint
name: Ansible-lint
description: This hook runs ansible-lint.
entry: ansible-lint
language: python
# do not pass files to ansible-lint, see:
# https://github.com/ansible/ansible-lint/issues/611
always_run: true
pass_filenames: false
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.25.0
hooks:
- id: yamllint
files: \.(yaml|yml)$
types: [file, yaml]
entry: yamllint -c .yamllint.yml --strict
次にgit add .pre-commit-config.yamlを実行して、実際にpre-commitからansible-lintが呼び出せるか確認しましょう。
❯ git add .pre-commit-config.yaml
❯ pre-commit run
Fix End of Files.........................................................Passed
Check Yaml...............................................................Failed
- hook id: check-yaml
- exit code: 1
mapping values are not allowed in this context
in "site.yml", line 7, column 18
Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed
Ansible-lint.............................................................Passed
yamllint.................................................................Failed
- hook id: yamllint
- exit code: 1
site.yml
7:18 error syntax error: mapping values are not allowed here (syntax)
ansible-lintとyamllintの処理が追加されていますね。
先ほどあえて引っかかるように作った、site.ymlがansible-lintでは通るが、yamllintとcheck-yamlで引っかかっていることが確認できるかと思います。
site.ymlを修正して引っかかる箇所がないかを確認します。
❯ git diff
diff --git a/site.yml b/site.yml
index 3221f74..c839566 100644
--- a/site.yml
+++ b/site.yml
@@ -3,7 +3,7 @@
name: 'Install ando configure nginx'
gather_facts: true
tasks:
- - name='Import nginx role'
+ - name: 'Import nginx role'
import_role:
name: 'nginx'
❯ git add site.yml
❯ pre-commit run
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed
Ansible-lint.............................................................Passed
yamllint.................................................................Passed
問題なく通りましたね、では次にansible-lintで引っかかるような部分を作って、動作を確認してみましょう。
❯ git diff
diff --git a/roles/nginx/molecule/docker/create.yml b/roles/nginx/molecule/docker/create.yml
--- a/roles/nginx/molecule/docker/create.yml
+++ b/roles/nginx/molecule/docker/create.yml
@@ -10,14 +10,14 @@
register: 'result'
- name: 'Build container image'
- command: 'docker build -t centos8_ssh .'
+ shell: 'docker build -t centos8_ssh .'
args:
chdir: './assets'
changed_when: false
when: '"localhost/centos8_ssh" not in result.stdout'
❯ git add roles/nginx/molecule/docker/create.yml
❯ git add .pre-commit-config.yaml
❯ git commit -m '[Add] Add molecule for docker'
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed
Ansible-lint.............................................................Failed
- hook id: ansible-lint
- exit code: 2
[305] Use shell only when shell functionality is required
roles/nginx/molecule/docker/create.yml:12
Task/Handler: Build container image
You can skip specific rules or tags by adding them to your configuration file:
┌──────────────────────────────────────────────────────────────────────────────┐
│ # .ansible-lint │
│ warn_list: # or 'skip_list' to silence them completely │
│ - '305' # Use shell only when shell functionality is required │
└──────────────────────────────────────────────────────────────────────────────┘
yamllint.................................................................Passed
きちんとshellじゃなくて、commandを使え!って怒られましたね。
実験成功です!
それでは問題の箇所を修正してcommitをしてみましょう!
❯ vim -c 12 roles/nginx/molecule/docker/create.yml
❯ git diff
diff --git a/roles/nginx/molecule/docker/create.yml b/roles/nginx/molecule/docker/create.yml
--- a/roles/nginx/molecule/docker/create.yml
+++ b/roles/nginx/molecule/docker/create.yml
@@ -10,14 +10,14 @@
register: 'result'
- name: 'Build container image'
+ command: 'docker build -t centos8_ssh .'
- shell: 'docker build -t centos8_ssh .'
args:
chdir: './assets'
changed_when: false
when: '"localhost/centos8_ssh" not in result.stdout'
❯ git add roles/nginx/molecule/docker/create.yml
❯ git commit -m '[Add] Add molecule for docker'
[INFO] Stashing unstaged files to /Users/yyamashi/.cache/pre-commit/patch1608791209.
Fix End of Files.........................................................Passed
Check Yaml...............................................................Passed
Mixed line ending........................................................Passed
Check for case conflicts.................................................Passed
Check for merge conflicts................................................Passed
Ansible-lint.............................................................Passed
yamllint.................................................................Passed
[INFO] Restored changes from /Users/yyamashi/.cache/pre-commit/patch1608791209.
[master 197b9da] [Add] Add molecule for docker
1 file changed, 1 insertion(+), 1 deletion(-)
やったー!コミットすることに成功しました!
細かいyammlintや、ansible-lintのconfigは**.yamllint.ymlや、.ansible-lint**に記述してあげてください。
私たちのチームではコーディング規約をlint rule化しているので、以下のようなconfigを書いています。
❯ cat .ansible-lint
exclude_paths:
- .pre-commit.yaml ### コーディング規約で.yamlを許可していないので、泣く泣くexclude...
parseable: false
quiet: false
rulesdir:
- ./rules/ ### Custom lint ruleのディレクトリ
skip_list:
- '301'
use_default_rules: true
終わりに
これでみなさんのansible開発レポジトリのcommitも綺麗になること間違いなしですね!
お付き合いいただき、ありがとうございました。
引き続きAnsibleアドベントカレンダーをおたのしみください!
明日はHideki Saitoさんが今年のAnsibleを締め括ってくれます!