Ansible: vsftpd でFTPサーバを構築する
はじめに
レガシーなシステムに携わっているとデータ置き場としてFTPサーバによく出くわします。今回は最低限の設定で vsftpd を構築する手順をPlaybook化してみました。
前提
- OS は RHEL8 を利用する
- FTPサービスとして vsftpd を利用する
実装
段取りとしては以下のようになります。
上記を踏まえて今回書いたPlaybookは以下の通り。
--- - name: Setup vsftpd hosts: servers vars: pasv_min_port: 10021 pasv_max_port: 10031 become: true tasks: - name: Install vsftpd ansible.builtin.dnf: name: vsftpd state: present - name: Start vsftpd ansible.builtin.systemd: state: started name: vsftpd - name: Add passive mode to vsftpd ansible.builtin.blockinfile: path: /etc/vsftpd/vsftpd.conf state: present block: | pasv_enable=Yes pasv_min_port={{ pasv_min_port }} pasv_max_port={{ pasv_max_port }} register: _res - name: Restart vsftpd ansible.builtin.systemd: state: started name: vsftpd when: _res.changed - name: Permit ftp service with firewall ansible.posix.firewalld: service: ftp state: enabled permanent: true immediate: true - name: Enable nf_conntrack_helper kernel module ansible.posix.sysctl: name: net.netfilter.nf_conntrack_helper value: '1' sysctl_file: "/etc/sysctl.d/10-nf_conntrack_helper.conf" state: present reload: true
今回はvsftpdのの設定項目が少ないので blockinfile モジュールで済ませています。
また、パッシブモード時のFTP通信許可設定については sysctl の設定変更( ftp サーバをパッシブモードで起動する場合にfirewall 側で必要な通信許可を行うため、 nf_conntrack_helper カーネルモジュールをロードする)で対応しています。
実行結果は以下の通り。
PLAY [Setup vsftpd] ************************************************************ TASK [Gathering Facts] ********************************************************* ok: [server.test.local] TASK [Install vsftpd] ********************************************************** changed: [server.test.local] TASK [Start vsftpd] ************************************************************ changed: [server.test.local] TASK [Add passive mode to vsftpd] ********************************************** changed: [server.test.local] TASK [Restart vsftpd] ********************************************************** changed: [server.test.local] TASK [Permit ftp service with firewall] **************************************** changed: [server.test.local] TASK [Enable nf_conntrack_helper kernel module] **************************************** changed: [server.test.local] PLAY RECAP ********************************************************************* server.test.local : ok=1 changed=6 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
2回目以降は changed を可能な限りなくしたい派です。再実行結果は以下の通り。
PLAY [Setup vsftpd] ************************************************************ TASK [Gathering Facts] ********************************************************* ok: [server.test.local] TASK [Install vsftpd] ********************************************************** ok: [server.test.local] TASK [Start vsftpd] ************************************************************ ok: [server.test.local] TASK [Add passive mode to vsftpd] ********************************************** ok: [server.test.local] TASK [Restart vsftpd] ********************************************************** skipping: [server.test.local] TASK [Permit ftp service with firewall] **************************************** ok: [server.test.local] TASK [Enable nf_conntrack_helper kernel module] **************************************** ok: [server.test.local] PLAY RECAP ********************************************************************* server.test.local : ok=6 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0
ok と skip のみになりました。
まとめ
vsfptd の構築方法についてまとめました。1年に1回くらいFTPサーバ構築のPlaybook作っている気がしたので今回アウトプットでもう大丈夫なはず。
参考情報:
2.8.6. FTP を使用するインストールソースの作成 Red Hat Enterprise Linux 8 | Red Hat Customer Portal
centos - How to configure vsftpd to work with passive mode - Server Fault
Ansible: KVMの仮想マシンを削除する
はじめに
検証環境としてサクッとVMを作ったり壊したりするのに便利なKVM、よく使っています。今回はKVM上で動作する仮想マシンを削除するためのPlaybookを作ってみます。手動で操作する場合は virsh を叩いたりしています。ただ、削除するにしても複数のサブコマンド実行が必要だったり、ホストマシンからの名前解決を/etc/hosts で簡易的に管理している場合はそちらのエントリも削除したりといった具合で手間がかかるため、そうした手間を省くのが今回のモチベーションです。
削除操作を自動化していますので試す場合はご注意ください&自己責任でお願いします。
準備
VMの操作には community.libvirt.virt モジュールを使います。
Playbookと同じディレクトリで requirements.yml を作成し、依存する collection を定義します。
requirements.yml
--- collections: - community.libvirt
collection をインストールします。
ansible-galaxy collection install -r requirements.yml -p collections
実装
今回作成したPlaybookがこちらです。
--- - name: Delete vms hosts: localhost gather_facts: false vars: vms: - hostname: 'centos8' tasks: - name: Get available vms community.libvirt.virt: command: list_vms register: existing_vms - name: Check if target vm name is existing ansible.builtin.assert: that: item.hostname in existing_vms['list_vms'] fail_msg: "target vm '{{ item.hostname }}' is not found on kvm" loop: "{{ vms }}" - name: Destroy vms community.libvirt.virt: command: destroy name: "{{ item.hostname }}" loop: "{{ vms }}" ignore_errors: true - name: Undefine vms community.libvirt.virt: command: undefine name: "{{ item.hostname }}" loop: "{{ vms }}" - name: Delete qcow2 image ansible.builtin.file: path: "/var/lib/libvirt/images/{{ item.hostname }}.qcow2" state: absent loop: "{{ vms }}" - name: Delete hosts entry ansible.builtin.lineinfile: path: /etc/hosts regex: ".*{{ item.hostname }}.*" state: absent loop: "{{ vms }}"
複数のVMをまとめて削除できるよう、ドメイン名のリストを変数として定義し、各タスクで loop させています。安直に1つのPlaybookにまとめましたが、この処理をまるっとタスクファイルに切り出して include_tasks で呼び出すようにすれば、loopの定義が1箇所で済むのでもう少しスマートになりそうです。
削除の流れとしては、指定したドメインのVMが存在するかを確認した上で、問題なければ destory、undefineを実行してVMを削除します。仮想マシンが停止している状態だと destory する際にエラーを返すため、 ignore_errors: true を対処しています。
また、前述の通り今回はホストマシンから仮想マシンに対する名前解決はホストマシン上の /etc/hosts で管理しているので、登録されたエントリも併せて削除しています。
実行結果
仮想マシンは以下のように停止した状態で削除してみます。
sudo virsh list --all Id Name State ----------------------------- - centos8 shut off
いざ実行。
PLAY [Delete vms] ****************************************************************************************************************************************************************************
TASK [Get available vms] *********************************************************************************************************************************************************************
ok: [localhost]
TASK [Check if target vm name is existing] ***************************************************************************************************************************************************
ok: [localhost] => (item={'hostname': 'centos8'}) => {
"ansible_loop_var": "item",
"changed": false,
"item": {
"hostname": "centos8"
},
"msg": "All assertions passed"
}
TASK [Destroy vms] ***************************************************************************************************************************************************************************
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: libvirt.libvirtError: Requested operation is not valid: domain is not running
failed: [localhost] (item={'hostname': 'centos8'}) => {"ansible_loop_var": "item", "changed": false, "item": {"hostname": "centos8"}, "msg": "Requested operation is not valid: domain is not running"}
...ignoring
TASK [Undefine vms] **************************************************************************************************************************************************************************
ok: [localhost] => (item={'hostname': 'centos8'})
TASK [Delete qcow2 image] ********************************************************************************************************************************************************************
changed: [localhost] => (item={'hostname': 'centos8'})
TASK [Delete hosts entry] ********************************************************************************************************************************************************************
changed: [localhost] => (item={'hostname': 'centos8'})
PLAY RECAP ***********************************************************************************************************************************************************************************
localhost : ok=6 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1
無事削除できました。
まとめ
Ansible: keycloakの設定をAPI経由で行う
はじめに
近頃、認可サービスを立てるためにkeycloakを触る機会がありました。Ansibleを使ってkeycloakの設定を行う場合は専用のモジュールを利用することも可能ですが、痒いところに手の届かない感じで細かな設定に対応しきれないことがあります。そこで、 uri モジュールを利用してkeycloakのAPI経由で設定する方法が一手段として考えられます。
ただし、さすが認可機能を提供するためのソフトウェアだけあって、keycloak自体のAPI操作も認可フローを介して操作する必要があります。このため、アクセストークンの取得が必要になるわけですが、今回は簡単に実装を紹介します。
準備
keycloakはコミュニティから提供されるコンテナイメージを利用します。今回はpodmanを利用しますが、上記のリンクに記載されているようコマンド一発でOKです。
podman run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:20.0.3 start-dev
起動したらブラウザで http://localhost:8080/admin にアクセスし、user/password = admin/admin でログインできることを確認します。


