サカトラ

自動化したり統計したり

初めてのAnsible モジュール作成

本記事は「エーピーコミュニケーションズ Advent Calendar 2022」の6日目のエントリとなります。

こんにちは!

今回はAnsibleのモジュール作成に挑戦していきます。
とはいえ、Ansibleのモジュール作成については初めてなので簡単なモジュールを作成してAnsibleモジュールの仕組みについてより深く理解していきたいと思います。

Ansibleのモジュール作成を学ぶ

Ansibleのモジュール作成については公式より解説ページ(Developing modules — Ansible Documentation)が出ていますが、これでじゃあ始めようってなるのは私にはちょっとレベルが高すぎて出来ませんでした。

とっつきやす解説記事としてクラスメソッド様が出している下記の2つがわかりやすかったです。

仮想環境を用意する

まずは開発に使うためのPythonの仮想環境を用意します。
venv、pipenvなど仮想環境の構築方法は色々ありますが、今回はPoetryを使用しました。

poetry init

で仮想環境を作成し、あとは良しなにansibleを入れます。

モジュールを入れるディレクトリを作る

ansibleのモジュールは呼び出すパスが決まっています。

Adding modules and plugins locally — Ansible Documentation

今回はお試しモジュールなので取り敢えず、この環境内で動けば十分です。
なのでAdding standalone local modules for selected playbooks or a single roleにある通りlibraryディレクトリを環境下に作成してその中にモジュールとして呼ぶPythonファイルを置いていきます。

ディレクトリ構造のイメージとしては下記になります。

.
├── library
│   └── モジュール.py --- モジュール本体
└── プレイブック.yaml --- 自作モジュールを呼び出して使うためのプレイブック

モジュールを書く

ここからはモジュールの本体となるPythonコードを書いていきます。 Pythonではansible.module_utils.basicからAnsibleModuleクラスをインポートでき、そのAnsibleModuleクラスでモジュールが受け取るパラメータなどを設定し、受け取ることが出来ます。

AnsibleModuleクラスについての詳細は下記を参照。

Ansible Reference: Module Utilities — Ansible Documentation

# AnsibleModuleクラスのインポート
from ansible.module_utils.basic import AnsibleModule

# モジュールとして呼び出された際の処理を記述
def main():
    # Playbookからのパラメータを受け取る
    module = AnsibleModule(
        argument_spec=dict(
            message=dict(
                type="str",
                required=True,
            )
        )
    )

if __name__ == '__main__':
    main()

上記では、ファイルが実行された際にmain関数を呼び出すようにしています。
そしてmain関数のmodule = AnsibleModule()の部分でPlaybook側からパラメータを受け取る処理をしています。
パラメータはargument_specで指定されたもののみ受け取ることが出来ます。

argument_sepcについての書き方はこちらを参照。

Ansible module architecture — Ansible Documentation

上記のargument_specについて

    module = AnsibleModule(
        argument_spec=dict(
            # パラメータ名messageについての設定
            message=dict(
                # typeで引数の型を指定(int, str, list, etc...)
                type="str",
                # 必須のパラメータとして指定
                required=True,
            )
        )
    )

argument_specの辞書をつくるときは{}ではなくdict()がよくつかわれている印象です。
おそらくキー名でクォーテーションを打たなくてよいからかと思います。

Playbookから受け取ったパラメータはmodule.params[<パラメータ名>]で参照できます。

    message = module.params['message']

これで参照できます。

