From ed3d5833b756b0fe35894f4317974a35e34d5d0e Mon Sep 17 00:00:00 2001 From: Lorentz Date: Tue, 7 Apr 2026 12:49:18 -0400 Subject: [PATCH] Add Planka deployment playbook and compose template Co-Authored-By: Claude Sonnet 4.6 --- playbooks/planka.yml | 131 ++++++++++++++++++++++ playbooks/templates/planka-compose.yml.j2 | 60 ++++++++++ 2 files changed, 191 insertions(+) create mode 100644 playbooks/planka.yml create mode 100644 playbooks/templates/planka-compose.yml.j2 diff --git a/playbooks/planka.yml b/playbooks/planka.yml new file mode 100644 index 0000000..33d6e64 --- /dev/null +++ b/playbooks/planka.yml @@ -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 diff --git a/playbooks/templates/planka-compose.yml.j2 b/playbooks/templates/planka-compose.yml.j2 new file mode 100644 index 0000000..00aaf93 --- /dev/null +++ b/playbooks/templates/planka-compose.yml.j2 @@ -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: