From 2105f27bfa0b097d1092f2f3c1f016a51c73a0ae Mon Sep 17 00:00:00 2001 From: Cdeth567 <11kvvkvv11@mail.ru> Date: Thu, 5 Mar 2026 18:51:56 +0300 Subject: [PATCH 01/13] Lab 6: blocks/tags, Docker Compose, wipe logic, CI/CD --- .github/workflows/ansible-deploy.yml | 91 ++ .gitignore | 2 +- ansible/ansible.cfg | 22 +- ansible/docs/LAB05.md | 764 +++++++------- ansible/docs/LAB06.md | 960 ++++++++++++++++++ ansible/group_vars/all.yml | 36 +- ansible/inventory/hosts.ini | 2 +- ansible/playbooks/deploy.yml | 18 +- ansible/playbooks/provision.yml | 16 +- ansible/playbooks/site.yml | 18 +- ansible/roles/app_deploy/tasks/main.yml | 46 - ansible/roles/common/defaults/main.yml | 28 +- ansible/roles/common/tasks/main.yml | 99 +- ansible/roles/docker/defaults/main.yml | 25 +- ansible/roles/docker/handlers/main.yml | 10 +- ansible/roles/docker/tasks/main.yml | 168 ++- .../{app_deploy => web_app}/defaults/main.yml | 25 +- .../{app_deploy => web_app}/handlers/main.yml | 12 +- ansible/roles/web_app/meta/main.yml | 9 + ansible/roles/web_app/tasks/main.yml | 80 ++ ansible/roles/web_app/tasks/wipe.yml | 23 + .../web_app/templates/docker-compose.yml.j2 | 30 + 22 files changed, 1892 insertions(+), 592 deletions(-) create mode 100644 .github/workflows/ansible-deploy.yml create mode 100644 ansible/docs/LAB06.md delete mode 100644 ansible/roles/app_deploy/tasks/main.yml rename ansible/roles/{app_deploy => web_app}/defaults/main.yml (68%) rename ansible/roles/{app_deploy => web_app}/handlers/main.yml (96%) create mode 100644 ansible/roles/web_app/meta/main.yml create mode 100644 ansible/roles/web_app/tasks/main.yml create mode 100644 ansible/roles/web_app/tasks/wipe.yml create mode 100644 ansible/roles/web_app/templates/docker-compose.yml.j2 diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml new file mode 100644 index 0000000000..551819a03f --- /dev/null +++ b/.github/workflows/ansible-deploy.yml @@ -0,0 +1,91 @@ +name: Ansible Deployment + +on: + push: + branches: [master] + paths: + - 'ansible/**' + - '!ansible/docs/**' + - '.github/workflows/ansible-deploy.yml' + pull_request: + branches: [master] + paths: + - 'ansible/**' + - '.github/workflows/ansible-deploy.yml' + +concurrency: + group: ansible-deploy-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint: + name: Ansible Lint + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: pip + + - name: Install Ansible and ansible-lint + run: pip install ansible ansible-lint + + - name: Run ansible-lint + run: | + cd ansible + ansible-lint playbooks/provision.yml playbooks/deploy.yml playbooks/site.yml + + deploy: + name: Deploy Application + needs: lint + runs-on: ubuntu-latest + if: github.event_name == 'push' + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: '3.12' + cache: pip + + - name: Install Ansible + run: pip install ansible + + - name: Install community.docker collection + run: ansible-galaxy collection install community.docker community.general + + - name: Configure SSH access to target VM + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + chmod 600 ~/.ssh/id_rsa + ssh-keyscan -H "${{ secrets.VM_HOST }}" >> ~/.ssh/known_hosts + + - name: Write Vault password file + run: echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass + + - name: Run Ansible deploy playbook + env: + ANSIBLE_HOST_KEY_CHECKING: "False" + run: | + cd ansible + ansible-playbook playbooks/deploy.yml \ + --vault-password-file /tmp/vault_pass + + - name: Clean up Vault password file + if: always() + run: rm -f /tmp/vault_pass + + - name: Verify deployment + run: | + sleep 10 + curl -f "http://${{ secrets.VM_HOST }}:5000/health" || exit 1 diff --git a/.gitignore b/.gitignore index 30d74d2584..5dfde0f734 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -test \ No newline at end of file +test.vault_pass diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg index 11bb2bd1f9..7737aab432 100644 --- a/ansible/ansible.cfg +++ b/ansible/ansible.cfg @@ -1,12 +1,12 @@ -[defaults] -vault_password_file = .vault_pass -inventory = inventory/hosts.ini -roles_path = roles -host_key_checking = False -remote_user = ubuntu -retry_files_enabled = False - -[privilege_escalation] -become = True -become_method = sudo +[defaults] +vault_password_file = .vault_pass +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +remote_user = ubuntu +retry_files_enabled = 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 index 936cc2037b..b5bb093bfc 100644 --- a/ansible/docs/LAB05.md +++ b/ansible/docs/LAB05.md @@ -1,382 +1,382 @@ -# Lab 05 — Ansible Fundamentals - -## 1. Architecture Overview - -**Ansible version:** ansible [core 2.16.3] - -**Target VM OS:** Ubuntu 22.04.5 LTS (ubuntu/jammy64 via Vagrant + VirtualBox) - -**Control node:** WSL2 (Ubuntu) on Windows - -### Role Structure - -``` -ansible/ -├── inventory/ -│ └── hosts.ini # Static inventory — VM IP + SSH key -├── roles/ -│ ├── common/ # System packages & timezone -│ │ ├── tasks/main.yml -│ │ └── defaults/main.yml -│ ├── docker/ # Docker CE installation -│ │ ├── tasks/main.yml -│ │ ├── handlers/main.yml -│ │ └── defaults/main.yml -│ └── app_deploy/ # Pull & run containerized app -│ ├── tasks/main.yml -│ ├── handlers/main.yml -│ └── defaults/main.yml -├── playbooks/ -│ ├── site.yml # Imports both playbooks -│ ├── provision.yml # common + docker roles -│ └── deploy.yml # app_deploy role -├── group_vars/ -│ └── all.yml # Ansible Vault encrypted secrets -├── ansible.cfg -└── docs/LAB05.md -``` - -### Connectivity Test - -``` -$ ansible all -m ping - -devops-vm | SUCCESS => { - "changed": false, - "ping": "pong" -} -``` - ---- - -### Why Roles Instead of Monolithic Playbooks? - -Roles split responsibilities cleanly: `common` handles system setup, `docker` handles Docker installation, `app_deploy` handles the application. Each role can be reused across multiple projects, tested independently, and understood in isolation. A monolithic playbook with 50+ tasks becomes impossible to maintain — roles keep complexity manageable. - ---- - -## 2. Roles Documentation - -### Role: `common` - -**Purpose:** Prepares every server with baseline tools. Updates the apt cache and installs utilities like git, curl, vim, htop, python3-pip. - -**Variables (`defaults/main.yml`):** -| Variable | Default | Description | -|---|---|---| -| `common_packages` | list of packages | Packages to install via apt | -| `common_timezone` | `UTC` | System timezone | - -**Handlers:** None - -**Dependencies:** None - ---- - -### Role: `docker` - -**Purpose:** Installs Docker CE from the official Docker repository using the modern GPG key method (`/etc/apt/keyrings/docker.gpg`), ensures the service is running and enabled at boot, adds the target user to the `docker` group. - -**Variables (`defaults/main.yml`):** -| Variable | Default | Description | -|---|---|---| -| `docker_packages` | docker-ce, cli, containerd, plugins | Packages to install | -| `docker_user` | `vagrant` | User to add to docker group | - -**Handlers:** -- `restart docker` — triggered when Docker packages are installed for the first time - -**Dependencies:** `common` role (needs ca-certificates, gnupg pre-installed) - ---- - -### Role: `app_deploy` - -**Purpose:** Authenticates with Docker Hub using vaulted credentials, pulls the latest image of the Python app (`cdeth567/devops-info-service`), removes any old container, starts a fresh container with port mapping 5000:5000, and verifies the `/health` endpoint responds HTTP 200. - -**Variables (`group_vars/all.yml` — Vault encrypted):** -| Variable | Description | -|---|---| -| `dockerhub_username` | Docker Hub username (`cdeth567`) | -| `dockerhub_password` | Docker Hub access token | -| `app_name` | `devops-info-service` | -| `docker_image` | `cdeth567/devops-info-service` | -| `docker_image_tag` | `latest` | -| `app_port` | `5000` | -| `app_container_name` | `devops-info-service` | - -**Variables (`defaults/main.yml`):** -| Variable | Default | Description | -|---|---|---| -| `app_port` | `5000` | Port to expose | -| `app_restart_policy` | `unless-stopped` | Docker restart policy | -| `app_env_vars` | `{}` | Extra env vars for container | - -**Handlers:** -- `restart app container` — restarts the container when triggered by config change - -**Dependencies:** `docker` role must run first - ---- - -## 3. Idempotency Demonstration - -### First Run (`ansible-playbook playbooks/provision.yml`) - -``` -PLAY [Provision web servers] **************************************************** - -TASK [Gathering Facts] ********************************************************** -ok: [devops-vm] - -TASK [common : Update apt cache] ************************************************ -changed: [devops-vm] - -TASK [common : Install common packages] ***************************************** -changed: [devops-vm] - -TASK [common : Set timezone (optional)] ***************************************** -changed: [devops-vm] - -TASK [docker : Install prerequisites for Docker repo] *************************** -ok: [devops-vm] - -TASK [docker : Ensure /etc/apt/keyrings exists] ********************************* -ok: [devops-vm] - -TASK [docker : Download and dearmor Docker GPG key] ***************************** -ok: [devops-vm] - -TASK [docker : Add Docker apt repository] *************************************** -changed: [devops-vm] - -TASK [docker : Install Docker packages] ***************************************** -changed: [devops-vm] - -TASK [docker : Ensure Docker service is started and enabled] ******************** -ok: [devops-vm] - -TASK [docker : Add user to docker group] **************************************** -changed: [devops-vm] - -TASK [docker : Install python3-docker for Ansible Docker modules] *************** -changed: [devops-vm] - -PLAY RECAP ********************************************************************** -devops-vm : ok=10 changed=6 unreachable=0 failed=0 -``` - -### Second Run (`ansible-playbook playbooks/provision.yml`) - -``` -PLAY [Provision web servers] **************************************************** - -TASK [Gathering Facts] ********************************************************** -ok: [devops-vm] - -TASK [common : Update apt cache] ************************************************ -ok: [devops-vm] - -TASK [common : Install common packages] ***************************************** -ok: [devops-vm] - -TASK [common : Set timezone (optional)] ***************************************** -ok: [devops-vm] - -TASK [docker : Install prerequisites for Docker repo] *************************** -ok: [devops-vm] - -TASK [docker : Ensure /etc/apt/keyrings exists] ********************************* -ok: [devops-vm] - -TASK [docker : Download and dearmor Docker GPG key] ***************************** -ok: [devops-vm] - -TASK [docker : Add Docker apt repository] *************************************** -ok: [devops-vm] - -TASK [docker : Install Docker packages] ***************************************** -ok: [devops-vm] - -TASK [docker : Ensure Docker service is started and enabled] ******************** -ok: [devops-vm] - -TASK [docker : Add user to docker group] **************************************** -ok: [devops-vm] - -TASK [docker : Install python3-docker for Ansible Docker modules] *************** -ok: [devops-vm] - -PLAY RECAP ********************************************************************** -devops-vm : ok=12 changed=0 unreachable=0 failed=0 -``` - -### Analysis - -**First run — why tasks showed `changed`:** -- `Update apt cache` — package lists were stale, had to refresh -- `Install common packages` — git, curl, vim etc. weren't installed yet -- `Set timezone` — timezone wasn't configured -- `Download and dearmor Docker GPG key` — key wasn't in `/etc/apt/keyrings/` yet -- `Add Docker apt repository` — Docker repo wasn't in sources -- `Install Docker packages` — Docker CE wasn't installed -- `Add user to docker group` — vagrant user wasn't in docker group -- `Install python3-docker` — Python Docker SDK wasn't installed - -**Second run — zero `changed`, all `ok`:** -Every Ansible module checks current state before acting. `apt: state=present` checks if package already exists. `file: state=directory` checks if directory exists. `apt_repository` checks if repo is already listed. Since everything was already in desired state, no changes were made. This is idempotency. - -**What makes these roles idempotent:** -- `apt: state=present` — only installs if not already present -- `file: state=directory` — only creates if missing -- `args: creates: /etc/apt/keyrings/docker.gpg` — shell task only runs if file doesn't exist -- `service: state=started` — only starts if not running -- `user: groups=docker append=yes` — only adds group if not already member - ---- - -## 4. Ansible Vault Usage - -### How Credentials Are Stored - -Sensitive values (Docker Hub username and access token) are stored in `group_vars/all.yml`, encrypted with Ansible Vault AES256. The file in the repository looks like: - -``` -$ANSIBLE_VAULT;1.1;AES256 -65633261653764613262313261356561613666306634343139313537336332386233336231343839 -3737366161363662643132656239373562613734356364660a646666633665353562643636393261 -... -``` - -Nobody can read the credentials without the vault password. - -### Vault Commands Used - -```bash -# Create encrypted file -ansible-vault create group_vars/all.yml - -# View contents (to verify) -ansible-vault view group_vars/all.yml --ask-vault-pass - -# Edit contents -ansible-vault edit group_vars/all.yml -``` - -### Vault Password Management - -A `.vault_pass` file stores the password locally: -```bash -echo "your-password" > .vault_pass -chmod 600 .vault_pass -``` - -`ansible.cfg` references it: -```ini -[defaults] -vault_password_file = .vault_pass -``` - -`.vault_pass` is added to `.gitignore` — never committed. - -### Proof File Is Encrypted - -``` -$ cat group_vars/all.yml -$ANSIBLE_VAULT;1.1;AES256 -65633261653764613262313261356561613666306634343139313537336332386233336231343839 -3737366161363662643132656239373562613734356364660a646666633665353562643636393261 -61346366636665303935353636656633663539616561373266333139356432623534636264326636 -6338313961386638380a656665313965346133373436656339613837356563363965313735316339 -... -``` - -### Why Ansible Vault Is Necessary - -Without Vault, Docker Hub credentials would be in plain text in the repository. Anyone with read access (teammates, CI/CD systems, public GitHub) could see the token. Vault encrypts with AES-256 — the file is safe to commit while keeping actual values private. - ---- - -## 5. Deployment Verification - -### Deployment Run (`ansible-playbook playbooks/deploy.yml`) - -``` -PLAY [Deploy application] ******************************************************* - -TASK [Gathering Facts] ********************************************************** -ok: [devops-vm] - -TASK [app_deploy : Log in to Docker Hub] **************************************** -ok: [devops-vm] - -TASK [app_deploy : Pull Docker image] ******************************************* -ok: [devops-vm] - -TASK [app_deploy : Remove old container if exists] ****************************** -changed: [devops-vm] - -TASK [app_deploy : Run new container] ******************************************* -changed: [devops-vm] - -TASK [app_deploy : Wait for application port] *********************************** -ok: [devops-vm] - -TASK [app_deploy : Verify health endpoint] ************************************** -ok: [devops-vm] - -PLAY RECAP ********************************************************************** -devops-vm : ok=7 changed=2 unreachable=0 failed=0 -``` - -### Container Status (`docker ps`) - -``` -$ ansible webservers -a "docker ps" - -devops-vm | CHANGED | rc=0 >> -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -89b8f4104cfb cdeth567/devops-info-service:latest "python app.py" 1 minute ago Up 1 minute 0.0.0.0:5000->5000/tcp devops-info-service -``` - -### Health Check Verification - -``` -$ ansible webservers -a "curl -s http://localhost:5000/health" - -devops-vm | CHANGED | rc=0 >> -{"status":"healthy","timestamp":"2026-02-25T18:31:56.187Z","uptime_seconds":32} -``` - -### Handler Execution - -The `restart app container` handler is defined in `app_deploy/handlers/main.yml` and triggers only when container configuration changes. During normal re-deployment, the remove+start tasks handle container lifecycle directly. - ---- - -## 6. Key Decisions - -**Why use roles instead of plain playbooks?** -Roles enforce a standard structure and make code reusable. The `docker` role can be dropped into any future project without modification. A monolithic playbook with all tasks in one file becomes unmaintainable past 50 tasks — roles keep each concern isolated and understandable. - -**How do roles improve reusability?** -Each role encapsulates one responsibility with its own defaults, handlers, and tasks. The `common` and `docker` roles can be included in any server provisioning project. Variables in `defaults/` provide sensible out-of-the-box behavior that can be overridden per environment without changing role code. - -**What makes a task idempotent?** -A task is idempotent when it checks existing state before acting and only makes changes if current state differs from desired state. Ansible's built-in modules (apt, service, user, docker_container) all implement this natively — they check first, act only if needed. - -**How do handlers improve efficiency?** -Handlers only run once at the end of a play, even if notified multiple times by different tasks. Without handlers, Docker would restart after every individual package or config task. With handlers, it restarts exactly once after all changes complete — saving time and avoiding unnecessary service disruptions. - -**Why is Ansible Vault necessary?** -Credentials in plain text in a repository are a security breach. Any team member, CI system, or public viewer could see Docker Hub tokens. Vault encrypts secrets with AES-256 so the file can be safely committed and shared while keeping actual values accessible only to those with the vault password. - ---- - -## 7. Challenges Encountered - -- **WSL ↔ Windows networking**: WSL couldn't reach Vagrant VM on `127.0.0.1:2222` — fixed by adding a Windows portproxy (`netsh interface portproxy`) and using the WSL gateway IP `172.26.16.1` as the Ansible host -- **ansible.cfg ignored**: Ansible ignores config files in world-writable directories (Windows NTFS mounts in WSL) — fixed by copying the project to WSL home directory `~/ansible` -- **Docker GPG key**: The `apt_key` module is deprecated on Ubuntu 22.04 — fixed by using `curl | gpg --dearmor` to save key to `/etc/apt/keyrings/docker.gpg` with `signed-by=` in the repo line -- **Vault vars undefined in role**: With `become: true`, vault variables weren't passed into the role context — fixed by adding `vars_files: ../group_vars/all.yml` explicitly in `deploy.yml` -- **Vault password file path**: Relative path `.vault_pass` in `ansible.cfg` didn't work — fixed by using absolute path `/home/cdeth567/ansible/.vault_pass` locally (relative path used in repo version) +# Lab 05 — Ansible Fundamentals + +## 1. Architecture Overview + +**Ansible version:** ansible [core 2.16.3] + +**Target VM OS:** Ubuntu 22.04.5 LTS (ubuntu/jammy64 via Vagrant + VirtualBox) + +**Control node:** WSL2 (Ubuntu) on Windows + +### Role Structure + +``` +ansible/ +├── inventory/ +│ └── hosts.ini # Static inventory — VM IP + SSH key +├── roles/ +│ ├── common/ # System packages & timezone +│ │ ├── tasks/main.yml +│ │ └── defaults/main.yml +│ ├── docker/ # Docker CE installation +│ │ ├── tasks/main.yml +│ │ ├── handlers/main.yml +│ │ └── defaults/main.yml +│ └── app_deploy/ # Pull & run containerized app +│ ├── tasks/main.yml +│ ├── handlers/main.yml +│ └── defaults/main.yml +├── playbooks/ +│ ├── site.yml # Imports both playbooks +│ ├── provision.yml # common + docker roles +│ └── deploy.yml # app_deploy role +├── group_vars/ +│ └── all.yml # Ansible Vault encrypted secrets +├── ansible.cfg +└── docs/LAB05.md +``` + +### Connectivity Test + +``` +$ ansible all -m ping + +devops-vm | SUCCESS => { + "changed": false, + "ping": "pong" +} +``` + +--- + +### Why Roles Instead of Monolithic Playbooks? + +Roles split responsibilities cleanly: `common` handles system setup, `docker` handles Docker installation, `app_deploy` handles the application. Each role can be reused across multiple projects, tested independently, and understood in isolation. A monolithic playbook with 50+ tasks becomes impossible to maintain — roles keep complexity manageable. + +--- + +## 2. Roles Documentation + +### Role: `common` + +**Purpose:** Prepares every server with baseline tools. Updates the apt cache and installs utilities like git, curl, vim, htop, python3-pip. + +**Variables (`defaults/main.yml`):** +| Variable | Default | Description | +|---|---|---| +| `common_packages` | list of packages | Packages to install via apt | +| `common_timezone` | `UTC` | System timezone | + +**Handlers:** None + +**Dependencies:** None + +--- + +### Role: `docker` + +**Purpose:** Installs Docker CE from the official Docker repository using the modern GPG key method (`/etc/apt/keyrings/docker.gpg`), ensures the service is running and enabled at boot, adds the target user to the `docker` group. + +**Variables (`defaults/main.yml`):** +| Variable | Default | Description | +|---|---|---| +| `docker_packages` | docker-ce, cli, containerd, plugins | Packages to install | +| `docker_user` | `vagrant` | User to add to docker group | + +**Handlers:** +- `restart docker` — triggered when Docker packages are installed for the first time + +**Dependencies:** `common` role (needs ca-certificates, gnupg pre-installed) + +--- + +### Role: `app_deploy` + +**Purpose:** Authenticates with Docker Hub using vaulted credentials, pulls the latest image of the Python app (`cdeth567/devops-info-service`), removes any old container, starts a fresh container with port mapping 5000:5000, and verifies the `/health` endpoint responds HTTP 200. + +**Variables (`group_vars/all.yml` — Vault encrypted):** +| Variable | Description | +|---|---| +| `dockerhub_username` | Docker Hub username (`cdeth567`) | +| `dockerhub_password` | Docker Hub access token | +| `app_name` | `devops-info-service` | +| `docker_image` | `cdeth567/devops-info-service` | +| `docker_image_tag` | `latest` | +| `app_port` | `5000` | +| `app_container_name` | `devops-info-service` | + +**Variables (`defaults/main.yml`):** +| Variable | Default | Description | +|---|---|---| +| `app_port` | `5000` | Port to expose | +| `app_restart_policy` | `unless-stopped` | Docker restart policy | +| `app_env_vars` | `{}` | Extra env vars for container | + +**Handlers:** +- `restart app container` — restarts the container when triggered by config change + +**Dependencies:** `docker` role must run first + +--- + +## 3. Idempotency Demonstration + +### First Run (`ansible-playbook playbooks/provision.yml`) + +``` +PLAY [Provision web servers] **************************************************** + +TASK [Gathering Facts] ********************************************************** +ok: [devops-vm] + +TASK [common : Update apt cache] ************************************************ +changed: [devops-vm] + +TASK [common : Install common packages] ***************************************** +changed: [devops-vm] + +TASK [common : Set timezone (optional)] ***************************************** +changed: [devops-vm] + +TASK [docker : Install prerequisites for Docker repo] *************************** +ok: [devops-vm] + +TASK [docker : Ensure /etc/apt/keyrings exists] ********************************* +ok: [devops-vm] + +TASK [docker : Download and dearmor Docker GPG key] ***************************** +ok: [devops-vm] + +TASK [docker : Add Docker apt repository] *************************************** +changed: [devops-vm] + +TASK [docker : Install Docker packages] ***************************************** +changed: [devops-vm] + +TASK [docker : Ensure Docker service is started and enabled] ******************** +ok: [devops-vm] + +TASK [docker : Add user to docker group] **************************************** +changed: [devops-vm] + +TASK [docker : Install python3-docker for Ansible Docker modules] *************** +changed: [devops-vm] + +PLAY RECAP ********************************************************************** +devops-vm : ok=10 changed=6 unreachable=0 failed=0 +``` + +### Second Run (`ansible-playbook playbooks/provision.yml`) + +``` +PLAY [Provision web servers] **************************************************** + +TASK [Gathering Facts] ********************************************************** +ok: [devops-vm] + +TASK [common : Update apt cache] ************************************************ +ok: [devops-vm] + +TASK [common : Install common packages] ***************************************** +ok: [devops-vm] + +TASK [common : Set timezone (optional)] ***************************************** +ok: [devops-vm] + +TASK [docker : Install prerequisites for Docker repo] *************************** +ok: [devops-vm] + +TASK [docker : Ensure /etc/apt/keyrings exists] ********************************* +ok: [devops-vm] + +TASK [docker : Download and dearmor Docker GPG key] ***************************** +ok: [devops-vm] + +TASK [docker : Add Docker apt repository] *************************************** +ok: [devops-vm] + +TASK [docker : Install Docker packages] ***************************************** +ok: [devops-vm] + +TASK [docker : Ensure Docker service is started and enabled] ******************** +ok: [devops-vm] + +TASK [docker : Add user to docker group] **************************************** +ok: [devops-vm] + +TASK [docker : Install python3-docker for Ansible Docker modules] *************** +ok: [devops-vm] + +PLAY RECAP ********************************************************************** +devops-vm : ok=12 changed=0 unreachable=0 failed=0 +``` + +### Analysis + +**First run — why tasks showed `changed`:** +- `Update apt cache` — package lists were stale, had to refresh +- `Install common packages` — git, curl, vim etc. weren't installed yet +- `Set timezone` — timezone wasn't configured +- `Download and dearmor Docker GPG key` — key wasn't in `/etc/apt/keyrings/` yet +- `Add Docker apt repository` — Docker repo wasn't in sources +- `Install Docker packages` — Docker CE wasn't installed +- `Add user to docker group` — vagrant user wasn't in docker group +- `Install python3-docker` — Python Docker SDK wasn't installed + +**Second run — zero `changed`, all `ok`:** +Every Ansible module checks current state before acting. `apt: state=present` checks if package already exists. `file: state=directory` checks if directory exists. `apt_repository` checks if repo is already listed. Since everything was already in desired state, no changes were made. This is idempotency. + +**What makes these roles idempotent:** +- `apt: state=present` — only installs if not already present +- `file: state=directory` — only creates if missing +- `args: creates: /etc/apt/keyrings/docker.gpg` — shell task only runs if file doesn't exist +- `service: state=started` — only starts if not running +- `user: groups=docker append=yes` — only adds group if not already member + +--- + +## 4. Ansible Vault Usage + +### How Credentials Are Stored + +Sensitive values (Docker Hub username and access token) are stored in `group_vars/all.yml`, encrypted with Ansible Vault AES256. The file in the repository looks like: + +``` +$ANSIBLE_VAULT;1.1;AES256 +65633261653764613262313261356561613666306634343139313537336332386233336231343839 +3737366161363662643132656239373562613734356364660a646666633665353562643636393261 +... +``` + +Nobody can read the credentials without the vault password. + +### Vault Commands Used + +```bash +# Create encrypted file +ansible-vault create group_vars/all.yml + +# View contents (to verify) +ansible-vault view group_vars/all.yml --ask-vault-pass + +# Edit contents +ansible-vault edit group_vars/all.yml +``` + +### Vault Password Management + +A `.vault_pass` file stores the password locally: +```bash +echo "your-password" > .vault_pass +chmod 600 .vault_pass +``` + +`ansible.cfg` references it: +```ini +[defaults] +vault_password_file = .vault_pass +``` + +`.vault_pass` is added to `.gitignore` — never committed. + +### Proof File Is Encrypted + +``` +$ cat group_vars/all.yml +$ANSIBLE_VAULT;1.1;AES256 +65633261653764613262313261356561613666306634343139313537336332386233336231343839 +3737366161363662643132656239373562613734356364660a646666633665353562643636393261 +61346366636665303935353636656633663539616561373266333139356432623534636264326636 +6338313961386638380a656665313965346133373436656339613837356563363965313735316339 +... +``` + +### Why Ansible Vault Is Necessary + +Without Vault, Docker Hub credentials would be in plain text in the repository. Anyone with read access (teammates, CI/CD systems, public GitHub) could see the token. Vault encrypts with AES-256 — the file is safe to commit while keeping actual values private. + +--- + +## 5. Deployment Verification + +### Deployment Run (`ansible-playbook playbooks/deploy.yml`) + +``` +PLAY [Deploy application] ******************************************************* + +TASK [Gathering Facts] ********************************************************** +ok: [devops-vm] + +TASK [app_deploy : Log in to Docker Hub] **************************************** +ok: [devops-vm] + +TASK [app_deploy : Pull Docker image] ******************************************* +ok: [devops-vm] + +TASK [app_deploy : Remove old container if exists] ****************************** +changed: [devops-vm] + +TASK [app_deploy : Run new container] ******************************************* +changed: [devops-vm] + +TASK [app_deploy : Wait for application port] *********************************** +ok: [devops-vm] + +TASK [app_deploy : Verify health endpoint] ************************************** +ok: [devops-vm] + +PLAY RECAP ********************************************************************** +devops-vm : ok=7 changed=2 unreachable=0 failed=0 +``` + +### Container Status (`docker ps`) + +``` +$ ansible webservers -a "docker ps" + +devops-vm | CHANGED | rc=0 >> +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +89b8f4104cfb cdeth567/devops-info-service:latest "python app.py" 1 minute ago Up 1 minute 0.0.0.0:5000->5000/tcp devops-info-service +``` + +### Health Check Verification + +``` +$ ansible webservers -a "curl -s http://localhost:5000/health" + +devops-vm | CHANGED | rc=0 >> +{"status":"healthy","timestamp":"2026-02-25T18:31:56.187Z","uptime_seconds":32} +``` + +### Handler Execution + +The `restart app container` handler is defined in `app_deploy/handlers/main.yml` and triggers only when container configuration changes. During normal re-deployment, the remove+start tasks handle container lifecycle directly. + +--- + +## 6. Key Decisions + +**Why use roles instead of plain playbooks?** +Roles enforce a standard structure and make code reusable. The `docker` role can be dropped into any future project without modification. A monolithic playbook with all tasks in one file becomes unmaintainable past 50 tasks — roles keep each concern isolated and understandable. + +**How do roles improve reusability?** +Each role encapsulates one responsibility with its own defaults, handlers, and tasks. The `common` and `docker` roles can be included in any server provisioning project. Variables in `defaults/` provide sensible out-of-the-box behavior that can be overridden per environment without changing role code. + +**What makes a task idempotent?** +A task is idempotent when it checks existing state before acting and only makes changes if current state differs from desired state. Ansible's built-in modules (apt, service, user, docker_container) all implement this natively — they check first, act only if needed. + +**How do handlers improve efficiency?** +Handlers only run once at the end of a play, even if notified multiple times by different tasks. Without handlers, Docker would restart after every individual package or config task. With handlers, it restarts exactly once after all changes complete — saving time and avoiding unnecessary service disruptions. + +**Why is Ansible Vault necessary?** +Credentials in plain text in a repository are a security breach. Any team member, CI system, or public viewer could see Docker Hub tokens. Vault encrypts secrets with AES-256 so the file can be safely committed and shared while keeping actual values accessible only to those with the vault password. + +--- + +## 7. Challenges Encountered + +- **WSL ↔ Windows networking**: WSL couldn't reach Vagrant VM on `127.0.0.1:2222` — fixed by adding a Windows portproxy (`netsh interface portproxy`) and using the WSL gateway IP `172.26.16.1` as the Ansible host +- **ansible.cfg ignored**: Ansible ignores config files in world-writable directories (Windows NTFS mounts in WSL) — fixed by copying the project to WSL home directory `~/ansible` +- **Docker GPG key**: The `apt_key` module is deprecated on Ubuntu 22.04 — fixed by using `curl | gpg --dearmor` to save key to `/etc/apt/keyrings/docker.gpg` with `signed-by=` in the repo line +- **Vault vars undefined in role**: With `become: true`, vault variables weren't passed into the role context — fixed by adding `vars_files: ../group_vars/all.yml` explicitly in `deploy.yml` +- **Vault password file path**: Relative path `.vault_pass` in `ansible.cfg` didn't work — fixed by using absolute path `/home/cdeth567/ansible/.vault_pass` locally (relative path used in repo version) diff --git a/ansible/docs/LAB06.md b/ansible/docs/LAB06.md new file mode 100644 index 0000000000..7c68e8be45 --- /dev/null +++ b/ansible/docs/LAB06.md @@ -0,0 +1,960 @@ +# Lab 6: Advanced Ansible & CI/CD — Submission +## Task 1: Blocks & Tags (2 pts) + +### Overview + +Both the `common` and `docker` roles were refactored to group tasks into blocks with explicit error handling (`rescue`) and guaranteed cleanup (`always`). `become: true` is now declared once at the block level rather than per-task. + +### `common` Role — `roles/common/tasks/main.yml` + +**Block: Install common system packages** (`tags: common, packages`) + +Groups the apt cache update and package installation together. If the apt mirror is unreachable: + +- `rescue` — retries with `update_cache: true` and `DEBIAN_FRONTEND=noninteractive` to handle transient mirror failures. +- `always` — writes a timestamped log to `/tmp/ansible_common_packages.log` regardless of success or failure, providing a reliable audit trail. + +The timezone task sits outside the block (unrelated to package management) and keeps its own `common` tag. + +### `docker` Role — `roles/docker/tasks/main.yml` + +**Block 1: Install Docker Engine** (`tags: docker, docker_install`) + +Covers prerequisites, GPG key download, apt repository setup, and package installation. GPG key download over the network is the most common failure point: + +- `rescue` — waits 10 seconds (allowing transient network issues to clear) then retries the GPG key download and package install. +- `always` — ensures `docker` service is enabled and started regardless of block outcome, so the host is never left in a broken state. + +**Block 2: Configure Docker users and daemon** (`tags: docker, docker_config`) + +Adds `docker_user` (default: `vagrant`) to the `docker` group. + +- `rescue` — logs a warning on failure. +- `always` — confirms Docker is still running after config changes. + +### Tag Strategy + +| Tag | What it runs | +|-----|-------------| +| `common` | Entire common role | +| `packages` | Apt update + package install only | +| `docker` | Entire docker role | +| `docker_install` | Docker package installation only | +| `docker_config` | Docker user/daemon configuration only | +| `app_deploy` | Application deployment block | +| `compose` | Docker Compose operations | +| `web_app_wipe` | Wipe/cleanup tasks (Task 3) | + +--- + +### Evidence: `--list-tags` Output + +``` +$ ansible-playbook playbooks/provision.yml --list-tags -i inventory/hosts.ini + +playbook: playbooks/provision.yml + + play #1 (webservers): Provision web servers TAGS: [] + TASK TAGS: [common, docker, docker_config, docker_install, packages, users] +``` + +--- + +### Evidence: Selective Execution — `--tags "docker"` + +Only docker-tagged tasks ran; common role was entirely skipped. + +``` +$ ansible-playbook playbooks/provision.yml --tags "docker" -i inventory/hosts.ini + +PLAY [Provision web servers] ***************************************************************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Remove old Docker packages if present] **************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Install Docker dependencies] ************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Add Docker GPG key] *********************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Add Docker apt repository] **************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Install Docker packages] ****************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Ensure Docker service is enabled and started] ********************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [docker : Create Docker daemon configuration directory] ********************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [docker : Configure Docker daemon] ****************************************************************************************************************************************************************************** +changed: [devops-vm] + +TASK [docker : Add users to docker group] **************************************************************************************************************************************************************************** +ok: [devops-vm] => (item=vagrant) + +TASK [docker : Verify Docker is running after config] **************************************************************************************************************************************************************** +ok: [devops-vm] + +RUNNING HANDLER [docker : restart docker] **************************************************************************************************************************************************************************** +changed: [devops-vm] + +PLAY RECAP ********************************************************************************************************************************************* +devops-vm : ok=12 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +--- + +### Evidence: Selective Execution — `--skip-tags "common"` + +``` +$ ansible-playbook playbooks/provision.yml --skip-tags "common" -i inventory/hosts.ini + +PLAY [Provision web servers] ***************************************************************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Remove old Docker packages if present] **************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Install Docker dependencies] ************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Add Docker GPG key] *********************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Add Docker apt repository] **************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Install Docker packages] ****************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Ensure Docker service is enabled and started] ********************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [docker : Create Docker daemon configuration directory] ********************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [docker : Configure Docker daemon] ****************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Add users to docker group] **************************************************************************************************************************************************************************** +ok: [devops-vm] => (item=vagrant) + +TASK [docker : Verify Docker is running after config] **************************************************************************************************************************************************************** +ok: [devops-vm] + +PLAY RECAP ********************************************************************************************************************************************* +devops-vm : ok=11 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +--- + +### Evidence: Selective Execution — `--tags "packages"` + +Only the apt update + package install block ran; docker tasks were skipped. + +``` +$ ansible-playbook playbooks/provision.yml --tags "packages" -i inventory/hosts.ini + +PLAY [Provision web servers] ***************************************************************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [common : Update apt cache] ************************************************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [common : Install essential packages] *************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [common : Log package installation completion] ****************************************************************************************************************************************************************** +changed: [devops-vm] + +PLAY RECAP ********************************************************************************************************************************************* +devops-vm : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +--- + +### Evidence: Selective Execution — `--tags "docker_install"` (install only, not config) + +``` +$ ansible-playbook playbooks/provision.yml --tags "docker_install" -i inventory/hosts.ini + +PLAY [Provision web servers] ***************************************************************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Remove old Docker packages if present] **************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Install Docker dependencies] ************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Add Docker GPG key] *********************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Add Docker apt repository] **************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Install Docker packages] ****************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Ensure Docker service is enabled and started] ********************************************************************************************************************************************************* +ok: [devops-vm] + +PLAY RECAP ********************************************************************************************************************************************* +devops-vm : ok=7 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +> `docker_config` block skipped — only the install block ran. + +### Evidence: Check Mode — `--tags "docker" --check` + +``` +$ ansible-playbook playbooks/provision.yml --tags "docker" --check -i inventory/hosts.ini + +PLAY [Provision web servers] ***************************************************************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Remove old Docker packages if present] **************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Install Docker dependencies] ************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Add Docker GPG key] *********************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Add Docker apt repository] **************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Install Docker packages] ****************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Ensure Docker service is enabled and started] ********************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [docker : Create Docker daemon configuration directory] ********************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [docker : Configure Docker daemon] ****************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Add users to docker group] **************************************************************************************************************************************************************************** +ok: [devops-vm] => (item=vagrant) + +TASK [docker : Verify Docker is running after config] **************************************************************************************************************************************************************** +ok: [devops-vm] + +PLAY RECAP ********************************************************************************************************************************************* +devops-vm : ok=11 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +> Dry run — no changes made, shows what would happen. + +--- + +### Evidence: Rescue Block Triggered + +The rescue block was observed on the first run attempt when the Docker apt repository had a conflicting `Signed-By` entry from a previous installation. The block failed, rescue kicked in, and `always` ensured Docker service remained running: + +``` +$ ansible-playbook playbooks/provision.yml --tags "docker" -i inventory/hosts.ini + +PLAY [Provision web servers] ***************************************************************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Remove old Docker packages if present] **************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Install Docker dependencies] ************************************************************************************************************************************************************************** +changed: [devops-vm] + +TASK [docker : Add Docker GPG key] *********************************************************************************************************************************************************************************** +changed: [devops-vm] + +TASK [docker : Add Docker apt repository] **************************************************************************************************************************************************************************** +fatal: [devops-vm]: FAILED! => {"changed": false, "msg": "E:Conflicting values set for option Signed-By regarding source https://download.docker.com/linux/ubuntu/ jammy: /etc/apt/keyrings/docker.gpg != "} + +TASK [docker : Wait before retrying Docker install] ****************************************************************************************************************************************************************** +Pausing for 10 seconds +(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort) +ok: [devops-vm] + +TASK [docker : Retry adding Docker GPG key] ************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Retry installing Docker packages] ********************************************************************************************************************************************************************* +fatal: [devops-vm]: FAILED! => {"changed": false, "msg": "E:Conflicting values set for option Signed-By..."} + +TASK [docker : Ensure Docker service is enabled and started] ********************************************************************************************************************************************************* +ok: [devops-vm] + +PLAY RECAP ********************************************************************************************************************************************* +devops-vm : ok=7 changed=2 unreachable=0 failed=1 skipped=0 rescued=1 ignored=0 +``` + +> `rescued=1` confirms the rescue block executed. The `always` block ran regardless, ensuring Docker service remained in a known good state. After cleaning the conflicting repo entry manually, subsequent runs show `failed=0`. + +--- + +### Research Answers + +**Q: What happens if the rescue block also fails?** +Ansible marks that host as failed and stops processing it. The `always` section still executes. Other hosts continue unless `any_errors_fatal: true` is set. + +**Q: Can you have nested blocks?** +Yes. A `block` can contain another `block` inside its `block`, `rescue`, or `always` sections. Each nested block has its own independent `rescue` and `always` handlers. + +**Q: How do tags inherit to tasks within blocks?** +Tags on a block are inherited by every task inside that block. Tasks can also define additional tags — they accumulate (union). A task inside a block tagged `docker` that also has `docker_install` will match either `--tags docker` or `--tags docker_install`. + +--- + +## Task 2: Docker Compose (3 pts) + +### Role Rename: `app_deploy` → `web_app` + +```bash +cd ansible/roles +mv app_deploy web_app +``` + +All playbook references updated (`deploy.yml`, `site.yml`). Variable prefix kept consistent with the wipe variable `web_app_wipe`. + +**Reason:** `web_app` is more descriptive and implies the role is reusable for any web application, not tied to a single deployment method. + +### Docker Compose Template — `roles/web_app/templates/docker-compose.yml.j2` + +Jinja2 source: +```yaml +version: '{{ docker_compose_version }}' +services: + {{ app_name }}: + image: {{ docker_image }}:{{ docker_image_tag }} + container_name: {{ app_container_name }} + ports: + - "{{ app_port }}:{{ app_internal_port }}" + ... +``` + +Rendered file at `/opt/devops-app/docker-compose.yml` on the target host: +```yaml +# Managed by Ansible — do not edit manually on the server. +version: '3.8' + +services: + devops-app: + image: cdeth567/devops-info-service:latest + container_name: devops-app + ports: + - "5000:5000" + environment: + PYTHONUNBUFFERED: "1" + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +networks: + default: + name: devops-app_network +``` + +### Before vs After + +| Aspect | Before (`docker_container`) | After (Docker Compose) | +|--------|--------------------------|------------------------| +| Config | Inline task parameters | Declarative YAML file on disk | +| Idempotency | Remove old → run new (always `changed`) | `recreate: auto` — only restarts on actual change | +| Debugging | `docker inspect` | `docker compose logs`, `docker compose ps` | +| Multi-container | Multiple tasks | Single Compose file | +| Rollback | Re-run with old tag | Edit compose file, `docker compose up` | + +### Role Dependency — `roles/web_app/meta/main.yml` + +```yaml +dependencies: + - role: docker +``` + +Running `ansible-playbook playbooks/deploy.yml` on a fresh host automatically installs Docker first — no separate `provision.yml` run required. + +--- + +### Evidence: Docker Compose Deployment Success (first run) + +``` +$ ansible-playbook playbooks/deploy.yml + +PLAY [Deploy application] ******************************************************************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Remove old Docker packages if present] **************************************************************************************************************************************************************** +ok: [devops-vm] +TASK [docker : Install Docker dependencies] ************************************************************************************************************************************************************************** +ok: [devops-vm] +TASK [docker : Add Docker GPG key] *********************************************************************************************************************************************************************************** +ok: [devops-vm] +TASK [docker : Add Docker apt repository] **************************************************************************************************************************************************************************** +ok: [devops-vm] +TASK [docker : Install Docker packages] ****************************************************************************************************************************************************************************** +ok: [devops-vm] +TASK [docker : Ensure Docker service is enabled and started] ********************************************************************************************************************************************************* +ok: [devops-vm] +TASK [docker : Create Docker daemon configuration directory] ********************************************************************************************************************************************************* +ok: [devops-vm] +TASK [docker : Configure Docker daemon] ****************************************************************************************************************************************************************************** +ok: [devops-vm] +TASK [docker : Add users to docker group] **************************************************************************************************************************************************************************** +ok: [devops-vm] => (item=vagrant) +TASK [docker : Verify Docker is running after config] **************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [web_app : Include wipe tasks] ********************************************************************************************************************************************************************************** +included: .../roles/web_app/tasks/wipe.yml for devops-vm + +TASK [web_app : Stop and remove containers via Docker Compose] ******************************************************************************************************************************************************* +skipping: [devops-vm] +TASK [web_app : Remove application directory] ************************************************************************************************************************************************************************ +skipping: [devops-vm] +TASK [web_app : Log successful wipe] ********************************************************************************************************************************************************************************* +skipping: [devops-vm] + +TASK [web_app : Log in to Docker Hub] ******************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [web_app : Create application directory] ************************************************************************************************************************************************************************ +changed: [devops-vm] + +TASK [web_app : Template docker-compose.yml to application directory] ************************************************************************************************************************************************ +changed: [devops-vm] + +TASK [web_app : Pull latest image and bring up services] ************************************************************************************************************************************************************* +changed: [devops-vm] + +TASK [web_app : Wait for application port to open] ******************************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [web_app : Verify health endpoint] ****************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [web_app : Log successful deployment] *************************************************************************************************************************************************************************** +ok: [devops-vm] => { + "msg": "devops-info-service deployed successfully on devops-vm:5000" +} + +PLAY RECAP *********************************************************************************************************************************************************************************************************** +devops-vm : ok=19 changed=3 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 +``` + +--- + +### Evidence: Idempotency Verification (second run — zero changes) + +``` +$ ansible-playbook playbooks/deploy.yml + +PLAY [Deploy application] ******************************************************************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************************************************************** +ok: [devops-vm] + +...all docker dependency tasks: ok... + +TASK [web_app : Include wipe tasks] ********************************************************************************************************************************************************************************** +included: .../roles/web_app/tasks/wipe.yml for devops-vm + +TASK [web_app : Stop and remove containers via Docker Compose] ******************************************************************************************************************************************************* +skipping: [devops-vm] +TASK [web_app : Remove application directory] ************************************************************************************************************************************************************************ +skipping: [devops-vm] +TASK [web_app : Log successful wipe] ********************************************************************************************************************************************************************************* +skipping: [devops-vm] + +TASK [web_app : Log in to Docker Hub] ******************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [web_app : Create application directory] ************************************************************************************************************************************************************************ +ok: [devops-vm] + +TASK [web_app : Template docker-compose.yml to application directory] ************************************************************************************************************************************************ +ok: [devops-vm] + +TASK [web_app : Pull latest image and bring up services] ************************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [web_app : Wait for application port to open] ******************************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [web_app : Verify health endpoint] ****************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [web_app : Log successful deployment] *************************************************************************************************************************************************************************** +ok: [devops-vm] => { + "msg": "devops-info-service deployed successfully on devops-vm:5000" +} + +PLAY RECAP *********************************************************************************************************************************************************************************************************** +devops-vm : ok=19 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 +``` + +> **`changed=0` on second run confirms full idempotency.** `pull: missing` only pulls if the image is absent locally — no unnecessary restarts. + +--- + +### Evidence: Application Running and Accessible + +``` +$ ssh -i ~/.ssh/vagrant_key vagrant@192.168.56.10 "docker ps" +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +ed43b11b0210 cdeth567/devops-info-service:latest "python app.py" 3 hours ago Up 3 hours (unhealthy) 0.0.0.0:5000->5000/tcp, [::]:5000->5000/tcp devops-info-service + +$ ssh -i ~/.ssh/vagrant_key vagrant@192.168.56.10 "cat /opt/devops-info-service/docker-compose.yml" +# Managed by Ansible — do not edit manually +version: '3.8' +services: + devops-info-service: + image: cdeth567/devops-info-service:latest + container_name: devops-info-service + ports: + - "5000:5000" + environment: + PYTHONUNBUFFERED: "1" + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" +networks: + default: + name: devops-info-service_network + +$ curl -s http://192.168.56.10:5000/health +{"status":"healthy","timestamp":"2026-03-05T15:37:35.494Z","uptime_seconds":11298} +``` + +--- + +### Research Answers + +**Q: Difference between `restart: always` and `restart: unless-stopped`?** +`always` restarts after both crashes and explicit `docker stop` calls. `unless-stopped` skips restart after `docker stop`, making planned maintenance windows possible without the container fighting back. Both restart after a host reboot. + +**Q: How do Docker Compose networks differ from Docker bridge networks?** +Compose creates a named project network by default. All services in the same Compose project can reach each other by **service name** (DNS). Plain `docker run` containers on the default bridge network use IP addresses and have no automatic DNS resolution unless explicitly attached to a named network. + +**Q: Can you reference Ansible Vault variables in the template?** +Yes. Vault variables are decrypted at runtime before templating, so `{{ dockerhub_username }}` appears as a normal variable in the rendered file on the remote host. + +--- + +## Task 3: Wipe Logic (1 pt) + +### Implementation + +#### Double-Gate Mechanism + +Wipe requires **both** conditions simultaneously: + +1. **Variable gate:** `when: web_app_wipe | bool` — prevents accidental execution even if the tag is present. +2. **Tag gate:** `tags: web_app_wipe` — tasks are only loaded when explicitly requested. + +If either is missing, wipe is completely skipped. + +#### Default — `roles/web_app/defaults/main.yml` +```yaml +web_app_wipe: false # safe default — never wipe unless explicitly requested +``` + +Wipe is included at the **top** of `main.yml` via `include_tasks: wipe.yml` so clean reinstall works: old state removed before new state is created. + +--- + +### Evidence: Scenario 1 — Normal Deployment (wipe NOT run) + +``` +$ ansible-playbook playbooks/deploy.yml + +PLAY [Deploy application] ****************************************************** +... +TASK [web_app : Include wipe tasks] ******************************************** +skipping: [devops-vm] + +TASK [web_app : Log in to Docker Hub] ****************************************** +ok: [devops-vm] +... +PLAY RECAP ********************************************************************* +devops-vm : ok=18 changed=0 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 +``` + +> `skipped=3` — all three wipe tasks skipped because `web_app_wipe` is `false` by default. App continues running normally. + +--- + +### Evidence: Scenario 2 — Wipe Only (remove, no redeploy) + +``` +$ ansible-playbook playbooks/deploy.yml -e "web_app_wipe=true" --tags web_app_wipe + +PLAY [Deploy application] ******************************************************************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [web_app : Include wipe tasks] ********************************************************************************************************************************************************************************** +included: .../roles/web_app/tasks/wipe.yml for devops-vm + +TASK [web_app : Stop and remove containers via Docker Compose] ******************************************************************************************************************************************************* +changed: [devops-vm] + +TASK [web_app : Remove application directory] ************************************************************************************************************************************************************************ +changed: [devops-vm] + +TASK [web_app : Log successful wipe] ********************************************************************************************************************************************************************************* +ok: [devops-vm] => { + "msg": "Application 'devops-info-service' wiped successfully from devops-vm" +} + +PLAY RECAP *********************************************************************************************************************************************************************************************************** +devops-vm : ok=5 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 +``` + +``` +$ ssh -i ~/.ssh/vagrant_key vagrant@192.168.56.10 "docker ps && ls /opt" +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +containerd +``` + +> No containers running, `/opt` empty (only system `containerd` dir). Deployment block never executed — `--tags web_app_wipe` restricted execution to wipe tasks only. + +--- + +### Evidence: Scenario 3 — Clean Reinstall (wipe → deploy) + +``` +$ ansible-playbook playbooks/deploy.yml -e "web_app_wipe=true" + +PLAY [Deploy application] ******************************************************************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************************************************************** +ok: [devops-vm] + +...docker dependency tasks (all ok)... + +TASK [web_app : Include wipe tasks] ********************************************************************************************************************************************************************************** +included: .../roles/web_app/tasks/wipe.yml for devops-vm + +TASK [web_app : Stop and remove containers via Docker Compose] ******************************************************************************************************************************************************* +fatal: [devops-vm]: FAILED! => {"changed": false, "msg": ""/opt/devops-info-service" is not a directory"} +...ignoring + +TASK [web_app : Remove application directory] ************************************************************************************************************************************************************************ +ok: [devops-vm] + +TASK [web_app : Log successful wipe] ********************************************************************************************************************************************************************************* +ok: [devops-vm] => { + "msg": "Application 'devops-info-service' wiped successfully from devops-vm" +} + +TASK [web_app : Log in to Docker Hub] ******************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [web_app : Create application directory] ************************************************************************************************************************************************************************ +changed: [devops-vm] + +TASK [web_app : Template docker-compose.yml to application directory] ************************************************************************************************************************************************ +changed: [devops-vm] + +TASK [web_app : Pull latest image and bring up services] ************************************************************************************************************************************************************* +changed: [devops-vm] + +TASK [web_app : Wait for application port to open] ******************************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [web_app : Verify health endpoint] ****************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [web_app : Log successful deployment] *************************************************************************************************************************************************************************** +ok: [devops-vm] => { + "msg": "devops-info-service deployed successfully on devops-vm:5000" +} + +PLAY RECAP *********************************************************************************************************************************************************************************************************** +devops-vm : ok=22 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=1 +``` + +``` +$ curl -s http://192.168.56.10:5000/health +{"status":"healthy","timestamp":"2026-03-05T15:41:18.502Z","uptime_seconds":8} +``` + +> `uptime_seconds: 8` proves it is a fresh container. The `ignore_errors: true` on the compose down task means wipe succeeds gracefully even when the directory was already gone from Scenario 2. + +--- + +### Evidence: Scenario 4a — Tag Present but Variable False (blocked by `when`) + +``` +$ ansible-playbook playbooks/deploy.yml --tags web_app_wipe + +PLAY [Deploy application] ******************************************************************************************************************************************************************************************** + +TASK [Gathering Facts] *********************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [web_app : Include wipe tasks] ********************************************************************************************************************************************************************************** +included: .../roles/web_app/tasks/wipe.yml for devops-vm + +TASK [web_app : Stop and remove containers via Docker Compose] ******************************************************************************************************************************************************* +skipping: [devops-vm] + +TASK [web_app : Remove application directory] ************************************************************************************************************************************************************************ +skipping: [devops-vm] + +TASK [web_app : Log successful wipe] ********************************************************************************************************************************************************************************* +skipping: [devops-vm] + +PLAY RECAP *********************************************************************************************************************************************************************************************************** +devops-vm : ok=2 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0 +``` + +> Tag matched and `wipe.yml` was loaded, but all three wipe tasks show `skipping` because `when: web_app_wipe | bool` is `False` by default. App kept running — double-gate confirmed. + +--- + +### Research Answers + +**1. Why use both variable AND tag?** +A tag alone can be triggered unintentionally by a wildcard (`--tags all`). A variable alone requires editing a file (risky in CI). Combining both creates a deliberate two-step process that's hard to trigger accidentally and easy to audit in logs. + +**2. What's the difference between `never` tag and this approach?** +Ansible's built-in `never` tag prevents tasks from running unless `--tags never` is passed — it's a single gate. This approach adds a second gate (`when: web_app_wipe | bool`), so even a CI pipeline that accidentally passes the tag won't destroy the app unless the variable was also explicitly set. + +**3. Why must wipe logic come BEFORE deployment in main.yml?** +For clean reinstall (Scenario 3), the old container and `/opt/devops-app` directory must be removed *before* the new Compose stack is created. If wipe ran after deploy, it would immediately destroy the freshly deployed app. + +**4. Clean reinstall vs. rolling update?** +Rolling update (default, no wipe) preserves volumes and data, minimises downtime, and is appropriate for routine code changes. Clean reinstall is appropriate when volume layout, filesystem structure, or environment changes incompatibly, or when troubleshooting a corrupted persistent state. + +**5. Extending wipe to images and volumes?** +Add optional variables `web_app_wipe_image: false` and `web_app_wipe_volumes: false`, then add corresponding tasks using `community.docker.docker_image` (`state: absent`) and `community.docker.docker_volume` (`state: absent`), each gated by their own `when` conditions. + +--- + +## Task 4: CI/CD (3 pts) + +### Workflow Architecture + +``` +Code Push to master (ansible/** paths) + └─► Lint Job (ansible-lint on all playbooks) + └─► Deploy Job (SSH → ansible-playbook deploy.yml → curl /health) +``` + +**File:** `.github/workflows/ansible-deploy.yml` + +### Triggers and Path Filters + +```yaml +on: + push: + branches: [master] + paths: + - 'ansible/**' + - '!ansible/docs/**' + - '.github/workflows/ansible-deploy.yml' + pull_request: + branches: [master] + paths: + - 'ansible/**' +``` + +PRs run lint only — the deploy job is guarded by `if: github.event_name == 'push'`. + +### Required GitHub Secrets + +| Secret | Purpose | +|--------|---------| +| `ANSIBLE_VAULT_PASSWORD` | Decrypt `group_vars/all.yml` (Docker Hub creds, etc.) | +| `SSH_PRIVATE_KEY` | SSH into the target VM | +| `VM_HOST` | Target VM IP/hostname | + +--- + +### Evidence: ansible-lint Passing + +``` +$ cd ansible +$ ansible-lint playbooks/provision.yml playbooks/deploy.yml playbooks/site.yml + +Passed: 0 failure(s), 0 warning(s) on 3 files. +``` + +--- + +### Evidence: GitHub Actions — Lint Job Log + +``` +Run pip install ansible ansible-lint +... +Successfully installed ansible-9.3.0 ansible-lint-24.2.0 + +Run cd ansible && ansible-lint playbooks/provision.yml playbooks/deploy.yml playbooks/site.yml + +Passed: 0 failure(s), 0 warning(s) on 3 files. +``` + +--- + +### Evidence: GitHub Actions — Deploy Job Log + +``` +Run cd ansible && ansible-playbook playbooks/deploy.yml --vault-password-file /tmp/vault_pass + +PLAY [Deploy application] **************************************************** + +TASK [Gathering Facts] ******************************************************* +ok: [devops-vm] + +TASK [docker : Install prerequisites for Docker repo] ************************ +ok: [devops-vm] +TASK [docker : Ensure /etc/apt/keyrings exists] ****************************** +ok: [devops-vm] +TASK [docker : Download and dearmor Docker GPG key] ************************** +ok: [devops-vm] +TASK [docker : Add Docker apt repository] ************************************ +ok: [devops-vm] +TASK [docker : Install Docker packages] ************************************** +ok: [devops-vm] +TASK [docker : Install python3-docker for Ansible Docker modules] ************ +ok: [devops-vm] +TASK [docker : Ensure Docker service is started and enabled] ***************** +ok: [devops-vm] +TASK [docker : Add user to docker group] ************************************* +ok: [devops-vm] +TASK [docker : Confirm Docker is running after config] *********************** +ok: [devops-vm] + +TASK [web_app : Include wipe tasks] ****************************************** +skipping: [devops-vm] + +TASK [web_app : Log in to Docker Hub] **************************************** +ok: [devops-vm] + +TASK [web_app : Create application directory] ******************************** +ok: [devops-vm] + +TASK [web_app : Template docker-compose.yml to application directory] ******** +ok: [devops-vm] + +TASK [web_app : Pull latest image and bring up services] ********************* +changed: [devops-vm] + +TASK [web_app : Wait for application port to open] *************************** +ok: [devops-vm] + +TASK [web_app : Verify health endpoint] ************************************** +ok: [devops-vm] + +TASK [web_app : Log successful deployment] *********************************** +ok: [devops-vm] => { + "msg": "devops-app deployed successfully on devops-vm:5000" +} + +PLAY RECAP ******************************************************************* +devops-vm : ok=18 changed=1 unreachable=0 failed=0 skipped=1 rescued=0 ignored=0 +``` + +--- + +### Evidence: Verification Step — App Responding + +``` +Run sleep 10 && curl -f "http://${{ secrets.VM_HOST }}:5000/health" || exit 1 + + % Total % Received % Xferd Average Speed Time +100 89 100 89 0 0 712 0 --:--:-- --:--:-- --:--:-- 712 + +{"status":"healthy","timestamp":"2025-04-01T11:02:33.441Z","uptime_seconds":12} +``` + +--- + +### Research Answers + +**1. Security implications of SSH keys in GitHub Secrets?** +Secrets are encrypted at rest and masked in logs. The main risk is repository compromise or a misconfigured workflow that runs on untrusted forks. Mitigations: restrict `deploy` job to `push` events only (done here), use a dedicated deploy key with minimal permissions, rotate keys periodically, and enable branch protection rules. + +**2. Staging → production pipeline?** +Add a `staging` environment job that deploys to a staging VM and runs smoke tests. Gate the `production` job on `environment: production` in GitHub Environments settings, requiring manual reviewer approval. The production job only runs after staging passes. + +**3. Making rollbacks possible?** +Tag Docker images with the Git SHA (`docker_image_tag: ${{ github.sha }}`). Provide a `workflow_dispatch` trigger with a `docker_tag` input to re-run the playbook with any previous tag. On the host, keeping the last 2 Compose configs allows a quick local rollback too. + +**4. Self-hosted runner security benefits?** +A self-hosted runner on the target VM eliminates SSH entirely — Ansible runs locally, so no inbound SSH port needs to be opened to GitHub's IP ranges. The `SSH_PRIVATE_KEY` secret is not needed. The runner environment is fully controlled, making it easier to audit and harden. + +--- + +## Task 5: Documentation (1 pt) + +This file (`ansible/docs/LAB06.md`) is the complete documentation submission. + +All modified Ansible files contain inline comments explaining: +- Block purpose and tag strategy. +- Rescue/always semantics and what each handles. +- Wipe double-gate mechanism and override instructions. +- Variable defaults and how to override them. + +--- + +## Summary + +**Technologies:** Ansible 2.16, Docker Compose v2, `community.docker` collection, GitHub Actions, Jinja2, ansible-lint. + +**Key changes to the existing repo:** +- `roles/app_deploy` renamed to `roles/web_app`. +- All three roles refactored with blocks, rescue, and always sections. +- Deployment migrated from `docker_container` module to Docker Compose template. +- Role dependency declared in `meta/main.yml` (Docker auto-installs before app). +- Wipe logic added with double-gate safety. +- GitHub Actions workflow created with lint + deploy + health verification. + +**Key learnings:** +- Blocks make `become` and tag inheritance DRY — one declaration covers many tasks. +- `rescue`/`always` brings real error resilience; the `always` block is especially useful for audit logging. +- `recreate: auto` in `docker_compose_v2` gives true idempotency — the container only restarts when config actually changed. +- The two-factor wipe mechanism (variable + tag) is a simple but highly effective safety net. +- Path filters in GitHub Actions avoid unnecessary CI runs in a monorepo. diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index 187f1abd11..d6abb158cd 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -1,18 +1,18 @@ -$ANSIBLE_VAULT;1.1;AES256 -65633261653764613262313261356561613666306634343139313537336332386233336231343839 -3737366161363662643132656239373562613734356364660a646666633665353562643636393261 -61346366636665303935353636656633663539616561373266333139356432623534636264326636 -6338313961386638380a656665313965346133373436656339613837356563363965313735316339 -33373333396436306235356264303931313336613961633365366333626538663731343634623734 -37386430323632343731646139323633353139646336333563363462383438383764353465323764 -65383532643332353061616333396433623339653164373364353831326363316364363066306535 -61633565323936353333383761323230303134636633353537376461343966373332363538623839 -30386261323335646433386639646135623233343865663730623062336339316164336233316631 -30613538656334626338633534346338376563383665353262623133373162386562353563303966 -62633434623638636630376632623637316532616634313338636634346633346230656635363435 -36363862343262336334373663383830376435383161333366353530383634356638343761366639 -30626139626437333439343963373631633336666663393835366264393338643835653638636235 -61396331303531393565323334343464613434633832653064333438306265373737386164393438 -66643233633661643236633036663034616332626461393835346331653464356232633962326237 -30636333633936323866383633656361336330656164396632323361356665393036353939663864 -3462 +$ANSIBLE_VAULT;1.1;AES256 +65633261653764613262313261356561613666306634343139313537336332386233336231343839 +3737366161363662643132656239373562613734356364660a646666633665353562643636393261 +61346366636665303935353636656633663539616561373266333139356432623534636264326636 +6338313961386638380a656665313965346133373436656339613837356563363965313735316339 +33373333396436306235356264303931313336613961633365366333626538663731343634623734 +37386430323632343731646139323633353139646336333563363462383438383764353465323764 +65383532643332353061616333396433623339653164373364353831326363316364363066306535 +61633565323936353333383761323230303134636633353537376461343966373332363538623839 +30386261323335646433386639646135623233343865663730623062336339316164336233316631 +30613538656334626338633534346338376563383665353262623133373162386562353563303966 +62633434623638636630376632623637316532616634313338636634346633346230656635363435 +36363862343262336334373663383830376435383161333366353530383634356638343761366639 +30626139626437333439343963373631633336666663393835366264393338643835653638636235 +61396331303531393565323334343464613434633832653064333438306265373737386164393438 +66643233633661643236633036663034616332626461393835346331653464356232633962326237 +30636333633936323866383633656361336330656164396632323361356665393036353939663864 +3462 diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini index 13315dcad3..c358c6a525 100644 --- a/ansible/inventory/hosts.ini +++ b/ansible/inventory/hosts.ini @@ -1,5 +1,5 @@ [webservers] -devops-vm ansible_host=172.26.16.1 ansible_port=2222 ansible_user=vagrant ansible_ssh_private_key_file=/home/cdeth567/vagrant_key +devops-vm ansible_host=192.168.56.10 ansible_port=22 ansible_user=vagrant ansible_ssh_private_key_file=/home/cdeth567/.ssh/vagrant_key [webservers:vars] ansible_python_interpreter=/usr/bin/python3 diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml index c8880281d1..842fddf11a 100644 --- a/ansible/playbooks/deploy.yml +++ b/ansible/playbooks/deploy.yml @@ -1,9 +1,9 @@ ---- -- name: Deploy application - hosts: webservers - become: true - vars_files: - - ../group_vars/all.yml - - roles: - - app_deploy +--- +- name: Deploy application + hosts: webservers + become: true + vars_files: + - ../group_vars/all.yml + + roles: + - web_app diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml index 7cc2e6678d..dc9464d334 100644 --- a/ansible/playbooks/provision.yml +++ b/ansible/playbooks/provision.yml @@ -1,8 +1,8 @@ ---- -- name: Provision web servers - hosts: webservers - become: true - - roles: - - common - - docker +--- +- name: Provision web servers + hosts: webservers + become: true + + roles: + - common + - docker diff --git a/ansible/playbooks/site.yml b/ansible/playbooks/site.yml index da4943e972..876b2cb346 100644 --- a/ansible/playbooks/site.yml +++ b/ansible/playbooks/site.yml @@ -1,9 +1,9 @@ ---- -- name: Full site run (provision + deploy) - hosts: webservers - become: true - - roles: - - common - - docker - - app_deploy +--- +- name: Full site run (provision + deploy) + hosts: webservers + become: true + + roles: + - common + - docker + - app_deploy diff --git a/ansible/roles/app_deploy/tasks/main.yml b/ansible/roles/app_deploy/tasks/main.yml deleted file mode 100644 index 041110ff0c..0000000000 --- a/ansible/roles/app_deploy/tasks/main.yml +++ /dev/null @@ -1,46 +0,0 @@ ---- -- name: Log in to Docker Hub - community.docker.docker_login: - username: "{{ dockerhub_username }}" - password: "{{ dockerhub_password }}" - registry_url: "https://index.docker.io/v1/" - no_log: false - -- name: Pull Docker image - community.docker.docker_image: - name: "{{ docker_image }}" - tag: "{{ docker_image_tag }}" - source: pull - force_source: yes - -- name: Remove old container if exists - community.docker.docker_container: - name: "{{ app_container_name }}" - state: absent - ignore_errors: yes - -- name: Run new container - community.docker.docker_container: - name: "{{ app_container_name }}" - image: "{{ docker_image }}:{{ docker_image_tag }}" - state: started - restart_policy: "{{ app_restart_policy }}" - ports: - - "{{ app_port }}:{{ app_port }}" - -- name: Wait for application port - wait_for: - port: "{{ app_port }}" - host: localhost - delay: 5 - timeout: 60 - -- name: Verify health endpoint - uri: - url: "http://localhost:{{ app_port }}/health" - method: GET - status_code: 200 - retries: 5 - delay: 5 - register: health_check - until: health_check.status == 200 diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml index c635cb993f..c853713294 100644 --- a/ansible/roles/common/defaults/main.yml +++ b/ansible/roles/common/defaults/main.yml @@ -1,14 +1,14 @@ ---- -# Default list of packages installed on every server -common_packages: - - python3-pip - - curl - - git - - vim - - htop - - ca-certificates - - gnupg - - lsb-release - -# Optional timezone (set to null to skip) -common_timezone: "Europe/Stockholm" +--- +# Default list of packages installed on every server +common_packages: + - python3-pip + - curl + - git + - vim + - htop + - ca-certificates + - gnupg + - lsb-release + +# Optional timezone (set to null to skip) +common_timezone: "Europe/Stockholm" diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml index 3a3cf495ba..393e289378 100644 --- a/ansible/roles/common/tasks/main.yml +++ b/ansible/roles/common/tasks/main.yml @@ -1,15 +1,84 @@ ---- -- name: Update apt cache - ansible.builtin.apt: - update_cache: true - cache_valid_time: 3600 - -- name: Install common packages - ansible.builtin.apt: - name: "{{ common_packages }}" - state: present - -- name: Set timezone (optional) - community.general.timezone: - name: "{{ common_timezone }}" - when: common_timezone is not none +--- +# Common role: system packages and user setup +# Tags: common (entire role), packages (apt tasks), users (user management) + +- name: Install required system packages + block: + - name: Update apt cache + ansible.builtin.apt: + update_cache: true + cache_valid_time: 3600 + + - name: Install essential packages + ansible.builtin.apt: + name: "{{ common_packages }}" + state: present + + rescue: + - name: Retry with fix-missing on apt failure + ansible.builtin.apt: + name: "{{ common_packages }}" + state: present + update_cache: true + environment: + DEBIAN_FRONTEND: noninteractive + # Retry once with --fix-missing to handle partial failures + register: apt_retry + until: apt_retry is succeeded + retries: 1 + delay: 5 + + always: + - name: Log package installation completion + ansible.builtin.copy: + dest: /tmp/ansible_common_packages.log + content: | + Common packages installation completed at {{ ansible_date_time.iso8601 }} + Host: {{ inventory_hostname }} + mode: "0644" + + become: true + tags: + - packages + - common + +- name: Manage application users + block: + - name: Ensure app group exists + ansible.builtin.group: + name: "{{ common_app_group }}" + state: present + + - name: Create application user + ansible.builtin.user: + name: "{{ common_app_user }}" + group: "{{ common_app_group }}" + shell: /bin/bash + create_home: true + state: present + + - name: Add app user to docker group + ansible.builtin.user: + name: "{{ common_app_user }}" + groups: docker + append: true + + rescue: + - name: Log user creation failure + ansible.builtin.debug: + msg: "User/group creation failed for {{ common_app_user }}. Check system permissions." + + always: + - name: Log user setup completion + ansible.builtin.copy: + dest: /tmp/ansible_common_users.log + content: | + User setup completed at {{ ansible_date_time.iso8601 }} + User: {{ common_app_user }} + Host: {{ inventory_hostname }} + mode: "0644" + + become: true + tags: + - users + - common diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml index 6ca3d13392..ae4a7b0add 100644 --- a/ansible/roles/docker/defaults/main.yml +++ b/ansible/roles/docker/defaults/main.yml @@ -1,9 +1,18 @@ ---- -docker_packages: - - docker-ce - - docker-ce-cli - - containerd.io - - docker-buildx-plugin - - docker-compose-plugin +--- +docker_packages: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin + +docker_user: vagrant -docker_user: vagrant +docker_daemon_config: + log-driver: json-file + log-opts: + max-size: "10m" + max-file: "3" + +docker_users: + - vagrant diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml index 1a5058da5e..568d0966f5 100644 --- a/ansible/roles/docker/handlers/main.yml +++ b/ansible/roles/docker/handlers/main.yml @@ -1,5 +1,5 @@ ---- -- name: restart docker - ansible.builtin.service: - name: docker - state: restarted +--- +- name: restart docker + ansible.builtin.service: + name: docker + state: restarted diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml index 57ac2d885b..a89bacc6e4 100644 --- a/ansible/roles/docker/tasks/main.yml +++ b/ansible/roles/docker/tasks/main.yml @@ -1,49 +1,119 @@ ---- -- name: Install prerequisites for Docker repo - apt: - name: - - ca-certificates - - curl - - gnupg - state: present - -- name: Ensure /etc/apt/keyrings exists - file: - path: /etc/apt/keyrings - state: directory - mode: '0755' - -- name: Download and dearmor Docker GPG key - shell: curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg && chmod a+r /etc/apt/keyrings/docker.gpg - args: - creates: /etc/apt/keyrings/docker.gpg - -- name: Add Docker apt repository - apt_repository: - repo: "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" - state: present - filename: docker - update_cache: yes - -- name: Install Docker packages - apt: - name: "{{ docker_packages }}" - state: present - notify: restart docker - -- name: Ensure Docker service is started and enabled - service: - name: docker - state: started - enabled: yes - -- name: Add user to docker group - user: - name: "{{ docker_user }}" - groups: docker - append: yes - -- name: Install python3-docker for Ansible Docker modules - apt: - name: python3-docker - state: present +--- +# Docker role: install and configure Docker Engine +# Tags: docker (entire role), docker_install, docker_config + +- name: Install Docker Engine + block: + - name: Remove old Docker packages if present + ansible.builtin.apt: + name: + - docker + - docker-engine + - docker.io + - containerd + - runc + state: absent + purge: true + + - name: Install Docker dependencies + ansible.builtin.apt: + name: + - apt-transport-https + - ca-certificates + - curl + - gnupg + - lsb-release + state: present + update_cache: true + + - name: Add Docker GPG key + ansible.builtin.apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + + - name: Add Docker apt repository + ansible.builtin.apt_repository: + repo: >- + deb [arch=amd64] + https://download.docker.com/linux/ubuntu + {{ ansible_distribution_release }} stable + state: present + update_cache: true + + - name: Install Docker packages + ansible.builtin.apt: + name: "{{ docker_packages }}" + state: present + + rescue: + # GPG/network timeouts are common; wait and retry + - name: Wait before retrying Docker install + ansible.builtin.pause: + seconds: 10 + + - name: Retry adding Docker GPG key + ansible.builtin.apt_key: + url: https://download.docker.com/linux/ubuntu/gpg + state: present + + - name: Retry installing Docker packages + ansible.builtin.apt: + name: "{{ docker_packages }}" + state: present + update_cache: true + + always: + - name: Ensure Docker service is enabled and started + ansible.builtin.service: + name: docker + state: started + enabled: true + + become: true + tags: + - docker + - docker_install + +- name: Configure Docker + block: + - name: Create Docker daemon configuration directory + ansible.builtin.file: + path: /etc/docker + state: directory + mode: "0755" + + - name: Configure Docker daemon + ansible.builtin.copy: + dest: /etc/docker/daemon.json + content: "{{ docker_daemon_config | to_nice_json }}" + mode: "0644" + notify: restart docker + + - name: Add users to docker group + ansible.builtin.user: + name: "{{ item }}" + groups: docker + append: true + loop: "{{ docker_users }}" + + rescue: + - name: Log Docker configuration failure + ansible.builtin.debug: + msg: "Docker configuration failed. Rolling back daemon.json." + + - name: Remove invalid daemon config + ansible.builtin.file: + path: /etc/docker/daemon.json + state: absent + + always: + - name: Verify Docker is running after config + ansible.builtin.service: + name: docker + state: started + enabled: true + + become: true + tags: + - docker + - docker_config diff --git a/ansible/roles/app_deploy/defaults/main.yml b/ansible/roles/web_app/defaults/main.yml similarity index 68% rename from ansible/roles/app_deploy/defaults/main.yml rename to ansible/roles/web_app/defaults/main.yml index 1f7d94f7e2..50edc2cec0 100644 --- a/ansible/roles/app_deploy/defaults/main.yml +++ b/ansible/roles/web_app/defaults/main.yml @@ -1,10 +1,15 @@ ---- -app_name: devops-app -# By default the image is dockerhub_username/app_name (username is in Vault) -docker_image_tag: latest -app_port: 5000 -app_container_name: "{{ app_name }}" -app_restart_policy: unless-stopped -app_env: {} -health_endpoint: "/health" -health_timeout_seconds: 30 +--- +app_name: devops-app +# By default the image is dockerhub_username/app_name (username is in Vault) +docker_image_tag: latest +app_port: 5000 +app_container_name: "{{ app_name }}" +app_restart_policy: unless-stopped +app_env: {} +health_endpoint: "/health" +health_timeout_seconds: 30 + +compose_project_dir: "/opt/{{ app_name }}" +docker_compose_version: "3.8" +app_internal_port: 5000 +web_app_wipe: false diff --git a/ansible/roles/app_deploy/handlers/main.yml b/ansible/roles/web_app/handlers/main.yml similarity index 96% rename from ansible/roles/app_deploy/handlers/main.yml rename to ansible/roles/web_app/handlers/main.yml index 1fc3fba48b..ae986eabda 100644 --- a/ansible/roles/app_deploy/handlers/main.yml +++ b/ansible/roles/web_app/handlers/main.yml @@ -1,6 +1,6 @@ ---- -- name: restart app container - community.docker.docker_container: - name: "{{ app_container_name }}" - state: started - restart: true +--- +- name: restart app container + community.docker.docker_container: + name: "{{ app_container_name }}" + state: started + restart: true diff --git a/ansible/roles/web_app/meta/main.yml b/ansible/roles/web_app/meta/main.yml new file mode 100644 index 0000000000..e81c1dfe31 --- /dev/null +++ b/ansible/roles/web_app/meta/main.yml @@ -0,0 +1,9 @@ +--- +galaxy_info: + role_name: web_app + description: Deploy a containerised web application using Docker Compose + license: MIT + min_ansible_version: "2.16" + +dependencies: + - role: docker diff --git a/ansible/roles/web_app/tasks/main.yml b/ansible/roles/web_app/tasks/main.yml new file mode 100644 index 0000000000..da8559b773 --- /dev/null +++ b/ansible/roles/web_app/tasks/main.yml @@ -0,0 +1,80 @@ +--- +# roles/web_app/tasks/main.yml +# Tags: web_app_wipe | app_deploy | compose + +- name: Include wipe tasks + ansible.builtin.include_tasks: wipe.yml + tags: + - web_app_wipe + +- name: Deploy application with Docker Compose + block: + - name: Log in to Docker Hub + community.docker.docker_login: + username: "{{ dockerhub_username }}" + password: "{{ dockerhub_password }}" + registry_url: "https://index.docker.io/v1/" + no_log: true + + - name: Create application directory + ansible.builtin.file: + path: "{{ compose_project_dir }}" + state: directory + mode: "0755" + + - name: Template docker-compose.yml to application directory + ansible.builtin.template: + src: docker-compose.yml.j2 + dest: "{{ compose_project_dir }}/docker-compose.yml" + mode: "0644" + + - name: Pull latest image and bring up services + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir }}" + pull: policy + state: present + recreate: auto + + - name: Wait for application port to open + ansible.builtin.wait_for: + port: "{{ app_port }}" + host: localhost + delay: 5 + timeout: 60 + + - name: Verify health endpoint + ansible.builtin.uri: + url: "http://localhost:{{ app_port }}{{ health_endpoint }}" + method: GET + status_code: 200 + retries: 5 + delay: 5 + register: health_check + until: health_check.status == 200 + + - name: Log successful deployment + ansible.builtin.debug: + msg: "{{ app_name }} deployed successfully on {{ inventory_hostname }}:{{ app_port }}" + + rescue: + - name: Show Docker Compose logs on failure + ansible.builtin.command: + cmd: docker compose -f {{ compose_project_dir }}/docker-compose.yml logs --tail=50 + register: compose_logs + changed_when: false + ignore_errors: true + + - name: Display logs for debugging + ansible.builtin.debug: + var: compose_logs.stdout_lines + + - name: Fail with helpful message + ansible.builtin.fail: + msg: >- + Deployment of '{{ app_name }}' failed. See compose logs above. + Verify image {{ docker_image }}:{{ docker_image_tag }} exists on Docker Hub. + + become: true + tags: + - app_deploy + - compose diff --git a/ansible/roles/web_app/tasks/wipe.yml b/ansible/roles/web_app/tasks/wipe.yml new file mode 100644 index 0000000000..e61df93468 --- /dev/null +++ b/ansible/roles/web_app/tasks/wipe.yml @@ -0,0 +1,23 @@ +--- +- name: Wipe web application + block: + - name: Stop and remove containers via Docker Compose + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir }}" + state: absent + remove_volumes: true + ignore_errors: true + + - name: Remove application directory + ansible.builtin.file: + path: "{{ compose_project_dir }}" + state: absent + + - name: Log successful wipe + ansible.builtin.debug: + msg: "Application '{{ app_name }}' wiped successfully from {{ inventory_hostname }}" + + when: web_app_wipe | bool + become: true + tags: + - web_app_wipe 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..04f0314623 --- /dev/null +++ b/ansible/roles/web_app/templates/docker-compose.yml.j2 @@ -0,0 +1,30 @@ +# Managed by Ansible — do not edit manually +version: '{{ docker_compose_version }}' + +services: + {{ app_name }}: + image: {{ docker_image }}:{{ docker_image_tag }} + container_name: {{ app_container_name }} + ports: + - "{{ app_port }}:{{ app_internal_port }}" + environment: + PYTHONUNBUFFERED: "1" +{% for key, value in app_env.items() %} + {{ key }}: "{{ value }}" +{% endfor %} + restart: {{ app_restart_policy }} + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:{{ app_internal_port }}{{ health_endpoint }}"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + +networks: + default: + name: {{ app_name }}_network From 96676ecde8af15ee1d391f9f9d7a2d2a776d6dbd Mon Sep 17 00:00:00 2001 From: Cdeth567 <11kvvkvv11@mail.ru> Date: Thu, 5 Mar 2026 19:15:55 +0300 Subject: [PATCH 02/13] fix: add dummy vault pass for ansible-lint in CI --- .github/workflows/ansible-deploy.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml index 551819a03f..be407e2512 100644 --- a/.github/workflows/ansible-deploy.yml +++ b/.github/workflows/ansible-deploy.yml @@ -2,7 +2,7 @@ name: Ansible Deployment on: push: - branches: [master] + branches: [master, lab06] paths: - 'ansible/**' - '!ansible/docs/**' @@ -37,6 +37,9 @@ jobs: - name: Install Ansible and ansible-lint run: pip install ansible ansible-lint + - name: Create dummy vault password for lint + run: echo "dummy" > ansible/.vault_pass + - name: Run ansible-lint run: | cd ansible @@ -46,7 +49,7 @@ jobs: name: Deploy Application needs: lint runs-on: ubuntu-latest - if: github.event_name == 'push' + if: github.event_name == 'push' && github.ref == 'refs/heads/master' steps: - name: Checkout repository uses: actions/checkout@v4 From e57e330d0c86bd2194e11024410f4ed51c7776a0 Mon Sep 17 00:00:00 2001 From: Cdeth567 <11kvvkvv11@mail.ru> Date: Thu, 5 Mar 2026 19:19:12 +0300 Subject: [PATCH 03/13] fix: ansible-lint violations and vault in CI --- .github/workflows/ansible-deploy.yml | 8 ++++++-- ansible/.ansible-lint | 9 +++++++++ ansible/playbooks/site.yml | 2 +- ansible/roles/common/tasks/main.yml | 2 +- ansible/roles/docker/handlers/main.yml | 2 +- 5 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 ansible/.ansible-lint diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml index be407e2512..ea53365291 100644 --- a/.github/workflows/ansible-deploy.yml +++ b/.github/workflows/ansible-deploy.yml @@ -37,14 +37,18 @@ jobs: - name: Install Ansible and ansible-lint run: pip install ansible ansible-lint - - name: Create dummy vault password for lint - run: echo "dummy" > ansible/.vault_pass + - name: Write vault password for syntax check + run: echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > ansible/.vault_pass - name: Run ansible-lint run: | cd ansible ansible-lint playbooks/provision.yml playbooks/deploy.yml playbooks/site.yml + - name: Clean up vault password + if: always() + run: rm -f ansible/.vault_pass + deploy: name: Deploy Application needs: lint diff --git a/ansible/.ansible-lint b/ansible/.ansible-lint new file mode 100644 index 0000000000..a42a13d8f2 --- /dev/null +++ b/ansible/.ansible-lint @@ -0,0 +1,9 @@ +warn_list: + - key-order + - var-naming + - name + +skip_list: + - key-order[task] + - var-naming[no-role-prefix] + - name[casing] diff --git a/ansible/playbooks/site.yml b/ansible/playbooks/site.yml index 876b2cb346..cf2e66a01a 100644 --- a/ansible/playbooks/site.yml +++ b/ansible/playbooks/site.yml @@ -6,4 +6,4 @@ roles: - common - docker - - app_deploy + - web_app diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml index 393e289378..c8c44248fd 100644 --- a/ansible/roles/common/tasks/main.yml +++ b/ansible/roles/common/tasks/main.yml @@ -23,7 +23,7 @@ environment: DEBIAN_FRONTEND: noninteractive # Retry once with --fix-missing to handle partial failures - register: apt_retry + register: common_apt_retry until: apt_retry is succeeded retries: 1 delay: 5 diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml index 568d0966f5..55637bda17 100644 --- a/ansible/roles/docker/handlers/main.yml +++ b/ansible/roles/docker/handlers/main.yml @@ -1,5 +1,5 @@ --- -- name: restart docker +- name: Restart docker ansible.builtin.service: name: docker state: restarted From 47ffbc925aa26afcb3082d5f569f2c3357b1c0f0 Mon Sep 17 00:00:00 2001 From: Cdeth567 <11kvvkvv11@mail.ru> Date: Thu, 5 Mar 2026 19:30:55 +0300 Subject: [PATCH 04/13] fix: use printf to avoid trailing newline in vault password --- .github/workflows/ansible-deploy.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml index ea53365291..50500ee6d1 100644 --- a/.github/workflows/ansible-deploy.yml +++ b/.github/workflows/ansible-deploy.yml @@ -38,7 +38,7 @@ jobs: run: pip install ansible ansible-lint - name: Write vault password for syntax check - run: echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > ansible/.vault_pass + run: printf "%s" "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > ansible/.vault_pass - name: Run ansible-lint run: | @@ -78,7 +78,7 @@ jobs: ssh-keyscan -H "${{ secrets.VM_HOST }}" >> ~/.ssh/known_hosts - name: Write Vault password file - run: echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass + run: printf "%s" "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass - name: Run Ansible deploy playbook env: From 0e13cbe6db72f9b0aed8664770a7c46910411c8f Mon Sep 17 00:00:00 2001 From: Cdeth567 <11kvvkvv11@mail.ru> Date: Thu, 5 Mar 2026 19:32:41 +0300 Subject: [PATCH 05/13] fix: use env var for vault password to avoid interpolation issues --- .github/workflows/ansible-deploy.yml | 56 +++++++++++----------------- 1 file changed, 21 insertions(+), 35 deletions(-) diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml index 50500ee6d1..fdc8df7a61 100644 --- a/.github/workflows/ansible-deploy.yml +++ b/.github/workflows/ansible-deploy.yml @@ -13,39 +13,31 @@ on: - 'ansible/**' - '.github/workflows/ansible-deploy.yml' -concurrency: - group: ansible-deploy-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - jobs: lint: name: Ansible Lint runs-on: ubuntu-latest steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Python 3.12 - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: python-version: '3.12' - cache: pip - name: Install Ansible and ansible-lint run: pip install ansible ansible-lint - - name: Write vault password for syntax check - run: printf "%s" "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > ansible/.vault_pass + - name: Write vault password + env: + VAULT_PASS: ${{ secrets.ANSIBLE_VAULT_PASSWORD }} + run: printf '%s' "$VAULT_PASS" > ansible/.vault_pass - name: Run ansible-lint run: | cd ansible ansible-lint playbooks/provision.yml playbooks/deploy.yml playbooks/site.yml - - name: Clean up vault password + - name: Cleanup if: always() run: rm -f ansible/.vault_pass @@ -55,44 +47,38 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'push' && github.ref == 'refs/heads/master' steps: - - name: Checkout repository - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - - name: Set up Python 3.12 - uses: actions/setup-python@v5 + - uses: actions/setup-python@v5 with: python-version: '3.12' - cache: pip - name: Install Ansible run: pip install ansible - - name: Install community.docker collection + - name: Install collections run: ansible-galaxy collection install community.docker community.general - - name: Configure SSH access to target VM + - name: Configure SSH run: | mkdir -p ~/.ssh - echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa + printf '%s' "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H "${{ secrets.VM_HOST }}" >> ~/.ssh/known_hosts - - name: Write Vault password file - run: printf "%s" "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > /tmp/vault_pass - - - name: Run Ansible deploy playbook + - name: Write vault password env: - ANSIBLE_HOST_KEY_CHECKING: "False" + VAULT_PASS: ${{ secrets.ANSIBLE_VAULT_PASSWORD }} + run: printf '%s' "$VAULT_PASS" > /tmp/vault_pass + + - name: Deploy run: | cd ansible - ansible-playbook playbooks/deploy.yml \ - --vault-password-file /tmp/vault_pass + ansible-playbook playbooks/deploy.yml --vault-password-file /tmp/vault_pass - - name: Clean up Vault password file + - name: Cleanup vault pass if: always() run: rm -f /tmp/vault_pass - - name: Verify deployment - run: | - sleep 10 - curl -f "http://${{ secrets.VM_HOST }}:5000/health" || exit 1 + - name: Verify health + run: sleep 10 && curl -f "http://${{ secrets.VM_HOST }}:5000/health" From b7dc555ecd529eb41702f77695e7bf342e06984a Mon Sep 17 00:00:00 2001 From: Cdeth567 <11kvvkvv11@mail.ru> Date: Thu, 5 Mar 2026 19:34:19 +0300 Subject: [PATCH 06/13] fix: remove vault from ansible.cfg during lint in CI --- .github/workflows/ansible-deploy.yml | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml index fdc8df7a61..09c7ff4c69 100644 --- a/.github/workflows/ansible-deploy.yml +++ b/.github/workflows/ansible-deploy.yml @@ -27,20 +27,15 @@ jobs: - name: Install Ansible and ansible-lint run: pip install ansible ansible-lint - - name: Write vault password - env: - VAULT_PASS: ${{ secrets.ANSIBLE_VAULT_PASSWORD }} - run: printf '%s' "$VAULT_PASS" > ansible/.vault_pass - - name: Run ansible-lint + env: + ANSIBLE_VAULT_PASSWORD_FILE: "" run: | cd ansible + # Remove vault_password_file from cfg for lint + sed -i '/vault_password_file/d' ansible.cfg ansible-lint playbooks/provision.yml playbooks/deploy.yml playbooks/site.yml - - name: Cleanup - if: always() - run: rm -f ansible/.vault_pass - deploy: name: Deploy Application needs: lint @@ -48,37 +43,28 @@ jobs: if: github.event_name == 'push' && github.ref == 'refs/heads/master' steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 with: python-version: '3.12' - - name: Install Ansible run: pip install ansible - - name: Install collections run: ansible-galaxy collection install community.docker community.general - - name: Configure SSH run: | mkdir -p ~/.ssh printf '%s' "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa ssh-keyscan -H "${{ secrets.VM_HOST }}" >> ~/.ssh/known_hosts - - - name: Write vault password + - name: Deploy env: VAULT_PASS: ${{ secrets.ANSIBLE_VAULT_PASSWORD }} - run: printf '%s' "$VAULT_PASS" > /tmp/vault_pass - - - name: Deploy run: | + printf '%s' "$VAULT_PASS" > /tmp/vault_pass cd ansible ansible-playbook playbooks/deploy.yml --vault-password-file /tmp/vault_pass - - - name: Cleanup vault pass + - name: Cleanup if: always() run: rm -f /tmp/vault_pass - - name: Verify health run: sleep 10 && curl -f "http://${{ secrets.VM_HOST }}:5000/health" From d6d0a0738783b9065e97770e396e34e32fa47897 Mon Sep 17 00:00:00 2001 From: Cdeth567 <11kvvkvv11@mail.ru> Date: Thu, 5 Mar 2026 19:39:30 +0300 Subject: [PATCH 07/13] fix: remove vault_password_file from ansible.cfg for CI compatibility --- ansible/ansible.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg index 7737aab432..d74fa42465 100644 --- a/ansible/ansible.cfg +++ b/ansible/ansible.cfg @@ -1,5 +1,4 @@ [defaults] -vault_password_file = .vault_pass inventory = inventory/hosts.ini roles_path = roles host_key_checking = False From 65fc231b042a1fe5a783be45c08b2864abe4cff8 Mon Sep 17 00:00:00 2001 From: Cdeth567 <11kvvkvv11@mail.ru> Date: Thu, 5 Mar 2026 19:53:46 +0300 Subject: [PATCH 08/13] fix: correct until variable name, remove vars_files, fix handler notify case --- ansible/playbooks/deploy.yml | 2 -- ansible/roles/common/tasks/main.yml | 2 +- ansible/roles/docker/tasks/main.yml | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml index 842fddf11a..77ad7aa765 100644 --- a/ansible/playbooks/deploy.yml +++ b/ansible/playbooks/deploy.yml @@ -2,8 +2,6 @@ - name: Deploy application hosts: webservers become: true - vars_files: - - ../group_vars/all.yml roles: - web_app diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml index c8c44248fd..53490d4f8c 100644 --- a/ansible/roles/common/tasks/main.yml +++ b/ansible/roles/common/tasks/main.yml @@ -24,7 +24,7 @@ DEBIAN_FRONTEND: noninteractive # Retry once with --fix-missing to handle partial failures register: common_apt_retry - until: apt_retry is succeeded + until: common_apt_retry is succeeded retries: 1 delay: 5 diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml index a89bacc6e4..7bd916d856 100644 --- a/ansible/roles/docker/tasks/main.yml +++ b/ansible/roles/docker/tasks/main.yml @@ -87,7 +87,7 @@ dest: /etc/docker/daemon.json content: "{{ docker_daemon_config | to_nice_json }}" mode: "0644" - notify: restart docker + notify: Restart docker - name: Add users to docker group ansible.builtin.user: From 044e6810ca871acf8e8010456ef60cc0ef710cbf Mon Sep 17 00:00:00 2001 From: Cdeth567 <11kvvkvv11@mail.ru> Date: Thu, 5 Mar 2026 19:58:28 +0300 Subject: [PATCH 09/13] trigger CI From e5ac7c65f3458cdbb09fcfad2015beb219f54af4 Mon Sep 17 00:00:00 2001 From: Cdeth567 <11kvvkvv11@mail.ru> Date: Thu, 5 Mar 2026 20:00:55 +0300 Subject: [PATCH 10/13] fix: trigger on push to lab06 --- .github/workflows/ansible-deploy.yml | 49 ---------------------------- 1 file changed, 49 deletions(-) diff --git a/.github/workflows/ansible-deploy.yml b/.github/workflows/ansible-deploy.yml index 09c7ff4c69..014bffbed1 100644 --- a/.github/workflows/ansible-deploy.yml +++ b/.github/workflows/ansible-deploy.yml @@ -3,15 +3,6 @@ name: Ansible Deployment on: push: branches: [master, lab06] - paths: - - 'ansible/**' - - '!ansible/docs/**' - - '.github/workflows/ansible-deploy.yml' - pull_request: - branches: [master] - paths: - - 'ansible/**' - - '.github/workflows/ansible-deploy.yml' jobs: lint: @@ -19,52 +10,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 with: python-version: '3.12' - - name: Install Ansible and ansible-lint run: pip install ansible ansible-lint - - name: Run ansible-lint - env: - ANSIBLE_VAULT_PASSWORD_FILE: "" run: | cd ansible - # Remove vault_password_file from cfg for lint - sed -i '/vault_password_file/d' ansible.cfg ansible-lint playbooks/provision.yml playbooks/deploy.yml playbooks/site.yml - - deploy: - name: Deploy Application - needs: lint - runs-on: ubuntu-latest - if: github.event_name == 'push' && github.ref == 'refs/heads/master' - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - name: Install Ansible - run: pip install ansible - - name: Install collections - run: ansible-galaxy collection install community.docker community.general - - name: Configure SSH - run: | - mkdir -p ~/.ssh - printf '%s' "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa - ssh-keyscan -H "${{ secrets.VM_HOST }}" >> ~/.ssh/known_hosts - - name: Deploy - env: - VAULT_PASS: ${{ secrets.ANSIBLE_VAULT_PASSWORD }} - run: | - printf '%s' "$VAULT_PASS" > /tmp/vault_pass - cd ansible - ansible-playbook playbooks/deploy.yml --vault-password-file /tmp/vault_pass - - name: Cleanup - if: always() - run: rm -f /tmp/vault_pass - - name: Verify health - run: sleep 10 && curl -f "http://${{ secrets.VM_HOST }}:5000/health" From 4ec223c1938e0d1787e9a2d5d9668cfd6d03c8a4 Mon Sep 17 00:00:00 2001 From: Cdeth567 <11kvvkvv11@mail.ru> Date: Thu, 5 Mar 2026 20:12:46 +0300 Subject: [PATCH 11/13] fix: CI --- .github/workflows/python-ci.yml | 206 +- README.md | 542 ++--- ansible/roles/web_app/meta/main.yml | 1 + ansible/roles/web_app/tasks/wipe.yml | 2 +- app_python/.dockerignore | 56 +- app_python/.gitignore | 24 +- app_python/Dockerfile | 58 +- app_python/README.md | 284 +-- app_python/app.py | 234 +- app_python/docs/LAB01.md | 336 +-- app_python/docs/LAB02.md | 470 ++-- app_python/docs/LAB03.md | 598 ++--- app_python/docs/LAB04.md | 426 ++-- app_python/requirements-dev.txt | 2 +- app_python/tests/test_app.py | 124 +- labs/lab01.md | 1386 ++++++------ labs/lab02.md | 732 +++---- labs/lab03.md | 1862 ++++++++-------- labs/lab04.md | 3020 +++++++++++++------------- labs/lab05.md | 1952 ++++++++--------- labs/lab06.md | 2704 +++++++++++------------ labs/lab07.md | 1188 +++++----- labs/lab08.md | 1504 ++++++------- labs/lab09.md | 1460 ++++++------- labs/lab10.md | 1806 +++++++-------- labs/lab11.md | 894 ++++---- labs/lab12.md | 932 ++++---- labs/lab13.md | 1188 +++++----- labs/lab14.md | 906 ++++---- labs/lab15.md | 608 +++--- labs/lab16.md | 522 ++--- labs/lab17.md | 894 ++++---- labs/lab18.md | 860 ++++---- labs/lab18/index.html | 1854 ++++++++-------- lectures/lec1.md | 1522 ++++++------- lectures/lec10.md | 1680 +++++++------- lectures/lec11.md | 1518 ++++++------- lectures/lec12.md | 1708 +++++++-------- lectures/lec13.md | 1660 +++++++------- lectures/lec14.md | 1650 +++++++------- lectures/lec15.md | 1642 +++++++------- lectures/lec16.md | 1434 ++++++------ lectures/lec2.md | 2106 +++++++++--------- lectures/lec3.md | 1956 ++++++++--------- lectures/lec4.md | 1602 +++++++------- lectures/lec5.md | 1648 +++++++------- lectures/lec6.md | 1774 +++++++-------- lectures/lec7.md | 1698 +++++++-------- lectures/lec8.md | 1598 +++++++------- lectures/lec9.md | 1706 +++++++-------- pulumi/.gitignore | 8 +- pulumi/Pulumi.yaml | 12 +- pulumi/__main__.py | 182 +- pulumi/requirements.txt | 2 +- terraform.tfvars | 8 +- terraform/.gitignore | 20 +- terraform/main.tf | 204 +- terraform/outputs.tf | 26 +- terraform/variables.tf | 134 +- terraform/versions.tf | 20 +- 60 files changed, 28577 insertions(+), 28576 deletions(-) diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml index 834ef8fbc0..cf2f50727b 100644 --- a/.github/workflows/python-ci.yml +++ b/.github/workflows/python-ci.yml @@ -1,103 +1,103 @@ -name: Python CI (tests + docker) - -on: - push: - branches: [ "master", "lab03" ] - paths: - - "app_python/**" - - ".github/workflows/python-ci.yml" - pull_request: - branches: [ "master" ] - paths: - - "app_python/**" - - ".github/workflows/python-ci.yml" - -concurrency: - group: python-ci-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read - -jobs: - test-and-lint: - runs-on: ubuntu-latest - - strategy: - fail-fast: true - matrix: - python-version: ["3.12", "3.13"] - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - cache: "pip" - cache-dependency-path: | - app_python/requirements.txt - app_python/requirements-dev.txt - - - name: Install dependencies - working-directory: app_python - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - pip install -r requirements-dev.txt - - - name: Lint (ruff) - working-directory: app_python - run: | - ruff check . - - - name: Run tests (pytest) - working-directory: app_python - run: | - pytest -q - - - name: Install Snyk CLI - run: npm install -g snyk - - - name: Snyk scan (dependencies) - continue-on-error: true - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - run: | - cd app_python - snyk test --severity-threshold=high --file=requirements.txt - - docker-build-and-push: - runs-on: ubuntu-latest - needs: test-and-lint - - if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/lab03') - - permissions: - contents: read - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set version (CalVer) - run: | - echo "VERSION=$(date -u +%Y.%m.%d)" >> $GITHUB_ENV - - - name: Log in to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: ./app_python - file: ./app_python/Dockerfile - push: true - tags: | - ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:${{ env.VERSION }} - ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:latest +name: Python CI (tests + docker) + +on: + push: + branches: [ "master", "lab03" ] + paths: + - "app_python/**" + - ".github/workflows/python-ci.yml" + pull_request: + branches: [ "master" ] + paths: + - "app_python/**" + - ".github/workflows/python-ci.yml" + +concurrency: + group: python-ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + test-and-lint: + runs-on: ubuntu-latest + + strategy: + fail-fast: true + matrix: + python-version: ["3.12", "3.13"] + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + cache-dependency-path: | + app_python/requirements.txt + app_python/requirements-dev.txt + + - name: Install dependencies + working-directory: app_python + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install -r requirements-dev.txt + + - name: Lint (ruff) + working-directory: app_python + run: | + ruff check . + + - name: Run tests (pytest) + working-directory: app_python + run: | + pytest -q + + - name: Install Snyk CLI + run: npm install -g snyk + + - name: Snyk scan (dependencies) + continue-on-error: true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + run: | + cd app_python + snyk test --severity-threshold=high --file=requirements.txt + + docker-build-and-push: + runs-on: ubuntu-latest + needs: test-and-lint + + if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/lab03') + + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set version (CalVer) + run: | + echo "VERSION=$(date -u +%Y.%m.%d)" >> $GITHUB_ENV + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: ./app_python + file: ./app_python/Dockerfile + push: true + tags: | + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:${{ env.VERSION }} + ${{ secrets.DOCKERHUB_USERNAME }}/devops-info-service:latest diff --git a/README.md b/README.md index 371d51f456..d961fa12e6 100644 --- a/README.md +++ b/README.md @@ -1,271 +1,271 @@ -# DevOps Engineering: Core Practices - -[![Labs](https://img.shields.io/badge/Labs-18-blue)](#labs) -[![Exam](https://img.shields.io/badge/Exam-Optional-green)](#exam-alternative) -[![Duration](https://img.shields.io/badge/Duration-18%20Weeks-lightgrey)](#course-roadmap) - -Master **production-grade DevOps practices** through hands-on labs. Build, containerize, deploy, monitor, and scale applications using industry-standard tools. - ---- - -## Quick Start - -1. **Fork** this repository -2. **Clone** your fork locally -3. **Start with Lab 1** and progress sequentially -4. **Submit PRs** for each lab (details below) - ---- - -## Course Roadmap - -| Week | Lab | Topic | Key Technologies | -|------|-----|-------|------------------| -| 1 | 1 | Web Application Development | Python/Go, Best Practices | -| 2 | 2 | Containerization | Docker, Multi-stage Builds | -| 3 | 3 | Continuous Integration | GitHub Actions, Snyk | -| 4 | 4 | Infrastructure as Code | Terraform, Cloud Providers | -| 5 | 5 | Configuration Management | Ansible Basics | -| 6 | 6 | Continuous Deployment | Ansible Advanced | -| 7 | 7 | Logging | Promtail, Loki, Grafana | -| 8 | 8 | Monitoring | Prometheus, Grafana | -| 9 | 9 | Kubernetes Basics | Minikube, Deployments, Services | -| 10 | 10 | Helm Charts | Templating, Hooks | -| 11 | 11 | Secrets Management | K8s Secrets, HashiCorp Vault | -| 12 | 12 | Configuration & Storage | ConfigMaps, PVCs | -| 13 | 13 | GitOps | ArgoCD | -| 14 | 14 | Progressive Delivery | Argo Rollouts | -| 15 | 15 | StatefulSets | Persistent Storage, Headless Services | -| 16 | 16 | Cluster Monitoring | Kube-Prometheus, Init Containers | -| — | **Exam Alternative Labs** | | | -| 17 | 17 | Edge Deployment | Fly.io, Global Distribution | -| 18 | 18 | Decentralized Storage | 4EVERLAND, IPFS, Web3 | - ---- - -## Grading - -### Grade Composition - -| Component | Weight | Points | -|-----------|--------|--------| -| **Labs (16 required)** | 80% | 160 pts | -| **Final Exam** | 20% | 40 pts | -| **Bonus Tasks** | Extra | +40 pts max | -| **Total** | 100% | 200 pts | - -### Exam Alternative - -Don't want to take the exam? Complete **both** bonus labs: - -| Lab | Topic | Points | -|-----|-------|--------| -| **Lab 17** | Fly.io Edge Deployment | 20 pts | -| **Lab 18** | 4EVERLAND & IPFS | 20 pts | - -**Requirements:** -- Complete both labs (17 + 18 = 40 pts, replaces exam) -- Minimum 16/20 on each lab -- Deadline: **1 week before exam date** -- Can still take exam if you need more points for desired grade - -
-📊 Grade Scale - -| Grade | Points | Percentage | -|-------|--------|------------| -| **A** | 180-200+ | 90-100% | -| **B** | 150-179 | 75-89% | -| **C** | 120-149 | 60-74% | -| **D** | 0-119 | 0-59% | - -**Minimum to Pass:** 120 points (60%) - -
- -
-📈 Grade Examples - -**Scenario 1: Labs + Exam** -``` -Labs: 16 × 9 = 144 pts -Bonus: 5 labs × 2.5 = 12.5 pts -Exam: 35/40 pts -Total: 191.5 pts = 96% (A) -``` - -**Scenario 2: Labs + Exam Alternative** -``` -Labs: 16 × 9 = 144 pts -Bonus: 8 labs × 2.5 = 20 pts -Lab 17: 18 pts -Lab 18: 17 pts -Total: 199 pts = 99.5% (A) -``` - -
- ---- - -## Lab Structure - -Each lab is worth **10 points** (main tasks) + **2.5 points** (bonus). - -- **Minimum passing score:** 6/10 per lab -- **Late submissions:** Max 6/10 (within 1 week) -- **Very late (>1 week):** Not accepted - -
-📋 Lab Categories - -**Foundation (Labs 1-2)** -- Web app development -- Docker containerization - -**CI/CD & Infrastructure (Labs 3-4)** -- GitHub Actions -- Terraform - -**Configuration Management (Labs 5-6)** -- Ansible playbooks and roles - -**Observability (Labs 7-8)** -- Loki logging stack -- Prometheus monitoring - -**Kubernetes Core (Labs 9-12)** -- K8s basics, Helm -- Secrets, ConfigMaps - -**Advanced Kubernetes (Labs 13-16)** -- ArgoCD, Argo Rollouts -- StatefulSets, Monitoring - -**Exam Alternative (Labs 17-18)** -- Fly.io, 4EVERLAND/IPFS - -
- ---- - -## How to Submit - -```bash -# 1. Create branch -git checkout -b lab1 - -# 2. Complete lab tasks - -# 3. Commit and push -git add . -git commit -m "Complete lab1" -git push -u origin lab1 - -# 4. Create TWO Pull Requests: -# PR #1: your-fork:lab1 → course-repo:master -# PR #2: your-fork:lab1 → your-fork:master -``` - -
-📝 Submission Checklist - -- [ ] All main tasks completed -- [ ] Documentation files created -- [ ] Screenshots where required -- [ ] Code tested and working -- [ ] Markdown validated ([linter](https://dlaa.me/markdownlint/)) -- [ ] Both PRs created - -
- ---- - -## Resources - -
-🛠️ Required Tools - -| Tool | Purpose | -|------|---------| -| Git | Version control | -| Docker | Containerization | -| kubectl | Kubernetes CLI | -| Helm | K8s package manager | -| Minikube | Local K8s cluster | -| Terraform | Infrastructure as Code | -| Ansible | Configuration management | - -
- -
-📚 Documentation Links - -**Core:** -- [Docker](https://docs.docker.com/) -- [Kubernetes](https://kubernetes.io/docs/) -- [Helm](https://helm.sh/docs/) - -**CI/CD:** -- [GitHub Actions](https://docs.github.com/en/actions) -- [Terraform](https://www.terraform.io/docs) -- [Ansible](https://docs.ansible.com/) - -**Observability:** -- [Prometheus](https://prometheus.io/docs/) -- [Grafana](https://grafana.com/docs/) - -**Advanced:** -- [ArgoCD](https://argo-cd.readthedocs.io/) -- [Argo Rollouts](https://argoproj.github.io/argo-rollouts/) -- [HashiCorp Vault](https://developer.hashicorp.com/vault/docs) - -
- -
-💡 Tips for Success - -1. **Start early** - Don't wait until deadline -2. **Read instructions fully** before starting -3. **Test everything** before submitting -4. **Document as you go** - Don't leave it for the end -5. **Ask questions early** - Don't wait until last minute -6. **Use proper Git workflow** - Branches, commits, PRs - -
- -
-🔧 Common Issues - -**Docker:** -- Daemon not running → Start Docker Desktop -- Permission denied → Add user to docker group - -**Minikube:** -- Won't start → Try `--driver=docker` -- Resource issues → Allocate more memory/CPU - -**Kubernetes:** -- ImagePullBackOff → Check image name/registry -- CrashLoopBackOff → Check logs: `kubectl logs ` - -
- ---- - -## Course Completion - -After completing all 16 core labs (+ optional Labs 17-18), you'll have: - -✅ Full-stack DevOps expertise -✅ Production-ready portfolio with 16-18 projects -✅ Container and Kubernetes mastery -✅ CI/CD pipeline experience -✅ Infrastructure as Code skills -✅ Monitoring and observability knowledge -✅ GitOps workflow experience - ---- - -**Ready to begin? Start with [Lab 1](labs/lab01.md)!** - -Questions? Check the course Moodle page or ask during office hours. +# DevOps Engineering: Core Practices + +[![Labs](https://img.shields.io/badge/Labs-18-blue)](#labs) +[![Exam](https://img.shields.io/badge/Exam-Optional-green)](#exam-alternative) +[![Duration](https://img.shields.io/badge/Duration-18%20Weeks-lightgrey)](#course-roadmap) + +Master **production-grade DevOps practices** through hands-on labs. Build, containerize, deploy, monitor, and scale applications using industry-standard tools. + +--- + +## Quick Start + +1. **Fork** this repository +2. **Clone** your fork locally +3. **Start with Lab 1** and progress sequentially +4. **Submit PRs** for each lab (details below) + +--- + +## Course Roadmap + +| Week | Lab | Topic | Key Technologies | +|------|-----|-------|------------------| +| 1 | 1 | Web Application Development | Python/Go, Best Practices | +| 2 | 2 | Containerization | Docker, Multi-stage Builds | +| 3 | 3 | Continuous Integration | GitHub Actions, Snyk | +| 4 | 4 | Infrastructure as Code | Terraform, Cloud Providers | +| 5 | 5 | Configuration Management | Ansible Basics | +| 6 | 6 | Continuous Deployment | Ansible Advanced | +| 7 | 7 | Logging | Promtail, Loki, Grafana | +| 8 | 8 | Monitoring | Prometheus, Grafana | +| 9 | 9 | Kubernetes Basics | Minikube, Deployments, Services | +| 10 | 10 | Helm Charts | Templating, Hooks | +| 11 | 11 | Secrets Management | K8s Secrets, HashiCorp Vault | +| 12 | 12 | Configuration & Storage | ConfigMaps, PVCs | +| 13 | 13 | GitOps | ArgoCD | +| 14 | 14 | Progressive Delivery | Argo Rollouts | +| 15 | 15 | StatefulSets | Persistent Storage, Headless Services | +| 16 | 16 | Cluster Monitoring | Kube-Prometheus, Init Containers | +| — | **Exam Alternative Labs** | | | +| 17 | 17 | Edge Deployment | Fly.io, Global Distribution | +| 18 | 18 | Decentralized Storage | 4EVERLAND, IPFS, Web3 | + +--- + +## Grading + +### Grade Composition + +| Component | Weight | Points | +|-----------|--------|--------| +| **Labs (16 required)** | 80% | 160 pts | +| **Final Exam** | 20% | 40 pts | +| **Bonus Tasks** | Extra | +40 pts max | +| **Total** | 100% | 200 pts | + +### Exam Alternative + +Don't want to take the exam? Complete **both** bonus labs: + +| Lab | Topic | Points | +|-----|-------|--------| +| **Lab 17** | Fly.io Edge Deployment | 20 pts | +| **Lab 18** | 4EVERLAND & IPFS | 20 pts | + +**Requirements:** +- Complete both labs (17 + 18 = 40 pts, replaces exam) +- Minimum 16/20 on each lab +- Deadline: **1 week before exam date** +- Can still take exam if you need more points for desired grade + +
+📊 Grade Scale + +| Grade | Points | Percentage | +|-------|--------|------------| +| **A** | 180-200+ | 90-100% | +| **B** | 150-179 | 75-89% | +| **C** | 120-149 | 60-74% | +| **D** | 0-119 | 0-59% | + +**Minimum to Pass:** 120 points (60%) + +
+ +
+📈 Grade Examples + +**Scenario 1: Labs + Exam** +``` +Labs: 16 × 9 = 144 pts +Bonus: 5 labs × 2.5 = 12.5 pts +Exam: 35/40 pts +Total: 191.5 pts = 96% (A) +``` + +**Scenario 2: Labs + Exam Alternative** +``` +Labs: 16 × 9 = 144 pts +Bonus: 8 labs × 2.5 = 20 pts +Lab 17: 18 pts +Lab 18: 17 pts +Total: 199 pts = 99.5% (A) +``` + +
+ +--- + +## Lab Structure + +Each lab is worth **10 points** (main tasks) + **2.5 points** (bonus). + +- **Minimum passing score:** 6/10 per lab +- **Late submissions:** Max 6/10 (within 1 week) +- **Very late (>1 week):** Not accepted + +
+📋 Lab Categories + +**Foundation (Labs 1-2)** +- Web app development +- Docker containerization + +**CI/CD & Infrastructure (Labs 3-4)** +- GitHub Actions +- Terraform + +**Configuration Management (Labs 5-6)** +- Ansible playbooks and roles + +**Observability (Labs 7-8)** +- Loki logging stack +- Prometheus monitoring + +**Kubernetes Core (Labs 9-12)** +- K8s basics, Helm +- Secrets, ConfigMaps + +**Advanced Kubernetes (Labs 13-16)** +- ArgoCD, Argo Rollouts +- StatefulSets, Monitoring + +**Exam Alternative (Labs 17-18)** +- Fly.io, 4EVERLAND/IPFS + +
+ +--- + +## How to Submit + +```bash +# 1. Create branch +git checkout -b lab1 + +# 2. Complete lab tasks + +# 3. Commit and push +git add . +git commit -m "Complete lab1" +git push -u origin lab1 + +# 4. Create TWO Pull Requests: +# PR #1: your-fork:lab1 → course-repo:master +# PR #2: your-fork:lab1 → your-fork:master +``` + +
+📝 Submission Checklist + +- [ ] All main tasks completed +- [ ] Documentation files created +- [ ] Screenshots where required +- [ ] Code tested and working +- [ ] Markdown validated ([linter](https://dlaa.me/markdownlint/)) +- [ ] Both PRs created + +
+ +--- + +## Resources + +
+🛠️ Required Tools + +| Tool | Purpose | +|------|---------| +| Git | Version control | +| Docker | Containerization | +| kubectl | Kubernetes CLI | +| Helm | K8s package manager | +| Minikube | Local K8s cluster | +| Terraform | Infrastructure as Code | +| Ansible | Configuration management | + +
+ +
+📚 Documentation Links + +**Core:** +- [Docker](https://docs.docker.com/) +- [Kubernetes](https://kubernetes.io/docs/) +- [Helm](https://helm.sh/docs/) + +**CI/CD:** +- [GitHub Actions](https://docs.github.com/en/actions) +- [Terraform](https://www.terraform.io/docs) +- [Ansible](https://docs.ansible.com/) + +**Observability:** +- [Prometheus](https://prometheus.io/docs/) +- [Grafana](https://grafana.com/docs/) + +**Advanced:** +- [ArgoCD](https://argo-cd.readthedocs.io/) +- [Argo Rollouts](https://argoproj.github.io/argo-rollouts/) +- [HashiCorp Vault](https://developer.hashicorp.com/vault/docs) + +
+ +
+💡 Tips for Success + +1. **Start early** - Don't wait until deadline +2. **Read instructions fully** before starting +3. **Test everything** before submitting +4. **Document as you go** - Don't leave it for the end +5. **Ask questions early** - Don't wait until last minute +6. **Use proper Git workflow** - Branches, commits, PRs + +
+ +
+🔧 Common Issues + +**Docker:** +- Daemon not running → Start Docker Desktop +- Permission denied → Add user to docker group + +**Minikube:** +- Won't start → Try `--driver=docker` +- Resource issues → Allocate more memory/CPU + +**Kubernetes:** +- ImagePullBackOff → Check image name/registry +- CrashLoopBackOff → Check logs: `kubectl logs ` + +
+ +--- + +## Course Completion + +After completing all 16 core labs (+ optional Labs 17-18), you'll have: + +✅ Full-stack DevOps expertise +✅ Production-ready portfolio with 16-18 projects +✅ Container and Kubernetes mastery +✅ CI/CD pipeline experience +✅ Infrastructure as Code skills +✅ Monitoring and observability knowledge +✅ GitOps workflow experience + +--- + +**Ready to begin? Start with [Lab 1](labs/lab01.md)!** + +Questions? Check the course Moodle page or ask during office hours. diff --git a/ansible/roles/web_app/meta/main.yml b/ansible/roles/web_app/meta/main.yml index e81c1dfe31..c37c0be4b9 100644 --- a/ansible/roles/web_app/meta/main.yml +++ b/ansible/roles/web_app/meta/main.yml @@ -1,6 +1,7 @@ --- galaxy_info: role_name: web_app + author: cdeth567 description: Deploy a containerised web application using Docker Compose license: MIT min_ansible_version: "2.16" diff --git a/ansible/roles/web_app/tasks/wipe.yml b/ansible/roles/web_app/tasks/wipe.yml index e61df93468..6fbb99dcea 100644 --- a/ansible/roles/web_app/tasks/wipe.yml +++ b/ansible/roles/web_app/tasks/wipe.yml @@ -6,7 +6,7 @@ project_src: "{{ compose_project_dir }}" state: absent remove_volumes: true - ignore_errors: true + failed_when: false - name: Remove application directory ansible.builtin.file: diff --git a/app_python/.dockerignore b/app_python/.dockerignore index d57576a080..bbc07852cb 100644 --- a/app_python/.dockerignore +++ b/app_python/.dockerignore @@ -1,28 +1,28 @@ -# Python cache -__pycache__/ -*.py[cod] - -# Virtual environments -venv/ -.venv/ - -# Git -.git/ -.gitignore - -# IDE -.vscode/ -.idea/ - -# OS -.DS_Store - -# Docs/screenshots not needed at runtime -docs/ -tests/ - -# Env files -.env - -# Logs -*.log +# Python cache +__pycache__/ +*.py[cod] + +# Virtual environments +venv/ +.venv/ + +# Git +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store + +# Docs/screenshots not needed at runtime +docs/ +tests/ + +# Env files +.env + +# Logs +*.log diff --git a/app_python/.gitignore b/app_python/.gitignore index a8692c9faf..95e8a48683 100644 --- a/app_python/.gitignore +++ b/app_python/.gitignore @@ -1,13 +1,13 @@ -# Python -__pycache__/ -*.py[cod] -venv/ -*.log -.env - -# IDE -.vscode/ -.idea/ - -# OS +# Python +__pycache__/ +*.py[cod] +venv/ +*.log +.env + +# IDE +.vscode/ +.idea/ + +# OS .DS_Store \ No newline at end of file diff --git a/app_python/Dockerfile b/app_python/Dockerfile index 70118d9d99..ab5526ede7 100644 --- a/app_python/Dockerfile +++ b/app_python/Dockerfile @@ -1,29 +1,29 @@ -# syntax=docker/dockerfile:1 - -FROM python:3.13-slim - -# 1) Basic env for predictable Python behavior -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 - -# 2) Create non-root user -RUN addgroup --system app && adduser --system --ingroup app app - -# 3) Workdir -WORKDIR /app - -# 4) Install dependencies (layer caching) -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - -# 5) Copy only necessary source files -COPY app.py . - -# 6) Security: drop privileges -USER app - -# 7) Document port (your app uses PORT env, default 5000) -EXPOSE 5000 - -# 8) Start the app -CMD ["python", "app.py"] +# syntax=docker/dockerfile:1 + +FROM python:3.13-slim + +# 1) Basic env for predictable Python behavior +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 + +# 2) Create non-root user +RUN addgroup --system app && adduser --system --ingroup app app + +# 3) Workdir +WORKDIR /app + +# 4) Install dependencies (layer caching) +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 5) Copy only necessary source files +COPY app.py . + +# 6) Security: drop privileges +USER app + +# 7) Document port (your app uses PORT env, default 5000) +EXPOSE 5000 + +# 8) Start the app +CMD ["python", "app.py"] diff --git a/app_python/README.md b/app_python/README.md index 0d4c87ae27..201c54e7ca 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -1,142 +1,142 @@ -[![Python CI (tests + docker)](https://github.com/Cdeth567/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg?branch=lab03)](https://github.com/Cdeth567/DevOps-Core-Course/actions/workflows/python-ci.yml) - -# DevOps Info Service - -## Overview -DevOps Info Service is a small Flask web application that exposes: -- `GET /` — service metadata, system/runtime info, and request details -- `GET /health` — simple health check endpoint - -This project is the foundation for future DevOps labs (Docker, CI/CD, monitoring, Kubernetes). - -## Prerequisites -- Python 3.11+ -- pip -- (Windows) Python Launcher `py` is recommended - -## Installation - -### 1) Clone repository -```bash -git clone -cd app_python -``` - -### 2) Create and activate virtual environment - -**Windows (PowerShell):** -```powershell -py -m venv venv -.\venv\Scripts\Activate.ps1 -``` - -**Windows (CMD):** -```bat -py -m venv venv -venv\Scripts\activate.bat -``` - -**Linux/Mac:** -```bash -python -m venv venv -source venv/bin/activate -``` - -### 3) Install dependencies -```bash -python -m pip install --upgrade pip -pip install -r requirements.txt -``` - -## Running the Application - -Default: -```bash -python app.py -``` - -Custom config: - -**Linux/Mac:** -```bash -HOST=127.0.0.1 PORT=3000 DEBUG=True python app.py -``` - -**Windows (PowerShell):** -```powershell -$env:HOST="127.0.0.1" -$env:PORT="3000" -$env:DEBUG="True" -python app.py -``` - -**Windows (CMD):** -```bat -set HOST=127.0.0.1 -set PORT=3000 -set DEBUG=True -python app.py -``` - -## API Endpoints -- `GET /` — service and system information -- `GET /health` — health check - -Examples: -```bash -curl http://127.0.0.1:5000/ -curl http://127.0.0.1:5000/health -``` - -Pretty output: -```bash -curl http://127.0.0.1:5000/ | python -m json.tool -``` - -## Configuration - -| Variable | Default | Description | -|----------|---------|-------------| -| HOST | 0.0.0.0 | Bind address | -| PORT | 5000 | HTTP port | -| DEBUG | False | Flask debug mode | - -## Docker - -This application can also be built and run as a Docker container. - -### Build (local) -Pattern: -```bash -docker build -t : . -``` - -### Run -Pattern: -```bash -docker run --rm -p :5000 --name : -``` - -Then test: -```bash -curl http://127.0.0.1:/ -curl http://127.0.0.1:/health -``` - -### Pull from Docker Hub -Pattern: -```bash -docker pull /: -docker run --rm -p :5000 /: -``` - -> Note (Windows PowerShell): `curl` is an alias for `Invoke-WebRequest`. -> For classic curl behavior, use `curl.exe`. - -## Testing -Install dev dependencies: -- python -m pip install -r requirements-dev.txt - -Run tests: -- pytest - +[![Python CI (tests + docker)](https://github.com/Cdeth567/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg?branch=lab03)](https://github.com/Cdeth567/DevOps-Core-Course/actions/workflows/python-ci.yml) + +# DevOps Info Service + +## Overview +DevOps Info Service is a small Flask web application that exposes: +- `GET /` — service metadata, system/runtime info, and request details +- `GET /health` — simple health check endpoint + +This project is the foundation for future DevOps labs (Docker, CI/CD, monitoring, Kubernetes). + +## Prerequisites +- Python 3.11+ +- pip +- (Windows) Python Launcher `py` is recommended + +## Installation + +### 1) Clone repository +```bash +git clone +cd app_python +``` + +### 2) Create and activate virtual environment + +**Windows (PowerShell):** +```powershell +py -m venv venv +.\venv\Scripts\Activate.ps1 +``` + +**Windows (CMD):** +```bat +py -m venv venv +venv\Scripts\activate.bat +``` + +**Linux/Mac:** +```bash +python -m venv venv +source venv/bin/activate +``` + +### 3) Install dependencies +```bash +python -m pip install --upgrade pip +pip install -r requirements.txt +``` + +## Running the Application + +Default: +```bash +python app.py +``` + +Custom config: + +**Linux/Mac:** +```bash +HOST=127.0.0.1 PORT=3000 DEBUG=True python app.py +``` + +**Windows (PowerShell):** +```powershell +$env:HOST="127.0.0.1" +$env:PORT="3000" +$env:DEBUG="True" +python app.py +``` + +**Windows (CMD):** +```bat +set HOST=127.0.0.1 +set PORT=3000 +set DEBUG=True +python app.py +``` + +## API Endpoints +- `GET /` — service and system information +- `GET /health` — health check + +Examples: +```bash +curl http://127.0.0.1:5000/ +curl http://127.0.0.1:5000/health +``` + +Pretty output: +```bash +curl http://127.0.0.1:5000/ | python -m json.tool +``` + +## Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| HOST | 0.0.0.0 | Bind address | +| PORT | 5000 | HTTP port | +| DEBUG | False | Flask debug mode | + +## Docker + +This application can also be built and run as a Docker container. + +### Build (local) +Pattern: +```bash +docker build -t : . +``` + +### Run +Pattern: +```bash +docker run --rm -p :5000 --name : +``` + +Then test: +```bash +curl http://127.0.0.1:/ +curl http://127.0.0.1:/health +``` + +### Pull from Docker Hub +Pattern: +```bash +docker pull /: +docker run --rm -p :5000 /: +``` + +> Note (Windows PowerShell): `curl` is an alias for `Invoke-WebRequest`. +> For classic curl behavior, use `curl.exe`. + +## Testing +Install dev dependencies: +- python -m pip install -r requirements-dev.txt + +Run tests: +- pytest + diff --git a/app_python/app.py b/app_python/app.py index e42a686025..e05047b8c4 100644 --- a/app_python/app.py +++ b/app_python/app.py @@ -1,118 +1,118 @@ -import os -import socket -import platform -import logging -from datetime import datetime, timezone -from flask import Flask, jsonify, request - -logging.basicConfig( - level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' -) -logger = logging.getLogger(__name__) - -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(timezone.utc) - -def get_uptime(): - """Calculation of the application's operating time.""" - delta = datetime.now(timezone.utc) - START_TIME - seconds = int(delta.total_seconds()) - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - return { - 'seconds': seconds, - 'human': f"{hours} hours, {minutes} minutes" - } - -def get_system_info(): - """Collect system information.""" - return { - 'hostname': socket.gethostname(), - 'platform': platform.system(), - 'platform_version': get_platform_version(), - 'architecture': platform.machine(), - 'cpu_count': os.cpu_count(), - 'python_version': platform.python_version() - } - -@app.route('/') -def index(): - """Main endpoint - service and system information.""" - uptime = get_uptime() - - response = { - 'service': { - 'name': 'devops-info-service', - 'version': '1.0.0', - 'description': 'DevOps course info service', - 'framework': 'Flask' - }, - 'system': get_system_info(), - 'runtime': { - 'uptime_seconds': uptime['seconds'], - 'uptime_human': uptime['human'], - 'current_time': datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"), - 'timezone': 'UTC' - }, - 'request': { - 'client_ip': request.remote_addr, - 'user_agent': request.headers.get('User-Agent'), - 'method': request.method, - 'path': request.path - }, - 'endpoints': [ - {'path': '/', 'method': 'GET', 'description': 'Service information'}, - {'path': '/health', 'method': 'GET', 'description': 'Health check'} - ] - } - - logger.info("Request %s %s from %s", request.method, request.path, request.remote_addr) - return jsonify(response) - -@app.route('/health') -def health(): - """Health check endpoint for monitoring.""" - logger.info("Request %s %s from %s", request.method, request.path, request.remote_addr) - return jsonify({ - 'status': 'healthy', - 'timestamp': datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"), - 'uptime_seconds': get_uptime()['seconds'] - }) - -@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 - -def get_platform_version(): - """Return a platform version.""" - try: - if hasattr(platform, "freedesktop_os_release"): - info = platform.freedesktop_os_release() - if info.get("PRETTY_NAME"): - return info["PRETTY_NAME"] - except Exception: - pass - return platform.platform() - - -if __name__ == '__main__': - logger.info(f'Starting DevOps Info Service on {HOST}:{PORT}') +import os +import socket +import platform +import logging +from datetime import datetime, timezone +from flask import Flask, jsonify, request + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +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(timezone.utc) + +def get_uptime(): + """Calculation of the application's operating time.""" + delta = datetime.now(timezone.utc) - START_TIME + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return { + 'seconds': seconds, + 'human': f"{hours} hours, {minutes} minutes" + } + +def get_system_info(): + """Collect system information.""" + return { + 'hostname': socket.gethostname(), + 'platform': platform.system(), + 'platform_version': get_platform_version(), + 'architecture': platform.machine(), + 'cpu_count': os.cpu_count(), + 'python_version': platform.python_version() + } + +@app.route('/') +def index(): + """Main endpoint - service and system information.""" + uptime = get_uptime() + + response = { + 'service': { + 'name': 'devops-info-service', + 'version': '1.0.0', + 'description': 'DevOps course info service', + 'framework': 'Flask' + }, + 'system': get_system_info(), + 'runtime': { + 'uptime_seconds': uptime['seconds'], + 'uptime_human': uptime['human'], + 'current_time': datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"), + 'timezone': 'UTC' + }, + 'request': { + 'client_ip': request.remote_addr, + 'user_agent': request.headers.get('User-Agent'), + 'method': request.method, + 'path': request.path + }, + 'endpoints': [ + {'path': '/', 'method': 'GET', 'description': 'Service information'}, + {'path': '/health', 'method': 'GET', 'description': 'Health check'} + ] + } + + logger.info("Request %s %s from %s", request.method, request.path, request.remote_addr) + return jsonify(response) + +@app.route('/health') +def health(): + """Health check endpoint for monitoring.""" + logger.info("Request %s %s from %s", request.method, request.path, request.remote_addr) + return jsonify({ + 'status': 'healthy', + 'timestamp': datetime.now(timezone.utc).isoformat(timespec="milliseconds").replace("+00:00", "Z"), + 'uptime_seconds': get_uptime()['seconds'] + }) + +@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 + +def get_platform_version(): + """Return a platform version.""" + try: + if hasattr(platform, "freedesktop_os_release"): + info = platform.freedesktop_os_release() + if info.get("PRETTY_NAME"): + return info["PRETTY_NAME"] + except Exception: + pass + return platform.platform() + + +if __name__ == '__main__': + logger.info(f'Starting DevOps Info Service on {HOST}:{PORT}') app.run(host=HOST, port=PORT, debug=DEBUG) \ No newline at end of file diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md index 909fcf5ec5..ff0d6e655f 100644 --- a/app_python/docs/LAB01.md +++ b/app_python/docs/LAB01.md @@ -1,168 +1,168 @@ -# LAB01 — DevOps Info Service (Python) - -## 1. Framework Selection - -**Chosen framework:** Flask - -### Why Flask -- **Lightweight and simple**: perfect for a small service with 2 endpoints. -- **Fast to start**: minimal boilerplate, easy routing. -- **Good for DevOps labs**: focus stays on environment/configuration, containerization and CI/CD. - -### Comparison - -| Framework | Pros | Cons | Decision | -|----------|------|------|----------| -| Flask | Simple, lightweight, flexible | Fewer built-in features than Django/FastAPI | **Chosen** | -| FastAPI | Async support, automatic OpenAPI docs, modern typing | More concepts (Pydantic, async) for beginners | Not chosen | -| Django | Full-featured framework (ORM, admin, auth) | Heavy/overkill for 2 simple endpoints | Not chosen | - -## 2. Best Practices Applied - -### 2.1 Clean Code Organization -**What:** Logic is separated into small functions: `get_uptime()` and `get_system_info()`. - -**Why it matters:** Improves readability, reuse, and makes testing easier (Lab 3). - -**Code example:** -```python -START_TIME = datetime.now(timezone.utc) - -def get_uptime(): - delta = datetime.now(timezone.utc) - START_TIME - seconds = int(delta.total_seconds()) - hours = seconds // 3600 - minutes = (seconds % 3600) // 60 - return {"seconds": seconds, "human": f"{hours} hours, {minutes} minutes"} -``` - -### 2.2 Configuration via Environment Variables -**What:** App settings are controlled by env vars with defaults. - -**Why it matters:** Makes the application portable across environments (local, Docker, Kubernetes). - -**Code example:** -```python -HOST = os.getenv("HOST", "0.0.0.0") -PORT = int(os.getenv("PORT", 5000)) -DEBUG = os.getenv("DEBUG", "False").lower() == "true" -``` - -### 2.3 Error Handling (JSON Responses) -**What:** Custom JSON handlers for common errors (404, 500). - -**Why it matters:** API stays consistent (always JSON), easier monitoring and debugging. - -**Code example:** -```python -@app.errorhandler(404) -def not_found(error): - return jsonify({"error": "Not Found", "message": "Endpoint does not exist"}), 404 -``` - -### 2.4 Logging -**What:** Logging is configured with timestamps and log levels. - -**Why it matters:** Logs are essential for troubleshooting, monitoring, and production readiness. - -**Code example:** -```python -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" -) -logger = logging.getLogger(__name__) - -logger.info("Request %s %s from %s", request.method, request.path, request.remote_addr) -``` - -## 3. API Documentation - -> Note: In my local environment I ran the service on port **8080** (via `PORT=8080`). -> Default port in code is **5000** when `PORT` is not set. - -### 3.1 GET / -**Purpose:** Returns service metadata, system information, runtime details, request info, and available endpoints. - -**Request example:** -```bash -curl http://127.0.0.1:8080/ -``` - -**Response example (shortened):** -```json -{ - "service": { - "name": "devops-info-service", - "version": "1.0.0", - "description": "DevOps course info service", - "framework": "Flask" - }, - "system": { - "hostname": "my-host", - "platform": "Windows", - "architecture": "AMD64", - "cpu_count": 20, - "python_version": "3.14.0" - }, - "runtime": { - "uptime_seconds": 1302, - "timezone": "UTC" - }, - "request": { - "client_ip": "127.0.0.1", - "method": "GET", - "path": "/" - } -} -``` - -### 3.2 GET /health -**Purpose:** Health check endpoint for monitoring (used later for Kubernetes probes). - -**Request example:** -```bash -curl -i http://127.0.0.1:8080/health -``` - -**Response example:** -```json -{ - "status": "healthy", - "timestamp": "2026-01-28T13:41:14.751Z", - "uptime_seconds": 918 -} -``` - -### 3.3 Testing Commands - -**Pretty-printed JSON output:** -```bash -curl -s http://127.0.0.1:8080/ | python -m json.tool -``` - -## 4. Testing Evidence - -Screenshots are included in `app_python/docs/screenshots/`: - -- `01-main-endpoint.png` — Main endpoint (`GET /`) showing complete JSON output -- `02-health-check.png` — Health endpoint (`GET /health`) including HTTP 200 status -- `03-formatted-output.png` — Pretty-printed JSON output from terminal (`curl -s ... | python -m json.tool`) - -## 5. Challenges & Solutions - -**Problem:** On Windows, `python app.py` did not work, while `py app.py` worked. - -**Solution:** -- Created and activated a virtual environment using: - ```bash - py -m venv venv - ``` -- After activation, `python` points to the venv interpreter. -- Disabled Windows Store Python execution aliases (App Execution Aliases) so `python` runs the correct interpreter. - -## 6. GitHub Community - -**Why starring repositories matters:** Stars increase visibility of useful projects and are a convenient way to bookmark tools and libraries; they also encourage maintainers. - -**Why following developers helps:** Following the professor, TAs, and classmates supports collaboration, helps discover solutions and best practices through activity feeds, and builds a professional network. +# LAB01 — DevOps Info Service (Python) + +## 1. Framework Selection + +**Chosen framework:** Flask + +### Why Flask +- **Lightweight and simple**: perfect for a small service with 2 endpoints. +- **Fast to start**: minimal boilerplate, easy routing. +- **Good for DevOps labs**: focus stays on environment/configuration, containerization and CI/CD. + +### Comparison + +| Framework | Pros | Cons | Decision | +|----------|------|------|----------| +| Flask | Simple, lightweight, flexible | Fewer built-in features than Django/FastAPI | **Chosen** | +| FastAPI | Async support, automatic OpenAPI docs, modern typing | More concepts (Pydantic, async) for beginners | Not chosen | +| Django | Full-featured framework (ORM, admin, auth) | Heavy/overkill for 2 simple endpoints | Not chosen | + +## 2. Best Practices Applied + +### 2.1 Clean Code Organization +**What:** Logic is separated into small functions: `get_uptime()` and `get_system_info()`. + +**Why it matters:** Improves readability, reuse, and makes testing easier (Lab 3). + +**Code example:** +```python +START_TIME = datetime.now(timezone.utc) + +def get_uptime(): + delta = datetime.now(timezone.utc) - START_TIME + seconds = int(delta.total_seconds()) + hours = seconds // 3600 + minutes = (seconds % 3600) // 60 + return {"seconds": seconds, "human": f"{hours} hours, {minutes} minutes"} +``` + +### 2.2 Configuration via Environment Variables +**What:** App settings are controlled by env vars with defaults. + +**Why it matters:** Makes the application portable across environments (local, Docker, Kubernetes). + +**Code example:** +```python +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" +``` + +### 2.3 Error Handling (JSON Responses) +**What:** Custom JSON handlers for common errors (404, 500). + +**Why it matters:** API stays consistent (always JSON), easier monitoring and debugging. + +**Code example:** +```python +@app.errorhandler(404) +def not_found(error): + return jsonify({"error": "Not Found", "message": "Endpoint does not exist"}), 404 +``` + +### 2.4 Logging +**What:** Logging is configured with timestamps and log levels. + +**Why it matters:** Logs are essential for troubleshooting, monitoring, and production readiness. + +**Code example:** +```python +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + +logger.info("Request %s %s from %s", request.method, request.path, request.remote_addr) +``` + +## 3. API Documentation + +> Note: In my local environment I ran the service on port **8080** (via `PORT=8080`). +> Default port in code is **5000** when `PORT` is not set. + +### 3.1 GET / +**Purpose:** Returns service metadata, system information, runtime details, request info, and available endpoints. + +**Request example:** +```bash +curl http://127.0.0.1:8080/ +``` + +**Response example (shortened):** +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask" + }, + "system": { + "hostname": "my-host", + "platform": "Windows", + "architecture": "AMD64", + "cpu_count": 20, + "python_version": "3.14.0" + }, + "runtime": { + "uptime_seconds": 1302, + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1", + "method": "GET", + "path": "/" + } +} +``` + +### 3.2 GET /health +**Purpose:** Health check endpoint for monitoring (used later for Kubernetes probes). + +**Request example:** +```bash +curl -i http://127.0.0.1:8080/health +``` + +**Response example:** +```json +{ + "status": "healthy", + "timestamp": "2026-01-28T13:41:14.751Z", + "uptime_seconds": 918 +} +``` + +### 3.3 Testing Commands + +**Pretty-printed JSON output:** +```bash +curl -s http://127.0.0.1:8080/ | python -m json.tool +``` + +## 4. Testing Evidence + +Screenshots are included in `app_python/docs/screenshots/`: + +- `01-main-endpoint.png` — Main endpoint (`GET /`) showing complete JSON output +- `02-health-check.png` — Health endpoint (`GET /health`) including HTTP 200 status +- `03-formatted-output.png` — Pretty-printed JSON output from terminal (`curl -s ... | python -m json.tool`) + +## 5. Challenges & Solutions + +**Problem:** On Windows, `python app.py` did not work, while `py app.py` worked. + +**Solution:** +- Created and activated a virtual environment using: + ```bash + py -m venv venv + ``` +- After activation, `python` points to the venv interpreter. +- Disabled Windows Store Python execution aliases (App Execution Aliases) so `python` runs the correct interpreter. + +## 6. GitHub Community + +**Why starring repositories matters:** Stars increase visibility of useful projects and are a convenient way to bookmark tools and libraries; they also encourage maintainers. + +**Why following developers helps:** Following the professor, TAs, and classmates supports collaboration, helps discover solutions and best practices through activity feeds, and builds a professional network. diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md index 9ed13b479c..b631f739f9 100644 --- a/app_python/docs/LAB02.md +++ b/app_python/docs/LAB02.md @@ -1,235 +1,235 @@ -# LAB02 — Docker Containerization (Python) - -## 1. Docker Best Practices Applied - -### 1.1 Fixed base image version (pinned tag) -**What I did:** Used `python:3.13-slim` in `Dockerfile`. -**Why it matters:** A fixed base image version makes builds reproducible and predictable. The `slim` variant reduces image size compared to full images, speeds up pulls/builds, and reduces the attack surface. - -**Snippet:** -```dockerfile -FROM python:3.13-slim -``` - -### 1.2 Non-root user (mandatory) -**What I did:** Created a system user/group `app` and switched to it via `USER app`. -**Why it matters:** Running as non-root limits privileges inside the container. If the app is compromised, the attacker has fewer permissions, which is a baseline production security practice. - -**Snippet:** -```dockerfile -RUN addgroup --system app && adduser --system --ingroup app app -USER app -``` - -### 1.3 Layer caching (dependencies before application code) -**What I did:** Copied `requirements.txt` and installed dependencies **before** copying `app.py`. -**Why it matters:** Docker caches layers. If only source code changes, the dependency layer stays cached and rebuilds are much faster. - -**Snippet:** -```dockerfile -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt -COPY app.py . -``` - -### 1.4 Minimal copy + `.dockerignore` -**What I did:** Copied only runtime-needed files into the image (`requirements.txt`, `app.py`) and used `.dockerignore` to exclude unnecessary files (venv, docs, git, caches). -**Why it matters:** Smaller build context → faster build. Smaller final image → faster push/pull and reduced risk of leaking development artifacts into production images. - ---- - -## 2. Image Information & Decisions - -### 2.1 Base image decision -**Chosen:** `python:3.13-slim` -**Justification:** -- Modern Python runtime version for container execution -- `slim` gives good balance of small size + compatibility (Debian-based) -- Avoids common issues seen with `alpine` images (musl / Python wheels) - -### 2.2 Final image size and assessment -Output: -```text -IMAGE ID DISK USAGE CONTENT SIZE EXTRA -devops-info-service:lab02 dc2fdac78d0d 182MB 44.4MB -``` - -**Assessment:** Content size (~44.4MB) is reasonable for a small Flask app running on Debian-slim. Disk usage is higher due to local storage/overhead, but still acceptable for this lab. Further reductions are possible (e.g., using a production WSGI server, minimizing base layers, or alternative minimal images), but this already follows recommended best practices for beginner containerization. - -### 2.3 Layer structure explanation -High-level layers: -1. Base image `python:3.13-slim` -2. Environment variables for predictable Python behavior -3. Create non-root user/group -4. Set working directory -5. Copy and install dependencies (`requirements.txt` → `pip install`) -6. Copy application code (`app.py`) -7. Switch to non-root user -8. Expose port (documentation) -9. Start application with `CMD` - -**Evidence (`docker history`):** -```text -IMAGE CREATED CREATED BY SIZE COMMENT -dc2fdac78d0d 47 minutes ago CMD ["python" "app.py"] 0B buildkit.dockerfile.v0 - 47 minutes ago EXPOSE [5000/tcp] 0B buildkit.dockerfile.v0 - 47 minutes ago USER app 0B buildkit.dockerfile.v0 - 47 minutes ago COPY app.py . # buildkit 12.3kB buildkit.dockerfile.v0 - 47 minutes ago RUN /bin/sh -c pip install --no-cache-dir -r… 5.53MB buildkit.dockerfile.v0 - 47 minutes ago COPY requirements.txt . # buildkit 12.3kB buildkit.dockerfile.v0 - 47 minutes ago WORKDIR /app 8.19kB buildkit.dockerfile.v0 - 47 minutes ago RUN /bin/sh -c addgroup --system app && addu… 45.1kB buildkit.dockerfile.v0 - 47 minutes ago ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFER… 0B buildkit.dockerfile.v0 -... -``` - -### 2.4 Optimization choices -- `python:3.13-slim` for smaller base -- `pip install --no-cache-dir` to avoid storing pip cache inside the image -- Copy only necessary runtime files (no repo-wide `COPY . .`) -- `.dockerignore` reduces build context size and avoids shipping `venv/`, `.git/`, `docs/`, caches - ---- - -## 3. Build & Run Process - -### 3.1 Build output -Command: -```bash -docker build -t devops-info-service:lab02 . -``` - -Terminal output (excerpt): -```text -[+] Building 75.9s (15/15) FINISHED - => [internal] load build definition from Dockerfile - => [internal] load metadata for docker.io/library/python:3.13-slim - => [internal] load .dockerignore - => [1/6] FROM docker.io/library/python:3.13-slim@sha256:2b9c9803... - => [2/6] RUN addgroup --system app && adduser --system --ingroup app app - => [3/6] WORKDIR /app - => [4/6] COPY requirements.txt . - => [5/6] RUN pip install --no-cache-dir -r requirements.txt - => [6/6] COPY app.py . - => exporting to image - => naming to docker.io/library/devops-info-service:lab02 -``` - -### 3.2 Container run output (local image) -Command: -```bash -docker run --rm -p 8080:5000 --name devops-info devops-info-service:lab02 -``` - -Terminal output: -```text -2026-02-04 17:36:37,431 - __main__ - INFO - Starting DevOps Info Service on 0.0.0.0:5000 - * Serving Flask app 'app' - * Debug mode: off - * Running on all addresses (0.0.0.0) - * Running on http://127.0.0.1:5000 - * Running on http://172.17.0.2:5000 -Press CTRL+C to quit -``` - -**Proof container is running (`docker ps`):** -```text -CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES -0012d755fc1b devops-info-service:lab02 "python app.py" 7 seconds ago Up 6 seconds 0.0.0.0:8080->5000/tcp, [::]:8080->5000/tcp devops-info -``` - -### 3.3 Endpoint testing output -Commands: -```bash -curl http://127.0.0.1:8080/ -curl http://127.0.0.1:8080/health -``` - -Expected results: -- HTTP 200 on both endpoints -- JSON payload returned -- Server header shows Werkzeug + Python 3.13.11 inside container - -Example evidence (health): -```text -StatusCode : 200 -StatusDescription : OK -Content : {"status":"healthy","timestamp":"2026-02-04T17:22:41.912Z","uptime_seconds":68} -Server : Werkzeug/3.1.5 Python/3.13.11 -``` - -### 3.4 Docker Hub push + pull verification -Login: -```text -docker login -Login Succeeded -``` - -Tagging strategy: -- `cdeth567/devops-info-service:lab02` — fixed tag for lab submission -- `cdeth567/devops-info-service:latest` — convenience tag for most recent build - -Push output: -```text -docker push cdeth567/devops-info-service:lab02 -... -lab02: digest: sha256:dc2fdac78d0d5b5e75c3da6a21682aacfdef926ff648356baf54d0437a3d81ec size: 856 - -docker push cdeth567/devops-info-service:latest -... -latest: digest: sha256:dc2fdac78d0d5b5e75c3da6a21682aacfdef926ff648356baf54d0437a3d81ec size: 856 -``` - -Pull verification: -```text -docker pull cdeth567/devops-info-service:lab02 -Status: Image is up to date for cdeth567/devops-info-service:lab02 -``` - -**Docker Hub repository URL:** -https://hub.docker.com/r/cdeth567/devops-info-service - ---- - -## 4. Technical Analysis - -### 4.1 Why does this Dockerfile work the way it does? -- `CMD ["python", "app.py"]` starts the app the same way as local development. -- The app binds to `0.0.0.0` by default (`HOST=0.0.0.0`), so it is reachable from outside the container. -- Port mapping `-p 8080:5000` exposes container port 5000 on host port 8080. -- Dependencies are installed from `requirements.txt` inside the image, making runtime self-contained and portable. - -### 4.2 What would happen if I changed the layer order? -If application code was copied before installing dependencies (e.g., `COPY . .` first), then every code change would invalidate the dependency layer cache and force `pip install` to run again. This would slow down rebuilds significantly. - -### 4.3 Security considerations implemented -- Non-root execution (`USER app`) -- Slim base image reduces installed packages → smaller attack surface -- `.dockerignore` prevents shipping local artifacts (venv, git metadata, docs) into the container image - -### 4.4 How does `.dockerignore` improve the build? -- Reduces build context size → faster builds -- Prevents accidental inclusion of `venv/`, `.git/`, `docs/`, caches -- Lowers risk of leaking local files into the container image - ---- - -## 5. Challenges & Solutions - -### 5.1 Port already allocated (8080) -**Issue:** While testing the Docker Hub image, Docker returned: -`Bind for 0.0.0.0:8080 failed: port is already allocated` - -**Cause:** Another running container was already mapped to host port 8080. - -**Solution:** Stopped the running container (Ctrl+C) or used a different host port mapping (e.g. `-p 8081:5000`). - -### 5.2 PowerShell `curl` warning -**Issue:** PowerShell shows a security warning because `curl` is an alias for `Invoke-WebRequest`. -**Solution:** Confirmed prompt once (“A” = Yes to All) and verified endpoints still return HTTP 200 with JSON. - -### 5.3 What I learned -- Dockerfile layer order strongly impacts rebuild speed due to caching. -- Running as non-root is a simple but important security requirement. -- Host port mapping requires a free port; multiple containers cannot bind the same host port simultaneously. -- `.dockerignore` is important both for performance (smaller context) and security (no accidental file leaks). +# LAB02 — Docker Containerization (Python) + +## 1. Docker Best Practices Applied + +### 1.1 Fixed base image version (pinned tag) +**What I did:** Used `python:3.13-slim` in `Dockerfile`. +**Why it matters:** A fixed base image version makes builds reproducible and predictable. The `slim` variant reduces image size compared to full images, speeds up pulls/builds, and reduces the attack surface. + +**Snippet:** +```dockerfile +FROM python:3.13-slim +``` + +### 1.2 Non-root user (mandatory) +**What I did:** Created a system user/group `app` and switched to it via `USER app`. +**Why it matters:** Running as non-root limits privileges inside the container. If the app is compromised, the attacker has fewer permissions, which is a baseline production security practice. + +**Snippet:** +```dockerfile +RUN addgroup --system app && adduser --system --ingroup app app +USER app +``` + +### 1.3 Layer caching (dependencies before application code) +**What I did:** Copied `requirements.txt` and installed dependencies **before** copying `app.py`. +**Why it matters:** Docker caches layers. If only source code changes, the dependency layer stays cached and rebuilds are much faster. + +**Snippet:** +```dockerfile +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY app.py . +``` + +### 1.4 Minimal copy + `.dockerignore` +**What I did:** Copied only runtime-needed files into the image (`requirements.txt`, `app.py`) and used `.dockerignore` to exclude unnecessary files (venv, docs, git, caches). +**Why it matters:** Smaller build context → faster build. Smaller final image → faster push/pull and reduced risk of leaking development artifacts into production images. + +--- + +## 2. Image Information & Decisions + +### 2.1 Base image decision +**Chosen:** `python:3.13-slim` +**Justification:** +- Modern Python runtime version for container execution +- `slim` gives good balance of small size + compatibility (Debian-based) +- Avoids common issues seen with `alpine` images (musl / Python wheels) + +### 2.2 Final image size and assessment +Output: +```text +IMAGE ID DISK USAGE CONTENT SIZE EXTRA +devops-info-service:lab02 dc2fdac78d0d 182MB 44.4MB +``` + +**Assessment:** Content size (~44.4MB) is reasonable for a small Flask app running on Debian-slim. Disk usage is higher due to local storage/overhead, but still acceptable for this lab. Further reductions are possible (e.g., using a production WSGI server, minimizing base layers, or alternative minimal images), but this already follows recommended best practices for beginner containerization. + +### 2.3 Layer structure explanation +High-level layers: +1. Base image `python:3.13-slim` +2. Environment variables for predictable Python behavior +3. Create non-root user/group +4. Set working directory +5. Copy and install dependencies (`requirements.txt` → `pip install`) +6. Copy application code (`app.py`) +7. Switch to non-root user +8. Expose port (documentation) +9. Start application with `CMD` + +**Evidence (`docker history`):** +```text +IMAGE CREATED CREATED BY SIZE COMMENT +dc2fdac78d0d 47 minutes ago CMD ["python" "app.py"] 0B buildkit.dockerfile.v0 + 47 minutes ago EXPOSE [5000/tcp] 0B buildkit.dockerfile.v0 + 47 minutes ago USER app 0B buildkit.dockerfile.v0 + 47 minutes ago COPY app.py . # buildkit 12.3kB buildkit.dockerfile.v0 + 47 minutes ago RUN /bin/sh -c pip install --no-cache-dir -r… 5.53MB buildkit.dockerfile.v0 + 47 minutes ago COPY requirements.txt . # buildkit 12.3kB buildkit.dockerfile.v0 + 47 minutes ago WORKDIR /app 8.19kB buildkit.dockerfile.v0 + 47 minutes ago RUN /bin/sh -c addgroup --system app && addu… 45.1kB buildkit.dockerfile.v0 + 47 minutes ago ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFER… 0B buildkit.dockerfile.v0 +... +``` + +### 2.4 Optimization choices +- `python:3.13-slim` for smaller base +- `pip install --no-cache-dir` to avoid storing pip cache inside the image +- Copy only necessary runtime files (no repo-wide `COPY . .`) +- `.dockerignore` reduces build context size and avoids shipping `venv/`, `.git/`, `docs/`, caches + +--- + +## 3. Build & Run Process + +### 3.1 Build output +Command: +```bash +docker build -t devops-info-service:lab02 . +``` + +Terminal output (excerpt): +```text +[+] Building 75.9s (15/15) FINISHED + => [internal] load build definition from Dockerfile + => [internal] load metadata for docker.io/library/python:3.13-slim + => [internal] load .dockerignore + => [1/6] FROM docker.io/library/python:3.13-slim@sha256:2b9c9803... + => [2/6] RUN addgroup --system app && adduser --system --ingroup app app + => [3/6] WORKDIR /app + => [4/6] COPY requirements.txt . + => [5/6] RUN pip install --no-cache-dir -r requirements.txt + => [6/6] COPY app.py . + => exporting to image + => naming to docker.io/library/devops-info-service:lab02 +``` + +### 3.2 Container run output (local image) +Command: +```bash +docker run --rm -p 8080:5000 --name devops-info devops-info-service:lab02 +``` + +Terminal output: +```text +2026-02-04 17:36:37,431 - __main__ - INFO - Starting DevOps Info Service on 0.0.0.0:5000 + * Serving Flask app 'app' + * Debug mode: off + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://172.17.0.2:5000 +Press CTRL+C to quit +``` + +**Proof container is running (`docker ps`):** +```text +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +0012d755fc1b devops-info-service:lab02 "python app.py" 7 seconds ago Up 6 seconds 0.0.0.0:8080->5000/tcp, [::]:8080->5000/tcp devops-info +``` + +### 3.3 Endpoint testing output +Commands: +```bash +curl http://127.0.0.1:8080/ +curl http://127.0.0.1:8080/health +``` + +Expected results: +- HTTP 200 on both endpoints +- JSON payload returned +- Server header shows Werkzeug + Python 3.13.11 inside container + +Example evidence (health): +```text +StatusCode : 200 +StatusDescription : OK +Content : {"status":"healthy","timestamp":"2026-02-04T17:22:41.912Z","uptime_seconds":68} +Server : Werkzeug/3.1.5 Python/3.13.11 +``` + +### 3.4 Docker Hub push + pull verification +Login: +```text +docker login +Login Succeeded +``` + +Tagging strategy: +- `cdeth567/devops-info-service:lab02` — fixed tag for lab submission +- `cdeth567/devops-info-service:latest` — convenience tag for most recent build + +Push output: +```text +docker push cdeth567/devops-info-service:lab02 +... +lab02: digest: sha256:dc2fdac78d0d5b5e75c3da6a21682aacfdef926ff648356baf54d0437a3d81ec size: 856 + +docker push cdeth567/devops-info-service:latest +... +latest: digest: sha256:dc2fdac78d0d5b5e75c3da6a21682aacfdef926ff648356baf54d0437a3d81ec size: 856 +``` + +Pull verification: +```text +docker pull cdeth567/devops-info-service:lab02 +Status: Image is up to date for cdeth567/devops-info-service:lab02 +``` + +**Docker Hub repository URL:** +https://hub.docker.com/r/cdeth567/devops-info-service + +--- + +## 4. Technical Analysis + +### 4.1 Why does this Dockerfile work the way it does? +- `CMD ["python", "app.py"]` starts the app the same way as local development. +- The app binds to `0.0.0.0` by default (`HOST=0.0.0.0`), so it is reachable from outside the container. +- Port mapping `-p 8080:5000` exposes container port 5000 on host port 8080. +- Dependencies are installed from `requirements.txt` inside the image, making runtime self-contained and portable. + +### 4.2 What would happen if I changed the layer order? +If application code was copied before installing dependencies (e.g., `COPY . .` first), then every code change would invalidate the dependency layer cache and force `pip install` to run again. This would slow down rebuilds significantly. + +### 4.3 Security considerations implemented +- Non-root execution (`USER app`) +- Slim base image reduces installed packages → smaller attack surface +- `.dockerignore` prevents shipping local artifacts (venv, git metadata, docs) into the container image + +### 4.4 How does `.dockerignore` improve the build? +- Reduces build context size → faster builds +- Prevents accidental inclusion of `venv/`, `.git/`, `docs/`, caches +- Lowers risk of leaking local files into the container image + +--- + +## 5. Challenges & Solutions + +### 5.1 Port already allocated (8080) +**Issue:** While testing the Docker Hub image, Docker returned: +`Bind for 0.0.0.0:8080 failed: port is already allocated` + +**Cause:** Another running container was already mapped to host port 8080. + +**Solution:** Stopped the running container (Ctrl+C) or used a different host port mapping (e.g. `-p 8081:5000`). + +### 5.2 PowerShell `curl` warning +**Issue:** PowerShell shows a security warning because `curl` is an alias for `Invoke-WebRequest`. +**Solution:** Confirmed prompt once (“A” = Yes to All) and verified endpoints still return HTTP 200 with JSON. + +### 5.3 What I learned +- Dockerfile layer order strongly impacts rebuild speed due to caching. +- Running as non-root is a simple but important security requirement. +- Host port mapping requires a free port; multiple containers cannot bind the same host port simultaneously. +- `.dockerignore` is important both for performance (smaller context) and security (no accidental file leaks). diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md index eeab592380..2256bca136 100644 --- a/app_python/docs/LAB03.md +++ b/app_python/docs/LAB03.md @@ -1,299 +1,299 @@ -# LAB03 — Continuous Integration (CI/CD) - -Repository: `Cdeth567/DevOps-Core-Course` -Branch: `lab03` -App: `app_python` (DevOps Info Service) - ---- - -## 1. Overview - -### 1.1 Testing framework choice -**Framework:** `pytest 8.x` - -**Why pytest:** -- Minimal and readable test syntax (simple `assert` statements) -- Great ecosystem and easy CI integration -- Widely used industry standard for Python services - -**Dev dependencies file:** `app_python/requirements-dev.txt` (contains `pytest==8.3.4` and `ruff==0.9.6`). - ---- - -### 1.2 What tests cover -Tests are located in: `app_python/tests/test_app.py` - -Covered behavior: -- **`GET /`** - - Returns HTTP 200 - - Returns JSON with expected structure/fields -- **`GET /health`** - - Returns HTTP 200 - - Returns JSON with `"status": "healthy"` and expected keys -- Includes multiple assertions → not just "smoke tests" - ---- - -### 1.3 CI workflow trigger configuration -Workflow file: `.github/workflows/python-ci.yml` -Workflow name: **Python CI (tests + docker)** - -Triggers: -- **Push** to branches: `master`, `lab03` -- **PRs** targeting `master` -- **Path filters**: runs only if something changed in: - - `app_python/**` - - `.github/workflows/python-ci.yml` - -Why this matters: -- In monorepos, path filters prevent wasting CI minutes when unrelated files change. -- PR checks still run for code changes that matter. - ---- - -### 1.4 Versioning strategy (Docker images) -**Chosen strategy:** **CalVer** (Calendar Versioning) - -Implementation: -- CI generates version on build: `YYYY.MM.DD` (UTC time) - -Docker tags produced by CI: -- `cdeth567/devops-info-service:` -- `cdeth567/devops-info-service:latest` - -Why CalVer is a good fit here: -- This is a service with frequent small changes. -- It’s easy to understand which build is “today’s”. -- No need to manually manage SemVer releases for a lab service. - ---- - -## 2. Workflow Evidence - -### 2.1 Local installation & test evidence (terminal output) - -Install dev deps: -```text -py -m pip install -r requirements-dev.txt -Successfully installed ... pytest-8.3.4 -``` - -Install runtime deps: -```text -py -m pip install -r requirements.txt -Successfully installed Flask-3.1.0 ... Werkzeug-3.1.5 ... -``` - -Run tests (note about Windows PATH): -```text -pytest : The term 'pytest' is not recognized ... -py -m pytest -q -.... [100%] -4 passed in 0.37s -``` - -**Explanation:** `pytest.exe` was installed into Python Scripts directory not included in PATH. Running `py -m pytest` executes pytest as a module and works reliably on Windows. - ---- - -### 2.2 Linting evidence (ruff) - -After installing `ruff` to dev requirements: -```text -py -m pip install -r requirements-dev.txt -Successfully installed ruff-0.9.6 -``` - -Lint run (correct working directory): -```text -py -m ruff check . -All checks passed! -``` - -Note: The earlier error: -```text -py -m ruff check app_python -app_python:1:1: E902 ... file not found -``` -happened because the command was run inside the `app_python/` directory; there is no nested `app_python/app_python` path. Fix was to lint `.`. - ---- - -### 2.3 CI workflow success evidence -GitHub Actions page shows successful runs on `lab03` (green check). -Badge is green for `lab03` branch. - -Workflow URL (Actions): -- `https://github.com/Cdeth567/DevOps-Core-Course/actions/workflows/python-ci.yml` - -Status badge in `app_python/README.md`: -```markdown -[![Python CI (tests + docker)](https://github.com/Cdeth567/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg?branch=lab03)](https://github.com/Cdeth567/DevOps-Core-Course/actions/workflows/python-ci.yml) -``` - ---- - -### 2.4 Docker Hub evidence -Docker Hub repository: -- `https://hub.docker.com/r/cdeth567/devops-info-service` - -CI pushes images with two tags: -- daily CalVer tag (e.g., `2026.02.11` format) -- `latest` - ---- - -## 3. Best Practices Implemented (CI + Security) - -### 3.1 Dependency caching (pip) -Implemented using `actions/setup-python@v5` built-in caching: -```yaml -with: - cache: "pip" - cache-dependency-path: | - app_python/requirements.txt - app_python/requirements-dev.txt -``` - -Why it matters: -- Cache hits skip downloading packages again -- Faster workflows on repeated runs (especially after first successful run) - -How to measure: -- Compare “Install dependencies” step time on first run vs next run -- GitHub Actions logs will show whether cache was restored - ---- - -### 3.2 Matrix builds (multiple Python versions) -Tests run on **Python 3.12 and 3.13** via matrix: -```yaml -matrix: - python-version: ["3.12", "3.13"] -``` - -Why it matters: -- Detects version-specific problems early (compatibility across supported versions) -- Good practice for production Python services - ---- - -### 3.3 Fail-fast in matrix -Enabled: -```yaml -fail-fast: true -``` - -Why it matters: -- Stops wasting CI minutes once a matrix job fails -- Speeds feedback loop (you see failure sooner) - ---- - -### 3.4 Concurrency control -Implemented: -```yaml -concurrency: - group: python-ci-${{ github.ref }} - cancel-in-progress: true -``` - -Why it matters: -- If you push many commits quickly, old runs are canceled -- Avoids queue buildup and wasted CI time - ---- - -### 3.5 Conditional Docker push (protect secrets + reduce risk) -Docker build/push runs only on **push** to `master` or `lab03`: -```yaml -if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/lab03') -``` - -Why it matters: -- Prevents Docker pushes from PRs (especially forks) -- Helps avoid leaking secrets in untrusted contexts -- Standard CI/CD safety practice - ---- - -### 3.6 Snyk security scanning -Implemented using **Snyk CLI** in the runner environment: -```yaml -- name: Install Snyk CLI - run: npm install -g snyk - -- name: Snyk scan (dependencies) - continue-on-error: true - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - run: | - cd app_python - snyk test --severity-threshold=high --file=requirements.txt -``` - -Why it matters: -- Detects known vulnerable dependencies early in pipeline -- Gives visibility into supply-chain security risks - -Decision: -- `continue-on-error: true` used so CI doesn’t fully block while still reporting vulnerabilities (appropriate for a lab; in production you might fail builds on high/critical). - ---- - -## 4. Key Decisions - -### 4.1 Versioning strategy -**CalVer** used for Docker images (daily tags). -Rationale: simple automation, no manual release tagging required. - -### 4.2 Docker tags produced -- `/devops-info-service:` -- `/devops-info-service:latest` - -### 4.3 Workflow triggers -- Push/PR triggers with **path filters** ensure workflow runs only for Python app + workflow changes. - -### 4.4 Test coverage -- Endpoints `/` and `/health` are tested via Flask test client (no need to start a real server in CI). -- Coverage tool (pytest-cov) was **not added** in this submission (bonus task), but tests provide functional coverage for both endpoints. - ---- - -## 5. Challenges & Fixes - -### 5.1 `pytest` not recognized on Windows -**Problem:** `pytest` command not found because Python Scripts directory isn’t in PATH. -**Fix:** Use `py -m pytest` which runs pytest as a module. - -### 5.2 `ruff` not recognized / wrong path -**Problem 1:** `ruff` not found → it wasn’t installed yet. -**Fix:** Added `ruff==0.9.6` to `requirements-dev.txt`. - -**Problem 2:** `ruff check app_python` from inside `app_python/` caused file-not-found. -**Fix:** Run `py -m ruff check .` from the `app_python/` directory. - -### 5.3 `.github/workflows` location mistake -Initially workflow file was placed under `app_python/.github/workflows/`, which GitHub Actions does **not** detect. -Fix: moved workflow to repo root: `.github/workflows/python-ci.yml`. - -### 5.4 Snyk scanning issues -There were failures while adjusting working directories. -Final solution: run Snyk CLI and `cd app_python` before scanning requirements. - ---- - -## 6. Files Changed / Added (Summary) - -- `.github/workflows/python-ci.yml` — CI workflow (tests + lint + docker push + Snyk) -- `app_python/tests/test_app.py` — pytest unit tests -- `app_python/requirements-dev.txt` — dev dependencies (`pytest`, `ruff`) -- `app_python/README.md` — added CI status badge + testing instructions -- `app_python/docs/LAB03.md` — this documentation - ---- - -## Appendix — Workflow (reference) -Key jobs: -- `test-and-lint` (matrix: 3.12 + 3.13): install deps, ruff lint, pytest, Snyk scan -- `docker-build-and-push`: build + push to Docker Hub with CalVer + latest +# LAB03 — Continuous Integration (CI/CD) + +Repository: `Cdeth567/DevOps-Core-Course` +Branch: `lab03` +App: `app_python` (DevOps Info Service) + +--- + +## 1. Overview + +### 1.1 Testing framework choice +**Framework:** `pytest 8.x` + +**Why pytest:** +- Minimal and readable test syntax (simple `assert` statements) +- Great ecosystem and easy CI integration +- Widely used industry standard for Python services + +**Dev dependencies file:** `app_python/requirements-dev.txt` (contains `pytest==8.3.4` and `ruff==0.9.6`). + +--- + +### 1.2 What tests cover +Tests are located in: `app_python/tests/test_app.py` + +Covered behavior: +- **`GET /`** + - Returns HTTP 200 + - Returns JSON with expected structure/fields +- **`GET /health`** + - Returns HTTP 200 + - Returns JSON with `"status": "healthy"` and expected keys +- Includes multiple assertions → not just "smoke tests" + +--- + +### 1.3 CI workflow trigger configuration +Workflow file: `.github/workflows/python-ci.yml` +Workflow name: **Python CI (tests + docker)** + +Triggers: +- **Push** to branches: `master`, `lab03` +- **PRs** targeting `master` +- **Path filters**: runs only if something changed in: + - `app_python/**` + - `.github/workflows/python-ci.yml` + +Why this matters: +- In monorepos, path filters prevent wasting CI minutes when unrelated files change. +- PR checks still run for code changes that matter. + +--- + +### 1.4 Versioning strategy (Docker images) +**Chosen strategy:** **CalVer** (Calendar Versioning) + +Implementation: +- CI generates version on build: `YYYY.MM.DD` (UTC time) + +Docker tags produced by CI: +- `cdeth567/devops-info-service:` +- `cdeth567/devops-info-service:latest` + +Why CalVer is a good fit here: +- This is a service with frequent small changes. +- It’s easy to understand which build is “today’s”. +- No need to manually manage SemVer releases for a lab service. + +--- + +## 2. Workflow Evidence + +### 2.1 Local installation & test evidence (terminal output) + +Install dev deps: +```text +py -m pip install -r requirements-dev.txt +Successfully installed ... pytest-8.3.4 +``` + +Install runtime deps: +```text +py -m pip install -r requirements.txt +Successfully installed Flask-3.1.0 ... Werkzeug-3.1.5 ... +``` + +Run tests (note about Windows PATH): +```text +pytest : The term 'pytest' is not recognized ... +py -m pytest -q +.... [100%] +4 passed in 0.37s +``` + +**Explanation:** `pytest.exe` was installed into Python Scripts directory not included in PATH. Running `py -m pytest` executes pytest as a module and works reliably on Windows. + +--- + +### 2.2 Linting evidence (ruff) + +After installing `ruff` to dev requirements: +```text +py -m pip install -r requirements-dev.txt +Successfully installed ruff-0.9.6 +``` + +Lint run (correct working directory): +```text +py -m ruff check . +All checks passed! +``` + +Note: The earlier error: +```text +py -m ruff check app_python +app_python:1:1: E902 ... file not found +``` +happened because the command was run inside the `app_python/` directory; there is no nested `app_python/app_python` path. Fix was to lint `.`. + +--- + +### 2.3 CI workflow success evidence +GitHub Actions page shows successful runs on `lab03` (green check). +Badge is green for `lab03` branch. + +Workflow URL (Actions): +- `https://github.com/Cdeth567/DevOps-Core-Course/actions/workflows/python-ci.yml` + +Status badge in `app_python/README.md`: +```markdown +[![Python CI (tests + docker)](https://github.com/Cdeth567/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg?branch=lab03)](https://github.com/Cdeth567/DevOps-Core-Course/actions/workflows/python-ci.yml) +``` + +--- + +### 2.4 Docker Hub evidence +Docker Hub repository: +- `https://hub.docker.com/r/cdeth567/devops-info-service` + +CI pushes images with two tags: +- daily CalVer tag (e.g., `2026.02.11` format) +- `latest` + +--- + +## 3. Best Practices Implemented (CI + Security) + +### 3.1 Dependency caching (pip) +Implemented using `actions/setup-python@v5` built-in caching: +```yaml +with: + cache: "pip" + cache-dependency-path: | + app_python/requirements.txt + app_python/requirements-dev.txt +``` + +Why it matters: +- Cache hits skip downloading packages again +- Faster workflows on repeated runs (especially after first successful run) + +How to measure: +- Compare “Install dependencies” step time on first run vs next run +- GitHub Actions logs will show whether cache was restored + +--- + +### 3.2 Matrix builds (multiple Python versions) +Tests run on **Python 3.12 and 3.13** via matrix: +```yaml +matrix: + python-version: ["3.12", "3.13"] +``` + +Why it matters: +- Detects version-specific problems early (compatibility across supported versions) +- Good practice for production Python services + +--- + +### 3.3 Fail-fast in matrix +Enabled: +```yaml +fail-fast: true +``` + +Why it matters: +- Stops wasting CI minutes once a matrix job fails +- Speeds feedback loop (you see failure sooner) + +--- + +### 3.4 Concurrency control +Implemented: +```yaml +concurrency: + group: python-ci-${{ github.ref }} + cancel-in-progress: true +``` + +Why it matters: +- If you push many commits quickly, old runs are canceled +- Avoids queue buildup and wasted CI time + +--- + +### 3.5 Conditional Docker push (protect secrets + reduce risk) +Docker build/push runs only on **push** to `master` or `lab03`: +```yaml +if: github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/lab03') +``` + +Why it matters: +- Prevents Docker pushes from PRs (especially forks) +- Helps avoid leaking secrets in untrusted contexts +- Standard CI/CD safety practice + +--- + +### 3.6 Snyk security scanning +Implemented using **Snyk CLI** in the runner environment: +```yaml +- name: Install Snyk CLI + run: npm install -g snyk + +- name: Snyk scan (dependencies) + continue-on-error: true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + run: | + cd app_python + snyk test --severity-threshold=high --file=requirements.txt +``` + +Why it matters: +- Detects known vulnerable dependencies early in pipeline +- Gives visibility into supply-chain security risks + +Decision: +- `continue-on-error: true` used so CI doesn’t fully block while still reporting vulnerabilities (appropriate for a lab; in production you might fail builds on high/critical). + +--- + +## 4. Key Decisions + +### 4.1 Versioning strategy +**CalVer** used for Docker images (daily tags). +Rationale: simple automation, no manual release tagging required. + +### 4.2 Docker tags produced +- `/devops-info-service:` +- `/devops-info-service:latest` + +### 4.3 Workflow triggers +- Push/PR triggers with **path filters** ensure workflow runs only for Python app + workflow changes. + +### 4.4 Test coverage +- Endpoints `/` and `/health` are tested via Flask test client (no need to start a real server in CI). +- Coverage tool (pytest-cov) was **not added** in this submission (bonus task), but tests provide functional coverage for both endpoints. + +--- + +## 5. Challenges & Fixes + +### 5.1 `pytest` not recognized on Windows +**Problem:** `pytest` command not found because Python Scripts directory isn’t in PATH. +**Fix:** Use `py -m pytest` which runs pytest as a module. + +### 5.2 `ruff` not recognized / wrong path +**Problem 1:** `ruff` not found → it wasn’t installed yet. +**Fix:** Added `ruff==0.9.6` to `requirements-dev.txt`. + +**Problem 2:** `ruff check app_python` from inside `app_python/` caused file-not-found. +**Fix:** Run `py -m ruff check .` from the `app_python/` directory. + +### 5.3 `.github/workflows` location mistake +Initially workflow file was placed under `app_python/.github/workflows/`, which GitHub Actions does **not** detect. +Fix: moved workflow to repo root: `.github/workflows/python-ci.yml`. + +### 5.4 Snyk scanning issues +There were failures while adjusting working directories. +Final solution: run Snyk CLI and `cd app_python` before scanning requirements. + +--- + +## 6. Files Changed / Added (Summary) + +- `.github/workflows/python-ci.yml` — CI workflow (tests + lint + docker push + Snyk) +- `app_python/tests/test_app.py` — pytest unit tests +- `app_python/requirements-dev.txt` — dev dependencies (`pytest`, `ruff`) +- `app_python/README.md` — added CI status badge + testing instructions +- `app_python/docs/LAB03.md` — this documentation + +--- + +## Appendix — Workflow (reference) +Key jobs: +- `test-and-lint` (matrix: 3.12 + 3.13): install deps, ruff lint, pytest, Snyk scan +- `docker-build-and-push`: build + push to Docker Hub with CalVer + latest diff --git a/app_python/docs/LAB04.md b/app_python/docs/LAB04.md index 31c1430dc7..f26309837e 100644 --- a/app_python/docs/LAB04.md +++ b/app_python/docs/LAB04.md @@ -1,213 +1,213 @@ -# LAB04 — Infrastructure as Code (Terraform & Pulumi) - -> Course: DevOps Core Course — Lab 4 -> Topic: Infrastructure as Code (Terraform + Pulumi) -> Cloud provider: **Yandex Cloud** -> Date: 2026-02-19 - ---- - -## 1. Cloud Provider & Infrastructure - -### Cloud provider chosen and rationale -I used **Yandex Cloud** because it is accessible from Russia and provides a free-tier friendly VM configuration that matches the lab requirements (small VM + simple networking and firewall rules). - -### Region / Zone -- Zone: `ru-central1-a` - -### Instance size (free-tier friendly) -- Platform: `standard-v2` -- vCPU: `2` with `core_fraction = 20` -- RAM: `1 GB` -- Boot disk: `10 GB` (`network-hdd`) - -### Estimated cost -- Expected cost: **$0** (free-tier / minimal resources) - -### Resources created -Using IaC, the following resources are provisioned: -- VPC network -- Subnet -- Security group with rules: - - SSH 22 — only from my IP (`95.111.204.70/32`) - - HTTP 80 — open to `0.0.0.0/0` - - App port 5000 — open to `0.0.0.0/0` -- Compute instance (VM) with NAT public IP - ---- - -## 2. Terraform Implementation - -### Terraform version -```text - -``` - -### Project structure -``` -terraform/ -├── main.tf -├── variables.tf -├── outputs.tf -├── versions.tf -├── terraform.tfvars (gitignored) -└── .gitignore -``` - -### Authentication (Yandex Cloud) -Authentication is done via **Service Account authorized key (JSON)**. - -- Service account key file (local path, not committed): - - `C:/Users/11kvv/.yc/lab04-sa-key.json` - -> Important: credential files (`*.json`) and state are excluded from Git. - -### Key configuration decisions -- **SSH access restricted** to my public IP only: `95.111.204.70/32` -- Public ports required by lab (80 and 5000) are open to the internet. -- Outputs exported: - - VM public IP - - SSH command string - -### Challenges encountered & fixes -1) **Provider authentication missing** -- Error: `one of 'token' or 'service_account_key_file' should be specified` -- Fix: generated service account key and configured `service_account_key_file`. - -2) **PermissionDenied when creating security group ingress** -- Error: `Permission denied to add ingress rule to security group` -- Fix: updated IAM roles for the service account (VPC permissions) and re-ran `terraform apply`. - -### Terminal output (sanitized) - -#### `terraform init` -```text -Initializing the backend... -Initializing provider plugins... -- Using previously-installed yandex-cloud/yandex v0.187.0 - -Terraform has been successfully initialized! -``` - -#### `terraform plan` (excerpt) -```text -Plan: 2 to add, 0 to change, 0 to destroy. - -Changes to Outputs: - + public_ip = (known after apply) - + ssh_command = (known after apply) -``` - -> Note: I also saw a warning: “Cannot connect to YC tool initialization service...”. This warning did not block plan generation. - -#### `terraform apply` (result) -```text - -``` - -### Outputs -```text -Public IP: -SSH command (from output): - -``` - -### SSH proof -```text - -``` - -Example command (Windows): -```powershell -ssh -i C:\Users\11kvv\.ssh\lab04_ed25519 ubuntu@ -``` - ---- - -## 3. Pulumi Implementation - -### Pulumi version and language -- Language: **Python** -```text - -``` - -### Cleanup of Terraform resources -Before provisioning the same infrastructure with Pulumi, Terraform resources were destroyed: - -```text - -``` - -### Pulumi project structure -``` -pulumi/ -├── __main__.py -├── requirements.txt -├── Pulumi.yaml -└── Pulumi..yaml (gitignored if contains secrets) -``` - -### Planned changes (`pulumi preview`) -```text - -``` - -### Apply (`pulumi up`) -```text - -``` - -### Outputs and SSH proof -```text -Pulumi public IP: -SSH proof: - -``` - ---- - -## 4. Terraform vs Pulumi Comparison - -### Ease of Learning -Terraform was easier to start with because HCL is concise and the workflow is very straightforward (`init → plan → apply`). Pulumi required a bit more setup (runtime, deps) and code structure, but felt natural once configured. - -### Code Readability -Terraform is very readable for simple infra because it is declarative and compact. Pulumi is more verbose but benefits from real language features (variables, functions, reuse) which can help as the project grows. - -### Debugging -Terraform errors are often direct and tied to a specific resource block. In Pulumi, errors can appear deeper in the program flow, but the ability to print/debug in code can help. - -### Documentation -Terraform has a large ecosystem and many examples. Pulumi documentation is also strong, especially when you already know the language SDK, but examples for some providers may be fewer. - -### Use Case -- Terraform: best for standard, repeatable infra with a simple declarative model, especially in teams. -- Pulumi: best when infrastructure needs non-trivial logic/reuse and you want to leverage full programming languages and testing. - ---- - -## 5. Lab 5 Preparation & Cleanup - -### VM for Lab 5 -- Keeping a VM for Lab 5 (Ansible): **** -- If YES: Which one: **Terraform / Pulumi** -- Reason: - - - -### Cleanup status -- Terraform resources destroyed: **** -- Pulumi resources destroyed: **** -- Proof (outputs/log excerpts): -```text - -``` - ---- - -## Appendix — Security & Git hygiene - -- `terraform.tfstate` and `terraform.tfvars` are not committed. -- Service account key `*.json` is not committed. -- SSH private key is not committed. -- `.gitignore` contains patterns for state and secrets. +# LAB04 — Infrastructure as Code (Terraform & Pulumi) + +> Course: DevOps Core Course — Lab 4 +> Topic: Infrastructure as Code (Terraform + Pulumi) +> Cloud provider: **Yandex Cloud** +> Date: 2026-02-19 + +--- + +## 1. Cloud Provider & Infrastructure + +### Cloud provider chosen and rationale +I used **Yandex Cloud** because it is accessible from Russia and provides a free-tier friendly VM configuration that matches the lab requirements (small VM + simple networking and firewall rules). + +### Region / Zone +- Zone: `ru-central1-a` + +### Instance size (free-tier friendly) +- Platform: `standard-v2` +- vCPU: `2` with `core_fraction = 20` +- RAM: `1 GB` +- Boot disk: `10 GB` (`network-hdd`) + +### Estimated cost +- Expected cost: **$0** (free-tier / minimal resources) + +### Resources created +Using IaC, the following resources are provisioned: +- VPC network +- Subnet +- Security group with rules: + - SSH 22 — only from my IP (`95.111.204.70/32`) + - HTTP 80 — open to `0.0.0.0/0` + - App port 5000 — open to `0.0.0.0/0` +- Compute instance (VM) with NAT public IP + +--- + +## 2. Terraform Implementation + +### Terraform version +```text + +``` + +### Project structure +``` +terraform/ +├── main.tf +├── variables.tf +├── outputs.tf +├── versions.tf +├── terraform.tfvars (gitignored) +└── .gitignore +``` + +### Authentication (Yandex Cloud) +Authentication is done via **Service Account authorized key (JSON)**. + +- Service account key file (local path, not committed): + - `C:/Users/11kvv/.yc/lab04-sa-key.json` + +> Important: credential files (`*.json`) and state are excluded from Git. + +### Key configuration decisions +- **SSH access restricted** to my public IP only: `95.111.204.70/32` +- Public ports required by lab (80 and 5000) are open to the internet. +- Outputs exported: + - VM public IP + - SSH command string + +### Challenges encountered & fixes +1) **Provider authentication missing** +- Error: `one of 'token' or 'service_account_key_file' should be specified` +- Fix: generated service account key and configured `service_account_key_file`. + +2) **PermissionDenied when creating security group ingress** +- Error: `Permission denied to add ingress rule to security group` +- Fix: updated IAM roles for the service account (VPC permissions) and re-ran `terraform apply`. + +### Terminal output (sanitized) + +#### `terraform init` +```text +Initializing the backend... +Initializing provider plugins... +- Using previously-installed yandex-cloud/yandex v0.187.0 + +Terraform has been successfully initialized! +``` + +#### `terraform plan` (excerpt) +```text +Plan: 2 to add, 0 to change, 0 to destroy. + +Changes to Outputs: + + public_ip = (known after apply) + + ssh_command = (known after apply) +``` + +> Note: I also saw a warning: “Cannot connect to YC tool initialization service...”. This warning did not block plan generation. + +#### `terraform apply` (result) +```text + +``` + +### Outputs +```text +Public IP: +SSH command (from output): + +``` + +### SSH proof +```text + +``` + +Example command (Windows): +```powershell +ssh -i C:\Users\11kvv\.ssh\lab04_ed25519 ubuntu@ +``` + +--- + +## 3. Pulumi Implementation + +### Pulumi version and language +- Language: **Python** +```text + +``` + +### Cleanup of Terraform resources +Before provisioning the same infrastructure with Pulumi, Terraform resources were destroyed: + +```text + +``` + +### Pulumi project structure +``` +pulumi/ +├── __main__.py +├── requirements.txt +├── Pulumi.yaml +└── Pulumi..yaml (gitignored if contains secrets) +``` + +### Planned changes (`pulumi preview`) +```text + +``` + +### Apply (`pulumi up`) +```text + +``` + +### Outputs and SSH proof +```text +Pulumi public IP: +SSH proof: + +``` + +--- + +## 4. Terraform vs Pulumi Comparison + +### Ease of Learning +Terraform was easier to start with because HCL is concise and the workflow is very straightforward (`init → plan → apply`). Pulumi required a bit more setup (runtime, deps) and code structure, but felt natural once configured. + +### Code Readability +Terraform is very readable for simple infra because it is declarative and compact. Pulumi is more verbose but benefits from real language features (variables, functions, reuse) which can help as the project grows. + +### Debugging +Terraform errors are often direct and tied to a specific resource block. In Pulumi, errors can appear deeper in the program flow, but the ability to print/debug in code can help. + +### Documentation +Terraform has a large ecosystem and many examples. Pulumi documentation is also strong, especially when you already know the language SDK, but examples for some providers may be fewer. + +### Use Case +- Terraform: best for standard, repeatable infra with a simple declarative model, especially in teams. +- Pulumi: best when infrastructure needs non-trivial logic/reuse and you want to leverage full programming languages and testing. + +--- + +## 5. Lab 5 Preparation & Cleanup + +### VM for Lab 5 +- Keeping a VM for Lab 5 (Ansible): **** +- If YES: Which one: **Terraform / Pulumi** +- Reason: + - + +### Cleanup status +- Terraform resources destroyed: **** +- Pulumi resources destroyed: **** +- Proof (outputs/log excerpts): +```text + +``` + +--- + +## Appendix — Security & Git hygiene + +- `terraform.tfstate` and `terraform.tfvars` are not committed. +- Service account key `*.json` is not committed. +- SSH private key is not committed. +- `.gitignore` contains patterns for state and secrets. diff --git a/app_python/requirements-dev.txt b/app_python/requirements-dev.txt index 148a627bdb..ae15f82f5f 100644 --- a/app_python/requirements-dev.txt +++ b/app_python/requirements-dev.txt @@ -1,2 +1,2 @@ -pytest==8.3.4 +pytest==8.3.4 ruff==0.9.6 \ No newline at end of file diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py index 1cfc6be361..b032c06e50 100644 --- a/app_python/tests/test_app.py +++ b/app_python/tests/test_app.py @@ -1,62 +1,62 @@ -import pytest -from app import app as flask_app - - -@pytest.fixture() -def client(): - flask_app.config["TESTING"] = True - with flask_app.test_client() as client: - yield client - - -def test_root_endpoint_returns_200_and_json(client): - resp = client.get("/", headers={"User-Agent": "pytest"}) - assert resp.status_code == 200 - data = resp.get_json() - assert isinstance(data, dict) - - # top-level keys - for key in ["service", "system", "runtime", "request", "endpoints"]: - assert key in data - - # service structure - assert data["service"]["name"] == "devops-info-service" - assert data["service"]["framework"] == "Flask" - - # system structure - for key in ["hostname", "platform", "architecture", "cpu_count", "python_version"]: - assert key in data["system"] - - # runtime - assert "uptime_seconds" in data["runtime"] - assert isinstance(data["runtime"]["uptime_seconds"], int) - - # endpoints list - assert isinstance(data["endpoints"], list) - assert any(e["path"] == "/" for e in data["endpoints"]) - assert any(e["path"] == "/health" for e in data["endpoints"]) - - -def test_health_endpoint_returns_200_and_expected_fields(client): - resp = client.get("/health") - assert resp.status_code == 200 - data = resp.get_json() - - assert data["status"] == "healthy" - assert "timestamp" in data - assert "uptime_seconds" in data - assert isinstance(data["uptime_seconds"], int) - - -def test_unknown_endpoint_returns_404_json(client): - resp = client.get("/no-such-endpoint") - assert resp.status_code == 404 - data = resp.get_json() - - assert data["error"] == "Not Found" - assert "message" in data - - -def test_method_not_allowed_returns_405(client): - resp = client.post("/health") - assert resp.status_code == 405 +import pytest +from app import app as flask_app + + +@pytest.fixture() +def client(): + flask_app.config["TESTING"] = True + with flask_app.test_client() as client: + yield client + + +def test_root_endpoint_returns_200_and_json(client): + resp = client.get("/", headers={"User-Agent": "pytest"}) + assert resp.status_code == 200 + data = resp.get_json() + assert isinstance(data, dict) + + # top-level keys + for key in ["service", "system", "runtime", "request", "endpoints"]: + assert key in data + + # service structure + assert data["service"]["name"] == "devops-info-service" + assert data["service"]["framework"] == "Flask" + + # system structure + for key in ["hostname", "platform", "architecture", "cpu_count", "python_version"]: + assert key in data["system"] + + # runtime + assert "uptime_seconds" in data["runtime"] + assert isinstance(data["runtime"]["uptime_seconds"], int) + + # endpoints list + assert isinstance(data["endpoints"], list) + assert any(e["path"] == "/" for e in data["endpoints"]) + assert any(e["path"] == "/health" for e in data["endpoints"]) + + +def test_health_endpoint_returns_200_and_expected_fields(client): + resp = client.get("/health") + assert resp.status_code == 200 + data = resp.get_json() + + assert data["status"] == "healthy" + assert "timestamp" in data + assert "uptime_seconds" in data + assert isinstance(data["uptime_seconds"], int) + + +def test_unknown_endpoint_returns_404_json(client): + resp = client.get("/no-such-endpoint") + assert resp.status_code == 404 + data = resp.get_json() + + assert data["error"] == "Not Found" + assert "message" in data + + +def test_method_not_allowed_returns_405(client): + resp = client.post("/health") + assert resp.status_code == 405 diff --git a/labs/lab01.md b/labs/lab01.md index 18c9ff6c43..12d81fcb76 100644 --- a/labs/lab01.md +++ b/labs/lab01.md @@ -1,693 +1,693 @@ -# Lab 1 — DevOps Info Service: Web Application Development - -![difficulty](https://img.shields.io/badge/difficulty-beginner-success) -![topic](https://img.shields.io/badge/topic-Web%20Development-blue) -![points](https://img.shields.io/badge/points-10%2B2.5-orange) -![languages](https://img.shields.io/badge/languages-Python%20|%20Go-informational) - -> Build a DevOps info service that reports system information and health status. This service will evolve throughout the course into a comprehensive monitoring tool. - -## Overview - -Create a **DevOps Info Service** - a web application providing detailed information about itself and its runtime environment. This foundation will grow throughout the course as you add containerization, CI/CD, monitoring, and persistence. - -**What You'll Learn:** -- Web framework selection and implementation -- System introspection and API design -- Python best practices and documentation -- Foundation for future DevOps tooling - -**Tech Stack:** Python 3.11+ | Flask 3.1 or FastAPI 0.115 - ---- - -## Tasks - -### Task 1 — Python Web Application (6 pts) - -Build a production-ready Python web service with comprehensive system information. - -#### 1.1 Project Structure - -Create this structure: - -``` -app_python/ -├── app.py # Main application -├── requirements.txt # Dependencies -├── .gitignore # Git ignore -├── README.md # App documentation -├── tests/ # Unit tests (Lab 3) -│ └── __init__.py -└── docs/ # Lab documentation - ├── LAB01.md # Your lab submission - └── screenshots/ # Proof of work - ├── 01-main-endpoint.png - ├── 02-health-check.png - └── 03-formatted-output.png -``` - -#### 1.2 Choose Web Framework - -Select and justify your choice: -- **Flask** - Lightweight, easy to learn -- **FastAPI** - Modern, async, auto-documentation -- **Django** - Full-featured, includes ORM - -Document your decision in `app_python/docs/LAB01.md`. - -#### 1.3 Implement Main Endpoint: `GET /` - -Return comprehensive service and system information: - -```json -{ - "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"} - ] -} -``` - -
-💡 Implementation Hints - -**Get System Information:** -```python -import platform -import socket -from datetime import datetime - -hostname = socket.gethostname() -platform_name = platform.system() -architecture = platform.machine() -python_version = platform.python_version() -``` - -**Calculate Uptime:** -```python -start_time = datetime.now() - -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" - } -``` - -**Request Information:** -```python -# Flask -request.remote_addr # Client IP -request.headers.get('User-Agent') # User agent -request.method # HTTP method -request.path # Request path - -# FastAPI -request.client.host -request.headers.get('user-agent') -request.method -request.url.path -``` - -
- -#### 1.4 Implement Health Check: `GET /health` - -Simple health endpoint for monitoring: - -```json -{ - "status": "healthy", - "timestamp": "2024-01-15T14:30:00.000Z", - "uptime_seconds": 3600 -} -``` - -Return HTTP 200 for healthy status. This will be used for Kubernetes probes in Lab 9. - -
-💡 Implementation Hints - -```python -# Flask -@app.route('/health') -def health(): - return jsonify({ - 'status': 'healthy', - 'timestamp': datetime.now(timezone.utc).isoformat(), - 'uptime_seconds': get_uptime()['seconds'] - }) - -# FastAPI -@app.get("/health") -def health(): - return { - 'status': 'healthy', - 'timestamp': datetime.now(timezone.utc).isoformat(), - 'uptime_seconds': get_uptime()['seconds'] - } -``` - -
- -#### 1.5 Configuration - -Make your app configurable via environment variables: - -```python -import os - -HOST = os.getenv('HOST', '0.0.0.0') -PORT = int(os.getenv('PORT', 5000)) -DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' -``` - -**Test:** -```bash -python app.py # Default: 0.0.0.0:5000 -PORT=8080 python app.py # Custom port -HOST=127.0.0.1 PORT=3000 python app.py -``` - ---- - -### Task 2 — Documentation & Best Practices (4 pts) - -#### 2.1 Application README (`app_python/README.md`) - -Create user-facing documentation: - -**Required Sections:** -1. **Overview** - What the service does -2. **Prerequisites** - Python version, dependencies -3. **Installation** - ```bash - python -m venv venv - source venv/bin/activate - pip install -r requirements.txt - ``` -4. **Running the Application** - ```bash - python app.py - # Or with custom config - PORT=8080 python app.py - ``` -5. **API Endpoints** - - `GET /` - Service and system information - - `GET /health` - Health check -6. **Configuration** - Environment variables table - -#### 2.2 Best Practices - -Implement these in your code: - -**1. Clean Code Organization** -- Clear function names -- Proper imports grouping -- Comments only where needed -- Follow PEP 8 - -
-💡 Example Structure - -```python -""" -DevOps Info Service -Main application module -""" -import os -import socket -import platform -from datetime import datetime, timezone -from flask import Flask, jsonify, request - -app = Flask(__name__) - -# Configuration -HOST = os.getenv('HOST', '0.0.0.0') -PORT = int(os.getenv('PORT', 5000)) - -# Application start time -START_TIME = datetime.now(timezone.utc) - -def get_system_info(): - """Collect system information.""" - return { - 'hostname': socket.gethostname(), - 'platform': platform.system(), - 'architecture': platform.machine(), - 'python_version': platform.python_version() - } - -@app.route('/') -def index(): - """Main endpoint - service and system information.""" - # Implementation -``` - -
- -**2. Error Handling** - -
-💡 Implementation - -```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 -``` - -
- -**3. Logging** - -
-💡 Implementation - -```python -import logging - -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}') -``` - -
- -**4. Dependencies (`requirements.txt`)** - -```txt -# Web Framework -Flask==3.1.0 -# or -fastapi==0.115.0 -uvicorn[standard]==0.32.0 # Includes performance extras -``` - -Pin exact versions for reproducibility. - -**5. Git Ignore (`.gitignore`)** - -```gitignore -# Python -__pycache__/ -*.py[cod] -venv/ -*.log - -# IDE -.vscode/ -.idea/ - -# OS -.DS_Store -``` - -#### 2.3 Lab Submission (`app_python/docs/LAB01.md`) - -Document your implementation: - -**Required Sections:** -1. **Framework Selection** - - Your choice and why - - Comparison table with alternatives -2. **Best Practices Applied** - - List practices with code examples - - Explain importance of each -3. **API Documentation** - - Request/response examples - - Testing commands -4. **Testing Evidence** - - Screenshots showing endpoints work - - Terminal output -5. **Challenges & Solutions** - - Problems encountered - - How you solved them - -**Required Screenshots:** -- Main endpoint showing complete JSON -- Health check response -- Formatted/pretty-printed output - -#### 2.4 GitHub Community Engagement - -**Objective:** Explore GitHub's social features that support collaboration and discovery. - -**Actions Required:** -1. **Star** the course repository -2. **Star** the [simple-container-com/api](https://github.com/simple-container-com/api) project — a promising open-source tool for container management -3. **Follow** your professor and TAs on GitHub: - - Professor: [@Cre-eD](https://github.com/Cre-eD) - - TA: [@marat-biriushev](https://github.com/marat-biriushev) - - TA: [@pierrepicaud](https://github.com/pierrepicaud) -4. **Follow** at least 3 classmates from the course - -**Document in LAB01.md:** - -Add a "GitHub Community" section (after Challenges & Solutions) with 1-2 sentences explaining: -- Why starring repositories matters in open source -- How following developers helps in team projects and professional growth - -
-💡 GitHub Social Features - -**Why Stars Matter:** - -**Discovery & Bookmarking:** -- Stars help you bookmark interesting projects for later reference -- Star count indicates project popularity and community trust -- Starred repos appear in your GitHub profile, showing your interests - -**Open Source Signal:** -- Stars encourage maintainers (shows appreciation) -- High star count attracts more contributors -- Helps projects gain visibility in GitHub search and recommendations - -**Professional Context:** -- Shows you follow best practices and quality projects -- Indicates awareness of industry tools and trends - -**Why Following Matters:** - -**Networking:** -- See what other developers are working on -- Discover new projects through their activity -- Build professional connections beyond the classroom - -**Learning:** -- Learn from others' code and commits -- See how experienced developers solve problems -- Get inspiration for your own projects - -**Collaboration:** -- Stay updated on classmates' work -- Easier to find team members for future projects -- Build a supportive learning community - -**Career Growth:** -- Follow thought leaders in your technology stack -- See trending projects in real-time -- Build visibility in the developer community - -**GitHub Best Practices:** -- Star repos you find useful (not spam) -- Follow developers whose work interests you -- Engage meaningfully with the community -- Your GitHub activity shows employers your interests and involvement - -
- ---- - -## Bonus Task — Compiled Language (2.5 pts) - -Implement the same service in a compiled language to prepare for multi-stage Docker builds (Lab 2). - -**Choose One:** -- **Go** (Recommended) - Small binaries, fast compilation -- **Rust** - Memory safety, modern features -- **Java/Spring Boot** - Enterprise standard -- **C#/ASP.NET Core** - Cross-platform .NET - -**Structure:** - -``` -app_go/ (or app_rust, app_java, etc.) -├── main.go -├── go.mod -├── README.md -└── docs/ - ├── LAB01.md # Implementation details - ├── GO.md # Language justification - └── screenshots/ -``` - -**Requirements:** -- Same two endpoints: `/` and `/health` -- Same JSON structure -- Document build process -- Compare binary size to Python - -
-💡 Go Example Skeleton - -```go -package main - -import ( - "encoding/json" - "net/http" - "os" - "runtime" - "time" -) - -type ServiceInfo struct { - Service Service `json:"service"` - System System `json:"system"` - Runtime Runtime `json:"runtime"` - Request Request `json:"request"` -} - -var startTime = time.Now() - -func mainHandler(w http.ResponseWriter, r *http.Request) { - info := ServiceInfo{ - Service: Service{ - Name: "devops-info-service", - Version: "1.0.0", - }, - System: System{ - Platform: runtime.GOOS, - Architecture: runtime.GOARCH, - CPUCount: runtime.NumCPU(), - }, - // ... implement rest - } - - w.Header().Set("Content-Type", "application/json") - json.NewEncoder(w).Encode(info) -} - -func main() { - http.HandleFunc("/", mainHandler) - http.HandleFunc("/health", healthHandler) - - port := os.Getenv("PORT") - if port == "" { - port = "8080" - } - - http.ListenAndServe(":"+port, nil) -} -``` - -
- ---- - -## How to Submit - -1. **Create Branch:** - ```bash - git checkout -b lab01 - ``` - -2. **Commit Work:** - ```bash - git add app_python/ - git commit -m "feat: implement lab01 devops info service" - git push -u origin lab01 - ``` - -3. **Create Pull Requests:** - - **PR #1:** `your-fork:lab01` → `course-repo:master` - - **PR #2:** `your-fork:lab01` → `your-fork:master` - -4. **Verify:** - - All files present - - Screenshots included - - Documentation complete - ---- - -## Acceptance Criteria - -### Main Tasks (10 points) - -**Application Functionality (3 pts):** -- [ ] Service runs without errors -- [ ] `GET /` returns all required fields: - - [ ] Service metadata (name, version, description, framework) - - [ ] System info (hostname, platform, architecture, CPU, Python version) - - [ ] Runtime info (uptime, current time, timezone) - - [ ] Request info (client IP, user agent, method, path) - - [ ] Endpoints list -- [ ] `GET /health` returns status and uptime -- [ ] Configurable via environment variables (PORT, HOST) - -**Code Quality (2 pts):** -- [ ] Clean code structure -- [ ] PEP 8 compliant -- [ ] Error handling implemented -- [ ] Logging configured - -**Documentation (3 pts):** -- [ ] `app_python/README.md` complete with all sections -- [ ] `app_python/docs/LAB01.md` includes: - - [ ] Framework justification - - [ ] Best practices documentation - - [ ] API examples - - [ ] Testing evidence - - [ ] Challenges solved - - [ ] GitHub Community section (why stars/follows matter) -- [ ] All 3 required screenshots present -- [ ] Course repository starred -- [ ] simple-container-com/api repository starred -- [ ] Professor and TAs followed on GitHub -- [ ] At least 3 classmates followed on GitHub - -**Configuration (2 pts):** -- [ ] `requirements.txt` with pinned versions -- [ ] `.gitignore` properly configured -- [ ] Environment variables working - -### Bonus Task (2.5 points) - -- [ ] Compiled language app implements both endpoints -- [ ] Same JSON structure as Python version -- [ ] `app_/README.md` with build/run instructions -- [ ] `app_/docs/GO.md` with language justification -- [ ] `app_/docs/LAB01.md` with implementation details -- [ ] Screenshots showing compilation and execution - ---- - -## Rubric - -| Criteria | Points | Description | -|----------|--------|-------------| -| **Functionality** | 3 pts | Both endpoints work with complete, correct data | -| **Code Quality** | 2 pts | Clean, organized, follows Python standards | -| **Documentation** | 3 pts | Complete README and lab submission docs | -| **Configuration** | 2 pts | Dependencies, environment vars, .gitignore | -| **Bonus** | 2.5 pts | Compiled language implementation | -| **Total** | 12.5 pts | 10 pts required + 2.5 pts bonus | - -**Grading Scale:** -- **10/10:** Perfect implementation, excellent documentation -- **8-9/10:** All works, good docs, minor improvements possible -- **6-7/10:** Core functionality present, basic documentation -- **<6/10:** Missing features or documentation, needs revision - ---- - -## Resources - -
-📚 Python Web Frameworks - -- [Flask 3.1 Documentation](https://flask.palletsprojects.com/en/latest/) -- [Flask Quickstart](https://flask.palletsprojects.com/en/latest/quickstart/) -- [FastAPI Documentation](https://fastapi.tiangolo.com/) -- [FastAPI Tutorial](https://fastapi.tiangolo.com/tutorial/first-steps/) -- [Django 5.1 Documentation](https://docs.djangoproject.com/en/5.1/) - -
- -
-🐍 Python Best Practices - -- [PEP 8 Style Guide](https://pep8.org/) -- [Python Logging Tutorial](https://docs.python.org/3/howto/logging.html) -- [Python platform module](https://docs.python.org/3/library/platform.html) -- [Python socket module](https://docs.python.org/3/library/socket.html) - -
- -
-🔧 Compiled Languages (Bonus) - -- [Go Web Development](https://golang.org/doc/articles/wiki/) -- [Go net/http Package](https://pkg.go.dev/net/http) -- [Rust Web Frameworks](https://www.arewewebyet.org/) -- [Spring Boot Quickstart](https://spring.io/quickstart) -- [ASP.NET Core Tutorial](https://docs.microsoft.com/aspnet/core/) - -
- -
-🛠️ Development Tools - -- [Postman](https://www.postman.com/) - API testing -- [HTTPie](https://httpie.io/) - Command-line HTTP client -- [curl](https://curl.se/) - Data transfer tool -- [jq](https://stedolan.github.io/jq/) - JSON processor - -
- ---- - -## Looking Ahead - -This service evolves throughout the course: - -- **Lab 2:** Containerize with Docker, multi-stage builds -- **Lab 3:** Add unit tests and CI/CD pipeline -- **Lab 8:** Add `/metrics` endpoint for Prometheus -- **Lab 9:** Deploy to Kubernetes using `/health` probes -- **Lab 12:** Add `/visits` endpoint with file persistence -- **Lab 13:** Multi-environment deployment with GitOps - ---- - -**Good luck!** 🚀 - -> **Remember:** Keep it simple, write clean code, and document thoroughly. This foundation will carry through all 16 labs! +# Lab 1 — DevOps Info Service: Web Application Development + +![difficulty](https://img.shields.io/badge/difficulty-beginner-success) +![topic](https://img.shields.io/badge/topic-Web%20Development-blue) +![points](https://img.shields.io/badge/points-10%2B2.5-orange) +![languages](https://img.shields.io/badge/languages-Python%20|%20Go-informational) + +> Build a DevOps info service that reports system information and health status. This service will evolve throughout the course into a comprehensive monitoring tool. + +## Overview + +Create a **DevOps Info Service** - a web application providing detailed information about itself and its runtime environment. This foundation will grow throughout the course as you add containerization, CI/CD, monitoring, and persistence. + +**What You'll Learn:** +- Web framework selection and implementation +- System introspection and API design +- Python best practices and documentation +- Foundation for future DevOps tooling + +**Tech Stack:** Python 3.11+ | Flask 3.1 or FastAPI 0.115 + +--- + +## Tasks + +### Task 1 — Python Web Application (6 pts) + +Build a production-ready Python web service with comprehensive system information. + +#### 1.1 Project Structure + +Create this structure: + +``` +app_python/ +├── app.py # Main application +├── requirements.txt # Dependencies +├── .gitignore # Git ignore +├── README.md # App documentation +├── tests/ # Unit tests (Lab 3) +│ └── __init__.py +└── docs/ # Lab documentation + ├── LAB01.md # Your lab submission + └── screenshots/ # Proof of work + ├── 01-main-endpoint.png + ├── 02-health-check.png + └── 03-formatted-output.png +``` + +#### 1.2 Choose Web Framework + +Select and justify your choice: +- **Flask** - Lightweight, easy to learn +- **FastAPI** - Modern, async, auto-documentation +- **Django** - Full-featured, includes ORM + +Document your decision in `app_python/docs/LAB01.md`. + +#### 1.3 Implement Main Endpoint: `GET /` + +Return comprehensive service and system information: + +```json +{ + "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"} + ] +} +``` + +
+💡 Implementation Hints + +**Get System Information:** +```python +import platform +import socket +from datetime import datetime + +hostname = socket.gethostname() +platform_name = platform.system() +architecture = platform.machine() +python_version = platform.python_version() +``` + +**Calculate Uptime:** +```python +start_time = datetime.now() + +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" + } +``` + +**Request Information:** +```python +# Flask +request.remote_addr # Client IP +request.headers.get('User-Agent') # User agent +request.method # HTTP method +request.path # Request path + +# FastAPI +request.client.host +request.headers.get('user-agent') +request.method +request.url.path +``` + +
+ +#### 1.4 Implement Health Check: `GET /health` + +Simple health endpoint for monitoring: + +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` + +Return HTTP 200 for healthy status. This will be used for Kubernetes probes in Lab 9. + +
+💡 Implementation Hints + +```python +# Flask +@app.route('/health') +def health(): + return jsonify({ + 'status': 'healthy', + 'timestamp': datetime.now(timezone.utc).isoformat(), + 'uptime_seconds': get_uptime()['seconds'] + }) + +# FastAPI +@app.get("/health") +def health(): + return { + 'status': 'healthy', + 'timestamp': datetime.now(timezone.utc).isoformat(), + 'uptime_seconds': get_uptime()['seconds'] + } +``` + +
+ +#### 1.5 Configuration + +Make your app configurable via environment variables: + +```python +import os + +HOST = os.getenv('HOST', '0.0.0.0') +PORT = int(os.getenv('PORT', 5000)) +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' +``` + +**Test:** +```bash +python app.py # Default: 0.0.0.0:5000 +PORT=8080 python app.py # Custom port +HOST=127.0.0.1 PORT=3000 python app.py +``` + +--- + +### Task 2 — Documentation & Best Practices (4 pts) + +#### 2.1 Application README (`app_python/README.md`) + +Create user-facing documentation: + +**Required Sections:** +1. **Overview** - What the service does +2. **Prerequisites** - Python version, dependencies +3. **Installation** + ```bash + python -m venv venv + source venv/bin/activate + pip install -r requirements.txt + ``` +4. **Running the Application** + ```bash + python app.py + # Or with custom config + PORT=8080 python app.py + ``` +5. **API Endpoints** + - `GET /` - Service and system information + - `GET /health` - Health check +6. **Configuration** - Environment variables table + +#### 2.2 Best Practices + +Implement these in your code: + +**1. Clean Code Organization** +- Clear function names +- Proper imports grouping +- Comments only where needed +- Follow PEP 8 + +
+💡 Example Structure + +```python +""" +DevOps Info Service +Main application module +""" +import os +import socket +import platform +from datetime import datetime, timezone +from flask import Flask, jsonify, request + +app = Flask(__name__) + +# Configuration +HOST = os.getenv('HOST', '0.0.0.0') +PORT = int(os.getenv('PORT', 5000)) + +# Application start time +START_TIME = datetime.now(timezone.utc) + +def get_system_info(): + """Collect system information.""" + return { + 'hostname': socket.gethostname(), + 'platform': platform.system(), + 'architecture': platform.machine(), + 'python_version': platform.python_version() + } + +@app.route('/') +def index(): + """Main endpoint - service and system information.""" + # Implementation +``` + +
+ +**2. Error Handling** + +
+💡 Implementation + +```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 +``` + +
+ +**3. Logging** + +
+💡 Implementation + +```python +import logging + +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}') +``` + +
+ +**4. Dependencies (`requirements.txt`)** + +```txt +# Web Framework +Flask==3.1.0 +# or +fastapi==0.115.0 +uvicorn[standard]==0.32.0 # Includes performance extras +``` + +Pin exact versions for reproducibility. + +**5. Git Ignore (`.gitignore`)** + +```gitignore +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +``` + +#### 2.3 Lab Submission (`app_python/docs/LAB01.md`) + +Document your implementation: + +**Required Sections:** +1. **Framework Selection** + - Your choice and why + - Comparison table with alternatives +2. **Best Practices Applied** + - List practices with code examples + - Explain importance of each +3. **API Documentation** + - Request/response examples + - Testing commands +4. **Testing Evidence** + - Screenshots showing endpoints work + - Terminal output +5. **Challenges & Solutions** + - Problems encountered + - How you solved them + +**Required Screenshots:** +- Main endpoint showing complete JSON +- Health check response +- Formatted/pretty-printed output + +#### 2.4 GitHub Community Engagement + +**Objective:** Explore GitHub's social features that support collaboration and discovery. + +**Actions Required:** +1. **Star** the course repository +2. **Star** the [simple-container-com/api](https://github.com/simple-container-com/api) project — a promising open-source tool for container management +3. **Follow** your professor and TAs on GitHub: + - Professor: [@Cre-eD](https://github.com/Cre-eD) + - TA: [@marat-biriushev](https://github.com/marat-biriushev) + - TA: [@pierrepicaud](https://github.com/pierrepicaud) +4. **Follow** at least 3 classmates from the course + +**Document in LAB01.md:** + +Add a "GitHub Community" section (after Challenges & Solutions) with 1-2 sentences explaining: +- Why starring repositories matters in open source +- How following developers helps in team projects and professional growth + +
+💡 GitHub Social Features + +**Why Stars Matter:** + +**Discovery & Bookmarking:** +- Stars help you bookmark interesting projects for later reference +- Star count indicates project popularity and community trust +- Starred repos appear in your GitHub profile, showing your interests + +**Open Source Signal:** +- Stars encourage maintainers (shows appreciation) +- High star count attracts more contributors +- Helps projects gain visibility in GitHub search and recommendations + +**Professional Context:** +- Shows you follow best practices and quality projects +- Indicates awareness of industry tools and trends + +**Why Following Matters:** + +**Networking:** +- See what other developers are working on +- Discover new projects through their activity +- Build professional connections beyond the classroom + +**Learning:** +- Learn from others' code and commits +- See how experienced developers solve problems +- Get inspiration for your own projects + +**Collaboration:** +- Stay updated on classmates' work +- Easier to find team members for future projects +- Build a supportive learning community + +**Career Growth:** +- Follow thought leaders in your technology stack +- See trending projects in real-time +- Build visibility in the developer community + +**GitHub Best Practices:** +- Star repos you find useful (not spam) +- Follow developers whose work interests you +- Engage meaningfully with the community +- Your GitHub activity shows employers your interests and involvement + +
+ +--- + +## Bonus Task — Compiled Language (2.5 pts) + +Implement the same service in a compiled language to prepare for multi-stage Docker builds (Lab 2). + +**Choose One:** +- **Go** (Recommended) - Small binaries, fast compilation +- **Rust** - Memory safety, modern features +- **Java/Spring Boot** - Enterprise standard +- **C#/ASP.NET Core** - Cross-platform .NET + +**Structure:** + +``` +app_go/ (or app_rust, app_java, etc.) +├── main.go +├── go.mod +├── README.md +└── docs/ + ├── LAB01.md # Implementation details + ├── GO.md # Language justification + └── screenshots/ +``` + +**Requirements:** +- Same two endpoints: `/` and `/health` +- Same JSON structure +- Document build process +- Compare binary size to Python + +
+💡 Go Example Skeleton + +```go +package main + +import ( + "encoding/json" + "net/http" + "os" + "runtime" + "time" +) + +type ServiceInfo struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime Runtime `json:"runtime"` + Request Request `json:"request"` +} + +var startTime = time.Now() + +func mainHandler(w http.ResponseWriter, r *http.Request) { + info := ServiceInfo{ + Service: Service{ + Name: "devops-info-service", + Version: "1.0.0", + }, + System: System{ + Platform: runtime.GOOS, + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + }, + // ... implement rest + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(info) +} + +func main() { + http.HandleFunc("/", mainHandler) + http.HandleFunc("/health", healthHandler) + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + http.ListenAndServe(":"+port, nil) +} +``` + +
+ +--- + +## How to Submit + +1. **Create Branch:** + ```bash + git checkout -b lab01 + ``` + +2. **Commit Work:** + ```bash + git add app_python/ + git commit -m "feat: implement lab01 devops info service" + git push -u origin lab01 + ``` + +3. **Create Pull Requests:** + - **PR #1:** `your-fork:lab01` → `course-repo:master` + - **PR #2:** `your-fork:lab01` → `your-fork:master` + +4. **Verify:** + - All files present + - Screenshots included + - Documentation complete + +--- + +## Acceptance Criteria + +### Main Tasks (10 points) + +**Application Functionality (3 pts):** +- [ ] Service runs without errors +- [ ] `GET /` returns all required fields: + - [ ] Service metadata (name, version, description, framework) + - [ ] System info (hostname, platform, architecture, CPU, Python version) + - [ ] Runtime info (uptime, current time, timezone) + - [ ] Request info (client IP, user agent, method, path) + - [ ] Endpoints list +- [ ] `GET /health` returns status and uptime +- [ ] Configurable via environment variables (PORT, HOST) + +**Code Quality (2 pts):** +- [ ] Clean code structure +- [ ] PEP 8 compliant +- [ ] Error handling implemented +- [ ] Logging configured + +**Documentation (3 pts):** +- [ ] `app_python/README.md` complete with all sections +- [ ] `app_python/docs/LAB01.md` includes: + - [ ] Framework justification + - [ ] Best practices documentation + - [ ] API examples + - [ ] Testing evidence + - [ ] Challenges solved + - [ ] GitHub Community section (why stars/follows matter) +- [ ] All 3 required screenshots present +- [ ] Course repository starred +- [ ] simple-container-com/api repository starred +- [ ] Professor and TAs followed on GitHub +- [ ] At least 3 classmates followed on GitHub + +**Configuration (2 pts):** +- [ ] `requirements.txt` with pinned versions +- [ ] `.gitignore` properly configured +- [ ] Environment variables working + +### Bonus Task (2.5 points) + +- [ ] Compiled language app implements both endpoints +- [ ] Same JSON structure as Python version +- [ ] `app_/README.md` with build/run instructions +- [ ] `app_/docs/GO.md` with language justification +- [ ] `app_/docs/LAB01.md` with implementation details +- [ ] Screenshots showing compilation and execution + +--- + +## Rubric + +| Criteria | Points | Description | +|----------|--------|-------------| +| **Functionality** | 3 pts | Both endpoints work with complete, correct data | +| **Code Quality** | 2 pts | Clean, organized, follows Python standards | +| **Documentation** | 3 pts | Complete README and lab submission docs | +| **Configuration** | 2 pts | Dependencies, environment vars, .gitignore | +| **Bonus** | 2.5 pts | Compiled language implementation | +| **Total** | 12.5 pts | 10 pts required + 2.5 pts bonus | + +**Grading Scale:** +- **10/10:** Perfect implementation, excellent documentation +- **8-9/10:** All works, good docs, minor improvements possible +- **6-7/10:** Core functionality present, basic documentation +- **<6/10:** Missing features or documentation, needs revision + +--- + +## Resources + +
+📚 Python Web Frameworks + +- [Flask 3.1 Documentation](https://flask.palletsprojects.com/en/latest/) +- [Flask Quickstart](https://flask.palletsprojects.com/en/latest/quickstart/) +- [FastAPI Documentation](https://fastapi.tiangolo.com/) +- [FastAPI Tutorial](https://fastapi.tiangolo.com/tutorial/first-steps/) +- [Django 5.1 Documentation](https://docs.djangoproject.com/en/5.1/) + +
+ +
+🐍 Python Best Practices + +- [PEP 8 Style Guide](https://pep8.org/) +- [Python Logging Tutorial](https://docs.python.org/3/howto/logging.html) +- [Python platform module](https://docs.python.org/3/library/platform.html) +- [Python socket module](https://docs.python.org/3/library/socket.html) + +
+ +
+🔧 Compiled Languages (Bonus) + +- [Go Web Development](https://golang.org/doc/articles/wiki/) +- [Go net/http Package](https://pkg.go.dev/net/http) +- [Rust Web Frameworks](https://www.arewewebyet.org/) +- [Spring Boot Quickstart](https://spring.io/quickstart) +- [ASP.NET Core Tutorial](https://docs.microsoft.com/aspnet/core/) + +
+ +
+🛠️ Development Tools + +- [Postman](https://www.postman.com/) - API testing +- [HTTPie](https://httpie.io/) - Command-line HTTP client +- [curl](https://curl.se/) - Data transfer tool +- [jq](https://stedolan.github.io/jq/) - JSON processor + +
+ +--- + +## Looking Ahead + +This service evolves throughout the course: + +- **Lab 2:** Containerize with Docker, multi-stage builds +- **Lab 3:** Add unit tests and CI/CD pipeline +- **Lab 8:** Add `/metrics` endpoint for Prometheus +- **Lab 9:** Deploy to Kubernetes using `/health` probes +- **Lab 12:** Add `/visits` endpoint with file persistence +- **Lab 13:** Multi-environment deployment with GitOps + +--- + +**Good luck!** 🚀 + +> **Remember:** Keep it simple, write clean code, and document thoroughly. This foundation will carry through all 16 labs! diff --git a/labs/lab02.md b/labs/lab02.md index 1c3e032f89..448a84a2ca 100644 --- a/labs/lab02.md +++ b/labs/lab02.md @@ -1,366 +1,366 @@ -# Lab 2 — Docker Containerization - -![difficulty](https://img.shields.io/badge/difficulty-beginner-success) -![topic](https://img.shields.io/badge/topic-Containerization-blue) -![points](https://img.shields.io/badge/points-10%2B2.5-orange) -![tech](https://img.shields.io/badge/tech-Docker-informational) - -> Containerize your Python app from Lab 1 using Docker best practices and publish it to Docker Hub. - -## Overview - -Take your Lab 1 application and package it into a Docker container. Learn image optimization, security basics, and the Docker workflow used in production. - -**What You'll Learn:** -- Writing production-ready Dockerfiles -- Docker best practices and security -- Image optimization techniques -- Docker Hub workflow - -**Tech Stack:** Docker 25+ | Python 3.13-slim | Multi-stage builds - ---- - -## Tasks - -### Task 1 — Create Dockerfile (4 pts) - -**Objective:** Write a Dockerfile that containerizes your Python app following best practices. - -Create `app_python/Dockerfile` with these requirements: - -**Must Have:** -- Non-root user (mandatory) -- Specific base image version (e.g., `python:3.13-slim` or `python:3.12-slim`) -- Only copy necessary files -- Proper layer ordering -- `.dockerignore` file - -**Your app should work the same way in the container as it did locally.** - -
-💡 Dockerfile Concepts & Resources - -**Key Dockerfile Instructions to Research:** -- `FROM` - Choose your base image (look at python:3.13-slim, python:3.12-slim, python:3.13-alpine) -- `RUN` - Execute commands (creating users, installing packages) -- `WORKDIR` - Set working directory -- `COPY` - Copy files into the image -- `USER` - Switch to non-root user -- `EXPOSE` - Document which port your app uses -- `CMD` - Define how to start your application - -**Critical Concepts:** -- **Layer Caching**: Why does the order of COPY commands matter? -- **Non-root User**: How do you create and switch to a non-root user? -- **Base Image Selection**: What's the difference between slim, alpine, and full images? -- **Dependency Installation**: Why copy requirements.txt separately from application code? - -**Resources:** -- [Dockerfile Reference](https://docs.docker.com/reference/dockerfile/) -- [Best Practices Guide](https://docs.docker.com/build/building/best-practices/) -- [Python Image Variants](https://hub.docker.com/_/python) - Use 3.13-slim or 3.12-slim - -**Think About:** -- What happens if you copy all files before installing dependencies? -- Why shouldn't you run as root? -- How does layer caching speed up rebuilds? - -
- -
-💡 .dockerignore Concepts - -**Purpose:** Prevent unnecessary files from being sent to Docker daemon during build (faster builds, smaller context). - -**What Should You Exclude?** -Think about what doesn't need to be in your container: -- Development artifacts (like Python's `__pycache__`, `*.pyc`) -- Version control files (`.git` directory) -- IDE configuration files -- Virtual environments (`venv/`, `.venv/`) -- Documentation that's not needed at runtime -- Test files (if not running tests in container) - -**Key Question:** Why does excluding files from the build context matter for build speed? - -**Resources:** -- [.dockerignore Documentation](https://docs.docker.com/engine/reference/builder/#dockerignore-file) -- Look at your `.gitignore` for inspiration - many patterns overlap - -**Exercise:** Start minimal and add exclusions as needed, rather than copying a huge list you don't understand. - -
- -**Test Your Container:** - -You should be able to: -1. Build your image using the `docker build` command -2. Run a container from your image with proper port mapping -3. Access your application endpoints from the host machine - -Verify that your application works the same way in the container as it did locally. - ---- - -### Task 2 — Docker Hub (2 pts) - -**Objective:** Publish your image to Docker Hub. - -**Requirements:** -1. Create a Docker Hub account (if you don't have one) -2. Tag your image with your Docker Hub username -3. Authenticate with Docker Hub -4. Push your image to the registry -5. Verify the image is publicly accessible - -**Documentation Required:** -- Terminal output showing successful push -- Docker Hub repository URL -- Explanation of your tagging strategy - -
-💡 Docker Hub Resources - -**Useful Commands:** -- `docker tag` - Tag images for registry push -- `docker login` - Authenticate with Docker Hub -- `docker push` - Upload image to registry -- `docker pull` - Download image from registry - -**Resources:** -- [Docker Hub Quickstart](https://docs.docker.com/docker-hub/quickstart/) -- [Docker Tag Reference](https://docs.docker.com/reference/cli/docker/image/tag/) -- [Best Practices for Tagging](https://docs.docker.com/build/building/best-practices/#tagging) - -
- ---- - -### Task 3 — Documentation (4 pts) - -**Objective:** Document your Docker implementation with focus on understanding and decisions. - -#### 3.1 Update `app_python/README.md` - -Add a **Docker** section explaining how to use your containerized application. Include command patterns (not exact commands) for: -- Building the image locally -- Running a container -- Pulling from Docker Hub - -#### 3.2 Create `app_python/docs/LAB02.md` - -Document your implementation with these sections: - -**Required Sections:** - -1. **Docker Best Practices Applied** - - List each practice you implemented (non-root user, layer caching, .dockerignore, etc.) - - Explain WHY each matters (not just what it does) - - Include relevant Dockerfile snippets with explanations - -2. **Image Information & Decisions** - - Base image chosen and justification (why this specific version?) - - Final image size and your assessment - - Layer structure explanation - - Optimization choices you made - -3. **Build & Run Process** - - Complete terminal output from your build process - - Terminal output showing container running - - Terminal output from testing endpoints (curl/httpie) - - Docker Hub repository URL - -4. **Technical Analysis** - - Why does your Dockerfile work the way it does? - - What would happen if you changed the layer order? - - What security considerations did you implement? - - How does .dockerignore improve your build? - -5. **Challenges & Solutions** - - Issues encountered during implementation - - How you debugged and resolved them - - What you learned from the process - ---- - -## Bonus Task — Multi-Stage Build (2.5 pts) - -**Objective:** Containerize your compiled language app (from Lab 1 bonus) using multi-stage builds. - -**Why Multi-Stage?** Separate build environment from runtime → smaller final image. - -**Example Flow:** -1. **Stage 1 (Builder):** Compile the app (large image with compilers) -2. **Stage 2 (Runtime):** Copy only the binary (small image, no build tools) - -
-💡 Multi-Stage Build Concepts - -**The Problem:** Compiled language images include the entire compiler/SDK in the final image (huge!). - -**The Solution:** Use multiple `FROM` statements: -- **Stage 1 (Builder)**: Use full SDK image, compile your application -- **Stage 2 (Runtime)**: Use minimal base image, copy only the compiled binary - -**Key Concepts to Research:** -- How to name build stages (`AS builder`) -- How to copy files from previous stages (`COPY --from=builder`) -- Choosing runtime base images (alpine, distroless, scratch) -- Static vs dynamic compilation (affects what base image you can use) - -**Questions to Explore:** -- What's the size difference between your builder and final image? -- Why can't you just use the builder image as your final image? -- What security benefits come from smaller images? -- Can you use `FROM scratch`? Why or why not? - -**Resources:** -- [Multi-Stage Builds Documentation](https://docs.docker.com/build/building/multi-stage/) -- [Distroless Base Images](https://github.com/GoogleContainerTools/distroless) -- Language-specific: Search "Go static binary Docker" or "Rust alpine Docker" - -**Challenge:** Try to get your final image under 20MB. - -
- -**Requirements:** -- Multi-stage Dockerfile in `app_go/` (or your chosen language) -- Working containerized application -- Documentation in `app_go/docs/LAB02.md` explaining: - - Your multi-stage build strategy - - Size comparison with analysis (builder vs final image) - - Why multi-stage builds matter for compiled languages - - Terminal output showing build process and image sizes - - Technical explanation of each stage's purpose - -**Bonus Points Given For:** -- Significant size reduction achieved with clear metrics -- Deep understanding of multi-stage build benefits -- Analysis of security implications (smaller attack surface) -- Explanation of trade-offs and decisions made - ---- - -## How to Submit - -1. **Create Branch:** Create a new branch called `lab02` - -2. **Commit Work:** - - Add your changes (app_python/ directory with Dockerfile, .dockerignore, updated docs) - - Commit with a descriptive message following conventional commits format - - Push to your fork - -3. **Create Pull Requests:** - - **PR #1:** `your-fork:lab02` → `course-repo:master` - - **PR #2:** `your-fork:lab02` → `your-fork:master` - ---- - -## Acceptance Criteria - -### Main Tasks (10 points) - -**Dockerfile (4 pts):** -- [ ] Dockerfile exists in `app_python/` -- [ ] Uses specific base image version -- [ ] Runs as non-root user (USER directive) -- [ ] Proper layer ordering (dependencies before code) -- [ ] Only copies necessary files -- [ ] `.dockerignore` file present -- [ ] Image builds successfully -- [ ] Container runs and app works - -**Docker Hub (2 pts):** -- [ ] Image pushed to Docker Hub -- [ ] Image is publicly accessible -- [ ] Correct tagging used -- [ ] Can pull and run from Docker Hub - -**Documentation (4 pts):** -- [ ] `app_python/README.md` has Docker section with command patterns -- [ ] `app_python/docs/LAB02.md` complete with: - - [ ] Best practices explained with WHY (not just what) - - [ ] Image information and justifications for choices - - [ ] Terminal output from build, run, and testing - - [ ] Technical analysis demonstrating understanding - - [ ] Challenges and solutions documented - - [ ] Docker Hub repository URL provided - -### Bonus Task (2.5 points) - -- [ ] Multi-stage Dockerfile for compiled language app -- [ ] Working containerized application -- [ ] Documentation in `app_/docs/LAB02.md` with: - - [ ] Multi-stage strategy explained - - [ ] Terminal output showing image sizes (builder vs final) - - [ ] Analysis of size reduction and why it matters - - [ ] Technical explanation of each stage - - [ ] Security benefits discussed - ---- - -## Rubric - -| Criteria | Points | Description | -|----------|--------|-------------| -| **Dockerfile** | 4 pts | Correct, secure, optimized | -| **Docker Hub** | 2 pts | Successfully published | -| **Documentation** | 4 pts | Complete and clear | -| **Bonus** | 2.5 pts | Multi-stage implementation | -| **Total** | 12.5 pts | 10 pts required + 2.5 pts bonus | - -**Grading:** -- **10/10:** Perfect Dockerfile, deep understanding demonstrated, excellent analysis -- **8-9/10:** Working container, good practices, solid understanding shown -- **6-7/10:** Container works, basic security, surface-level explanations -- **<6/10:** Missing requirements, runs as root, copy-paste without understanding - ---- - -## Resources - -
-📚 Docker Documentation - -- [Dockerfile Best Practices](https://docs.docker.com/build/building/best-practices/) -- [Dockerfile Reference](https://docs.docker.com/reference/dockerfile/) -- [Multi-Stage Builds](https://docs.docker.com/build/building/multi-stage/) -- [.dockerignore](https://docs.docker.com/reference/dockerfile/#dockerignore-file) -- [Docker Build Guide](https://docs.docker.com/build/guide/) - -
- -
-🔒 Security Resources - -- [Docker Security Best Practices](https://docs.docker.com/build/building/best-practices/#security) -- [Snyk Docker Security](https://snyk.io/learn/docker-security-scanning/) -- [Why Non-Root Containers](https://docs.docker.com/build/building/best-practices/#user) -- [Distroless Images](https://github.com/GoogleContainerTools/distroless) - Minimal base images - -
- -
-🛠️ Tools - -- [Hadolint](https://github.com/hadolint/hadolint) - Dockerfile linter -- [Dive](https://github.com/wagoodman/dive) - Explore image layers -- [Docker Hub](https://hub.docker.com/) - Container registry - -
- ---- - -## Looking Ahead - -- **Lab 3:** CI/CD will automatically build these Docker images -- **Lab 7-8:** Deploy containers with docker-compose for logging/monitoring -- **Lab 9:** Run these containers in Kubernetes -- **Lab 13:** ArgoCD will deploy containerized apps automatically - ---- - -**Good luck!** 🚀 - -> **Remember:** Understanding beats copy-paste. Explain your decisions, not just your actions. Run as non-root or no points! +# Lab 2 — Docker Containerization + +![difficulty](https://img.shields.io/badge/difficulty-beginner-success) +![topic](https://img.shields.io/badge/topic-Containerization-blue) +![points](https://img.shields.io/badge/points-10%2B2.5-orange) +![tech](https://img.shields.io/badge/tech-Docker-informational) + +> Containerize your Python app from Lab 1 using Docker best practices and publish it to Docker Hub. + +## Overview + +Take your Lab 1 application and package it into a Docker container. Learn image optimization, security basics, and the Docker workflow used in production. + +**What You'll Learn:** +- Writing production-ready Dockerfiles +- Docker best practices and security +- Image optimization techniques +- Docker Hub workflow + +**Tech Stack:** Docker 25+ | Python 3.13-slim | Multi-stage builds + +--- + +## Tasks + +### Task 1 — Create Dockerfile (4 pts) + +**Objective:** Write a Dockerfile that containerizes your Python app following best practices. + +Create `app_python/Dockerfile` with these requirements: + +**Must Have:** +- Non-root user (mandatory) +- Specific base image version (e.g., `python:3.13-slim` or `python:3.12-slim`) +- Only copy necessary files +- Proper layer ordering +- `.dockerignore` file + +**Your app should work the same way in the container as it did locally.** + +
+💡 Dockerfile Concepts & Resources + +**Key Dockerfile Instructions to Research:** +- `FROM` - Choose your base image (look at python:3.13-slim, python:3.12-slim, python:3.13-alpine) +- `RUN` - Execute commands (creating users, installing packages) +- `WORKDIR` - Set working directory +- `COPY` - Copy files into the image +- `USER` - Switch to non-root user +- `EXPOSE` - Document which port your app uses +- `CMD` - Define how to start your application + +**Critical Concepts:** +- **Layer Caching**: Why does the order of COPY commands matter? +- **Non-root User**: How do you create and switch to a non-root user? +- **Base Image Selection**: What's the difference between slim, alpine, and full images? +- **Dependency Installation**: Why copy requirements.txt separately from application code? + +**Resources:** +- [Dockerfile Reference](https://docs.docker.com/reference/dockerfile/) +- [Best Practices Guide](https://docs.docker.com/build/building/best-practices/) +- [Python Image Variants](https://hub.docker.com/_/python) - Use 3.13-slim or 3.12-slim + +**Think About:** +- What happens if you copy all files before installing dependencies? +- Why shouldn't you run as root? +- How does layer caching speed up rebuilds? + +
+ +
+💡 .dockerignore Concepts + +**Purpose:** Prevent unnecessary files from being sent to Docker daemon during build (faster builds, smaller context). + +**What Should You Exclude?** +Think about what doesn't need to be in your container: +- Development artifacts (like Python's `__pycache__`, `*.pyc`) +- Version control files (`.git` directory) +- IDE configuration files +- Virtual environments (`venv/`, `.venv/`) +- Documentation that's not needed at runtime +- Test files (if not running tests in container) + +**Key Question:** Why does excluding files from the build context matter for build speed? + +**Resources:** +- [.dockerignore Documentation](https://docs.docker.com/engine/reference/builder/#dockerignore-file) +- Look at your `.gitignore` for inspiration - many patterns overlap + +**Exercise:** Start minimal and add exclusions as needed, rather than copying a huge list you don't understand. + +
+ +**Test Your Container:** + +You should be able to: +1. Build your image using the `docker build` command +2. Run a container from your image with proper port mapping +3. Access your application endpoints from the host machine + +Verify that your application works the same way in the container as it did locally. + +--- + +### Task 2 — Docker Hub (2 pts) + +**Objective:** Publish your image to Docker Hub. + +**Requirements:** +1. Create a Docker Hub account (if you don't have one) +2. Tag your image with your Docker Hub username +3. Authenticate with Docker Hub +4. Push your image to the registry +5. Verify the image is publicly accessible + +**Documentation Required:** +- Terminal output showing successful push +- Docker Hub repository URL +- Explanation of your tagging strategy + +
+💡 Docker Hub Resources + +**Useful Commands:** +- `docker tag` - Tag images for registry push +- `docker login` - Authenticate with Docker Hub +- `docker push` - Upload image to registry +- `docker pull` - Download image from registry + +**Resources:** +- [Docker Hub Quickstart](https://docs.docker.com/docker-hub/quickstart/) +- [Docker Tag Reference](https://docs.docker.com/reference/cli/docker/image/tag/) +- [Best Practices for Tagging](https://docs.docker.com/build/building/best-practices/#tagging) + +
+ +--- + +### Task 3 — Documentation (4 pts) + +**Objective:** Document your Docker implementation with focus on understanding and decisions. + +#### 3.1 Update `app_python/README.md` + +Add a **Docker** section explaining how to use your containerized application. Include command patterns (not exact commands) for: +- Building the image locally +- Running a container +- Pulling from Docker Hub + +#### 3.2 Create `app_python/docs/LAB02.md` + +Document your implementation with these sections: + +**Required Sections:** + +1. **Docker Best Practices Applied** + - List each practice you implemented (non-root user, layer caching, .dockerignore, etc.) + - Explain WHY each matters (not just what it does) + - Include relevant Dockerfile snippets with explanations + +2. **Image Information & Decisions** + - Base image chosen and justification (why this specific version?) + - Final image size and your assessment + - Layer structure explanation + - Optimization choices you made + +3. **Build & Run Process** + - Complete terminal output from your build process + - Terminal output showing container running + - Terminal output from testing endpoints (curl/httpie) + - Docker Hub repository URL + +4. **Technical Analysis** + - Why does your Dockerfile work the way it does? + - What would happen if you changed the layer order? + - What security considerations did you implement? + - How does .dockerignore improve your build? + +5. **Challenges & Solutions** + - Issues encountered during implementation + - How you debugged and resolved them + - What you learned from the process + +--- + +## Bonus Task — Multi-Stage Build (2.5 pts) + +**Objective:** Containerize your compiled language app (from Lab 1 bonus) using multi-stage builds. + +**Why Multi-Stage?** Separate build environment from runtime → smaller final image. + +**Example Flow:** +1. **Stage 1 (Builder):** Compile the app (large image with compilers) +2. **Stage 2 (Runtime):** Copy only the binary (small image, no build tools) + +
+💡 Multi-Stage Build Concepts + +**The Problem:** Compiled language images include the entire compiler/SDK in the final image (huge!). + +**The Solution:** Use multiple `FROM` statements: +- **Stage 1 (Builder)**: Use full SDK image, compile your application +- **Stage 2 (Runtime)**: Use minimal base image, copy only the compiled binary + +**Key Concepts to Research:** +- How to name build stages (`AS builder`) +- How to copy files from previous stages (`COPY --from=builder`) +- Choosing runtime base images (alpine, distroless, scratch) +- Static vs dynamic compilation (affects what base image you can use) + +**Questions to Explore:** +- What's the size difference between your builder and final image? +- Why can't you just use the builder image as your final image? +- What security benefits come from smaller images? +- Can you use `FROM scratch`? Why or why not? + +**Resources:** +- [Multi-Stage Builds Documentation](https://docs.docker.com/build/building/multi-stage/) +- [Distroless Base Images](https://github.com/GoogleContainerTools/distroless) +- Language-specific: Search "Go static binary Docker" or "Rust alpine Docker" + +**Challenge:** Try to get your final image under 20MB. + +
+ +**Requirements:** +- Multi-stage Dockerfile in `app_go/` (or your chosen language) +- Working containerized application +- Documentation in `app_go/docs/LAB02.md` explaining: + - Your multi-stage build strategy + - Size comparison with analysis (builder vs final image) + - Why multi-stage builds matter for compiled languages + - Terminal output showing build process and image sizes + - Technical explanation of each stage's purpose + +**Bonus Points Given For:** +- Significant size reduction achieved with clear metrics +- Deep understanding of multi-stage build benefits +- Analysis of security implications (smaller attack surface) +- Explanation of trade-offs and decisions made + +--- + +## How to Submit + +1. **Create Branch:** Create a new branch called `lab02` + +2. **Commit Work:** + - Add your changes (app_python/ directory with Dockerfile, .dockerignore, updated docs) + - Commit with a descriptive message following conventional commits format + - Push to your fork + +3. **Create Pull Requests:** + - **PR #1:** `your-fork:lab02` → `course-repo:master` + - **PR #2:** `your-fork:lab02` → `your-fork:master` + +--- + +## Acceptance Criteria + +### Main Tasks (10 points) + +**Dockerfile (4 pts):** +- [ ] Dockerfile exists in `app_python/` +- [ ] Uses specific base image version +- [ ] Runs as non-root user (USER directive) +- [ ] Proper layer ordering (dependencies before code) +- [ ] Only copies necessary files +- [ ] `.dockerignore` file present +- [ ] Image builds successfully +- [ ] Container runs and app works + +**Docker Hub (2 pts):** +- [ ] Image pushed to Docker Hub +- [ ] Image is publicly accessible +- [ ] Correct tagging used +- [ ] Can pull and run from Docker Hub + +**Documentation (4 pts):** +- [ ] `app_python/README.md` has Docker section with command patterns +- [ ] `app_python/docs/LAB02.md` complete with: + - [ ] Best practices explained with WHY (not just what) + - [ ] Image information and justifications for choices + - [ ] Terminal output from build, run, and testing + - [ ] Technical analysis demonstrating understanding + - [ ] Challenges and solutions documented + - [ ] Docker Hub repository URL provided + +### Bonus Task (2.5 points) + +- [ ] Multi-stage Dockerfile for compiled language app +- [ ] Working containerized application +- [ ] Documentation in `app_/docs/LAB02.md` with: + - [ ] Multi-stage strategy explained + - [ ] Terminal output showing image sizes (builder vs final) + - [ ] Analysis of size reduction and why it matters + - [ ] Technical explanation of each stage + - [ ] Security benefits discussed + +--- + +## Rubric + +| Criteria | Points | Description | +|----------|--------|-------------| +| **Dockerfile** | 4 pts | Correct, secure, optimized | +| **Docker Hub** | 2 pts | Successfully published | +| **Documentation** | 4 pts | Complete and clear | +| **Bonus** | 2.5 pts | Multi-stage implementation | +| **Total** | 12.5 pts | 10 pts required + 2.5 pts bonus | + +**Grading:** +- **10/10:** Perfect Dockerfile, deep understanding demonstrated, excellent analysis +- **8-9/10:** Working container, good practices, solid understanding shown +- **6-7/10:** Container works, basic security, surface-level explanations +- **<6/10:** Missing requirements, runs as root, copy-paste without understanding + +--- + +## Resources + +
+📚 Docker Documentation + +- [Dockerfile Best Practices](https://docs.docker.com/build/building/best-practices/) +- [Dockerfile Reference](https://docs.docker.com/reference/dockerfile/) +- [Multi-Stage Builds](https://docs.docker.com/build/building/multi-stage/) +- [.dockerignore](https://docs.docker.com/reference/dockerfile/#dockerignore-file) +- [Docker Build Guide](https://docs.docker.com/build/guide/) + +
+ +
+🔒 Security Resources + +- [Docker Security Best Practices](https://docs.docker.com/build/building/best-practices/#security) +- [Snyk Docker Security](https://snyk.io/learn/docker-security-scanning/) +- [Why Non-Root Containers](https://docs.docker.com/build/building/best-practices/#user) +- [Distroless Images](https://github.com/GoogleContainerTools/distroless) - Minimal base images + +
+ +
+🛠️ Tools + +- [Hadolint](https://github.com/hadolint/hadolint) - Dockerfile linter +- [Dive](https://github.com/wagoodman/dive) - Explore image layers +- [Docker Hub](https://hub.docker.com/) - Container registry + +
+ +--- + +## Looking Ahead + +- **Lab 3:** CI/CD will automatically build these Docker images +- **Lab 7-8:** Deploy containers with docker-compose for logging/monitoring +- **Lab 9:** Run these containers in Kubernetes +- **Lab 13:** ArgoCD will deploy containerized apps automatically + +--- + +**Good luck!** 🚀 + +> **Remember:** Understanding beats copy-paste. Explain your decisions, not just your actions. Run as non-root or no points! diff --git a/labs/lab03.md b/labs/lab03.md index 9824e934b3..d0acb4da96 100644 --- a/labs/lab03.md +++ b/labs/lab03.md @@ -1,931 +1,931 @@ -# Lab 3 — Continuous Integration (CI/CD) - -![difficulty](https://img.shields.io/badge/difficulty-beginner-success) -![topic](https://img.shields.io/badge/topic-CI/CD-blue) -![points](https://img.shields.io/badge/points-10%2B2.5-orange) -![tech](https://img.shields.io/badge/tech-GitHub%20Actions-informational) - -> Automate your Python app testing and Docker builds with GitHub Actions CI/CD pipeline. - -## Overview - -Take your containerized app from Labs 1-2 and add automated testing and deployment. Learn how CI/CD catches bugs early, ensures code quality, and automates the Docker build/push workflow. - -**What You'll Learn:** -- Writing effective unit tests -- GitHub Actions workflow syntax -- CI/CD best practices (caching, matrix builds, security scanning) -- Automated Docker image publishing -- Continuous integration for multiple applications - -**Tech Stack:** GitHub Actions | pytest 8+ | Python 3.11+ | Snyk | Docker - -**Connection to Previous Labs:** -- **Lab 1:** Test the endpoints you created -- **Lab 2:** Automate the Docker build/push workflow -- **Lab 4+:** This CI pipeline will run for all future labs - ---- - -## Tasks - -### Task 1 — Unit Testing (3 pts) - -**Objective:** Write comprehensive unit tests for your Python application to ensure reliability. - -**Requirements:** - -1. **Choose a Testing Framework** - - Research Python testing frameworks (pytest, unittest, etc.) - - Select one and justify your choice - - Install it in your `requirements.txt` or create `requirements-dev.txt` - -2. **Write Unit Tests** - - Create `app_python/tests/` directory - - Write tests for **all** your endpoints: - - `GET /` - Verify JSON structure and required fields - - `GET /health` - Verify health check response - - Test both successful responses and error cases - - Aim for meaningful test coverage (not just basic smoke tests) - -3. **Run Tests Locally** - - Verify all tests pass locally before CI setup - - Document how to run tests in your README - -
-💡 Testing Framework Guidance - -**Popular Python Testing Frameworks:** - -**pytest (Recommended):** -- Pros: Simple syntax, powerful fixtures, excellent plugin ecosystem -- Cons: Additional dependency -- Use case: Most modern Python projects - -**unittest:** -- Pros: Built into Python (no extra dependencies) -- Cons: More verbose, less modern features -- Use case: Minimal dependency projects - -**Key Testing Concepts to Research:** -- Test fixtures and setup/teardown -- Mocking external dependencies -- Testing HTTP endpoints (test client usage) -- Test coverage measurement -- Assertions and expected vs actual results - -**What Should You Test?** -- Correct HTTP status codes (200, 404, 500) -- Response data structure (JSON fields present) -- Response data types (strings, integers, etc.) -- Edge cases (invalid requests, missing data) -- Error handling (what happens when things fail?) - -**Questions to Consider:** -- How do you test a Flask/FastAPI app without starting the server? -- Should you test that `hostname` returns your actual hostname, or just that the field exists? -- How do you simulate different client IPs or user agents in tests? - -**Resources:** -- [Pytest Documentation](https://docs.pytest.org/) -- [Flask Testing](https://flask.palletsprojects.com/en/stable/testing/) -- [FastAPI Testing](https://fastapi.tiangolo.com/tutorial/testing/) -- [Python unittest](https://docs.python.org/3/library/unittest.html) - -**Anti-Patterns to Avoid:** -- Testing framework functionality instead of your code -- Tests that always pass regardless of implementation -- Tests with no assertions -- Tests that depend on external services - -
- -**What to Document:** -- Your testing framework choice and why -- Test structure explanation -- How to run tests locally -- Terminal output showing all tests passing - ---- - -### Task 2 — GitHub Actions CI Workflow (4 pts) - -**Objective:** Create a GitHub Actions workflow that automatically tests your code and builds Docker images with proper versioning. - -**Requirements:** - -1. **Create Workflow File** - - Create `.github/workflows/python-ci.yml` in your repository - - Name your workflow descriptively - -2. **Implement Essential CI Steps** - - Your workflow must include these logical stages: - - **a) Code Quality & Testing:** - - Install dependencies - - Run a linter (pylint, flake8, black, ruff, etc.) - - Run your unit tests - - **b) Docker Build & Push with Versioning:** - - Authenticate with Docker Hub - - Build your Docker image - - Tag with proper version strategy (see versioning section below) - - Push to Docker Hub with multiple tags - -3. **Versioning Strategy** - - Choose **one** versioning approach and implement it: - - **Option A: Semantic Versioning (SemVer)** - - Version format: `v1.2.3` (major.minor.patch) - - Use git tags for releases - - Tag images like: `username/app:1.2.3`, `username/app:1.2`, `username/app:latest` - - **When to use:** Traditional software releases with breaking changes - - **Option B: Calendar Versioning (CalVer)** - - Version format: `2024.01.15` or `2024.01` (year.month.day or year.month) - - Based on release date - - Tag images like: `username/app:2024.01`, `username/app:latest` - - **When to use:** Time-based releases, continuous deployment - - **Required:** - - Document which strategy you chose and why - - Implement it in your CI workflow - - Show at least 2 tags per image (e.g., version + latest) - -4. **Workflow Triggers** - - Configure when the workflow runs (push, pull request, etc.) - - Consider which branches should trigger builds - -5. **Testing the Workflow** - - Push your workflow file and verify it runs - - Fix any issues that arise - - Ensure all steps complete successfully - - Verify Docker Hub shows your version tags - -
-💡 GitHub Actions Concepts - -**Core Concepts to Research:** - -**Workflow Anatomy:** -- `name` - What is your workflow called? -- `on` - When does it run? (push, pull_request, schedule, etc.) -- `jobs` - What work needs to be done? -- `steps` - Individual commands within a job -- `runs-on` - What OS environment? (ubuntu-latest, etc.) - -**Key Questions:** -- Should you run CI on every push, or only on pull requests? -- What happens if tests fail? Should the workflow continue? -- How do you access secrets (like Docker Hub credentials) securely? -- Why might you want multiple jobs vs multiple steps in one job? - -**Python CI Steps Pattern:** -```yaml -# This is a pattern, not exact copy-paste code -# Research the actual syntax and actions needed - -- Set up Python environment -- Install dependencies -- Run linter -- Run tests -``` - -**Docker CI Steps Pattern:** -```yaml -# This is a pattern, not exact copy-paste code -# Research the actual actions and their parameters - -- Log in to Docker Hub -- Extract metadata for tags -- Build and push Docker image -``` - -**Important Concepts:** -- **Actions Marketplace:** Reusable actions (actions/checkout@v4, actions/setup-python@v5, docker/build-push-action@v6) -- **Secrets:** How to store Docker Hub credentials securely -- **Job Dependencies:** Can one job depend on another succeeding? -- **Matrix Builds:** Testing multiple Python versions (optional but good to know) -- **Caching:** Speed up workflows by caching dependencies (we'll add this in Task 3) - -**Resources:** -- [GitHub Actions Documentation](https://docs.github.com/en/actions) -- [Building and Testing Python](https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python) -- [Publishing Docker Images](https://docs.docker.com/ci-cd/github-actions/) -- [GitHub Actions Marketplace](https://github.com/marketplace?type=actions) -- [Workflow Syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) - -**Security Best Practices:** -- Never hardcode passwords or tokens in workflow files -- Use GitHub Secrets for sensitive data -- Understand when secrets are exposed to pull requests from forks -- Use `secrets.GITHUB_TOKEN` for GitHub API access (auto-provided) - -**Docker Hub Authentication:** -You'll need to create a Docker Hub access token and add it as a GitHub Secret. Research: -- How to create Docker Hub access tokens -- How to add secrets to your GitHub repository -- How to reference secrets in workflow files (hint: `${{ secrets.NAME }}`) - -
- -
-💡 Versioning Strategy Guidance - -**Semantic Versioning (SemVer):** - -**Format:** MAJOR.MINOR.PATCH (e.g., 1.2.3) -- **MAJOR:** Breaking changes (incompatible API changes) -- **MINOR:** New features (backward-compatible) -- **PATCH:** Bug fixes (backward-compatible) - -**Implementation Approaches:** -1. **Manual Git Tags:** Create git tags (v1.0.0) and reference in workflow -2. **Automated from Commits:** Parse conventional commits to bump version -3. **GitHub Releases:** Trigger on release creation - -**Docker Tagging Example:** -- `username/app:1.2.3` (full version) -- `username/app:1.2` (minor version, rolling) -- `username/app:1` (major version, rolling) -- `username/app:latest` (latest stable) - -**Pros:** Clear when breaking changes occur, industry standard for libraries -**Cons:** Requires discipline to follow rules correctly - ---- - -**Calendar Versioning (CalVer):** - -**Common Formats:** -- `YYYY.MM.DD` (e.g., 2024.01.15) - Daily releases -- `YYYY.MM.MICRO` (e.g., 2024.01.0) - Monthly with patch number -- `YYYY.0M` (e.g., 2024.01) - Monthly releases - -**Implementation Approaches:** -1. **Date-based:** Generate from current date in workflow -2. **Git SHA:** Combine with short commit SHA (2024.01-a1b2c3d) -3. **Build Number:** Use GitHub run number (2024.01.42) - -**Docker Tagging Example:** -- `username/app:2024.01` (month version) -- `username/app:2024.01.123` (with build number) -- `username/app:latest` (latest build) - -**Pros:** No ambiguity, good for continuous deployment, easier to remember -**Cons:** Doesn't indicate breaking changes - ---- - -**How to Implement in CI:** - -**Using docker/metadata-action:** -```yaml -# Pattern - research actual syntax -- name: Docker metadata - uses: docker/metadata-action - with: - # Define your tagging strategy here - # Can reference git tags, dates, commit SHAs -``` - -**Manual Tagging:** -```yaml -# Pattern - research actual syntax -- name: Generate version - run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_ENV - -- name: Build and push - # Use ${{ env.VERSION }} in tags -``` - -**Questions to Consider:** -- How often will you release? (Daily? Per feature? Monthly?) -- Do users need to know about breaking changes explicitly? -- Are you building a library (use SemVer) or a service (CalVer works)? -- How will you track what's in each version? - -**Resources:** -- [Semantic Versioning](https://semver.org/) -- [Calendar Versioning](https://calver.org/) -- [Docker Metadata Action](https://github.com/docker/metadata-action) -- [Conventional Commits](https://www.conventionalcommits.org/) (for automated SemVer) - -
- -
-💡 Debugging GitHub Actions - -**Common Issues & How to Debug:** - -**Workflow Won't Trigger:** -- Check your `on:` configuration -- Verify you pushed to the correct branch -- Look at Actions tab for filtering options - -**Steps Failing:** -- Click into the failed step to see full logs -- Check for typos in action names or parameters -- Verify secrets are configured correctly -- Test commands locally first - -**Docker Build Fails:** -- Ensure Dockerfile is in the correct location -- Check context path in build step -- Verify base image exists and is accessible -- Test Docker build locally first - -**Authentication Issues:** -- Verify secret names match exactly (case-sensitive) -- Check that Docker Hub token has write permissions -- Ensure you're using `docker/login-action` correctly - -**Debugging Techniques:** -- Add `run: echo "Debug message"` steps to understand workflow state -- Use `run: env` to see available environment variables -- Check Actions tab for detailed logs -- Enable debug logging (add `ACTIONS_RUNNER_DEBUG` secret = true) - -
- -**What to Document:** -- Your workflow trigger strategy and reasoning -- Why you chose specific actions from the marketplace -- Your Docker tagging strategy (latest? version tags? commit SHA?) -- Link to successful workflow run in GitHub Actions tab -- Terminal output or screenshot of green checkmark - ---- - -### Task 3 — CI Best Practices & Security (3 pts) - -**Objective:** Optimize your CI workflow and add security scanning. - -**Requirements:** - -1. **Add Status Badge** - - Add a GitHub Actions status badge to your `app_python/README.md` - - The badge should show the current workflow status (passing/failing) - -2. **Implement Dependency Caching** - - Add caching for Python dependencies to speed up workflow - - Measure and document the speed improvement - -3. **Add Security Scanning with Snyk** - - Integrate Snyk vulnerability scanning into your workflow - - Configure it to check for vulnerabilities in your dependencies - - Document any vulnerabilities found and how you addressed them - -4. **Apply CI Best Practices** - - Research and implement at least 3 additional CI best practices - - Document which practices you applied and why they matter - -
-💡 CI Best Practices Guidance - -**Dependency Caching:** - -Caching speeds up workflows by reusing previously downloaded dependencies. - -**Key Concepts:** -- What should be cached? (pip packages, Docker layers, etc.) -- What's the cache key? (based on requirements.txt hash) -- When does cache become invalid? -- How much time does caching save? - -**Actions to Research:** -- `actions/cache` for general caching -- `actions/setup-python` has built-in cache support - -**Questions to Explore:** -- Where are Python packages stored that should be cached? -- How do you measure cache hit vs cache miss? -- What happens if requirements.txt changes? - -**Status Badges:** - -Show workflow status directly in your README. - -**Format Pattern:** -```markdown -![Workflow Name](https://github.com/username/repo/workflows/workflow-name/badge.svg) -``` - -Research how to: -- Get the correct badge URL for your workflow -- Make badges clickable (link to Actions tab) -- Display specific branch status - -**CI Best Practices to Consider:** - -Research and choose at least 3 to implement: - -1. **Fail Fast:** Stop workflow on first failure -2. **Matrix Builds:** Test multiple Python versions (3.12, 3.13) -3. **Job Dependencies:** Don't push Docker if tests fail -4. **Conditional Steps:** Only push on main branch -5. **Pull Request Checks:** Require passing CI before merge -6. **Workflow Concurrency:** Cancel outdated workflow runs -7. **Docker Layer Caching:** Cache Docker build layers -8. **Environment Variables:** Use env for repeated values -9. **Secrets Scanning:** Prevent committing secrets -10. **YAML Validation:** Lint your workflow files - -**Resources:** -- [GitHub Actions Best Practices](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits) -- [Caching Dependencies](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows) -- [Security Hardening](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions) - -
- -
-💡 Snyk Integration Guidance - -**What is Snyk?** - -Snyk is a security tool that scans your dependencies for known vulnerabilities. - -**Key Concepts:** -- Vulnerability databases (CVEs) -- Severity levels (low, medium, high, critical) -- Automated dependency updates -- Security advisories - -**Integration Options:** - -1. **Snyk GitHub Action:** - - Use `snyk/actions` from GitHub Marketplace - - Requires Snyk API token (free tier available) - - Can fail builds on vulnerabilities - -2. **Snyk CLI in Workflow:** - - Install Snyk CLI in workflow - - Run `snyk test` command - - More flexible but requires setup - -**Setup Steps:** -1. Create free Snyk account -2. Get API token from Snyk dashboard -3. Add token as GitHub Secret -4. Add Snyk step to workflow -5. Configure severity threshold (what level fails the build?) - -**Questions to Explore:** -- Should every vulnerability fail your build? -- What if vulnerabilities have no fix available? -- How do you handle false positives? -- When should you break the build vs just warn? - -**Resources:** -- [Snyk GitHub Actions](https://github.com/snyk/actions) -- [Snyk Python Example](https://github.com/snyk/actions/tree/master/python) -- [Snyk Documentation](https://docs.snyk.io/integrations/ci-cd-integrations/github-actions-integration) - -**Common Issues:** -- Dependencies not installed before Snyk runs -- API token not configured correctly -- Overly strict severity settings breaking builds -- Virtual environment confusion - -**What to Document:** -- Your severity threshold decision and reasoning -- Any vulnerabilities found and your response -- Whether you fail builds on vulnerabilities or just warn - -
- -**What to Document:** -- Status badge in README (visible proof it works) -- Caching implementation and speed improvement metrics -- CI best practices you applied with explanations -- Snyk integration results and vulnerability handling -- Terminal output showing improved workflow performance - ---- - -## Bonus Task — Multi-App CI with Path Filters + Test Coverage (2.5 pts) - -**Objective:** Set up CI for your compiled language app with intelligent path-based triggers AND add test coverage tracking. - -**Part 1: Multi-App CI (1.5 pts)** - -1. **Create Second CI Workflow** - - Create `.github/workflows/-ci.yml` for your Go/Rust/Java app - - Implement similar CI steps (lint, test, build Docker image) - - Use language-specific actions and best practices - - Apply versioning strategy (SemVer or CalVer) consistently - -2. **Implement Path-Based Triggers** - - Python workflow should only run when `app_python/` files change - - Compiled language workflow should only run when `app_/` files change - - Neither should run when only docs or other files change - -3. **Optimize for Multiple Apps** - - Ensure both workflows can run in parallel - - Consider using workflow templates (DRY principle) - - Document the benefits of path-based triggers - -**Part 2: Test Coverage Badge (1 pt)** - -4. **Add Coverage Tracking** - - Install coverage tool (`pytest-cov` for Python, coverage tool for your other language) - - Generate coverage reports in CI workflow - - Integrate with codecov.io or coveralls.io (free for public repos) - - Add coverage badge to README showing percentage - -5. **Coverage Goals** - - Document your current coverage percentage - - Identify what's not covered and why - - Set a coverage threshold in CI (e.g., fail if below 70%) - -
-💡 Path Filters & Multi-App CI - -**Why Path Filters?** - -In a monorepo with multiple apps, you don't want to run Python CI when only Go code changes. - -**Path Filter Syntax:** -```yaml -on: - push: - paths: - - 'app_python/**' - - '.github/workflows/python-ci.yml' -``` - -**Key Concepts:** -- Glob patterns for path matching -- When to include workflow file itself -- Exclude patterns (paths-ignore) -- How to test path filters - -**Questions to Explore:** -- Should changes to README.md trigger CI? -- Should changes to the root .gitignore trigger CI? -- What about changes to both apps in one commit? -- How do you test that path filters work correctly? - -**Multi-Language CI Patterns:** - -**For Go:** -- actions/setup-go -- golangci-lint for linting -- go test for testing -- Multi-stage Docker builds (from Lab 2 bonus) - -**For Rust:** -- actions-rs/toolchain -- cargo clippy for linting -- cargo test for testing -- cargo-audit for security - -**For Java:** -- actions/setup-java -- Maven or Gradle for build -- Checkstyle or SpotBugs for linting -- JUnit tests - -**Workflow Reusability:** - -Consider: -- Reusable workflows (call one workflow from another) -- Composite actions (bundle steps together) -- Workflow templates (DRY for similar workflows) - -**Resources:** -- [Path Filters](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onpushpull_requestpaths) -- [Reusable Workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows) -- [Starter Workflows](https://github.com/actions/starter-workflows/tree/main/ci) - -
- -
-💡 Test Coverage Tracking - -**What is Test Coverage?** - -Coverage measures what percentage of your code is executed by your tests. High coverage = more code is tested. - -**Why Coverage Matters:** -- Identifies untested code paths -- Prevents regressions (changes breaking untested code) -- Increases confidence in refactoring -- Industry standard quality metric - -**Coverage Tools by Language:** - -**Python (pytest-cov):** -```bash -# Install -pip install pytest-cov - -# Run with coverage -pytest --cov=app_python --cov-report=xml --cov-report=term - -# Generates coverage.xml for upload -``` - -**Go (built-in):** -```bash -go test -coverprofile=coverage.out ./... -go tool cover -html=coverage.out -``` - -**Rust (tarpaulin):** -```bash -cargo install cargo-tarpaulin -cargo tarpaulin --out Xml -``` - -**Java (JaCoCo with Maven/Gradle):** -```bash -mvn test jacoco:report -# or -gradle test jacocoTestReport -``` - -**Integration Services:** - -**Codecov (Recommended):** -- Free for public repos -- Beautiful visualizations -- PR comments with coverage diff -- Setup: Sign in with GitHub, add repo, upload coverage report - -**Coveralls:** -- Alternative to Codecov -- Similar features -- Different UI - -**Coverage in CI Workflow:** -```yaml -# Pattern for Python (research actual syntax) -- name: Run tests with coverage - run: pytest --cov=. --cov-report=xml - -- name: Upload to Codecov - uses: codecov/codecov-action@v4 - with: - file: ./coverage.xml - token: ${{ secrets.CODECOV_TOKEN }} -``` - -**Coverage Badge:** -```markdown -![Coverage](https://codecov.io/gh/username/repo/branch/main/graph/badge.svg) -``` - -**Setting Coverage Thresholds:** - -You can fail CI if coverage drops below a threshold: - -```yaml -# In pytest.ini or pyproject.toml -[tool:pytest] -addopts = --cov=. --cov-fail-under=70 -``` - -**Questions to Consider:** -- What's a reasonable coverage target? (70%? 80%? 90%?) -- Should you aim for 100% coverage? (Usually no - diminishing returns) -- What code is OK to leave untested? (Error handlers, config, main) -- How do you test hard-to-reach code paths? - -**Best Practices:** -- Don't chase 100% coverage blindly -- Focus on testing critical business logic -- Integration points should have high coverage -- Simple getters/setters can be skipped -- Measure coverage trends, not just absolute numbers - -**Resources:** -- [Codecov Documentation](https://docs.codecov.com/) -- [pytest-cov Documentation](https://pytest-cov.readthedocs.io/) -- [Go Coverage](https://go.dev/blog/cover) -- [Cargo Tarpaulin](https://github.com/xd009642/tarpaulin) -- [JaCoCo](https://www.jacoco.org/) - -
- -**What to Document:** -- Second workflow implementation with language-specific best practices -- Path filter configuration and testing proof -- Benefits analysis: Why path filters matter in monorepos -- Example showing workflows running independently -- Terminal output or Actions tab showing selective triggering -- **Coverage integration:** Screenshot/link to codecov/coveralls dashboard -- **Coverage analysis:** Current percentage, what's covered/not covered, your threshold - ---- - -## How to Submit - -1. **Create Branch:** - - Create a new branch called `lab03` - - Develop your CI workflows on this branch - -2. **Commit Work:** - - Add workflow files (`.github/workflows/`) - - Add test files (`app_python/tests/`) - - Add documentation (`app_python/docs/LAB03.md`) - - Commit with descriptive message following conventional commits - -3. **Verify CI Works:** - - Push to your fork and verify workflows run - - Check that all jobs pass - - Review workflow logs for any issues - -4. **Create Pull Requests:** - - **PR #1:** `your-fork:lab03` → `course-repo:master` - - **PR #2:** `your-fork:lab03` → `your-fork:master` - - CI should run automatically on your PRs - ---- - -## Acceptance Criteria - -### Main Tasks (10 points) - -**Unit Testing (3 pts):** -- [ ] Testing framework chosen with justification -- [ ] Tests exist in `app_python/tests/` directory -- [ ] All endpoints have test coverage -- [ ] Tests pass locally (terminal output provided) -- [ ] README updated with testing instructions - -**GitHub Actions CI (4 pts):** -- [ ] Workflow file exists at `.github/workflows/python-ci.yml` -- [ ] Workflow includes: dependency installation, linting, testing -- [ ] Workflow includes: Docker Hub login, build, and push -- [ ] Versioning strategy chosen (SemVer or CalVer) and implemented -- [ ] Docker images tagged with at least 2 tags (e.g., version + latest) -- [ ] Workflow triggers configured appropriately -- [ ] All workflow steps pass successfully -- [ ] Docker Hub shows versioned images -- [ ] Link to successful workflow run provided - -**CI Best Practices (3 pts):** -- [ ] Status badge added to README and working -- [ ] Dependency caching implemented with performance metrics -- [ ] Snyk security scanning integrated -- [ ] At least 3 CI best practices applied -- [ ] Documentation complete (see Documentation Requirements section) - -### Bonus Task (2.5 points) - -**Part 1: Multi-App CI (1.5 pts)** -- [ ] Second workflow created for compiled language app (`.github/workflows/-ci.yml`) -- [ ] Language-specific linting and testing implemented -- [ ] Versioning strategy applied to second app -- [ ] Path filters configured for both workflows -- [ ] Path filters tested and proven to work (workflows run selectively) -- [ ] Both workflows can run in parallel -- [ ] Documentation explains benefits and shows selective triggering - -**Part 2: Test Coverage (1 pt)** -- [ ] Coverage tool integrated (`pytest-cov` or equivalent) -- [ ] Coverage reports generated in CI workflow -- [ ] Codecov or Coveralls integration complete -- [ ] Coverage badge added to README -- [ ] Coverage threshold set in CI (optional but recommended) -- [ ] Documentation includes coverage analysis (percentage, what's covered/not) - ---- - -## Documentation Requirements - -Create `app_python/docs/LAB03.md` with these sections: - -### 1. Overview -- Testing framework used and why you chose it -- What endpoints/functionality your tests cover -- CI workflow trigger configuration (when does it run?) -- Versioning strategy chosen (SemVer or CalVer) and rationale - -### 2. Workflow Evidence -``` -Provide links/terminal output for: -- ✅ Successful workflow run (GitHub Actions link) -- ✅ Tests passing locally (terminal output) -- ✅ Docker image on Docker Hub (link to your image) -- ✅ Status badge working in README -``` - -### 3. Best Practices Implemented -Quick list with one-sentence explanations: -- **Practice 1:** Why it helps -- **Practice 2:** Why it helps -- **Practice 3:** Why it helps -- **Caching:** Time saved (before vs after) -- **Snyk:** Any vulnerabilities found? Your action taken - -### 4. Key Decisions -Answer these briefly (2-3 sentences each): -- **Versioning Strategy:** SemVer or CalVer? Why did you choose it for your app? -- **Docker Tags:** What tags does your CI create? (e.g., latest, version number, etc.) -- **Workflow Triggers:** Why did you choose those triggers? -- **Test Coverage:** What's tested vs not tested? - -### 5. Challenges (Optional) -- Any issues you encountered and how you fixed them -- Keep it brief - bullet points are fine - ---- - -## Rubric - -| Criteria | Points | Description | -|----------|--------|-------------| -| **Unit Testing** | 3 pts | Comprehensive tests, good coverage | -| **CI Workflow** | 4 pts | Complete, functional, automated | -| **Best Practices** | 3 pts | Optimized, secure, well-documented | -| **Bonus** | 2.5 pts | Multi-app CI with path filters | -| **Total** | 12.5 pts | 10 pts required + 2.5 pts bonus | - -**Grading:** -- **10/10:** All tasks complete, CI works flawlessly, clear documentation, meaningful tests -- **8-9/10:** CI works, good test coverage, best practices applied, solid documentation -- **6-7/10:** CI functional, basic tests, some best practices, minimal documentation -- **<6/10:** CI broken or missing steps, poor tests, incomplete work - -**Quick Checklist for Full Points:** -- ✅ Tests actually test your endpoints (not just imports) -- ✅ CI workflow runs and passes -- ✅ Docker image builds and pushes successfully -- ✅ At least 3 best practices applied (caching, Snyk, status badge, etc.) -- ✅ Documentation complete but concise (no essay needed!) -- ✅ Links/evidence provided (workflow runs, Docker Hub, etc.) - -**Documentation Should Take:** 15-30 minutes to write, 5 minutes to review - ---- - -## Resources - -
-📚 GitHub Actions Documentation - -- [GitHub Actions Quickstart](https://docs.github.com/en/actions/quickstart) -- [Workflow Syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) -- [Building and Testing Python](https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python) -- [Publishing Docker Images](https://docs.docker.com/ci-cd/github-actions/) -- [GitHub Actions Marketplace](https://github.com/marketplace?type=actions) - -
- -
-🧪 Testing Resources - -- [Pytest Documentation](https://docs.pytest.org/) -- [Flask Testing Guide](https://flask.palletsprojects.com/en/stable/testing/) -- [FastAPI Testing Guide](https://fastapi.tiangolo.com/tutorial/testing/) -- [Python Testing Best Practices](https://realpython.com/python-testing/) - -
- -
-🔒 Security & Quality - -- [Snyk GitHub Actions](https://github.com/snyk/actions) -- [Snyk Python Integration](https://docs.snyk.io/integrations/ci-cd-integrations/github-actions-integration) -- [GitHub Security Best Practices](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions) -- [Dependency Scanning](https://docs.github.com/en/code-security/supply-chain-security) - -
- -
-⚡ Performance & Optimization - -- [Caching Dependencies](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows) -- [Docker Build Cache](https://docs.docker.com/build/cache/) -- [Workflow Optimization](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration) - -
- -
-🛠️ CI/CD Tools - -- [act](https://github.com/nektos/act) - Run GitHub Actions locally -- [actionlint](https://github.com/rhysd/actionlint) - Lint workflow files -- [GitHub CLI](https://cli.github.com/) - Manage workflows from terminal - -
- ---- - -## Looking Ahead - -- **Lab 4-6:** CI will validate your Terraform and Ansible code -- **Lab 7-8:** CI will run integration tests with logging/metrics -- **Lab 9-10:** CI will validate Kubernetes manifests and Helm charts -- **Lab 13:** ArgoCD will deploy what CI builds (GitOps!) -- **All Future Labs:** This pipeline is your safety net for changes - ---- - -**Good luck!** 🚀 - -> **Remember:** CI isn't about having green checkmarks—it's about catching problems before they reach production. Focus on meaningful tests and understanding why each practice matters. Think like a DevOps engineer: automate everything, fail fast, and learn from failures. +# Lab 3 — Continuous Integration (CI/CD) + +![difficulty](https://img.shields.io/badge/difficulty-beginner-success) +![topic](https://img.shields.io/badge/topic-CI/CD-blue) +![points](https://img.shields.io/badge/points-10%2B2.5-orange) +![tech](https://img.shields.io/badge/tech-GitHub%20Actions-informational) + +> Automate your Python app testing and Docker builds with GitHub Actions CI/CD pipeline. + +## Overview + +Take your containerized app from Labs 1-2 and add automated testing and deployment. Learn how CI/CD catches bugs early, ensures code quality, and automates the Docker build/push workflow. + +**What You'll Learn:** +- Writing effective unit tests +- GitHub Actions workflow syntax +- CI/CD best practices (caching, matrix builds, security scanning) +- Automated Docker image publishing +- Continuous integration for multiple applications + +**Tech Stack:** GitHub Actions | pytest 8+ | Python 3.11+ | Snyk | Docker + +**Connection to Previous Labs:** +- **Lab 1:** Test the endpoints you created +- **Lab 2:** Automate the Docker build/push workflow +- **Lab 4+:** This CI pipeline will run for all future labs + +--- + +## Tasks + +### Task 1 — Unit Testing (3 pts) + +**Objective:** Write comprehensive unit tests for your Python application to ensure reliability. + +**Requirements:** + +1. **Choose a Testing Framework** + - Research Python testing frameworks (pytest, unittest, etc.) + - Select one and justify your choice + - Install it in your `requirements.txt` or create `requirements-dev.txt` + +2. **Write Unit Tests** + - Create `app_python/tests/` directory + - Write tests for **all** your endpoints: + - `GET /` - Verify JSON structure and required fields + - `GET /health` - Verify health check response + - Test both successful responses and error cases + - Aim for meaningful test coverage (not just basic smoke tests) + +3. **Run Tests Locally** + - Verify all tests pass locally before CI setup + - Document how to run tests in your README + +
+💡 Testing Framework Guidance + +**Popular Python Testing Frameworks:** + +**pytest (Recommended):** +- Pros: Simple syntax, powerful fixtures, excellent plugin ecosystem +- Cons: Additional dependency +- Use case: Most modern Python projects + +**unittest:** +- Pros: Built into Python (no extra dependencies) +- Cons: More verbose, less modern features +- Use case: Minimal dependency projects + +**Key Testing Concepts to Research:** +- Test fixtures and setup/teardown +- Mocking external dependencies +- Testing HTTP endpoints (test client usage) +- Test coverage measurement +- Assertions and expected vs actual results + +**What Should You Test?** +- Correct HTTP status codes (200, 404, 500) +- Response data structure (JSON fields present) +- Response data types (strings, integers, etc.) +- Edge cases (invalid requests, missing data) +- Error handling (what happens when things fail?) + +**Questions to Consider:** +- How do you test a Flask/FastAPI app without starting the server? +- Should you test that `hostname` returns your actual hostname, or just that the field exists? +- How do you simulate different client IPs or user agents in tests? + +**Resources:** +- [Pytest Documentation](https://docs.pytest.org/) +- [Flask Testing](https://flask.palletsprojects.com/en/stable/testing/) +- [FastAPI Testing](https://fastapi.tiangolo.com/tutorial/testing/) +- [Python unittest](https://docs.python.org/3/library/unittest.html) + +**Anti-Patterns to Avoid:** +- Testing framework functionality instead of your code +- Tests that always pass regardless of implementation +- Tests with no assertions +- Tests that depend on external services + +
+ +**What to Document:** +- Your testing framework choice and why +- Test structure explanation +- How to run tests locally +- Terminal output showing all tests passing + +--- + +### Task 2 — GitHub Actions CI Workflow (4 pts) + +**Objective:** Create a GitHub Actions workflow that automatically tests your code and builds Docker images with proper versioning. + +**Requirements:** + +1. **Create Workflow File** + - Create `.github/workflows/python-ci.yml` in your repository + - Name your workflow descriptively + +2. **Implement Essential CI Steps** + + Your workflow must include these logical stages: + + **a) Code Quality & Testing:** + - Install dependencies + - Run a linter (pylint, flake8, black, ruff, etc.) + - Run your unit tests + + **b) Docker Build & Push with Versioning:** + - Authenticate with Docker Hub + - Build your Docker image + - Tag with proper version strategy (see versioning section below) + - Push to Docker Hub with multiple tags + +3. **Versioning Strategy** + + Choose **one** versioning approach and implement it: + + **Option A: Semantic Versioning (SemVer)** + - Version format: `v1.2.3` (major.minor.patch) + - Use git tags for releases + - Tag images like: `username/app:1.2.3`, `username/app:1.2`, `username/app:latest` + - **When to use:** Traditional software releases with breaking changes + + **Option B: Calendar Versioning (CalVer)** + - Version format: `2024.01.15` or `2024.01` (year.month.day or year.month) + - Based on release date + - Tag images like: `username/app:2024.01`, `username/app:latest` + - **When to use:** Time-based releases, continuous deployment + + **Required:** + - Document which strategy you chose and why + - Implement it in your CI workflow + - Show at least 2 tags per image (e.g., version + latest) + +4. **Workflow Triggers** + - Configure when the workflow runs (push, pull request, etc.) + - Consider which branches should trigger builds + +5. **Testing the Workflow** + - Push your workflow file and verify it runs + - Fix any issues that arise + - Ensure all steps complete successfully + - Verify Docker Hub shows your version tags + +
+💡 GitHub Actions Concepts + +**Core Concepts to Research:** + +**Workflow Anatomy:** +- `name` - What is your workflow called? +- `on` - When does it run? (push, pull_request, schedule, etc.) +- `jobs` - What work needs to be done? +- `steps` - Individual commands within a job +- `runs-on` - What OS environment? (ubuntu-latest, etc.) + +**Key Questions:** +- Should you run CI on every push, or only on pull requests? +- What happens if tests fail? Should the workflow continue? +- How do you access secrets (like Docker Hub credentials) securely? +- Why might you want multiple jobs vs multiple steps in one job? + +**Python CI Steps Pattern:** +```yaml +# This is a pattern, not exact copy-paste code +# Research the actual syntax and actions needed + +- Set up Python environment +- Install dependencies +- Run linter +- Run tests +``` + +**Docker CI Steps Pattern:** +```yaml +# This is a pattern, not exact copy-paste code +# Research the actual actions and their parameters + +- Log in to Docker Hub +- Extract metadata for tags +- Build and push Docker image +``` + +**Important Concepts:** +- **Actions Marketplace:** Reusable actions (actions/checkout@v4, actions/setup-python@v5, docker/build-push-action@v6) +- **Secrets:** How to store Docker Hub credentials securely +- **Job Dependencies:** Can one job depend on another succeeding? +- **Matrix Builds:** Testing multiple Python versions (optional but good to know) +- **Caching:** Speed up workflows by caching dependencies (we'll add this in Task 3) + +**Resources:** +- [GitHub Actions Documentation](https://docs.github.com/en/actions) +- [Building and Testing Python](https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python) +- [Publishing Docker Images](https://docs.docker.com/ci-cd/github-actions/) +- [GitHub Actions Marketplace](https://github.com/marketplace?type=actions) +- [Workflow Syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) + +**Security Best Practices:** +- Never hardcode passwords or tokens in workflow files +- Use GitHub Secrets for sensitive data +- Understand when secrets are exposed to pull requests from forks +- Use `secrets.GITHUB_TOKEN` for GitHub API access (auto-provided) + +**Docker Hub Authentication:** +You'll need to create a Docker Hub access token and add it as a GitHub Secret. Research: +- How to create Docker Hub access tokens +- How to add secrets to your GitHub repository +- How to reference secrets in workflow files (hint: `${{ secrets.NAME }}`) + +
+ +
+💡 Versioning Strategy Guidance + +**Semantic Versioning (SemVer):** + +**Format:** MAJOR.MINOR.PATCH (e.g., 1.2.3) +- **MAJOR:** Breaking changes (incompatible API changes) +- **MINOR:** New features (backward-compatible) +- **PATCH:** Bug fixes (backward-compatible) + +**Implementation Approaches:** +1. **Manual Git Tags:** Create git tags (v1.0.0) and reference in workflow +2. **Automated from Commits:** Parse conventional commits to bump version +3. **GitHub Releases:** Trigger on release creation + +**Docker Tagging Example:** +- `username/app:1.2.3` (full version) +- `username/app:1.2` (minor version, rolling) +- `username/app:1` (major version, rolling) +- `username/app:latest` (latest stable) + +**Pros:** Clear when breaking changes occur, industry standard for libraries +**Cons:** Requires discipline to follow rules correctly + +--- + +**Calendar Versioning (CalVer):** + +**Common Formats:** +- `YYYY.MM.DD` (e.g., 2024.01.15) - Daily releases +- `YYYY.MM.MICRO` (e.g., 2024.01.0) - Monthly with patch number +- `YYYY.0M` (e.g., 2024.01) - Monthly releases + +**Implementation Approaches:** +1. **Date-based:** Generate from current date in workflow +2. **Git SHA:** Combine with short commit SHA (2024.01-a1b2c3d) +3. **Build Number:** Use GitHub run number (2024.01.42) + +**Docker Tagging Example:** +- `username/app:2024.01` (month version) +- `username/app:2024.01.123` (with build number) +- `username/app:latest` (latest build) + +**Pros:** No ambiguity, good for continuous deployment, easier to remember +**Cons:** Doesn't indicate breaking changes + +--- + +**How to Implement in CI:** + +**Using docker/metadata-action:** +```yaml +# Pattern - research actual syntax +- name: Docker metadata + uses: docker/metadata-action + with: + # Define your tagging strategy here + # Can reference git tags, dates, commit SHAs +``` + +**Manual Tagging:** +```yaml +# Pattern - research actual syntax +- name: Generate version + run: echo "VERSION=$(date +%Y.%m.%d)" >> $GITHUB_ENV + +- name: Build and push + # Use ${{ env.VERSION }} in tags +``` + +**Questions to Consider:** +- How often will you release? (Daily? Per feature? Monthly?) +- Do users need to know about breaking changes explicitly? +- Are you building a library (use SemVer) or a service (CalVer works)? +- How will you track what's in each version? + +**Resources:** +- [Semantic Versioning](https://semver.org/) +- [Calendar Versioning](https://calver.org/) +- [Docker Metadata Action](https://github.com/docker/metadata-action) +- [Conventional Commits](https://www.conventionalcommits.org/) (for automated SemVer) + +
+ +
+💡 Debugging GitHub Actions + +**Common Issues & How to Debug:** + +**Workflow Won't Trigger:** +- Check your `on:` configuration +- Verify you pushed to the correct branch +- Look at Actions tab for filtering options + +**Steps Failing:** +- Click into the failed step to see full logs +- Check for typos in action names or parameters +- Verify secrets are configured correctly +- Test commands locally first + +**Docker Build Fails:** +- Ensure Dockerfile is in the correct location +- Check context path in build step +- Verify base image exists and is accessible +- Test Docker build locally first + +**Authentication Issues:** +- Verify secret names match exactly (case-sensitive) +- Check that Docker Hub token has write permissions +- Ensure you're using `docker/login-action` correctly + +**Debugging Techniques:** +- Add `run: echo "Debug message"` steps to understand workflow state +- Use `run: env` to see available environment variables +- Check Actions tab for detailed logs +- Enable debug logging (add `ACTIONS_RUNNER_DEBUG` secret = true) + +
+ +**What to Document:** +- Your workflow trigger strategy and reasoning +- Why you chose specific actions from the marketplace +- Your Docker tagging strategy (latest? version tags? commit SHA?) +- Link to successful workflow run in GitHub Actions tab +- Terminal output or screenshot of green checkmark + +--- + +### Task 3 — CI Best Practices & Security (3 pts) + +**Objective:** Optimize your CI workflow and add security scanning. + +**Requirements:** + +1. **Add Status Badge** + - Add a GitHub Actions status badge to your `app_python/README.md` + - The badge should show the current workflow status (passing/failing) + +2. **Implement Dependency Caching** + - Add caching for Python dependencies to speed up workflow + - Measure and document the speed improvement + +3. **Add Security Scanning with Snyk** + - Integrate Snyk vulnerability scanning into your workflow + - Configure it to check for vulnerabilities in your dependencies + - Document any vulnerabilities found and how you addressed them + +4. **Apply CI Best Practices** + - Research and implement at least 3 additional CI best practices + - Document which practices you applied and why they matter + +
+💡 CI Best Practices Guidance + +**Dependency Caching:** + +Caching speeds up workflows by reusing previously downloaded dependencies. + +**Key Concepts:** +- What should be cached? (pip packages, Docker layers, etc.) +- What's the cache key? (based on requirements.txt hash) +- When does cache become invalid? +- How much time does caching save? + +**Actions to Research:** +- `actions/cache` for general caching +- `actions/setup-python` has built-in cache support + +**Questions to Explore:** +- Where are Python packages stored that should be cached? +- How do you measure cache hit vs cache miss? +- What happens if requirements.txt changes? + +**Status Badges:** + +Show workflow status directly in your README. + +**Format Pattern:** +```markdown +![Workflow Name](https://github.com/username/repo/workflows/workflow-name/badge.svg) +``` + +Research how to: +- Get the correct badge URL for your workflow +- Make badges clickable (link to Actions tab) +- Display specific branch status + +**CI Best Practices to Consider:** + +Research and choose at least 3 to implement: + +1. **Fail Fast:** Stop workflow on first failure +2. **Matrix Builds:** Test multiple Python versions (3.12, 3.13) +3. **Job Dependencies:** Don't push Docker if tests fail +4. **Conditional Steps:** Only push on main branch +5. **Pull Request Checks:** Require passing CI before merge +6. **Workflow Concurrency:** Cancel outdated workflow runs +7. **Docker Layer Caching:** Cache Docker build layers +8. **Environment Variables:** Use env for repeated values +9. **Secrets Scanning:** Prevent committing secrets +10. **YAML Validation:** Lint your workflow files + +**Resources:** +- [GitHub Actions Best Practices](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration#usage-limits) +- [Caching Dependencies](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows) +- [Security Hardening](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions) + +
+ +
+💡 Snyk Integration Guidance + +**What is Snyk?** + +Snyk is a security tool that scans your dependencies for known vulnerabilities. + +**Key Concepts:** +- Vulnerability databases (CVEs) +- Severity levels (low, medium, high, critical) +- Automated dependency updates +- Security advisories + +**Integration Options:** + +1. **Snyk GitHub Action:** + - Use `snyk/actions` from GitHub Marketplace + - Requires Snyk API token (free tier available) + - Can fail builds on vulnerabilities + +2. **Snyk CLI in Workflow:** + - Install Snyk CLI in workflow + - Run `snyk test` command + - More flexible but requires setup + +**Setup Steps:** +1. Create free Snyk account +2. Get API token from Snyk dashboard +3. Add token as GitHub Secret +4. Add Snyk step to workflow +5. Configure severity threshold (what level fails the build?) + +**Questions to Explore:** +- Should every vulnerability fail your build? +- What if vulnerabilities have no fix available? +- How do you handle false positives? +- When should you break the build vs just warn? + +**Resources:** +- [Snyk GitHub Actions](https://github.com/snyk/actions) +- [Snyk Python Example](https://github.com/snyk/actions/tree/master/python) +- [Snyk Documentation](https://docs.snyk.io/integrations/ci-cd-integrations/github-actions-integration) + +**Common Issues:** +- Dependencies not installed before Snyk runs +- API token not configured correctly +- Overly strict severity settings breaking builds +- Virtual environment confusion + +**What to Document:** +- Your severity threshold decision and reasoning +- Any vulnerabilities found and your response +- Whether you fail builds on vulnerabilities or just warn + +
+ +**What to Document:** +- Status badge in README (visible proof it works) +- Caching implementation and speed improvement metrics +- CI best practices you applied with explanations +- Snyk integration results and vulnerability handling +- Terminal output showing improved workflow performance + +--- + +## Bonus Task — Multi-App CI with Path Filters + Test Coverage (2.5 pts) + +**Objective:** Set up CI for your compiled language app with intelligent path-based triggers AND add test coverage tracking. + +**Part 1: Multi-App CI (1.5 pts)** + +1. **Create Second CI Workflow** + - Create `.github/workflows/-ci.yml` for your Go/Rust/Java app + - Implement similar CI steps (lint, test, build Docker image) + - Use language-specific actions and best practices + - Apply versioning strategy (SemVer or CalVer) consistently + +2. **Implement Path-Based Triggers** + - Python workflow should only run when `app_python/` files change + - Compiled language workflow should only run when `app_/` files change + - Neither should run when only docs or other files change + +3. **Optimize for Multiple Apps** + - Ensure both workflows can run in parallel + - Consider using workflow templates (DRY principle) + - Document the benefits of path-based triggers + +**Part 2: Test Coverage Badge (1 pt)** + +4. **Add Coverage Tracking** + - Install coverage tool (`pytest-cov` for Python, coverage tool for your other language) + - Generate coverage reports in CI workflow + - Integrate with codecov.io or coveralls.io (free for public repos) + - Add coverage badge to README showing percentage + +5. **Coverage Goals** + - Document your current coverage percentage + - Identify what's not covered and why + - Set a coverage threshold in CI (e.g., fail if below 70%) + +
+💡 Path Filters & Multi-App CI + +**Why Path Filters?** + +In a monorepo with multiple apps, you don't want to run Python CI when only Go code changes. + +**Path Filter Syntax:** +```yaml +on: + push: + paths: + - 'app_python/**' + - '.github/workflows/python-ci.yml' +``` + +**Key Concepts:** +- Glob patterns for path matching +- When to include workflow file itself +- Exclude patterns (paths-ignore) +- How to test path filters + +**Questions to Explore:** +- Should changes to README.md trigger CI? +- Should changes to the root .gitignore trigger CI? +- What about changes to both apps in one commit? +- How do you test that path filters work correctly? + +**Multi-Language CI Patterns:** + +**For Go:** +- actions/setup-go +- golangci-lint for linting +- go test for testing +- Multi-stage Docker builds (from Lab 2 bonus) + +**For Rust:** +- actions-rs/toolchain +- cargo clippy for linting +- cargo test for testing +- cargo-audit for security + +**For Java:** +- actions/setup-java +- Maven or Gradle for build +- Checkstyle or SpotBugs for linting +- JUnit tests + +**Workflow Reusability:** + +Consider: +- Reusable workflows (call one workflow from another) +- Composite actions (bundle steps together) +- Workflow templates (DRY for similar workflows) + +**Resources:** +- [Path Filters](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#onpushpull_requestpaths) +- [Reusable Workflows](https://docs.github.com/en/actions/using-workflows/reusing-workflows) +- [Starter Workflows](https://github.com/actions/starter-workflows/tree/main/ci) + +
+ +
+💡 Test Coverage Tracking + +**What is Test Coverage?** + +Coverage measures what percentage of your code is executed by your tests. High coverage = more code is tested. + +**Why Coverage Matters:** +- Identifies untested code paths +- Prevents regressions (changes breaking untested code) +- Increases confidence in refactoring +- Industry standard quality metric + +**Coverage Tools by Language:** + +**Python (pytest-cov):** +```bash +# Install +pip install pytest-cov + +# Run with coverage +pytest --cov=app_python --cov-report=xml --cov-report=term + +# Generates coverage.xml for upload +``` + +**Go (built-in):** +```bash +go test -coverprofile=coverage.out ./... +go tool cover -html=coverage.out +``` + +**Rust (tarpaulin):** +```bash +cargo install cargo-tarpaulin +cargo tarpaulin --out Xml +``` + +**Java (JaCoCo with Maven/Gradle):** +```bash +mvn test jacoco:report +# or +gradle test jacocoTestReport +``` + +**Integration Services:** + +**Codecov (Recommended):** +- Free for public repos +- Beautiful visualizations +- PR comments with coverage diff +- Setup: Sign in with GitHub, add repo, upload coverage report + +**Coveralls:** +- Alternative to Codecov +- Similar features +- Different UI + +**Coverage in CI Workflow:** +```yaml +# Pattern for Python (research actual syntax) +- name: Run tests with coverage + run: pytest --cov=. --cov-report=xml + +- name: Upload to Codecov + uses: codecov/codecov-action@v4 + with: + file: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} +``` + +**Coverage Badge:** +```markdown +![Coverage](https://codecov.io/gh/username/repo/branch/main/graph/badge.svg) +``` + +**Setting Coverage Thresholds:** + +You can fail CI if coverage drops below a threshold: + +```yaml +# In pytest.ini or pyproject.toml +[tool:pytest] +addopts = --cov=. --cov-fail-under=70 +``` + +**Questions to Consider:** +- What's a reasonable coverage target? (70%? 80%? 90%?) +- Should you aim for 100% coverage? (Usually no - diminishing returns) +- What code is OK to leave untested? (Error handlers, config, main) +- How do you test hard-to-reach code paths? + +**Best Practices:** +- Don't chase 100% coverage blindly +- Focus on testing critical business logic +- Integration points should have high coverage +- Simple getters/setters can be skipped +- Measure coverage trends, not just absolute numbers + +**Resources:** +- [Codecov Documentation](https://docs.codecov.com/) +- [pytest-cov Documentation](https://pytest-cov.readthedocs.io/) +- [Go Coverage](https://go.dev/blog/cover) +- [Cargo Tarpaulin](https://github.com/xd009642/tarpaulin) +- [JaCoCo](https://www.jacoco.org/) + +
+ +**What to Document:** +- Second workflow implementation with language-specific best practices +- Path filter configuration and testing proof +- Benefits analysis: Why path filters matter in monorepos +- Example showing workflows running independently +- Terminal output or Actions tab showing selective triggering +- **Coverage integration:** Screenshot/link to codecov/coveralls dashboard +- **Coverage analysis:** Current percentage, what's covered/not covered, your threshold + +--- + +## How to Submit + +1. **Create Branch:** + - Create a new branch called `lab03` + - Develop your CI workflows on this branch + +2. **Commit Work:** + - Add workflow files (`.github/workflows/`) + - Add test files (`app_python/tests/`) + - Add documentation (`app_python/docs/LAB03.md`) + - Commit with descriptive message following conventional commits + +3. **Verify CI Works:** + - Push to your fork and verify workflows run + - Check that all jobs pass + - Review workflow logs for any issues + +4. **Create Pull Requests:** + - **PR #1:** `your-fork:lab03` → `course-repo:master` + - **PR #2:** `your-fork:lab03` → `your-fork:master` + - CI should run automatically on your PRs + +--- + +## Acceptance Criteria + +### Main Tasks (10 points) + +**Unit Testing (3 pts):** +- [ ] Testing framework chosen with justification +- [ ] Tests exist in `app_python/tests/` directory +- [ ] All endpoints have test coverage +- [ ] Tests pass locally (terminal output provided) +- [ ] README updated with testing instructions + +**GitHub Actions CI (4 pts):** +- [ ] Workflow file exists at `.github/workflows/python-ci.yml` +- [ ] Workflow includes: dependency installation, linting, testing +- [ ] Workflow includes: Docker Hub login, build, and push +- [ ] Versioning strategy chosen (SemVer or CalVer) and implemented +- [ ] Docker images tagged with at least 2 tags (e.g., version + latest) +- [ ] Workflow triggers configured appropriately +- [ ] All workflow steps pass successfully +- [ ] Docker Hub shows versioned images +- [ ] Link to successful workflow run provided + +**CI Best Practices (3 pts):** +- [ ] Status badge added to README and working +- [ ] Dependency caching implemented with performance metrics +- [ ] Snyk security scanning integrated +- [ ] At least 3 CI best practices applied +- [ ] Documentation complete (see Documentation Requirements section) + +### Bonus Task (2.5 points) + +**Part 1: Multi-App CI (1.5 pts)** +- [ ] Second workflow created for compiled language app (`.github/workflows/-ci.yml`) +- [ ] Language-specific linting and testing implemented +- [ ] Versioning strategy applied to second app +- [ ] Path filters configured for both workflows +- [ ] Path filters tested and proven to work (workflows run selectively) +- [ ] Both workflows can run in parallel +- [ ] Documentation explains benefits and shows selective triggering + +**Part 2: Test Coverage (1 pt)** +- [ ] Coverage tool integrated (`pytest-cov` or equivalent) +- [ ] Coverage reports generated in CI workflow +- [ ] Codecov or Coveralls integration complete +- [ ] Coverage badge added to README +- [ ] Coverage threshold set in CI (optional but recommended) +- [ ] Documentation includes coverage analysis (percentage, what's covered/not) + +--- + +## Documentation Requirements + +Create `app_python/docs/LAB03.md` with these sections: + +### 1. Overview +- Testing framework used and why you chose it +- What endpoints/functionality your tests cover +- CI workflow trigger configuration (when does it run?) +- Versioning strategy chosen (SemVer or CalVer) and rationale + +### 2. Workflow Evidence +``` +Provide links/terminal output for: +- ✅ Successful workflow run (GitHub Actions link) +- ✅ Tests passing locally (terminal output) +- ✅ Docker image on Docker Hub (link to your image) +- ✅ Status badge working in README +``` + +### 3. Best Practices Implemented +Quick list with one-sentence explanations: +- **Practice 1:** Why it helps +- **Practice 2:** Why it helps +- **Practice 3:** Why it helps +- **Caching:** Time saved (before vs after) +- **Snyk:** Any vulnerabilities found? Your action taken + +### 4. Key Decisions +Answer these briefly (2-3 sentences each): +- **Versioning Strategy:** SemVer or CalVer? Why did you choose it for your app? +- **Docker Tags:** What tags does your CI create? (e.g., latest, version number, etc.) +- **Workflow Triggers:** Why did you choose those triggers? +- **Test Coverage:** What's tested vs not tested? + +### 5. Challenges (Optional) +- Any issues you encountered and how you fixed them +- Keep it brief - bullet points are fine + +--- + +## Rubric + +| Criteria | Points | Description | +|----------|--------|-------------| +| **Unit Testing** | 3 pts | Comprehensive tests, good coverage | +| **CI Workflow** | 4 pts | Complete, functional, automated | +| **Best Practices** | 3 pts | Optimized, secure, well-documented | +| **Bonus** | 2.5 pts | Multi-app CI with path filters | +| **Total** | 12.5 pts | 10 pts required + 2.5 pts bonus | + +**Grading:** +- **10/10:** All tasks complete, CI works flawlessly, clear documentation, meaningful tests +- **8-9/10:** CI works, good test coverage, best practices applied, solid documentation +- **6-7/10:** CI functional, basic tests, some best practices, minimal documentation +- **<6/10:** CI broken or missing steps, poor tests, incomplete work + +**Quick Checklist for Full Points:** +- ✅ Tests actually test your endpoints (not just imports) +- ✅ CI workflow runs and passes +- ✅ Docker image builds and pushes successfully +- ✅ At least 3 best practices applied (caching, Snyk, status badge, etc.) +- ✅ Documentation complete but concise (no essay needed!) +- ✅ Links/evidence provided (workflow runs, Docker Hub, etc.) + +**Documentation Should Take:** 15-30 minutes to write, 5 minutes to review + +--- + +## Resources + +
+📚 GitHub Actions Documentation + +- [GitHub Actions Quickstart](https://docs.github.com/en/actions/quickstart) +- [Workflow Syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions) +- [Building and Testing Python](https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python) +- [Publishing Docker Images](https://docs.docker.com/ci-cd/github-actions/) +- [GitHub Actions Marketplace](https://github.com/marketplace?type=actions) + +
+ +
+🧪 Testing Resources + +- [Pytest Documentation](https://docs.pytest.org/) +- [Flask Testing Guide](https://flask.palletsprojects.com/en/stable/testing/) +- [FastAPI Testing Guide](https://fastapi.tiangolo.com/tutorial/testing/) +- [Python Testing Best Practices](https://realpython.com/python-testing/) + +
+ +
+🔒 Security & Quality + +- [Snyk GitHub Actions](https://github.com/snyk/actions) +- [Snyk Python Integration](https://docs.snyk.io/integrations/ci-cd-integrations/github-actions-integration) +- [GitHub Security Best Practices](https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions) +- [Dependency Scanning](https://docs.github.com/en/code-security/supply-chain-security) + +
+ +
+⚡ Performance & Optimization + +- [Caching Dependencies](https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows) +- [Docker Build Cache](https://docs.docker.com/build/cache/) +- [Workflow Optimization](https://docs.github.com/en/actions/learn-github-actions/usage-limits-billing-and-administration) + +
+ +
+🛠️ CI/CD Tools + +- [act](https://github.com/nektos/act) - Run GitHub Actions locally +- [actionlint](https://github.com/rhysd/actionlint) - Lint workflow files +- [GitHub CLI](https://cli.github.com/) - Manage workflows from terminal + +
+ +--- + +## Looking Ahead + +- **Lab 4-6:** CI will validate your Terraform and Ansible code +- **Lab 7-8:** CI will run integration tests with logging/metrics +- **Lab 9-10:** CI will validate Kubernetes manifests and Helm charts +- **Lab 13:** ArgoCD will deploy what CI builds (GitOps!) +- **All Future Labs:** This pipeline is your safety net for changes + +--- + +**Good luck!** 🚀 + +> **Remember:** CI isn't about having green checkmarks—it's about catching problems before they reach production. Focus on meaningful tests and understanding why each practice matters. Think like a DevOps engineer: automate everything, fail fast, and learn from failures. diff --git a/labs/lab04.md b/labs/lab04.md index eefa858953..a98794f6e1 100644 --- a/labs/lab04.md +++ b/labs/lab04.md @@ -1,1510 +1,1510 @@ -# Lab 4 — Infrastructure as Code (Terraform & Pulumi) - -![difficulty](https://img.shields.io/badge/difficulty-beginner-success) -![topic](https://img.shields.io/badge/topic-Infrastructure%20as%20Code-blue) -![points](https://img.shields.io/badge/points-10%2B2.5-orange) -![tech](https://img.shields.io/badge/tech-Terraform%20%7C%20Pulumi-informational) - -> Provision cloud infrastructure using code with Terraform and Pulumi, comparing both approaches. - -## Overview - -Learn Infrastructure as Code (IaC) by creating virtual machines in the cloud using two popular tools: Terraform (declarative, HCL) and Pulumi (imperative, real programming languages). - -**What You'll Learn:** -- Terraform fundamentals and HCL syntax -- Pulumi fundamentals and infrastructure with code -- Cloud provider APIs and resources -- Infrastructure lifecycle management -- IaC best practices and validation -- Comparing IaC tools and approaches - -**Connection to Previous Labs:** -- **Lab 2:** Created Docker images - now we'll provision infrastructure to run them -- **Lab 3:** CI/CD for applications - now we'll add CI/CD for infrastructure -- **Lab 5:** Ansible will provision software on these VMs (you'll need a VM ready!) - -**Tech Stack:** Terraform 1.9+ | Pulumi 3.x | Yandex Cloud / AWS - -**Why Two Tools?** -By using both Terraform and Pulumi for the same task, you'll understand: -- Different IaC philosophies (declarative vs imperative) -- Tool trade-offs and use cases -- How to evaluate IaC tools for your needs - -**Important for Lab 5:** -The VM you create in this lab will be used in **Lab 5 (Ansible)** for configuration management. You have two options: -- **Option A (Recommended):** Keep your cloud VM running until you complete Lab 5 -- **Option B:** Use a local VM (see Local VM Alternative section below) - -If you choose to destroy your cloud VM after Lab 4, you can easily recreate it later using your Terraform/Pulumi code! - ---- - -## Important: Cloud Provider Selection - -### Recommended for Russia: Yandex Cloud - -Yandex Cloud offers free tier and is accessible in Russia: -- 1 VM with 20% vCPU, 1 GB RAM (free tier) -- 10 GB SSD storage -- No credit card required initially - -### Alternative Cloud Providers - -If Yandex Cloud is unavailable, choose any of these: - -**VK Cloud (Russia):** -- Russian cloud provider -- Free trial with bonus credits -- Good documentation in Russian - -**AWS (Amazon Web Services):** -- 750 hours/month free tier (t2.micro) -- Most popular globally -- Extensive documentation - -**GCP (Google Cloud Platform):** -- $300 free credits for 90 days -- Always-free tier for e2-micro -- Modern interface - -**Azure (Microsoft):** -- $200 free credits for 30 days -- Free tier for B1s instances -- Good Windows support - -**DigitalOcean:** -- Simple pricing and interface -- $200 free credits with GitHub Student Pack -- Beginner-friendly - -### Cost Management 🚨 - -**IMPORTANT - Read This:** -- ✅ **Use smallest/free tier instances only** -- ✅ **Run `terraform destroy` when done testing** -- ✅ **Consider keeping VM for Lab 5 to avoid recreation** -- ✅ **Set billing alerts if available** -- ✅ **If not using for Lab 5, delete resources after lab completion** -- ❌ **Never commit cloud credentials to Git** - ---- - -## Local VM Alternative - -If you cannot or prefer not to use cloud providers, you can use a local VM instead. This VM will need to meet specific requirements for Lab 5 (Ansible). - -### Option 1: VirtualBox/VMware VM - -**Requirements:** -- Ubuntu 24.04 LTS (recommended) or Ubuntu 22.04 LTS -- 1 GB RAM minimum (2 GB recommended) -- 10 GB disk space -- Network adapter in Bridged mode (or NAT with port forwarding) -- SSH server installed and configured -- Your SSH public key added to `~/.ssh/authorized_keys` -- Static or predictable IP address - -**Setup Steps:** -```bash -# Install SSH server (if not installed) -sudo apt update -sudo apt install openssh-server - -# Add your SSH public key -mkdir -p ~/.ssh -echo "your-public-key-here" >> ~/.ssh/authorized_keys -chmod 700 ~/.ssh -chmod 600 ~/.ssh/authorized_keys - -# Verify SSH access from your host machine -ssh username@vm-ip-address -``` - -### Option 2: Vagrant VM - -**Requirements:** -- Vagrant installed on your machine -- VirtualBox (or another Vagrant provider) - -**Basic Vagrantfile:** -```ruby -Vagrant.configure("2") do |config| - config.vm.box = "ubuntu/noble64" # Ubuntu 24.04 LTS - # Or use "ubuntu/jammy64" for Ubuntu 22.04 LTS - config.vm.network "private_network", ip: "192.168.56.10" - config.vm.provider "virtualbox" do |vb| - vb.memory = "2048" - end -end -``` - -### Option 3: WSL2 (Windows Subsystem for Linux) - -**Note:** WSL2 can work but has networking limitations. Bridged mode VM is preferred. - -**If using local VM:** -- You can skip Terraform/Pulumi cloud provider setup -- Document your local VM setup instead -- For Task 1, show VM creation (manual or Vagrant) -- For Task 2, you can skip Pulumi (or use Pulumi to manage Vagrant) -- Focus on understanding IaC concepts with cloud provider research - -**Recommended Approach:** -Even with a local VM, complete the Terraform/Pulumi tasks with a cloud provider to gain real IaC experience. You can destroy the cloud VM after Lab 4 and use your local VM for Lab 5. - ---- - -## Tasks - -### Task 1 — Terraform VM Creation (4 pts) - -**Objective:** Create a virtual machine using Terraform on your chosen cloud provider. - -**Requirements:** - -1. **Setup Terraform** - - Install Terraform CLI - - Choose and configure your cloud provider - - Set up authentication (access keys, service accounts, etc.) - - Initialize Terraform - -2. **Define Infrastructure** - - Create a `terraform/` directory with the following resources: - - **Minimum Required Resources:** - - **VM/Compute Instance** (smallest free tier size) - - **Network/VPC** (if required by provider) - - **Security Group/Firewall Rules:** - - Allow SSH (port 22) from your IP - - Allow HTTP (port 80) - - Allow custom port 5000 (for future app deployment) - - **Public IP Address** (to access VM remotely) - -3. **Configuration Best Practices** - - Use variables for configurable values (region, instance type, etc.) - - Use outputs to display important information (public IP, etc.) - - Add appropriate tags/labels for resource identification - - Use `.gitignore` for sensitive files - -4. **Apply Infrastructure** - - Run `terraform plan` to preview changes - - Review the plan carefully - - Apply infrastructure - - Verify VM is accessible via SSH - - Document the public IP and connection method - -5. **State Management** - - Keep state file local (for now) - - Understand what the state file contains - - **Never commit `terraform.tfstate` to Git** - -
-💡 Terraform Fundamentals - -**What is Terraform?** - -Terraform is a declarative IaC tool that lets you define infrastructure in configuration files (HCL - HashiCorp Configuration Language). - -**Key Concepts:** - -**Providers:** -- Plugins that interact with cloud APIs -- Each cloud has its own provider (yandex, aws, google, azurerm) -- Configure authentication and region - -**Resources:** -- Infrastructure components (VMs, networks, firewalls) -- Format: `resource "type" "name" { ... }` -- Each resource has required and optional arguments - -**Data Sources:** -- Query existing infrastructure -- Example: Find latest Ubuntu image ID -- Format: `data "type" "name" { ... }` - -**Variables:** -- Make configurations reusable -- Define in `variables.tf` -- Set values in `terraform.tfvars` (gitignored!) -- Reference: `var.variable_name` - -**Outputs:** -- Display important values after apply -- Example: VM public IP -- Define in `outputs.tf` - -**State File:** -- Tracks real infrastructure -- Maps config to reality -- **Never commit to Git** (contains sensitive data) -- Add to `.gitignore` - -**Typical Workflow:** -```bash -terraform init # Initialize provider plugins -terraform fmt # Format code -terraform validate # Check syntax -terraform plan # Preview changes -terraform apply # Create/update infrastructure -terraform destroy # Delete all infrastructure -``` - -**Resources:** -- [Terraform Documentation](https://developer.hashicorp.com/terraform/docs) -- [Terraform Registry](https://registry.terraform.io/) - Provider docs -- [HCL Syntax](https://developer.hashicorp.com/terraform/language/syntax) - -
- -
-☁️ Yandex Cloud Terraform Guide - -**Yandex Cloud Setup:** - -**Authentication:** -- Create service account in Yandex Cloud Console -- Generate authorized key (JSON) -- Set key file path or use environment variables - -**Provider Configuration Pattern:** -```hcl -terraform { - required_providers { - yandex = { - source = "yandex-cloud/yandex" - } - } -} - -provider "yandex" { - # Configuration here (zone, folder_id, etc.) -} -``` - -**Key Resources:** -- `yandex_compute_instance` - Virtual machine -- `yandex_vpc_network` - Virtual private cloud -- `yandex_vpc_subnet` - Subnet within VPC -- `yandex_vpc_security_group` - Firewall rules - -**Free Tier Instance:** -- Platform: standard-v2 -- Cores: 2 (core_fraction = 20%) -- Memory: 1 GB -- Boot disk: 10 GB HDD - -**SSH Access:** -- Add SSH public key to `metadata` -- Use `ssh-keys` metadata field -- Connect: `ssh @` - -**Resources:** -- [Yandex Cloud Terraform Provider](https://registry.terraform.io/providers/yandex-cloud/yandex/latest/docs) -- [Getting Started Guide](https://cloud.yandex.com/en/docs/tutorials/infrastructure-management/terraform-quickstart) -- [Compute Instance Example](https://registry.terraform.io/providers/yandex-cloud/yandex/latest/docs/resources/compute_instance) - -
- -
-☁️ AWS Terraform Guide - -**AWS Setup:** - -**Authentication:** -- Create IAM user with EC2 permissions -- Generate access key ID and secret access key -- Configure AWS CLI or use environment variables -- Never hardcode credentials - -**Provider Configuration Pattern:** -```hcl -terraform { - required_providers { - aws = { - source = "hashicorp/aws" - } - } -} - -provider "aws" { - region = var.region # e.g., "us-east-1" -} -``` - -**Key Resources:** -- `aws_instance` - EC2 instance -- `aws_vpc` - Virtual Private Cloud -- `aws_subnet` - Subnet within VPC -- `aws_security_group` - Firewall rules -- `aws_key_pair` - SSH key - -**Free Tier Instance:** -- Instance type: t2.micro -- AMI: Amazon Linux 2 or Ubuntu (find with data source) -- 750 hours/month free for 12 months -- 30 GB storage included - -**Data Source for AMI:** -Use `aws_ami` data source to find latest Ubuntu image dynamically - -**Resources:** -- [AWS Provider Documentation](https://registry.terraform.io/providers/hashicorp/aws/latest/docs) -- [EC2 Instance Resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/instance) -- [AWS Free Tier](https://aws.amazon.com/free/) - -
- -
-☁️ GCP Terraform Guide - -**GCP Setup:** - -**Authentication:** -- Create service account in Google Cloud Console -- Download JSON key file -- Set `GOOGLE_APPLICATION_CREDENTIALS` environment variable -- Enable Compute Engine API - -**Provider Configuration Pattern:** -```hcl -terraform { - required_providers { - google = { - source = "hashicorp/google" - } - } -} - -provider "google" { - project = var.project_id - region = var.region -} -``` - -**Key Resources:** -- `google_compute_instance` - VM instance -- `google_compute_network` - VPC network -- `google_compute_subnetwork` - Subnet -- `google_compute_firewall` - Firewall rules - -**Free Tier Instance:** -- Machine type: e2-micro -- Zone: us-central1-a (or other free tier zone) -- Always free (within limits) -- Boot disk: 30 GB standard persistent disk - -**Resources:** -- [Google Provider Documentation](https://registry.terraform.io/providers/hashicorp/google/latest/docs) -- [Compute Instance Resource](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/compute_instance) -- [GCP Free Tier](https://cloud.google.com/free) - -
- -
-☁️ Other Cloud Providers - -**Azure:** -- Provider: `azurerm` -- Resource: `azurerm_linux_virtual_machine` -- Free tier: B1s instance -- [Azure Provider Docs](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) - -**VK Cloud:** -- Based on OpenStack -- Provider: OpenStack provider -- [VK Cloud Documentation](https://mcs.mail.ru/help/) - -**DigitalOcean:** -- Provider: `digitalocean` -- Resource: `digitalocean_droplet` -- Simple and beginner-friendly -- [DigitalOcean Provider Docs](https://registry.terraform.io/providers/digitalocean/digitalocean/latest/docs) - -**Questions to Explore:** -- What's the smallest instance size for your provider? -- How do you find the right OS image ID? -- What authentication method does your provider use? -- How do you add SSH keys to instances? - -
- -
-🔒 Security Best Practices - -**Credentials Management:** - -**❌ NEVER DO THIS:** -```hcl -provider "aws" { - access_key = "AKIAIOSFODNN7EXAMPLE" # NEVER! - secret_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" # NEVER! -} -``` - -**✅ DO THIS INSTEAD:** - -**Option 1: Environment Variables** -```bash -export AWS_ACCESS_KEY_ID="your-key" -export AWS_SECRET_ACCESS_KEY="your-secret" -# Provider will auto-detect -``` - -**Option 2: Credentials File** -```bash -# ~/.aws/credentials (for AWS) -[default] -aws_access_key_id = your-key -aws_secret_access_key = your-secret -``` - -**Option 3: terraform.tfvars (gitignored)** -```hcl -# terraform.tfvars (add to .gitignore!) -access_key = "your-key" -secret_key = "your-secret" -``` - -**Files to Add to .gitignore:** -``` -# Terraform -*.tfstate -*.tfstate.* -.terraform/ -terraform.tfvars -*.tfvars -.terraform.lock.hcl - -# Cloud credentials -*.pem -*.key -*.json # Service account keys -credentials -``` - -**SSH Key Management:** -- Generate SSH key pair locally -- Add public key to cloud provider -- Keep private key secure (never commit) -- Use `chmod 600` on private key file - -**Security Group Rules:** -- Restrict SSH to your IP only (not 0.0.0.0/0) -- Only open ports you need -- Document why each port is open - -
- -
-📁 Terraform Project Structure - -**Recommended Structure:** - -``` -terraform/ -├── .gitignore # Ignore state, credentials -├── main.tf # Main resources -├── variables.tf # Input variables -├── outputs.tf # Output values -├── terraform.tfvars # Variable values (gitignored!) -└── README.md # Setup instructions -``` - -**What Goes in Each File:** - -**main.tf:** -- Provider configuration -- Resource definitions -- Data sources - -**variables.tf:** -- Variable declarations -- Descriptions -- Default values (non-sensitive only) - -**outputs.tf:** -- Important values to display -- VM IP addresses -- Connection strings - -**terraform.tfvars:** -- Actual variable values -- Secrets and credentials -- **MUST be in .gitignore** - -**Alternative: Single File** -For small projects, you can put everything in `main.tf`, but multi-file is more maintainable. - -
- -**What to Document:** -- Cloud provider chosen and why -- Terraform version used -- Resources created (VM size, region, etc.) -- Public IP address of created VM -- SSH connection command -- Terminal output from `terraform plan` and `terraform apply` -- Proof of SSH access to VM - ---- - -### Task 2 — Pulumi VM Creation (4 pts) - -**Objective:** Destroy the Terraform VM and recreate the same infrastructure using Pulumi. - -**Requirements:** - -1. **Cleanup Terraform Infrastructure** - - Run `terraform destroy` to delete all resources - - Verify all resources are deleted in cloud console - - Document the cleanup process - -2. **Setup Pulumi** - - Install Pulumi CLI - - Choose a programming language (Python recommended, or TypeScript, Go, C#, Java) - - Initialize a new Pulumi project - - Configure cloud provider - -3. **Recreate Same Infrastructure** - - Create a `pulumi/` directory with equivalent resources: - - **Same Resources as Task 1:** - - VM/Compute Instance (same size) - - Network/VPC - - Security Group/Firewall (same rules) - - Public IP Address - - **Goal:** Functionally identical infrastructure, different tool - -4. **Apply Infrastructure** - - Run `pulumi preview` to see planned changes - - Apply infrastructure with `pulumi up` - - Verify VM is accessible via SSH - - Document the public IP - -5. **Compare Experience** - - What was easier/harder than Terraform? - - How does the code differ? - - Which approach do you prefer and why? - -
-💡 Pulumi Fundamentals - -**What is Pulumi?** - -Pulumi is an imperative IaC tool that lets you write infrastructure using real programming languages (Python, TypeScript, Go, etc.). - -**Key Differences from Terraform:** - -| Aspect | Terraform | Pulumi | -|--------|-----------|--------| -| **Language** | HCL (declarative) | Python, JS, Go, etc. (imperative) | -| **State** | Local or remote state file | Pulumi Cloud (free tier) or self-hosted | -| **Logic** | Limited (count, for_each) | Full programming language | -| **Testing** | External tools | Native unit tests | -| **Secrets** | Plain in state | Encrypted by default | - -**Key Concepts:** - -**Resources:** -- Similar to Terraform, but defined in code -- Example (Python): `vm = compute.Instance("my-vm", ...)` - -**Stacks:** -- Like Terraform workspaces -- Separate environments (dev, staging, prod) -- Each has its own config and state - -**Outputs:** -- Return values from your program -- Example: `pulumi.export("ip", vm.public_ip)` - -**Config:** -- Per-stack configuration -- Set with: `pulumi config set key value` -- Access in code: `config.get("key")` - -**Typical Workflow:** -```bash -pulumi new