diff --git a/.github/workflows/python-ci.yml b/.github/workflows/python-ci.yml new file mode 100644 index 0000000000..e861141a8e --- /dev/null +++ b/.github/workflows/python-ci.yml @@ -0,0 +1,123 @@ +name: Python CI/CD Pipeline + +on: + push: + branches: [ master, lab03 ] + paths: + - 'app_python/**' + - '.github/workflows/python-ci.yml' + pull_request: + branches: [ master ] + paths: + - 'app_python/**' + +env: + REGISTRY: docker.io + IMAGE_NAME: ${{ github.repository_owner }}/devops-info-service + PYTHON_VERSION: '3.13' + +jobs: + code-quality-and-testing: + name: Code Quality & Testing + runs-on: ubuntu-latest + defaults: + run: + working-directory: app_python + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python ${{ env.PYTHON_VERSION }} + uses: actions/setup-python@v5 + with: + python-version: ${{ env.PYTHON_VERSION }} + cache: 'pip' + cache-dependency-path: 'app_python/requirements.txt' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install flake8 black pytest pytest-cov + + - name: Lint with flake8 + run: | + echo "Running flake8 linting..." + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + + - name: Check code formatting with black + run: | + echo "Checking code formatting with black..." + black --check --diff . + + - name: Run unit tests with pytest + run: | + echo "Running unit tests with pytest..." + pytest --cov=app --cov-report=term-missing -v + + - name: Security scan with Snyk + uses: snyk/actions/python@master + continue-on-error: true + env: + SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} + with: + args: --severity-threshold=high --file=requirements.txt + + docker-build-and-push: + name: Docker Build & Push + runs-on: ubuntu-latest + needs: code-quality-and-testing + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} + + - name: Generate version tags + id: vars + run: | + echo "DATE_TAG=$(date +'%Y.%m.%d')" >> $GITHUB_OUTPUT + echo "SHORT_SHA=${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT + + COMMIT_COUNT=$(git rev-list --count --since="$(date +'%Y-%m-%d 00:00:00')" HEAD 2>/dev/null || echo "0") + echo "CALVER_TAG=$(date +'%Y.%m').$COMMIT_COUNT" >> $GITHUB_OUTPUT + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./app_python + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.DATE_TAG }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.CALVER_TAG }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.DATE_TAG }}-${{ steps.vars.outputs.SHORT_SHA }} + labels: | + org.opencontainers.image.title=DevOps Info Service + org.opencontainers.image.description=DevOps course info service + org.opencontainers.image.version=${{ steps.vars.outputs.CALVER_TAG }} + org.opencontainers.image.created=${{ steps.vars.outputs.DATE_TAG }} + org.opencontainers.image.revision=${{ github.sha }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Verify pushed images + run: | + echo "Docker images pushed with tags:" + echo "- latest" + echo "- ${{ steps.vars.outputs.DATE_TAG }}" + echo "- ${{ steps.vars.outputs.CALVER_TAG }}" + echo "- ${{ steps.vars.outputs.DATE_TAG }}-${{ steps.vars.outputs.SHORT_SHA }}" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 30d74d2584..7d5b034ea5 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ -test \ No newline at end of file +test +*.retry +.vault_pass +ansible/inventory/*.pyc +__pycache__/ \ No newline at end of file diff --git a/ansible/ansible.cfg b/ansible/ansible.cfg new file mode 100644 index 0000000000..7d3d2ea046 --- /dev/null +++ b/ansible/ansible.cfg @@ -0,0 +1,11 @@ +[defaults] +inventory = inventory/hosts.ini +roles_path = roles +host_key_checking = False +remote_user = devops +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..d1ca7ca7ef --- /dev/null +++ b/ansible/docs/LAB05.md @@ -0,0 +1,439 @@ +# Lab 5 — Ansible Fundamentals + +## 1. Architecture Overview + +- **Ansible version**: 2.20.2 (control node: Arch Linux) +- **Target VM**: Ubuntu 24.04 LTS (running on VirtualBox, accessible via SSH on port 2222) +- **Project structure**: Role-based, following recommended Ansible practices. + +``` +ansible +├── ansible.cfg +├── docs +│ └── LAB05.md # this doc +├── inventory +│ ├── group_vars +│ │ └── all.yml # encrypted variables (Ansible vault) +│ └── hosts.ini # static inventory +├── playbooks +│ ├── deploy.yml # application deployment +│ └── provision.yml # system provisioning +└── roles + ├── app_deploy # application deployment + │ ├── defaults + │ │ └── main.yml + │ ├── handlers + │ │ └── main.yml + │ └── tasks + │ └── main.yml + ├── common # common system packages + │ ├── defaults + │ │ └── main.yml + │ ├── handlers + │ └── tasks + │ └── main.yml + └── docker # docker installation + ├── defaults + │ └── main.yml + ├── handlers + │ └── main.yml + └── tasks + └── main.yml +``` + +- **Why roles?** Roles enable modular, reusable, and maintainable automation. Each role focuses on a specific concern, making the playbooks clean and easy to extend. + +## 2. Roles Documentation + +### 2.1 `common` Role + +**Purpose**: Install essential system packages and ensure the system is up‑to‑date. + +**Variables** (in `defaults/main.yml`): +```yaml +common_packages: + - python3-pip + - curl + - wget + - git + - vim + - htop + - net-tools + - unzip +``` + +**Tasks**: +- Update APT cache (with `cache_valid_time=3600` to avoid unnecessary updates) +- Install the packages listed above. + +### 2.2 `docker` Role + +**Purpose**: Install Docker CE and its dependencies, start the Docker service, and add the remote user to the `docker` group. + +**Variables** (`defaults/main.yml`): +```yaml +docker_user: "{{ ansible_user }}" +docker_packages: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin +``` + +**Handlers** (`handlers/main.yml`): +- `restart docker` – restarts the Docker daemon (used after configuration changes). + +**Tasks**: +1. Remove any conflicting packages (like `docker.io`). +2. Install required system packages (`ca-certificates`, `curl`). +3. Create the keyrings directory. +4. Add Docker’s official GPG key. +5. Add the Docker APT repository (using `ansible_distribution_release` to get the Ubuntu codename). +6. Install Docker packages. +7. Ensure Docker is running and enabled. +8. Add the user to the `docker` group. +9. Install `python3-docker` via APT (required for Ansible Docker modules). + +### 2.3 `app_deploy` Role + +**Purpose**: Pull the Docker image from Docker Hub and run the container with the correct configuration. + +**Variables** (`defaults/main.yml`): +```yaml +app_restart_policy: unless-stopped +app_env_vars: {} +``` +Actual values (image name, tag, port) are taken from the encrypted `group_vars/all.yml`. + +**Handlers** (`handlers/main.yml`): +- `restart app container` – restarts the application container (not used in current version but defined for future use). + +**Tasks**: +1. **Login to Docker Hub** – uses credentials from vault (with `no_log: true` to hide secrets). +2. **Pull the Docker image** – pulls `s3rap1s/devops-info-service:latest`. +3. **Check if container is already running** – registers `container_info`. +4. **Stop and remove existing container** – if it exists. +5. **Run the container** – with the following parameters: + - name: `devops-info-service` + - image: `s3rap1s/devops-info-service:latest` + - restart policy: `unless-stopped` + - port mapping: `5000:5000` +6. **Wait for the application to be ready** – using `wait_for` on port 5000. +7. **Verify health endpoint** – using `uri` module to check `/health` returns 200 OK. +8. **Display health check result** – prints the JSON response. + +## 3. Idempotency Demonstration + +### First run of `provision.yml` + +```bash +s3rap1s in ~/devops/DevOps-Core-Course/ansible on lab04 ● ● λ ansible-playbook playbooks/provision.yml --vault-password-file .vault_pass + +PLAY [Provision web servers with common tools and Docker] ********************************************************************************************************************************************************************************************************************** + +TASK [Gathering Facts] ********************************************************************************************************************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [common : Update apt cache] *********************************************************************************************************************************************************************************************************************************************** +changed: [devops-vm] + +TASK [common : Install common essential packages] ****************************************************************************************************************************************************************************************************************************** +changed: [devops-vm] + +TASK [docker : Remove conflicting packages] ************************************************************************************************************************************************************************************************************************************ +ok: [devops-vm] + +TASK [docker : Install required system packages] ******************************************************************************************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [docker : Create keyrings directory] ************************************************************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Add Docker's official GPG key] ********************************************************************************************************************************************************************************************************************************** +changed: [devops-vm] + +TASK [docker : Add Docker APT repository] ************************************************************************************************************************************************************************************************************************************** +[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg. +[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24. +Origin: /home/s3rap1s/devops/DevOps-Core-Course/ansible/roles/docker/tasks/main.yml:39:11 + +37 - name: Add Docker APT repository +38 apt_repository: +39 repo: "deb [arch={{ ansible_architecture | replace('x86_64','amd64') }} signed-by=/etc/apt/keyrings/docker.asc... + ^ column 11 + +Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead. + +changed: [devops-vm] + +TASK [docker : Install Docker packages] **************************************************************************************************************************************************************************************************************************************** +changed: [devops-vm] + +TASK [docker : Ensure Docker service is running 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=5 changed=7 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +``` + +### Second run of `provision.yml` + +```bash +s3rap1s in ~/devops/DevOps-Core-Course/ansible on lab04 ● ● λ ansible-playbook playbooks/provision.yml --vault-password-file .vault_pass + +PLAY [Provision web servers with common tools and Docker] ********************************************************************************************************************************************************************************************************************** + +TASK [Gathering Facts] ********************************************************************************************************************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [common : Update apt cache] *********************************************************************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [common : Install common essential packages] ****************************************************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Remove conflicting packages] ************************************************************************************************************************************************************************************************************************************ +ok: [devops-vm] + +TASK [docker : Install required system packages] ******************************************************************************************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [docker : Create keyrings directory] ************************************************************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Add Docker's official GPG key] ********************************************************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Add Docker APT repository] ************************************************************************************************************************************************************************************************************************************** +[WARNING]: Deprecation warnings can be disabled by setting `deprecation_warnings=False` in ansible.cfg. +[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS default to `True` is deprecated, top-level facts will not be auto injected after the change. This feature will be removed from ansible-core version 2.24. +Origin: /home/s3rap1s/devops/DevOps-Core-Course/ansible/roles/docker/tasks/main.yml:39:11 + +37 - name: Add Docker APT repository +38 apt_repository: +39 repo: "deb [arch={{ ansible_architecture | replace('x86_64','amd64') }} signed-by=/etc/apt/keyrings/docker.asc... + ^ column 11 + +Use `ansible_facts["fact_name"]` (no `ansible_` prefix) instead. + +ok: [devops-vm] + +TASK [docker : Install Docker packages] **************************************************************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [docker : Ensure Docker service is running 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 skipped=0 rescued=0 ignored=0 +``` + +**Analysis**: +- First run: many tasks reported **changed** because packages were installed, repositories added, etc. +- Second run: all tasks reported **ok** (green) because the system already matched the desired state. +- This **idempotency** proves that the roles are correctly written – they only make changes when necessary and do not break anything when run repeatedly. + +## 4. Ansible Vault Usage + +Sensitive information (Docker Hub credentials) is stored encrypted using Ansible Vault. + +**Creation of vault file**: +```bash +ansible-vault create inventory/group_vars/all.yml +``` +Vault password is stored in a local `.vault_pass` file (added to `.gitignore`). The vault file contains: +```yaml +dockerhub_username: "s3rap1s" +dockerhub_password: "dckr_pat_xxxxxx" # nah uh +app_name: "devops-info-service" +docker_image: "{{ dockerhub_username }}/{{ app_name }}" +docker_image_tag: "latest" +app_port: 5000 +app_container_name: "{{ app_name }}" +``` + +**Usage in playbook**: +All tasks that use these variables refer to them normally. The vault password is supplied via the command line: +```bash +ansible-playbook playbooks/deploy.yml --vault-password-file .vault_pass +``` + +**Why Ansible Vault matters**: +- Secrets are never exposed in plain text. +- The vault file can be safely committed to Git (it is encrypted). +- Access to secrets is controlled by the vault password, which is kept outside the repository. + +## 5. Deployment Verification + +### Playbook execution +```bash +s3rap1s in ~/devops/DevOps-Core-Course/ansible on lab04 ● ● λ ansible-playbook playbooks/deploy.yml --vault-password-file .vault_pass + +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 : Check if container is running] ****************************************************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [app_deploy : Stop and remove existing container if it exists] ************************************************************************************************************************************************************************************************************ +changed: [devops-vm] + +TASK [app_deploy : Run Docker container] *************************************************************************************************************************************************************************************************************************************** +changed: [devops-vm] + +TASK [app_deploy : Wait for application to be ready] *************************************************************************************************************************************************************************************************************************** +ok: [devops-vm] + +TASK [app_deploy : Verify health endpoint] ************************************************************************************************************************************************************************************************************************************* +ok: [devops-vm] + +TASK [app_deploy : Display health check result] ******************************************************************************************************************************************************************************************************************************** +ok: [devops-vm] => { + "msg": "Health check passed! Response: {'status': 'healthy', 'timestamp': '2026-02-25T13:03:39.898895+00:00', 'uptime_seconds': 5}" +} + +PLAY RECAP ********************************************************************************************************************************************************************************************************************************************************************* +devops-vm : ok=9 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0 + +``` + +### Manual checks +```bash +s3rap1s in ~/devops/DevOps-Core-Course/ansible on lab04 ● ● λ ssh devops@localhost -p 2222 +Welcome to Ubuntu 24.04.4 LTS (GNU/Linux 6.8.0-100-generic x86_64) + + * Documentation: https://help.ubuntu.com + * Management: https://landscape.canonical.com + * Support: https://ubuntu.com/pro + + System information as of Wed Feb 25 01:03:17 PM UTC 2026 + + System load: 0.0 + Usage of /: 16.8% of 24.44GB + Memory usage: 20% + Swap usage: 0% + Processes: 111 + Users logged in: 1 + IPv4 address for enp0s3: 10.0.2.15 + IPv6 address for enp0s3: fd17:625c:f037:2:a00:27ff:fe00:936e + + * Strictly confined Kubernetes makes edge and IoT secure. Learn how MicroK8s + just raised the bar for easy, resilient and secure K8s cluster deployment. + + https://ubuntu.com/engage/secure-kubernetes-at-the-edge + +Expanded Security Maintenance for Applications is not enabled. + +20 updates can be applied immediately. +17 of these updates are standard security updates. +To see these additional updates run: apt list --upgradable + +Enable ESM Apps to receive additional future security updates. +See https://ubuntu.com/esm or run: sudo pro status + + +Last login: Wed Feb 25 13:03:39 2026 from 10.0.2.2 +devops@devops:~$ docker ps +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +9efe64c0a550 s3rap1s/devops-info-service:latest "python app.py" 28 seconds ago Up 27 seconds 0.0.0.0:5000->5000/tcp devops-info-service +devops@devops:~$ curl http://localhost:5000/health +{"status":"healthy","timestamp":"2026-02-25T13:04:18.686599+00:00","uptime_seconds":44} +``` + +Everything works as expected – the application is running and healthy. + +## 6. Key Decisions + +### Why use roles instead of plain playbooks? +- **Reusability**: the same role can be applied to multiple hosts or projects. +- **Maintainability**: each role is isolated, making it easier to update or debug one part without affecting others. +- **Clarity**: the structure clearly separates concerns (system, Docker, application). + +### How do roles improve reusability? +- Roles can be shared via Ansible Galaxy. +- Variables and defaults allow easy customisation without changing the role code. +- Handlers and files are bundled together, so the role is self-contained. + +### What makes a task idempotent? +- Using state‑oriented modules (e.g., `apt: state=present`, `service: state=started`) instead of raw commands. +- Modules check the current state before applying changes and only act when necessary. +- For example, the Docker repository is added only once; subsequent runs see that it already exists. + +### How do handlers improve efficiency? +- Handlers are triggered only when a task reports a change, and run once at the end of the play. +- This avoids unnecessary restarts (e.g., restarting Docker after every small change). + +### Why is Ansible Vault necessary? +- To store secrets (passwords, tokens) securely in version control. +- Prevents accidental exposure of credentials. +- Enforces that only authorised users (with the vault password) can see the secrets. + +## 7. Challenges (Optional) + +- **HashiCorp repository error**: The target VM had a broken `hashicorp.list` file that caused `apt update` to fail. Solved by manually removing the file (`sudo rm /etc/apt/sources.list.d/hashicorp.list`). +- **Python external‑management error**: Ubuntu 24.04 blocks `pip install` system‑wide. Replaced `pip` installation of `docker` Python module with `apt install python3-docker`. +- **Vault variables not visible in roles**: Initially the variables from `group_vars/all.yml` were not loaded because the file was placed in the wrong directory. Moving it to `inventory/group_vars/all.yml` solved the issue. +- **Timeout on health check**: The `wait_for` task was delegated to `localhost` while the container runs inside the VM. Removing `delegate_to: localhost` fixed it. + +## 8. Bonus Task – Dynamic Inventory (Theoretical) + +If I had a cloud VM (e.g., on AWS or Yandex Cloud), I would implement dynamic inventory to avoid hardcoding IP addresses. + +**Planned approach** (for AWS as an example): + +1. Install the required collection: + ```bash + ansible-galaxy collection install amazon.aws + ``` + +2. Create `inventory/aws_ec2.yml`: + ```yaml + plugin: amazon.aws.aws_ec2 + regions: + - us-east-1 + filters: + instance-state-name: running + tag:Environment: dev + keyed_groups: + - key: tags.Role + prefix: role + hostnames: + - public-ip-address + compose: + ansible_user: "'ubuntu'" + ``` + +3. Use it with the existing playbooks: + ```bash + ansible-playbook -i inventory/aws_ec2.yml playbooks/provision.yml + ``` + +**Benefits of dynamic inventory**: +- Automatically discovers new instances. +- Groups hosts by tags (e.g., `role_webserver`). +- No manual updates when IPs change. + +**Why not implemented here**: +- I used a local VM for cost reasons and simplicity, so there is no cloud infrastructure to manage dynamically. diff --git a/ansible/inventory/group_vars/all.yml b/ansible/inventory/group_vars/all.yml new file mode 100644 index 0000000000..28ca316a76 --- /dev/null +++ b/ansible/inventory/group_vars/all.yml @@ -0,0 +1,18 @@ +$ANSIBLE_VAULT;1.1;AES256 +66393162653239306333353934323434623761303435643362313063666662313363646238663130 +3666313737363935383034353466643233613439663366360a383664613365666566326662356265 +35666637393632356663316533376630623539386138393535633264376163396461616336373239 +3935363938643761300a656561363235346432396235393531316161346437646465356265363737 +34323437353037616665626466663662356564613032343566393061626136656666666430393062 +30306361303763623936376561633932656361366631386331663832363838636361653839376464 +34386539623733653466333838353364656162343237656133393966333436333438333230343337 +64343231353939323033326530643035316131396537633664343736343738386263343439666637 +33363934376139323864623233353665393361376461376533636464373965323366373862336632 +66613938643839333737636263383738366537643936376366346335343638663866386464643539 +62333165653837343462386233393261316230313166646164653161616539663562383039643564 +61623639633933333733613764353632323064653061656431353935666435306334633961616466 +31643337653433336365666634613033353033306564343734383830613935623130653433366531 +37626461613232323539313762386662376466636366376163653933663466653838323439616266 +66336239303334653435306334336132366331356461303539396139623931316564663564393266 +36303465633131396262346538636566633130386332336461303463623862636237383737613438 +61323861666365373139323230333163663937313531623230366339656262323963 diff --git a/ansible/inventory/hosts.ini b/ansible/inventory/hosts.ini new file mode 100644 index 0000000000..f84bb682b6 --- /dev/null +++ b/ansible/inventory/hosts.ini @@ -0,0 +1,5 @@ +[webservers] +devops-vm ansible_host=127.0.0.1 ansible_port=2222 ansible_user=devops ansible_ssh_private_key_file=~/.ssh/id_ed25519 + +[webservers:vars] +ansible_python_interpreter=/usr/bin/python3 \ No newline at end of file diff --git a/ansible/playbooks/deploy.yml b/ansible/playbooks/deploy.yml new file mode 100644 index 0000000000..10be59c817 --- /dev/null +++ b/ansible/playbooks/deploy.yml @@ -0,0 +1,6 @@ +- name: Deploy application + hosts: webservers + become: yes + + roles: + - app_deploy diff --git a/ansible/playbooks/provision.yml b/ansible/playbooks/provision.yml new file mode 100644 index 0000000000..abc6c81307 --- /dev/null +++ b/ansible/playbooks/provision.yml @@ -0,0 +1,7 @@ +- name: Provision web servers with common tools and Docker + hosts: webservers + become: yes + + roles: + - common + - docker \ No newline at end of file diff --git a/ansible/roles/app_deploy/defaults/main.yml b/ansible/roles/app_deploy/defaults/main.yml new file mode 100644 index 0000000000..ba30f4edd6 --- /dev/null +++ b/ansible/roles/app_deploy/defaults/main.yml @@ -0,0 +1,2 @@ +app_restart_policy: unless-stopped +app_env_vars: {} \ No newline at end of file diff --git a/ansible/roles/app_deploy/handlers/main.yml b/ansible/roles/app_deploy/handlers/main.yml new file mode 100644 index 0000000000..7e75326c55 --- /dev/null +++ b/ansible/roles/app_deploy/handlers/main.yml @@ -0,0 +1,6 @@ +- name: restart app container + docker_container: + name: "{{ app_container_name }}" + state: started + restart: yes + become: yes \ No newline at end of file diff --git a/ansible/roles/app_deploy/tasks/main.yml b/ansible/roles/app_deploy/tasks/main.yml new file mode 100644 index 0000000000..31fac035f9 --- /dev/null +++ b/ansible/roles/app_deploy/tasks/main.yml @@ -0,0 +1,61 @@ +- name: Log in to Docker Hub + community.docker.docker_login: + username: "{{ dockerhub_username }}" + password: "{{ dockerhub_password }}" + no_log: false + become: yes + +- name: Pull Docker image + community.docker.docker_image: + name: "{{ docker_image }}" + tag: "{{ docker_image_tag }}" + source: pull + force_source: yes + become: yes + notify: restart app container + +- name: Check if container is running + community.docker.docker_container_info: + name: "{{ app_container_name }}" + register: container_info + become: yes + +- name: Stop and remove existing container if it exists + community.docker.docker_container: + name: "{{ app_container_name }}" + state: absent + when: container_info.exists + become: yes + +- name: Run Docker container + community.docker.docker_container: + name: "{{ app_container_name }}" + image: "{{ docker_image }}:{{ docker_image_tag }}" + state: started + restart_policy: "{{ app_restart_policy }}" + ports: + - "{{ app_port }}:{{ app_port }}" + env: "{{ app_env_vars }}" + become: yes + register: container_run + +- name: Wait for application to be ready + wait_for: + port: "{{ app_port }}" + delay: 5 + timeout: 30 + +- name: Verify health endpoint + uri: + url: "http://localhost:{{ app_port }}/health" + method: GET + status_code: 200 + register: health_result + until: health_result.status == 200 + retries: 5 + delay: 2 + +- name: Display health check result + debug: + msg: "Health check passed! Response: {{ health_result.json }}" + when: health_result.status == 200 \ No newline at end of file diff --git a/ansible/roles/common/defaults/main.yml b/ansible/roles/common/defaults/main.yml new file mode 100644 index 0000000000..fcda7bd924 --- /dev/null +++ b/ansible/roles/common/defaults/main.yml @@ -0,0 +1,9 @@ +common_packages: + - python3-pip + - curl + - wget + - git + - vim + - htop + - net-tools + - unzip \ No newline at end of file diff --git a/ansible/roles/common/tasks/main.yml b/ansible/roles/common/tasks/main.yml new file mode 100644 index 0000000000..cebd77c3f5 --- /dev/null +++ b/ansible/roles/common/tasks/main.yml @@ -0,0 +1,11 @@ +- name: Update apt cache + apt: + update_cache: yes + cache_valid_time: 3600 + become: yes + +- name: Install common essential packages + apt: + name: "{{ common_packages }}" + state: present + become: yes \ No newline at end of file diff --git a/ansible/roles/docker/defaults/main.yml b/ansible/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..285e71fd52 --- /dev/null +++ b/ansible/roles/docker/defaults/main.yml @@ -0,0 +1,7 @@ +docker_user: "{{ ansible_user }}" +docker_packages: + - docker-ce + - docker-ce-cli + - containerd.io + - docker-buildx-plugin + - docker-compose-plugin \ No newline at end of file diff --git a/ansible/roles/docker/handlers/main.yml b/ansible/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..b2fdfa6384 --- /dev/null +++ b/ansible/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +- name: restart docker + service: + name: docker + state: restarted + become: yes \ No newline at end of file diff --git a/ansible/roles/docker/tasks/main.yml b/ansible/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..0c5375ad47 --- /dev/null +++ b/ansible/roles/docker/tasks/main.yml @@ -0,0 +1,71 @@ +- name: Remove conflicting packages + apt: + name: + - docker.io + - docker-doc + - docker-compose + - podman-docker + - containerd + - runc + state: absent + become: yes + ignore_errors: yes + +- name: Install required system packages + apt: + name: + - ca-certificates + - curl + state: present + become: yes + +- name: Create keyrings directory + file: + path: /etc/apt/keyrings + state: directory + mode: '0755' + become: yes + +- name: Add Docker's official GPG key + get_url: + url: https://download.docker.com/linux/ubuntu/gpg + dest: /etc/apt/keyrings/docker.asc + mode: '0644' + force: true + become: yes + +- name: Add Docker APT repository + apt_repository: + repo: "deb [arch={{ ansible_architecture | replace('x86_64','amd64') }} signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu {{ ansible_distribution_release }} stable" + state: present + update_cache: yes + become: yes + +- name: Install Docker packages + apt: + name: "{{ docker_packages }}" + state: present + update_cache: yes + become: yes + notify: restart docker + +- name: Ensure Docker service is running and enabled + service: + name: docker + state: started + enabled: yes + become: yes + +- name: Add user to docker group + user: + name: "{{ docker_user }}" + groups: docker + append: yes + become: yes + notify: restart docker + +- name: Install python3-docker for Ansible docker modules + apt: + name: python3-docker + state: present + become: yes \ No newline at end of file diff --git a/app_go/.dockerignore b/app_go/.dockerignore new file mode 100644 index 0000000000..cfb650bdca --- /dev/null +++ b/app_go/.dockerignore @@ -0,0 +1,43 @@ +# Go artifacts +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out +*.o +*.prof +*.trace +vendor/ +__pycache__/ + +# Git +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Documentation +*.md +docs/ +README.md + +# Docker +Dockerfile* +docker-compose* +.dockerignore + +# Temporary files +*.tmp +*.log +tmp/ +logs/ diff --git a/app_go/Dockerfile b/app_go/Dockerfile new file mode 100644 index 0000000000..2a3e22eea0 --- /dev/null +++ b/app_go/Dockerfile @@ -0,0 +1,42 @@ +# Stage 1: Builder - full build environment +FROM golang:1.21-alpine AS builder + +# Install system dependencies for build (git for go mod download) +RUN apk add --no-cache git ca-certificates + +# Set working directory +WORKDIR /app + +# Copy Go module files +COPY go.mod ./ + +# Download Go module dependencies +RUN go mod download + +# Copy application source code +COPY . . + +# Build Go application with optimizations +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -a \ + -ldflags="-w -s -extldflags '-static'" \ + -o devops-info-service . + +# Stage 2: Minimal runtime +FROM scratch + +# Copy CA certificates from builder for HTTPS support +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Copy the compiled binary from builder +COPY --from=builder /app/devops-info-service /app/devops-info-service + +# Expose port +EXPOSE 5000 + +# Set environment variables +ENV HOST=0.0.0.0 +ENV PORT=5000 + +# Run the application +CMD ["/app/devops-info-service"] \ No newline at end of file diff --git a/app_go/README.md b/app_go/README.md new file mode 100644 index 0000000000..292daf65ab --- /dev/null +++ b/app_go/README.md @@ -0,0 +1,97 @@ +# DevOps Info Service (Go) +## Overview +A Go-based web service designed to furnish details about itself and its operational environment. This compiled version of the service is optimized for containerization and multi-stage Docker builds, offering improved performance and smaller deployment footprints compared to interpreted languages. + +## Prerequisites +- Go 1.21 or higher +- Git (for dependency management) + +## Installation +1. Clone repository: + +```bash +# Clone the project +git clone https://github.com/s3rap1s/DevOps-Core-Course.git +cd DevOps-Core-Course/app_go + +# Download dependencies +go mod download + +# Build the application +go build -o devops-info-service +``` + +## Running the Application +```bash +# Default configuration +./devops-info-service + +# With custom port +PORT=8080 ./devops-info-service + +# With custom port and host +HOST=127.0.0.1 PORT=3000 ./devops-info-service + +# Run directly without building +go run main.go +Building for Different Platforms +bash +# Build for current platform +go build -o devops-info-service +``` + +## API Endpoints +### GET / +Return comprehensive service and system information: + +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Go" + }, + "system": { + "hostname": "my-laptop", + "platform": "linux", + "platform_version": "Linux Kernel", + "architecture": "amd64", + "cpu_count": 8, + "go_version": "go1.21.4" + }, + "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:12345", + "user_agent": "curl/7.81.0", + "method": "GET", + "path": "/" + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] +} +``` + +### GET /health +Simple health endpoint for monitoring: + +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` + +## Configuration +| Variable | Default | Description | +| -------- | --------- | ---------------------------- | +| `HOST` | `0.0.0.0` | Network interface to bind | +| `PORT` | `5000` | Port to listen on | \ No newline at end of file diff --git a/app_go/docs/GO.md b/app_go/docs/GO.md new file mode 100644 index 0000000000..b42a88d6a8 --- /dev/null +++ b/app_go/docs/GO.md @@ -0,0 +1,18 @@ +# Language Justification +## My Choice: Go +I selected **Go** as the compiled language for the bonus task implementation. Here's why: + +**Comparison Table:** + +| Criteria | Go | Rust | Java | C# | +|----------|----|------|------|----| +|Learning Curve | Low | High | Medium | Medium | +|Development Speed | High | Low | Medium | Medium | +|Standard Library | Excellent | Good | Extensive | Extensive | +|Performance | Excellent | Outstanding | Good | Good | +|Binary Size | ~7 MB | ~3 MB | ~40 MB | ~30 MB | +|Memory Safety | GC | Compile-time | GC | GC | +| **Choice for Bonus** | **✓** | | | | + +**Justification:** +Go offers the perfect balance for a DevOps service: it compiles to a single static binary with no runtime dependencies, has excellent concurrency support, and provides a rich standard library including HTTP server functionality. Its simplicity and fast compilation make it ideal for the iterative development required in this course. Go is also widely used in the DevOps ecosystem (Docker, Kubernetes, Prometheus), making it a relevant choice. \ No newline at end of file diff --git a/app_go/docs/LAB01.md b/app_go/docs/LAB01.md new file mode 100644 index 0000000000..4b93018d26 --- /dev/null +++ b/app_go/docs/LAB01.md @@ -0,0 +1,293 @@ +# Lab 1 — DevOps Info Service: Web Application Development on Go +## Language Selection +### My Choice: Go +I selected **Go** as the compiled language for the bonus task implementation. Here's why: + +**Comparison Table:** + +| Criteria | Go | Rust | Java | C# | +|----------|----|------|------|----| +|Learning Curve | Low | High | Medium | Medium | +|Development Speed | High | Low | Medium | Medium | +|Standard Library | Excellent | Good | Extensive | Extensive | +|Performance | Excellent | Outstanding | Good | Good | +|Binary Size | ~7 MB | ~3 MB | ~40 MB | ~30 MB | +|Memory Safety | GC | Compile-time | GC | GC | +| **Choice for Bonus** | **✓** | | | | + +**Justification:** +Go offers the perfect balance for a DevOps service: it compiles to a single static binary with no runtime dependencies, has excellent concurrency support, and provides a rich standard library including HTTP server functionality. Its simplicity and fast compilation make it ideal for the iterative development required in this course. Go is also widely used in the DevOps ecosystem (Docker, Kubernetes, Prometheus), making it a relevant choice. + +## Best Practices Applied +### 1. Clean Code Organization +```go +// Clear imports grouping +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "runtime" + "time" +) + +// Descriptive function names +func getSystemInfo() System { + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown" + } + + return System{ + Hostname: hostname, + Platform: runtime.GOOS, + PlatformVersion: getOSVersion(), + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + GoVersion: runtime.Version(), + } +} +``` +**Importance:** Clean organization with clear separation of concerns makes the code maintainable and testable. Following Go conventions (camelCase, exported/unexported identifiers) ensures consistency. + +### 2. Comprehensive Error Handling +```go +func notFoundHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{ + "error": "Not Found", + "message": "Endpoint does not exist", + }) + + log.Printf("404 Not Found: %s", r.URL.Path) +} +``` +**Importance:** Proper error handling prevents application crashes and provides meaningful feedback to API consumers. Each error type returns appropriate HTTP status codes and structured JSON responses. + +### 3. Structured Logging +```go +func main() { + // Read environment variables + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + host := os.Getenv("HOST") + if host == "" { + host = "0.0.0.0" + } + + log.Printf("Starting DevOps Info Service on %s:%s", host, port) + log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%s", host, port), nil)) +} +``` +**Importance:** Logging provides visibility into application behavior and startup configuration. The standard log package is sufficient for this simple service, though larger applications might use structured logging libraries. + +### 4. Configuration via Environment Variables +```go +port := os.Getenv("PORT") +if port == "" { + port = "5000" +} +``` +**Importance:** Following the 12-factor app methodology, configuration via environment variables makes the application portable across different environments without recompilation. + +### 5. Minimal Dependencies +```go +// go.mod - only Go standard library is used +module devops-info-service + +go 1.21 +``` +**Importance:** Using only the standard library eliminates dependency management overhead and reduces security vulnerabilities. The resulting binary is self-contained. + +### 6. Static Typing and Compile-Time Safety +```go +type ServiceInfo struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime Runtime `json:"runtime"` + Request Request `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} +``` +**Importance:** Static typing catches many errors at compile time rather than runtime, improving reliability. Struct tags provide clear mapping between Go structs and JSON output. + +## API Documentation +### Endpoint 1: GET / +**Description:** Returns comprehensive service information, system details, runtime data, and request metadata. + +**Request:** +```bash +curl http://localhost:8080/ +``` +**Response (example):** +```json +{ + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Go" + }, + "system": { + "hostname": "ubuntu-dev", + "platform": "linux", + "platform_version": "Linux Kernel", + "architecture": "amd64", + "cpu_count": 8, + "go_version": "go1.21.4" + }, + "runtime": { + "uptime_seconds": 125, + "uptime_human": "0 hours, 2 minutes", + "current_time": "2026-01-27T10:30:00.000Z", + "timezone": "UTC" + }, + "request": { + "client_ip": "127.0.0.1:54321", + "user_agent": "curl/7.88.1", + "method": "GET", + "path": "/" + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"} + ] +} +``` +### Endpoint 2: GET /health +**Description:** Health check endpoint for monitoring system. Always returns HTTP 200 with service status. + +**Request:** +```bash +curl http://localhost:8080/health +``` +**Response:** +```json +{ + "status": "healthy", + "timestamp": "2026-01-27T10:30:00.000Z", + "uptime_seconds": 125 +} +``` +### Testing Commands +1. **Basic endpoint test:** + +```bash +curl http://localhost:8080/ +``` + +2. **Health check test:** +```bash +curl http://localhost:8080/health +``` +3. **Pretty-printed output:** +```bash +curl http://localhost:8080/ | jq . +``` +4. **Custom configuration:** +```bash +PORT=8080 ./devops-info-service +curl http://localhost:8080/health +``` + +5. **Error simulation:** +```bash +curl -v http://localhost:8080/nonexistent +# Should return 404 error +``` +## Build Process +### Compilation +```bash +# Initialize Go module +go mod init devops-info-service + +# Build standard binary +go build -o devops-info-service +``` +###Running +```bash +# Run the compiled binary +./devops-info-service + +# Run with custom configuration +HOST=127.0.0.1 PORT=3000 ./devops-info-service + +# Run directly (without building) +go run main.go +``` +## Testing Evidence +### Main endpoint: +![Main Endpoint](screenshots/01-main-endpoint.png) + +### Health check: +![Health Check](screenshots/02-health-check.png) + +### Formatted output: +![Formatted output](screenshots/03-formatted-output.png) + +## Challenges & Solutions +### Challenge 1: HTTP Handler Registration +**Problem:** Go's http.HandleFunc doesn't allow multiple registrations for the same path, unlike Flask's decorator pattern. + +**Solution:** Implemented a routing check within the main handler: +```go +func mainHandler(w http.ResponseWriter, r *http.Request) { + // Handle only root path + if r.URL.Path != "/" { + notFoundHandler(w, r) + return + } + // ... rest of handler +} +``` +### Challenge 2: Platform Version Detection +**Problem:** Go's standard library doesn't provide detailed OS version information like Python's platform.release(). + +**Solution:** Created a simple mapping function: +```go +func getOSVersion() string { + switch runtime.GOOS { + case "linux": + return "Linux Kernel" + case "darwin": + return "macOS" + case "windows": + return "Windows" + default: + return runtime.GOOS + } +} +``` +### Challenge 3: Uptime Formatting +**Problem:** Converting seconds to human-readable format required manual calculation. + +**Solution:** Implemented a reusable function: +```go +func getUptime() (int, string) { + duration := time.Since(startTime) + seconds := int(duration.Seconds()) + + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + + return seconds, fmt.Sprintf("%d hours, %d minutes", hours, minutes) +} +``` +### Challenge 4: JSON Serialization +**Problem:** Ensuring proper JSON field naming and null handling. + +**Solution:** Used struct tags and proper initialization: +```go +type System struct { + Hostname string `json:"hostname"` + Platform string `json:"platform"` + PlatformVersion string `json:"platform_version"` + // ... other fields +} +``` + diff --git a/app_go/docs/LAB02.md b/app_go/docs/LAB02.md new file mode 100644 index 0000000000..9cec8ec40f --- /dev/null +++ b/app_go/docs/LAB02.md @@ -0,0 +1,252 @@ +# Lab 2 Bonus — Multi-Stage Docker Build for Go Application + +## Multi-Stage Strategy + +### Stage 1: Builder +```dockerfile +# Stage 1: Builder - full build environment +FROM golang:1.21-alpine AS builder + +# Install system dependencies for build (git for go mod download) +RUN apk add --no-cache git ca-certificates + +# Set working directory +WORKDIR /app + +# Copy Go module files +COPY go.mod ./ + +# Download Go module dependencies +RUN go mod download + +# Copy application source code +COPY . . + +# Build Go application with optimizations +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -a \ + -ldflags="-w -s -extldflags '-static'" \ + -o devops-info-service . +``` + +**Purpose:** Complete build environment containing: +- Go 1.21 compiler and standard library +- Git for dependency management +- All source code and build tools +- Temporary workspace for compilation + +### Stage 2: Runtime +```dockerfile +FROM scratch + +# Copy CA certificates from builder for HTTPS support +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Copy the compiled binary from builder +COPY --from=builder /app/devops-info-service /app/devops-info-service + +# Expose port +EXPOSE 5000 + +# Set environment variables +ENV HOST=0.0.0.0 +ENV PORT=5000 + +# Run the application +CMD ["/app/devops-info-service"] +``` + +**Purpose:** Absolute minimal runtime environment containing only: +- Statically compiled Go binary +- CA certificates for HTTPS/TLS support +- No operating system, shell, or package manager + +## Size Comparison + +### Image Size Analysis +| Component | Size | Contents | +|-----------|------|----------| +| **Builder Stage** | ~350MB | Full Go 1.21 SDK + Alpine + build tools | +| **Runtime Stage** | **7.16MB** | Static binary + CA certificates | +| **Size Reduction** | **~98%** | 350MB → 7.16MB | + +### Detailed Breakdown +- **Builder image:** Uses `golang:1.21-alpine` (~350MB with build tools) +- **Final image:** Uses `scratch` (0MB base) + binary + certificates +- **Binary size:** ~7.16MB (statically compiled Go application + CA certificate) + +## Why Multi-Stage Builds Matter for Compiled Languages + +### 1. Drastic Size Reduction +Compiled languages like Go have a fundamental advantage: they produce standalone binaries. Multi-stage builds leverage this by: +- **Separating concerns:** Build environment (large) vs runtime (minimal) +- **Eliminating build tools:** Compiler, linker, SDK removed from production +- **Removing dependencies:** Only the binary and absolute essentials remain + +### 2. Enhanced Security +```dockerfile +FROM scratch # No operating system, no shell, no package manager +``` + +**Security benefits:** +- **No shell access:** Cannot spawn shells even if compromised +- **Immutable runtime:** Binary cannot be modified without rebuilding +- **Principle of least privilege:** Only what's absolutely necessary + +### 3. Production Performance +- **Faster deployment:** Smaller images download quicker +- **Reduced storage:** Less disk space required across development/staging/production +- **Lower memory footprint:** Minimal OS overhead +- **Quick startup:** No initialization of unused services + +## Terminal Output + +### Build Process Output +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker build -t devops-info-service:go . +[+] Building 5.1s (14/14) FINISHED docker:default + => [internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 1.00kB 0.0s + => [internal] load metadata for docker.io/library/golang:1.21-alpine 0.7s + => [internal] load .dockerignore 0.0s + => => transferring context: 359B 0.0s + => [builder 1/7] FROM docker.io/library/golang:1.21-alpine@sha256:2414035b086e3c42b99654c8b26e6f5b1b1598080d65fd03c7f499552ff4dc94 0.0s + => => resolve docker.io/library/golang:1.21-alpine@sha256:2414035b086e3c42b99654c8b26e6f5b1b1598080d65fd03c7f499552ff4dc94 0.0s + => [internal] load build context 0.0s + => => transferring context: 54B 0.0s + => CACHED [builder 2/7] RUN apk add --no-cache git ca-certificates 0.0s + => CACHED [builder 3/7] WORKDIR /app 0.0s + => CACHED [builder 4/7] COPY go.mod ./ 0.0s + => CACHED [builder 5/7] RUN go mod download 0.0s + => CACHED [builder 6/7] COPY . . 0.0s + => [builder 7/7] RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -a -ldflags="-w -s -extldflags '-static'" -o devops-info-service . 3.9s + => [stage-1 1/2] COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ 0.0s + => [stage-1 2/2] COPY --from=builder /app/devops-info-service /app/devops-info-service 0.0s + => exporting to image 0.4s + => => exporting layers 0.3s + => => exporting manifest sha256:4e1764a6a80bfc8666f97655b398a31766e6ef0b7e113b73651ca44601a369e5 0.0s + => => exporting config sha256:07f16274913e502fcb7339751611566f733555d340310768666604f24375006b 0.0s + => => exporting attestation manifest sha256:594b42605a1d485598b5ca39be853c1e154958e927a235027e0ca1b5fce2efa9 0.0s + => => exporting manifest list sha256:c5f945015fb0dfd3f762151c64c5393121944441d08e191e5a7533aadcf4f4eb 0.0s + => => naming to docker.io/library/devops-info-service:go 0.0s + => => unpacking to docker.io/library/devops-info-service:go +``` + +### Image Size Verification +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker images | grep devops-info-service +WARNING: This output is designed for human readability. For machine-readable output, please use --format. +devops-info-service:go c5f945015fb0 7.16MB 2.2MB U +devops-info-service:python 4b08b6e2f063 199MB 48.1MB +s3rap1s/devops-info-service:python ef074c1a118d 199MB 48.1MB + +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker history devops-info-service:go +IMAGE CREATED CREATED BY SIZE COMMENT +c5f945015fb0 14 minutes ago CMD ["/app/devops-info-service"] 0B buildkit.dockerfile.v0 + 14 minutes ago ENV PORT=5000 0B buildkit.dockerfile.v0 + 14 minutes ago ENV HOST=0.0.0.0 0B buildkit.dockerfile.v0 + 14 minutes ago EXPOSE [5000/tcp] 0B buildkit.dockerfile.v0 + 14 minutes ago COPY /app/devops-info-service /app/devops-in… 4.72MB buildkit.dockerfile.v0 + 14 minutes ago COPY /etc/ssl/certs/ca-certificates.crt /etc… 238kB buildkit.dockerfile.v0 +``` + +### Runtime Testing +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker run -d --name devops-go -p 5000:5000 devops-info-service:go +0d2cb36ff83b03fca8090248aa3a6fe1beba1e879617a8dd2e5a9c3a588e8c1c + +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ curl http://localhost:5000/ +{"service":{"name":"devops-info-service","version":"1.0.0","description":"DevOps course info service","framework":"Go"},"system":{"hostname":"0d2cb36ff83b","platform":"linux","platform_version":"Linux Kernel","architecture":"amd64","cpu_count":12,"go_version":"go1.21.13"},"runtime":{"uptime_seconds":17,"uptime_human":"0 hours, 0 minutes","current_time":"2026-01-31T20:20:36Z","timezone":"UTC"},"request":{"client_ip":"172.17.0.1:50750","user_agent":"curl/8.18.0","method":"GET","path":"/"},"endpoints":[{"path":"/","method":"GET","description":"Service information"},{"path":"/health","method":"GET","description":"Health check"}]} + +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ curl http://localhost:5000/health +{"status":"healthy","timestamp":"2026-01-31T20:21:20Z","uptime_seconds":61} + +s3rap1s in ~/devops/DevOps-Core-Course/app_go on lab01 ● ● λ docker logs devops-go +2026/01/31 20:20:19 Starting DevOps Info Service on 0.0.0.0:5000 +2026/01/31 20:20:34 Health check from 172.17.0.1:50742 +2026/01/31 20:20:36 Request: GET / from 172.17.0.1:50750 +2026/01/31 20:21:13 404 Not Found: /helath +2026/01/31 20:21:20 Health check from 172.17.0.1:38286 +``` + +## Technical Explanation of Each Stage + +### Stage 1: Builder (`golang:1.21-alpine`) +**Purpose:** Provide complete compilation environment + +**Key operations:** +1. **Base setup:** Alpine Linux with Go 1.21 toolchain +2. **Dependencies:** Install git and CA certificates +3. **Module management:** Download Go dependencies with caching +4. **Compilation:** Build optimized static binary with: + - `CGO_ENABLED=0`: Disable CGO for pure Go static binary + - `-ldflags="-w -s"`: Strip debug symbols and DWARF tables + - `-extldflags '-static'`: Force static linking + - `-a`: Force rebuilding of packages + +**Output:** `/app/devops-info-service` (6.9MB static binary) + +### Stage 2: Runtime (`scratch`) +**Purpose:** Provide minimal production runtime + +**Key operations:** +1. **Base image:** `scratch` (empty filesystem) +2. **Binary copy:** Transfer compiled binary from builder +3. **Certificates:** Copy CA certificates for TLS/HTTPS support +4. **Configuration:** Set environment variables and expose port + +**Output:** Production-ready container image (7.16MB) + +## Security Benefits of Smaller Images + +### Specific Security Advantages +1. **No shell:** Cannot execute arbitrary commands or spawn shells +2. **Immutable filesystem:** Only the binary exists, cannot be modified +3. **Minimal CVE surface:** No packages = no vulnerabilities to patch +4. **Isolated execution:** Runs as PID 1 with no background services +5. **Resource limits:** Minimal memory/cpu usage reduces DoS impact + +## Why FROM scratch? Trade-offs and Decisions + +### Why `scratch` Was Chosen +```dockerfile +FROM scratch # Instead of alpine, distroless, or other minimal bases +``` + +**Advantages:** +1. **Absolute minimalism:** 0MB base, only binary + certs +2. **Maximum security:** No OS, no shell, no utilities +3. **Great for static binaries:** Go compiles to fully static executables + +### Trade-offs Considered +| Base Image | Size | Pros | Cons | Decision | +|------------|------|------|------|----------| +| **scratch** | 0MB | Max security, minimal size | No debugging tools, no shell | ✅ **Chosen** | +| **alpine** | 5.5MB | Shell for debugging, small | Larger, more attack surface | Rejected | +| **distroless** | 20MB | Secure | Much larger than scratch | Rejected | + +## Analysis of Size Reduction and Why It Matters + +### Why Size Reduction Matters +1. **Cost efficiency:** 98% reduction in storage and bandwidth costs +2. **Deployment speed:** Images deploy in seconds instead of minutes +3. **Developer productivity:** Faster CI/CD pipeline execution +4. **Environmental impact:** Less energy for storage and transfer +5. **Edge computing:** Suitable for resource-constrained environments + +## Challenges and Solutions + +### Challenge: Certificate Management with `scratch` +**Problem:** `scratch` has no CA certificates, breaking HTTPS calls from the application. + +**Solution:** Copy certificates from builder stage: +```dockerfile +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +``` + +## What I Learned + +1. **Multi-stage builds** are transformative for compiled languages, enabling near-zero runtime overhead +2. **Static compilation** is powerful but requires careful dependency management +3. **Security through minimalism** is achievable with `scratch` base images +4. **Trade-offs exist** between debuggability and security/minimalism 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..6acabfa3cf 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..f905599cf0 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..f4dd6737db 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..307ce0d1c5 --- /dev/null +++ b/app_go/go.mod @@ -0,0 +1,3 @@ +module devops-info-service + +go 1.21 diff --git a/app_go/main.go b/app_go/main.go new file mode 100644 index 0000000000..7b0e355b6c --- /dev/null +++ b/app_go/main.go @@ -0,0 +1,205 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "runtime" + "time" +) + +// Data structures +type ServiceInfo struct { + Service Service `json:"service"` + System System `json:"system"` + Runtime Runtime `json:"runtime"` + Request Request `json:"request"` + Endpoints []Endpoint `json:"endpoints"` +} + +type Service struct { + Name string `json:"name"` + Version string `json:"version"` + Description string `json:"description"` + Framework string `json:"framework"` +} + +type System struct { + Hostname string `json:"hostname"` + Platform string `json:"platform"` + PlatformVersion string `json:"platform_version"` + Architecture string `json:"architecture"` + CPUCount int `json:"cpu_count"` + GoVersion string `json:"go_version"` +} + +type Runtime struct { + UptimeSeconds int `json:"uptime_seconds"` + UptimeHuman string `json:"uptime_human"` + CurrentTime string `json:"current_time"` + Timezone string `json:"timezone"` +} + +type Request struct { + ClientIP string `json:"client_ip"` + UserAgent string `json:"user_agent"` + Method string `json:"method"` + Path string `json:"path"` +} + +type Endpoint struct { + Path string `json:"path"` + Method string `json:"method"` + Description string `json:"description"` +} + +type HealthResponse struct { + Status string `json:"status"` + Timestamp string `json:"timestamp"` + UptimeSeconds int `json:"uptime_seconds"` +} + +// Global variables +var startTime time.Time + +func init() { + startTime = time.Now() +} + +// Helper functions +func getSystemInfo() System { + hostname, err := os.Hostname() + if err != nil { + hostname = "unknown" + } + + return System{ + Hostname: hostname, + Platform: runtime.GOOS, + PlatformVersion: getOSVersion(), + Architecture: runtime.GOARCH, + CPUCount: runtime.NumCPU(), + GoVersion: runtime.Version(), + } +} + +func getOSVersion() string { + switch runtime.GOOS { + case "linux": + return "Linux Kernel" + case "darwin": + return "macOS" + case "windows": + return "Windows" + default: + return runtime.GOOS + } +} + +func getUptime() (int, string) { + duration := time.Since(startTime) + seconds := int(duration.Seconds()) + + hours := seconds / 3600 + minutes := (seconds % 3600) / 60 + + return seconds, fmt.Sprintf("%d hours, %d minutes", hours, minutes) +} + +func getCurrentTime() string { + return time.Now().UTC().Format(time.RFC3339) +} + +// HTTP handlers +func mainHandler(w http.ResponseWriter, r *http.Request) { + // Handle only root path + if r.URL.Path != "/" { + notFoundHandler(w, r) + return + } + + systemInfo := getSystemInfo() + uptimeSeconds, uptimeHuman := getUptime() + + response := ServiceInfo{ + Service: Service{ + Name: "devops-info-service", + Version: "1.0.0", + Description: "DevOps course info service", + Framework: "Go", + }, + System: systemInfo, + Runtime: Runtime{ + UptimeSeconds: uptimeSeconds, + UptimeHuman: uptimeHuman, + CurrentTime: getCurrentTime(), + Timezone: "UTC", + }, + Request: Request{ + ClientIP: r.RemoteAddr, + UserAgent: r.UserAgent(), + Method: r.Method, + Path: r.URL.Path, + }, + Endpoints: []Endpoint{ + {Path: "/", Method: "GET", Description: "Service information"}, + {Path: "/health", Method: "GET", Description: "Health check"}, + }, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + + log.Printf("Request: %s %s from %s", r.Method, r.URL.Path, r.RemoteAddr) +} + +func healthHandler(w http.ResponseWriter, r *http.Request) { + uptimeSeconds, _ := getUptime() + + response := HealthResponse{ + Status: "healthy", + Timestamp: getCurrentTime(), + UptimeSeconds: uptimeSeconds, + } + + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(response) + + log.Printf("Health check from %s", r.RemoteAddr) +} + +func notFoundHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusNotFound) + json.NewEncoder(w).Encode(map[string]string{ + "error": "Not Found", + "message": "Endpoint does not exist", + }) + + log.Printf("404 Not Found: %s", r.URL.Path) +} + +// Main function +func main() { + // Read environment variables + port := os.Getenv("PORT") + if port == "" { + port = "5000" + } + + host := os.Getenv("HOST") + if host == "" { + host = "0.0.0.0" + } + + // Setup HTTP routes + http.HandleFunc("/", mainHandler) + http.HandleFunc("/health", healthHandler) + + // Start server + addr := fmt.Sprintf("%s:%s", host, port) + log.Printf("Starting DevOps Info Service on %s", addr) + log.Fatal(http.ListenAndServe(addr, nil)) +} \ No newline at end of file diff --git a/app_python/.dockerignore b/app_python/.dockerignore new file mode 100644 index 0000000000..df2ec2cfb1 --- /dev/null +++ b/app_python/.dockerignore @@ -0,0 +1,39 @@ +# Python +__pycache__/ +*.py[cod] +*.pyo +*.so +.Python +venv/ +env/ +.venv/ +.env +*.log + +# Git +.git/ +.gitignore + +# IDE +.vscode/ +.idea/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Documentation +*.md +docs/ +README.md + +# Tests +tests/ +test*.py + +# Docker +Dockerfile* +docker-compose* +.dockerignore \ No newline at end of file diff --git a/app_python/.gitignore b/app_python/.gitignore new file mode 100644 index 0000000000..4de420a8f7 --- /dev/null +++ b/app_python/.gitignore @@ -0,0 +1,12 @@ +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store \ No newline at end of file diff --git a/app_python/Dockerfile b/app_python/Dockerfile new file mode 100644 index 0000000000..3de1bb9253 --- /dev/null +++ b/app_python/Dockerfile @@ -0,0 +1,56 @@ +# Dockerfile for Python Application with Virtual Environment +FROM python:3.13-slim AS builder + +# Installing system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* + +# Installing the working directory +WORKDIR /app + +# Creating a virtual environment +RUN python -m venv /opt/venv + +# Activating the virtual environment +ENV PATH="/opt/venv/bin:$PATH" + +# Copying requirements file +COPY requirements.txt . +RUN pip install --no-cache-dir --upgrade pip && \ + pip install --no-cache-dir -r requirements.txt + +# Final stage: Copying the virtual environment and application code +FROM python:3.13-slim + +# Creating non-root user and group +RUN groupadd -r appgroup && useradd -r -g appgroup appuser + +# Setting the working directory +WORKDIR /app + +# Copying the virtual environment from the builder stage +COPY --from=builder /opt/venv /opt/venv + +# Copying the application code +COPY . . + +# Setting permissions for the application directory +RUN chown -R appuser:appgroup /app && chmod -R 755 /app + +# Switching to non-root user +USER appuser + +# Setting the PATH to include the virtual environment +ENV PATH="/opt/venv/bin:$PATH" + +# Opening the port for the application +EXPOSE 5000 + +# Setting environment variables +ENV HOST=0.0.0.0 +ENV PORT=5000 +ENV PYTHONUNBUFFERED=1 + +# Running the application +CMD ["python", "app.py"] \ No newline at end of file diff --git a/app_python/README.md b/app_python/README.md new file mode 100644 index 0000000000..5442b06fea --- /dev/null +++ b/app_python/README.md @@ -0,0 +1,176 @@ +# DevOps Info Service + +## Overview +A Python-based web service designed to furnish details about itself and its operational environment. This service serves as a foundation for subsequent experiments in containerization, continuous integration and continuous deployment (CI/CD), monitoring, and deployment processes. + +## CI/CD Pipeline +![Python CI/CD](https://github.com/s3rap1s/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg) + +### Overview +This project uses GitHub Actions for continuous integration and deployment. The pipeline includes: + +1. **Code Quality Checks** + - Linting with flake8 + - Code formatting with black + - Security scanning with Snyk + +2. **Testing** + - Unit tests with pytest + - Test coverage tracking + - 90%+ code coverage requirement + +3. **Docker Build & Deployment** + - Multi-stage Docker builds + - Automated tagging with Calendar Versioning + - Push to Docker Hub + +### Versioning Strategy +We use **Calendar Versioning (CalVer)** with the format `YYYY.MM.MICRO`: + +- **YYYY.MM.DD** - Specific date of build +- **YYYY.MM.MICRO** - Version with micro release number +- **latest** - Most recent stable build + + +## Prerequisites +- Python 3.11 or higher +- pip (Python Package manager) + +## Installation +1. Clone repository: +```bash +# Clone the project +git clone https://github.com/s3rap1s/DevOps-Core-Course.git +cd DevOps-Core-Course/app_python + +# Create virtual environment +python3 -m venv venv +source venv/bin/activate # on Linux / macOs or .\venv\Scripts\Activate.ps1 on windows + +# Install dependencies +pip install -r requirements.txt +``` + +## Running the Application + +```bash +# Default configuration +python app.py + +# With custom port +PORT=8080 python app.py + +# With custom port and host +HOST=127.0.0.1 PORT=3000 python app.py +``` + +## Testing the Application +```bash +pytest # Run all tests +pytest --cov=app --cov-report=term-missing # Run with coverage +``` + +## Docker + +This application is containerized and available as a Docker image. + +### Building the Image Locally + +```bash +docker build -t devops-info-service:latest . +``` + +### Running a Container + +```bash +# Run with default port mapping +docker run -d -p 5000:5000 devops-info-service:latest + +# Run with custom port +docker run -d -p 8080:5000 devops-info-service:latest + +# Run with environment variables +docker run -d -p 3000:3000 -e PORT=3000 -e HOST=0.0.0.0 devops-info-service:latest +``` + +### Pulling from Docker Hub + +```bash +# Pull the latest version +docker pull your-username/devops-info-service:latest + +# Run pulled image +docker run -d -p 5000:5000 your-username/devops-info-service:latest +``` + +### Environment Variables in Docker +When running in Docker, you can pass environment variables using the `-e` flag: + +```bash +docker run -d -p 5000:5000 \ + -e HOST=0.0.0.0 \ + -e PORT=5000 \ + -e DEBUG=false \ + devops-info-service:latest +``` + +## API Endpoints + +### `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"} + ] +} +``` + +### `GET /health` + +Simple health endpoint for monitoring: + +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` + + +## Configuration + +| Variable | Default | Description | +| -------- | --------- | ---------------------------- | +| `HOST` | `0.0.0.0` | Network interface to bind | +| `PORT` | `5000` | Port to listen on | +| `DEBUG` | `false` | Enable debug mode | diff --git a/app_python/app.py b/app_python/app.py new file mode 100644 index 0000000000..919477653a --- /dev/null +++ b/app_python/app.py @@ -0,0 +1,132 @@ +""" +DevOps Info Service +Main application module +""" + +import os +import socket +import platform +import logging +from datetime import datetime, timezone +from flask import Flask, jsonify, request + +# Flask app initialization +app = Flask(__name__) + +# Configuration via environment variables +HOST = os.getenv("HOST", "0.0.0.0") +PORT = int(os.getenv("PORT", 5000)) +DEBUG = os.getenv("DEBUG", "False").lower() == "true" + +# Application startup time +START_TIME = datetime.now(timezone.utc) + +# Setting up logging +logging.basicConfig( + level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s" +) +logger = logging.getLogger(__name__) + + +def get_system_info(): + """Collecting information about the system. + + Returns: + dict: System configuration + """ + return { + "hostname": socket.gethostname(), + "platform": platform.system(), + "platform_version": platform.release(), + "architecture": platform.machine(), + "cpu_count": os.cpu_count() or 0, + "python_version": platform.python_version(), + } + + +def get_uptime(): + """Calculating the running time of the application. + + Returns: + dict: Uptime in seconds and human-readable format + """ + 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"} + + +@app.route("/") +def index(): + """The main endpoint - Information about the service and the system.""" + client_ip = request.remote_addr + user_agent = request.headers.get("User-Agent", "Unknown") + + # Forming a response + response = { + "service": { + "name": "devops-info-service", + "version": "1.0.0", + "description": "DevOps course info service", + "framework": "Flask", + }, + "system": get_system_info(), + "runtime": { + "uptime_seconds": get_uptime()["seconds"], + "uptime_human": get_uptime()["human"], + "current_time": datetime.now(timezone.utc).isoformat(), + "timezone": "UTC", + }, + "request": { + "client_ip": client_ip, + "user_agent": user_agent, + "method": request.method, + "path": request.path, + }, + "endpoints": [ + {"path": "/", "method": "GET", "description": "Service information"}, + {"path": "/health", "method": "GET", "description": "Health check"}, + ], + } + logger.info(f"Request: {request.method} {request.path} from {client_ip}") + return jsonify(response) + + +@app.route("/health") +def health(): + """Endpoint for health check.""" + response = { + "status": "healthy", + "timestamp": datetime.now(timezone.utc).isoformat(), + "uptime_seconds": get_uptime()["seconds"], + } + logger.debug(f"Health check: {response}") + return jsonify(response), 200 + + +@app.errorhandler(404) +def not_found(error): + """Error handler 404.""" + logger.warning(f"404 Not Found: {request.path}") + return jsonify({"error": "Not Found", "message": "Endpoint does not exist"}), 404 + + +@app.errorhandler(500) +def internal_error(error): + """Error handler 500.""" + logger.error(f"500 Internal Server Error: {str(error)}") + return ( + jsonify( + { + "error": "Internal Server Error", + "message": "An unexpected error occurred", + } + ), + 500, + ) + + +if __name__ == "__main__": + logger.info(f"Starting DevOps Info Service on {HOST}:{PORT} (debug={DEBUG})") + app.run(host=HOST, port=PORT, debug=DEBUG) diff --git a/app_python/docs/LAB01.md b/app_python/docs/LAB01.md new file mode 100644 index 0000000000..90f920df57 --- /dev/null +++ b/app_python/docs/LAB01.md @@ -0,0 +1,271 @@ +# Lab 1 — DevOps Info Service: Web Application Development + +## Framework Selection + +### My Choice: Flask + +I selected **Flask** as the web framework for this DevOps Info Service. Here's why: + +**Comparison Table:** +| Criteria | Flask | FastAPI | Django | +|----------|-------|---------|--------| +| Learning Curve | Very low | Moderate | Steep | +| Development Speed | High | High | Medium | +| Built-in Features | Minimal | Moderate | Extensive | +| Auto-documentation | Requires extensions | Built-in (OpenAPI) | Requires extensions | +| Performance | Good | Excellent (async) | Good | +| Complexity | Low | Medium | High | +| **Choice for Lab 1** | **✓** | | | + +**Justification:** +Flask is a lightweight, minimalistic framework that perfectly suits our simple service with only two endpoints. For a DevOps monitoring tool foundation, we don't need the complexity of Django or the async capabilities of FastAPI yet. Flask allows rapid development with clean, understandable code, making it ideal for this educational project. Its simplicity aligns with the Unix philosophy of "do one thing well" - in this case, serve system information via HTTP. + +## Best Practices Applied + +### 1. Clean Code Organization +```python +# Clear imports grouping +import os +import socket +import platform +import logging +from datetime import datetime, timezone +from flask import Flask, jsonify, request + +# Descriptive function names with docstrings +def get_system_info(): + """Collecting information about the system. + Returns: + dict: System configuration + """ + return { + 'hostname': socket.gethostname(), + 'platform': platform.system(), + # ... more fields + } +``` + +**Importance:** Clean organization makes code maintainable, readable, and easier to debug. Following PEP 8 ensures consistency across Python projects. + +### 2. Comprehensive Error Handling +```python +@app.errorhandler(404) +def not_found(error): + """Error handler 404.""" + logger.warning(f"404 Not Found: {request.path}") + return jsonify({ + 'error': 'Not Found', + 'message': 'Endpoint does not exist' + }), 404 + +@app.errorhandler(500) +def internal_error(error): + """Error handler 500.""" + logger.error(f"500 Internal Server Error: {str(error)}") + return jsonify({ + 'error': 'Internal Server Error', + 'message': 'An unexpected error occurred' + }), 500 +``` + +**Importance:** Proper error handling prevents application crashes and provides meaningful feedback to API consumers. Each error type returns appropriate HTTP status codes and structured JSON responses. + +### 3. Structured Logging +```python +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +logger.info(f'Starting DevOps Info Service on {HOST}:{PORT} (debug={DEBUG})') +logger.info(f"Request: {request.method} {request.path} from {client_ip}") +``` + +**Importance:** Logging provides visibility into application behavior, helps with debugging in production, and allows monitoring of API usage patterns. + +### 4. Configuration via Environment Variables +```python +HOST = os.getenv('HOST', '0.0.0.0') +PORT = int(os.getenv('PORT', 5000)) +DEBUG = os.getenv('DEBUG', 'False').lower() == 'true' +``` + +**Importance:** Following the 12-factor app methodology, configuration via environment variables makes the application portable across different environments (development, testing, production) without code changes. + +### 5. Version-Pinned Dependencies +```txt +# Web framework +Flask==3.1.0 + +# Virtual environment for python +python-dotenv==1.0.1 +``` + +**Importance:** Pinning exact versions ensures consistent behavior across all deployments and prevents "works on my machine" issues due to dependency version mismatches. + +### 6. Git Ignore for Development Artifacts +```gitignore +# Python +__pycache__/ +*.py[cod] +venv/ +*.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +``` + +**Importance:** Prevents accidental commits of generated files, virtual environments, and sensitive data, keeping the repository clean and focused on source code. + +## API Documentation + +### Endpoint 1: `GET /` + +**Description:** Returns comprehensive service information, system details, runtime data, and request metadata. + +**Request:** +```bash +curl http://localhost:5000/ +``` + +**Response (example):** +```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"} + ] +} +``` + +### Endpoint 2: `GET /health` + +**Description:** Health check endpoint for monitoring system. Always returns HTTP 200 with service status. + +**Request:** +```bash +curl http://localhost:5000/health +``` + +**Response (example):** +```json +{ + "status": "healthy", + "timestamp": "2024-01-15T14:30:00.000Z", + "uptime_seconds": 3600 +} +``` + +### Testing Commands + +1. **Basic endpoint test:** + ```bash + curl http://localhost:5000/ + ``` + +2. **Health check test:** + ```bash + curl http://localhost:5000/health + ``` + +3. **Pretty-printed output:** + ```bash + curl http://localhost:5000/ | jq . + ``` + +4. **Custom configuration:** + ```bash + PORT=8080 python app.py + curl http://localhost:8080/health + ``` + +5. **Error simulation:** + ```bash + curl http://localhost:5000/nonexistent + # Should return 404 error + ``` + +## Testing Evidence + +### Main endpoint: +![Main Endpoint](screenshots/01-main-endpoint.png) + +### Health check: +![Health Check](screenshots/02-health-check.png) + +### Formatted output: +![Formatted output](screenshots/03-formatted-output.png) + + +## Challenges & Solutions + +### Challenge 1: Timezone-Aware Timestamps +**Problem:** `datetime.now()` without timezone creates naive datetime objects, which can cause issues with serialization and timezone calculations. + +**Solution:** Used `timezone.utc` consistently: +```python +from datetime import datetime, timezone +START_TIME = datetime.now(timezone.utc) +# ... +datetime.now(timezone.utc).isoformat() +``` + +### Challenge 2: Logging Configuration +**Problem:** Determining the appropriate log level and format for different types of messages. + +**Solution:** Configured logging with INFO level for normal operations, DEBUG for health checks, and WARNING/ERROR for error handlers: +```python +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +logger.info(f"Request: {request.method} {request.path} from {client_ip}") +logger.debug(f"Health check: {response}") +logger.warning(f"404 Not Found: {request.path}") +``` + +### Challenge 3: CPU Count Handling +**Problem:** `os.cpu_count()` can return None on some systems or when the count cannot be determined. + +**Solution:** Added a fallback value: +```python +'cpu_count': os.cpu_count() or 0 +``` + +## GitHub Community +### Why starring repositories matters in open source: +Starring repositories serves as both a bookmarking tool for personal reference and a public endorsement that helps projects gain visibility, attracting more contributors and showing appreciation to maintainers for their work. + +### How following developers helps in team projects and professional growth: +Following developers enables you to stay updated on their projects and insights, fostering collaboration and knowledge sharing that accelerates team productivity and your own skill development in the tech community. \ No newline at end of file diff --git a/app_python/docs/LAB02.md b/app_python/docs/LAB02.md new file mode 100644 index 0000000000..aad9c97090 --- /dev/null +++ b/app_python/docs/LAB02.md @@ -0,0 +1,213 @@ +# Lab 2 — Containerization with Docker + +## Docker Best Practices Applied + +### 1. Multi-Stage Build +**Implementation:** +```dockerfile +FROM python:3.13-slim AS builder +# ... build stage with dependencies +FROM python:3.13-slim +COPY --from=builder /opt/venv /opt/venv +``` + +**Importance:** Separates the build environment from the runtime environment, reducing the final image size by excluding build tools and intermediate files. + +### 2. Non-Root User +**Implementation:** +```dockerfile +RUN groupadd -r appgroup && useradd -r -g appgroup appuser +USER appuser +``` + +**Importance:** Enhances security by following the principle of least privilege, minimizing potential damage if the container is compromised. + +### 3. Layer Caching Optimization +**Implementation:** +```dockerfile +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt +COPY . . +``` + +**Importance:** Docker caches layers. By copying requirements.txt first and installing dependencies, this layer is cached and only rebuilt when requirements change, speeding up subsequent builds. + +### 4. .dockerignore File +**Implementation:** Created a comprehensive `.dockerignore` file to exclude unnecessary files (development artifacts, IDE files, git, etc.). + +**Importance:** Reduces build context size, speeding up the build process and preventing sensitive or irrelevant files from being included. + +### 5. Specific Base Image Version +**Implementation:** `python:3.13-slim` (instead of `python:latest` or `python:3.13`) + +**Importance:** Ensures reproducible builds and avoids unexpected changes from base image updates. + +### 6. Clean Package Installation +**Implementation:** +```dockerfile +RUN pip install --no-cache-dir -r requirements.txt +``` + +**Importance:** The `--no-cache-dir` flag prevents pip from caching packages, reducing image size. + +### 7. System Dependency Cleanup +**Implementation:** +```dockerfile +RUN apt-get update && apt-get install -y --no-install-recommends \ + gcc \ + && rm -rf /var/lib/apt/lists/* +``` + +**Importance:** Removes the package lists after installation, reducing image size and keeping the image clean. + +### 8. Virtual Environment Isolation +**Implementation:** +```dockerfile +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" +``` + +**Importance:** Isolates application dependencies from the system Python, avoiding conflicts and making the environment reproducible. + +## Image Information & Decisions + +### Base Image Choice +**Selected:** `python:3.13-slim` + +**Justification:** +- **slim variant** provides a minimal Python runtime without unnesecary extra tools +- **Specific version (3.13)** ensures reproducibility and avoids breaking changes from future updates +- **Alternative considered:** `python:3.13-alpine` (about 45MB) was rejected due to potential compatibility issues with some Python packages that require compiled binaries. + +### Final Image Size +- **Final image size:** ~199MB + +**Assessment:** The image size is reasonable for a Python application. The multi-stage build helps keep it minimal by excluding build tools. Further reduction could be achieved by using Alpine, but at the cost of potential compatibility issues. + +### Layer Structure +The layer structure (from bottom to top): +1. **Base image layer:** `python:3.13-slim` +2. **System dependencies layer:** Installs gcc (builder stage) +3. **Python dependencies layer:** Creates virtual environment and installs packages (cached separately) +4. **Application code layer:** Copies the rest of the application +5. **Configuration layer:** Sets permissions, user, environment variables, and command + +### Optimization Choices Made +1. **Multi-stage build:** Separates build and runtime, removing build tools from final image. +2. **Dependency layer caching:** Requirements are installed in a separate layer that caches well. +3. **Cleanup:** Removal of apt lists and pip cache. +4. **Non-root user:** Added for security without significant overhead. +5. **Virtual environment:** Ensures dependency isolation and easier path management. + +## Build & Run Process + +### Complete Terminal Output from Build Process +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ docker build -t devops-info-service:python . +[+] Building 2.1s (17/17) FINISHED docker:default + => [internal] load build definition from Dockerfile 0.0s + => => transferring dockerfile: 1.41kB 0.0s + => [internal] load metadata for docker.io/library/python:3.13-slim 1.9s + => [auth] library/python:pull token for registry-1.docker.io 0.0s + => [internal] load .dockerignore 0.0s + => => transferring context: 321B 0.0s + => [internal] load build context 0.0s + => => transferring context: 63B 0.0s + => [builder 1/6] FROM docker.io/library/python:3.13-slim@sha256:51e1a0a317fdb6e170dc791bbeae63fac5272c82f43958ef74a34e170c6f8b18 0.0s + => => resolve docker.io/library/python:3.13-slim@sha256:51e1a0a317fdb6e170dc791bbeae63fac5272c82f43958ef74a34e170c6f8b18 0.0s + => CACHED [stage-1 2/6] RUN groupadd -r appgroup && useradd -r -g appgroup appuser 0.0s + => CACHED [stage-1 3/6] WORKDIR /app 0.0s + => CACHED [builder 2/6] RUN apt-get update && apt-get install -y --no-install-recommends gcc && rm -rf /var/lib/apt/lists/* 0.0s + => CACHED [builder 3/6] WORKDIR /app 0.0s + => CACHED [builder 4/6] RUN python -m venv /opt/venv 0.0s + => CACHED [builder 5/6] COPY requirements.txt . 0.0s + => CACHED [builder 6/6] RUN pip install --no-cache-dir --upgrade pip && pip install --no-cache-dir -r requirements.txt 0.0s + => CACHED [stage-1 4/6] COPY --from=builder /opt/venv /opt/venv 0.0s + => CACHED [stage-1 5/6] COPY . . 0.0s + => CACHED [stage-1 6/6] RUN chown -R appuser:appgroup /app && chmod -R 755 /app 0.0s + => exporting to image 0.1s + => => exporting layers 0.0s + => => exporting manifest sha256:d9c4d5bbff6c71a63a4664b6176a7cf8d5738ea116827f910b356d290148a06f 0.0s + => => exporting config sha256:0cea5c6e8fea36e6da7112c67af628d9a5ecaca41edfd9f12b32a6ebf2f6c9b2 0.0s + => => exporting attestation manifest sha256:45c2bd60bc20c64827da237a0a245707051321a55ecbac6b03d1001102cc86d2 0.0s + => => exporting manifest list sha256:4b08b6e2f06333a4d7781a83bcebcdb3303c99ef310af40ae3e0e85e2a020d3e 0.0s + => => naming to docker.io/library/devops-info-service:python 0.0s + => => unpacking to docker.io/library/devops-info-service:python +``` + +### Terminal Output Showing Container Running +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ docker run -d --name devops-python -p 5000:5000 devops-info-service:python +234bff345b8f2c930681218fd9536b405c131b375a4d382a0b28a4f77d067b2c + +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ docker logs devops-python +2026-01-31 18:45:04,632 - __main__ - INFO - Starting DevOps Info Service on 0.0.0.0:5000 (debug=False) + * Serving Flask app 'app' + * Debug mode: off +2026-01-31 18:45:04,639 - werkzeug - INFO - WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead. + * Running on all addresses (0.0.0.0) + * Running on http://127.0.0.1:5000 + * Running on http://172.17.0.2:5000 +2026-01-31 18:45:04,639 - werkzeug - INFO - Press CTRL+C to quit +``` + +### Terminal Output from Testing Endpoints +```bash +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ curl http://localhost:5000/ +{"endpoints":[{"description":"Service information","method":"GET","path":"/"},{"description":"Health check","method":"GET","path":"/health"}],"request":{"client_ip":"172.17.0.1","method":"GET","path":"/","user_agent":"curl/8.18.0"},"runtime":{"current_time":"2026-01-31T18:45:48.101588+00:00","timezone":"UTC","uptime_human":"0 hours, 0 minutes","uptime_seconds":43},"service":{"description":"DevOps course info service","framework":"Flask","name":"devops-info-service","version":"1.0.0"},"system":{"architecture":"x86_64","cpu_count":12,"hostname":"234bff345b8f","platform":"Linux","platform_version":"6.18.3-arch1-1","python_version":"3.13.11"}} + +s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab01 ● λ curl http://localhost:5000/health +{"status":"healthy","timestamp":"2026-01-31T18:45:54.984116+00:00","uptime_seconds":50} +``` + +### Docker Hub Repository URL +**URL:** https://hub.docker.com/repository/docker/s3rap1s/devops-info-service/general + +## Technical Analysis + +### Why Does Your Dockerfile Work the Way It Does? +The Dockerfile uses a multi-stage build to separate concerns: +1. **Builder stage:** Installs system dependencies and Python packages in a virtual environment. +2. **Runtime stage:** Copies only the virtual environment and application code, then sets up a secure non-root user. + +This approach ensures that the final image contains only what's necessary to run the application, improving security and reducing size. + +### What Would Happen If You Changed the Layer Order? +If the layer order were changed, then every time any file in the project changes, the `COPY` layer would be invalidated, causing the `RUN` layer to also be invalidated (since Docker caches layers based on the previous layer's hash). This would result in a full reinstallation of dependencies on every code change, significantly slowing down builds. + +### What Security Considerations Did You Implement? +1. **Non-root user:** The application runs as a dedicated user with minimal privileges. +2. **Minimal base image:** The `slim` variant reduces attack surface. +3. **Virtual environment isolation:** Prevents dependency conflicts and limits access. +4. **No unnecessary services:** Only the Python application runs in the container. +5. **Cleanup of package lists:** Removes sensitive data and reduces image size. +6. **Explicit port exposure:** Only port 5000 is exposed. + +### How Does .dockerignore Improve Your Build? +The `.dockerignore` file excludes: +- Development artifacts (`.git`, `__pycache__`, `.venv`) +- IDE files (`.vscode`, `.idea`) +- Logs and temporary files +- Documentation and tests (not needed at runtime) + +This reduces the build context sent to the Docker daemon, resulting in: +- **Faster build** - smaller context to transfer +- **Smaller image sizes** - unnecessary files aren't included +- **Improved security** - sensitive files like secrets aren't accidentally included + +## Challenges & Solutions + +### Challenge: Port Configuration Inside Container +**Problem:** The application inside the container was binding to `localhost`, making it inaccessible from the host. + +**Solution:** Set the `HOST` environment variable to `0.0.0.0` in the Dockerfile to bind to all interfaces: +```dockerfile +ENV HOST=0.0.0.0 +``` + +## What I Learned + +1. **Layer caching** is crucial for efficient Docker builds +2. **Security** must be considered from the start +3. **`.dockerignore`** is as important as `.gitignore` for Docker projects, affecting both performance and security +4. **Reproducibility** requires pinning specific versions of base images and dependencies diff --git a/app_python/docs/LAB03.md b/app_python/docs/LAB03.md new file mode 100644 index 0000000000..8201a6fa31 --- /dev/null +++ b/app_python/docs/LAB03.md @@ -0,0 +1,200 @@ +# Lab 3 — Continuous Integration (CI/CD) + +## Overview + +### Testing Framework Choice +I selected **pytest** as the testing framework for the following reasons: + +1. **Simple and intuitive syntax** - easy to write and read tests +2. **Rich feature set** - fixtures, parameterization, and plugins +3. **Active community** - extensive documentation and support +4. **CI/CD integration** - seamless integration with GitHub Actions + +### Versioning Strategy +**Calendar Versioning (CalVer)** in the format `YYYY.MM.MICRO` + +**Why CalVer was chosen:** +1. **DevOps service** with frequent updates and rare breaking changes +2. **Stable API** - backward compatible changes only +3. **Date clarity** - immediately shows image freshness +4. **Flexibility** - micro version allows multiple builds per day + +### CI Workflow Triggers +- **Push** to branches: master, lab03 (only when app_python/ files change) +- **Pull Request** to branches: master (for code review) +- **Path filters** - workflow only runs when relevant files are modified + +## Workflow Evidence + +### Successful Workflow Run +[Link to successful workflow run](https://github.com/s3rap1s/DevOps-Core-Course/actions/runs/21864360584/) + +### Terminal Output from Local Testing +```bash +(venv) s3rap1s in ~/devops/DevOps-Core-Course/app_python on lab03 ● ● λ pytest --cov=app --cov-report=term-missing -v +======================================================================================================================== test session starts ======================================================================================================================== +platform linux -- Python 3.14.2, pytest-8.1.1, pluggy-1.6.0 -- /home/s3rap1s/devops/DevOps-Core-Course/app_python/venv/bin/python3 +cachedir: .pytest_cache +rootdir: /home/s3rap1s/devops/DevOps-Core-Course/app_python +plugins: cov-5.0.0 +collected 8 items + +tests/test_app.py::test_get_system_info PASSED [ 12%] +tests/test_app.py::test_get_uptime PASSED [ 25%] +tests/test_app.py::test_main_endpoint PASSED [ 37%] +tests/test_app.py::test_health_endpoint PASSED [ 50%] +tests/test_app.py::test_404_error PASSED [ 62%] +tests/test_app.py::test_different_user_agent PASSED [ 75%] +tests/test_app.py::test_json_structure_types PASSED [ 87%] +tests/test_app.py::test_health_response_structure PASSED [100%] + +---------- coverage: platform linux, python 3.14.2-final-0 ----------- +Name Stmts Miss Cover Missing +-------------------------------------- +app.py 44 4 91% 118-119, 131-132 +-------------------------------------- +TOTAL 44 4 91% + + +========================================================================================================================= 8 passed in 0.09s ========================================================================================================================= +``` + +### Docker Hub Images +- **Latest:** `s3rap1s/devops-info-service:latest` +- **Date-based:** `s3rap1s/devops-info-service:2026.02.10` +- **CalVer:** `username/devops-info-service:2026.02.3` + +**Docker Hub URL:** https://hub.docker.com/r/s3rap1s/devops-info-service + +## Best Practices Implemented + +### 1. Dependency Caching +- **Pip caching:** Saves ~40 seconds per workflow run +- **Docker layer caching:** Speeds up image builds by ~67% +- **Cache key strategy:** Based on dependency file hash for maximum efficiency + +### 2. Security Scanning with Snyk +- Integrated vulnerability scanning for Python dependencies +- Configured to fail only on "high" severity vulnerabilities +- Automated scanning in every CI run + +### 3. Path Filters +- Workflow only triggers when app_python/ files change +- Prevents unnecessary CI runs for documentation or other app changes +- Saves CI/CD minutes and resources + +### 4. Job Dependencies +- Docker build job depends on successful test completion +- Prevents pushing broken code to Docker Hub +- Ensures only tested code reaches production + +### 5. Docker Layer Caching +- Caches Docker build layers between workflow runs +- Significant performance improvement for multi-stage builds +- Uses GitHub Actions cache for persistence + +### 6. Multiple Docker Tags +- `latest` - for production deployments +- `YYYY.MM.DD` - specific date builds +- `YYYY.MM.MICRO` - CalVer versioning + +### 7. Fail Fast Strategy +- Stops workflow on first linting or testing failure +- Provides immediate feedback to developers +- Reduces resource consumption on failed builds + +## Key Decisions + +### Versioning Strategy: CalVer +**Why CalVer over SemVer?** +1. **Infrastructure service** - frequent updates without breaking API changes +2. **Time-based relevance** - date indicates service freshness +3. **Simpler management** - no need for manual version bumping +4. **Industry practice** - common for DevOps and infrastructure tools + +### Workflow Trigger Configuration +**Why these triggers?** +1. **Push to master** - Automate production deployments +2. **Pull requests** - Ensure code quality before merging +3. **Path filters** - Optimize CI resource usage +4. **Branch-specific logic** - Different behavior for feature branches vs main + +## Test Coverage Analysis + +### Current Coverage: 91% + +**What's covered:** +- All API endpoints (`GET /` and `GET /health`) +- Error handling (404 responses) +- JSON structure validation +- Data type checking +- Function-level unit tests + +**Coverage goal:** Maintain >85% coverage threshold + +## Challenges & Solutions + +### Challenge 1: Snyk Integration Complexity +**Problem:** Snyk dependenicy for python failed during installation +**Solution:** Used official Docker-container from Snyk, which has all needed instruments and has seamless connection in GitHub + + +## Performance Metrics + +### Workflow Execution Time +| Stage | Without Caching | With Caching | Improvement | +|-------|----------------|--------------|-------------| +| Dependency Installation | 45s | 5s | 89% | +| Docker Build | 60s | 20s | 67% | +| Total Workflow | 2m 30s | 1m 10s | 53% | + +### Resource Optimization +- **CI minutes saved:** ~50% per workflow run +- **Storage optimization:** Docker layer cache reduces image size +- **Network efficiency:** Cached dependencies reduce download time + +## Security Considerations + +### Snyk Scanning Results +**Configuration:** +- Severity threshold: High +- Scan type: Python dependencies +- Action on vulnerabilities: Warning only (doesn't fail build) + +**Findings:** +- No high severity vulnerabilities detected +- Regular monitoring ensures security updates + +### Docker Security Best Practices +1. **Non-root user** in Dockerfile +2. **Minimal base image** (python:3.13-slim) +3. **Regular vulnerability scanning** +4. **Immutable tags** for production deployments + +## Integration Points + +### Code Quality Tools +- **flake8** - Code linting and style checking +- **black** - Automatic code formatting +- **pytest** - Comprehensive testing framework + +### External Services +- **GitHub Actions** - CI/CD platform +- **Docker Hub** - Container registry +- **Snyk** - Security scanning +- **Git** - Version control and tagging + +### Screenshots +![CI/CD Workflow Success](screenshots/04-ci-success.png) +![Test Coverage Report](screenshots/05-test-coverage.png) + +## Conclusion + +This CI/CD implementation provides: +- **Automated testing** with 91% code coverage +- **Security scanning** with Snyk integration +- **Efficient Docker builds** with layer caching +- **Meaningful versioning** with CalVer strategy +- **Resource optimization** through dependency caching + +The pipeline ensures code quality, security, and reliable deployments while optimizing CI resource usage and providing clear feedback to developers. diff --git a/app_python/docs/screenshots/01-main-endpoint.png b/app_python/docs/screenshots/01-main-endpoint.png new file mode 100644 index 0000000000..c374142b86 Binary files /dev/null and b/app_python/docs/screenshots/01-main-endpoint.png differ diff --git a/app_python/docs/screenshots/02-health-check.png b/app_python/docs/screenshots/02-health-check.png new file mode 100644 index 0000000000..ae97ae06f3 Binary files /dev/null and b/app_python/docs/screenshots/02-health-check.png differ diff --git a/app_python/docs/screenshots/03-formatted-output.png b/app_python/docs/screenshots/03-formatted-output.png new file mode 100644 index 0000000000..0c99336f5c Binary files /dev/null and b/app_python/docs/screenshots/03-formatted-output.png differ diff --git a/app_python/docs/screenshots/04-ci-success.png b/app_python/docs/screenshots/04-ci-success.png new file mode 100644 index 0000000000..d4f73eb7e9 Binary files /dev/null and b/app_python/docs/screenshots/04-ci-success.png differ diff --git a/app_python/docs/screenshots/05-test-coverage.png b/app_python/docs/screenshots/05-test-coverage.png new file mode 100644 index 0000000000..a1c7a63cf5 Binary files /dev/null and b/app_python/docs/screenshots/05-test-coverage.png differ diff --git a/app_python/requirements.txt b/app_python/requirements.txt new file mode 100644 index 0000000000..f209ed3f6c --- /dev/null +++ b/app_python/requirements.txt @@ -0,0 +1,10 @@ +# Web framework +Flask==3.1.0 + +# Testing +pytest==8.1.1 +pytest-cov==5.0.0 +requests==2.31.0 + +# Virtual environment for python +python-dotenv==1.0.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..be62617d4e --- /dev/null +++ b/app_python/tests/__init__.py @@ -0,0 +1 @@ +# Unit tests (Lab 3) diff --git a/app_python/tests/test_app.py b/app_python/tests/test_app.py new file mode 100644 index 0000000000..c018bb570c --- /dev/null +++ b/app_python/tests/test_app.py @@ -0,0 +1,170 @@ +import pytest +import sys +import os + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) +from app import app + + +@pytest.fixture +def client(): + """Fixture for test client Flask""" + with app.test_client() as client: + yield client + + +def test_get_system_info(): + """Test of get_system_info()""" + from app import get_system_info + + info = get_system_info() + + assert isinstance(info, dict) + assert "hostname" in info + assert "platform" in info + assert "python_version" in info + assert isinstance(info["cpu_count"], int) + + +def test_get_uptime(): + """Test of get_uptime()""" + from app import get_uptime + + uptime = get_uptime() + + assert isinstance(uptime, dict) + assert "seconds" in uptime + assert "human" in uptime + assert isinstance(uptime["seconds"], int) + assert isinstance(uptime["human"], str) + + +def test_main_endpoint(client): + """Test of main endpoint GET /""" + response = client.get("/") + + # Status check + assert response.status_code == 200 + + # Json structure test + data = response.get_json() + + # Service structure test + assert "service" in data + assert data["service"]["name"] == "devops-info-service" + assert data["service"]["version"] == "1.0.0" + assert data["service"]["framework"] == "Flask" + + # System structure test + assert "system" in data + assert all( + key in data["system"] + for key in [ + "hostname", + "platform", + "platform_version", + "architecture", + "cpu_count", + "python_version", + ] + ) + + # Time structure test + assert "runtime" in data + assert "uptime_seconds" in data["runtime"] + assert "current_time" in data["runtime"] + assert data["runtime"]["timezone"] == "UTC" + + # Request structure test + assert "request" in data + assert "client_ip" in data["request"] + assert "method" in data["request"] + assert data["request"]["method"] == "GET" + + # Endpoints structure test + assert "endpoints" in data + assert isinstance(data["endpoints"], list) + assert len(data["endpoints"]) >= 2 + + +def test_health_endpoint(client): + """Test of health endpoint GET /health""" + response = client.get("/health") + + # Status check + assert response.status_code == 200 + + # Json structure test + data = response.get_json() + + assert "status" in data + assert data["status"] == "healthy" + assert "timestamp" in data + assert "uptime_seconds" in data + assert isinstance(data["uptime_seconds"], int) + + +def test_404_error(client): + """404 error handling test""" + response = client.get("/nonexistent") + + assert response.status_code == 404 + + data = response.get_json() + assert "error" in data + assert "message" in data + assert data["error"] == "Not Found" + + +def test_different_user_agent(client): + """Test with different User-Agent headers.""" + headers = {"User-Agent": "Test-Agent/1.0"} + response = client.get("/", headers=headers) + + assert response.status_code == 200 + data = response.get_json() + assert data["request"]["user_agent"] == "Test-Agent/1.0" + + +def test_json_structure_types(client): + """Checking the data types in the JSON response""" + response = client.get("/") + data = response.get_json() + + # Type check in service + assert isinstance(data["service"]["name"], str) + assert isinstance(data["service"]["version"], str) + assert isinstance(data["service"]["description"], str) + + # Type check in system + assert isinstance(data["system"]["hostname"], str) + assert isinstance(data["system"]["cpu_count"], int) + assert isinstance(data["system"]["python_version"], str) + + # Type check in runtime + assert isinstance(data["runtime"]["uptime_seconds"], int) + assert isinstance(data["runtime"]["current_time"], str) + + +def test_health_response_structure(client): + """Detailed verification of the health endpoint structure""" + response = client.get("/health") + data = response.get_json() + + # Checking all required fields + required_fields = ["status", "timestamp", "uptime_seconds"] + for field in required_fields: + assert field in data + + # Checking the status value + assert data["status"] == "healthy" + + # Checking the timestamp format + from datetime import datetime + + try: + datetime.fromisoformat(data["timestamp"].replace("Z", "+00:00")) + timestamp_valid = True + except ValueError: + timestamp_valid = False + assert timestamp_valid diff --git a/docs/LAB04.md b/docs/LAB04.md new file mode 100644 index 0000000000..3ad7dc822b --- /dev/null +++ b/docs/LAB04.md @@ -0,0 +1,392 @@ +# Lab 4 — Infrastructure as Code (Terraform & Pulumi) + +## Cloud Provider & Infrastructure + +### Choice of Environment +I chose to use a **local virtual machine** as my "cloud provider" for this lab. The VM runs on VirtualBox on my Archlinux host. This decision was made to avoid any cloud costs, simplify the setup, and have full control over the infrastructure. The VM will also be used in Lab 5 (Ansible). + +### VM Specifications +- **Virtualization Software:** VirtualBox 7.2.6 +- **Guest OS:** Ubuntu Server 24.04 LTS +- **Resources:** 2 GB RAM, 25 GB dynamic disk +- **Network:** NAT with port forwarding (host port 2222 → guest port 22) +- **SSH Access:** `ssh devops@localhost -p 2222` + +The VM was created manually following the lab instructions, and its configuration is documented here to represent the infrastructure managed by IaC tools. + + +## Terraform Implementation + +### Terraform Version +``` +Terraform v1.14.5 on linux_amd64 +``` + +### Project Structure +``` +terraform/ +├── main.tf # Main resources (local files) +├── variables.tf # Input variables (empty for now) +├── outputs.tf # Output definitions +└── .gitignore # Ignore state files +``` + +### Key Configuration Decisions +Since the infrastructure is a local VM, we cannot manage it directly with Terraform's cloud providers. Instead, we used the `local` provider to create descriptive files that represent the infrastructure. This demonstrates the concept of Infrastructure as Code – defining infrastructure in code, even if the actual resources are provisioned outside of Terraform. + +**main.tf**: +```hcl +resource "local_file" "vm_info" { + content = <<-EOT + This file represents the infrastructure created by Terraform for Lab 4. + VM Name: devops-vm + SSH User: devops + SSH Port (Host): 2222 + OS: Ubuntu Server 24.04 LTS + Managed by: Terraform + Created at: ${timestamp()} + EOT + filename = "${path.module}/vm_terraform_info.txt" +} + +resource "local_file" "ansible_inventory" { + content = <<-EOT + [devops_vm] + devops-vm ansible_host=localhost ansible_port=2222 ansible_user=devops ansible_ssh_private_key_file=~/.ssh/id_rsa + EOT + filename = "${path.module}/../ansible_inventory.ini" +} + +output "vm_info_file_created" { + value = local_file.vm_info.filename +} + +output "ansible_inventory_file" { + value = local_file.ansible_inventory.filename +} +``` + +**outputs.tf**: +```hcl +output "vm_info_file" { + value = local_file.vm_info.filename +} + +output "ansible_inventory_file" { + value = local_file.ansible_inventory.filename +} + +output "creation_timestamp" { + value = timestamp() +} +``` + +### Applying Infrastructure +```bash +devops@devops:~/devops_lab/terraform$ terraform init +Initializing the backend... +Initializing provider plugins... +- Finding latest version of hashicorp/local... +- Installing hashicorp/local v2.5.1... +- Installed hashicorp/local v2.5.1 (unauthenticated) +Terraform has created a lock file .terraform.lock.hcl to record the provider +selections it made above. Include this file in your version control repository +so that Terraform can guarantee to make the same selections by default when +you run "terraform init" in the future. + +╷ +│ Warning: Incomplete lock file information for providers +│ +│ Due to your customized provider installation methods, Terraform was forced to calculate lock file checksums locally for the following providers: +│ - hashicorp/local +│ +│ The current .terraform.lock.hcl file only includes checksums for linux_amd64, so Terraform running on another platform will fail to install these providers. +│ +│ To calculate additional checksums for another platform, run: +│ terraform providers lock -platform=linux_amd64 +│ (where linux_amd64 is the platform to generate) +╵ +Terraform has been successfully initialized! + +You may now begin working with Terraform. Try running "terraform plan" to see +any changes that are required for your infrastructure. All Terraform commands +should now work. + +If you ever set or change modules or backend configuration for Terraform, +rerun this command to reinitialize your working directory. If you forget, other +commands will detect it and remind you to do so if necessary. +devops@devops:~/devops_lab/terraform$ terraform plan + +Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # local_file.ansible_inventory will be created + + resource "local_file" "ansible_inventory" { + + content = <<-EOT + [devops_vm] + devops-vm ansible_host=localhost ansible_port=2222 ansible_user=devops ansible_ssh_private_key_file=~/.ssh/id_rsa + EOT + + content_base64sha256 = (known after apply) + + content_base64sha512 = (known after apply) + + content_md5 = (known after apply) + + content_sha1 = (known after apply) + + content_sha256 = (known after apply) + + content_sha512 = (known after apply) + + directory_permission = "0777" + + file_permission = "0777" + + filename = "./../ansible_inventory.ini" + + id = (known after apply) + } + + # local_file.vm_info will be created + + resource "local_file" "vm_info" { + + content = (known after apply) + + content_base64sha256 = (known after apply) + + content_base64sha512 = (known after apply) + + content_md5 = (known after apply) + + content_sha1 = (known after apply) + + content_sha256 = (known after apply) + + content_sha512 = (known after apply) + + directory_permission = "0777" + + file_permission = "0777" + + filename = "./vm_terraform_info.txt" + + id = (known after apply) + } + +Plan: 2 to add, 0 to change, 0 to destroy. + +Changes to Outputs: + + ansible_inventory_file = "./../ansible_inventory.ini" + + vm_info_file_created = "./vm_terraform_info.txt" + +──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── + +Note: You didn't use the -out option to save this plan, so Terraform can't guarantee to take exactly these actions if you run "terraform apply" now. +devops@devops:~/devops_lab/terraform$ terraform apply -auto-approve + +Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following symbols: + + create + +Terraform will perform the following actions: + + # local_file.ansible_inventory will be created + + resource "local_file" "ansible_inventory" { + + content = <<-EOT + [devops_vm] + devops-vm ansible_host=localhost ansible_port=2222 ansible_user=devops ansible_ssh_private_key_file=~/.ssh/id_rsa + EOT + + content_base64sha256 = (known after apply) + + content_base64sha512 = (known after apply) + + content_md5 = (known after apply) + + content_sha1 = (known after apply) + + content_sha256 = (known after apply) + + content_sha512 = (known after apply) + + directory_permission = "0777" + + file_permission = "0777" + + filename = "./../ansible_inventory.ini" + + id = (known after apply) + } + + # local_file.vm_info will be created + + resource "local_file" "vm_info" { + + content = (known after apply) + + content_base64sha256 = (known after apply) + + content_base64sha512 = (known after apply) + + content_md5 = (known after apply) + + content_sha1 = (known after apply) + + content_sha256 = (known after apply) + + content_sha512 = (known after apply) + + directory_permission = "0777" + + file_permission = "0777" + + filename = "./vm_terraform_info.txt" + + id = (known after apply) + } + +Plan: 2 to add, 0 to change, 0 to destroy. + +Changes to Outputs: + + ansible_inventory_file = "./../ansible_inventory.ini" + + vm_info_file_created = "./vm_terraform_info.txt" +local_file.ansible_inventory: Creating... +local_file.vm_info: Creating... +local_file.ansible_inventory: Creation complete after 0s [id=d2dd8bfe83944cd4c03041974ec1e5b7b986a264] +local_file.vm_info: Creation complete after 0s [id=3e82a1249bfd3da17b5906b4f457c664214a1651] + +Apply complete! Resources: 2 added, 0 changed, 0 destroyed. + +Outputs: + +ansible_inventory_file = "./../ansible_inventory.ini" +vm_info_file_created = "./vm_terraform_info.txt" +``` + +### Verification +Both files were created successfully: +```bash +devops@devops:~/devops_lab/terraform$ cat vm_terraform_info.txt +This file represents the infrastructure created by Terraform for Lab 4. +VM Name: devops-vm +SSH User: devops +SSH Port (Host): 2222 +OS: Ubuntu Server 24.04 LTS +Managed by: Terraform +Created at: 2026-02-18T13:50:07Z +devops@devops:~/devops_lab/terraform$ cat ../ansible_inventory.ini +[devops_vm] +devops-vm ansible_host=localhost ansible_port=2222 ansible_user=devops ansible_ssh_private_key_file=~/.ssh/id_rsa +``` + +## Pulumi Implementation + +### Pulumi Version & Language +- **Pulumi version:** 3.221.0 +- **Language:** Python 3.12.3 +- **State backend:** Local (`pulumi login --local`) + +### Project Structure +``` +pulumi/ +├── __main__.py # Infrastructure code +├── requirements.txt # Python dependencies +├── Pulumi.yaml # Project metadata +└── Pulumi.dev.yaml # Stack configuration (local) +``` + +### Code Implementation +The Pulumi program creates the same two files using plain Python code, demonstrating the imperative approach. + +**__main__.py**: +```python +import pulumi +from datetime import datetime + +vm_info_content = f""" +This file represents the infrastructure created by Pulumi for Lab 4. +VM Name: devops-vm +SSH User: devops +SSH Port (Host): 2222 +OS: Ubuntu Server 24.04 LTS +Managed by: Pulumi (Python) +Created at: {datetime.now().isoformat()} +""" + +with open('./vm_pulumi_info.txt', 'w') as f: + f.write(vm_info_content) + +pulumi.export('vm_info_file', './vm_pulumi_info.txt') +pulumi.export('message', 'Pulumi infrastructure applied successfully!') +pulumi.export('timestamp', datetime.now().isoformat()) + +inventory_lines = [ + "[devops_vm]", + "devops-vm ansible_host=localhost ansible_port=2222 ansible_user=devops ansible_ssh_private_key_file=~/.ssh/id_rsa" +] +inventory_content = "\n".join(inventory_lines) +with open('../pulumi_ansible_inventory.ini', 'w') as f: + f.write(inventory_content) + +pulumi.export('ansible_inventory', '../pulumi_ansible_inventory.ini') +``` + +### Applying Infrastructure +```bash +(venv) devops@devops:~/devops_lab/pulumi$ pulumi up -y +Enter your passphrase to unlock config/secrets + (set PULUMI_CONFIG_PASSPHRASE or PULUMI_CONFIG_PASSPHRASE_FILE to remember): +Enter your passphrase to unlock config/secrets +Previewing update (dev): + Type Name Plan + + pulumi:pulumi:Stack project-dev create + +Outputs: + ansible_inventory: "../pulumi_ansible_inventory.ini" + message : "Pulumi infrastructure applied successfully!" + timestamp : "2026-02-18T13:58:26.931155" + vm_info_file : "./vm_pulumi_info.txt" + +Resources: + + 1 to create + +Updating (dev): + Type Name Status + + pulumi:pulumi:Stack project-dev created (0.00s) + +Outputs: + ansible_inventory: "../pulumi_ansible_inventory.ini" + message : "Pulumi infrastructure applied successfully!" + timestamp : "2026-02-18T13:58:27.238400" + vm_info_file : "./vm_pulumi_info.txt" + +Resources: + + 1 created + +Duration: 1s +``` + +### Verification +Files were created and contain the expected data: +```bash +(venv) devops@devops:~/devops_lab/pulumi$ cat vm_pulumi_info.txt + +This file represents the infrastructure created by Pulumi for Lab 4. +VM Name: devops-vm +SSH User: devops +SSH Port (Host): 2222 +OS: Ubuntu Server 24.04 LTS +Managed by: Pulumi (Python) +Created at: 2026-02-18T13:58:27.238285 +(venv) devops@devops:~/devops_lab/pulumi$ cat ../pulumi_ansible_inventory.ini +[devops_vm] +devops-vm ansible_host=localhost ansible_port=2222 ansible_user=devops ansible_ssh_private_key_file=~/.ssh/id_rsa +``` + + +## Terraform vs Pulumi Comparison + +| Aspect | Terraform | Pulumi | +|--------|-----------|--------| +| **Ease of Learning** | HCL is simple and declarative, easy to pick up even without programming background | Requires knowledge of a programming language (Python/TypeScript/Go), but does not require any new language if developer already familiar with any | +| **Code Readability** | Configuration is clean and resource-focused | The code is imperative and can mix infrastructure logic with application logic. For simple resources, it's still readable, but complex logic may obscure the infrastructure intent | +| **Debugging** | Error messages are generally clear, but debugging complex interpolation can be tricky | Full language debugging makes troubleshooting much easier | +| **Documentation** | Terraform Registry has well-structured documentation | Pulumi Registry also has good docs, but often you need to know the underlying cloud provider API as well | +| **Use Case** | Best for pure infrastructure provisioning, especially in team environments where a declarative approach is preferred | Ideal when infrastructure needs to be tightly integrated with application code, or when you need complex logic (loops, conditionals, external libraries) | + +**My Personal Preference:** +For this simple task, both tools worked well. However, I found Pulumi more intuitive because I am comfortable with imperative languages + + +## Lab 5 Preparation & Cleanup + +### Keeping the VM for Lab 5 +- **I am keeping the VM running** (`devops`) for Lab 5 (Ansible). +- The VM is accessible via `ssh devops@localhost -p 2222`. +- All necessary files (Ansible inventory generated by both Terraform and Pulumi) are already in place. + +### Cleanup Status +- I have **not destroyed** the Terraform or Pulumi resources because the VM itself is not managed by these tools. +- The local files created by Terraform and Pulumi remain in the repository for documentation and future reference. + + +## Challenges & Solutions + +### Challenge 1: Pulumi requiring a backend +**Problem:** Running `pulumi new python` initially prompted for a Pulumi Cloud token. +**Solution:** Used `pulumi login --local` to configure a local state backend, then created the project normally. This kept everything self-contained. + +### Challenge 2: Simulating infrastructure without a real cloud provider +**Problem:** Both tools are designed to provision cloud resources, but I only have a local VM. +**Solution:** I used file resources as a proxy to demonstrate IaC concepts. The files contain metadata about the VM, effectively representing the infrastructure in code. + +### Challenge 3: Downloading Terraform due to geographical restrictions +**Problem:** Direct download of Terraform from the official HashiCorp website was blocked from inside the virtual machine due to regional network restrictions. +**Solution:** I downloaded the Terraform binary on my host machine (Arch Linux) using a working connection, then transferred it to the VM via `scp` over the forwarded SSH port (`scp -P 2222 terraform-provider-local_v2.5.1_x5 devops@localhost:/home/devops/devops_lab/terraform/`). After transferring, I unzipped the archive and moved the binary to `/usr/local/bin/` inside the VM. + +## Screenshots + +1. **Terraform apply output** + ![Terraform Apply](screenshots/terraform-apply.png) + +2. **Pulumi up output** + ![Pulumi Up](screenshots/pulumi-up.png) diff --git a/docs/screenshots/pulumi-up.png b/docs/screenshots/pulumi-up.png new file mode 100644 index 0000000000..264cc47706 Binary files /dev/null and b/docs/screenshots/pulumi-up.png differ diff --git a/docs/screenshots/terraform-apply.png b/docs/screenshots/terraform-apply.png new file mode 100644 index 0000000000..bd8ab162ae Binary files /dev/null and b/docs/screenshots/terraform-apply.png differ