あとは、普通にPythonとしての処理を記述したのちに最後はreturnではなく.exit_json()が処理の正常終了となります。
異常終了は.fail_jsonです。

  • .exit_json(**kwargs)
    引数はキーワード引数となっておりモジュールが返すキーバリューを自由に定めることが出来ます。
    ただしタスクのchangedokはここの引数でchanged=True or Falseで変わるためこのキーだけは重要。
  • fail_json(msg, **kwargs) msg`が必須ですがその他は自由です。

プレイブックからのパラメータ取得やタスクへの結果の返し方がやや特殊ですが、それ以外はごく普通のプログラムです。

ということで簡単に作ってみました。

  • パラメータのmessageに応じた挨拶を返してくれる
  • 対応できない場合はエラーになる

こんな感じ

#! /usr/bin/python3
from ansible.module_utils.basic import AnsibleModule


def main():
    module = AnsibleModule(
        argument_spec=dict(
            message=dict(
                type="str",
                required=True,
            )
        )
    )
    reply = []
    greeting_msg = module.params["message"]

    if greeting_msg == "こんにちは":
        reply.append(
            [
                "_                    ,-、        _______________________",
                ".:ヾ、       ,へ、__ /. l       |                      |",
                ".   l       |  /     `ヽ|       |    こんにちワン!!    |",
                ">、__」    __ 人,/  tッ    `ー┐    |                      |",
                "` ー―‐r'  :.     _ .. ┴ '′     //¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯",
                "       ;   :.        `ー-r┘",
                ".      ;    :.、__ _ _ノ",
                "      ;   ;.   └ー-rィ",
                "     ',.  `'  ,.. -ノ",
                "/`ー 、  }   ,: __, /",
                "    ` -{  ,r‐i´  l",
                "      l   l  ',.  |",
                "       |  |    '  |",
                "       |. l.    } l_",
                "       ', ヽ、 `:、_,.)",
                "        └-‐'",
            ]
        )
    elif greeting_msg == "ありがとう":
        reply.append(
            [
                "                        _______________________",
                "              ァ、        |                      |",
                ".            i',゙,      |    ありがとウサギ❤   |",
                ".           i ;',       |                      |",
                "          ...!. ゙ ,     //¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯",
                "        ,''      ゙ ,          _ ,,..,,..,..,,.., _",
                "      , '゙  ●        ゙ヾ''''゙´               ゙  ,",
                ".     {Y                                         ゙ ,",
                "      ゙丶, _                                        ゙ ,",
                "            ;                                           ゙",
                "             ;                                        ゙ ,",
                "            ゙,                                        ゙",
                "              ゙,    ,   ,,          _ ,,.. ゙' ,,  , ゙",
                "                ゙,゙  ,゙,,..,,..,,..,,.,  ''゙, ゙、  ヾ",
                "                  /   ,''|   :'       }   ,゙ `'   ,",
                "                 /   ,゙  i   ゙       /  ,'    i   ",
                "                ノ   ,゙  .!   ゙      /  ,'゙    i   {",
                "             , ''´ ノ  r''  /゙     ァ''´ /   r', , !",
                "             ` ̄ ̄      ̄´        ̄ ̄ ´    ` ̄´",
            ]
        )
    else:
        module.fail_json(changed=True, failed=True, msg="ごめんなサイ🦏")

    module.exit_json(changed=False, reply=reply)


if __name__ == "__main__":
    main()

動かしてみる

モジュールのPythonファイルはlibraryディレクトリに置いたので特に設定なしで呼び出すことが出来ます。

プレイブックで今回作ったモジュールを指定してあげるだけでOKです。

---
- hosts: localhost
  gather_facts: false
  tasks:
    - name: Hello!
      ac_greeting:
        message: こんにちは
      register: hello

    - name: Debug Hello
      debug:
        msg: "{{hello.reply}}"

こんにちワン!

PLAY [localhost] **************************************************************************************************************************************************

TASK [Hello!] *****************************************************************************************************************************************************
ok: [localhost]

TASK [Debug Hello] ************************************************************************************************************************************************
ok: [localhost] => {
    "msg": [
        [
            "_                    ,-、        _______________________",
            ".:ヾ、       ,へ、__ /. l       |                      |",
            ".   l       |  /     `ヽ|       |    こんにちワン!!    |",
            ">、__」    __ 人,/  tッ    `ー┐    |                      |",
            "` ー―‐r'  :.     _ .. ┴ '′     //¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯",
            "       ;   :.        `ー-r┘",
            ".      ;    :.、__ _ _ノ",
            "      ;   ;.   └ー-rィ",
            "     ',.  `'  ,.. -ノ",
            "/`ー 、  }   ,: __, /",
            "    ` -{  ,r‐i´  l",
            "      l   l  ',.  |",
            "       |  |    '  |",
            "       |. l.    } l_",
            "       ', ヽ、 `:、_,.)",
            "        └-‐'"
        ]
    ]
}

このような形でオリジナルのモジュールを簡単に作ってみました。

完全にお遊びモジュールなので次は何か実用的なものを書きたいですね。

入力する枠が可変の入力欄を作る(React + TypeScript)

React + TypeScriptを用いて入力できる枠の数がユーザー側で調整可能な入力欄を作っていきます。 完成イメージはこんな感じ↓

完成イメージ

入力値をstring型の配列のStateとして持つような形で作るとこんな感じ ただ入力欄だけ作っても面白みがないので下記のフリーのAPIサービスを利用してステータスコードを入れると犬の画像が出るような形にしました。

https://http.dog/

const MultiInputPage = () => {
  const [statusCodes, setStatusCodes] = useState([""])
  const [dogImgUrls, setDogImgUrls] = useState<string[]>([])

  // 要素を追加
  const addStatusCodes = (): void => setStatusCodes([...statusCodes, ""])
  // 要素を編集
  const changeStatusCodes = (e: React.ChangeEvent<HTMLInputElement>): void => (
    setStatusCodes(statusCodes.map((value, index) => String(index) === e.currentTarget.name ? e.currentTarget.value : value))
  )
  // 要素を削除
  const removeStatusCodes = (e: React.MouseEvent<HTMLButtonElement>): void => (
    setStatusCodes(statusCodes.filter((_, index) => String(index) !== e.currentTarget.name))
  )

  const onSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    setDogImgUrls(statusCodes.map((statusCode) => `https://http.dog/${statusCode}.jpg`))
  }

  return (
    <>
      <h1>ステータスコードから犬画像を取得</h1>
      <form onSubmit={onSubmit}>
        {
          statusCodes.map((value, index) => (
            <div key={index}>
              <input type="text" name={String(index)} value={value} onChange={changeStatusCodes} />
              {
                index === 0 ?
                  <button type="button" onClick={addStatusCodes}>追加</button>
                  :
                  <button type="button" name={String(index)} onClick={removeStatusCodes}>削除</button>
              }
            </div>
          ))
        }
        <button type="submit">画像取得</button>
      </form>
      {
        dogImgUrls.length > 0 && <>{
          dogImgUrls.map((imgUrl, index) => (
            <>
              <p>status = {statusCodes[index]}</p>
              <img width={200} src={imgUrl} />
            </>))
        }</>
      }
    </>
  )
}

