【Ansible】YAMLからマークダウン手順書をお手軽に作りたい

### はじめに

- バージョン
  - Ansible:2.9.7
  - Jinja2: 3.1.0

　最近、マークダウンで手順書などを作ることが増えてきたのですが、連番を降ったり空行を入れたりが面倒になってきたので、AnsbileのtemplateモジュールでYAMLからマークダウンを自動作成する仕組みを考えてみました。  
　手順書の標準化にも役立つ内容かと思いますのでぜひご覧ください。

### 1. ディレクトリ構成

　ディレクトリ構成は以下のようになっています。マークダウン作成用Playbookが変数ファイルを読み込み、マークダウンを作成する構成としています。

```text
./
 ├ ansible.cfg
 ├ markdown_create.yml
 ├ settings/
 │  └ markdown.yml
 ├ templates/
 │  ├ markdown_main.j2
 │  └ markdown_macro.j2
 └ output/
    └ result.md
```

|ファイル名|用途|
|---|---|
|markdown_create.yml|マークダウン作成用Playbook|
|markdown.yml|変数ファイル。作成したい手順書の内容を記載する|
|markdown_main.j2|手順書作成用テンプレート。<br>Playbookは実行時にこのファイルを指定する|
|markdown_macro.j2|関数のみのテンプレート。<br>テンプレートが使用するマクロ(関数)を記載する|
|result.md|作成されたマークダウン|


### 2. マークダウン作成用Playbook

　Playbookに関しては特に変わったことはしていません。include_varsでYAMLファイルを読み込んで変数markdown_settingに格納し、tamplateモジュールでマークダウンを作成します。

```yaml:markdown_create.yml
---
- name: markdown create
  hosts: localhost
  gather_facts: false
  tasks:
    - include_vars:
        file: "./settings/markdown.yml"
        name: markdown_setting

    - name: markdown create
      template:
        src: "./templates/markdown_main.j2"
        dest: "./output/result.md"
```

### 3. 変数ファイル

　変数ファイルは、以下のようになっています。

```yaml:markdown.yml
---
title: h1    # h1ヘッダー
description: description
chapter:
  - title: h2-1    # h2ヘッダー
    title_number: true
    content_number: false
    content:
      - line: h2-1 line
        line_sub: h2-1 line sub
    chapter: []
  - title: h2-2    # h2ヘッダー
    title_number: true
    content_number: true
    content:
      - line: h2-2 line
        line_sub: h2-2 line sub
    chapter:
      - title: h3-1    # h3ヘッダー
        title_number: false
        content_number: false
        content:
          - line: h3-1 line
            line_sub: h3-1 line sub
        chapter: []
```

仕様としては、以下のようになっています。(テンプレートの説明は後述)