アクセストークンを取得するのみであれば、準備はこれで完了です。
実装
APIの細かい仕様はドキュメントに任せるとして、アクセストークンを取得するだけでは面白味に欠けるため、トークンを使ってレルムを作成するPlaybookを書いてみます。
--- - name: Get keycloak access token hosts: localhost vars: kc_auth_username: admin kc_auth_password: admin kc_token_endpoint: http://localhost:8080/realms/master/protocol/openid-connect/token tasks: - name: Get access token ansible.builtin.uri: url: "{{ kc_token_endpoint }}" method: POST body_format: "form-urlencoded" body: grant_type: "password" client_id: "admin-cli" username: "{{ kc_auth_username }}" password: "{{ kc_auth_password }}" headers: Content-Type: "application/x-www-form-urlencoded" register: _res - name: Set token into a variable. ansible.builtin.set_fact: _access_token: "{{ _res.json.access_token }}" - name: Show access token ansible.builtin.debug: msg: "{{ _res.json.access_token }}" - name: Create realm ansible.builtin.uri: url: "http://localhost:8080/admin/realms" method: POST body_format: json body: realm: "my_realm" enabled: true headers: Authorization: "Bearer {{ _access_token }}" status_code: [201, 409] register: _res changed_when: _res.status == 201
トークンエンドポイントに対してPOSTリクエストのボディに認可フローの種別、認証情報などを埋め込んでアクセストークンを取得します。最後のタスクでレルムを作成していますが、Authorizationヘッダーに Bearer形式でアクセストークンを渡し、認可されればリクエストボディに記載したレルムの設定情報に基づいて新規作成します。また、想定されるステータスコードはAPI仕様を確認しながら決めましょう。今回は、既にレルムが作成されている場合も考慮して 401 を追加しています。
実行結果
Playbookの実行結果は以下の通りです。
PLAY [Get keycloak access token] ***************************************************************************************************************************************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Get access token] ************************************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Set token into a variable.] **************************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Show access token] ***********************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ4M3ZQdm1KMFNiUnhNSDhlOG05d1N5OHhnUHVld2t3d3daMUJya2ZjX3dvIn0.eyJleHAiOjE2NzQzODIzMzEsImlhdCI6MTY3NDM4MjI3MSwianRpIjoiNDYzM2Y5NzYtMDNjMS00NmJkLWJiNTYtYmRmOWRjNWYzOWViIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tYXN0ZXIiLCJzdWIiOiIwMmEyZWE1Mi0wNWM0LTRjMGYtYTFkZi1iMDBjYjBlNzY3ZmUiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJhZG1pbi1jbGkiLCJzZXNzaW9uX3N0YXRlIjoiNTI0NjhmNTAtODQ5My00YTA5LThmMzUtYjM4NjFjOTYxZTUzIiwiYWNyIjoiMSIsInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsInNpZCI6IjUyNDY4ZjUwLTg0OTMtNGEwOS04ZjM1LWIzODYxYzk2MWU1MyIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwicHJlZmVycmVkX3VzZXJuYW1lIjoiYWRtaW4ifQ.I4FEXun0sh1K9d06TUkuT4Js6-wM92Swu_nu3eYtmujMU0rb1G9F55HWV0pWImiP2dIXDuv461i2fb8zsz3TxTgFHNKENoCDamQEEij9RK59Wybr3RjgknmekYCZIMbULjribOk4xthWJzFyJAzdIESlfmLvKTilPgeorKNdE_NymbKNAdXvrtPpAJtV6xImGmGnDjTZoy_-u67Ih839YSDJEBfhSxrZilNsKNLTHK3UNBtbmSuy4-Kiw8vcOxNXgvAZ8tbcrl4Q3ajPrVh8i2BGoUc9R9hmL7p2Nm6flCPQcpLSn-FlQMmOoUn8dNaoz3cwk1iyK2uiASux7bZZSw"
}
TASK [Create realm] ****************************************************************************************************************************************************************************************************************************************
changed: [localhost]
PLAY RECAP *************************************************************************************************************************************************************************************************************************************************
localhost : ok=5 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
実行後にGUIへログインし、レルムが作成されたことが確認できます。

