diff --git a/.coverage b/.coverage new file mode 100644 index 0000000000..35c588c955 Binary files /dev/null and b/.coverage differ diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..751eb095a5 --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,99 @@ +name: Python CI + +on: + push: + paths: + - 'app_python/**' + pull_request: + paths: + - 'app_python/**' + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python 3.11 + uses: actions/setup-python@v5 + with: + python-version: 3.11 + cache: "pip" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r app_python/requirements.txt + pip install -r app_python/requirements-dev.txt + + - name: Run linter + run: ruff check . + + - name: Run pytest tests + run: | + pytest -v + + - name: Install Snyk CLI + run: npm install -g snyk + + - name: Run Snyk Test + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + run: snyk test app_python --severity-threshold=high + + deploy: + runs-on: ubuntu-latest + needs: test + if: github.ref == 'refs/heads/main' + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Set up Python 3.12 + uses: actions/setup-python@v5 + with: + python-version: 3.12 + + - name: Install Ansible + run: pip install ansible + + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v2 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }} + aws-region: us-east-1 + + - name: Set up SSH key + run: | + echo "${{ secrets.SSH_PRIVATE_KEY }}" > key.pem + chmod 600 key.pem + + - name: Prepare Password + run: | + echo "${{ secrets.ANSIBLE_PASSWORD }}" > pass.txt + chmod 600 pass.txt + + - name: Create temporary inventory + env: + SERVER_IP: ${{ secrets.SERVER_IP }} + run: | + echo "[webservers]" > inventory.ini + echo "ubuntu ansible_host=$SERVER_IP ansible_user=ubuntu ansible_ssh_private_key_file=key.pem" >> inventory.ini + cat inventory.ini + + - name: Ansible Run + run: | + ansible-playbook -i inventory.ini ./ansible/playbooks/deploy.yml \ + --extra-vars "@ansible/group_vars/all.yml" \ + --vault-password-file="pass.txt" + + - name: Remove Password + run: | + rm pass.txt + rm key.pem + + diff --git a/.gitignore b/.gitignore index 30d74d2584..24fa533aa7 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,14 @@ -test \ No newline at end of file +# Python +__pycache__/ +*.py[cod] +venv/ +*.log +app_python/venv/ +app_python/__pycache__/ + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store \ No newline at end of file diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..95bfb1b333 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,11 @@ +[defaults] +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +remote_user = maior +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 new file mode 100644 index 0000000000..bb083d95d9 --- /dev/null +++ b/ansible/docs/LAB05.md @@ -0,0 +1,200 @@ +# Lab 5 — Ansible Fundamentals + +## 1. Architecture Overview + +* **Ansible version:** 2.16.14 +* **Target VM OS:** Ubuntu 24.04 LTS +* **Role structure:** + +``` +ansible/ +├── inventory/ +│ └── hosts.ini # Static inventory +├── roles/ +│ ├── common/ # Common system tasks +│ │ ├── tasks/ +│ │ │ └── main.yml +│ │ └── defaults/ +│ │ └── main.yml +│ ├── docker/ # Docker installation +│ │ ├── tasks/ +│ │ │ └── main.yml +│ │ ├── handlers/ +│ │ │ └── main.yml +│ │ └── defaults/ +│ │ └── main.yml +│ └── app_deploy/ # Application deployment +│ ├── tasks/ +│ │ └── main.yml +│ ├── handlers/ +│ │ └── main.yml +│ └── defaults/ +│ └── main.yml +├── playbooks/ +│ ├── site.yml # Main playbook +│ ├── provision.yml # System provisioning +│ └── deploy.yml # App deployment +├── group_vars/ +│ └── all.yml # Encrypted variables (Vault) +├── ansible.cfg # Ansible configuration +└── docs/ + └── LAB05.md # Your documentation +``` + +**Why roles?** +Roles allow modular, reusable, and maintainable code. Each role encapsulates tasks, defaults, handlers, and variables, so playbooks remain clean. + +# Command to run: +```bash +docker run -it --rm -v ${PWD}\app_python\ansible:/ansible -v C:\Users\maior\.ssh\vm_machine_ubuntu:/root/.ssh/vm_machine_ubuntu -w /ansible willhallonline/ansible:2.16-debian-bookworm-slim ` bash +``` +--- + +## 2. Roles Documentation + +### **2.1 Common Role** + +* **Purpose:** Basic system provisioning (apt updates, packages, timezone) +* **Defaults:** `common_packages: [python3-pip, curl, git, vim, htop]` +* **Handlers:** None +* **Dependencies:** None + +### **2.2 Docker Role** + +* **Purpose:** Install and configure Docker engine +* **Defaults:** Docker version, user to add to `docker` group +* **Handlers:** `restart docker` — restarts Docker service if needed +* **Dependencies:** `common` role + +### **2.3 App Deploy Role** + +* **Purpose:** Deploy containerized Python app from previous labs +* **Defaults:** + * `app_name: devops-info-service` + * `docker_image_tag: latest` + * `app_port: 5000` + * `app_container_name: devops-info-service` + * link to image: https://hub.docker.com/r/daniil20xx/devops-info-service +* **Handlers:** `restart app container` — restarts app container if task triggers +* **Dependencies:** `docker` role + +--- + +## 3. Idempotency Demonstration + +### **3.1 Provisioning (common + docker)** + +**First run:** + +![playbook-1](/ansible/docs/screenshots/playbook-1.png) + +* Tasks with **error** and **changes** + +**Second run:** + +![playbook-2](/ansible/docs/screenshots/playbook-2.png) + + +* Tasks with **changes** + +**Third run:** + +![playbook-3](/ansible/docs/screenshots/playbook-3.png) + +* All tasks `ok` (green), `changed=0` +* Idempotency confirmed + +### **3.2 Deployment (app_deploy)** + +**First run:** + +![docker-playbook-1](/ansible/docs/screenshots/docker-playbook-1.png) + +* Docker container pulled and started +* `changed=3` (container creation, old container removal, docker login) +* `ignored=1`(container was not created yet) + +**Second run:** + +![docker-playbook-2](/ansible/docs/screenshots/docker-playhook-2.png) + +* Many tasks `ok`, `changed=3` + +**Third run:** + +![docker-playbook-3](/ansible/docs/screenshots/docker-playhook-3.png) + +* Many tasks `ok`, `changed=3` + +**Analysis:** + +##### changed=3 is repeated why: +- Stop existing container - the module checks if the container is running; even if the container is already running, Ansible marks the task as changed when bringing it to the desired state (e.g. restart or pull latest image). +- Remove old container - the old container is removed (if it is updated or recreated). +- Run container - a new container is created based on the latest image (if tag latest, Ansible pulls the latest image even when restarting). + +--- + +## 4. Ansible Vault Usage + +* Vault file: `group_vars/all.yml` + +```yaml +dockerhub_username: daniil20xx +dockerhub_password: [HIDDEN] +app_name: devops-info-service +docker_image: "{{ dockerhub_username }}/{{ app_name }}" +docker_image_tag: latest +app_port: 5000 +app_container_name: "{{ app_name }}" +``` + +* Vault password not committed to repo +* Deployed with: + +```bash +ansible-playbook playbooks/deploy.yml --ask-vault-pass +``` + +* **Purpose:** Keep Docker Hub credentials secure +* **Best practice:** `no_log: true` prevents secrets from appearing in logs + +--- + +## 5. Deployment Verification + +**Check containers:** + +![docker-check](/ansible/docs/screenshots/docker-check.png) + +**Health check:** + +![health-check](/ansible/docs/screenshots/working_url.png) + +--- + +## 6. Key Decisions + +* **Why use roles instead of plain playbooks?** + Roles separate concerns, making playbooks modular, reusable, and easier to maintain. + +* **How do roles improve reusability?** + Each role encapsulates a repeatable task set, which can be reused in multiple projects. + +* **What makes a task idempotent?** + Using stateful modules ensures tasks only make changes when necessary. + +* **How do handlers improve efficiency?** + Handlers execute only when notified, avoiding unnecessary service restarts. + +* **Why is Ansible Vault necessary?** + Vault securely stores credentials and sensitive variables, preventing secrets from leaking in version control. + +--- + +## 7. Challenges + +* Run `ansible` on Docker and move nessasary keys to it +* Connect to vm using ansible and ssh keys + +--- diff --git a/ansible/docs/LAB06.md b/ansible/docs/LAB06.md new file mode 100644 index 0000000000..81459de8a4 --- /dev/null +++ b/ansible/docs/LAB06.md @@ -0,0 +1,245 @@ +# Lab 6: Advanced Ansible & CI/CD + +**Name:** Daniil Mayorov +**Date:** 2005-08-21 + +## Overview + +In this lab the infrastructure automation from the previous lab was extended using advanced features of Ansible. + +The following improvements were implemented: + +* task grouping with blocks +* selective execution with tags +* application deployment using Docker Compose +* role dependencies +* wipe logic for safe application removal +* automated deployment using GitHub Actions + +The application is deployed as a container from Docker Hub and managed through Ansible roles. + +--- + +# Task 1: Blocks & Tags + +Blocks were used to group related tasks and apply common directives such as `become`, `when`, and `tags`. + +Example pattern used in roles: + +![Example pattern used in roles](/ansible/docs/screenshots/lab6/example_pattern_used_in_roles.png) + +Tags allow selective execution of tasks. + +Example: + +![empl tags execution](/ansible/docs/screenshots/lab6/tag_docker.png) + + +Tags used in the project: + +| Tag | Purpose | +| -------------- | ------------------------- | +| common | entire common role | +| packages | package installation | +| users | user creation | +| docker | docker role | +| docker_install | docker installation | +| docker_config | docker configuration | +| app_wipe | web app wipe | + +--- + +# Task 2: Docker Compose Deployment + +The application deployment was migrated from manual container execution to Docker Compose. + +Advantages: + +* declarative configuration +* easier updates +* reproducible deployments +* simpler multi-container support + +The compose configuration is generated using a Jinja2 template. + +Example template: + +![Jinja2 template](/ansible/docs/screenshots/lab6/jinja2template.png) + +Variables used: + +* `app_name` +* `docker_image` +* `docker_tag` +* `app_port` +* `app_internal_port` +* `compose_project_dir` + +--- + +# Task 3: Wipe Logic + +Wipe logic was implemented to safely remove deployed applications. + +The wipe mechanism uses **double protection**: + +1. variable `app_wipe` +2. tag `app_wipe` + +Default configuration: + +```yaml +app_wipe: false +``` + +Wipe tasks perform: + +* stopping containers +* removing docker-compose configuration +* removing application directory + +Example wipe command: + +![app wipe](/ansible/docs/screenshots/lab6/app_wipe.png) + +Example clean reinstall: + +![wipe2](/ansible/docs/screenshots/lab6/wipe2.png) + +Execution order: + +``` +wipe tasks → deployment tasks +``` + +This allows clean reinstallation of the application. + +--- +# CI/CD +All pipeline ready, but I have troubles with connection to server via Ansible. + +# Testing Results + +The deployment process was tested with the following scenarios: + +**Normal deployment** + +![normal dep](/ansible/docs/screenshots/lab6/playbook-1.png) + + +**Idempotency test** + +![idem1](/ansible/docs/screenshots/lab6/playbook-2.png) +![idem2](/ansible/docs/screenshots/lab6/playbook-2.png) +![idem3](/ansible/docs/screenshots/lab6/playbook-3.png) +![idem4](/ansible/docs/screenshots/lab6/playbook-4.png) + +**Selective execution** + +![sel-doc](/ansible/docs/screenshots/lab6/tag_docker.png) + +Only Docker tasks were executed. + +--- + +# Challenges & Solutions + +**Docker module errors** + +The Docker modules required additional Python dependencies on the target host. +This was solved by installing `python3-docker`. + +**Template variable errors** + +Incorrect variable names in the compose template caused deployment failures. +This was fixed by aligning variable names with `group_vars`. + +**CI/CD authentication** + +SSH authentication required storing private keys in GitHub Secrets. + +--- + +# Research Answers + +### What happens if rescue block also fails? + +If the rescue block fails, the task is marked as failed and Ansible stops execution of the play unless errors are ignored. + +--- + +### Can you have nested blocks? + +Yes. Ansible allows nested blocks, but it is recommended to keep them shallow to maintain readability. + +--- + +### How do tags inherit inside blocks? + +Tags defined on a block automatically apply to all tasks inside the block. + +--- + +### Difference between `restart: always` and `restart: unless-stopped`? + +* `always` - container always restarts, even after manual stop +* `unless-stopped` - container restarts automatically unless it was manually stopped + +--- + +### Why use both variable and tag for wipe logic? + +This provides a **double safety mechanism**: + +* tag ensures wipe tasks are not executed during normal runs +* variable ensures wipe does not run accidentally if the tag is used + +Both conditions must be satisfied. + +--- + +### Difference between `never` tag and this approach? + +`never` prevents tasks from running unless explicitly called. +The variable + tag approach provides more flexible control and safer execution. + +--- + +### Why must wipe logic run before deployment? + +It allows **clean reinstallation**: + +``` +wipe old installation → deploy new version +``` + +Without this order the old containers might conflict with new deployment. + +--- + +### Security implications of storing SSH keys in GitHub Secrets? + +Secrets are encrypted and not visible in logs, but if a repository is compromised the attacker may gain access to the stored credentials. + +--- + +### How to implement staging -> production deployment? + +Two environments can be created with separate inventories: + +``` +inventory/staging +inventory/production +``` + +CI/CD first deploys to staging, runs tests, then deploys to production. + +--- + +### How would you add rollback support? + +Rollback can be implemented by: + +* storing previous Docker image tags +* redeploying a previous version +* keeping versioned releases in Docker Hub. diff --git a/ansible/docs/screenshots/lab5/docker-check.png b/ansible/docs/screenshots/lab5/docker-check.png new file mode 100644 index 0000000000..d99060fcd1 Binary files /dev/null and b/ansible/docs/screenshots/lab5/docker-check.png differ diff --git a/ansible/docs/screenshots/lab5/docker-playbook-1.png b/ansible/docs/screenshots/lab5/docker-playbook-1.png new file mode 100644 index 0000000000..61fc9f89f5 Binary files /dev/null and b/ansible/docs/screenshots/lab5/docker-playbook-1.png differ diff --git a/ansible/docs/screenshots/lab5/docker-playhook-2.png b/ansible/docs/screenshots/lab5/docker-playhook-2.png new file mode 100644 index 0000000000..e73db6d7cd Binary files /dev/null and b/ansible/docs/screenshots/lab5/docker-playhook-2.png differ diff --git a/ansible/docs/screenshots/lab5/docker-playhook-3.png b/ansible/docs/screenshots/lab5/docker-playhook-3.png new file mode 100644 index 0000000000..a0a496793e Binary files /dev/null and b/ansible/docs/screenshots/lab5/docker-playhook-3.png differ diff --git a/ansible/docs/screenshots/lab5/playbook-1.png b/ansible/docs/screenshots/lab5/playbook-1.png new file mode 100644 index 0000000000..ea0a82d6d0 Binary files /dev/null and b/ansible/docs/screenshots/lab5/playbook-1.png differ diff --git a/ansible/docs/screenshots/lab5/playbook-2.png b/ansible/docs/screenshots/lab5/playbook-2.png new file mode 100644 index 0000000000..5ac6143aee Binary files /dev/null and b/ansible/docs/screenshots/lab5/playbook-2.png differ diff --git a/ansible/docs/screenshots/lab5/playbook-3.png b/ansible/docs/screenshots/lab5/playbook-3.png new file mode 100644 index 0000000000..d591076c69 Binary files /dev/null and b/ansible/docs/screenshots/lab5/playbook-3.png differ diff --git a/ansible/docs/screenshots/lab5/working_url.png b/ansible/docs/screenshots/lab5/working_url.png new file mode 100644 index 0000000000..988625427d Binary files /dev/null and b/ansible/docs/screenshots/lab5/working_url.png differ diff --git a/ansible/docs/screenshots/lab6/app_wipe.png b/ansible/docs/screenshots/lab6/app_wipe.png new file mode 100644 index 0000000000..90bb6703d0 Binary files /dev/null and b/ansible/docs/screenshots/lab6/app_wipe.png differ diff --git a/ansible/docs/screenshots/lab6/example_pattern_used_in_roles.png b/ansible/docs/screenshots/lab6/example_pattern_used_in_roles.png new file mode 100644 index 0000000000..d224b493aa Binary files /dev/null and b/ansible/docs/screenshots/lab6/example_pattern_used_in_roles.png differ diff --git a/ansible/docs/screenshots/lab6/jinja2template.png b/ansible/docs/screenshots/lab6/jinja2template.png new file mode 100644 index 0000000000..128d4f883d Binary files /dev/null and b/ansible/docs/screenshots/lab6/jinja2template.png differ diff --git a/ansible/docs/screenshots/lab6/playbook-1.png b/ansible/docs/screenshots/lab6/playbook-1.png new file mode 100644 index 0000000000..233fbc6e2a Binary files /dev/null and b/ansible/docs/screenshots/lab6/playbook-1.png differ diff --git a/ansible/docs/screenshots/lab6/playbook-2.png b/ansible/docs/screenshots/lab6/playbook-2.png new file mode 100644 index 0000000000..f40a64794a Binary files /dev/null and b/ansible/docs/screenshots/lab6/playbook-2.png differ diff --git a/ansible/docs/screenshots/lab6/playbook-3.png b/ansible/docs/screenshots/lab6/playbook-3.png new file mode 100644 index 0000000000..74d0bf16b3 Binary files /dev/null and b/ansible/docs/screenshots/lab6/playbook-3.png differ diff --git a/ansible/docs/screenshots/lab6/playbook-4.png b/ansible/docs/screenshots/lab6/playbook-4.png new file mode 100644 index 0000000000..663a53d7d6 Binary files /dev/null and b/ansible/docs/screenshots/lab6/playbook-4.png differ diff --git a/ansible/docs/screenshots/lab6/tag_docker.png b/ansible/docs/screenshots/lab6/tag_docker.png new file mode 100644 index 0000000000..b4c4cf2ffc Binary files /dev/null and b/ansible/docs/screenshots/lab6/tag_docker.png differ diff --git a/ansible/docs/screenshots/lab6/wipe2.png b/ansible/docs/screenshots/lab6/wipe2.png new file mode 100644 index 0000000000..63c0577e41 Binary files /dev/null and b/ansible/docs/screenshots/lab6/wipe2.png differ diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml new file mode 100644 index 0000000000..7b94cbf14a --- /dev/null +++ b/ansible/group_vars/all.yml @@ -0,0 +1,23 @@ +$ANSIBLE_VAULT;1.1;AES256 +65383461346138623466313663313464353664383365336530356134633035343432346331643636 +3031383035336334333337323963313863623734393765660a386633353738326662346438383166 +37303065623130376566383431326663656437386338383033383362623762313135653739326563 +3863613436356566630a383061393434363361323438643430313434356635353332336165393561 +32636431633962396163396438663665643864333936346635616533636339343532363333656165 +66626536373063383166333639663732306466326134636561313834613265633239653763303833 +34313339303162363435383265383335393766333238333839356234316333366330303161376634 +35363335613634343736626436393837633235623938316334346637306336633830633566393536 +31613462643861636633616535323263646534653533363563636665373665653830313134346131 +37343536663961316165393331363865626235633930663962316163663331643063633438633661 +65323633663935653162636434613330663362383766366663353166363462636563333665663935 +61613431386566636432616561643566343138643965343539306664393632326337343133376265 +33636135656365363763376439626139363964636339386634613132303535323965376363313163 +30356563623962616138326530643137396566623861336236303437363864633936373864613431 +65373134396337323433303336313439646262353633313761363463313535313536393331306333 +61363731653034646565396531303436316163653762306664646532616230383632393762663362 +65353639653163383063303764643733383333313337633136396461336264623166343339663731 +62333436663131336665343035366432343537663865376635336638623138373265356565363931 +34373830333634633839366632636465643631653834643837313734623734323239383536376339 +32313436633731623065396264343231616362353334663833393963643733653834353032613534 +64643231646163313731356434373361623364653762303137346565623437313930666431303865 +3337656466616635613861636265383632306332356637336362 diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..ded1608cbd --- /dev/null +++ b/ansible/inventory/hosts.ini @@ -0,0 +1,2 @@ +[webservers] +ubuntu ansible_host=192.168.1.178 ansible_user=maior ansible_ssh_private_key_file=/root/.ssh/vm_machine_ubuntu diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml new file mode 100644 index 0000000000..57ce4901dd --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,8 @@ +--- +- name: Deploy application + hosts: webservers + become: true + gather_facts: true + + roles: + - role: web_app \ No newline at end of file diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml new file mode 100644 index 0000000000..17d437513f --- /dev/null +++ b/ansible/playbooks/provision.yml @@ -0,0 +1,8 @@ +--- +- name: Provision web servers + hosts: webservers + become: yes + + roles: + - common + - docker \ No newline at end of file diff --git a/ansible/playbooks/roles/common/defaults/main.yml b/ansible/playbooks/roles/common/defaults/main.yml new file mode 100644 index 0000000000..2a613876dd --- /dev/null +++ b/ansible/playbooks/roles/common/defaults/main.yml @@ -0,0 +1,8 @@ +--- +common_packages: + - python3-pip + - curl + - git + - vim + - htop + - unzip \ No newline at end of file diff --git a/ansible/playbooks/roles/common/tasks/main.yml b/ansible/playbooks/roles/common/tasks/main.yml new file mode 100644 index 0000000000..4880e135f3 --- /dev/null +++ b/ansible/playbooks/roles/common/tasks/main.yml @@ -0,0 +1,40 @@ +--- +- name: Package installation + + block: + - name: Update apt cache + ansible.builtin.apt: + update_cache: yes + + - name: Install common packages + ansible.builtin.apt: + name: "{{ common_packages }}" + state: present + + rescue: + - name: Fix apt cache + ansible.builtin.command: apt-get update --fix-missing + + always: + - name: Log package installation completion + ansible.builtin.file: + path: /tmp/common_packages_done + state: touch + + become: true + tags: + - packages + + +- name: User configuration + block: + - name: Ensure user exists + ansible.builtin.user: + name: "{{ common_user }}" + state: present + groups: sudo + append: yes + + become: true + tags: + - users \ No newline at end of file diff --git a/ansible/playbooks/roles/docker/defaults/main.yml b/ansible/playbooks/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..0d6b1f50c0 --- /dev/null +++ b/ansible/playbooks/roles/docker/defaults/main.yml @@ -0,0 +1,6 @@ +--- +docker_user: maior +docker_packages: + - docker-ce + - docker-ce-cli + - containerd.io \ No newline at end of file diff --git a/ansible/playbooks/roles/docker/handlers/main.yml b/ansible/playbooks/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..ad85b66150 --- /dev/null +++ b/ansible/playbooks/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: restart docker + service: + name: docker + state: restarted \ No newline at end of file diff --git a/ansible/playbooks/roles/docker/tasks/main.yml b/ansible/playbooks/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..d4ba5559b1 --- /dev/null +++ b/ansible/playbooks/roles/docker/tasks/main.yml @@ -0,0 +1,43 @@ +--- +- name: Docker installation + block: + + - name: Install Docker package + apt: + name: docker.io + state: present + update_cache: yes + + rescue: + + - name: Update apt cache if install failed + command: apt-get update + become: true + + always: + + - name: Ensure Docker service is started + service: + name: docker + state: started + enabled: true + + become: true + tags: + - docker + - docker_install + + +- name: Docker configuration + block: + + - name: Add ubuntu user to docker group + user: + name: ubuntu + groups: docker + append: yes + + become: true + tags: + - docker + - docker_config \ No newline at end of file diff --git a/ansible/playbooks/roles/web_app/defaults/main.yml b/ansible/playbooks/roles/web_app/defaults/main.yml new file mode 100644 index 0000000000..985feb7270 --- /dev/null +++ b/ansible/playbooks/roles/web_app/defaults/main.yml @@ -0,0 +1,2 @@ +--- +app_restart_policy: unless-stopped \ No newline at end of file diff --git a/ansible/playbooks/roles/web_app/handlers/main.yml b/ansible/playbooks/roles/web_app/handlers/main.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/ansible/playbooks/roles/web_app/meta/main.yml b/ansible/playbooks/roles/web_app/meta/main.yml new file mode 100644 index 0000000000..fc95875336 --- /dev/null +++ b/ansible/playbooks/roles/web_app/meta/main.yml @@ -0,0 +1,3 @@ +--- +dependencies: + - role: docker \ No newline at end of file diff --git a/ansible/playbooks/roles/web_app/tasks/main.yml b/ansible/playbooks/roles/web_app/tasks/main.yml new file mode 100644 index 0000000000..48c006c01f --- /dev/null +++ b/ansible/playbooks/roles/web_app/tasks/main.yml @@ -0,0 +1,27 @@ +--- +- name: Include wipe tasks + import_tasks: wipe.yml + +- name: Deploy web application + block: + + - name: Create application directory + file: + path: "{{ compose_project_dir }}" + state: directory + mode: '0755' + + - name: Render docker-compose file + template: + src: docker-compose.yml.j2 + dest: "{{ compose_project_dir }}/docker-compose.yml" + + - name: Start application via Docker Compose + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir }}" + state: present + + become: true + tags: + - web_app + - web_app_deploy \ No newline at end of file diff --git a/ansible/playbooks/roles/web_app/tasks/wipe.yml b/ansible/playbooks/roles/web_app/tasks/wipe.yml new file mode 100644 index 0000000000..4c85225707 --- /dev/null +++ b/ansible/playbooks/roles/web_app/tasks/wipe.yml @@ -0,0 +1,21 @@ +--- +- name: Wipe web application + block: + + - name: Stop and remove containers + community.docker.docker_compose_v2: + project_src: "{{ compose_project_dir }}" + state: absent + + - name: Remove application directory + file: + path: "{{ compose_project_dir }}" + state: absent + + - name: Log wipe action + debug: + msg: "Application {{ app_name }} wiped" + + when: app_wipe | bool + tags: + - app_wipe \ No newline at end of file diff --git a/ansible/playbooks/roles/web_app/templates/docker-compose.yml.j2 b/ansible/playbooks/roles/web_app/templates/docker-compose.yml.j2 new file mode 100644 index 0000000000..efb608d0aa --- /dev/null +++ b/ansible/playbooks/roles/web_app/templates/docker-compose.yml.j2 @@ -0,0 +1,17 @@ +services: + web: + image: {{ docker_image }}:{{ docker_tag }} + container_name: {{ app_name }} + restart: unless-stopped + + ports: + - "{{ app_port }}:{{ app_internal_port }}" + + environment: + APP_ENV: {{ app_env }} + + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:{{ app_internal_port }}"] + interval: 30s + timeout: 10s + retries: 5 \ No newline at end of file diff --git a/ansible/playbooks/site.yml b/ansible/playbooks/site.yml new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_go/Dockerfile b/app_go/Dockerfile new file mode 100644 index 0000000000..1035e07719 --- /dev/null +++ b/app_go/Dockerfile @@ -0,0 +1,23 @@ +FROM golang:1.25-alpine AS builder + +WORKDIR /app + +COPY go.mod ./ +RUN go mod download + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o app + +FROM alpine:3.19 + +WORKDIR /app + +RUN adduser -D appuser +USER appuser + +COPY --from=builder /app/app . + +EXPOSE 5000 + +CMD ["./app"] diff --git a/app_go/README.md b/app_go/README.md new file mode 100644 index 0000000000..8d6fcf25ca --- /dev/null +++ b/app_go/README.md @@ -0,0 +1,164 @@ +# Lab01 — DevOps Info Service + +## Overview + +**DevOps Info Service (Go version)** is a lightweight web application written in Go that provides detailed information about the service itself, the system it runs on, and its runtime environment. + +This implementation is part of the **bonus task** for Lab 01 and is intended to demonstrate the advantages of using a compiled language in DevOps workflows, especially for containerization and multi-stage Docker builds. + +**Features:** + +* `GET /` — returns service, system, runtime, and request information +* `GET /health` — simple health check endpoint +* Configurable via environment variables + +--- + +## Prerequisites + +* Go **1.24.5** + +--- + +## Installation + +1. Clone the repository: + +```bash +git clone https://github.com/Daniil20xx/DevOps-Core-Course.git +``` + +2. Navigate to the Go application directory: + +```bash +cd app_go +``` + +3. Initialize Go module (if not already initialized): + +```bash +go mod init devops-info-service +``` + +--- + +## Running the Application + +### Run directly + +```bash +cd app_go +go run main.go +``` + +By default, the service runs on: + +``` +http://0.0.0.0:5000 +``` + +### Run with custom configuration + +```bash +HOST=127.0.0.1 PORT=8080 go run main.go +``` + +--- + +## API Endpoints + +### `GET /` + +Returns detailed information about the service and the system. + +**Example:** + +```json +{ + "endpoints": [ + { + "description": "Service information", + "method": "GET", + "path": "/" + }, + { + "description": "Health check", + "method": "GET", + "path": "/health" + } + ], + "request": { + "client_ip": "::1", + "method": "GET", + "path": "/", + "user_agent": "Mozilla/5.0 (Windows NT; Windows NT 10.0; ru-RU) WindowsPowerShell/5.1.26100.7462" + }, + "runtime": { + "current_time": "2026-01-28T08:32:20Z", + "timezone": "UTC", + "uptime_human": "0 hours, 24 minutes", + "uptime_seconds": 1452 + }, + "service": { + "description": "DevOps course info service", + "framework": "net/http", + "name": "devops-info-service", + "version": "1.0.0" + }, + "system": { + "architecture": "amd64", + "cpu_count": 16, + "go_version": "go1.24.5", + "hostname": "Daniil", + "platform": "windows", + "platform_version": "go1.24.5" + } +} +``` + +--- + +### `GET /health` + +Returns service health status. + +**Example:** + +```bash +curl http://localhost:8080/health +``` + +**Response:** + +```json +{ + "status": "healthy", + "timestamp": "2026-01-28T08:33:26Z", + "uptime_seconds": 1518 +} +``` + +--- + +## Configuration + +The application can be configured using environment variables: + +| Environment Variable | Default | Description | +| -------------------- | --------- | ---------------------------------- | +| `HOST` | `0.0.0.0` | Host address to bind the server | +| `PORT` | `8080` | Port to run the application on | + +--- + +## Project Structure + +``` +app_go/ +├── main.go +├── go.mod +├── README.md +└── docs/ + ├── LAB01.md + └── screenshots/ +``` diff --git a/app_go/docs/GO.md b/app_go/docs/GO.md new file mode 100644 index 0000000000..222904676b --- /dev/null +++ b/app_go/docs/GO.md @@ -0,0 +1,60 @@ +# GO — Language Selection Justification + +## Overview + +For the bonus part of Lab 01, the DevOps Info Service was reimplemented using **Go**, a compiled programming language widely used in modern DevOps. +The goal of this implementation is to demonstrate the advantages of compiled languages in terms of performance, deployment, and containerization. + +--- + +## Why Go? + +Go was selected for the following reasons: + +### 1. Compiled Language + +Go compiles source code into a **single native binary**, which eliminates the need for a runtime interpreter (unlike Python). +This results in: + +* Faster application startup +* Lower runtime overhead +* Simpler deployment process + +--- + +### 2. Standard Library for Web Services + +Go provides a powerful and production-ready HTTP server through the standard `net/http` package. +This allows building web services without relying on external frameworks, reducing dependencies and potential security risks. + +--- + +### 3. Performance and Resource Efficiency + +Compared to interpreted languages, Go applications: + +* Use less memory +* Handle concurrent requests efficiently +* Scale well under load + +This makes Go a popular choice for infrastructure tools, monitoring systems, and backend services. + +--- + +## Comparison with Python Implementation + +| Aspect | Python (Flask) | Go | +| --------------------- | ----------------------- | ----------------------- | +| Language Type | Interpreted | Compiled | +| Startup Time | Slower | Faster | +| Deployment | Requires Python runtime | Single binary | +| Docker Image Size | Larger | Smaller | +| Performance | Good for small services | High | +| Dependency Management | External packages | Mostly standard library | + +--- + +## Conclusion + +Go was chosen for the bonus implementation because it provides a clean, efficient, and production-ready approach to building web services. +Using Go alongside Python in this lab demonstrates the trade-offs between interpreted and compiled languages and prepares the project for future DevOps tasks such as containerization, CI/CD pipelines, and Kubernetes deployments. diff --git a/app_go/docs/LAB01.md b/app_go/docs/LAB01.md new file mode 100644 index 0000000000..a9ff176781 --- /dev/null +++ b/app_go/docs/LAB01.md @@ -0,0 +1,143 @@ +# LAB01 — DevOps Info Service (Go Version) + +## 1. Framework / Language Selection + +**Chosen Language:** Go (Golang) 1.24.5 + +**Justification:** +Go is a compiled language suitable for building lightweight, high-performance web services. + +--- + +## 2. Best Practices Applied + +**1. Clean Code Structure** + +* Separation of concerns: utility functions, handlers, and main server logic +* `getUptime()` function calculates runtime +* Route handlers: `mainHandler` for `/`, `healthHandler` for `/health` +* Consistent logging of requests + +**2. Error Handling** + +* Returns default page `/` if error happens + +**3. Logging** + +* Uses Go’s standard `log` package +* Optional verbose logging via `DEBUG` environment variable + +**4. Environment Configuration** + +* `HOST`, `PORT`, `DEBUG` are configurable through environment variables + +**5. Dependency Management** + +* Uses Go modules (`go.mod`) to track dependencies + +--- + +## 3. API Documentation + +### `GET /` + +Returns service, system, runtime, and request information. + +**Example Request:** + +```bash +curl http://localhost:8080/ +``` + +**Sample Response:** + +```json +{ + "endpoints": [ + { + "description": "Service information", + "method": "GET", + "path": "/" + }, + { + "description": "Health check", + "method": "GET", + "path": "/health" + } + ], + "request": { + "client_ip": "::1", + "method": "GET", + "path": "/", + "user_agent": "Mozilla/5.0 (Windows NT; Windows NT 10.0; ru-RU) WindowsPowerShell/5.1.26100.7462" + }, + "runtime": { + "current_time": "2026-01-28T08:32:20Z", + "timezone": "UTC", + "uptime_human": "0 hours, 24 minutes", + "uptime_seconds": 1452 + }, + "service": { + "description": "DevOps course info service", + "framework": "net/http", + "name": "devops-info-service", + "version": "1.0.0" + }, + "system": { + "architecture": "amd64", + "cpu_count": 16, + "go_version": "go1.24.5", + "hostname": "Daniil", + "platform": "windows", + "platform_version": "go1.24.5" + } +} +``` + +--- + +### `GET /health` + +Returns health status. + +**Example Request:** + +```bash +curl http://localhost:8080/health +``` + +**Sample Response:** + +```json +{ + "status": "healthy", + "timestamp": "2026-01-28T08:33:26Z", + "uptime_seconds": 1518 +} +``` + +--- + +## 4. Testing Evidence + +* **Main Endpoint:** + ![Main endpoint](screenshots/01-main-endpoint.png) + +* **Health Check:** + ![Health endpoint](screenshots/02-health-check.png) + +* **Command-line Test Example:** + +```bash +curl http://localhost:8080/ +curl http://localhost:8080/health +``` + +--- + +## 6. Summary + +The Go implementation mirrors the Python (Flask) version of the DevOps Info Service: + +* Same endpoints and JSON structure +* Faster startup and compiled binary \ No newline at end of file diff --git a/app_go/docs/screenshots/01-main-endpoint.png b/app_go/docs/screenshots/01-main-endpoint.png new file mode 100644 index 0000000000..87aa89e304 Binary files /dev/null and b/app_go/docs/screenshots/01-main-endpoint.png differ diff --git a/app_go/docs/screenshots/02-health-check.png b/app_go/docs/screenshots/02-health-check.png new file mode 100644 index 0000000000..f41883ad78 Binary files /dev/null and b/app_go/docs/screenshots/02-health-check.png differ diff --git a/app_go/docs/screenshots/03-formatted-output.png b/app_go/docs/screenshots/03-formatted-output.png new file mode 100644 index 0000000000..fef60e9aa8 Binary files /dev/null and b/app_go/docs/screenshots/03-formatted-output.png differ diff --git a/app_go/go.mod b/app_go/go.mod new file mode 100644 index 0000000000..2281baaf71 --- /dev/null +++ b/app_go/go.mod @@ -0,0 +1,3 @@ +module app_go + +go 1.24.5 diff --git a/app_go/main.go b/app_go/main.go new file mode 100644 index 0000000000..69d4a5f3ed --- /dev/null +++ b/app_go/main.go @@ -0,0 +1,132 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net" + "net/http" + "os" + "runtime" + "time" +) + +var startTime = time.Now().UTC() + +func getEnv(key, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} + +func getUptime() (int64, string) { + seconds := int64(time.Since(startTime).Seconds()) + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + return seconds, + formatUptime(hours, minutes) +} + +func formatUptime(hours, minutes int64) string { + return fmt.Sprintf("%d hours, %d minutes", hours, minutes) +} + +func getHostname() string { + hostname, err := os.Hostname() + if err != nil { + return "unknown" + } + return hostname +} + +func getClientIP(r *http.Request) string { + ip, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return r.RemoteAddr + } + return ip +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + uptimeSeconds, _ := getUptime() + + response := map[string]interface{}{ + "status": "healthy", + "timestamp": time.Now().UTC().Format(time.RFC3339), + "uptime_seconds": uptimeSeconds, + } + + log.Printf("Health check from %s", getClientIP(r)) + writeJSON(w, http.StatusOK, response) +} + +func mainHandler(w http.ResponseWriter, r *http.Request) { + uptimeSeconds, uptimeHuman := getUptime() + + response := map[string]interface{}{ + "service": map[string]interface{}{ + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "net/http", + }, + "system": map[string]interface{}{ + "hostname": getHostname(), + "platform": runtime.GOOS, + "platform_version": runtime.Version(), + "architecture": runtime.GOARCH, + "cpu_count": runtime.NumCPU(), + "go_version": runtime.Version(), + }, + "runtime": map[string]interface{}{ + "uptime_seconds": uptimeSeconds, + "uptime_human": uptimeHuman, + "current_time": time.Now().UTC().Format(time.RFC3339), + "timezone": "UTC", + }, + "request": map[string]interface{}{ + "client_ip": getClientIP(r), + "user_agent": r.UserAgent(), + "method": r.Method, + "path": r.URL.Path, + }, + "endpoints": []map[string]string{ + { + "path": "/", + "method": "GET", + "description": "Service information", + }, + { + "path": "/health", + "method": "GET", + "description": "Health check", + }, + }, + } + + log.Printf("%s %s from %s", r.Method, r.URL.Path, getClientIP(r)) + writeJSON(w, http.StatusOK, response) +} + +func writeJSON(w http.ResponseWriter, status int, data interface{}) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + _ = json.NewEncoder(w).Encode(data) +} + +func main() { + host := getEnv("HOST", "0.0.0.0") + port := getEnv("PORT", "5000") + + log.Printf("Starting Go DevOps Info Service on %s:%s", host, port) + + http.HandleFunc("/", mainHandler) + http.HandleFunc("/health", healthHandler) + + err := http.ListenAndServe(host+":"+port, nil) + if err != nil { + log.Fatalf("Server failed to start: %v", err) + } +} diff --git a/app_go/main_test.go b/app_go/main_test.go new file mode 100644 index 0000000000..0528093881 --- /dev/null +++ b/app_go/main_test.go @@ -0,0 +1,83 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" +) + +func setupServer() *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/", mainHandler) + mux.HandleFunc("/health", healthHandler) + return mux +} + +func TestHealthEndpoint(t *testing.T) { + server := httptest.NewServer(setupServer()) + defer server.Close() + + resp, err := http.Get(server.URL + "/health") + if err != nil { + t.Fatalf("Failed to make request: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + var data map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&data) + if err != nil { + t.Fatalf("Invalid JSON response") + } + + if data["status"] != "healthy" { + t.Errorf("Expected status 'healthy'") + } + + if data["uptime_seconds"] == nil { + t.Errorf("Missing uptime_seconds") + } +} + +func TestMainEndpoint(t *testing.T) { + server := httptest.NewServer(setupServer()) + defer server.Close() + + req, _ := http.NewRequest(http.MethodGet, server.URL+"/", nil) + req.Header.Set("User-Agent", "test-agent") + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + t.Fatalf("Request failed: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + t.Errorf("Expected status 200, got %d", resp.StatusCode) + } + + var data map[string]interface{} + err = json.NewDecoder(resp.Body).Decode(&data) + if err != nil { + t.Fatalf("Invalid JSON") + } + + requiredBlocks := []string{ + "service", + "system", + "runtime", + "request", + "endpoints", + } + + for _, block := range requiredBlocks { + if data[block] == nil { + t.Errorf("Missing block: %s", block) + } + } +} diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..e96a348868 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,19 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd + +.git/ +.gitignore + +.venv/ +venv/ + +.env +.idea/ +.vscode/ + +docs/ +tests/ + +README.md diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..a0c33d54e7 Binary files /dev/null and b/app_python/.gitignore differ diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..64eabc356e --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,20 @@ +FROM python:3.12-slim + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +RUN useradd -m dockeruser + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN chown -R dockeruser:dockeruser /app +USER dockeruser + +EXPOSE 5000 + +CMD ["python", "app.py"] diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..efa95697ab Binary files /dev/null and b/app_python/README.md differ diff --git a/app_python/__init__.py b/app_python/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..f51e2c92f9 --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,110 @@ +import os +import socket +from flask import Flask, jsonify, request +import platform +import logging +from datetime import datetime, timezone + +HOST = os.getenv('HOST', '0.0.0.0') +PORT = int(os.getenv('PORT', 5000)) +DEBUG = os.getenv('DEBUG', 'False').lower() in ('true', '1', 't') + +app = Flask(__name__) + +logging.basicConfig( + level=logging.DEBUG if DEBUG else logging.INFO, + format="%(asctime)s | %(levelname)s | %(message)s" +) +logger = logging.getLogger(__name__) + +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" + } + +def get_response(): + uptime = get_uptime() + + response = { + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask" + }, + "system": { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.version(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count(), + "python_version": platform.python_version(), + }, + "runtime": { + "uptime_seconds": uptime["seconds"], + "uptime_human": uptime["human"], + "current_time": datetime.now(timezone.utc).isoformat(), + "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"} + ] + } + return response + +@app.route('/health') +def health(): + logger.info(f"Health check from {request.remote_addr}") + return jsonify({ + 'status': 'healthy', + 'timestamp': datetime.now(timezone.utc).isoformat(), + 'uptime_seconds': get_uptime()['seconds'] + }) + +@app.route("/", methods=["GET"]) +def index(): + logger.info(f"{request.method} {request.path} from {request.remote_addr}") + return jsonify(get_response()) + +@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": "Unexpected server error" + }), 500 + +@app.errorhandler(Exception) +def handle_exception(e): + logger.exception("Unexpected error") + return jsonify({ + "error": "Internal Server Error", + "message": str(e) + }), 500 + +if __name__ == "__main__": + logger.info("Starting application") + app.run(host=HOST, port=PORT, debug=DEBUG) + + + diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..8eb9cf49f3 Binary files /dev/null and b/app_python/docs/LAB01.md differ diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..c4f50650ec --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,130 @@ +# LAB02 — Docker Containerization + +## 1. Docker Best Practices Applied + +### Non-root user + +The container runs under a non-root user instead of the default root user. This significantly improves security because even if an attacker gains access to the container, they will not have full administrative privileges. + +In the Dockerfile, a dedicated user is created and activated using the `USER` directive. This follows Docker security best practices and reduces the potential impact of vulnerabilities. + +### Specific base image version + +The image is based on `python:3.12-slim`. Using a specific version instead of `latest` ensures build reproducibility and prevents unexpected breaking changes when the base image is updated. + +The `slim` variant was chosen because it provides a good balance between minimal size and compatibility with Python dependencies. + +### Layer caching optimization + +Dependencies are installed before copying the application source code. This allows Docker to reuse cached layers when only the application code changes, which significantly speeds up rebuilds during development. + +### .dockerignore usage + +A `.dockerignore` file is used to exclude unnecessary files such as virtual environments, Git metadata, cache files, and IDE configuration. This reduces the build context size, speeds up the build process, and helps keep the final image smaller and cleaner. + +--- + +## 2. Image Information & Decisions + +### Base image choice + +The base image used is `python:3.12-slim`. + +**Justification:** + +* Matches the Python version used during local development +* Smaller image size compared to full Python images +* Official image with regular security updates + +### Final image size + +The final image size is approximately **42.68 MB**, which is acceptable for a Python web application with Flask and demonstrates reasonable optimization. + +### Layer structure + +The image layers are structured as follows: + +1. Base Python image +2. System setup and non-root user creation +3. Dependency installation (`requirements.txt`) +4. Application source code + +This structure maximizes cache reuse and minimizes rebuild time. + +### Optimization choices + +* Used `python:slim` instead of a full image +* Excluded unnecessary files using `.dockerignore` +* Installed only required dependencies + +--- + +## 3. Build & Run Process + +### Build process + +The image was built locally using Docker. Below is the terminal output from the build process: + +![Build Stage](screenshots/lab02-docker-build.png) + +### Run process + +The container was started with port mapping so the service is accessible from the host: + +```bash +$ docker run -p 5000:5000 lab02-python:1.0.0 +``` +![Docker Run](screenshots/lab02-docker-run.png) + +### Endpoint testing + +The application endpoints were tested using browser: + +![Testing Docker](screenshots/lab02-docker-testing.png) + +### Docker Hub + +The image was pushed to Docker Hub and is publicly available: + +**Repository URL:** + +``` +https://hub.docker.com/r/daniil20xx/lab02-python +``` + +--- + +## 4. Technical Analysis + +### Dockerfile behavior + +The Dockerfile works by first preparing a secure and minimal runtime environment, then installing dependencies, and finally copying the application code. This ensures both security and efficiency. + +### Layer order importance + +If the application code were copied before installing dependencies, any code change would invalidate the cache and force dependency reinstallation, significantly slowing down rebuilds. + +### Security considerations + +* The container does not run as root +* Uses an official Python base image +* Minimal image size reduces attack surface + +### .dockerignore benefits + +By excluding unnecessary files from the build context, `.dockerignore` improves build speed, reduces image size, and prevents accidental inclusion of sensitive or irrelevant files. + +--- + +## 5. Challenges & Solutions + +### Issue: Port not accessible + +Initially, the application was not accessible from the host machine because the container port was not correctly mapped. + +**Solution:** +The issue was resolved by explicitly mapping the container port to the host port using the `-p` option in `docker run`. + +### Learning outcome + +Through this lab, I gained a deeper understanding of Dockerfile structure, image optimization, security best practices, and the full workflow of building, running, and publishing Docker images. diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..cc06200cd8 --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,61 @@ +# Lab 3 — Continuous Integration (CI/CD) + +## 1. Overview + +**Testing Framework:** `pytest` +**Why it was chosen:** +- Simple and straightforward syntax +- Integrates well with Flask via test client +- Supports plugins, including `pytest-cov` for code coverage +- Modern standard for Python projects + +**ЧWhat cover tests:** +- `GET /health` — status checks, JSON structure, uptime +- `GET /` — JSON structure, blocks: service, system, runtime, request, endpoints +- Error Handlers: + - 404 Not Found + - 500 Internal Server Error +- Edge cases: + - Different Methods (For example, POST on GET endpoint) +- Additional: + - checks User-Agent and IP in request in block + +**CI Workflow Trigger:** +- **push:** 'app_python/' +- **pull_request:** 'app_python/' + +**Versioning Strategy:** Calendar Versioning (CalVer) +- Version format: `YYYY.MM.DD` +- Docker image tags: `2026.02.11` and `latest` +- Why chosen: allows you to quickly and easily understand the build date, convenient for daily service updates + +--- + +## 2. Workflow Evidence + +**GitHub Actions:** + +- Workflow file: `.github/workflows/python-ci.yml` +- Status of steps: + - Linting (ruff) + - Unit tests (pytest) + - Docker build & push +- Workflow run (example): [![Python CI](https://github.com/Daniil20xx/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)](https://github.com/Daniil20xx/DevOps-Core-Course/actions/workflows/python-ci.yml) + +**Local tests:** + +![pytest debug](screenshots/lab03-pytest-passed.png) +![ruff debug](screenshots/lab03-ruff.png) +![pytest cov debug](screenshots/lab03-pytest-cov.png) + +3. Best Practices Implemented +- **Dependency caching**: speeds up pip dependency installation in CI +- **Fail fast**: CI stops if tests fail, Docker does not build +- **Path filters (for bonus)**: Python workflow only runs when app_python/ changes +- **Linting (ruff)**: automatic code quality check +- **Docker push only from main**: prevents accidental publication of unstable builds +- **Snyk** (security scanning): + - Integration into workflow + - Checks dependencies for known vulnerabilities + - No critical vulnerabilities found at this time + diff --git a/app_python/docs/LAB04.md b/app_python/docs/LAB04.md new file mode 100644 index 0000000000..f45c32a539 --- /dev/null +++ b/app_python/docs/LAB04.md @@ -0,0 +1,151 @@ +# Lab 4 — Terraform VM Creation (AWS) + +## 1. Cloud Provider & Infrastructure + +- **Cloud Provider:** AWS +- **Region:** us-east-1 +- **Availability Zone:** default +- **Instance Type:** t2.micro (free tier) +- **Operating System:** Ubuntu 22.04 LTS +- **Total Cost:** $0 (within free tier limits) +- **Resources Created:** + - EC2 Instance: `DevOpsLab4` + - Security Group: `devops-lab4-sg` + - Ingress: + - SSH 22 (from all IPs `0.0.0.0/0`) + - HTTP 80 (from all IPs) + - HTTPS 443 (from all IPs) + - Egress: + - All traffic allowed (0.0.0.0/0) + - Public IP attached to EC2 instance + +--- + +## 2. Terraform Implementation + +- **Terraform Version:** v1.9+ +- **Project Structure:** + +``` +terraform/ + ─ main.tf # Provider + resources + ─ outputs.tf # Outputs (public IP, instance ID) +``` + +- **Key Configuration Decisions:** + - Security group allows SSH only to my IP (for better security) + - Outputs configured to easily access Public IP + +- **Challenges Encountered:** + - Selecting correct AMI ID + - Selecting cloud for free trial use + +--- + +## 3. Terminal Output + +### AWS config +![aws config](./screenshots/lab04/aws-configuration.png) + +### Terraform Initialization +![ti](./screenshots/lab04/terraform-init.png) + +### Terraform Apply +![ta](./screenshots/lab04/terraform-apply.png) + +### Terraform Apply finish +![taf](./screenshots/lab04/terrafrom-apply-finish.png) + +### Connection via SSH +![cvs](./screenshots/lab04/connect_via_ssh.png) + +### Terraform Destroy +![td](./screenshots/lab04/terrafrom-destroy.png) + + +--- + +## 3. Pulumi Implementation + +### Pulumi Version and Language + +* **Pulumi CLI version:** v3.222.0 +* **Programming language:** Python +* **Stack name:** `dev` + +### Project Structure + +``` +pulumi/ +├── __main__.py # Main infrastructure code +├── requirements.txt # Python dependencies +├── Pulumi.yaml # Project metadata +``` + +### Resources Created + +* **EC2 Instance:** `DevOpsLab4` + + * AMI: `ami-0b6c6ebed2801a5cb` + * Instance type: `t2.micro` + * Root volume: 16 GB +* **Security Group:** `devops-lab4-sg` + + * Ingress rules: SSH (22), HTTP (80), HTTPS (443) + * Egress: all traffic allowed +* **Public IP:** automatically assigned and outputted +* **Public DNS:** automatically assigned and outputted + +### Key Configuration Decisions + +* Python was used for full programmatic control. +* SSH, HTTP, and HTTPS ports were opened for demonstration and future app deployment. +* `t2.micro` free-tier instance chosen to satisfy lab requirements. +* Output variables (`public_ip`, `public_dns`) for easy connection and verification. + +### Challenges Encountered + +* No challandes. After terraform, it more simpler. + +### Terminal Output + +#### Pulumi init +![pi](./screenshots/lab04/pulumi-init.png) + +#### Pulumi up +![pu](./screenshots/lab04/pulumi-up.png) + +#### Pulumi ssh +![pssh](./screenshots/lab04/pulumi-ssh.png) + + +### Comparison with Terraform + +* **Code:** Pulumi uses Python (imperative), Terraform uses HCL (declarative). +* **Ease of Use:** Pulumi allows loops, functions, and IDE autocomplete. +* **State Management:** Pulumi stores state locally or in Pulumi Service; Terraform uses local or remote state. +* **Outputs:** Pulumi outputs are accessible directly in Python. +* **Preference:** Pulumi is better for dynamic configurations and conditional logic; Terraform is faster for simple declarative VM setups. + +--- + + +## Bonus Task — Terraform CI/CD Integration + +Objective: Automatically validate and apply infrastructure changes using GitHub Actions. + +Workflow Overviews + +- Trigger: Runs on pull requests for preview (terraform plan) and on main branch for applying (terraform apply). + +- Steps for Terraform Validation: + * Checkout code. + * Set up Terraform + * Terraform Init + * Terraform Apply + +Applies the infrastructure automatically (terraform apply -auto-approve) using stored AWS credentials. + +![tfpipe](./screenshots/lab04/pipeline.png) +![afterpipe](./screenshots/lab04/after_pipe.png) + \ No newline at end of file diff --git a/app_python/docs/screenshots/lab01/01-main-endpoint.png b/app_python/docs/screenshots/lab01/01-main-endpoint.png new file mode 100644 index 0000000000..f1fc65b032 Binary files /dev/null and b/app_python/docs/screenshots/lab01/01-main-endpoint.png differ diff --git a/app_python/docs/screenshots/lab01/02-health-check.png b/app_python/docs/screenshots/lab01/02-health-check.png new file mode 100644 index 0000000000..ec2a50f2c9 Binary files /dev/null and b/app_python/docs/screenshots/lab01/02-health-check.png differ diff --git a/app_python/docs/screenshots/lab01/03-formatted-output.png b/app_python/docs/screenshots/lab01/03-formatted-output.png new file mode 100644 index 0000000000..92851d9afe Binary files /dev/null and b/app_python/docs/screenshots/lab01/03-formatted-output.png differ diff --git a/app_python/docs/screenshots/lab02/lab02-docker-build.png b/app_python/docs/screenshots/lab02/lab02-docker-build.png new file mode 100644 index 0000000000..78d189e0a3 Binary files /dev/null and b/app_python/docs/screenshots/lab02/lab02-docker-build.png differ diff --git a/app_python/docs/screenshots/lab02/lab02-docker-run.png b/app_python/docs/screenshots/lab02/lab02-docker-run.png new file mode 100644 index 0000000000..e200df43c0 Binary files /dev/null and b/app_python/docs/screenshots/lab02/lab02-docker-run.png differ diff --git a/app_python/docs/screenshots/lab02/lab02-docker-testing.png b/app_python/docs/screenshots/lab02/lab02-docker-testing.png new file mode 100644 index 0000000000..5ea7c52d72 Binary files /dev/null and b/app_python/docs/screenshots/lab02/lab02-docker-testing.png differ diff --git a/app_python/docs/screenshots/lab03/lab03-pytest-cov.png b/app_python/docs/screenshots/lab03/lab03-pytest-cov.png new file mode 100644 index 0000000000..514b94284d Binary files /dev/null and b/app_python/docs/screenshots/lab03/lab03-pytest-cov.png differ diff --git a/app_python/docs/screenshots/lab03/lab03-pytest.png b/app_python/docs/screenshots/lab03/lab03-pytest.png new file mode 100644 index 0000000000..df6a9ca5b3 Binary files /dev/null and b/app_python/docs/screenshots/lab03/lab03-pytest.png differ diff --git a/app_python/docs/screenshots/lab03/lab03-ruff.png b/app_python/docs/screenshots/lab03/lab03-ruff.png new file mode 100644 index 0000000000..c4c25018a6 Binary files /dev/null and b/app_python/docs/screenshots/lab03/lab03-ruff.png differ diff --git a/app_python/docs/screenshots/lab04/after_pipe.png b/app_python/docs/screenshots/lab04/after_pipe.png new file mode 100644 index 0000000000..197729bc72 Binary files /dev/null and b/app_python/docs/screenshots/lab04/after_pipe.png differ diff --git a/app_python/docs/screenshots/lab04/aws-configuration.png b/app_python/docs/screenshots/lab04/aws-configuration.png new file mode 100644 index 0000000000..5ee6b2c7ae Binary files /dev/null and b/app_python/docs/screenshots/lab04/aws-configuration.png differ diff --git a/app_python/docs/screenshots/lab04/connect_via_ssh.png b/app_python/docs/screenshots/lab04/connect_via_ssh.png new file mode 100644 index 0000000000..08d86fe0ff Binary files /dev/null and b/app_python/docs/screenshots/lab04/connect_via_ssh.png differ diff --git a/app_python/docs/screenshots/lab04/install_pulumi_aws.png b/app_python/docs/screenshots/lab04/install_pulumi_aws.png new file mode 100644 index 0000000000..c6fe938a9a Binary files /dev/null and b/app_python/docs/screenshots/lab04/install_pulumi_aws.png differ diff --git a/app_python/docs/screenshots/lab04/pipeline.png b/app_python/docs/screenshots/lab04/pipeline.png new file mode 100644 index 0000000000..e347d34927 Binary files /dev/null and b/app_python/docs/screenshots/lab04/pipeline.png differ diff --git a/app_python/docs/screenshots/lab04/pulumi-init.png b/app_python/docs/screenshots/lab04/pulumi-init.png new file mode 100644 index 0000000000..9cd07f6fe4 Binary files /dev/null and b/app_python/docs/screenshots/lab04/pulumi-init.png differ diff --git a/app_python/docs/screenshots/lab04/pulumi-ssh.png b/app_python/docs/screenshots/lab04/pulumi-ssh.png new file mode 100644 index 0000000000..db20cf9c20 Binary files /dev/null and b/app_python/docs/screenshots/lab04/pulumi-ssh.png differ diff --git a/app_python/docs/screenshots/lab04/pulumi-up.png b/app_python/docs/screenshots/lab04/pulumi-up.png new file mode 100644 index 0000000000..cf57f789f0 Binary files /dev/null and b/app_python/docs/screenshots/lab04/pulumi-up.png differ diff --git a/app_python/docs/screenshots/lab04/terraform-apply.png b/app_python/docs/screenshots/lab04/terraform-apply.png new file mode 100644 index 0000000000..ae25f80913 Binary files /dev/null and b/app_python/docs/screenshots/lab04/terraform-apply.png differ diff --git a/app_python/docs/screenshots/lab04/terraform-init.png b/app_python/docs/screenshots/lab04/terraform-init.png new file mode 100644 index 0000000000..b7bd5c1c03 Binary files /dev/null and b/app_python/docs/screenshots/lab04/terraform-init.png differ diff --git a/app_python/docs/screenshots/lab04/terrafrom-apply-finish.png b/app_python/docs/screenshots/lab04/terrafrom-apply-finish.png new file mode 100644 index 0000000000..51c26c17ea Binary files /dev/null and b/app_python/docs/screenshots/lab04/terrafrom-apply-finish.png differ diff --git a/app_python/docs/screenshots/lab04/terrafrom-destroy.png b/app_python/docs/screenshots/lab04/terrafrom-destroy.png new file mode 100644 index 0000000000..87b47e6c38 Binary files /dev/null and b/app_python/docs/screenshots/lab04/terrafrom-destroy.png differ diff --git a/app_python/pulumi/.gitignore b/app_python/pulumi/.gitignore new file mode 100644 index 0000000000..a3807e5bdb --- /dev/null +++ b/app_python/pulumi/.gitignore @@ -0,0 +1,2 @@ +*.pyc +venv/ diff --git a/app_python/pulumi/Pulumi.yaml b/app_python/pulumi/Pulumi.yaml new file mode 100644 index 0000000000..c8b505c5de --- /dev/null +++ b/app_python/pulumi/Pulumi.yaml @@ -0,0 +1,11 @@ +name: app_python-devops +description: A minimal Python Pulumi program +runtime: + name: python + options: + toolchain: pip + virtualenv: venv +config: + pulumi:tags: + value: + pulumi:template: python diff --git a/app_python/pulumi/__main__.py b/app_python/pulumi/__main__.py new file mode 100644 index 0000000000..61dc7977a3 --- /dev/null +++ b/app_python/pulumi/__main__.py @@ -0,0 +1,33 @@ +"""A Python Pulumi program""" + +import pulumi +import pulumi_aws as aws + +# Security Group +sg = aws.ec2.SecurityGroup( + "devops-lab4-sg", + description="Allow SSH, HTTP, HTTPS", + ingress=[ + {"protocol": "tcp", "from_port": 22, "to_port": 22, "cidr_blocks": ["0.0.0.0/0"]}, + {"protocol": "tcp", "from_port": 80, "to_port": 80, "cidr_blocks": ["0.0.0.0/0"]}, + {"protocol": "tcp", "from_port": 443, "to_port": 443, "cidr_blocks": ["0.0.0.0/0"]}, + ], + egress=[ + {"protocol": "-1", "from_port": 0, "to_port": 0, "cidr_blocks": ["0.0.0.0/0"]}, + ] +) + +# EC2 Instance +instance = aws.ec2.Instance( + "DevOpsLab4", + ami="ami-0b6c6ebed2801a5cb", + instance_type="t2.micro", + key_name="vockey", + vpc_security_group_ids=[sg.id], + tags={"Name": "DevOpsLab4"}, + root_block_device={"volume_size": 16} +) + +# Export public IP +pulumi.export("public_ip", instance.public_ip) +pulumi.export("public_dns", instance.public_dns) diff --git a/app_python/pulumi/requirements.txt b/app_python/pulumi/requirements.txt new file mode 100644 index 0000000000..bc4e43087b --- /dev/null +++ b/app_python/pulumi/requirements.txt @@ -0,0 +1 @@ +pulumi>=3.0.0,<4.0.0 diff --git a/app_python/requirements-dev.txt b/app_python/requirements-dev.txt new file mode 100644 index 0000000000..132ff97744 --- /dev/null +++ b/app_python/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest==8.3.5 +pytest-cov==7.0.0 +ruff==0.15.0 \ No newline at end of file diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..11e9a868d4 Binary files /dev/null and b/app_python/requirements.txt differ diff --git a/app_python/terraform/main.tf b/app_python/terraform/main.tf new file mode 100644 index 0000000000..d9669aa416 --- /dev/null +++ b/app_python/terraform/main.tf @@ -0,0 +1,47 @@ +resource "aws_security_group" "devops_lab4_sg" { + name = "devops-lab4-sg" + + ingress { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } +} + +resource "aws_instance" "devops-lab4" { + ami = "ami-0b6c6ebed2801a5cb" + + tags = { + Name = "DevOpsLab4" + } + + instance_type = "t2.micro" + key_name = "vockey" + vpc_security_group_ids = [aws_security_group.devops_lab4_sg.id] + + root_block_device { + volume_size = 16 + } +} diff --git a/app_python/terraform/outputs.tf b/app_python/terraform/outputs.tf new file mode 100644 index 0000000000..0633d9503d --- /dev/null +++ b/app_python/terraform/outputs.tf @@ -0,0 +1,3 @@ +output "public_ip" { + value = aws_instance.devops-lab4.public_ip +} \ No newline at end of file diff --git a/app_python/terraform/provider.tf b/app_python/terraform/provider.tf new file mode 100644 index 0000000000..1fae3b4d01 --- /dev/null +++ b/app_python/terraform/provider.tf @@ -0,0 +1,12 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = "us-east-1" +} \ No newline at end of file diff --git a/app_python/tests/__init__.py b/app_python/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/app_python/tests/__pycache__/__init__.cpython-312.pyc b/app_python/tests/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000..90ba723636 Binary files /dev/null and b/app_python/tests/__pycache__/__init__.cpython-312.pyc differ diff --git a/app_python/tests/__pycache__/conftest.cpython-312-pytest-8.3.5.pyc b/app_python/tests/__pycache__/conftest.cpython-312-pytest-8.3.5.pyc new file mode 100644 index 0000000000..95e9006067 Binary files /dev/null and b/app_python/tests/__pycache__/conftest.cpython-312-pytest-8.3.5.pyc differ diff --git a/app_python/tests/__pycache__/test_errors.cpython-312-pytest-8.3.5.pyc b/app_python/tests/__pycache__/test_errors.cpython-312-pytest-8.3.5.pyc new file mode 100644 index 0000000000..d87f5390f3 Binary files /dev/null and b/app_python/tests/__pycache__/test_errors.cpython-312-pytest-8.3.5.pyc differ diff --git a/app_python/tests/__pycache__/test_health.cpython-312-pytest-8.3.5.pyc b/app_python/tests/__pycache__/test_health.cpython-312-pytest-8.3.5.pyc new file mode 100644 index 0000000000..0157b345b3 Binary files /dev/null and b/app_python/tests/__pycache__/test_health.cpython-312-pytest-8.3.5.pyc differ diff --git a/app_python/tests/__pycache__/test_index.cpython-312-pytest-8.3.5.pyc b/app_python/tests/__pycache__/test_index.cpython-312-pytest-8.3.5.pyc new file mode 100644 index 0000000000..c218becd60 Binary files /dev/null and b/app_python/tests/__pycache__/test_index.cpython-312-pytest-8.3.5.pyc differ diff --git a/app_python/tests/conftest.py b/app_python/tests/conftest.py new file mode 100644 index 0000000000..66351c9584 --- /dev/null +++ b/app_python/tests/conftest.py @@ -0,0 +1,9 @@ + +import pytest +from app_python.app import app + +@pytest.fixture +def client(): + app.config["TESTING"] = True + with app.test_client() as client: + yield client diff --git a/app_python/tests/test_errors.py b/app_python/tests/test_errors.py new file mode 100644 index 0000000000..ad0dbfee6c --- /dev/null +++ b/app_python/tests/test_errors.py @@ -0,0 +1,15 @@ +def test_404_handler(client): + response = client.get("/ifyoureaditthenyouaregoodta") + assert response.status_code == 404 + + data = response.get_json() + + assert "message" in data + assert "error" in data + + assert data["error"] == "Not Found" + + +def test_method_not_allowed(client): + response = client.post("/") + assert response.status_code in (405, 500) \ No newline at end of file diff --git a/app_python/tests/test_health.py b/app_python/tests/test_health.py new file mode 100644 index 0000000000..9b8c1e3ab7 --- /dev/null +++ b/app_python/tests/test_health.py @@ -0,0 +1,14 @@ +def test_health_status_code(client): + response = client.get("/health") + assert response.status_code == 200 + + +def test_health_response_structure(client): + response = client.get("/health") + data = response.get_json() + + assert "status" in data + assert "timestamp" in data + assert "uptime_seconds" in data + + assert data["status"] == "healthy" diff --git a/app_python/tests/test_index.py b/app_python/tests/test_index.py new file mode 100644 index 0000000000..ef788c730a --- /dev/null +++ b/app_python/tests/test_index.py @@ -0,0 +1,25 @@ +def test_index_status_code(client): + response = client.get("/") + assert response.status_code == 200 + + +def test_index_response_structure(client): + response = client.get("/") + data = response.get_json() + + assert "endpoints" in data + assert "request" in data + assert "runtime" in data + assert "service" in data + assert "system" in data + + assert data["service"]["name"] == "devops-info-service" + assert data["service"]["framework"] == "Flask" + + assert "hostname" in data["system"] + assert "python_version" in data["system"] + + assert isinstance(data["runtime"]["uptime_seconds"], int) + + assert data["request"]["method"] == "GET" + assert data["request"]["path"] == "/"