feat: Integrate Ansible playbooks into CI/CD workflows
- Add deploy-application-code.yml for Git-based code deployment - Add install-composer-dependencies.yml for dependency installation - Add deploy-image.yml for Docker image deployment - Update build-image.yml to use Ansible playbooks - Update manual-deploy.yml to use Ansible playbooks - Add ANSIBLE_VAULT_PASSWORD secret handling
This commit is contained in:
123
deployment/ansible/playbooks/deploy-application-code.yml
Normal file
123
deployment/ansible/playbooks/deploy-application-code.yml
Normal file
@@ -0,0 +1,123 @@
|
||||
---
|
||||
- name: Deploy Application Code via Git
|
||||
hosts: "{{ deployment_hosts | default('production') }}"
|
||||
gather_facts: yes
|
||||
become: no
|
||||
|
||||
vars:
|
||||
application_code_dest: "/home/deploy/michaelschiemer/current"
|
||||
git_repository_url: "{{ git_repository_url | default('https://git.michaelschiemer.de/michael/michaelschiemer.git') }}"
|
||||
# Determine branch based on environment
|
||||
git_branch: >-
|
||||
{%- if deployment_environment == 'staging' -%}
|
||||
{{ git_branch | default('staging') }}
|
||||
{%- else -%}
|
||||
{{ git_branch | default('main') }}
|
||||
{%- endif -%}
|
||||
git_token: "{{ git_token | default('') }}"
|
||||
# Deployment environment (staging or production)
|
||||
deployment_environment: "{{ deployment_environment | default('production') }}"
|
||||
|
||||
tasks:
|
||||
- name: Ensure Git is installed
|
||||
ansible.builtin.apt:
|
||||
name: git
|
||||
state: present
|
||||
update_cache: no
|
||||
become: yes
|
||||
|
||||
- name: Ensure application code directory exists
|
||||
file:
|
||||
path: "{{ application_code_dest }}"
|
||||
state: directory
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
mode: '0755'
|
||||
become: yes
|
||||
|
||||
- name: Check if repository already exists
|
||||
stat:
|
||||
path: "{{ application_code_dest }}/.git"
|
||||
register: git_repo_exists
|
||||
|
||||
- name: Clone repository (if not exists)
|
||||
ansible.builtin.git:
|
||||
repo: "{{ git_repository_url }}"
|
||||
dest: "{{ application_code_dest }}"
|
||||
version: "{{ git_branch }}"
|
||||
force: no
|
||||
update: no
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
when: not git_repo_exists.stat.exists
|
||||
environment:
|
||||
GIT_TERMINAL_PROMPT: "0"
|
||||
vars:
|
||||
ansible_become: no
|
||||
|
||||
- name: Update repository (if exists)
|
||||
ansible.builtin.git:
|
||||
repo: "{{ git_repository_url }}"
|
||||
dest: "{{ application_code_dest }}"
|
||||
version: "{{ git_branch }}"
|
||||
force: yes
|
||||
update: yes
|
||||
owner: "{{ ansible_user }}"
|
||||
group: "{{ ansible_user }}"
|
||||
when: git_repo_exists.stat.exists
|
||||
environment:
|
||||
GIT_TERMINAL_PROMPT: "0"
|
||||
vars:
|
||||
ansible_become: no
|
||||
|
||||
- name: Ensure executable permissions on PHP scripts
|
||||
file:
|
||||
path: "{{ application_code_dest }}/{{ item }}"
|
||||
mode: '0755'
|
||||
loop:
|
||||
- worker.php
|
||||
- console.php
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Verify worker.php exists
|
||||
stat:
|
||||
path: "{{ application_code_dest }}/worker.php"
|
||||
register: worker_php_stat
|
||||
|
||||
- name: Verify console.php exists
|
||||
stat:
|
||||
path: "{{ application_code_dest }}/console.php"
|
||||
register: console_php_stat
|
||||
|
||||
- name: Verify composer.json exists
|
||||
stat:
|
||||
path: "{{ application_code_dest }}/composer.json"
|
||||
register: composer_json_stat
|
||||
|
||||
- name: Get current Git commit hash
|
||||
shell: |
|
||||
cd {{ application_code_dest }} && git rev-parse HEAD
|
||||
register: git_commit_hash
|
||||
changed_when: false
|
||||
when: git_repo_exists.stat.exists
|
||||
|
||||
- name: Display file verification results
|
||||
debug:
|
||||
msg: |
|
||||
File Verification:
|
||||
- worker.php: {{ 'EXISTS' if worker_php_stat.stat.exists else 'MISSING' }}
|
||||
- console.php: {{ 'EXISTS' if console_php_stat.stat.exists else 'MISSING' }}
|
||||
- composer.json: {{ 'EXISTS' if composer_json_stat.stat.exists else 'MISSING' }}
|
||||
- Git Branch: {{ git_branch }}
|
||||
- Git Commit: {{ git_commit_hash.stdout | default('N/A') }}
|
||||
|
||||
- name: Fail if critical files are missing
|
||||
fail:
|
||||
msg: |
|
||||
Critical files are missing after Git deployment:
|
||||
{% if not worker_php_stat.stat.exists %}- worker.php{% endif %}
|
||||
{% if not console_php_stat.stat.exists %}- console.php{% endif %}
|
||||
{% if not composer_json_stat.stat.exists %}- composer.json{% endif %}
|
||||
when:
|
||||
- not worker_php_stat.stat.exists or not console_php_stat.stat.exists or not composer_json_stat.stat.exists
|
||||
|
||||
142
deployment/ansible/playbooks/deploy-image.yml
Normal file
142
deployment/ansible/playbooks/deploy-image.yml
Normal file
@@ -0,0 +1,142 @@
|
||||
---
|
||||
- name: Deploy Docker Image to Application Stack
|
||||
hosts: "{{ deployment_hosts | default('production') }}"
|
||||
gather_facts: yes
|
||||
become: no
|
||||
|
||||
vars:
|
||||
# Determine stack path based on environment
|
||||
application_stack_dest: >-
|
||||
{%- if deployment_environment == 'staging' -%}
|
||||
{{ staging_stack_path | default(stacks_base_path + '/staging') }}
|
||||
{%- else -%}
|
||||
{{ app_stack_path | default(stacks_base_path + '/production') }}
|
||||
{%- endif -%}
|
||||
application_compose_suffix: >-
|
||||
{%- if deployment_environment == 'staging' -%}
|
||||
staging.yml
|
||||
{%- else -%}
|
||||
production.yml
|
||||
{%- endif -%}
|
||||
# Image to deploy (can be overridden via -e image_tag=...)
|
||||
image_tag: "{{ image_tag | default('latest') }}"
|
||||
docker_registry: "{{ docker_registry | default('registry.michaelschiemer.de') }}"
|
||||
app_name: "{{ app_name | default('framework') }}"
|
||||
# Full image URL
|
||||
deploy_image: "{{ docker_registry }}/{{ app_name }}:{{ image_tag }}"
|
||||
# Deployment environment (staging or production)
|
||||
deployment_environment: "{{ deployment_environment | default('production') }}"
|
||||
|
||||
tasks:
|
||||
- name: Determine Docker registry password from vault or extra vars
|
||||
ansible.builtin.set_fact:
|
||||
registry_password: >-
|
||||
{%- if docker_registry_password is defined and docker_registry_password | string | trim != '' -%}
|
||||
{{ docker_registry_password }}
|
||||
{%- elif vault_docker_registry_password is defined and vault_docker_registry_password | string | trim != '' -%}
|
||||
{{ vault_docker_registry_password }}
|
||||
{%- else -%}
|
||||
{{ '' }}
|
||||
{%- endif -%}
|
||||
no_log: yes
|
||||
|
||||
- name: Check if registry is accessible
|
||||
ansible.builtin.uri:
|
||||
url: "http://{{ docker_registry }}/v2/"
|
||||
method: GET
|
||||
status_code: [200, 401]
|
||||
timeout: 5
|
||||
register: registry_check
|
||||
ignore_errors: yes
|
||||
delegate_to: "{{ inventory_hostname }}"
|
||||
become: no
|
||||
|
||||
- name: Login to Docker registry
|
||||
community.docker.docker_login:
|
||||
registry_url: "{{ docker_registry }}"
|
||||
username: "{{ docker_registry_username | default('admin') }}"
|
||||
password: "{{ registry_password }}"
|
||||
when:
|
||||
- registry_password | string | trim != ''
|
||||
- registry_check.status | default(0) in [200, 401]
|
||||
no_log: yes
|
||||
ignore_errors: yes
|
||||
register: docker_login_result
|
||||
|
||||
- name: Pull Docker image
|
||||
community.docker.docker_image:
|
||||
name: "{{ deploy_image }}"
|
||||
source: pull
|
||||
pull: true
|
||||
register: image_pull_result
|
||||
failed_when: image_pull_result.failed | default(false)
|
||||
|
||||
- name: Verify image exists locally
|
||||
community.docker.docker_image_info:
|
||||
name: "{{ deploy_image }}"
|
||||
register: image_info
|
||||
failed_when: image_info.failed | default(false)
|
||||
|
||||
- name: Update docker-compose file with new image tag
|
||||
ansible.builtin.replace:
|
||||
path: "{{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }}"
|
||||
regexp: '^(\s+image:\s+)({{ docker_registry }}/{{ app_name }}:)(.*)$'
|
||||
replace: '\1\2{{ image_tag }}'
|
||||
register: compose_update_result
|
||||
failed_when: false
|
||||
changed_when: compose_update_result.changed | default(false)
|
||||
|
||||
- name: Update docker-compose file with new image (alternative pattern)
|
||||
ansible.builtin.replace:
|
||||
path: "{{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }}"
|
||||
regexp: 'image:\s+{{ docker_registry }}/{{ app_name }}:.*'
|
||||
replace: 'image: {{ deploy_image }}'
|
||||
register: compose_update_alt
|
||||
when: compose_update_result.changed == false
|
||||
failed_when: false
|
||||
changed_when: compose_update_alt.changed | default(false)
|
||||
|
||||
- name: Ensure Docker networks exist
|
||||
community.docker.docker_network:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
loop:
|
||||
- traefik-public
|
||||
- app-internal
|
||||
ignore_errors: yes
|
||||
|
||||
- name: Deploy application stack with new image
|
||||
shell: |
|
||||
cd {{ application_stack_dest }}
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.{{ application_compose_suffix }} up -d --pull missing --force-recreate --remove-orphans
|
||||
register: compose_deploy_result
|
||||
changed_when: true
|
||||
|
||||
- name: Wait for containers to start
|
||||
ansible.builtin.pause:
|
||||
seconds: 15
|
||||
|
||||
- name: Check container status
|
||||
shell: |
|
||||
cd {{ application_stack_dest }}
|
||||
docker compose -f docker-compose.base.yml -f docker-compose.{{ application_compose_suffix }} ps
|
||||
register: container_status
|
||||
changed_when: false
|
||||
|
||||
- name: Display deployment summary
|
||||
ansible.builtin.debug:
|
||||
msg: |
|
||||
========================================
|
||||
Image Deployment Summary
|
||||
========================================
|
||||
Image: {{ deploy_image }}
|
||||
Tag: {{ image_tag }}
|
||||
Environment: {{ deployment_environment }}
|
||||
Stack: {{ application_stack_dest }}
|
||||
Status: SUCCESS
|
||||
========================================
|
||||
|
||||
Container Status:
|
||||
{{ container_status.stdout | default('Not available') }}
|
||||
========================================
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
- name: Install Composer Dependencies in Application Container
|
||||
hosts: "{{ deployment_hosts | default('production') }}"
|
||||
gather_facts: no
|
||||
become: no
|
||||
|
||||
vars:
|
||||
# Determine stack path based on environment
|
||||
application_stack_dest: >-
|
||||
{%- if deployment_environment == 'staging' -%}
|
||||
{{ staging_stack_path | default(stacks_base_path + '/staging') }}
|
||||
{%- else -%}
|
||||
{{ app_stack_path | default(stacks_base_path + '/production') }}
|
||||
{%- endif -%}
|
||||
application_compose_suffix: >-
|
||||
{%- if deployment_environment == 'staging' -%}
|
||||
staging.yml
|
||||
{%- else -%}
|
||||
production.yml
|
||||
{%- endif -%}
|
||||
# Deployment environment (staging or production)
|
||||
deployment_environment: "{{ deployment_environment | default('production') }}"
|
||||
# Service name (php for production, staging-app for staging)
|
||||
php_service_name: >-
|
||||
{%- if deployment_environment == 'staging' -%}
|
||||
staging-app
|
||||
{%- else -%}
|
||||
php
|
||||
{%- endif -%}
|
||||
|
||||
tasks:
|
||||
- name: Check if composer.json exists
|
||||
stat:
|
||||
path: /home/deploy/michaelschiemer/current/composer.json
|
||||
delegate_to: "{{ inventory_hostname }}"
|
||||
register: composer_json_exists
|
||||
|
||||
- name: Fail if composer.json is missing
|
||||
fail:
|
||||
msg: "composer.json not found at /home/deploy/michaelschiemer/current/composer.json"
|
||||
when: not composer_json_exists.stat.exists
|
||||
|
||||
- name: Install composer dependencies in PHP container
|
||||
shell: |
|
||||
docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }} exec -T {{ php_service_name }} composer install --no-dev --optimize-autoloader --no-interaction
|
||||
register: composer_install
|
||||
changed_when: true
|
||||
failed_when: composer_install.rc != 0
|
||||
|
||||
- name: Restart queue-worker and scheduler to pick up vendor directory (production only)
|
||||
shell: |
|
||||
docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }} restart queue-worker scheduler
|
||||
register: restart_workers
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
when: deployment_environment == 'production'
|
||||
|
||||
- name: Display composer install output
|
||||
debug:
|
||||
msg: |
|
||||
Composer Install Output:
|
||||
{{ composer_install.stdout }}
|
||||
|
||||
- name: Verify vendor/autoload.php exists
|
||||
shell: |
|
||||
docker compose -f {{ application_stack_dest }}/docker-compose.base.yml -f {{ application_stack_dest }}/docker-compose.{{ application_compose_suffix }} exec -T {{ php_service_name }} test -f /var/www/html/vendor/autoload.php && echo "EXISTS" || echo "MISSING"
|
||||
register: autoload_check
|
||||
changed_when: false
|
||||
|
||||
- name: Display autoload verification
|
||||
debug:
|
||||
msg: "vendor/autoload.php: {{ autoload_check.stdout.strip() }}"
|
||||
|
||||
- name: Fail if autoload.php is missing
|
||||
fail:
|
||||
msg: "vendor/autoload.php was not created after composer install"
|
||||
when: "autoload_check.stdout.strip() != 'EXISTS'"
|
||||
|
||||
Reference in New Issue
Block a user