この例だとフォームも複雑じゃないし、submit時の処理も大したことないのであまり問題は感じないですが、これが複雑で記述量も多いフォームになってくると、配列の操作の部分とかが若干邪魔になるような気がしますね。

engineering.linecorp.com

こちらの記事にあるrender Hooksの概念を使用して複数入力を受け付けるコンポーネントを分離していきたいと思います。

type UseMultiInputResult = [string[], () => JSX.Element]
const useMultiInput = (initValue: string[] = [""]): UseMultiInputResult => {
  const [values, setValues] = useState(initValue)

  // 要素を追加
  const addValues = (): void => setValues([...values, ""])
  // 要素を編集
  const changeValues = (e: React.ChangeEvent<HTMLInputElement>): void => (
    setValues(values.map((value, index) => String(index) === e.currentTarget.name ? e.currentTarget.value : value))
  )
  // 要素を削除
  const removeValues = (e: React.MouseEvent<HTMLButtonElement>): void => (
    setValues(values.filter((_, index) => String(index) !== e.currentTarget.name))
  )

  const render = () => (
    <>
      {
        values.map((value, index) => (
          <div key={index}>
            <input type="text" name={String(index)} value={value} onChange={changeValues} />
            {
              index === 0 ?
                <button type="button" onClick={addValues}>追加</button>
                :
                <button type="button" name={String(index)} onClick={removeValues}>削除</button>
            }
          </div>
        ))
      }
    </>
  )
  return [values, render]
}

これでフォームをレンダリングするコンポーネントからは複数入力についてその内部の処理を分離し、最終的に使用する入力値の配列とレンダリング用の関数だけを提供できています。 こうすると、先ほどのフォームのコンポーネントも見やすくなります。

export const SimpleMultiInput = () => {
  const [statusCodes, renderInput] = useMultiInput()
  const [dogImgUrls, setDogImgUrls] = useState<string[]>([])

  const onSubmit = (e: React.FormEvent) => {
    e.preventDefault()
    setDogImgUrls(statusCodes.map((statusCode) => `https://http.dog/${statusCode}.jpg`))
  }
  return <div style={{ justifyContent: "center" }}>
    <h1>ステータスコードから犬画像を取得</h1>
    <form onSubmit={onSubmit}>
      {renderInput()}
      <button type="submit">画像取得</button>
    </form>
    {
      dogImgUrls.length > 0 && <>{
        dogImgUrls.map((imgUrl, index) => (
          <>
            <p>status = {statusCodes[index]}</p>
            <img width={200} src={imgUrl} />
          </>))
      }</>
    }
  </div>
}

render Hooksを使って処理体系をいい感じにできたんじゃないかなと思います。 このrender Hooksちょっと中毒性がありますね。 これがベストなんじゃないかって思考が止まってしまう。 便利なんだけど考えなしに使うのはさすがにまずいかな。

おまけ

デザインがイケてないのでCSSとかを当てていい感じにする取り組み MUIのstyledコンポーネントを使ってみる。

const StatusCodeInput = styled(TextField)({
  marginLeft: "1rem",
  marginRight: "1rem",
  marginBottom: "0.5rem"
})

const AddButton = styled(Button)({
  marginRight: "1rem",
})
const DeleteButton = styled(Button)({
  marginRight: "1rem",
  backgroundColor: "red",
  "&:hover": {
    backgroundColor: "#DF143C",
  },
})

const useMultiInput = (initValue: string[] = [""]): UseMultiInputResult => {
  const [values, setValues] = useState(initValue)

  // 要素を追加
  const addValues = (e: React.MouseEvent<HTMLButtonElement>): void => {
    const tmpValues = values.slice(0)
    tmpValues.splice(Number(e.currentTarget.name) + 1, 0, "")
    setValues(tmpValues)
  }
  // 要素を編集
  const changeValues = (e: React.ChangeEvent<HTMLInputElement>): void => (
    setValues(values.map((value, index) => String(index) === e.currentTarget.name ? e.currentTarget.value : value))
  )
  // 要素を削除
  const removeValues = (e: React.MouseEvent<HTMLButtonElement>): void => (
    setValues(values.filter((_, index) => String(index) !== e.currentTarget.name))
  )

  const render = () => (
    <>
      {
        values.map((value, index) => (
          <div key={index}>
            <StatusCodeInput label="status code" name={String(index)} value={value} onChange={changeValues} />
            <AddButton variant="contained" name={String(index)} onClick={addValues}>追加</AddButton>
            {
              index !== 0 &&
              <DeleteButton variant="contained" name={String(index)} onClick={removeValues}>削除</DeleteButton>
            }
          </div>
        ))
      }
    </>
  )
  return [values, render]
}

