diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..cfc447d428 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,76 @@ +name: Python test and build + +on: + push: + branches: [ lab** ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PYTHON_VERSION: '3.12' + DOCKER_IMAGE_NAME: 'devops-info-service' + APP_PATH: 'app_python' + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + cache-dependency-path: '${{ env.APP_PATH }}/requirements.txt' + + - name: Install dependencies + run: | + pip install -r ${{ env.APP_PATH }}/requirements.txt + pip install pytest flake8 + + - name: Run a linter + run: flake8 ${{ env.APP_PATH }}/app.py --max-line-length=100 --statistics + + - name: Run unit tests + run: pytest ${{ env.APP_PATH }} + + - name: Install Snyk CLI + run: | + curl https://downloads.snyk.io/cli/stable/snyk-linux -o snyk-linux + curl https://downloads.snyk.io/cli/stable/snyk-linux.sha256 -o snyk.sha256 + sha256sum -c snyk.sha256 + chmod +x snyk-linux + sudo mv snyk-linux /usr/local/bin/snyk + - name: Run Snyk to test project dependencies + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + run: | + cd ${{ env.APP_PATH }} + snyk auth ${{ secrets.SNYK_TOKEN }} + snyk test + + docker: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Generate version + run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_ENV + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: ./${{ env.APP_PATH }} + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }}:latest + ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.DOCKER_IMAGE_NAME }}:${{ env.VERSION }} \ No newline at end of file diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..78a73a2ab9 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,12 @@ +[defaults] +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +remote_user = ubuntu +retry_files_enabled = False +deprecation_warnings = False + +[privilege_escalation] +become = True +become_method = sudo +become_user = root \ No newline at end of file diff --git a/ansible/docs/LAB05.md b/ansible/docs/LAB05.md new file mode 100644 index 0000000000..4d45030acf --- /dev/null +++ b/ansible/docs/LAB05.md @@ -0,0 +1,462 @@ +# LAB 05 + +## 1. Architecture Overview +### Ansible version used +ansible 2.10.8 + +### Target VM OS and version +Ubuntu 22.04.5 LTS + +### Role structure diagram +``` +ansible/ +├── inventory/ +│ └── hosts.ini # Static inventory with VM details +├── group_vars/ +│ └── all.yml # Encrypted variables (Vault) +├── roles/ +│ ├── common/ # Base system configuration +│ │ ├── tasks/ +│ │ │ └── main.yml # System updates and packages +│ │ └── defaults/ +│ │ └── main.yml # Default package list +│ ├── docker/ # Docker installation +│ │ ├── tasks/ +│ │ │ └── main.yml # Docker setup tasks +│ │ ├── handlers/ +│ │ │ └── main.yml # Docker restart handler +│ │ └── defaults/ +│ │ └── main.yml # Docker version defaults +│ └── app_deploy/ # Application deployment +│ ├── tasks/ +│ │ └── main.yml # Container deployment +│ ├── handlers/ +│ │ └── main.yml # Container restart +│ └── defaults/ +│ └── main.yml # App configuration +├── playbooks/ +│ ├── provision.yml # System provisioning +│ └── deploy.yml # Application deployment +└── ansible.cfg # Ansible configuration +``` + +### Why roles instead of monolithic playbooks? +Roles provide modularity, reusability, and maintainability. Instead of having one large playbook with hundreds of tasks, roles allow you to: +- Separate concerns: Each role handles a specific functionality (system setup, Docker, application) +- Reuse code: Same role can be used across different projects +- Share with community: Roles can be easily shared via Ansible Galaxy +- Parallel development: Multiple team members can work on different roles simultaneously +- Easier testing: Each role can be tested independently + +## 2. Roles Documentation +### Common Role +**Purpose**: Configures the base system with essential packages and timezone settings. This role ensures all servers have consistent basic configuration. + +**Variables**: + +| Variable | Default | Description +|----------|--------|-------------| +|common_packages | [python3-pip, curl, git, vim, htop, ...] | List of essential packages +|common_timezone | "UTC" | System timezone + +**Handlers**: None defined in this role. + +**Dependencies**: No dependencies on other roles. + +### Docker Role +**Purpose**: Installs and configures Docker CE on Ubuntu systems, including all necessary dependencies and user permissions. + +**Variables**: +| Variable | Default | Description +|----------|--------|-------------| +| docker_version | ["latest"] | Docker version to install +| docker_user | "{{ ansible_user }}" | User to add to docker group +| dockdocker_packageser_user | [docker-ce, docker-ce-cli, ...] | Docker packages to install + +**Handlers**: +- `restart docker`: Restarts Docker service when configuration changes + +**Dependencies**: None, but requires common role for base packages. + +### App_Deploy Role +**Purpose**: Deploys the application container from Docker Hub, manages container lifecycle, and performs health checks. + +**Variables**: +| Variable | Default | Description +|----------|--------|-------------| +| app_name | "devops-info-service" | Application name +| app_port | "5000" | Container port +| restart_policy | "unless-stopped" | Docker restart policy +| app_environment | {...} | Environment variables + +**Handlers**: +- `restart app container`: Restarts the application container + +**Dependencies**: Requires Docker role to be executed first. + + +## 3. Idempotency Demonstration + +### Terminal output from FIRST provision.yml run +``` +$ ansible-playbook playbooks/provision.yml + +PLAY [Provision web servers with common tools and Docker] ****************************************************** + +TASK [Gathering Facts] ***************************************************************************************** +ok: [damir-VirtualBox] + +TASK [common : Update apt cache] ******************************************************************************* +changed: [damir-VirtualBox] + +TASK [common : Install essential packages] ********************************************************************* +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +changed: [damir-VirtualBox] + +TASK [common : Set timezone] *********************************************************************************** +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +ok: [damir-VirtualBox] + +TASK [docker : Remove old Docker packages (if any)] ************************************************************ +ok: [damir-VirtualBox] + +TASK [docker : Install required system packages] *************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add Docker GPG key] ***************************************************************************** +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +changed: [damir-VirtualBox] + +TASK [docker : Add Docker repository] ************************************************************************** +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +changed: [damir-VirtualBox] + +TASK [docker : Install Docker packages] ************************************************************************ +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +changed: [damir-VirtualBox] + +TASK [docker : Ensure Docker service is running and enabled] *************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add user to docker group] *********************************************************************** +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +changed: [damir-VirtualBox] + +TASK [docker : Install Python Docker module for Ansible] ******************************************************* +changed: [damir-VirtualBox] + +TASK [docker : Verify Docker installation] ********************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Display Docker version] ************************************************************************* +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +ok: [damir-VirtualBox] => { + "msg": "Docker installed: Docker version 29.2.1, build a5c7197" +} + +RUNNING HANDLER [docker : restart docker] ********************************************************************** +changed: [damir-VirtualBox] + +PLAY RECAP ***************************************************************************************************** +damir-VirtualBox : ok=15 changed=8 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### Terminal output from SECOND provision.yml run +``` +$ ansible-playbook playbooks/provision.yml + +PLAY [Provision web servers with common tools and Docker] ****************************************************** + +TASK [Gathering Facts] ***************************************************************************************** +ok: [damir-VirtualBox] + +TASK [common : Update apt cache] ******************************************************************************* +ok: [damir-VirtualBox] + +TASK [common : Install essential packages] ********************************************************************* +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +ok: [damir-VirtualBox] + +TASK [common : Set timezone] *********************************************************************************** +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +ok: [damir-VirtualBox] + +TASK [docker : Remove old Docker packages (if any)] ************************************************************ +ok: [damir-VirtualBox] + +TASK [docker : Install required system packages] *************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add Docker GPG key] ***************************************************************************** +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +ok: [damir-VirtualBox] + +TASK [docker : Add Docker repository] ************************************************************************** +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +ok: [damir-VirtualBox] + +TASK [docker : Install Docker packages] ************************************************************************ +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +ok: [damir-VirtualBox] + +TASK [docker : Ensure Docker service is running and enabled] *************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add user to docker group] *********************************************************************** +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +ok: [damir-VirtualBox] + +TASK [docker : Install Python Docker module for Ansible] ******************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Verify Docker installation] ********************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Display Docker version] ************************************************************************* +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/core.py) as it seems to be +invalid: cannot import name 'environmentfilter' from 'jinja2.filters' (/home/damir/.local/lib/python3.10/site- +packages/jinja2/filters.py) +[WARNING]: Skipping plugin (/usr/lib/python3/dist-packages/ansible/plugins/filter/mathstuff.py) as it seems to +be invalid: cannot import name 'environmentfilter' from 'jinja2.filters' +(/home/damir/.local/lib/python3.10/site-packages/jinja2/filters.py) +ok: [damir-VirtualBox] => { + "msg": "Docker installed: Docker version 29.2.1, build a5c7197" +} + +PLAY RECAP ***************************************************************************************************** +damir-VirtualBox : ok=14 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +### First Run Analysis +What changed: +- Common role: Updated apt cache, installed packages +- Docker role: Added GPG key, repository, installed Docker, added user to group, installed Python modules +- Handlers: Restarted Docker service + +### Second Run Analysis +What didn't change: +- No package installations (all already present) +- No repository additions (already configured) +- No user modifications (already in docker group) +- No service restarts (configuration stable) + +### Why Idempotency Works +Ansible modules are designed to be idempotent: +- apt module: Checks if package is installed before attempting installation +- docker_container: Checks container state before creating/modifying +- user module: Verifies group membership before adding +- systemd: Confirms service state before changing +Each task checks the current state of the system and only makes changes when necessary, ensuring predictable and safe executions. + +## 4. Ansible Vault Usage +### Secure Credential Storage +``` +$ cat group_vars/all.yml +$ANSIBLE_VAULT;1.1;AES256 +37363538613332346638663330653662623065303465383039323431613066323366376465393965 +3338613132343965316433313431346233343935383430300a346130633437633365343664353132 +39353134616263626561356135356133313834343936386664646233306639366463303166303630 +6435343166373463370a623635656434353730356366373738643564616432663065653237353334 +62393931313930363734666133656463666535306435633765373764303131373462643961633733 +36653863633037663766656661316362633965326632303037623634383535336261393463666231 +35646431386239353037306535303663323161386536303663386230313165303239636466373264 +33343638616264666438376665313839646335633231633532623733306132613765386263363439 +39363337643531393634666131623163666432663631363734303333373032653663383666623035 +32363138396432613661313630383763303231383233656335656335613163336566646364336239 +63333034346632336561633263663136383539666562396635633434623966396166313063393038 +31666265633161613537353430333133663732363237636164613765386564663433346539636135 +37396237616537623565393566636134396138616538333065656164663166306338633865633464 +39333538623033643934373163643330356132383361343934323066303735373961653637663865 +31613230623435316330393531646538333162353363656363376132306261346163306430353432 +38303737316666333731373065383030633731326638343932653938333131336464353530313765 +62653065336138656235323932643831643238306130353731666233366530333361313737353233 +65343932353839343665663238376163373233623931343036346163613632353637376237396663 +38303266646665626231646463313530363466393335333565643934363833633463303238653764 +39613761633234363133383236303039623635386261346266373663313064373962306630313564 +35383932666139373336333634396365303364653339613938306238636139386337653331326266 +31356134333662306538323465663261356433616433343933633439663663623238363363333065 +396137666236663538343338633061376438 +``` + +### Vault Password Management Strategy +1. Local Development: Use .vault_pass file with restricted permissions (600) +2. CI/CD Pipeline: Store password in secure CI/CD variables +3. Team Environment: Use password manager with shared access +4. Backup: Encrypted backup of password in company vault + +### Why Ansible Vault is Important +- Security: Prevents exposure of sensitive data in version control +- Compliance: Meets security requirements for credential management +- Auditability: Encrypted files show clear intent to protect data +- Separation of concerns: Keeps configuration separate from secrets + +## 5. Deployment Verification + +### Terminal output from deploy.yml run +``` +$ ansible-playbook playbooks/deploy.yml --ask-vault-pass +Vault password: + +PLAY [Deploy application to web servers] *********************************************************************** + +TASK [Gathering Facts] ***************************************************************************************** +ok: [damir-VirtualBox] + +TASK [Display deployment information] ************************************************************************** +ok: [damir-VirtualBox] => { + "msg": [ + "Starting deployment of devops-info-service", + "Image: damirsadykov/devops-info-service:latest", + "Target host: damir-VirtualBox" + ] +} + +TASK [app_deploy : Log in to Docker Hub] *********************************************************************** +ok: [damir-VirtualBox] + +TASK [app_deploy : Pull Docker image] ************************************************************************** +ok: [damir-VirtualBox] + +TASK [app_deploy : Check if container is running] ************************************************************** +ok: [damir-VirtualBox] + +TASK [app_deploy : Stop existing container if running] ********************************************************* +skipping: [damir-VirtualBox] + +TASK [app_deploy : Remove old container if exists] ************************************************************* +skipping: [damir-VirtualBox] + +TASK [app_deploy : Run new container] ************************************************************************** +changed: [damir-VirtualBox] + +TASK [app_deploy : Wait for application to be ready] *********************************************************** +ok: [damir-VirtualBox] + +TASK [app_deploy : Check health endpoint] ********************************************************************** +ok: [damir-VirtualBox] + +TASK [app_deploy : Display container info] ********************************************************************* +ok: [damir-VirtualBox] => { + "msg": [ + "Application deployed successfully!", + "Container: devops-info-service", + "Port: 5000", + "Health endpoint: http://10.76.148.84:5000/health", + "Main endpoint: http://10.76.148.84:5000/" + ] +} + +TASK [Verify deployment] *************************************************************************************** +ok: [damir-VirtualBox] + +TASK [Show deployment status] ********************************************************************************** +ok: [damir-VirtualBox] => { + "msg": [ + "Deployment completed successfully!", + "Container status: running", + "Container started: 2026-02-25T16:12:17.417271155Z" + ] +} + +PLAY RECAP ***************************************************************************************************** +damir-VirtualBox : ok=11 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 + +``` + +### Container status: docker ps output +``` + ansible webservers -a "docker ps" --ask-vault-pass +Vault password: +damir-VirtualBox | CHANGED | rc=0 >> +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +e26c8e3625c7 damirsadykov/devops-info-service:latest "python app.py" 3 minutes ago Up 3 minutes 0.0.0.0:5000->5000/tcp devops-info-service +``` + +### Health check verification: curl outputs +``` +$ curl http://10.76.148.84:5000/health +{"status":"healthy","timestamp":"2026-02-25T16:13:14.969966Z","uptime_seconds":57} +$ curl http://10.76.148.84:5000/ +{"endpoints":[{"description":"Service information","method":"GET","path":"/"},{"description":"Health check","method":"GET","path":"/health"}],"request":{"client_ip":"10.76.148.244","method":"GET","path":"/","user_agent":"curl/7.81.0"},"runtime":{"current_time":"2026-02-25T16:13:30.343720Z","timezone":"UTC","uptime_human":"0 hours, 1 minute","uptime_seconds":72},"service":{"description":"DevOps course info service","framework":"Flask","name":"devops-info-service","version":"1.0.0"},"system":{"architecture":"x86_64","cpu_count":1,"hostname":"e26c8e3625c7","platform":"Linux","platform_version":"#100~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon Jan 19 17:10:19 UTC ","python_version":"3.12.12"}} +``` + +## Key Decisions +### Why use roles instead of plain playbooks? +Roles provide modular organization, making the codebase easier to understand, maintain, and reuse across different projects. They also enable team collaboration by allowing different people to work on different components simultaneously. + +### How do roles improve reusability? +Roles encapsulate specific functionality with well-defined interfaces (variables and defaults), allowing them to be shared via Ansible Galaxy and used in multiple playbooks with different configurations. + +### What makes a task idempotent? +A task is idempotent when it checks the current state before making changes and only acts if the desired state differs from the current state, ensuring multiple runs produce the same result. + +### How do handlers improve efficiency? +Handlers run only when notified by tasks and execute at the end of the play, preventing multiple unnecessary restarts and ensuring efficient resource usage. + +### Why is Ansible Vault necessary? +Ansible Vault protects sensitive information like passwords and API keys from exposure in version control systems, maintaining security while allowing infrastructure to be defined as code. \ No newline at end of file diff --git a/ansible/docs/LAB06.md b/ansible/docs/LAB06.md new file mode 100644 index 0000000000..19542005ea --- /dev/null +++ b/ansible/docs/LAB06.md @@ -0,0 +1,1044 @@ +# 1. Blocks & Tags + +## Block Usage in Each Role +Common role (roles/common/tasks/main.yml): + + Two main blocks: Package installation and User creation. + + Each block has its own tags (packages, users) and rescue/always sections. + + The rescue block fixes apt cache issues and retries installation. + + The always block logs completion. + +Docker role (roles/docker/tasks/main.yml): + + Blocks for docker_install and docker_config. + + rescue in the installation block waits 10 seconds and retries (handles transient network errors). + + always ensures Docker service is enabled. + +Web_app role (roles/web_app/tasks/main.yml): + + The deployment logic is wrapped in a block with rescue (logs failure) and always (shows status). + + Wipe tasks are included separately with their own tag. + +## Tag Strategy +Role‑level tags: common, docker, web_app. + +Fine‑grained tags: + + packages, users (common) + + docker_install, docker_config (docker) + + web_app_wipe (wipe only) + + compose, deploy (web_app deployment) + +## Output showing selective execution with --tags +```sh +$ ansible-playbook playbooks/provision.yml --tags "docker" + +PLAY [Provision web servers with common tools and Docker] ************************************************************************* + +TASK [Gathering Facts] ************************************************************************************************************ +ok: [damir-VirtualBox] + +TASK [common : Update apt cache] ************************************************************************************************** +ok: [damir-VirtualBox] + +TASK [common : Install essential packages] **************************************************************************************** +ok: [damir-VirtualBox] + +TASK [common : Log package installation completion] ******************************************************************************* +changed: [damir-VirtualBox] + +TASK [common : Ensure common user exists] ***************************************************************************************** +changed: [damir-VirtualBox] + +TASK [common : Log common role completion] **************************************************************************************** +changed: [damir-VirtualBox] + +TASK [docker : Include Docker installation tasks] ********************************************************************************* +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/install.yml for damir-VirtualBox + +TASK [docker : Remove old Docker packages (if any)] ******************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install required system packages] ********************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add Docker GPG key] ************************************************************************************************ +ok: [damir-VirtualBox] + +TASK [docker : Add Docker repository] ********************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install Docker packages] ******************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Include Docker configuration tasks] ******************************************************************************** +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/config.yml for damir-VirtualBox + +TASK [docker : Ensure Docker service is running and enabled] ********************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add user to docker group] ****************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Install Python Docker module for Ansible] ************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Verify Docker installation] **************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Display Docker version] ******************************************************************************************** +ok: [damir-VirtualBox] => { + "msg": "Docker installed: Docker version 29.2.1, build a5c7197" +} + +TASK [docker : Ensure Docker service is enabled and started] ********************************************************************** +ok: [damir-VirtualBox] + +PLAY RECAP ************************************************************************************************************************ +damir-VirtualBox : ok=19 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +$ ansible-playbook playbooks/provision.yml --skip-tags "common" + +PLAY [Provision web servers with common tools and Docker] ************************************************************************* + +TASK [Gathering Facts] ************************************************************************************************************ +ok: [damir-VirtualBox] + +TASK [docker : Include Docker installation tasks] ********************************************************************************* +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/install.yml for damir-VirtualBox + +TASK [docker : Remove old Docker packages (if any)] ******************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install required system packages] ********************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add Docker GPG key] ************************************************************************************************ +ok: [damir-VirtualBox] + +TASK [docker : Add Docker repository] ********************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install Docker packages] ******************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Include Docker configuration tasks] ******************************************************************************** +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/config.yml for damir-VirtualBox + +TASK [docker : Ensure Docker service is running and enabled] ********************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add user to docker group] ****************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Install Python Docker module for Ansible] ************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Verify Docker installation] **************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Display Docker version] ******************************************************************************************** +ok: [damir-VirtualBox] => { + "msg": "Docker installed: Docker version 29.2.1, build a5c7197" +} + +TASK [docker : Ensure Docker service is enabled and started] ********************************************************************** +ok: [damir-VirtualBox] + +PLAY RECAP ************************************************************************************************************************ +damir-VirtualBox : ok=14 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +$ ansible-playbook playbooks/provision.yml --tags "packages" + +PLAY [Provision web servers with common tools and Docker] ************************************************************************* + +TASK [Gathering Facts] ************************************************************************************************************ +ok: [damir-VirtualBox] + +TASK [common : Update apt cache] ************************************************************************************************** +ok: [damir-VirtualBox] + +TASK [common : Install essential packages] **************************************************************************************** +ok: [damir-VirtualBox] + +TASK [common : Log package installation completion] ******************************************************************************* +changed: [damir-VirtualBox] + +TASK [common : Ensure common user exists] ***************************************************************************************** +ok: [damir-VirtualBox] + +TASK [common : Log common role completion] **************************************************************************************** +changed: [damir-VirtualBox] + +PLAY RECAP ************************************************************************************************************************ +damir-VirtualBox : ok=6 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +$ ansible-playbook playbooks/provision.yml --tags "docker" --check + +PLAY [Provision web servers with common tools and Docker] ************************************************************************* + +TASK [Gathering Facts] ************************************************************************************************************ +ok: [damir-VirtualBox] + +TASK [common : Update apt cache] ************************************************************************************************** +ok: [damir-VirtualBox] + +TASK [common : Install essential packages] **************************************************************************************** +ok: [damir-VirtualBox] + +TASK [common : Log package installation completion] ******************************************************************************* +changed: [damir-VirtualBox] + +TASK [common : Ensure common user exists] ***************************************************************************************** +ok: [damir-VirtualBox] + +TASK [common : Log common role completion] **************************************************************************************** +changed: [damir-VirtualBox] + +TASK [docker : Include Docker installation tasks] ********************************************************************************* +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/install.yml for damir-VirtualBox + +TASK [docker : Remove old Docker packages (if any)] ******************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install required system packages] ********************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add Docker GPG key] ************************************************************************************************ +ok: [damir-VirtualBox] + +TASK [docker : Add Docker repository] ********************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install Docker packages] ******************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Include Docker configuration tasks] ******************************************************************************** +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/config.yml for damir-VirtualBox + +TASK [docker : Ensure Docker service is running and enabled] ********************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add user to docker group] ****************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Install Python Docker module for Ansible] ************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Verify Docker installation] **************************************************************************************** +skipping: [damir-VirtualBox] + +TASK [docker : Display Docker version] ******************************************************************************************** +skipping: [damir-VirtualBox] + +TASK [docker : Ensure Docker service is enabled and started] ********************************************************************** +ok: [damir-VirtualBox] + +PLAY RECAP ************************************************************************************************************************ +damir-VirtualBox : ok=17 changed=2 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 +``` + +## Output showing error handling with rescue block triggered +Here when retriving GPG key rescue block activates: +``` +$ ansible-playbook playbooks/provision.yml --tags "docker_install" + +PLAY [Provision web servers with common tools and Docker] ************************************************************************* + +TASK [Gathering Facts] ************************************************************************************************************ +ok: [damir-VirtualBox] + +TASK [common : Update apt cache] ************************************************************************************************** +ok: [damir-VirtualBox] + +TASK [common : Install essential packages] **************************************************************************************** +ok: [damir-VirtualBox] + +TASK [common : Log package installation completion] ******************************************************************************* +changed: [damir-VirtualBox] + +TASK [common : Ensure common user exists] ***************************************************************************************** +ok: [damir-VirtualBox] + +TASK [common : Log common role completion] **************************************************************************************** +changed: [damir-VirtualBox] + +TASK [docker : Include Docker installation tasks] ********************************************************************************* +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/install.yml for damir-VirtualBox + +TASK [docker : Remove old Docker packages (if any)] ******************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install required system packages] ********************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add Docker GPG key] ************************************************************************************************ +fatal: [damir-VirtualBox]: FAILED! => {"changed": false, "msg": "Failed to download key at https://download.docker.com/linux/ubuntu/gpg: Connection failure: Remote end closed connection without response"} + +TASK [docker : Wait 10 seconds before retry] ************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Retry Docker installation] ***************************************************************************************** +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/install.yml for damir-VirtualBox + +TASK [docker : Remove old Docker packages (if any)] ******************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install required system packages] ********************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add Docker GPG key] ************************************************************************************************ +ok: [damir-VirtualBox] + +TASK [docker : Add Docker repository] ********************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install Docker packages] ******************************************************************************************* +ok: [damir-VirtualBox] + +PLAY RECAP ************************************************************************************************************************ +damir-VirtualBox : ok=16 changed=2 unreachable=0 failed=0 skipped=0 rescued=1 ignored=0 +``` + +## List of all available tags (--list-tags output) +```sh +$ ansible-playbook playbooks/provision.yml --list-tags + +playbook: playbooks/provision.yml + + play #1 (webservers): Provision web servers with common tools and Docker TAGS: [] + TASK TAGS: [always, common, docker, docker_config, docker_install, packages, users] +``` + + + + + +# 2. Docker Compose Migration +## Template Structure +``` +--- + +services: + {{ app_name }}: + image: "{{ dockerhub_username }}/{{ app_name }}:latest" + container_name: "{{ app_name }}-compose" + ports: + - "{{ app_port }}:{{ app_internal_port | default(app_port) }}" + environment: + APP_NAME: "{{ app_name }}" + APP_PORT: "{{ app_internal_port | default(app_port) }}" + ENVIRONMENT: "{{ app_environment.ENVIRONMENT | default('production') }}" + + restart: "{{ restart_policy | default('unless-stopped') }}" + networks: + - app_network + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +networks: + app_network: + driver: bridge +``` + +## Role Dependencies +Defined in roles/web_app/meta/main.yml +This ensures Docker is installed before any container deployment – Ansible automatically executes the docker role first. + +Before/After Comparison +Aspect docker run (previous) Docker Compose (new) +Definition Imperative commands Declarative YAML +Multi‑container Manual linking Built‑in networks, dependencies +Idempotency Custom checks docker_compose module handles it +Updates Stop/remove/run cycle docker compose up with changed config +Environment Passed via env parameter Managed in the compose file +Networks Default bridge Custom network app_network + +## Output showing Docker Compose deployment success +```sh +$ ansible-playbook playbooks/deploy.yml --ask-vault-pass +Vault password: + +PLAY [Deploy application to web servers] ****************************************************************************************** + +TASK [Gathering Facts] ************************************************************************************************************ +[WARNING]: Platform linux on host damir-VirtualBox is using the discovered Python interpreter at /usr/bin/python3.10, but future +installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible- +core/2.17/reference_appendices/interpreter_discovery.html for more information. +ok: [damir-VirtualBox] + +TASK [Display deployment information] ********************************************************************************************* +ok: [damir-VirtualBox] => { + "msg": [ + "Starting deployment of devops-info-service", + "Image: damirsadykov/devops-info-service:latest", + "Target host: damir-VirtualBox" + ] +} + +TASK [docker : Include Docker installation tasks] ********************************************************************************* +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/install.yml for damir-VirtualBox + +TASK [docker : Remove old Docker packages (if any)] ******************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install required system packages] ********************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add Docker GPG key] ************************************************************************************************ +ok: [damir-VirtualBox] + +TASK [docker : Add Docker repository] ********************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install Docker packages] ******************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Include Docker configuration tasks] ******************************************************************************** +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/config.yml for damir-VirtualBox + +TASK [docker : Ensure Docker service is running and enabled] ********************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add user to docker group] ****************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Install Python Docker module for Ansible] ************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Verify Docker installation] **************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Display Docker version] ******************************************************************************************** +ok: [damir-VirtualBox] => { + "msg": "Docker installed: Docker version 29.3.0, build 5927d80" +} + +TASK [docker : Ensure Docker service is enabled and started] ********************************************************************** +ok: [damir-VirtualBox] + +TASK [web_app : Ensure docker-compose Python library is installed (for older modules)] ******************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Create application directory] ************************************************************************************* +ok: [damir-VirtualBox] + +TASK [web_app : Template docker-compose.yml] ************************************************************************************** +changed: [damir-VirtualBox] + +TASK [web_app : Log in to Docker Hub (if credentials provided)] ******************************************************************* +changed: [damir-VirtualBox] + +TASK [web_app : Deploy with Docker Compose] *************************************************************************************** +changed: [damir-VirtualBox] + +TASK [web_app : Wait for application to be ready] ********************************************************************************* +ok: [damir-VirtualBox] + +TASK [web_app : Check health endpoint] ******************************************************************************************** +ok: [damir-VirtualBox] + +TASK [web_app : Show deployment status] ******************************************************************************************* +ok: [damir-VirtualBox] => { + "msg": [ + "Application deployed with Docker Compose", + "Project directory: /opt/devops-info-service", + "Container status: run 'docker ps' on target" + ] +} + +TASK [Verify deployment] ********************************************************************************************************** +ok: [damir-VirtualBox] + +TASK [Show deployment status] ***************************************************************************************************** +ok: [damir-VirtualBox] => { + "msg": [ + "Deployment completed successfully!", + "Container status: exited", + "Container started: 2026-03-05T19:37:33.021512188Z" + ] +} + +PLAY RECAP ************************************************************************************************************************ +damir-VirtualBox : ok=24 changed=3 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 +``` + +## Idempotency proof (second run shows "ok" not "changed") +```sh +$ ansible-playbook playbooks/deploy.yml --ask-vault-pass +Vault password: + +PLAY [Deploy application to web servers] ****************************************************************************************** + +TASK [Gathering Facts] ************************************************************************************************************ +[WARNING]: Platform linux on host damir-VirtualBox is using the discovered Python interpreter at /usr/bin/python3.10, but future +installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible- +core/2.17/reference_appendices/interpreter_discovery.html for more information. +ok: [damir-VirtualBox] + +TASK [Display deployment information] ********************************************************************************************* +ok: [damir-VirtualBox] => { + "msg": [ + "Starting deployment of devops-info-service", + "Image: damirsadykov/devops-info-service:latest", + "Target host: damir-VirtualBox" + ] +} + +TASK [docker : Include Docker installation tasks] ********************************************************************************* +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/install.yml for damir-VirtualBox + +TASK [docker : Remove old Docker packages (if any)] ******************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install required system packages] ********************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add Docker GPG key] ************************************************************************************************ +ok: [damir-VirtualBox] + +TASK [docker : Add Docker repository] ********************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install Docker packages] ******************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Include Docker configuration tasks] ******************************************************************************** +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/config.yml for damir-VirtualBox + +TASK [docker : Ensure Docker service is running and enabled] ********************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add user to docker group] ****************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Install Python Docker module for Ansible] ************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Verify Docker installation] **************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Display Docker version] ******************************************************************************************** +ok: [damir-VirtualBox] => { + "msg": "Docker installed: Docker version 29.3.0, build 5927d80" +} + +TASK [docker : Ensure Docker service is enabled and started] ********************************************************************** +ok: [damir-VirtualBox] + +TASK [web_app : Ensure docker-compose Python library is installed (for older modules)] ******************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Create application directory] ************************************************************************************* +ok: [damir-VirtualBox] + +TASK [web_app : Template docker-compose.yml] ************************************************************************************** +ok: [damir-VirtualBox] + +TASK [web_app : Log in to Docker Hub (if credentials provided)] ******************************************************************* +changed: [damir-VirtualBox] + +TASK [web_app : Deploy with Docker Compose] *************************************************************************************** +ok: [damir-VirtualBox] + +TASK [web_app : Wait for application to be ready] ********************************************************************************* +skipping: [damir-VirtualBox] + +TASK [web_app : Check health endpoint] ******************************************************************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Show deployment status] ******************************************************************************************* +ok: [damir-VirtualBox] => { + "msg": [ + "Application deployed with Docker Compose", + "Project directory: /opt/devops-info-service", + "Container status: run 'docker ps' on target" + ] +} + +TASK [Verify deployment] ********************************************************************************************************** +ok: [damir-VirtualBox] + +TASK [Show deployment status] ***************************************************************************************************** +ok: [damir-VirtualBox] => { + "msg": [ + "Deployment completed successfully!", + "Container status: exited", + "Container started: 2026-03-05T19:37:33.021512188Z" + ] +} + +PLAY RECAP ************************************************************************************************************************ +damir-VirtualBox : ok=22 changed=1 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 +``` + +## Application running and accessible +```sh +$ curl http://192.168.1.8:5000/ +{"endpoints":[{"description":"Service information","method":"GET","path":"/"},{"description":"Health check","method":"GET","path":"/health"}],"request":{"client_ip":"192.168.1.9","method":"GET","path":"/","user_agent":"curl/7.81.0"},"runtime":{"current_time":"2026-03-05T20:26:24.910321Z","timezone":"UTC","uptime_human":"0 hours, 2 minutes","uptime_seconds":159},"service":{"description":"DevOps course info service","framework":"Flask","name":"devops-info-service","version":"1.0.0"},"system":{"architecture":"x86_64","cpu_count":1,"hostname":"3e63ccceb7b4","platform":"Linux","platform_version":"#100~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Mon Jan 19 17:10:19 UTC ","python_version":"3.12.12"}} +``` + +## Contents of templated docker-compose.yml +``` sh +$ ssh damir@192.168.1.8 cat /opt/devops-info-service/docker-compose.yml +--- + +services: + devops-info-service: + image: "damirsadykov/devops-info-service:latest" + container_name: "devops-info-service-compose" + ports: + - "5000:5000" + environment: + APP_NAME: "devops-info-service" + APP_PORT: "5000" + ENVIRONMENT: "production" + + restart: "unless-stopped" + networks: + - app_network + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +networks: + app_network: + driver: bridge +``` + + +# 3. Wipe Logic + +## Implementation Details +Controlled by variable web_app_wipe (default false in defaults/main.yml). + +Tasks are placed in a separate file wipe.yml and included at the beginning of main.yml. + +Tag web_app_wipe allows selective execution. + +## Variable + Tag Double Safety +Variable prevents accidental wipe even if the tag is supplied. + +Tag allows limiting execution to wipe tasks only (without deploying). + +This ensures that wipe can only happen when explicitly requested. + +## Output of Scenario 1 showing normal deployment (wipe skipped) +```sh +$ ansible-playbook playbooks/deploy.yml --ask-vault-pass +Vault password: + +PLAY [Deploy application to web servers] ****************************************************************************************** + +TASK [Gathering Facts] ************************************************************************************************************ +[WARNING]: Platform linux on host damir-VirtualBox is using the discovered Python interpreter at /usr/bin/python3.10, but future +installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible- +core/2.17/reference_appendices/interpreter_discovery.html for more information. +ok: [damir-VirtualBox] + +TASK [Display deployment information] ********************************************************************************************* +ok: [damir-VirtualBox] => { + "msg": [ + "Starting deployment of devops-info-service", + "Image: damirsadykov/devops-info-service:latest", + "Target host: damir-VirtualBox" + ] +} + +TASK [docker : Include Docker installation tasks] ********************************************************************************* +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/install.yml for damir-VirtualBox + +TASK [docker : Remove old Docker packages (if any)] ******************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install required system packages] ********************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add Docker GPG key] ************************************************************************************************ +ok: [damir-VirtualBox] + +TASK [docker : Add Docker repository] ********************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install Docker packages] ******************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Include Docker configuration tasks] ******************************************************************************** +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/config.yml for damir-VirtualBox + +TASK [docker : Ensure Docker service is running and enabled] ********************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add user to docker group] ****************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Install Python Docker module for Ansible] ************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Verify Docker installation] **************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Display Docker version] ******************************************************************************************** +ok: [damir-VirtualBox] => { + "msg": "Docker installed: Docker version 29.3.0, build 5927d80" +} + +TASK [docker : Ensure Docker service is enabled and started] ********************************************************************** +ok: [damir-VirtualBox] + +TASK [web_app : Include wipe tasks] *********************************************************************************************** +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for damir-VirtualBox + +TASK [web_app : Stop and remove containers with Docker Compose] ******************************************************************* +skipping: [damir-VirtualBox] + +TASK [web_app : Remove docker-compose.yml file] *********************************************************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Remove application directory] ************************************************************************************* +skipping: [damir-VirtualBox] + +TASK [web_app : (Optional) Remove Docker images] ********************************************************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Log wipe completion] ********************************************************************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Ensure docker-compose Python library is installed (for older modules)] ******************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Create application directory] ************************************************************************************* +ok: [damir-VirtualBox] + +TASK [web_app : Template docker-compose.yml] ************************************************************************************** +ok: [damir-VirtualBox] + +TASK [web_app : Log in to Docker Hub (if credentials provided)] ******************************************************************* +changed: [damir-VirtualBox] + +TASK [web_app : Deploy with Docker Compose] *************************************************************************************** +ok: [damir-VirtualBox] + +TASK [web_app : Wait for application to be ready] ********************************************************************************* +skipping: [damir-VirtualBox] + +TASK [web_app : Check health endpoint] ******************************************************************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Show deployment status] ******************************************************************************************* +ok: [damir-VirtualBox] => { + "msg": [ + "Application deployed with Docker Compose", + "Project directory: /opt/devops-info-service", + "Container status: run 'docker ps' on target" + ] +} + +TASK [Verify deployment] ********************************************************************************************************** +ok: [damir-VirtualBox] + +TASK [Show deployment status] ***************************************************************************************************** +ok: [damir-VirtualBox] => { + "msg": [ + "Deployment completed successfully!", + "Container status: exited", + "Container started: 2026-03-05T19:37:33.021512188Z" + ] +} + +PLAY RECAP ************************************************************************************************************************ +damir-VirtualBox : ok=23 changed=1 unreachable=0 failed=0 skipped=8 rescued=0 ignored=0 + +$ ssh damir@192.168.1.8 "docker ps" +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +3e63ccceb7b4 damirsadykov/devops-info-service:latest "python app.py" 9 minutes ago Up 9 minutes 0.0.0.0:5000->5000/tcp, [::]:5000->5000/tcp devops-info-service-compose +``` + +## Output of Scenario 2 showing wipe-only operation +``` sh +$ ansible-playbook playbooks/deploy.yml --ask-vault-pass -e "web_app_wipe=true" --tags web_app_wipe +Vault password: + +PLAY [Deploy application to web servers] ****************************************************************************************** + +TASK [Gathering Facts] ************************************************************************************************************ +[WARNING]: Platform linux on host damir-VirtualBox is using the discovered Python interpreter at /usr/bin/python3.10, but future +installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible- +core/2.17/reference_appendices/interpreter_discovery.html for more information. +ok: [damir-VirtualBox] + +TASK [web_app : Include wipe tasks] *********************************************************************************************** +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for damir-VirtualBox + +TASK [web_app : Stop and remove containers with Docker Compose] ******************************************************************* +changed: [damir-VirtualBox] + +TASK [web_app : Remove docker-compose.yml file] *********************************************************************************** +changed: [damir-VirtualBox] + +TASK [web_app : Remove application directory] ************************************************************************************* +changed: [damir-VirtualBox] + +TASK [web_app : (Optional) Remove Docker images] ********************************************************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Log wipe completion] ********************************************************************************************** +ok: [damir-VirtualBox] => { + "msg": "Application devops-info-service wiped successfully from /opt/devops-info-service" +} + +TASK [web_app : Ensure docker-compose Python library is installed (for older modules)] ******************************************** +skipping: [damir-VirtualBox] + +PLAY RECAP ************************************************************************************************************************ +damir-VirtualBox : ok=6 changed=3 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0 + +damir@damir-VB:~/Desktop/DevOps/DevOps-Core-Course/ansible$ ssh damir@192.168.1.8 "docker ps" +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +damir@damir-VB:~/Desktop/DevOps/DevOps-Core-Course/ansible$ ssh damir@192.168.1.8 "ls /opt" +containerd +``` + +## Output of Scenario 3 showing clean reinstall (wipe → deploy) +``` sh +$ ansible-playbook playbooks/deploy.yml --ask-vault-pass -e "web_app_wipe=true" +Vault password: + +PLAY [Deploy application to web servers] ****************************************************************************************** + +TASK [Gathering Facts] ************************************************************************************************************ +[WARNING]: Platform linux on host damir-VirtualBox is using the discovered Python interpreter at /usr/bin/python3.10, but future +installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible- +core/2.17/reference_appendices/interpreter_discovery.html for more information. +ok: [damir-VirtualBox] + +TASK [Display deployment information] ********************************************************************************************* +ok: [damir-VirtualBox] => { + "msg": [ + "Starting deployment of devops-info-service", + "Image: damirsadykov/devops-info-service:latest", + "Target host: damir-VirtualBox" + ] +} + +TASK [docker : Include Docker installation tasks] ********************************************************************************* +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/install.yml for damir-VirtualBox + +TASK [docker : Remove old Docker packages (if any)] ******************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install required system packages] ********************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add Docker GPG key] ************************************************************************************************ +ok: [damir-VirtualBox] + +TASK [docker : Add Docker repository] ********************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Install Docker packages] ******************************************************************************************* +ok: [damir-VirtualBox] + +TASK [docker : Include Docker configuration tasks] ******************************************************************************** +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/docker/tasks/config.yml for damir-VirtualBox + +TASK [docker : Ensure Docker service is running and enabled] ********************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Add user to docker group] ****************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Install Python Docker module for Ansible] ************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Verify Docker installation] **************************************************************************************** +ok: [damir-VirtualBox] + +TASK [docker : Display Docker version] ******************************************************************************************** +ok: [damir-VirtualBox] => { + "msg": "Docker installed: Docker version 29.3.0, build 5927d80" +} + +TASK [docker : Ensure Docker service is enabled and started] ********************************************************************** +ok: [damir-VirtualBox] + +TASK [web_app : Include wipe tasks] *********************************************************************************************** +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for damir-VirtualBox + +TASK [web_app : Stop and remove containers with Docker Compose] ******************************************************************* +fatal: [damir-VirtualBox]: FAILED! => {"changed": false, "msg": "\"/opt/devops-info-service\" is not a directory"} +...ignoring + +TASK [web_app : Remove docker-compose.yml file] *********************************************************************************** +ok: [damir-VirtualBox] + +TASK [web_app : Remove application directory] ************************************************************************************* +ok: [damir-VirtualBox] + +TASK [web_app : (Optional) Remove Docker images] ********************************************************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Log wipe completion] ********************************************************************************************** +ok: [damir-VirtualBox] => { + "msg": "Application devops-info-service wiped successfully from /opt/devops-info-service" +} + +TASK [web_app : Ensure docker-compose Python library is installed (for older modules)] ******************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Create application directory] ************************************************************************************* +changed: [damir-VirtualBox] + +TASK [web_app : Template docker-compose.yml] ************************************************************************************** +changed: [damir-VirtualBox] + +TASK [web_app : Log in to Docker Hub (if credentials provided)] ******************************************************************* +changed: [damir-VirtualBox] + +TASK [web_app : Deploy with Docker Compose] *************************************************************************************** +changed: [damir-VirtualBox] + +TASK [web_app : Wait for application to be ready] ********************************************************************************* +ok: [damir-VirtualBox] + +TASK [web_app : Check health endpoint] ******************************************************************************************** +ok: [damir-VirtualBox] + +TASK [web_app : Show deployment status] ******************************************************************************************* +ok: [damir-VirtualBox] => { + "msg": [ + "Application deployed with Docker Compose", + "Project directory: /opt/devops-info-service", + "Container status: run 'docker ps' on target" + ] +} + +TASK [Verify deployment] ********************************************************************************************************** +ok: [damir-VirtualBox] + +TASK [Show deployment status] ***************************************************************************************************** +ok: [damir-VirtualBox] => { + "msg": [ + "Deployment completed successfully!", + "Container status: exited", + "Container started: 2026-03-05T19:37:33.021512188Z" + ] +} + +PLAY RECAP ************************************************************************************************************************ +damir-VirtualBox : ok=29 changed=4 unreachable=0 failed=0 skipped=2 rescued=0 ignored=1 + +damir@damir-VB:~/Desktop/DevOps/DevOps-Core-Course/ansible$ ssh damir@192.168.1.8 "docker ps" +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +d93503af48c6 damirsadykov/devops-info-service:latest "python app.py" 35 seconds ago Up 34 seconds 0.0.0.0:5000->5000/tcp, [::]:5000->5000/tcp devops-info-service-compose +``` + +## Output of Scenario 4a showing wipe blocked by when condition +```sh + ansible-playbook playbooks/deploy.yml --ask-vault-pass --tags web_app_wipe +Vault password: + +PLAY [Deploy application to web servers] ****************************************************************************************** + +TASK [Gathering Facts] ************************************************************************************************************ +[WARNING]: Platform linux on host damir-VirtualBox is using the discovered Python interpreter at /usr/bin/python3.10, but future +installation of another Python interpreter could change the meaning of that path. See https://docs.ansible.com/ansible- +core/2.17/reference_appendices/interpreter_discovery.html for more information. +ok: [damir-VirtualBox] + +TASK [web_app : Include wipe tasks] *********************************************************************************************** +included: /home/damir/Desktop/DevOps/DevOps-Core-Course/ansible/roles/web_app/tasks/wipe.yml for damir-VirtualBox + +TASK [web_app : Stop and remove containers with Docker Compose] ******************************************************************* +skipping: [damir-VirtualBox] + +TASK [web_app : Remove docker-compose.yml file] *********************************************************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Remove application directory] ************************************************************************************* +skipping: [damir-VirtualBox] + +TASK [web_app : (Optional) Remove Docker images] ********************************************************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Log wipe completion] ********************************************************************************************** +skipping: [damir-VirtualBox] + +TASK [web_app : Ensure docker-compose Python library is installed (for older modules)] ******************************************** +skipping: [damir-VirtualBox] + +PLAY RECAP ************************************************************************************************************************ +damir-VirtualBox : ok=2 changed=0 unreachable=0 failed=0 skipped=6 rescued=0 ignored=0 +``` + +# 4. CI/CD Integration - skipped + +# Research Answers +## Q: What happens if rescue block also fails? +If the rescue block itself fails, Ansible stops execution on that host and marks the task as failed (unless ignore_errors is set). The always block still runs afterwards. + +## Q: Can you have nested blocks? +Yes, blocks can be nested. Inner blocks can have their own rescue/always, and errors propagate to outer blocks. + +## Q: How do tags inherit to tasks within blocks? +Tags applied to a block are inherited by all tasks inside that block. Tags can also be overridden at task level. + +## Q: What's the difference between restart: always and restart: unless-stopped? + always – container restarts regardless of exit status, even if manually stopped. + + unless-stopped – restarts unless the container was explicitly stopped. Better for production because manual stops are honoured. + +## Q: How do Docker Compose networks differ from Docker bridge networks? + +Compose automatically creates a dedicated bridge network for the project, providing service discovery via container names. It isolates containers from other projects. +## Q: Can you reference Ansible Vault variables in the template? + +Yes, as long as the playbook has access to the vault password, variables like {{ app_secret_key }} are decrypted and can be used in templates. +## Q: Why use both variable AND tag? (Double safety mechanism) + + Variable ensures wipe cannot happen accidentally even if the tag is present. + + Tag allows limiting execution to wipe tasks only (no deployment). + Together they give fine‑grained control and prevent mistakes. + +## Q: What's the difference between never tag and this approach? + +The never tag permanently excludes tasks from running unless explicitly requested. Our approach uses a variable condition, which is more flexible (e.g., can be overridden per run with -e). +## Q: Why must wipe logic come BEFORE deployment in main.yml? + +To support clean reinstallation – remove the old application before deploying the new one, ensuring a fresh state. +## Q: When would you want clean reinstallation vs. rolling update? + + Clean reinstall – when application state is broken or you need a guaranteed fresh start (e.g., schema changes). + + Rolling update – for zero‑downtime updates; not implemented here but could be done with Compose. + + +## Q: How would you extend this to wipe Docker images and volumes too? + +Add tasks in wipe.yml: +yaml + +- name: Remove Docker images + docker_image: + name: "{{ dockerhub_username }}/{{ app_name }}:{{ docker_tag }}" + state: absent +- name: Remove named volumes + docker_volume: + name: "{{ item }}" + state: absent + loop: "{{ volumes_to_remove }}" \ No newline at end of file diff --git a/ansible/docs/screenshots/lab6_task3.png b/ansible/docs/screenshots/lab6_task3.png new file mode 100644 index 0000000000..eb2f8503c6 Binary files /dev/null and b/ansible/docs/screenshots/lab6_task3.png differ diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml new file mode 100644 index 0000000000..a8df275b15 --- /dev/null +++ b/ansible/group_vars/all.yml @@ -0,0 +1,55 @@ +$ANSIBLE_VAULT;1.1;AES256 +66363163383865346639333733303737653036373230636663373139643332653439633861653532 +3566366563323736633532363331656565333361323933390a613136396236353364303464663264 +37376632393134633835363263326638393561323733343937363830356165336533363863333064 +6435653261613932390a613366393634363232386435633834396164653266616131636262313333 +33323930323437666161366332336637383731666634323562643831616139336661626138333832 +32383161646635333330366563393139656132643133326539646462313764346466323836396264 +38656565383630653466306435646637363131663964373862613265386431666439633935303533 +66386635303139326363393735373265306437326236306233626635333431666462346461316363 +64383633626539373335626364613966613035326238363830383739343366313138303139373262 +36353036373537636635363835633266663437663434633566373836663335363031386463313261 +36313930336636656163656533353964643965633138383462346564363038313162663534323039 +36653065636161653533393631306134346364383138613939353461383636336465323632663036 +61666136643835333637613735346139303264383536613430333233376364323765316133323432 +31316132623537636234303031663830616632306233303035613534323965633366623966333164 +34353536393635363037373237393239633539653439326637666138656237363263353734336431 +38346539376364623132313530613339663135303630346461383333653838316336336237623366 +32613162353761353566663039626163636263343864356162376531353630356437646135643037 +66653631303665303261636233633232646133363533636463636566303935656431616665613538 +31363365353663643039613535383966666564636336386564313130393334326637363034666530 +30386130633965363830313233666438613366313636343531376230626462623231643430346363 +66643863396362623763623736303230343730653263633432663033653863313833613963616465 +37376235626635353364383736613163323638643661376630643230616238323639316266366163 +32326436313932313531613639356166356634303935353739306332623063333933316530323435 +61313738363530323735346565306636306230306564333138353964393231386239303563323662 +63386631636231626134393734666632636533316435363636366237326239343739373637623661 +32656566663830636566636231313030323331373562376263313933323962626539666362366437 +66626533353964356132393764316334376263373666386264373866303938656634343339366664 +63633537626665366161663964323832333531383862303463666230343034643661303532303762 +30626537636430363162316230386534306138396238393135353634643631623361376364323865 +65393462383265306165633861613965663432303661636536326534303130623339393932636330 +35636238323432616330623966356437356237653665353232386231393765316263353264356264 +35386439636462316163353962626633616533363330333539373837376531386534386230646537 +35366230336632613332356334626633376563373738383339613136366236643531323738373665 +34666530343462613665633636623761646264623035353332653536323737376136303262623132 +65306663313964303634323737663061303538636331326231303331383166343035336633373064 +30643666633666623234633537313338303939303333626637393037636638393861353334366534 +39366539613362623036636633333932363938323539663662386366633363366263316366613033 +66653337613139323562303435633034343962306132663631623663653039666230383437343138 +31306131326266316665613266636432613737396238653136326435343564366666383738313435 +64663238633265363636386130373139306464346231636135646135623733613035363635646165 +38633633656530626466333566303866636535393435303938373932353864353433643338353061 +64303037306237613064333164333861653638333030383965653533343931346133633735643062 +63363137653564353836333231303565326431616563303463643062653331656363386564656563 +30623932666235343364376363353561356330323833656639623364346431663236343031646661 +62633937303661386139666465363034616563646633613330323937396239373930646463353763 +37393830363832383536653665353934373163373762363432363064653862323238363434373438 +64393461333962303066666532363937653330356166353332303536346231623234386465643765 +34373666316633626335383335656535656235326366393265353332623062653836306162623363 +36386661383963396339613437376435633362333930373731316237376563393233656334613630 +39646537636665343565343066623434343634613062323164346335613930623365623537306638 +61303364363731306138663434393366336639376266336661343537353665323863663234663763 +61383237386637323831343838306337323531643830633364303862633434656132383235373639 +37326662376230303030646331663734326636663363616164626461303563656436353433373962 +3361353964383637616264356164666561623934393666383139 diff --git a/ansible/group_vars/all.yml.backup b/ansible/group_vars/all.yml.backup new file mode 100644 index 0000000000..96de23f27f --- /dev/null +++ b/ansible/group_vars/all.yml.backup @@ -0,0 +1,24 @@ +$ANSIBLE_VAULT;1.1;AES256 +37363538613332346638663330653662623065303465383039323431613066323366376465393965 +3338613132343965316433313431346233343935383430300a346130633437633365343664353132 +39353134616263626561356135356133313834343936386664646233306639366463303166303630 +6435343166373463370a623635656434353730356366373738643564616432663065653237353334 +62393931313930363734666133656463666535306435633765373764303131373462643961633733 +36653863633037663766656661316362633965326632303037623634383535336261393463666231 +35646431386239353037306535303663323161386536303663386230313165303239636466373264 +33343638616264666438376665313839646335633231633532623733306132613765386263363439 +39363337643531393634666131623163666432663631363734303333373032653663383666623035 +32363138396432613661313630383763303231383233656335656335613163336566646364336239 +63333034346632336561633263663136383539666562396635633434623966396166313063393038 +31666265633161613537353430333133663732363237636164613765386564663433346539636135 +37396237616537623565393566636134396138616538333065656164663166306338633865633464 +39333538623033643934373163643330356132383361343934323066303735373961653637663865 +31613230623435316330393531646538333162353363656363376132306261346163306430353432 +38303737316666333731373065383030633731326638343932653938333131336464353530313765 +62653065336138656235323932643831643238306130353731666233366530333361313737353233 +65343932353839343665663238376163373233623931343036346163613632353637376237396663 +38303266646665626231646463313530363466393335333565643934363833633463303238653764 +39613761633234363133383236303039623635386261346266373663313064373962306630313564 +35383932666139373336333634396365303364653339613938306238636139386337653331326266 +31356134333662306538323465663261356433616433343933633439663663623238363363333065 +396137666236663538343338633061376438 diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..b58dda11a5 --- /dev/null +++ b/ansible/inventory/hosts.ini @@ -0,0 +1,2 @@ +[webservers] +damir-VirtualBox ansible_host=192.168.1.8 ansible_user=damir \ No newline at end of file diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml new file mode 100644 index 0000000000..6fde8d4848 --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,32 @@ +--- +- name: Deploy application to web servers + hosts: webservers + become: yes + gather_facts: yes + + vars_files: + - ../group_vars/all.yml + + pre_tasks: + - name: Display deployment information + debug: + msg: + - "Starting deployment of {{ app_name }}" + - "Image: {{ dockerhub_username }}/{{ app_name }}:{{ docker_image_tag }}" + - "Target host: {{ inventory_hostname }}" + + roles: + - role: web_app + + post_tasks: + - name: Verify deployment + docker_container_info: + name: "{{ app_container_name }}" + register: final_container + + - name: Show deployment status + debug: + msg: + - "Deployment completed successfully!" + - "Container status: {{ final_container.container.State.Status }}" + - "Container started: {{ final_container.container.State.StartedAt }}" \ No newline at end of file diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml new file mode 100644 index 0000000000..9f90fd31e6 --- /dev/null +++ b/ansible/playbooks/provision.yml @@ -0,0 +1,14 @@ +--- +- name: Provision web servers with common tools and Docker + hosts: webservers + become: yes + gather_facts: yes + + roles: + - role: common + common_timezone: "Europe/Moscow" + tags: [common, always] + + - role: docker + docker_user: "damir" + tags: [docker] \ No newline at end of file diff --git a/ansible/playbooks/site.yml b/ansible/playbooks/site.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml new file mode 100644 index 0000000000..f02a3597be --- /dev/null +++ b/ansible/roles/common/defaults/main.yml @@ -0,0 +1,25 @@ +--- +# Default packages to install +common_packages: + - python3-pip + - python3-venv + - curl + - wget + - git + - vim + - htop + - net-tools + - software-properties-common + - apt-transport-https + - ca-certificates + - gnupg + - lsb-release + - unzip + - tree + - tmux + +# Default timezone +common_timezone: "UTC" + +# User to create (for demonstration) +common_user_name: appuser \ No newline at end of file diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000000..63d12aae97 --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,55 @@ +--- +- name: Common role tasks + block: + - name: Package installation block + block: + - name: Update apt cache + apt: + update_cache: yes + cache_valid_time: 3600 + register: apt_update_result + + - name: Install essential packages + apt: + name: "{{ common_packages }}" + state: present + + rescue: + - name: Fix apt cache on failure + command: apt-get update --fix-missing + + - name: Retry package installation + apt: + name: "{{ common_packages }}" + state: present + + always: + - name: Log package installation completion + copy: + content: "Common packages installed at {{ ansible_date_time.iso8601 }}" + dest: /tmp/common_packages_completed.log + + tags: + - packages + become: yes + + - name: User creation block + block: + - name: Ensure common user exists + user: + name: "{{ common_user_name }}" + state: present + shell: /bin/bash + create_home: yes + tags: + - users + become: yes + + always: + - name: Log common role completion + copy: + content: "Common role completed at {{ ansible_date_time.iso8601 }}" + dest: /tmp/common_role_completed.log + become: yes + + tags: common \ No newline at end of file diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..fc0b160967 --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -0,0 +1,19 @@ +--- +# Docker version constraints (use 'latest' or specific version) +docker_version: "latest" +docker_compose_version: "latest" + +# User to add to docker group +docker_user: "{{ ansible_user }}" + +# Docker repository settings +docker_repo_url: "https://download.docker.com/linux/ubuntu" +docker_gpg_key: "https://download.docker.com/linux/ubuntu/gpg" + +# Docker packages to install +docker_packages: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin \ No newline at end of file diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..4199982a6d --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,7 @@ +--- +- name: restart docker + systemd: + name: docker + state: restarted + daemon_reload: yes + become: yes \ No newline at end of file diff --git a/ansible/roles/docker/tasks/config.yml b/ansible/roles/docker/tasks/config.yml new file mode 100644 index 0000000000..a3ffbbefec --- /dev/null +++ b/ansible/roles/docker/tasks/config.yml @@ -0,0 +1,30 @@ +--- +- name: Ensure Docker service is running and enabled + systemd: + name: docker + state: started + enabled: yes + +- name: Add user to docker group + user: + name: "{{ docker_user }}" + groups: docker + append: yes + notify: restart docker + +- name: Install Python Docker module for Ansible + pip: + name: + - docker + - docker-compose + state: present + +- name: Verify Docker installation + command: docker --version + register: docker_version_check + changed_when: false + +- name: Display Docker version + debug: + msg: "Docker installed: {{ docker_version_check.stdout }}" + when: docker_version_check.stdout is defined \ No newline at end of file diff --git a/ansible/roles/docker/tasks/install.yml b/ansible/roles/docker/tasks/install.yml new file mode 100644 index 0000000000..e54fc26d9c --- /dev/null +++ b/ansible/roles/docker/tasks/install.yml @@ -0,0 +1,40 @@ +--- +- name: Remove old Docker packages (if any) + apt: + name: + - docker + - docker-engine + - docker.io + - containerd + - runc + state: absent + +- name: Install required system packages + apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + state: present + update_cache: yes + +- name: Add Docker GPG key + apt_key: + url: "{{ docker_gpg_key }}" + state: present + +- name: Add Docker repository + apt_repository: + repo: "deb [arch=amd64] {{ docker_repo_url }} {{ ansible_distribution_release }} stable" + state: present + update_cache: yes + register: docker_repo_added + +- name: Install Docker packages + apt: + name: "{{ docker_packages }}" + state: "{{ 'latest' if docker_version == 'latest' else 'present' }}" + update_cache: yes + notify: restart docker \ No newline at end of file diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..2530bb3c89 --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,33 @@ +--- +- name: Docker role tasks + block: + - name: Docker installation block + block: + - name: Include Docker installation tasks + include_tasks: install.yml + rescue: + - name: Wait 10 seconds before retry + wait_for: + timeout: 10 + - name: Retry Docker installation + include_tasks: install.yml + tags: docker_install + become: yes + + - name: Docker configuration block + block: + - name: Include Docker configuration tasks + include_tasks: config.yml + tags: docker_config + become: yes + + always: + - name: Ensure Docker service is enabled and started + systemd: + name: docker + state: started + enabled: yes + become: yes + ignore_errors: yes # in case Docker is not installed + + tags: docker \ No newline at end of file diff --git a/ansible/roles/web_app/defaults/main.yml b/ansible/roles/web_app/defaults/main.yml new file mode 100644 index 0000000000..b61d5ede36 --- /dev/null +++ b/ansible/roles/web_app/defaults/main.yml @@ -0,0 +1,30 @@ +--- +# Application defaults +app_name: devops-info-service +app_port: "5000" +app_internal_port: "5000" +app_container_name: "{{ app_name }}" +docker_tag: latest +dockerhub_username: your_dockerhub_username + +# Container settings +docker_compose_version: "3.8" +compose_project_dir: "/opt/{{ app_name }}" +restart_policy: unless-stopped +container_state: started + +# Health check settings +health_check_retries: 30 +health_check_delay: 2 +health_check_endpoint: "/health" + +# Environment variables +app_environment: + APP_NAME: "{{ app_name }}" + APP_PORT: "{{ app_port }}" + ENVIRONMENT: production + +app_secret_key: "insecure-dev-key" + +# Wipe Logic Control +web_app_wipe: false \ No newline at end of file diff --git a/ansible/roles/web_app/handlers/main.yml b/ansible/roles/web_app/handlers/main.yml new file mode 100644 index 0000000000..1d3cbafe24 --- /dev/null +++ b/ansible/roles/web_app/handlers/main.yml @@ -0,0 +1,12 @@ +--- +- name: restart app container + docker_container: + name: "{{ app_container_name }}" + image: "{{ dockerhub_username }}/{{ app_name }}:{{ docker_image_tag }}" + state: started + restart: yes + restart_policy: "{{ restart_policy }}" + ports: + - "{{ app_port }}:{{ app_port }}" + env: "{{ app_environment }}" + become: no \ No newline at end of file diff --git a/ansible/roles/web_app/meta/main.yaml b/ansible/roles/web_app/meta/main.yaml new file mode 100644 index 0000000000..4715b20372 --- /dev/null +++ b/ansible/roles/web_app/meta/main.yaml @@ -0,0 +1,5 @@ +--- +dependencies: + - role: docker + # No need to pass vars; docker role uses its own defaults + # This ensures Docker is installed before we try to deploy containers. \ No newline at end of file diff --git a/ansible/roles/web_app/tasks/main.yml b/ansible/roles/web_app/tasks/main.yml new file mode 100644 index 0000000000..4cc47f4003 --- /dev/null +++ b/ansible/roles/web_app/tasks/main.yml @@ -0,0 +1,91 @@ +--- +# Wipe logic runs first (when explicitly requested) +- name: Include wipe tasks + include_tasks: wipe.yml + tags: + - web_app_wipe + +- name: Ensure docker-compose Python library is installed (for older modules) + pip: + name: docker-compose + state: present + become: yes + tags: always + when: false + +- name: Deploy application with Docker Compose + block: + - name: Create application directory + file: + path: "{{ compose_project_dir | default('/opt/' + app_name) }}" + state: directory + mode: '0755' + become: yes + + - name: Template docker-compose.yml + template: + src: docker-compose.yml.j2 + dest: "{{ compose_project_dir | default('/opt/' + app_name) }}/docker-compose.yml" + mode: '0644' + become: yes + register: compose_file + + - name: Log in to Docker Hub (if credentials provided) + docker_login: + username: "{{ dockerhub_username }}" + password: "{{ dockerhub_password }}" + reauthorize: yes + become: no + no_log: true + when: dockerhub_username is defined and dockerhub_password is defined + + - name: Deploy with Docker Compose + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir | default('/opt/' + app_name) }}" + state: present + pull: always + remove_orphans: yes + become: no + register: compose_result + + - name: Wait for application to be ready + wait_for: + port: "{{ app_port }}" + host: "{{ ansible_default_ipv4.address | default('127.0.0.1') }}" + delay: 5 + timeout: 60 + state: started + become: no + when: compose_result is changed + + - name: Check health endpoint + uri: + url: "http://{{ ansible_default_ipv4.address | default('127.0.0.1') }}:{{ app_port }}/health" + method: GET + status_code: 200 + timeout: 5 + register: health_check + until: health_check.status == 200 + retries: 5 + delay: 2 + become: no + when: compose_result is changed + + rescue: + - name: Log deployment failure + debug: + msg: "Deployment failed! Check logs at {{ compose_project_dir }}/docker-compose.yml" + failed_when: true + + always: + - name: Show deployment status + debug: + msg: + - "Application deployed with Docker Compose" + - "Project directory: {{ compose_project_dir | default('/opt/' + app_name) }}" + - "Container status: run 'docker ps' on target" + + tags: + - web_app + - compose + - deploy \ No newline at end of file diff --git a/ansible/roles/web_app/tasks/wipe.yml b/ansible/roles/web_app/tasks/wipe.yml new file mode 100644 index 0000000000..27d23207ff --- /dev/null +++ b/ansible/roles/web_app/tasks/wipe.yml @@ -0,0 +1,42 @@ +--- +- name: Wipe web application + block: + - name: Stop and remove containers with Docker Compose + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir | default('/opt/' + app_name) }}" + state: absent + remove_orphans: yes + remove_volumes: yes # удалить также тома (если есть) + become: no + ignore_errors: yes + register: compose_down + + - name: Remove docker-compose.yml file + file: + path: "{{ compose_project_dir | default('/opt/' + app_name) }}/docker-compose.yml" + state: absent + become: yes + ignore_errors: yes + + - name: Remove application directory + file: + path: "{{ compose_project_dir | default('/opt/' + app_name) }}" + state: absent + become: yes + ignore_errors: yes + + - name: (Optional) Remove Docker images + docker_image: + name: "{{ dockerhub_username }}/{{ app_name }}:{{ docker_tag | default('latest') }}" + state: absent + become: no + ignore_errors: yes + when: web_app_remove_images | default(false) | bool # дополнительная опция + + - name: Log wipe completion + debug: + msg: "Application {{ app_name }} wiped successfully from {{ compose_project_dir | default('/opt/' + app_name) }}" + + when: web_app_wipe | default(false) | bool + tags: + - web_app_wipe \ No newline at end of file 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..279b82d8bb --- /dev/null +++ b/ansible/roles/web_app/templates/docker-compose.yml.j2 @@ -0,0 +1,25 @@ +--- + +services: + {{ app_name }}: + image: "{{ dockerhub_username }}/{{ app_name }}:latest" + container_name: "{{ app_name }}-compose" + ports: + - "{{ app_port }}:{{ app_internal_port | default(app_port) }}" + environment: + APP_NAME: "{{ app_name }}" + APP_PORT: "{{ app_internal_port | default(app_port) }}" + ENVIRONMENT: "{{ app_environment.ENVIRONMENT | default('production') }}" + + restart: "{{ restart_policy | default('unless-stopped') }}" + networks: + - app_network + logging: + driver: "json-file" + options: + max-size: "10m" + max-file: "3" + +networks: + app_network: + driver: bridge \ No newline at end of file diff --git a/app_go/Dockerfile b/app_go/Dockerfile new file mode 100644 index 0000000000..8ffdcbdbe9 --- /dev/null +++ b/app_go/Dockerfile @@ -0,0 +1,14 @@ +FROM golang:1.21 AS builder +WORKDIR /app +COPY go.mod ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o myapp + +FROM alpine:3.18 +RUN adduser -D appuser +WORKDIR /app +COPY --from=builder /app/myapp . +USER appuser +EXPOSE 5000 +CMD ["./myapp"] \ No newline at end of file diff --git a/app_go/README.md b/app_go/README.md new file mode 100644 index 0000000000..fee9c90b68 --- /dev/null +++ b/app_go/README.md @@ -0,0 +1,84 @@ +# Overview +A Go implementation of the DevOps Info Service with the same functionality as the Python/Flask version. + +# Prerequisites +- Go 1.21 or higher + +# Installation +1. **Clone the repository**: +2. **Build**: +```bash +# Build for current platform +go build -o devops-info-service main.go + +# Build for Linux (cross-compilation) +GOOS=linux GOARCH=amd64 go build -o devops-info-service-linux main.go + +``` + +# Running the Application +``` +# Run directly with go +go run main.go + +# Run compiled binary +./devops-info-service + +# Run with custom configuration +HOST=127.0.0.1 PORT=3000 DEBUG=true ./devops-info-service +``` + +# API Endpoint +## `GET /` - Service and system information +### Example Response: +``` +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Go" + }, + "system": { + "hostname": "my-laptop", + "platform": "Linux", + "platform_version": "Ubuntu 24.04", + "architecture": "x86_64", + "cpu_count": 8, + "go": "go1.21.0" + }, + "runtime": { + "uptime_seconds": 3600, + "uptime_human": "1 hour, 0 minutes", + "current_time": "2026-01-07T14:30:00.000Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1", + "user_agent": "curl/7.81.0", + "method": "GET", + "path": "/" + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] +} +``` + +## `GET /health` - Health check +### Example Response: +``` +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` + +# Configuration +| Variable name | Basic value | Description | +|----------|--------|-------------| +| **Host** | 0.0.0.0 | IP of the service | +| **Port** | 5000 | Port of the service | +| **Debug** | False | Debug mode enabeled | diff --git a/app_go/docs/GO.md b/app_go/docs/GO.md new file mode 100644 index 0000000000..9a858175ae --- /dev/null +++ b/app_go/docs/GO.md @@ -0,0 +1,3 @@ +# Go Language Justification + +I already familiar with this language and moreover: I want to study it by hard. So making the assignment on this language is good exercise for me not just to code on GO more, but also to use same things I can use in Python there. \ No newline at end of file diff --git a/app_go/docs/LAB01.md b/app_go/docs/LAB01.md new file mode 100644 index 0000000000..54e738a518 --- /dev/null +++ b/app_go/docs/LAB01.md @@ -0,0 +1,16 @@ +# Lab 01 + +## Framework Selection + +Stated in GO.md + +## Best Practices Applied +Same as in python implemmentation (except PEP8 and I have no dependencies in go implementation) +## API Documentation +Same as in Python implementation (except Framework:Go and no python_version but go_version) + +## Challenges & Solutions +For first week there were no significant challanges. + +## GitHub Community +Starring repositories matters because it supports open-source projects by increasing their visibility and helping maintainers gauge community interest. Following developers helps build professional networks that foster collaboration, learning, and growth opportunities in team projects. \ No newline at end of file diff --git a/app_go/docs/LAB02.md b/app_go/docs/LAB02.md new file mode 100644 index 0000000000..c615d24d16 --- /dev/null +++ b/app_go/docs/LAB02.md @@ -0,0 +1,161 @@ +# Lab 02 + +## Multi-stage strategy +The total process is in 2 stages: +1. Create an optimized, statically compiled binary. +2. Minimal production runtime environment. + +We want to firstly build our app and for this we use all we need. Then we want to reduce container size, remove all compiler's stuff and tools we won't use. And for this we copy only executable file with go-alpine. + +## Build Process & Size Analysis + +### Terminal Output +```sh +damir@damir-VB:~/Desktop/DevOps/DevOps-Core-Course/app_go$ docker build -t devops-info-service-go:latest . +DEPRECATED: The legacy builder is deprecated and will be removed in a future release. + Install the buildx component to build images with BuildKit: + https://docs.docker.com/go/buildx/ + +Sending build context to Docker daemon 825.3kB +Step 1/13 : FROM golang:1.21 AS builder + ---> 246ea1ed9cdb +Step 2/13 : WORKDIR /app + ---> Using cache + ---> f14686bd9c3d +Step 3/13 : COPY go.mod ./ + ---> 120f56ebd2dc +Step 4/13 : RUN go mod download + ---> Running in 83ea1c732318 +go: no module dependencies to download + ---> Removed intermediate container 83ea1c732318 + ---> ac49e8ffa4a1 +Step 5/13 : COPY . . + ---> 8293d31a8458 +Step 6/13 : RUN CGO_ENABLED=0 go build -o myapp + ---> Running in ef0aa4e2f41c + ---> Removed intermediate container ef0aa4e2f41c + ---> b140eafdac12 +Step 7/13 : FROM alpine:3.18 +3.18: Pulling from library/alpine +44cf07d57ee4: Pull complete +Digest: sha256:de0eb0b3f2a47ba1eb89389859a9bd88b28e82f5826b6969ad604979713c2d4f +Status: Downloaded newer image for alpine:3.18 + ---> 802c91d52981 +Step 8/13 : RUN adduser -D appuser + ---> Running in 86943f47828a + ---> Removed intermediate container 86943f47828a + ---> 868848cc0cdb +Step 9/13 : WORKDIR /app + ---> Running in 62cf6e903abe + ---> Removed intermediate container 62cf6e903abe + ---> 7d3432469112 +Step 10/13 : COPY --from=builder /app/myapp . + ---> ae233bad30d3 +Step 11/13 : USER appuser + ---> Running in e2d2076c73e9 + ---> Removed intermediate container e2d2076c73e9 + ---> 42bde78130ee +Step 12/13 : EXPOSE 5000 + ---> Running in 8f663d6562d1 + ---> Removed intermediate container 8f663d6562d1 + ---> a1e0dafdc306 +Step 13/13 : CMD ["./myapp"] + ---> Running in b6fd172a8246 + ---> Removed intermediate container b6fd172a8246 + ---> b2e17931f148 +Successfully built b2e17931f148 +Successfully tagged devops-info-service-go:latest +damir@damir-VB:~/Desktop/DevOps/DevOps-Core-Course/app_go$ docker images | grep devops +devops-info-service-go latest b2e17931f148 14 minutes ago 14.3MB +damirsadykov/devops-info-service 1.0 8a7e51a097ec 17 hours ago 132MB +damirsadykov/devops-info-service latest 8a7e51a097ec 17 hours ago 132MB +devops-info-service latest 8a7e51a097ec 17 hours ago 132MB +``` + +### Size analysis +```sh +damir@damir-VB:~/Desktop/DevOps/DevOps-Core-Course/app_go$ docker image ls --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" +REPOSITORY TAG SIZE +devops-info-service-go latest 14.3MB +golang 1.21 814MB +``` + +### Size breakdown +``` +Multi-Stage Build: +├── Builder Stage (golang:1.21): 814MB +│ ├── Go toolchain: ~814MB +│ └── Dependencies: <1MB +│ +└── Final Stage (alpine:3.18 + binary): 14.3MB + ├── Alpine base: 7.05MB + ├── Compiled binary: 7.2MB + └── User setup: 0.05MB + +Size Reduction: 864MB → 14.3MB (98.6% reduction!) +``` + +### Comparison Table: +|Aspect Single-Stage | Multi-Stage (Your) | Improvement +|----------|--------|-------------| +|Image Size 814MB | 14.3MB | 98.6% smaller +|Security Risk | High (compilers, source) | Low (binary only) | Significantly safer +|Attack Surface | Large (~1000 packages) | Minimal (~50 packages) | 95% reduction +|Deployment Speed | Slow (large transfer) | Fast (small transfer) | 73x faster transfer +|Registry Storage Cost | High | Minimal | ~$0.87/month vs $0.01/month + + +## Technical Stage-by-Stage Analysis + +### Stage 1: Builder (golang:1.21) +```dockerfile +FROM golang:1.21 AS builder +WORKDIR /app +COPY go.mod ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -o myapp +``` + +#### Purpose: Create an optimized, statically compiled binary. + +#### Key Decisions: +1. `golang:1.21`: Specific version ensures reproducible builds + +2. `WORKDIR /app`: Consistent working directory + +3. Copy `go.mod` first: Leverages Docker cache for dependencies + +4. `go mod download`: Downloads dependencies separately (cache optimization) + +5. `CGO_ENABLED=0`: Produces static binary with no C dependencies + +6. `-o myapp`: Explicit output name for clarity + +### Stage 2: Runtime (alpine:3.18) +```dockerfile +FROM alpine:3.18 +RUN adduser -D appuser +WORKDIR /app +COPY --from=builder /app/myapp . +USER appuser +EXPOSE 5000 +CMD ["./myapp"] +``` + +#### Purpose: Minimal production runtime environment. + +#### Key Decisions: +1. `alpine:3.18`: Extremely small base image (~7MB) +2. Non-root user: Security best practice +3. `COPY --from=builder`: Only copies the binary, not build tools +4. Explicit CMD: Clear entry point + + +## Security Benefits Analysis +### Security Improvements: +1. No Compilers: Can't compile malicious code inside container +2. No Source Code: Intellectual property protected +3. Minimal Packages: Fewer CVEs to patch +4. Non-Root User: Principle of least privilege +5. Static Binary: No runtime dependency vulnerabilities \ No newline at end of file diff --git a/app_go/docs/screenshots/01-main-endpoint.png b/app_go/docs/screenshots/01-main-endpoint.png new file mode 100644 index 0000000000..083b341375 Binary files /dev/null and b/app_go/docs/screenshots/01-main-endpoint.png differ diff --git a/app_go/docs/screenshots/02-health-check.png b/app_go/docs/screenshots/02-health-check.png new file mode 100644 index 0000000000..3e214baab4 Binary files /dev/null and b/app_go/docs/screenshots/02-health-check.png differ diff --git a/app_go/docs/screenshots/03-formatted-output.png b/app_go/docs/screenshots/03-formatted-output.png new file mode 100644 index 0000000000..659860a066 Binary files /dev/null and b/app_go/docs/screenshots/03-formatted-output.png differ diff --git a/app_go/go.mod b/app_go/go.mod new file mode 100644 index 0000000000..d63c03bb47 --- /dev/null +++ b/app_go/go.mod @@ -0,0 +1,7 @@ +module devops-info-service-go + +go 1.21 + +require ( + // No external dependencies for this basic service +) \ No newline at end of file diff --git a/app_go/main.go b/app_go/main.go new file mode 100644 index 0000000000..3fa31a68ff --- /dev/null +++ b/app_go/main.go @@ -0,0 +1,209 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "os" + "runtime" + "time" +) + +// Structs for the response +type ServiceInfo struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime Runtime `json:"runtime"` + Request Request `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} + +type Service struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Framework string `json:"framework"` +} + +type System struct { + Hostname string `json:"hostname"` + Platform string `json:"platform"` + PlatformVersion string `json:"platform_version"` + Architecture string `json:"architecture"` + CPUCount int `json:"cpu_count"` + GoVersion string `json:"go_version"` +} + +type Runtime struct { + UptimeSeconds int `json:"uptime_seconds"` + UptimeHuman string `json:"uptime_human"` + CurrentTime string `json:"current_time"` + Timezone string `json:"timezone"` +} + +type Request struct { + ClientIP string `json:"client_ip"` + UserAgent string `json:"user_agent"` + Method string `json:"method"` + Path string `json:"path"` +} + +type Endpoint struct { + Path string `json:"path"` + Method string `json:"method"` + Description string `json:"description"` +} + +type HealthResponse struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` + UptimeSeconds int `json:"uptime_seconds"` +} + +// Application start time +var startTime = time.Now() + +// Helper function to get client IP +func getClientIP(r *http.Request) string { + // Try to get IP from X-Forwarded-For header + ip := r.Header.Get("X-Forwarded-For") + if ip == "" { + // Fall back to RemoteAddr + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return r.RemoteAddr + } + return ip + } + return ip +} + +// Helper function to format uptime +func getUptime() (int, string) { + uptime := time.Since(startTime) + seconds := int(uptime.Seconds()) + + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + remainingSeconds := seconds % 60 + + if hours > 0 { + return seconds, fmt.Sprintf("%d hour%s, %d minute%s", + hours, plural(hours), + minutes, plural(minutes)) + } else if minutes > 0 { + return seconds, fmt.Sprintf("%d minute%s", + minutes, plural(minutes)) + } + return seconds, fmt.Sprintf("%d second%s", + remainingSeconds, plural(remainingSeconds)) +} + +// Helper function for pluralization +func plural(count int) string { + if count == 1 { + return "" + } + return "s" +} + +// Main handler +func mainHandler(w http.ResponseWriter, r *http.Request) { + // Get uptime + uptimeSeconds, uptimeHuman := getUptime() + + // Get hostname + hostname, _ := os.Hostname() + if hostname == "" { + hostname = "unknown" + } + + // Create response + info := ServiceInfo{ + Service: Service{ + Name: "devops-info-service", + Version: "1.0.0", + Description: "DevOps course info service", + Framework: "Go", + }, + System: System{ + Hostname: hostname, + Platform: runtime.GOOS, + PlatformVersion: runtime.Version(), // Go doesn't have direct OS version, using Go version + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + GoVersion: runtime.Version(), + }, + Runtime: Runtime{ + UptimeSeconds: uptimeSeconds, + UptimeHuman: uptimeHuman, + CurrentTime: time.Now().UTC().Format(time.RFC3339Nano), + Timezone: "UTC", + }, + Request: Request{ + ClientIP: getClientIP(r), + UserAgent: r.UserAgent(), + Method: r.Method, + Path: r.URL.Path, + }, + Endpoints: []Endpoint{ + {Path: "/", Method: "GET", Description: "Service information"}, + {Path: "/health", Method: "GET", Description: "Health check"}, + }, + } + + // Set headers and encode JSON + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(info) +} + +// Health handler +func healthHandler(w http.ResponseWriter, r *http.Request) { + uptimeSeconds, _ := getUptime() + + response := HealthResponse{ + Status: "healthy", + Timestamp: time.Now().UTC().Format(time.RFC3339Nano), + UptimeSeconds: uptimeSeconds, + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + json.NewEncoder(w).Encode(response) +} + +func main() { + // Get configuration from environment variables + host := getEnv("HOST", "0.0.0.0") + port := getEnv("PORT", "5000") + debug := getEnv("DEBUG", "false") + + // Set up routes + http.HandleFunc("/", mainHandler) + http.HandleFunc("/health", healthHandler) + + // Log startup information + log.Printf("Starting DevOps Info Service (Go)...") + log.Printf("Host: %s, Port: %s, Debug: %s", host, port, debug) + log.Printf("Go version: %s", runtime.Version()) + log.Printf("Listening on http://%s:%s", host, port) + + // Start server + addr := fmt.Sprintf("%s:%s", host, port) + err := http.ListenAndServe(addr, nil) + if err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} + +// Helper function to get environment variable with default +func getEnv(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..3efaf08141 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,18 @@ +.git +.gitignore + +__pycache__ +*.pyc +*.pyo +venv/ +.venv/ + +.env +*.pem +secrets/ + +*.md +docs/ + +tests/ +.pytest_cache/ \ No newline at end of file diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..4de420a8f7 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,12 @@ +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store \ No newline at end of file diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..19735e7ab9 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.12-slim + +RUN useradd --create-home --shell /bin/bash appuser + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . + +USER appuser + +EXPOSE 5000 + +CMD ["python", "app.py"] \ No newline at end of file diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..8acb8e640e --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,120 @@ +# Overview +This service is a Flask-based study service in python that returns comprehensive service and system information. It provides endpoints for service metadata, system statistics, and health monitoring, making it useful for DevOps monitoring and system diagnostics. + +# Prerequisites +- **Python Version**: 3.8 or higher +- **Dependencies** (from requirements.txt): + - Flask==3.1.0 + +# Installation +1. **Clone the repository**: +2. **Create and activate a virtual environment**: +```bash +python -m venv venv +source venv/bin/activate +``` +3. **Install dependencies**: +```bash +pip install -r requirements.txt +``` + +# Running the Application +``` +python app.py +# Or with custom config +PORT=8080 python app.py +HOST=127.0.0.1 PORT=3000 python app.py +``` + +# API Endpoint +## `GET /` - Service and system information +### Example Response: +``` +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask" + }, + "system": { + "hostname": "my-laptop", + "platform": "Linux", + "platform_version": "Ubuntu 24.04", + "architecture": "x86_64", + "cpu_count": 8, + "python_version": "3.13.1" + }, + "runtime": { + "uptime_seconds": 3600, + "uptime_human": "1 hour, 0 minutes", + "current_time": "2026-01-07T14:30:00.000Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1", + "user_agent": "curl/7.81.0", + "method": "GET", + "path": "/" + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] +} +``` + +## `GET /health` - Health check +### Example Response: +``` +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` + +# Configuration +| Variable name | Basic value | Description | +|----------|--------|-------------| +| **Host** | 0.0.0.0 | IP of the service | +| **Port** | 5000 | Port of the service | +| **Debug** | False | Debug mode enabeled | + +# Docker +This application is containerized and ready for deployment using Docker. + +## Building the Image Locally +To build the Docker image from source: +```sh +# Navigate to the application directory +cd app + +# Build the image with a tag +docker build -t : . +``` + +## Running the Container +To run the application in a container: +```sh +# Basic run with port mapping +docker run -d -p : : +``` + +## Pulling from Docker Hub +The image is available on Docker Hub and can be pulled directly: +```sh +# Pull the latest version +docker pull /:latest +``` + + +# GitHub Actions Status Badge +[![Python test and build](https://github.com/SamuelAnton/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg?branch=lab03)](https://github.com/SamuelAnton/DevOps-Core-Course/actions/workflows/python-ci.yml) + +# Testing +To test locally run +```sh +cd app_python +pytest . +``` \ No newline at end of file diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..d9d5cf8a39 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,133 @@ +""" +DevOps Info Service +Main application module +""" +import os +import socket +import platform +from datetime import datetime, timezone +from flask import Flask, jsonify, request +import logging + +app = Flask(__name__) + +# Configuration +HOST = os.getenv('HOST', '0.0.0.0') +PORT = int(os.getenv('PORT', 5000)) +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' + +# Application start time +start_time = datetime.now() + +# Logging +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + + +# Function that collects system info +def get_system_info(): + """Collect system information.""" + return { + 'hostname': socket.gethostname(), + 'platform': platform.system(), + 'platform_version': platform.version(), + 'architecture': platform.machine(), + 'cpu_count': os.cpu_count(), + 'python_version': platform.python_version() + } + + +# Function that gets total uptime of a service +def get_uptime(): + delta = datetime.now() - start_time + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + hum = f"{hours} hour{'s' if hours != 1 else ''}, {minutes} minute{'s' if minutes != 1 else ''}" + return { + 'seconds': seconds, + 'human': hum + } + + +# API Endpoints +# Main - service and system information +@app.route('/') +def index(): + """Main endpoint - service and system information.""" + # Collect all required information + system_info = get_system_info() + uptime_info = get_uptime() + + response = { + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask" + }, + "system": { + "hostname": system_info['hostname'], + "platform": system_info['platform'], + "platform_version": system_info['platform_version'], + "architecture": system_info['architecture'], + "cpu_count": system_info['cpu_count'], + "python_version": system_info['python_version'] + }, + "runtime": { + "uptime_seconds": uptime_info['seconds'], + "uptime_human": uptime_info['human'], + "current_time": datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'), + "timezone": "UTC" + }, + "request": { + "client_ip": request.remote_addr, + "user_agent": request.headers.get('User-Agent', 'Unknown'), + "method": request.method, + "path": request.path + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] + } + + return jsonify(response) + + +# Health check +@app.route('/health') +def health(): + return jsonify({ + 'status': 'healthy', + 'timestamp': datetime.now(timezone.utc).isoformat().replace('+00:00', 'Z'), + 'uptime_seconds': get_uptime()['seconds'] + }) + + +# Error Handling +@app.errorhandler(404) +def not_found(error): + return jsonify({ + 'error': 'Not Found', + 'message': 'Endpoint does not exist' + }), 404 + + +@app.errorhandler(500) +def internal_error(error): + return jsonify({ + 'error': 'Internal Server Error', + 'message': 'An unexpected error occurred' + }), 500 + + +# Start of the service +if __name__ == '__main__': + logger.info('Application starting...') + logger.info(f'Running on http://{HOST}:{PORT}') + logger.info(f'Debug mode: {DEBUG}') + app.run(host=HOST, port=PORT, debug=DEBUG) diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..8c00a0bf96 --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,251 @@ +# Lab 01 + +## Framework Selection + +I choosed `Flask ` because, how it was stated, it is lightweight and I have already worked with it in my project, so it will be easier to use it than study new tools (even if I think that study new tools is great, I want to practice with Flask more). + +| Framework | Pros | Cons | +|----------|--------|-------------| +| **Flask** | Lightweight, already know from previous project | Not so good choice for complex projects | +| **FastAPI** | Modern, async, auto-documentation | Complex and requires learning it | +| **Django** | Full-featured, includes ORM | Too complex, requires a lot to learn | + + +## Best Practices Applied +### Clean Code Organization +Clear code is easier to maintain and read. It's especially important when working in teams, as it reduces time spent understanding code rather than working on it. Additionally, when returning to a project after some time, clean code helps you quickly understand what's happening. + +**Implementation:** +```python +# Clear function names with descriptive docstrings +def get_system_info(): + """Collect system information.""" + return { + 'hostname': socket.gethostname(), + 'platform': platform.system(), + 'architecture': platform.machine(), + 'python_version': platform.python_version() + } + +# Proper imports grouping +import os +import socket +import platform +from datetime import datetime, timezone +from flask import Flask, jsonify, request +import logging + +# Comments only where needed +""" +DevOps Info Service +Main application module +""" +import os +... + +# Configuration - clearly separated section +HOST = os.getenv('HOST', '0.0.0.0') +... + +# Following PEP 8 style guide +def get_uptime(): + delta = datetime.now() - start_time + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return { + 'seconds': seconds, + 'human': f"{hours} hours, {minutes} minutes" + } +``` + + +### Error Handling +Error handling is crucial because it helps ensure your service works correctly. Good error handling allows you to identify issues during testing and provides users with meaningful error messages. +``` python +@app.errorhandler(404) +def not_found(error): + return jsonify({ + 'error': 'Not Found', + 'message': 'Endpoint does not exist' + }), 404 + +@app.errorhandler(500) +def internal_error(error): + return jsonify({ + 'error': 'Internal Server Error', + 'message': 'An unexpected error occurred' + }), 500 +``` + +### Logging +Logging is a crucial part of development. It shows what's happening in the code: events, errors, and other important information. This helps identify hidden issues and confirms that everything is working as expected. +``` python +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +logger.info('Application starting...') +logger.debug(f'Request: {request.method} {request.path}') +``` + +### Dependencies +t's good practice to collect all project dependencies in a dedicated file. This makes it easier to maintain the project and deploy it to different machines. +``` +Flask==3.1.0 +``` + +### Git Ignore +It's good practice to exclude unnecessary files from your repository to keep it clean and organized. +``` +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +``` + +## API Documentation +### GET / - Service Information + +**Description**: Returns comprehensive service and system information including service metadata, system details, runtime statistics, and request information. + +**Request:** +```bash +curl http://localhost:5000/ +``` + +**Response:** +```bash +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask" + }, + "system": { + "hostname": "my-laptop", + "platform": "Linux", + "platform_version": "Ubuntu 24.04", + "architecture": "x86_64", + "cpu_count": 8, + "python_version": "3.13.1" + }, + "runtime": { + "uptime_seconds": 3600, + "uptime_human": "1 hour, 0 minutes", + "current_time": "2026-01-07T14:30:00.000Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1", + "user_agent": "curl/7.81.0", + "method": "GET", + "path": "/" + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] +} +``` + +### GET /health - Health Check +**Description:** Simple endpoint for service monitoring and health checks. + +**Request:** +```bash +curl http://localhost:5000/health +``` + +**Response:** +``` +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` + +### Testing +#### Screenshots +In the proper folder. You can find them there. +#### Terminal output + +**Starting the application:** +``` +2026-01-27 23:28:29,367 - __main__ - INFO - Application starting... +2026-01-27 23:28:29,367 - __main__ - INFO - Running on http://0.0.0.0:5000 +2026-01-27 23:28:29,367 - __main__ - INFO - Debug mode: False +``` + +**Testing GET / endpoint:** +```bash +$curl http://localhost:5000/ | python3 -m json.tool +{ + "endpoints": [ + { + "description": "Service information", + "method": "GET", + "path": "/" + }, + { + "description": "Health check", + "method": "GET", + "path": "/health" + } + ], + "request": { + "client_ip": "127.0.0.1", + "method": "GET", + "path": "/", + "user_agent": "curl/7.81.0" + }, + "runtime": { + "current_time": "2026-01-27T20:35:29.676613Z", + "timezone": "UTC", + "uptime_human": "0 hours, 0 minutes", + "uptime_seconds": 9 + }, + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "name": "devops-info-service", + "version": "1.0.0" + }, + "system": { + "architecture": "x86_64", + "cpu_count": 2, + "hostname": "damir-VB", + "platform": "Linux", + "platform_version": "#89~22.04.2-Ubuntu SMP PREEMPT_DYNAMIC Wed Oct 29 10:45:25 UTC 2", + "python_version": "3.10.12" + } +} +``` + +**Testing GET /health endpoint:** +```bash +$curl http://localhost:5000/health | python3 -m json.tool +{ + "status": "healthy", + "timestamp": "2026-01-27T20:37:22.664269Z", + "uptime_seconds": 7 +} +``` + +## Challenges & Solutions +For first week there were no significant challanges. + +## GitHub Community +Starring repositories matters because it supports open-source projects by increasing their visibility and helping maintainers gauge community interest. Following developers helps build professional networks that foster collaboration, learning, and growth opportunities in team projects. \ No newline at end of file diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..36e5df550e --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,316 @@ +# Lab 02 + +## 1. Docker Best Practices Applied +### 1.1 Non-Root User +- Security: Running as non-root minimizes the impact of potential container breaches +- Principle of Least Privilege: The application only has permissions it needs +- Compliance: Many security standards require non-root execution +- Risk Reduction: If the application is compromised, the attacker has limited system access + +```dockerfile +RUN useradd --create-home --shell /bin/bash appuser + +USER appuser +``` + +### 1.2 Specific Base Image Version +- Reproducibility: Specific versions ensure consistent builds across environments +- Security: Known, patched versions reduce vulnerability exposure +- Stability: Avoids breaking changes from latest tags +- Size Optimization: slim variant reduces image size by ~50% compared to full Python image + +```dockerfile +FROM python:3.12-slim +``` + +### 1.3 Layer Caching Optimization +- Build Speed: Changing code doesn't trigger dependency reinstallation +- Cache Efficiency: Leverages Docker's layer caching for faster builds +- CI/CD Performance: Reduces build times in pipelines +- Developer Experience: Quicker iteration during development + +```dockerfile +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . +``` + +### 1.4 .dockerignore File +- Smaller Images: Excludes unnecessary files, reducing image size +- Security: Prevents secrets from accidentally being included +- Build Performance: Less context to send to Docker daemon +- Cleanliness: Only production-relevant files in the image + +``` +.git +.gitignore +... +``` + +### 1.5 No Cache Directory +- Image Size: Removes pip cache +- Security: Eliminates cached package files that could contain vulnerabilities +- Clean Builds: Ensures fresh downloads each build + +```dockerfile +RUN pip install --no-cache-dir -r requirements.txt +``` + + +## 2. Image Information & Decisions +### Chosen: `python:3.12-slim` +- `python:3.12-slim` provides the optimal balance of size (~125MB), compatibility (glibc), and security (minimal packages). +- `python:3.12` - too large (~1GB) +- `python:3.12-alpine` - too small - Python packages may fail + +### Final image size: 132MB +- Assessment: Acceptable for a Python service. +- Size Breakdown: + - Base image (python:3.12-slim): 125MB + - Application dependencies: 8MB + - Application code: 3,7 kB + + Total: 132MB + +### Layer Structure +1. Base layers (immutable, cached across all builds) +2. System packages (rarely changed) +3. Dependencies (changed when requirements.txt updates) +4. Application code (frequently changed) +5. Configuration (runtime settings) + +### Optimization Choices +Implemented: +- Layer ordering: Dependencies before code +- Slim base: Minimal OS footprint +- No cache: Clean pip installs +- Multi-command RUN: Single layer for user creation + + +## 3. Build & Run ProcessBuild & Run Process + +### Build and push process terminal output: +```sh +damir@damir-VB:~/Desktop/DevOps/DevOps-Core-Course$ docker build -t devops-info-service:latest ./app_python/ +DEPRECATED: The legacy builder is deprecated and will be removed in a future release. + Install the buildx component to build images with BuildKit: + https://docs.docker.com/go/buildx/ + +Sending build context to Docker daemon 8.704kB +Step 1/9 : FROM python:3.12-slim + ---> c78a70d7588f +Step 2/9 : RUN useradd --create-home --shell /bin/bash appuser + ---> Using cache + ---> 633616f640da +Step 3/9 : WORKDIR /app + ---> Using cache + ---> 428cc6c9edd9 +Step 4/9 : COPY requirements.txt . + ---> Using cache + ---> 5c3f41c3170b +Step 5/9 : RUN pip install --no-cache-dir -r requirements.txt + ---> Using cache + ---> d56ad894e57a +Step 6/9 : COPY app.py . + ---> Using cache + ---> 67026a81e73f +Step 7/9 : USER appuser + ---> Using cache + ---> f4455d3b7569 +Step 8/9 : EXPOSE 5000 + ---> Using cache + ---> acfc132c1e47 +Step 9/9 : CMD ["python", "app.py"] + ---> Using cache + ---> 8a7e51a097ec +Successfully built 8a7e51a097ec +Successfully tagged devops-info-service:latest +damir@damir-VB:~/Desktop/DevOps/DevOps-Core-Course$ docker tag devops-info-service:latest damirsadykov/devops-info-service:latest +damir@damir-VB:~$ docker tag devops-info-service:latest damirsadykov/devops-info-service:1.0 +damir@damir-VB:~/Desktop/DevOps/DevOps-Core-Course$ docker login +Authenticating with existing credentials... +WARNING! Your password will be stored unencrypted in ~/.docker/config.json. +Configure a credential helper to remove this warning. See +https://docs.docker.com/engine/reference/commandline/login/#credential-stores + +Login Succeeded +damir@damir-VB:~/Desktop/DevOps/DevOps-Core-Course$ docker push damirsadykov/devops-info-service:latest +The push refers to repository [docker.io/damirsadykov/devops-info-service] +33c1e7bf52a9: Pushed +ba6ad2b86434: Pushed +56f38b3c8b1d: Pushed +6f891e75b169: Pushed +d85f0fb2b9c2: Pushed +343fbb74dfa7: Mounted from library/python +cfdc6d123592: Mounted from library/python +ff565e4de379: Mounted from library/python +e50a58335e13: Mounted from library/python +latest: digest: sha256:... size: 2199 +damir@damir-VB:~/Desktop/DevOps/DevOps-Core-Course$ docker pull damirsadykov/devops-info-service:latest +latest: Pulling from damirsadykov/devops-info-service +Digest: sha256:0... +Status: Image is up to date for damirsadykov/devops-info-service:latest +docker.io/damirsadykov/devops-info-service:latest +damir@damir-VB:~$ docker push damirsadykov/devops-info-service:1.0 +The push refers to repository [docker.io/damirsadykov/devops-info-service] +33c1e7bf52a9: Layer already exists +ba6ad2b86434: Layer already exists +56f38b3c8b1d: Layer already exists +6f891e75b169: Layer already exists +d85f0fb2b9c2: Layer already exists +343fbb74dfa7: Layer already exists +cfdc6d123592: Layer already exists +ff565e4de379: Layer already exists +e50a58335e13: Layer already exists +1.0: digest: sha256:... size: 2199 +``` + +### Run and test terminal output: +```sh +damir@damir-VB:~$ docker run -dp 5000:5000 --name test devops-info-service:latest +24252bc5470ee314a9d6be615792db5bec4661b25e4bae9e59fdb560a236f6d4 +damir@damir-VB:~$ curl http://localhost:5000/ | python3 -m json.tool + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 692 100 692 0 0 83293 0 --:--:-- --:--:-- --:--:-- 86500 +{ + "endpoints": [ + { + "description": "Service information", + "method": "GET", + "path": "/" + }, + { + "description": "Health check", + "method": "GET", + "path": "/health" + } + ], + "request": { + "client_ip": "172.17.0.1", + "method": "GET", + "path": "/", + "user_agent": "curl/7.81.0" + }, + "runtime": { + "current_time": "2026-02-03T11:33:55.523995Z", + "timezone": "UTC", + "uptime_human": "0 hours, 0 minutes", + "uptime_seconds": 16 + }, + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "name": "devops-info-service", + "version": "1.0.0" + }, + "system": { + "architecture": "x86_64", + "cpu_count": 2, + "hostname": "24252bc5470e", + "platform": "Linux", + "platform_version": "#91~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Thu Nov 20 15:20:45 UTC 2", + "python_version": "3.12.12" + } +} +damir@damir-VB:~$ curl http://localhost:5000/health | python3 -m json.tool + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 83 100 83 0 0 33413 0 --:--:-- --:--:-- --:--:-- 41500 +{ + "status": "healthy", + "timestamp": "2026-02-03T11:33:59.810204Z", + "uptime_seconds": 20 +} +damir@damir-VB:~$ docker stop test +test +damir@damir-VB:~$ docker rm test +test +``` + +### Docker Hub repository URL +https://hub.docker.com/repository/docker/damirsadykov/devops-info-service/general + +### Tagging strategy +Tag Structure: +``` +/: +``` + +`` : +1. `latest`, as it is latest (and only) build. It is for users who want the most recent stable build +2. `1.0`, as it is first (and only) build version. It is for versions reproducibility + +## 4. Technical Analysis + +### Why This Dockerfile Works +1. Progressive Layering: Each layer builds upon the previous, optimizing cache usage +2. Minimalist Approach: Only includes what's necessary for runtime +3. Security First: Non-root user, no secrets, clean builds + +### Layer Order Impact + +#### Current order: +```dockerfile +# Layer 1: Base image (cached) +FROM python:3.12-slim + +# Layer 2: User setup (rarely changes) +RUN useradd --create-home --shell /bin/bash appuser +WORKDIR /app + +# Layer 3: Dependencies (changes when requirements.txt updates) +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Layer 4: Application code (changes frequently) +COPY app.py . + +# Layer 5: Runtime configuration +USER appuser +EXPOSE 5000 +CMD ["python", "app.py"] +``` + +#### What if we reversed layers 3 and 4? +```dockerfile +COPY app.py . # Changes frequently - cache invalidated often +RUN pip install -r requirements.txt # Reinstalls every code change +``` + +#### Impact: +- Build Time: Increases from ~8s to ~30s per build +- Cache Efficiency: Dependencies reinstalled on every code change +- Network Usage: Downloads packages repeatedly +- CI/CD Costs: Longer pipeline runtimes + + +### Security Considerations + +#### Implemented: +1. Non-Root Execution: Limits container breakout impact +2. Specific Base Version: Known, patched vulnerabilities +3. No Secrets in Image: Environment variables or mounts only +4. Minimal Packages: Reduced attack surface +5. Clean Builds: No cached files or metadata + +#### Security Benefits: +- CVE Reduction: Smaller images = fewer potential vulnerabilities +- Compliance: Meets security standards for container deployment +- Auditability: Clear layer history for security reviews +- Runtime Safety: Limited permissions if compromised + + +### `.dockerignore` Benefits +- Size Reduction: Image ~20MB smaller +- Build Speed: Less context to transfer (faster builds) +- Security: No accidental secret inclusion +- Cleanliness: Production-only files in image +- Consistency: Same image regardless of development environment + + +## 5. Challenges & Solutions +I have not meet any serious issues with this task. + +From the process, I have learned about proper stages layout. \ No newline at end of file diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..13ac48a024 --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,89 @@ +# Lab 02 + +# Overview +## Choosing a Testing Framework +I choosed to use `pytest`, because I already a little bit familiar with it. In this semester, but in different course we studied it too, so I will use it here to practice more. + +## Test structure explanation +1. Make a test client +2. Request all pathes (health, default and error) +3. Fully check presence of each response section on all depths. Also check types where possible + +## Workflow trigger strategy +The workflow will trigger on every push into lab** branches. Since in this course we need to push only there, it is more than okay practice. After each last (or even single) push to lab**, we make to PRs, so it is not so good to start workflow again 2 times. + +## Docker tagging strategy +I choose "Option B: Calendar Versioning (CalVer)" with format: "YYYY.MM.DD", so I can easily distinguish each versions: newer date - newer assignment. Also, when I will do lab assignment and make some really small changes in one day - I will save only last version with all fixes and bugs found. Also, I ofcourse use `latest` tag + +# Workflow Evidance +## Successful workflow run +https://github.com/SamuelAnton/DevOps-Core-Course/actions/runs/21873318288 + +## Tests passing locally +### Terminal output +```sh +damir@damir-VB:~/Desktop/DevOps/DevOps-Core-Course/app_python$ pytest . +============================================= test session starts ============================================== +platform linux -- Python 3.10.12, pytest-9.0.2, pluggy-1.6.0 +rootdir: /home/damir/Desktop/DevOps/DevOps-Core-Course/app_python +collected 3 items + +tests/test_app.py ... [100%] + +============================================== 3 passed in 0.30s =============================================== +``` + +## Docker image on Docker Hub +https://hub.docker.com/r/damirsadykov/devops-info-service + +## Status badge working in README +See the README file for python app, or: +[![Python test and build](https://github.com/SamuelAnton/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg?branch=lab03)](https://github.com/SamuelAnton/DevOps-Core-Course/actions/workflows/python-ci.yml) + + + +# Best Practices Implemented +## Job Dependencies +There are 3 steps in CI workflow: +- Testing that application is working correctly + Security check with Snyk +- Docker build and push to DockerHub + +This means, that if some test failed or security is low, we wouldn't build and push container + +## Workflow Concurrency +I added this: +```yaml +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true +``` +So, if there will be queue to execution the workflow, only latest will run and rest will be canceled. + +## Environment Variables +I use this env variables for repeated values, so I can change them in 1 place, to apply changes in all places: +- PYTHON_VERSION: '3.12' +- DOCKER_IMAGE_NAME: 'devops-info-service' +- APP_PATH: 'app_python' + +## Caching +### Speed improvement with caching +- Before caching: test stage take 10-16s +- After caching: test stage take 9s + +## Snyk +Found that `Flask==3.1.0` is vulnaruble. So I updated to `Flask==3.1.1`. + +# Key Decisions +## Versioning Strategy +I choose "Option B: Calendar Versioning (CalVer)" with format: "YYYY.MM.DD", so I can easily distinguish each versions: newer date - newer assignment. Also, when I will do lab assignment and make some really small changes in one day - I will save only last version with all fixes and bugs found. Also, I ofcourse use `latest` tag + +## Docker Tags +- latest +- date tag in format: "YYYY.MM.DD" + +## Workflow trigger strategy +The workflow will trigger on every push into lab** branches. Since in this course we need to push only there, it is more than okay practice. After each last (or even single) push to lab**, we make to PRs, so it is not so good to start workflow again 2 times. + +## Test Coverage +2. Request all pathes (health, default and error) +3. Fully check presence of each response section on all depths. Also check types where possible \ No newline at end of file diff --git a/app_python/docs/screenshots/lab1/01-main-endpoint.png b/app_python/docs/screenshots/lab1/01-main-endpoint.png new file mode 100644 index 0000000000..607dd32f1c Binary files /dev/null and b/app_python/docs/screenshots/lab1/01-main-endpoint.png differ diff --git a/app_python/docs/screenshots/lab1/02-health-check.png b/app_python/docs/screenshots/lab1/02-health-check.png new file mode 100644 index 0000000000..dcb93cc6a3 Binary files /dev/null and b/app_python/docs/screenshots/lab1/02-health-check.png differ diff --git a/app_python/docs/screenshots/lab1/03-formatted-output.png b/app_python/docs/screenshots/lab1/03-formatted-output.png new file mode 100644 index 0000000000..54aa3bc0b3 Binary files /dev/null and b/app_python/docs/screenshots/lab1/03-formatted-output.png differ diff --git a/app_python/docs/screenshots/lab3/Green checkmark.png b/app_python/docs/screenshots/lab3/Green checkmark.png new file mode 100644 index 0000000000..9ca61c0e61 Binary files /dev/null and b/app_python/docs/screenshots/lab3/Green checkmark.png differ diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..f5a08eb470 --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,2 @@ +Flask==3.1.3 +pytest==9.0.2 \ No newline at end of file diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py new file mode 100644 index 0000000000..6712558102 --- /dev/null +++ b/app_python/tests/test_app.py @@ -0,0 +1,82 @@ +import pytest +from app import app + + +def test_health_endpoint(): + # Test health chech + client = app.test_client() + response = client.get('/health') + + assert response.status_code == 200 + assert response.get_json()["status"] == "healthy" + + +def test_default_endpoint(): + # Test default route returns expected structure + client = app.test_client() + response = client.get('/') + + assert response.status_code == 200 + data = response.get_json() + + assert "service" in data + assert "system" in data + assert "runtime" in data + assert "request" in data + assert "endpoints" in data + + service = data["service"] + assert "name" in service + assert "version" in service + assert "description" in service + assert "framework" in service + assert isinstance(service["name"], str) + assert isinstance(service["version"], str) + assert isinstance(service["description"], str) + assert isinstance(service["framework"], str) + + system = data["system"] + assert "hostname" in system + assert "platform" in system + assert "platform_version" in system + assert "architecture" in system + assert "cpu_count" in system + assert "python_version" in system + + runtime = data["runtime"] + assert "uptime_seconds" in runtime + assert "uptime_human" in runtime + assert "current_time" in runtime + assert "timezone" in runtime + assert isinstance(runtime["timezone"], str) + + request = data["request"] + assert "client_ip" in request + assert "user_agent" in request + assert "method" in request + assert "path" in request + + endpoints = data["endpoints"] + assert isinstance(endpoints, list) + assert len(endpoints) == 2 + for i in range(2): + assert "path" in endpoints[i] + assert "method" in endpoints[i] + assert "description" in endpoints[i] + assert isinstance(endpoints[i]["path"], str) + assert isinstance(endpoints[i]["method"], str) + assert isinstance(endpoints[i]["description"], str) + + +def test_error_404(): + # Test error 404 response + client = app.test_client() + response = client.get('/nonexistingpath') + + assert response.status_code == 404 + + data = response.get_json() + assert "error" in data + assert "message" in data + assert isinstance(data["error"], str) + assert isinstance(data["message"], str) \ No newline at end of file