Переиспользование плейбука

Иногда приходишь к тому, что плейбук становится универсальным или однотипным для каких-то нужд и при этом ты постоянно его копируешь. Копировать в целом неплохо, но хочется сократить количество открытых файлов и вносимых изменений.

Суть этого трюка в двух моментах:

  • вынести общие таски деплоя в отдельный файл, чтобы импортировать
  • кастомизировать отдельные таски деплоя в переменных роли

Вот древо файлов для этого трюка:

├── app
│   ├── defaults
│   │   └── main.yml
│   └── tasks
│       └── main.yml
└── common
    └── app.yml

Как обычно, исходники можно найти тут - src/00_sources/06_tricks/02_playbook_reuse.

Предположим есть такой плейбук:

# 01_example/common/app.yml
---
- name: Create the domain in Cloudflare
  community.general.cloudflare_dns:
    zone: "{{ base_domain }}"
    record: "{{ app_name }}"
    type: A
    value: "{{ server_ipv4 }}"
    account_email: "{{ cloudflare_email }}"
    account_api_key: "{{ cloudflare_api_key }}"
  when: app_init

- name: Create directories
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
  with_items:
    - "{{ app_directory }}"
    - "{{ app_source_directory }}"
  when: app_init

- name: Download files via git
  ansible.builtin.git:
    repo: "{{ app_repository }}"
    dest: "{{ app_source_directory }}"
    force: true
    accept_hostkey: true
    key_file: "{{ git_ssh_key_path }}"

- name: Build docker image
  community.general.docker_image:
    build:
      path: "{{ app_source_directory }}"
    name: "{{ app_name }}"
    tag: latest
    source: build
    state: present

- name: Start docker container
  community.docker.docker_container:
    name: "{{ app_name }}"
    image: "{{ app_name }}:latest"
    state: started
    pull: false
    recreate: true
    restart_policy: "unless-stopped"
    command: "{{ app_command }}"
    labels: "{{ app_labels | items2dict }}"

- name: Wait for the app to be online
  uri:
    url: "https://{{ app_host }}"
    status_code: 200
  register: result
  until: result.status == 200
  retries: 60
  delay: 1

- name: "Done"
  debug:
    msg: "https://{{ app_host }}"

Который можно кастомизировать с помощью переменных вот так:

---
app_init: no
app_directory: "/path/to/myapp"
app_source_directory: "{{ app_directory }}/src"
app_repository: "ssh://[email protected]/myapp.git"
app_name: "myapp"
app_domain: "{{ app_name }}"
app_host: "{{ app_domain }}.{{ base_domain }}"
app_internal_port: "8080"

app_command:
  - "--db-uri=mongodb://mongodb:27017"
  - "server"
  - "--bind=:{{ app_internal_port }}"

app_labels:
  - key: "traefik.enable"
    value: "true"
  - key: "traefik.http.routers.{{ app_name }}.entrypoints"
    value: "https"
  - key: "traefik.http.routers.{{ app_name }}.rule"
    value: "Host(`{{ app_host }}`)"
  - key: "traefik.http.routers.{{ app_name }}.tls"
    value: "true"
  - key: "traefik.http.routers.{{ app_name }}.tls.certresolver"
    value: "letsencrypt"
  - key: "traefik.http.services.{{ app_name }}.loadbalancer.server.port"
    value: "{{ app_internal_port }}"

Из интересных вещей тут следующие:

  • команды для запуска внутри контейнера в одной переменной в виде списка
app_command:
  - "--db-uri=mongodb://mongodb:27017"
  - "server"
  - "--bind=:{{ app_internal_port }}"
  • лейблы для контейнера разложены на key/value, которые дальше в плейбуке раскладываются с помощью items2dict
# 01_example/app/defaults/main.yml
app_labels:
  - key: "traefik.enable"
    value: "true"
  - key: "traefik.http.routers.{{ app_name }}.entrypoints"
    value: "https"
  - key: "traefik.http.routers.{{ app_name }}.rule"
    value: "Host(`{{ app_host }}`)"
  - key: "traefik.http.routers.{{ app_name }}.tls"
    value: "true"
  - key: "traefik.http.routers.{{ app_name }}.tls.certresolver"
    value: "letsencrypt"
  - key: "traefik.http.services.{{ app_name }}.loadbalancer.server.port"
    value: "{{ app_internal_port }}"
# 01_example/common/app.yml
    labels: "{{ app_labels | items2dict }}"

И теперь всё, что остаётся для каждого нового приложения - это заимпортировать из common/app.yml и изменить переменные по вкусу:

# 01_example/app/tasks/main.yml
---
- name: Generic app tasks
  ansible.builtin.include_tasks: "../../common/tasks/app.yml"