ハトネコエ Web がくしゅうちょう

プログラミングとかAndroid

Ansibleで、サーバー上の特定ディレクトリにあるファイル名一覧を取得する

個人のサーバーでgit運用したくて、Gitbucket を立ててるんです。
それで、この前 Gitbucket の更新をするときにプラグインファイルも更新をかけたのですが、
困ったことに、プラグインをダウンロードするだけのAnsibleタスクだと
古いバージョンのプラグインファイルもディレクトリに残ってしまってバグるんです。

なので、古いプラグインファイルを削除するためにどうしようかと考えて、
ダウンロードするプラグインファイル名一覧と
現在サーバーにあるプラグインファイル名一覧を比較して、それが異なる場合には、
ダウンロード前にサーバー上のプラグインディレクトリを一度削除することにしました。

やり方が少しややこしいのでここに書き残しておきます。
(Ansibleで配列を扱うのは苦労が多いので、AnsibleでがんばるよりはFabricなりCapistranoなりでデプロイを別におこなうのが本当は健全)

1. もともとのタスク構成

こうなってました。

- name: create plugins directory
  become: true
  become_user: "{{ main_user_name }}"
  become_method: sudo
  file:
    path: "{{ gitbucket_plugins_dir }}"
    state: directory

- name: download plugins
  become: true
  become_user: "{{ main_user_name }}"
  become_method: sudo
  get_url:
    url: "{{ item.url }}"
    dest: "{{ gitbucket_plugins_dir }}"
    mode: 0755
  with_items: "{{ gitbucket_plugins }}"
  notify: restart gitbucket

変数 gitbucket_plugins は以下のとおりです。

gitbucket_plugins:
  - name: emoji
    url: https://github.com/gitbucket/gitbucket-emoji-plugin/releases/download/4.4.0/gitbucket-emoji-plugin_2.12-4.4.0.jar
  - name: gist
    url: https://github.com/gitbucket/gitbucket-gist-plugin/releases/download/4.9.0/gitbucket-gist-plugin_2.12-4.9.0.jar

そしてサーバー上には path/to/plugins/ ディレクトリに以下のファイルが入っているとします。

gitbucket-emoji-plugin_2.12-4.4.0.jar
gitbucket-gist-plugin_2.12-4.8.0.jar

このままタスクを実行すると、path/to/plugins/ ディレクトリには

  • gitbucket-emoji-plugin_2.12-4.4.0.jar
  • gitbucket-gist-plugin_2.12-4.8.0.jar
  • gitbucket-gist-plugin_2.12-4.9.0.jar

が存在してしまい、バージョン違いのプラグインが同居して挙動がおかしくなってしまう。そういうわけです。

2. map で解決

そういうわけで、create plugins directory の前に以下のようなタスクを定義することで解決させました。

- name: check current plugins
  become: true
  become_user: root
  find:
    path: "{{ gitbucket_plugins_dir }}"
  register: current_plugins

- set_fact:
    current_plugins_list: >
      {{ current_plugins.files | map(attribute='path') | map('basename') | list | sort }}
    install_plugins_list: >
      {{ gitbucket_plugins | map(attribute='url') | map('basename') | list | sort }}

- set_fact:
    is_gitbucket_plugins_updated: >
      {{ current_plugins_list != install_plugins_list }}

- name: remove plugins directory (when plugins updated)
  become: true
  become_user: "{{ main_user_name }}"
  become_method: sudo
  file:
    path: "{{ gitbucket_plugins_dir }}"
    state: absent
  when: is_gitbucket_plugins_updated

強引な力技ですね・・・(笑)
では、内容を説明していきます。

まず check current plugins にて find を用い、その結果を変数 current_plugins に渡します。

current_plugins.files は、サーバー上のプラグインディレクトリにあるファイルすべての stat を配列で持ちます。

[
  {
    'path': 'path/to/plugins/gitbucket-emoji-plugin_2.12-4.4.0.jar',
    'mode': '0755',
    'size': ...後略
  },
  {
    'path': 'path/to/plugins/gitbucket-gist-plugin_2.12-4.8.0.jar',
    'mode': ...後略
  }
] 

欲しいのはファイル名だけですから、配列の各要素に対してキー名を指定してそこだけ取り出します。
それが map(attribute=‘path’) の部分です。

この Filter をかけることで、配列は以下のようになります。

[
  'path/to/plugins/gitbucket-emoji-plugin_2.12-4.4.0.jar',
  'path/to/plugins/gitbucket-gist-plugin_2.12-4.8.0.jar'
] 

そしてこの path からファイル名の部分だけ取り出すため、
Ansible側で用意されている basename Filter を使います。

map と組み合わせることにより、各要素に対して basename Filter をかけることができ、配列は以下のようになります。

[
  'gitbucket-emoji-plugin_2.12-4.4.0.jar',
  'gitbucket-gist-plugin_2.12-4.8.0.jar'
] 

そしてその後に list Filter をかけます。
さっきまで『配列は以下のようになります』と書いていましたが、実際には map Filter が返すのはmapオブジェクトなので、
list Filter をかけなければ配列にはなりません。

最後に、sort による辞書順の並び替えをしておいて、
このあとの配列の比較に備えます。
この配列を変数 current_plugins_list として保存します。

ダウンロードする予定のプラグイン一覧にも似たような処理をかけて
変数 install_plugins_list に配列を保存します。

そしてこの2つの変数の値が異なるときは
変数 is_gitbucket_plugins_updated は True となり、
サーバー上のプラグインディレクトリの削除がおこなわれる。という仕組みです。

3. 感想

だいぶ無理矢理やってて、魔術じみてますね。
Ansibleで配列使うのはしんどいですが、こういうことができるんだなーと思って書きました。

もう少しがんばれば、プラグインフォルダーを削除するのでなく、
不要になったプラグインファイルだけを削除する、というふうに書けそうな気がしますが、
中途半端なところで妥協しています。

個人サーバーなので複雑なことしたくなくて、アプリケーションの展開までAnsibleにやらせてますが、
結果Ansibleのレシピが複雑になってるので、ちゃんとデプロイツール使ったほうがいいなという気がしてくるこの頃です……。