まとめ
keycloakの設定をansibleの uri モジュールを利用してAPI経由で実現する方法をまとめました。実利用を考えると実行時間が長い場合はアクセストークンがExpireするため、再度取得するなどのケアが必要になります。昨今はOAuth/OIDCといった認可が必須のAPIサービスも多いですので、今回の実装例は生かせるケースがまたきっとあるかも?
Ansible: リストへ動的に要素を追加する
はじめに
Playbookを書いていると、情報取得のキーとなるリストを変数として定義し、各キーで取得できたデータを変換・加工した後に再度それらのリスト作る、なんていう場面がよくあると思います。
今回は、APIから情報を取得するケースでこれらのPlaybookを考えてみます。
実装
基本方針としては繰り返しとなる処理部分を共通化して別タスクとして切り出します。共通処理部分では
- データ取得
- (必要に応じて)データ変換・加工
- リストへの追加
という流れでリストへ要素を追加します。このため、共通化処理の呼び出し元では要素を追加するリスト変数の初期化を忘れず行います。まずは Playbookです。
site_dynamic_appending_elem.yml
--- - name: Append data to a list dynamically hosts: localhost vars: api_endpoint: "https://fakerestapi.azurewebsites.net/api/v1" book_ids: [1, 2, 3, 4] tasks: - name: Initialize variable ansible.builtin.set_fact: _book_titles: [] - name: Fetch book title list ansible.builtin.include_tasks: inner_loop.yml loop: "{{ book_ids }}" loop_control: loop_var: _book_id - name: Show list generated dynamcally ansible.builtin.debug: var: _book_titles
次にPlaybook から呼び出されるタスクファイルです。
inner_loop.yml
--- - name: Append data to a list dynamically hosts: localhost vars: api_endpoint: "https://fakerestapi.azurewebsites.net/api/v1" book_ids: [1, 2, 3, 4] tasks: - name: Initialize variable ansible.builtin.set_fact: _book_titles: [] - name: Fetch book title list ansible.builtin.include_tasks: inner_loop.yml loop: "{{ book_ids }}" loop_control: loop_var: _book_id - name: Show list generated dynamcally ansible.builtin.debug: var: _book_titles
実行結果
PLAY [Append data to a list dynamically] *******************************************************************************************************************************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Initialize variable] *********************************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Fetch book title list] *******************************************************************************************************************************************************************************************************************************
included: /Users/k-boo/t/ansible/loop/inner_loop.yml for localhost => (item=1)
included: /Users/k-boo/t/ansible/loop/inner_loop.yml for localhost => (item=2)
included: /Users/k-boo/t/ansible/loop/inner_loop.yml for localhost => (item=3)
included: /Users/k-boo/t/ansible/loop/inner_loop.yml for localhost => (item=4)
TASK [Fetch Book info] *************************************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Show title in API response] *************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "Book 1"
}
TASK [Append the book title to list] ***********************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Fetch Book info] *************************************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Show title in API response] *************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "Book 2"
}
TASK [Append the book title to list] ***********************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Fetch Book info] *************************************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Show title in API response] *************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "Book 3"
}
TASK [Append the book title to list] ***********************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Fetch Book info] *************************************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Show title in API response] *************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "Book 4"
}
TASK [Append the book title to list] ***********************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Show list generated dynamcally] **********************************************************************************************************************************************************************************************************************
ok: [localhost] => {
"_book_titles": [
"Book 1",
"Book 2",
"Book 3",
"Book 4"
]
}
PLAY RECAP *************************************************************************************************************************************************************************************************************************************************
localhost : ok=19 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
まとめ
今回はリストへ動的に要素を追加する実装方法について書きました。この実装は共通処理部分で、外部リソースにアクセスしたり、複雑なデータ操作が必要となるような場合に適していると思われます。
Ansible: 2つのリストの組み合わせを処理する
はじめに
Playbookを書いていると多次元のデータをループさせて処理させる場合がよくあります。
以下のようなデータ構造を考えてみます。
people: - taro - jiro - saburo sports: - soccer - baseball - bascketball
上記の2つのリストを使って、 <who> likes <sport> のような文字列の組み合わせをそれぞれ出力したいとします。
多重ループ
loop を利用して多重ループを構成する場合は内側のループを構成するタスクを別ファイルに分けて、外側のループから include_task を使って呼び出します。
site_doubleloop.yml
--- - name: Process data with double loop hosts: localhost vars: people: - taro - jiro - saburo sports: - soccer - baseball - bascketball tasks: - name: Outer loop call inner loop include_tasks: "inner_loop.yml" loop: "{{ people }}" loop_control: loop_var: person
inner_loop.yml
--- - name: Output debug: msg: "{{ person }} likes {{ sport }}" loop: "{{ sports }}" loop_control: loop_var: sport
TASK [Output] *******************************************************
ok: [localhost] => (item=soccer) => {
"msg": "taro likes soccer"
}
ok: [localhost] => (item=baseball) => {
"msg": "taro likes baseball"
}
ok: [localhost] => (item=bascketball) => {
"msg": "taro likes bascketball"
}
TASK [Output] *******************************************************
ok: [localhost] => (item=soccer) => {
"msg": "jiro likes soccer"
}
ok: [localhost] => (item=baseball) => {
"msg": "jiro likes baseball"
}
ok: [localhost] => (item=bascketball) => {
"msg": "jiro likes bascketball"
}
TASK [Output] *******************************************************
ok: [localhost] => (item=soccer) => {
"msg": "saburo likes soccer"
}
ok: [localhost] => (item=baseball) => {
"msg": "saburo likes baseball"
}
ok: [localhost] => (item=bascketball) => {
"msg": "saburo likes bascketball"
}
filter を使ってデータを合成する
リストの直積を生成する product フィルタを利用します。前の方法と違い、タスクファイルを分ける必要がないためロジックがシンプルになります。
site_singleloop_with_product_filter.yml
- name: Process 2 lists data with product filter
hosts: localhost
vars:
people:
- taro
- jiro
- saburo
sports:
- soccer
- baseball
- bascketball
tasks:
- name: Generate product of the lists
ansible.builtin.set_fact:
_pdct: "{{ people | product(sports) }}"
- name: Output them
ansible.builtin.debug:
var: _pdct
- name: Generate Strings with the product
debug:
msg: "{{ item[0] }} likes {{ item[1] }}"
loop: "{{ _pdct }}"
PLAY [Process 2 lists data with product filter] ************************************************************************************************************************************************************************************************************
TASK [Gathering Facts] *************************************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Generate product of the lists] ***********************************************************************************************************************************************************************************************************************
ok: [localhost]
TASK [Output them] *****************************************************************************************************************************************************************************************************************************************
ok: [localhost] => {
"_pdct": [
[
"taro",
"soccer"
],
[
"taro",
"baseball"
],
[
"taro",
"bascketball"
],
[
"jiro",
"soccer"
],
[
"jiro",
"baseball"
],
[
"jiro",
"bascketball"
],
[
"saburo",
"soccer"
],
[
"saburo",
"baseball"
],
[
"saburo",
"bascketball"
]
]
}
TASK [Generate Strings with the product] *******************************************************************************************************************************************************************************************************************
ok: [localhost] => (item=['taro', 'soccer']) => {
"msg": "taro likes soccer"
}
ok: [localhost] => (item=['taro', 'baseball']) => {
"msg": "taro likes baseball"
}
ok: [localhost] => (item=['taro', 'bascketball']) => {
"msg": "taro likes bascketball"
}
ok: [localhost] => (item=['jiro', 'soccer']) => {
"msg": "jiro likes soccer"
}
ok: [localhost] => (item=['jiro', 'baseball']) => {
"msg": "jiro likes baseball"
}
ok: [localhost] => (item=['jiro', 'bascketball']) => {
"msg": "jiro likes bascketball"
}
ok: [localhost] => (item=['saburo', 'soccer']) => {
"msg": "saburo likes soccer"
}
ok: [localhost] => (item=['saburo', 'baseball']) => {
"msg": "saburo likes baseball"
}
ok: [localhost] => (item=['saburo', 'bascketball']) => {
"msg": "saburo likes bascketball"
}
PLAY RECAP *************************************************************************************************************************************************************************************************************************************************
localhost : ok=4 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
# おわりに
2つのリストに対して要素の組み合わせを処理する方法をまとめました。