MUIを使用

MUIを使うだけでちょっと洗練された感じがでてとても満足です。

YAMLからもっとAnsible Playbookを理解する(その他編)

YAMLの公式ドキュメント等を見て面白いなと思ったところのメモ

Flow Style

YAMLでは配列、キーバリューのデータ構造について記述するときに、インデントを活用したYAMLではなくインジケーターを使用した形でも記述が可能です。

- [ 1, 2, 3 ]
- [ a, b, c ]
- [ x, y, z ]

これはすなわち、、、 以下の形と同義ということになるようです。

- 
  - 1
  - 2
  - 3
-
  - a
  - b
  - c
- 
  - x
  - y
  - z

Ansibleで確認

プレイブック

---
- hosts: localhost
  gather_facts: no

  vars:
    demo1:
      - [1, 2, 3]
      - [a, b, c]
      - [x, y, z]
    demo2:
      - 
        - 1
        - 2
        - 3
      - 
        - a
        - b
        - c
      - 
        - x
        - y
        - z

  tasks:
    - name: debug demo1
      debug:
        var: demo1
      
    - name: debug demo2
      debug:
        var: demo2

実行結果

PLAY [localhost] ****************************************************************************************************************************************************************************************

TASK [debug demo1] **************************************************************************************************************************************************************************************
ok: [localhost] => {
    "demo1": [
        [
            1,
            2,
            3
        ],
        [
            "a",
            "b",
            "c"
        ],
        [
            "x",
            "y",
            "z"
        ]
    ]
}

TASK [debug demo2] **************************************************************************************************************************************************************************************
ok: [localhost] => {
    "demo2": [
        [
            1,
            2,
            3
        ],
        [
            "a",
            "b",
            "c"
        ],
        [
            "x",
            "y",
            "z"
        ]
    ]
}

PLAY RECAP **********************************************************************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

以上の結果をもって2つの書き方がAnsibleの処理においても同義になっていることがわかりました。

キーバリュー構造についても同様に下記の2つは同義ということになります。

a: b
{a: b}

ansibleで実行してみる プレイブック

---
- hosts: localhost
  gather_facts: no

  vars:
    demo1: {first: 1, second: 2, third: 3}
    demo2:
      first: 1
      second: 2
      third: 3

  tasks:
    - name: debug demo1
      debug:
        var: demo1
      
    - name: debug demo2
      debug:
        var: demo2

結果 (表示時にキーでソートされています。)

PLAY [localhost] *************************************************************************************************************************************************************

TASK [debug demo1] ***********************************************************************************************************************************************************
ok: [localhost] => {
    "demo1": {
        "first": 1,
        "second": 2,
        "third": 3
    }
}

TASK [debug demo2] ***********************************************************************************************************************************************************
ok: [localhost] => {
    "demo2": {
        "first": 1,
        "second": 2,
        "third": 3
    }
}

PLAY RECAP *******************************************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
 

キーバリューの方もこれで2つの書き方が同義であることが確認できました。

ちなみにAnsibleの方のドキュメントにはこれらはフローコレクションという形で書かれていました。

YAML Syntax — Ansible Documentation

Block Style

先ほどのFlow Styleが複数行にまたがる構造を1行にまとめられるものであるのに対して、Block Styleは基本的には1行で書かれるものを複数行に分けて書くことができるようになるというものです。

Block Scalar

:の後につけるインジケーターによって微妙に改行の扱いが異なります。

  • |の場合
    改行をそのまま取り扱います。
sample: |
  a
  b

debugモジュールで参照した結果

ok: [localhost] => {
    "sample": "a\nb\n"
}
  • >の場合
    文中の改行は折りたたまれてスペースになります。
sample: >
  a
  b

debugモジュールで参照した結果

ok: [localhost] => {
    "sample": "a b\n"
}

Block Folding

ただし改行が折りたたまれるパターンは行頭が文字で始まるときの直前の改行となるので、改行を2つ空けたり、スペースで始まる行については>を使った場合でも改行が入ります。

sample1: >
  a

  b
# "a\nb"

sample2: >
  a
   b
# "a\n b"

ansible実行結果

プレイブック

---
- hosts: localhost
  gather_facts: false

  vars: 
    sample1: >
      a

      b

    sample2: >
      a
       b
  tasks:
    - debug:
        var: sample1
    - debug:
        var: sample2

実行結果

PLAY [localhost] ********************************************************************************************************************************************************************************************************

TASK [debug] ************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "sample1": "a\nb\n"
}

TASK [debug] ************************************************************************************************************************************************************************************************************
ok: [localhost] => {
    "sample2": "a\n b\n"
}

PLAY RECAP **************************************************************************************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

Explicit Block Mapping Entries