1. キー``title``のインデントが深いほどヘッダー(#)の数が増える(h4ヘッダーまで)
1. ``title_number: true``にすると、キー``title``に連番が振られる
1. ``content_number: true``にすると、キー``content``に連番が振られる

```markdown
## 1. header    <- フラグtitle_numberにより番号が追加

1. line         <- フラグcontent_numberにより番号が追加
   - line sub
```

次の階層のヘッダーを作る必要が無い場合は、キー``chapter``の内容を空のリストにするだけで 階層の調整が可能です。

### 4. テンプレートファイル

　テンプレートは、マクロ機能を使って可能な限りコードの行数を減らしました。mainテンプレートはヘッダー(h1～h4)ごとに、macroを呼び出す構成にしています。連番を振る際にfor文のループ変数をカウントするためのリストを作成して、リストの最後の要素の数字に1加えた値を要素として追加する方式にしました。

```jinja
{% set cnt = [0] -%}
{%- for i in ["a", "b", "c"] -%}
{%- set _ = cnt.append(cnt[-1] + 1) -%}
{{ i }}: {{ cnt[-1] }}
{% endfor -%}
```

#### 4-1. mainテンプレート

　mainテンプレートは前述のようにmacroの呼び出しを行い、マクロ内の関数を実行します。

```jinja:markdown_main.j2
{%- import "markdown_macro.j2" as macro -%}
# {{ markdown_setting.title }}

{{ markdown_setting.description }}

{% set h2_cnt = [0] -%}
{%- for h2 in markdown_setting.chapter -%}
{%- set _ = h2_cnt.append(h2_cnt[-1] + 1) -%}
{{ macro.output_chapter("##", h2, h2_cnt[-1]) }}
{% set h3_cnt = [0] -%}
{%- for h3 in h2.chapter -%}
{%- set _ = h3_cnt.append(h3_cnt[-1] + 1) -%}
{{ macro.output_chapter("###", h3, h3_cnt[-1]) }}
{% set h4_cnt = [0] -%}
{%- for h4 in h3.chapter -%}
{%- set _ = h4_cnt.append(h4_cnt[-1] + 1) -%}
{{ macro.output_chapter("####", h4, h4_cnt[-1]) }}
{% endfor -%}
{%- endfor -%}
{%- endfor -%}
```

#### 4-2. macroテンプレート

　マクロテンプレートでは、フラグ``title_number``と``content_number``により、
ヘッダー配下に記載する内容を分岐します。

```jinja:markdown_macro.j2
{%- macro output_chapter(header, chapter, header_cnt) -%}
{%- if chapter.title_number -%}
{{ header }} {{ header_cnt }}. {{ chapter.title }}

{% else -%}
{{ header }} {{ chapter.title }}

{% endif -%}
{%- set content_cnt = [0] -%}
{%- if chapter.content %}
{%- for chapter_content in chapter.content -%}
{%- set _ = content_cnt.append(content_cnt[-1] + 1) -%}
{%- if chapter.content_number and chapter_content.line_sub -%}
{{ content_cnt[-1] }}. {{ chapter_content.line }}
{{ ' ' * (content_cnt[-1] | string | length) }}  - {{ chapter_content.line_sub }}
{% elif chapter.content_number -%}
{{ content_cnt[-1] }}. {{ chapter_content.line }}
{% elif chapter_content.line_sub -%}
- {{ chapter_content.line }}
  - {{ chapter_content.line_sub }}
{% else -%}
- {{ chapter_content.line }}
{% endif -%}
{%- endfor -%}
{%- endif -%}
{%- endmacro -%}
```

### 5. 実行結果

#### 5-1. 作成されたマークダウン

　以下が実際に作成されるマークダウンです。YAMLファイルからマークダウンが自動生成
されるようになりました。手順書が機械的に作成されるので、改行の仕方など作業者による
違いもなくレビューが楽になります。

```markdown
# h1

description

## 1. h2-1

- h2-1 line
  - h2-1 line sub

## 2. h2-2

1. h2-2 line
   - h2-2 line sub

### h3-1

- h3-1 line
  - h3-1 line sub
```

#### 5-2. レンダリング結果

　レンダリング結果は以下です。マークダウンはヘッダーで自由に文字サイズが変更できるので便利ですね。

![markdown.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554812/f63464b7-b02c-3bc5-418a-08c2a7c02b06.png)

### 6. 実践編

　実践編として、カレーのレシピを書いてみます。(あくまでもサンプルです)

#### 6-1. 変数ファイル

```yaml:markdown.yml
---
title: カレーの作り方    # h1ヘッダー
description: おいしいよ
chapter:
  - title: 材料(6人分)    # h2ヘッダー
    title_number: true
    content_number: false
    content:
      - line: カレー粉 1/2箱
        line_sub: 辛口が好み
      - line: 牛肉 250g
        line_sub: ""
      - line: 玉ねぎ 2個
        line_sub: ""
      - line: じゃがいも 2個
        line_sub: ""
      - line: にんじん 1/2本
        line_sub: ""
      - line: サラダ油 大さじ1
        line_sub: ""
      - line: 水 850ml
        line_sub: ""
    chapter: []
  - title: 作り方    # h2ヘッダー
    title_number: true
    content_number: true
    content: []
    chapter:
      - title: 具材を切る    # h3ヘッダー
        title_number: true
        content_number: true
        content:
          - line: 玉ねぎを切る
            line_sub: 玉ねぎの切り方
          - line: じゃがいもを切る
            line_sub: じゃがいもの切り方
        chapter:
          - title: Point    # h4ヘッダー
            title_number: false
            content_number: false
            content:
              - line: じゃがいもは丁寧に切ってください
                line_sub: ""
      - title: 具材を炒める    # h3ヘッダー
        title_number: true
        content_number: true
        content:
          - line: 鍋を加熱する
            line_sub: やけどに注意
          - line: 野菜を炒める
            line_sub: ""
        chapter: []
      - title: 煮る    # h3ヘッダー
        title_number: true
        content_number: true
        content:
          - line: 水を加え、沸騰したらあくを取ります
            line_sub: ""
        chapter:
          - title: Point    # h4ヘッダー
            title_number: false
            content_number: false
            content:
              - line: あく取りはしっかりやってください
                line_sub: ""
```

#### 6-2. 作成されたマークダウン

　条件により、空行が連続で作られてしまっている部分がありますが。
レンダリングする上では問題ありません。

```yaml:result.md
# カレーの作り方

おいしいよ

## 1. 材料(6人分)

- カレー粉 1/2箱
  - 辛口が好み
- 牛肉 250g
- 玉ねぎ 2個
- じゃがいも 2個
- にんじん 1/2本
- サラダ油 大さじ1
- 水 850ml

## 2. 作り方


### 1. 具材を切る

1. 玉ねぎを切る
   - 玉ねぎの切り方
2. じゃがいもを切る
   - じゃがいもの切り方

#### Point

- じゃがいもは丁寧に切ってください

### 2. 具材を炒める

1. 鍋を加熱する
   - やけどに注意
2. 野菜を炒める

### 3. 煮る

1. 水を加え、沸騰したらあくを取ります

#### Point

- あく取りはしっかりやってください
```

#### 6-3. レンダリング結果

　想定通りレシピを作成することが出来ました。シンプルな手順であれば色々と活用できそうです。

![curry.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/554812/c65db5e6-be1e-b249-1daf-2f0b3332e555.png)

### まとめ

　今回は、YAMLからマークダウン手順書を作る方法を紹介しました。今回のテンプレートではマークダウンの機能の一部しか活用できていないので、以下のような機能追加も今後の課題としていきたいと思います。

1. テーブル
2. リンク
3. コードブロック
4. 画像

　なお、テンプレートをAnsibleで実行する場合とPythonスクリプトで実行する場合では、出力結果が想定と変わってしまうのでご注意ください。（詳細は関連記事を参照）

### 関連記事

[【Ansible】templateモジュールから独自関数を呼び出したい（macro）](https://qiita.com/masa2223/items/1c9266716c9c3ab41df7)
[【Ansible】ハイフンの位置による改行の変化～templateモジュール編～](https://qiita.com/masa2223/items/0a0ca5d562e39aed97a7)
[【jinja2】ハイフンの位置による改行の変化](https://qiita.com/masa2223/items/5eb9847feef67b08ef3c)