Add Planka deployment playbook and compose template
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7ae81da55a
commit
ed3d5833b7
2 changed files with 191 additions and 0 deletions
131
playbooks/planka.yml
Normal file
131
playbooks/planka.yml
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
---
|
||||||
|
- name: Deploy Planka kanban board
|
||||||
|
hosts: all
|
||||||
|
become: true
|
||||||
|
|
||||||
|
vars:
|
||||||
|
planka_stack_dir: /opt/stacks/planka
|
||||||
|
planka_version: "2.0.0-rc.3"
|
||||||
|
planka_port: 3000
|
||||||
|
planka_base_url: "http://{{ ansible_default_ipv4.address }}:{{ planka_port }}"
|
||||||
|
planka_trust_proxy: false
|
||||||
|
planka_token_expires_in: 365
|
||||||
|
planka_log_level: warn
|
||||||
|
planka_db_name: planka
|
||||||
|
planka_admin_email: "admin@example.com"
|
||||||
|
planka_admin_password: "ChangeMe123!"
|
||||||
|
planka_admin_name: "Planka Admin"
|
||||||
|
planka_admin_username: "admin"
|
||||||
|
# Set true to force regeneration of SECRET_KEY and INTERNAL_ACCESS_TOKEN
|
||||||
|
planka_rotate_secrets: false
|
||||||
|
|
||||||
|
pre_tasks:
|
||||||
|
- name: Fail if planka_admin_password is default
|
||||||
|
ansible.builtin.fail:
|
||||||
|
msg: "planka_admin_password is still the default value. Pass a real password via -e or Semaphore extra vars."
|
||||||
|
when: planka_admin_password == "ChangeMe123!"
|
||||||
|
|
||||||
|
- name: Fail if planka_base_url is not set
|
||||||
|
ansible.builtin.fail:
|
||||||
|
msg: "planka_base_url is required. Pass it via -e planka_base_url=https://planka.example.wulf.cloud"
|
||||||
|
when: planka_base_url is not defined or planka_base_url | length == 0
|
||||||
|
|
||||||
|
- name: Verify Docker is running
|
||||||
|
ansible.builtin.service_facts:
|
||||||
|
|
||||||
|
- name: Fail if Docker is not running
|
||||||
|
ansible.builtin.fail:
|
||||||
|
msg: "Docker is not running on this host."
|
||||||
|
when: "'docker.service' not in ansible_facts.services or ansible_facts.services['docker.service'].state != 'running'"
|
||||||
|
|
||||||
|
- name: Ensure pangolin Docker network exists
|
||||||
|
community.docker.docker_network:
|
||||||
|
name: pangolin
|
||||||
|
driver: bridge
|
||||||
|
state: present
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
- name: Create stack directory
|
||||||
|
ansible.builtin.file:
|
||||||
|
path: "{{ planka_stack_dir }}"
|
||||||
|
state: directory
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0750"
|
||||||
|
|
||||||
|
- name: Check if .secrets file exists (persisted secrets)
|
||||||
|
ansible.builtin.stat:
|
||||||
|
path: "{{ planka_stack_dir }}/.secrets"
|
||||||
|
register: _secrets_stat
|
||||||
|
|
||||||
|
- name: Generate new secrets
|
||||||
|
ansible.builtin.shell:
|
||||||
|
cmd: "echo SECRET_KEY=$(openssl rand -hex 64) && echo INTERNAL_ACCESS_TOKEN=$(openssl rand -hex 32)"
|
||||||
|
register: _new_secrets
|
||||||
|
changed_when: false
|
||||||
|
when: not _secrets_stat.stat.exists or planka_rotate_secrets
|
||||||
|
|
||||||
|
- name: Persist generated secrets
|
||||||
|
ansible.builtin.copy:
|
||||||
|
dest: "{{ planka_stack_dir }}/.secrets"
|
||||||
|
content: "{{ _new_secrets.stdout }}"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0600"
|
||||||
|
when: not _secrets_stat.stat.exists or planka_rotate_secrets
|
||||||
|
|
||||||
|
- name: Read persisted secrets
|
||||||
|
ansible.builtin.slurp:
|
||||||
|
src: "{{ planka_stack_dir }}/.secrets"
|
||||||
|
register: _secrets_file
|
||||||
|
|
||||||
|
- name: Parse secret values
|
||||||
|
ansible.builtin.set_fact:
|
||||||
|
planka_secret_key: "{{ (_secrets_file.content | b64decode).split('\n') | select('match', '^SECRET_KEY=') | first | regex_replace('^SECRET_KEY=', '') }}"
|
||||||
|
planka_access_token: "{{ (_secrets_file.content | b64decode).split('\n') | select('match', '^INTERNAL_ACCESS_TOKEN=') | first | regex_replace('^INTERNAL_ACCESS_TOKEN=', '') }}"
|
||||||
|
|
||||||
|
- name: Deploy compose.yaml
|
||||||
|
ansible.builtin.template:
|
||||||
|
src: templates/planka-compose.yml.j2
|
||||||
|
dest: "{{ planka_stack_dir }}/compose.yaml"
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0640"
|
||||||
|
notify: Restart planka
|
||||||
|
|
||||||
|
- name: Deploy Planka stack
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ planka_stack_dir }}"
|
||||||
|
pull: always
|
||||||
|
build: never
|
||||||
|
state: present
|
||||||
|
register: _compose_result
|
||||||
|
|
||||||
|
- name: Wait for Planka to respond
|
||||||
|
ansible.builtin.uri:
|
||||||
|
url: "http://{{ ansible_default_ipv4.address }}:{{ planka_port }}"
|
||||||
|
status_code: 200
|
||||||
|
follow_redirects: safe
|
||||||
|
register: _health
|
||||||
|
until: _health.status == 200
|
||||||
|
retries: 12
|
||||||
|
delay: 5
|
||||||
|
ignore_errors: true
|
||||||
|
|
||||||
|
- name: Deployment summary
|
||||||
|
ansible.builtin.debug:
|
||||||
|
msg:
|
||||||
|
- "Planka deployed"
|
||||||
|
- "URL: {{ planka_base_url }}"
|
||||||
|
- "Admin: {{ planka_admin_email }}"
|
||||||
|
- "API token: {{ planka_access_token }}"
|
||||||
|
- "Stack dir: {{ planka_stack_dir }}"
|
||||||
|
- "Secrets rotated: {{ planka_rotate_secrets }}"
|
||||||
|
|
||||||
|
handlers:
|
||||||
|
- name: Restart planka
|
||||||
|
community.docker.docker_compose_v2:
|
||||||
|
project_src: "{{ planka_stack_dir }}"
|
||||||
|
state: present
|
||||||
|
recreate: always
|
||||||
|
listen: Restart planka
|
||||||
60
playbooks/templates/planka-compose.yml.j2
Normal file
60
playbooks/templates/planka-compose.yml.j2
Normal file
|
|
@ -0,0 +1,60 @@
|
||||||
|
networks:
|
||||||
|
pangolin:
|
||||||
|
external: true
|
||||||
|
planka_net:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
services:
|
||||||
|
planka:
|
||||||
|
image: ghcr.io/plankanban/planka:{{ planka_version }}
|
||||||
|
container_name: planka_app
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "{{ planka_port }}:1337"
|
||||||
|
volumes:
|
||||||
|
- favicons:/app/public/favicons
|
||||||
|
- user-avatars:/app/public/user-avatars
|
||||||
|
- background-images:/app/public/background-images
|
||||||
|
- attachments:/app/private/attachments
|
||||||
|
environment:
|
||||||
|
BASE_URL: {{ planka_base_url }}
|
||||||
|
DATABASE_URL: postgresql://postgres@postgres/{{ planka_db_name }}
|
||||||
|
SECRET_KEY: {{ planka_secret_key }}
|
||||||
|
INTERNAL_ACCESS_TOKEN: {{ planka_access_token }}
|
||||||
|
LOG_LEVEL: {{ planka_log_level }}
|
||||||
|
TRUST_PROXY: "{{ planka_trust_proxy | lower }}"
|
||||||
|
TOKEN_EXPIRES_IN: "{{ planka_token_expires_in }}"
|
||||||
|
DEFAULT_ADMIN_EMAIL: {{ planka_admin_email }}
|
||||||
|
DEFAULT_ADMIN_PASSWORD: {{ planka_admin_password }}
|
||||||
|
DEFAULT_ADMIN_NAME: {{ planka_admin_name }}
|
||||||
|
DEFAULT_ADMIN_USERNAME: {{ planka_admin_username }}
|
||||||
|
depends_on:
|
||||||
|
postgres:
|
||||||
|
condition: service_healthy
|
||||||
|
networks:
|
||||||
|
- pangolin
|
||||||
|
- planka_net
|
||||||
|
|
||||||
|
postgres:
|
||||||
|
image: postgres:16-alpine
|
||||||
|
container_name: planka-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
volumes:
|
||||||
|
- db-data:/var/lib/postgresql/data
|
||||||
|
environment:
|
||||||
|
POSTGRES_DB: {{ planka_db_name }}
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U postgres -d {{ planka_db_name }}"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
networks:
|
||||||
|
- planka_net
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
favicons:
|
||||||
|
user-avatars:
|
||||||
|
background-images:
|
||||||
|
attachments:
|
||||||
|
db-data:
|
||||||
Loading…
Add table
Add a link
Reference in a new issue