先ほどのBlock Scalarのように複数行で一つとなる定義をキーバリューの構造でも行うことができます。 ?をインジケーターとして用いるとキーとバリューの間に改行があってもインデントがあっていれば正しく認識することができます。

simple: ? a:
          b
# { "a": "b" }

complex: ? a:
          ? b:
            [1, 2, 3]
# { "a":  { "b": [1, 2, 3] } }

ansible実行結果 プレイブック

---
- hosts: localhost
  gather_facts: false
  vars:
    simple:
      ? a
      : b
    complex:
      ? a
      : ? b
        : [1, 2, 3]
  
  tasks:
    - debug:
        var: simple
    - debug:
        var: complex

実行結果

PLAY [localhost] ************************************************************************************************************************************************

TASK [debug] ****************************************************************************************************************************************************
ok: [localhost] => {
    "simple": {
        "a": "b"
    }
}

TASK [debug] ****************************************************************************************************************************************************
ok: [localhost] => {
    "complex": {
        "a": {
            "b": [
                1,
                2,
                3
            ]
        }
    }
}

PLAY RECAP ******************************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Tag Prefix

yamlでは変数の型を宣言せずとも自動的に解釈し、型をつけてくれます。

first: 1 # => 整数型
point: 0.2 # => 浮動小数点型
gather_facts: yes # => bool型
name: do something # => 文字列型

ここでTag Prefixを使うと明示的な型付けができます。

string_yes: !!str yes # => 明示的に文字列型になる
type_float:  !!float 1.2 # => 明示的に浮動小数点型になる

