diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml new file mode 100644 index 0000000000..b844b5fe5b --- /dev/null +++ b/.github/workflows/ansible-deploy.yml @@ -0,0 +1,75 @@ +name: Ansible Deploy Python + +on: + push: + branches: [master] + paths: + - 'ansible/playbooks/**' + - 'ansible/roles/**' + - '.github/workflows/ansible-deploy.yml' + pull_request: + branches: [master] + paths: + - 'ansible/**' + - '!ansible/docs/**' + - '.github/workflows/ansible-deploy.yml' + +jobs: + lint: + name: Ansible Lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.14' + + - name: Install dependencies + run: pip install ansible ansible-lint + + - name: Create vault password for lint + run: echo "empty" > ansible/.vault_pass + + - name: Run ansible-lint + run: ansible-lint playbooks/*.yml + working-directory: ansible + + deploy: + name: Deploy Python App + needs: lint + if: github.event_name == 'push' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.14' + + - name: Install Ansible + run: pip install ansible + + # - name: Setup SSH + # run: | + # mkdir -p ~/.ssh + # echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519 + # chmod 600 ~/.ssh/id_ed25519 + # ssh-keyscan -H ${{ secrets.VM_HOST }} >> ~/.ssh/known_hosts 2>/dev/null + + # - name: Create vault password file + # run: echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass + + # - name: Run Ansible playbook + # run: | + # ansible-playbook playbooks/deploy_python.yml \ + # -i inventory/hosts.ini \ + # --vault-password-file /tmp/vault_pass + # working-directory: ansible + + # - name: Verify deployment + # run: | + # sleep 10 + # curl -f http://${{ secrets.VM_HOST }}:5000/health + + # - name: Cleanup + # if: always() + # run: rm -f /tmp/vault_pass \ No newline at end of file diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg index 44142226bc..54b3683b8c 100644 --- a/ansible/ansible.cfg +++ b/ansible/ansible.cfg @@ -9,4 +9,4 @@ vault_password_file = .vault_pass [privilege_escalation] become = True become_method = sudo -become_user = root \ No newline at end of file +become_user = root diff --git a/ansible/docs/LAB06.md b/ansible/docs/LAB06.md new file mode 100644 index 0000000000..42a09d7289 --- /dev/null +++ b/ansible/docs/LAB06.md @@ -0,0 +1,78 @@ +# Lab 06 + +## 1. Overview + +All roles refactored with blocks, docker replaced with docker-compose and Jinja2 templates. + +Technologies used: Ansible, docker-compose, Jinja2, GItHub Actions. + +## 2. Blocks & Tags + +### `common` role + +`block` installs packages, `rescue` forces cache update and `always` logs completition. + +Tags: packages. + +### `docker` role + +`block` installs docker related packages, `rescue` forces cache update and `always` ensures everything is installed. + +Tags: docker_install, docker_config. + +### `web_app` role + +`block` deploys app and `rescue` logs failure. + +Tags: web_app_wipe, web_app_deploy. + +### Example run + +```text +$ ansible-playbook playbooks/provision.yml --tags "docker" +PLAY RECAP +lab05-server: ok=9 changed=0 (only docker tasks ran, common skipped) +``` + +## 3. Docker Compose Migration + +### Template structure + +Template includes app name, image name, ports, enviroment values and restart policy. + +### Role dependencies + +`web_app` role depends on `docker` role, since the last one installs essential packages. + +### Before/after comparison + +Before: manual docker-related calls (like pull, run, stop). +After: `docker compose up -d --pull always` manages everything. + +## 4. Wipe Logic + +### Implementation + +`wipe.yml` task stops and remove container, removes docker-compose file, removes app directory, removes docker image and logs completition. + +### Variable + tag + +Controlled by variable `web_app_wipe` (default: `false`) and tag `web_app_wipe`. + +### Test results + +```text +$ ansible-playbook playbooks/deploy_python.yml -e "web_app_wipe=true" --tags web_app_wipe +TASK [web_app : Stop and remove containers] changed +TASK [web_app : Remove docker-compose file] changed +TASK [web_app : Remove app directory] changed +TASK [web_app : Remove Docker image] changed +TASK [web_app : Log wipe completion] "Application devops-python wiped successfully" +PLAY RECAP: ok=7 changed=4 +``` + +## 5. CI/CD Integration + +### Workflow architecture + +Workflow should deploy app on push to `master` branch. Workflow has 2 jobs: lint and deploy. Workflow triggers only if ansible-related thing changed or workflow. diff --git a/ansible/group_vars/all.yml b/ansible/inventory/group_vars/all.yml similarity index 100% rename from ansible/group_vars/all.yml rename to ansible/inventory/group_vars/all.yml diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini index 581e824a45..538488af0e 100644 --- a/ansible/inventory/hosts.ini +++ b/ansible/inventory/hosts.ini @@ -2,4 +2,4 @@ lab05-server ansible_host=84.252.129.211 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa [webservers:vars] -ansible_python_interpreter=/usr/bin/python3 \ No newline at end of file +ansible_python_interpreter=/usr/bin/python3 diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml index 5c2250aa20..95174b9e0e 100644 --- a/ansible/playbooks/deploy.yml +++ b/ansible/playbooks/deploy.yml @@ -4,4 +4,4 @@ become: true roles: - - app_deploy \ No newline at end of file + - web_app diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml index 9156774548..7cc2e6678d 100644 --- a/ansible/playbooks/provision.yml +++ b/ansible/playbooks/provision.yml @@ -5,4 +5,4 @@ roles: - common - - docker \ No newline at end of file + - docker diff --git a/ansible/playbooks/site.yml b/ansible/playbooks/site.yml index 98ad3f13dd..128fcf3238 100644 --- a/ansible/playbooks/site.yml +++ b/ansible/playbooks/site.yml @@ -1,3 +1,9 @@ --- -- import_playbook: provision.yml -- import_playbook: deploy.yml \ No newline at end of file +- name: Full site setup + hosts: webservers + become: true + + roles: + - common + - docker + - web_app diff --git a/ansible/roles/app_deploy/defaults/main.yml b/ansible/roles/app_deploy/defaults/main.yml deleted file mode 100644 index 443697f650..0000000000 --- a/ansible/roles/app_deploy/defaults/main.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -app_restart_policy: unless-stopped -app_healthcheck_path: /health -app_environment: - APP_NAME: "{{ app_name }}" - PORT: "{{ app_port }}" \ No newline at end of file diff --git a/ansible/roles/app_deploy/handlers/main.yml b/ansible/roles/app_deploy/handlers/main.yml deleted file mode 100644 index 2840c90fa8..0000000000 --- a/ansible/roles/app_deploy/handlers/main.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -- name: restart app - community.docker.docker_container: - name: "{{ app_container_name }}" - state: started - restart: true \ No newline at end of file diff --git a/ansible/roles/app_deploy/tasks/main.yml b/ansible/roles/app_deploy/tasks/main.yml deleted file mode 100644 index 66f502d11e..0000000000 --- a/ansible/roles/app_deploy/tasks/main.yml +++ /dev/null @@ -1,49 +0,0 @@ ---- -- name: Log in to Docker Hub - community.docker.docker_login: - username: "{{ dockerhub_username }}" - password: "{{ dockerhub_password }}" - registry_url: https://index.docker.io/v1/ - no_log: true - -- name: Pull Docker image - community.docker.docker_image: - name: "{{ docker_image }}" - tag: "{{ docker_image_tag }}" - source: pull - force_source: yes - -- name: Stop (if running) and remove old (if exists) container - community.docker.docker_container: - name: "{{ app_container_name }}" - state: absent - ignore_errors: yes - -- name: Start new container - community.docker.docker_container: - name: "{{ app_container_name }}" - image: "{{ docker_image }}:{{ docker_image_tag }}" - state: started - restart_policy: "{{ app_restart_policy }}" - ports: - - "{{ app_port }}:{{ app_port }}" - env: "{{ app_environment_vars | default({}) }}" - notify: restart app - -- name: Wait for application port to be available - wait_for: - port: "{{ app_port }}" - delay: 5 - timeout: 60 - state: started - -- name: Verify health endpoint - uri: - url: "http://localhost:{{ app_port }}{{ app_healthcheck_path }}" - method: GET - status_code: 200 - timeout: 10 - register: health_check - retries: 5 - delay: 5 - until: health_check.status == 200 \ No newline at end of file diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml index 6ff7ccc8be..cf9753126f 100644 --- a/ansible/roles/common/defaults/main.yml +++ b/ansible/roles/common/defaults/main.yml @@ -2,4 +2,4 @@ common_packages: - python3-pip - curl - git - - nano \ No newline at end of file + - nano diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml index a2d3f85453..0c13344800 100644 --- a/ansible/roles/common/tasks/main.yml +++ b/ansible/roles/common/tasks/main.yml @@ -1,10 +1,33 @@ --- -- name: Update apt cache - apt: - update_cache: yes - cache_valid_time: 3600 - -- name: Install common packages - apt: - name: "{{ common_packages }}" - state: present \ No newline at end of file +- name: Install system packages + become: true + tags: + - packages + block: + - name: Update apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + + - name: Install common packages + ansible.builtin.apt: + name: "{{ common_packages }}" + state: present + + rescue: + - name: Force update cache + ansible.builtin.apt: + update_cache: true + force: true + + - name: Retry package installation + ansible.builtin.apt: + name: "{{ common_packages }}" + state: present + + always: + - name: Log package setup completion + ansible.builtin.copy: + content: "Common packages provisioned at {{ ansible_date_time.iso8601 }}\n" + dest: /tmp/ansible_common_done.log + mode: '0644' diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml index 153dacb14a..9b96f8e188 100644 --- a/ansible/roles/docker/defaults/main.yml +++ b/ansible/roles/docker/defaults/main.yml @@ -7,4 +7,4 @@ docker_packages: - docker-buildx-plugin - docker-compose-plugin -docker_repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" \ No newline at end of file +docker_repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml index ad85b66150..07aa0eb290 100644 --- a/ansible/roles/docker/handlers/main.yml +++ b/ansible/roles/docker/handlers/main.yml @@ -1,5 +1,5 @@ --- -- name: restart docker - service: +- name: Restart docker + ansible.builtin.service: name: docker - state: restarted \ No newline at end of file + state: restarted diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml index 861f4503f6..a294febcb0 100644 --- a/ansible/roles/docker/tasks/main.yml +++ b/ansible/roles/docker/tasks/main.yml @@ -1,54 +1,80 @@ --- -- name: Install Docker prerequisites - apt: - name: - - ca-certificates - - curl - - gnupg - - lsb-release - state: present - update_cache: true - cache_valid_time: 3600 - -- name: Create directory for Docker GPG key - file: - path: /etc/apt/keyrings - state: directory - mode: '0755' - -- name: Add Docker GPG key - apt_key: - url: https://download.docker.com/linux/ubuntu/gpg - keyring: /etc/apt/keyrings/docker.gpg - state: present - -- name: Add Docker apt repository - apt_repository: - repo: "{{ docker_repo }}" - filename: docker - state: present - update_cache: true - notify: restart docker - -- name: Install Docker packages - apt: - name: "{{ docker_packages }}" - state: present - notify: restart docker - -- name: Install Python Docker SDK package - apt: - name: python3-docker - state: present - -- name: Ensure Docker service is enabled and running - service: - name: docker - state: started - enabled: true - -- name: Add user to docker group - user: - name: "{{ docker_user }}" - groups: docker - append: true \ No newline at end of file +- name: Install Docker engine + become: true + tags: + - docker_install + block: + - name: Install Docker prerequisites + ansible.builtin.apt: + name: + - ca-certificates + - curl + - gnupg + - lsb-release + state: present + update_cache: true + cache_valid_time: 3600 + + - name: Create directory for Docker GPG key + ansible.builtin.file: + path: /etc/apt/keyrings + state: directory + mode: '0755' + + - name: Add Docker GPG key + ansible.builtin.apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + keyring: /etc/apt/keyrings/docker.gpg + state: present + + - name: Add Docker apt repository + ansible.builtin.apt_repository: + repo: "{{ docker_repo }}" + filename: docker + state: present + update_cache: true + notify: Restart docker + + - name: Install Docker packages + ansible.builtin.apt: + name: "{{ docker_packages }}" + state: present + notify: Restart docker + + - name: Ensure Docker service is enabled and running + ansible.builtin.service: + name: docker + state: started + enabled: true + + rescue: + - name: Wait before retrying + ansible.builtin.pause: + seconds: 10 + + - name: Retry apt update + ansible.builtin.apt: + update_cache: true + + always: + - name: Ensure Docker service is enabled and started + ansible.builtin.service: + name: docker + state: started + enabled: true + +- name: Configure Docker + become: true + tags: + - docker_config + block: + - name: Add user to docker group + ansible.builtin.user: + name: "{{ docker_user }}" + groups: docker + append: true + + - name: Install Python Docker SDK package + ansible.builtin.apt: + name: python3-docker + state: present diff --git a/ansible/roles/web_app/defaults/main.yml b/ansible/roles/web_app/defaults/main.yml new file mode 100644 index 0000000000..c986659878 --- /dev/null +++ b/ansible/roles/web_app/defaults/main.yml @@ -0,0 +1,13 @@ +--- +web_app_dir: /app +web_app_name: devops-app +web_app_docker_image: your_dockerhub_username/devops-info-service +web_app_docker_tag: latest +web_app_port: 8000 +web_app_restart_policy: unless-stopped +web_app_healthcheck_path: /health +web_app_wipe: false + +web_app_environment: + APP_NAME: "{{ app_name }}" + PORT: "{{ app_port }}" diff --git a/ansible/roles/web_app/handlers/main.yml b/ansible/roles/web_app/handlers/main.yml new file mode 100644 index 0000000000..ff802e6d9a --- /dev/null +++ b/ansible/roles/web_app/handlers/main.yml @@ -0,0 +1,6 @@ +--- +- name: Restart app + ansible.builtin.command: + cmd: docker compose restart + chdir: "{{ web_app_dir }}" + changed_when: true diff --git a/ansible/roles/web_app/meta/main.yml b/ansible/roles/web_app/meta/main.yml new file mode 100644 index 0000000000..cb7d8e0460 --- /dev/null +++ b/ansible/roles/web_app/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: docker diff --git a/ansible/roles/web_app/tasks/main.yml b/ansible/roles/web_app/tasks/main.yml new file mode 100644 index 0000000000..3ab3db6edf --- /dev/null +++ b/ansible/roles/web_app/tasks/main.yml @@ -0,0 +1,53 @@ +--- +- name: Include wipe tasks + ansible.builtin.include_tasks: wipe.yml + tags: + - web_app_wipe + +- name: Deploy application with Docker Compose + tags: + - web_app_deploy + - compose + block: + - name: Create application directory + ansible.builtin.file: + path: "{{ web_app_dir }}" + state: directory + mode: "0755" + + - name: Template docker-compose file + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ web_app_dir }}/docker-compose.yml" + mode: "0644" + + - name: Start docker-compose project + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir }}" + state: present + pull: always + recreate: auto + register: web_app_compose_result + + - name: Wait for application port to be available + ansible.builtin.wait_for: + port: "{{ web_app_port }}" + delay: 5 + timeout: 60 + state: started + + - name: Verify health endpoint + ansible.builtin.uri: + url: "http://localhost:{{ web_app_port }}{{ web_app_healthcheck_path }}" + method: GET + status_code: 200 + timeout: 10 + register: web_app_health_check + retries: 5 + delay: 5 + until: health_check.status == 200 + + rescue: + - name: Log deployment failure + ansible.builtin.debug: + msg: "Deployment of {{ web_app_name }} failed" diff --git a/ansible/roles/web_app/tasks/wipe.yml b/ansible/roles/web_app/tasks/wipe.yml new file mode 100644 index 0000000000..a2e60045d7 --- /dev/null +++ b/ansible/roles/web_app/tasks/wipe.yml @@ -0,0 +1,32 @@ +--- +- name: Wipe web application + when: web_app_wipe | bool + tags: + - web_app_wipe + block: + - name: Stop and remove containers via docker-compose + ansible.builtin.command: + cmd: docker compose down --remove-orphans + chdir: "{{ web_app_dir }}" + changed_when: true + failed_when: false + + - name: Remove docker-compose file + ansible.builtin.file: + path: "{{ web_app_dir }}/docker-compose.yml" + state: absent + + - name: Remove app directory + ansible.builtin.file: + path: "{{ web_app_dir }}" + state: absent + + - name: Remove Docker image + ansible.builtin.command: + cmd: "docker rmi {{ docker_image }}:{{ docker_image_tag }}" + failed_when: false + changed_when: true + + - name: Log wipe completion + ansible.builtin.debug: + msg: "Application {{ web_app_name }} wiped successfully" diff --git a/ansible/roles/web_app/templates/docker-compose.yml.j2 b/ansible/roles/web_app/templates/docker-compose.yml.j2 new file mode 100644 index 0000000000..c834c2c13e --- /dev/null +++ b/ansible/roles/web_app/templates/docker-compose.yml.j2 @@ -0,0 +1,8 @@ +services: + {{ web_app_name }}: + image: {{ web_app_docker_image }}:{{ web_app_docker_tag }} + container_name: {{ web_app_name }} + ports: + - "{{ web_app_port }}:{{ web_app_port }}" + enviroment: {{ web_app_environment | default({}) }} + restart: {{ web_app_restart_policy }}