Tag Prefixを使う際には!!の後に型名を入れることで明示的な指定を行うことができます。 YAMLではTagはRFC4151のTag URIスキーマを参照する形になっています。 デフォルトで!!にはtag:yaml.org,2002:が当たっているので!!str!!intはtag:yaml.org,2002:strtag:yaml.org,2002:intを参照しています。 またTagは!`の形で参照することもできる他、Tagディレクティブで宣言することもできます。なので下記の形でもTagを使うことができます。
詳しくは公式を参照してください⇒ YAML Ain’t Markup Language (YAML™) revision 1.2.2

%TAG !e! tag:yaml.org,2002:
---
foo: !<tag:yaml.org,2002:str> bar
hoge: !e! map { piyo: 1}

ansible 実行結果 プレイブック

%TAG !e! tag:yaml.org,2002:
---
- hosts: localhost
  gather_facts: no
  vars:
    not_tag:
      - 12345
      - 1.0
      - 1
    not_tag_date: 2022-01-01
    tag_var: 
      - !!str 12345
      - !!float 1.0
      - !<tag:yaml.org,2002:str> 1 
      - !e!str 2022-01-01

  tasks:
    - name: not tag
      block:
        - debug:
            msg: "{{ item | type_debug }}"
          loop: "{{ not_tag }}"

        - debug:
            msg: "{{ not_tag_date | type_debug }}"

    - name: debug vars with tag
      debug:
        msg: "{{ item | type_debug }}"
      loop: "{{ tag_var }}"

実行結果

PLAY [localhost] *******************************************************************************************************************************************

TASK [debug] ***********************************************************************************************************************************************
ok: [localhost] => (item=12345) => {
    "msg": "int"
}
ok: [localhost] => (item=1.0) => {
    "msg": "float"
}
ok: [localhost] => (item=1) => {
    "msg": "int"
}

TASK [debug] ***********************************************************************************************************************************************
ok: [localhost] => {
    "msg": "date"
}

TASK [debug vars with tag] *********************************************************************************************************************************
ok: [localhost] => (item=12345) => {
    "msg": "str"
}
ok: [localhost] => (item=1.0) => {
    "msg": "float"
}
ok: [localhost] => (item=1) => {
    "msg": "str"
}
ok: [localhost] => (item=2022-01-01) => {
    "msg": "str"
}

PLAY RECAP *************************************************************************************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Anchor Alias

&*を使って値の再利用ができます。 下記の通りほとんど変数のように使用が可能です。

foo: &test bar
hoge: *test # => 値は"bar"となる

Anchorはスカラ値だけでなくキーバリューや配列でも使用可能です。 このAnchorはYAMLが持つ機能なのでAnsibleでは変数として取り扱えないようなものも再利用できます。

---
- hosts: localhost
  gather_facts: false

  vars:
    sample: &default happy
    feeling: *default

  tasks:
    - &debug_task
      name: debug sample
      debug:
        var: sample

    - name: debug feeling
      debug:
        var: feeling
    
    - *debug_task

上記のプレイブックを実行するとdebug sampleのタスクが*debug_taskの箇所で実行されます。

PLAY [localhost] ********************************************************************************************************************************************************************************

TASK [debug sample] *****************************************************************************************************************************************************************************
ok: [localhost] => {
    "sample": "happy"
}

TASK [debug feeling] ****************************************************************************************************************************************************************************
ok: [localhost] => {
    "feeling": "happy"
}

TASK [debug sample] *****************************************************************************************************************************************************************************
ok: [localhost] => {
    "sample": "happy"
}

PLAY RECAP **************************************************************************************************************************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

またキーバリューと配列のAnchoraは<<:で展開もできます。 これで展開すると、キーバリューの場合は値の上書きができます。 感覚的にはJavaScriptでスプレッド構文を使ったオブジェクトのプロパティ上書きに近いですね。

- &debug_task
  name: debug sample
  debug:
    var: sample

- <<: *debug_task
  name: debug sample2
# nameの値を入れ替え(下記の構造になる)
# - name: debug sample2
#   debug:
#     var: sample

Ansible 実行結果 プレイブック

---
- hosts: localhost
  gather_facts: false
  vars:
    sample: hello!

  tasks:
    - &debug_task
      name: debug sample
      debug:
        var: sample

    - <<: *debug_task
      name: debug sample2

実行結果

PLAY [localhost] ************************************************************************************************************************************************

TASK [debug sample] *********************************************************************************************************************************************
ok: [localhost] => {
    "sample": "hello!"
}

TASK [debug sample2] ********************************************************************************************************************************************
ok: [localhost] => {
    "sample": "hello!"
}

PLAY RECAP ******************************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  

docker_compose.ymlですがNetBoxのdocker_compose.ymlはAnchorがいい感じに使われているので一見の価値あるかと、、、

Anchor、AliasはYAMLの形式を活かした形だと思うので今後のPlaybook開発に活用することが出来ればよいかなと思いました。 それ以外はあまり使いどころとしては微妙かも、、、

参考

YAML Ain’t Markup Language (YAML™) revision 1.2.2

YAML の記述 - CircleCI

YAMLのAnchorとAliasをAnsibleで使う - 赤帽エンジニアブログ

netbox-docker/docker-compose.yml at release · netbox-community/netbox-docker · GitHub

YAMLという形式からAnsible Playbookをもっと理解する

Playbookをなんとなく書いている。

業務でAnsibleを触るようになってからPlaybookを読んだり書いたりする場面はただ勉強としてAnsibleを触っていた時より当たり前ですが格段に増えました。 そういう中でそもそもPlaybookてYAMLで書かれているけどそのYAMLについてって意外とわかってないこと多いなと思い、やっぱり

Ansibleのプレイブックの形式として、、、 以下の形が最もよく見る形なのかなと思います。

---
- hosts: host_name
  gather_facts: false
  tasks:
    - name: task_name
      module:
        parameter:
          - arg1
          - arg2 
      loop: "{{ var }}"
      when: specific condition

一番初めに- hosts:を書いてそのあとはインデント下げてgather_facts: falseを書いて、、、と
半ば形式的に書いているところがあるのでこれをYAMLの構造から考えてもっとPlaybookを理解していきたいと思います。

YAML?

そもそもYAMLってなんだっけとなったので公式サイトを確認。

公式サイトによるとYAMLは2004年にYAML1.0がリリースされており、もともとはXMLの仕様などに対して疑問を感じていた人々が作り上げたコミュニティの中から生まれてきたようです。
その後2005年にYAML1.1、2009年にYAML1.2.0と1.2.1がリリースされ、少し時を開けて昨年(2021)の10月に最新版のYAML1.2.2がリリースされました。
この時を経ての更新について公式のページには以下のようにあります。

This YAML 1.2.2 specification, published in October 2021, is the first step in YAML’s rejuvenated development journey.

ここからYAMLはさらなる進化を遂げるのでしょうか。

YAMLの構造について

YAMLの基本的な構造について再確認。

基本的な配列構造、キーバリュー構造

おなじみの配列

- 1
- 2
- 3
- 4

おなじみのキーバリュー

name: neko
height: 30
home: Tokyo

以上からプレイブックの構造についてを見ていきます。

プレイブックの構造を考える

これをYAMLの構造から考えると、プレイブックの一番大きな構造として配列から始まっているということがわかります。

- hosts: # <= ココ!

...

そしてhosts:tasks:は配列の最初の要素のキーバリューのキーであるということ、tasks:以下は配列になっていてその要素には行う処理のモジュールやloopwhenなどの制御系のキーが入っているということ。 そして、それらの構造が崩れなければ色々ずらしても大丈夫だということ。
実際こんな風に書くことはないけど、これもありなんだ!って気づくことは自分にとってプレイブックの理解に大きく貢献しました。

---
- tasks:
    - loop: "{{ sample }}"
      debug:
        var: item
      name: debug var
  hosts: localhost
  vars:
    - sample:
      - This is sample
      - You can write it this way.

これで全然通る。

プレイブックの実行結果

これがわかったときに何がよかったか

自分はこれを認識するまではAnsibleのPlaybookについてYAMLの記法を意識する部分がtasks以下の部分でしかなく、それ以外は決まりきったものととらえていたので「よくわかってないけどそういうもの」という認識でPlaybookの大半を書いていました。ですが、こうしてPlaybookについてYAMLとして紐解いてみることでPlaybookとは、YAMLとはの部分がより深まったように思います。

Rockerを始める

Rstudioのバージョン更新結構ほったらかしがち(N=1)

恐る恐る手元のRのバージョン確認したら3.2使ってました、、、(執筆時のWindowsの最新版は4.1.2)
Rのバージョンアップ本当にやらない。
やらない理由はただ面倒なだけやったらいいのに

そこでRocker

Rocker Project というRのコンテナ環境を作っているプロジェクトがあります。
そこがDocker Hubに挙げているイメージを使って最新のRのコンテナ環境をサクッと作ることができるというわけなんですね、ありがたい。
さらにコンテナ環境ということでRstudio Serverを使う形になっているのでクラウドとかに用意すれば簡単に複数人でおなじR環境を使えるようになる!
いいことづくめかもしれないですね。

早速実践

今回の環境は以下になります。
完全に自分用なのでWSL2上に構築する形にしてみました。
- Ubuntu20.04(WSL2) - Docker Dockerは公式のインストール手順をもとにインストール

今回は以下の記事をかなり参考にしています。
MeCab のインストール(Ubuntu 上)

RockerのイメージはDocker Hubから拾ってきます。
Rockerもいくつかの種類がありますが、自分はtidyverseがあれば満足かなというところでrocker/tidyverseを選びました。

以下がDocker Imageです。 下に行くほどパッケージが充実しています。

イメージ 内容
r-ver 安定版のRとソースビルドツール
rstudio Rstudioを追加
tidyverse tidyverseとdevtoolsを追加
verse texなどの文書作成パッケージを追加
geospatial 地理情報のライブラリを追加

※他にもいくつかイメージがあります⇒こちらを参照

docker runで立ち上げてみる

まずは簡単にdocker runコマンドのみで立ち上げてみます。
Rstudio Serverが8787番ポートで起動するのでそのポートをホストにつなげます。
また、環境変数でのパスワード設定が必要になるのでそれも置いておきます。

$ docker container run -d -p 8787:8787 -e PASSWORD=myPassword rocker/tidyverse

コンテナの立ち上げに成功したらlocalhost:8787に接続してみます。
下のようなログイン画面が出れば大丈夫です。

f:id:Ruc_4130:20220116140723p:plain
Rstudio Server ログイン画面

ユーザー名はrstudioパスワードは環境変数で設定したものでログインができます。 いい感じです! f:id:Ruc_4130:20220116141029p:plain

ボリュームを永続化する

とりあえずRockerのイメージを使ってRstudio Serverのコンテナを動かして接続するところまではできました。
しかしこれだとRstudioで作業した内容がコンテナの中にしかないのでコンテナが破棄されるとデータも消えてしまいます。

すなわち、Rockerのバージョンを入れ替えたい時などにデータが引き継げなくなってしまいます。
これを避けるためにデータをコンテナホスト側と共有することでデータを永続化できます。
Rockerのコンテナイメージではデフォルトで/home/rstudio/スクリプト等のデータがおかれるため以下のコマンドでrockerを立ち上げればコンテナ側で作成したファイル等はコンテナホスト側のrstudioディレクトリに保存されることになりデータの永続化が可能になります。
(※Rstudio側でも保存するパスは選べるので保存先を/home/rstudio以外にすることも可能、ただし下記のコマンドで立ち上げた場合は/home/rstudio配下以外のファイル、ディレクトリは共有されない)

$ docker container run -d -p 8787:8787 -e PASSWORD=myPassword -v rstudio:/home/rstudio rocker/tidyverse

もっとカスタマイズ

ここまででかなりカスタマイズできたがDockerfileを使ったさらなるカスタマイズをしていきます。

RMeCabを入れる

Rで形態素解析をする際にはRMeCabというパッケージを使って行います。
RMeCabMeCabというソフトのラッパーです。RMeCabの使用にはMeCabのインストールが必要になります。 ということでMeCabRMeCabがインストールされたRockerをDockerfileを使って作っていきます。

FROM rocker/tidyverse
RUN apt-get update && apt-get install -y \
    mecab \
    libmecab-dev \
    mecab-utils \
    mecab-ipadic-utf8
RUN install2.r RMeCab --repo https://rmecab.jp/R

Dockerfileからイメージのビルドをする

docker image build -t rocker_mecab .

ビルドされたイメージからコンテナを起動する。 rocker_mecabでイメージにタグをつけたのでそのタグ名を使って呼び出します。 せっかくなのでコンテナに名前も付けましょう。rstudio_serverと名付けました。

docker container run -d --name rstudio_server -p 8787:8787 -e PASSWORD=myPassword -v rstudio:/home/rstudio rocker_mecab

これで立ち上がったコンテナにアクセスしてRMeCabが使えるかどうかを試してみます。

f:id:Ruc_4130:20220204012018p:plain
RMeCabをお試し

RMeCabを問題なく使うことができました。

neologd辞書を入れる

ここまででRMeCab入りのRockerコンテナを立ち上げることができました。
ただこのMeCabが持つデフォルト辞書では新語などには対応できないという難点があります。

RMeCabC("鬼滅の刃を見ながら午後の紅茶を飲む")
[[1]]
名詞 
"鬼" 

[[2]]
名詞 
"滅" 

[[3]]
助詞 
"の" 

[[4]]
名詞 
"刃" 

[[5]]
助詞 
"を" 

[[6]]
動詞 
"見" 

[[7]]
    助詞 
"ながら" 

[[8]]
  名詞 
"午後" 

[[9]]
助詞 
"の" 

[[10]]
  名詞 
"紅茶" 

[[11]]
助詞 
"を" 

[[12]]
  動詞 
"飲む" 

鬼滅の刃」や「午後の紅茶」は固有名詞であるのそれらは1語で扱ってほしいところです。
このような新語・固有名詞についてもかなりの対応しているのがNEologd辞書になります。 github.com ということでNEologd辞書を入れつつそれをデフォルト辞書に設定するDockerfileを作ってみました。 mecablinuxalternativeシステムを使ってデフォルト辞書のパスをとっているのでそれをupdate-alternativesコマンドでNEologdの方に曲げてあります。

FROM rocker/tidyverse
RUN apt-get update && apt-get install -y \
    mecab \
    libmecab-dev \
    mecab-utils \
    mecab-ipadic-utf-8 \
    git \
    build-essintial \
    curl

RUN install2.r rmecab --repo https://rmecab.jp/R

# NEologd辞書を入れる
RUN git clone --depth 1 https://github.com/neologd/mecab-ipadic-neologd.git  && mecab-ipadic-neologd/bin/install-mecab-ipadic-neologd -n

# NEologdをデフォルトに設定
RUN update-alternatives --install /var/lib/mecab/dic/debian mecab-dictionary /usr/lib/x86_64-linux-gnu/mecab/dic/mecab-ipadic-neologd 100

これビルドするのにそこそこ時間がかかりました。(特にNEologdのダウンロード)

上記ファイルよりビルドされたイメージでコンテナを立ち上げたらもう一度先ほどの文章をMeCabに投げてみます。

 RMeCabC("鬼滅の刃を見ながら午後の紅茶を飲む")
[[1]]
      名詞 
"鬼滅の刃" 

[[2]]
助詞 
"を" 

[[3]]
動詞 
"見" 

[[4]]
    助詞 
"ながら" 

[[5]]
        名詞 
"午後の紅茶" 

[[6]]
助詞 
"を" 

[[7]]
  動詞 
"飲む" 

ということできちんとNEologd辞書が反映されていることが確認できます。

これで自然言語処理を行うベースもできました。

今後はこのイメージを使って色々やっていこうと思います。

JavaScriptはブラウザ上で動くことを全然理解していなかった人の話

Web開発始めました

インフラ周りの業務しかやっていなかったのでなんも知らんところから頑張ってやっていますが根本を理解していませんでしたという話。
React + Django + Nginxを使って開発をしていますが正直Reactは、、、(勘で書いている)

Fetchがうまく通らない

事の発端はReactで作成しビルドしたJSのFetchがうまく通らないことでした。
システムの構成としてはDocker composeでNginxとDjangoコンテナを動かしDjangoの方はuwsgiで受けてリクエストを捌く、みたいなよくある感じです。
それでNginxの方にReactで作成しビルドしたHTML、JS、CSSのファイルを配置し、バックエンド側のDjango Rest Framework(以下DRF)をたたいて情報を取得し、レンダリングするみたいなことをするつもりでした。
Reactはcreate-react-appで作っているのでnpm run startのコマンドを使って開発サーバーでの挙動の確認を行っていましたがその時はうまく動いているように見えました。

しかし、いざコンテナ化して動きを確認すると全然動かない、、、
デベロッパーツールを開いてConsoleを見ると、、、ERR_NAME_NOT_RESOLVED、、、
あらら~なんで~DRFの設定が悪いのかなと思って色々試したがうまくいかず、Docker composeの作り方が悪いのかなと思ってそっちもあれやこれやしてみたがうまくいかず、途方に暮れているとこんなありがたいお言葉が・・・

But using fetch API, it's done on the browser side so it does not know the server/docker names

雑翻訳⇒fetch APIを使うときって、それ自体はブラウザ側で行われるからブラウザはそのサーバ/dockerの名前は知らないよ。 ↓ソース

stackoverflow.com

なんとまあこんなこともわからないとは、正直これを呼んだ時に自分の思考の足りなさに呆れてしまいました。。。

結論

JavaScriptはブラウザ上で動くという基本的なことを忘れておりFetchするURLをコンテナのサービス名にしてました。なのでコンテナ化したときに名前解決ができなくなってしまっていました。(Fetchする先をDocker composeのサービス名にしていたのでそれがブラウザ側で名前解決できていなかった。)
ついスクリプトはそれがあるサーバ上で動くという思い込みが先行し、こんな基本的な話なのにここにたどり着くのにかなり時間を要してしまいました。

絵をかいてみました。

f:id:Ruc_4130:20220208010635p:plain
間違えていた解釈

f:id:Ruc_4130:20220208010719p:plain
JavaScriptはブラウザ上で動くということ