diff --git a/Vagrantfile b/Vagrantfile index 736e46bd70..53e2f6e4b0 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -1,6 +1,6 @@ Vagrant.configure("2") do |config| config.vm.box = "ubuntu/jammy64" - config.vm.hostname = "lab05" + config.vm.hostname = "lab09" # Disable project folder sharing inside the VM. # This avoids common Windows path issues (spaces, Cyrillic characters) @@ -15,11 +15,13 @@ Vagrant.configure("2") do |config| config.vm.network "forwarded_port", guest: 3100, host: 3100, host_ip: "0.0.0.0", id: "loki", auto_correct: true config.vm.network "forwarded_port", guest: 9080, host: 9080, host_ip: "0.0.0.0", id: "promtail", auto_correct: true config.vm.network "forwarded_port", guest: 9090, host: 9090, host_ip: "0.0.0.0", id: "prometheus", auto_correct: true + config.vm.network "forwarded_port", guest: 30080, host: 30080, host_ip: "0.0.0.0", id: "k8s-app1", auto_correct: true + config.vm.network "forwarded_port", guest: 30081, host: 30081, host_ip: "0.0.0.0", id: "k8s-app2", auto_correct: true config.ssh.insert_key = true config.vm.provider "virtualbox" do |vb| - vb.name = "lab07-monitoring" + vb.name = "lab09" vb.memory = 3072 vb.cpus = 2 end diff --git a/app_python/Dockerfile b/app_python/Dockerfile index 2ca732ad78..7548f6608b 100644 --- a/app_python/Dockerfile +++ b/app_python/Dockerfile @@ -1,4 +1,3 @@ - # Production-oriented image for a small Flask app. # Pin a specific Python version for reproducible builds. FROM python:3.13.1-slim @@ -9,9 +8,9 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ WORKDIR /app -# Create a dedicated non-root user (mandatory best practice). -RUN addgroup --system app \ - && adduser --system --ingroup app --no-create-home app +# Create a dedicated non-root user with numeric UID/GID. +RUN addgroup --system --gid 10001 app \ + && adduser --system --uid 10001 --ingroup app --no-create-home app # Install dependencies first to leverage Docker layer caching. COPY requirements.txt ./ @@ -20,8 +19,8 @@ RUN pip install --no-cache-dir -r requirements.txt # Copy only the application code needed at runtime. COPY app.py ./ -# Drop privileges -USER app +# Drop privileges using a numeric user for Kubernetes runAsNonRoot validation. +USER 10001:10001 # Document the port (Flask defaults to 5000 in this repo) EXPOSE 5000 diff --git a/app_python/README.md b/app_python/README.md index 48cffa6caa..d418c0fd0e 100644 --- a/app_python/README.md +++ b/app_python/README.md @@ -1,159 +1,168 @@ -# DevOps Info Service - -[![python-ci](https://github.com/dorley174/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)](https://github.com/dorley174/DevOps-Core-Course/actions/workflows/python-ci.yml) - -## Overview -DevOps Info Service is a production-ready starter web service for the DevOps course. -It reports service metadata, runtime details, and basic system information. - -The service exposes two endpoints: -- `GET /` — service + system + runtime + request information -- `GET /health` — health check endpoint (used later for Kubernetes probes) - -## Prerequisites -- Python **3.11+** -- `pip` -- (Recommended) Virtual environment (`venv`) -- **Windows:** Python Launcher `py` is recommended - -## Installation - -```bash -python -m venv venv -# Windows: .\venv\Scripts\activate -# Linux/macOS: source venv/bin/activate - -pip install -r requirements.txt -``` - -## Running the Application - -### Default run (port 5000) -> If `PORT` is not set, the application runs on **0.0.0.0:5000**. -```bash -python app.py -``` - -### Custom configuration - -**Linux/Mac:** -```bash -HOST=127.0.0.1 PORT=8080 DEBUG=True python app.py -``` - -**Windows (PowerShell):** -```powershell -$env:HOST="127.0.0.1" -$env:PORT="8080" -$env:DEBUG="True" -python app.py -``` - -**Windows (CMD):** -```bat -set HOST=127.0.0.1 -set PORT=8080 -set DEBUG=True -python app.py -``` - -## API Endpoints - -### `GET /` -Returns service metadata, system information, runtime details, request info, and a list of available endpoints. - -Example: -```bash -curl http://127.0.0.1:5000/ -``` - -### `GET /health` -Returns a minimal health response for monitoring. - -Example (includes HTTP status): -```bash -curl -i http://127.0.0.1:5000/health -``` - -## Testing / Pretty Output - -### Pretty-printed JSON -**Windows PowerShell tip:** use `curl.exe`. -```bash -curl -s http://127.0.0.1:5000/ | python -m json.tool -``` - -## Testing & Linting (LAB03) - -> Dev dependencies live in `requirements-dev.txt` (pytest, coverage, ruff). - -Install dev deps: -```bash -pip install -r requirements-dev.txt -``` - -Run linter: -```bash -ruff check . -``` - -Run tests + coverage: -```bash -pytest -q tests --cov=. --cov-report=term-missing -``` - -## CI/CD Secrets (GitHub Actions) - -In your GitHub repository: -**Settings → Secrets and variables → Actions → New repository secret** - -Add: -- `DOCKERHUB_USERNAME` — your Docker Hub username -- `DOCKERHUB_TOKEN` — Docker Hub Access Token (Account Settings → Security) -- `SNYK_TOKEN` — Snyk API token (Account settings → API token) - -## Configuration - -| Variable | Default | Description | -|----------|---------|-------------| -| HOST | 0.0.0.0 | Bind address | -| PORT | 5000 | HTTP port | -| DEBUG | False | Flask debug mode | - ---- - -## Docker - -> Examples below use placeholders like `` and ``. - -### Build (local) - -```bash -docker build -t : . -``` - -### Run - -```bash -docker run --rm -p 5000:5000 : -``` - -(Optional: override env vars) - -```bash -docker run --rm -p 5000:5000 -e PORT=5000 -e DEBUG=false : -``` - -### Pull from Docker Hub - -```bash -docker pull /: -docker run --rm -p 5000:5000 /: -``` - -### Quick test - -```bash -curl http://localhost:5000/health -curl http://localhost:5000/ -``` +# DevOps Info Service + +[![python-ci](https://github.com/dorley174/DevOps-Core-Course/actions/workflows/python-ci.yml/badge.svg)](https://github.com/dorley174/DevOps-Core-Course/actions/workflows/python-ci.yml) + +## Overview +DevOps Info Service is a production-ready starter web service for the DevOps course. +It reports service metadata, runtime details, and basic system information. + +The service exposes two endpoints: +- `GET /` — service + system + runtime + request information +- `GET /health` — liveness health endpoint +- `GET /ready` — readiness health endpoint for Kubernetes + +## Prerequisites +- Python **3.11+** +- `pip` +- (Recommended) Virtual environment (`venv`) +- **Windows:** Python Launcher `py` is recommended + +## Installation + +```bash +python -m venv venv +# Windows: .\venv\Scripts\activate +# Linux/macOS: source venv/bin/activate + +pip install -r requirements.txt +``` + +## Running the Application + +### Default run (port 5000) +> If `PORT` is not set, the application runs on **0.0.0.0:5000**. +```bash +python app.py +``` + +### Custom configuration + +**Linux/Mac:** +```bash +HOST=127.0.0.1 PORT=8080 DEBUG=True python app.py +``` + +**Windows (PowerShell):** +```powershell +$env:HOST="127.0.0.1" +$env:PORT="8080" +$env:DEBUG="True" +python app.py +``` + +**Windows (CMD):** +```bat +set HOST=127.0.0.1 +set PORT=8080 +set DEBUG=True +python app.py +``` + +## API Endpoints + +### `GET /` +Returns service metadata, system information, runtime details, request info, and a list of available endpoints. + +Example: +```bash +curl http://127.0.0.1:5000/ +``` + +### `GET /health` +Returns a minimal liveness response for monitoring and Kubernetes liveness probes. + +Example (includes HTTP status): +```bash +curl -i http://127.0.0.1:5000/health +``` + +### `GET /ready` +Returns readiness information for Kubernetes readiness probes. + +Example: +```bash +curl -i http://127.0.0.1:5000/ready +``` + +## Testing / Pretty Output + +### Pretty-printed JSON +**Windows PowerShell tip:** use `curl.exe`. +```bash +curl -s http://127.0.0.1:5000/ | python -m json.tool +``` + +## Testing & Linting (LAB03) + +> Dev dependencies live in `requirements-dev.txt` (pytest, coverage, ruff). + +Install dev deps: +```bash +pip install -r requirements-dev.txt +``` + +Run linter: +```bash +ruff check . +``` + +Run tests + coverage: +```bash +pytest -q tests --cov=. --cov-report=term-missing +``` + +## CI/CD Secrets (GitHub Actions) + +In your GitHub repository: +**Settings → Secrets and variables → Actions → New repository secret** + +Add: +- `DOCKERHUB_USERNAME` — your Docker Hub username +- `DOCKERHUB_TOKEN` — Docker Hub Access Token (Account Settings → Security) +- `SNYK_TOKEN` — Snyk API token (Account settings → API token) + +## Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| HOST | 0.0.0.0 | Bind address | +| PORT | 5000 | HTTP port | +| DEBUG | False | Flask debug mode | + +--- + +## Docker + +> Examples below use placeholders like `` and ``. + +### Build (local) + +```bash +docker build -t : . +``` + +### Run + +```bash +docker run --rm -p 5000:5000 : +``` + +(Optional: override env vars) + +```bash +docker run --rm -p 5000:5000 -e PORT=5000 -e DEBUG=false : +``` + +### Pull from Docker Hub + +```bash +docker pull /: +docker run --rm -p 5000:5000 /: +``` + +### Quick test + +```bash +curl http://localhost:5000/health +curl http://localhost:5000/ +``` diff --git a/app_python/app.py b/app_python/app.py index 428b98833a..36e505df46 100644 --- a/app_python/app.py +++ b/app_python/app.py @@ -33,10 +33,12 @@ PORT: int = int(os.getenv("PORT", "5000")) DEBUG: bool = os.getenv("DEBUG", "False").strip().lower() == "true" -SERVICE_NAME = "devops-info-service" -SERVICE_VERSION = "1.1.0" -SERVICE_DESCRIPTION = "DevOps course info service" +SERVICE_NAME = os.getenv("SERVICE_NAME", "devops-info-service") +SERVICE_VERSION = os.getenv("SERVICE_VERSION", "1.1.0") +SERVICE_DESCRIPTION = os.getenv("SERVICE_DESCRIPTION", "DevOps course info service") SERVICE_FRAMEWORK = "Flask" +APP_VARIANT = os.getenv("APP_VARIANT", "primary") +APP_MESSAGE = os.getenv("APP_MESSAGE", "running") START_TIME_UTC = datetime.now(timezone.utc) @@ -290,7 +292,8 @@ def get_system_info() -> Dict[str, Any]: def build_endpoints() -> list[Dict[str, str]]: return [ {"path": "/", "method": "GET", "description": "Service information"}, - {"path": "/health", "method": "GET", "description": "Health check"}, + {"path": "/health", "method": "GET", "description": "Liveness health check"}, + {"path": "/ready", "method": "GET", "description": "Readiness health check"}, {"path": "/metrics", "method": "GET", "description": "Prometheus metrics"}, ] @@ -310,6 +313,8 @@ def index(): "version": SERVICE_VERSION, "description": SERVICE_DESCRIPTION, "framework": SERVICE_FRAMEWORK, + "variant": APP_VARIANT, + "message": APP_MESSAGE, }, "system": get_system_info(), "runtime": { @@ -339,6 +344,22 @@ def health(): "status": "healthy", "timestamp": iso_utc_now_z(), "uptime_seconds": uptime["seconds"], + "variant": APP_VARIANT, + } + ), 200 + + +@app.get("/ready") +def ready(): + """Readiness endpoint used by Kubernetes readiness probes.""" + uptime = get_uptime() + return jsonify( + { + "status": "ready", + "timestamp": iso_utc_now_z(), + "uptime_seconds": uptime["seconds"], + "variant": APP_VARIANT, + "message": APP_MESSAGE, } ), 200 diff --git a/get-docker.sh b/get-docker.sh new file mode 100644 index 0000000000..9a7bddb001 --- /dev/null +++ b/get-docker.sh @@ -0,0 +1,764 @@ +#!/bin/sh +set -e +# Docker Engine for Linux installation script. +# +# This script is intended as a convenient way to configure docker's package +# repositories and to install Docker Engine, This script is not recommended +# for production environments. Before running this script, make yourself familiar +# with potential risks and limitations, and refer to the installation manual +# at https://docs.docker.com/engine/install/ for alternative installation methods. +# +# The script: +# +# - Requires `root` or `sudo` privileges to run. +# - Attempts to detect your Linux distribution and version and configure your +# package management system for you. +# - Doesn't allow you to customize most installation parameters. +# - Installs dependencies and recommendations without asking for confirmation. +# - Installs the latest stable release (by default) of Docker CLI, Docker Engine, +# Docker Buildx, Docker Compose, containerd, and runc. When using this script +# to provision a machine, this may result in unexpected major version upgrades +# of these packages. Always test upgrades in a test environment before +# deploying to your production systems. +# - Isn't designed to upgrade an existing Docker installation. When using the +# script to update an existing installation, dependencies may not be updated +# to the expected version, resulting in outdated versions. +# +# Source code is available at https://github.com/docker/docker-install/ +# +# Usage +# ============================================================================== +# +# To install the latest stable versions of Docker CLI, Docker Engine, and their +# dependencies: +# +# 1. download the script +# +# $ curl -fsSL https://get.docker.com -o install-docker.sh +# +# 2. verify the script's content +# +# $ cat install-docker.sh +# +# 3. run the script with --dry-run to verify the steps it executes +# +# $ sh install-docker.sh --dry-run +# +# 4. run the script either as root, or using sudo to perform the installation. +# +# $ sudo sh install-docker.sh +# +# Command-line options +# ============================================================================== +# +# --version +# Use the --version option to install a specific version, for example: +# +# $ sudo sh install-docker.sh --version 23.0 +# +# --channel +# +# Use the --channel option to install from an alternative installation channel. +# The following example installs the latest versions from the "test" channel, +# which includes pre-releases (alpha, beta, rc): +# +# $ sudo sh install-docker.sh --channel test +# +# Alternatively, use the script at https://test.docker.com, which uses the test +# channel as default. +# +# --mirror +# +# Use the --mirror option to install from a mirror supported by this script. +# Available mirrors are "Aliyun" (https://mirrors.aliyun.com/docker-ce), and +# "AzureChinaCloud" (https://mirror.azure.cn/docker-ce), for example: +# +# $ sudo sh install-docker.sh --mirror AzureChinaCloud +# +# --setup-repo +# +# Use the --setup-repo option to configure Docker's package repositories without +# installing Docker packages. This is useful when you want to add the repository +# but install packages separately: +# +# $ sudo sh install-docker.sh --setup-repo +# +# Automatic Service Start +# +# By default, this script automatically starts the Docker daemon and enables the docker +# service after installation if systemd is used as init. +# +# If you prefer to start the service manually, use the --no-autostart option: +# +# $ sudo sh install-docker.sh --no-autostart +# +# Note: Starting the service requires appropriate privileges to manage system services. +# +# ============================================================================== + + +# Git commit from https://github.com/docker/docker-install when +# the script was uploaded (Should only be modified by upload job): +SCRIPT_COMMIT_SHA="f381ee68b32e515bb4dc034b339266aff1fbc460" + +# strip "v" prefix if present +VERSION="${VERSION#v}" + +# The channel to install from: +# * stable +# * test +DEFAULT_CHANNEL_VALUE="stable" +if [ -z "$CHANNEL" ]; then + CHANNEL=$DEFAULT_CHANNEL_VALUE +fi + +DEFAULT_DOWNLOAD_URL="https://download.docker.com" +if [ -z "$DOWNLOAD_URL" ]; then + DOWNLOAD_URL=$DEFAULT_DOWNLOAD_URL +fi + +DEFAULT_REPO_FILE="docker-ce.repo" +if [ -z "$REPO_FILE" ]; then + REPO_FILE="$DEFAULT_REPO_FILE" + # Automatically default to a staging repo fora + # a staging download url (download-stage.docker.com) + case "$DOWNLOAD_URL" in + *-stage*) REPO_FILE="docker-ce-staging.repo";; + esac +fi + +mirror='' +DRY_RUN=${DRY_RUN:-} +REPO_ONLY=${REPO_ONLY:-0} +NO_AUTOSTART=${NO_AUTOSTART:-0} +while [ $# -gt 0 ]; do + case "$1" in + --channel) + CHANNEL="$2" + shift + ;; + --dry-run) + DRY_RUN=1 + ;; + --mirror) + mirror="$2" + shift + ;; + --version) + VERSION="${2#v}" + shift + ;; + --setup-repo) + REPO_ONLY=1 + shift + ;; + --no-autostart) + NO_AUTOSTART=1 + ;; + --*) + echo "Illegal option $1" + ;; + esac + shift $(( $# > 0 ? 1 : 0 )) +done + +case "$mirror" in + Aliyun) + DOWNLOAD_URL="https://mirrors.aliyun.com/docker-ce" + ;; + AzureChinaCloud) + DOWNLOAD_URL="https://mirror.azure.cn/docker-ce" + ;; + "") + ;; + *) + >&2 echo "unknown mirror '$mirror': use either 'Aliyun', or 'AzureChinaCloud'." + exit 1 + ;; +esac + +case "$CHANNEL" in + stable|test) + ;; + *) + >&2 echo "unknown CHANNEL '$CHANNEL': use either stable or test." + exit 1 + ;; +esac + +command_exists() { + command -v "$@" > /dev/null 2>&1 +} + +# version_gte checks if the version specified in $VERSION is at least the given +# SemVer (Maj.Minor[.Patch]), or CalVer (YY.MM) version.It returns 0 (success) +# if $VERSION is either unset (=latest) or newer or equal than the specified +# version, or returns 1 (fail) otherwise. +# +# examples: +# +# VERSION=23.0 +# version_gte 23.0 // 0 (success) +# version_gte 20.10 // 0 (success) +# version_gte 19.03 // 0 (success) +# version_gte 26.1 // 1 (fail) +version_gte() { + if [ -z "$VERSION" ]; then + return 0 + fi + version_compare "$VERSION" "$1" +} + +# version_compare compares two version strings (either SemVer (Major.Minor.Path), +# or CalVer (YY.MM) version strings. It returns 0 (success) if version A is newer +# or equal than version B, or 1 (fail) otherwise. Patch releases and pre-release +# (-alpha/-beta) are not taken into account +# +# examples: +# +# version_compare 23.0.0 20.10 // 0 (success) +# version_compare 23.0 20.10 // 0 (success) +# version_compare 20.10 19.03 // 0 (success) +# version_compare 20.10 20.10 // 0 (success) +# version_compare 19.03 20.10 // 1 (fail) +version_compare() ( + set +x + + yy_a="$(echo "$1" | cut -d'.' -f1)" + yy_b="$(echo "$2" | cut -d'.' -f1)" + if [ "$yy_a" -lt "$yy_b" ]; then + return 1 + fi + if [ "$yy_a" -gt "$yy_b" ]; then + return 0 + fi + mm_a="$(echo "$1" | cut -d'.' -f2)" + mm_b="$(echo "$2" | cut -d'.' -f2)" + + # trim leading zeros to accommodate CalVer + mm_a="${mm_a#0}" + mm_b="${mm_b#0}" + + if [ "${mm_a:-0}" -lt "${mm_b:-0}" ]; then + return 1 + fi + + return 0 +) + +is_dry_run() { + if [ -z "$DRY_RUN" ]; then + return 1 + else + return 0 + fi +} + +is_wsl() { + case "$(uname -r)" in + *microsoft* ) true ;; # WSL 2 + *Microsoft* ) true ;; # WSL 1 + * ) false;; + esac +} + +is_darwin() { + case "$(uname -s)" in + *darwin* ) true ;; + *Darwin* ) true ;; + * ) false;; + esac +} + +deprecation_notice() { + distro=$1 + distro_version=$2 + echo + printf "\033[91;1mDEPRECATION WARNING\033[0m\n" + printf " This Linux distribution (\033[1m%s %s\033[0m) reached end-of-life and is no longer supported by this script.\n" "$distro" "$distro_version" + echo " No updates or security fixes will be released for this distribution, and users are recommended" + echo " to upgrade to a currently maintained version of $distro." + echo + printf "Press \033[1mCtrl+C\033[0m now to abort this script, or wait for the installation to continue." + echo + sleep 10 +} + +get_distribution() { + lsb_dist="" + # Every system that we officially support has /etc/os-release + if [ -r /etc/os-release ]; then + lsb_dist="$(. /etc/os-release && echo "$ID")" + fi + # Returning an empty string here should be alright since the + # case statements don't act unless you provide an actual value + echo "$lsb_dist" +} + +start_docker_daemon() { + # Use systemctl if available (for systemd-based systems) + if command_exists systemctl; then + is_dry_run || >&2 echo "Using systemd to manage Docker service" + if ( + is_dry_run || set -x + $sh_c systemctl enable --now docker.service 2>/dev/null + ); then + is_dry_run || echo "INFO: Docker daemon enabled and started" >&2 + else + is_dry_run || echo "WARNING: unable to enable the docker service" >&2 + fi + else + # No service management available (container environment) + if ! is_dry_run; then + >&2 echo "Note: Running in a container environment without service management" + >&2 echo "Docker daemon cannot be started automatically in this environment" + >&2 echo "The Docker packages have been installed successfully" + fi + fi + >&2 echo +} + +echo_docker_as_nonroot() { + if is_dry_run; then + return + fi + if command_exists docker && [ -e /var/run/docker.sock ]; then + ( + set -x + $sh_c 'docker version' + ) || true + fi + + # intentionally mixed spaces and tabs here -- tabs are stripped by "<<-EOF", spaces are kept in the output + echo + echo "================================================================================" + echo + if version_gte "20.10"; then + echo "To run Docker as a non-privileged user, consider setting up the" + echo "Docker daemon in rootless mode for your user:" + echo + echo " dockerd-rootless-setuptool.sh install" + echo + echo "Visit https://docs.docker.com/go/rootless/ to learn about rootless mode." + echo + fi + echo + echo "To run the Docker daemon as a fully privileged service, but granting non-root" + echo "users access, refer to https://docs.docker.com/go/daemon-access/" + echo + echo "WARNING: Access to the remote API on a privileged Docker daemon is equivalent" + echo " to root access on the host. Refer to the 'Docker daemon attack surface'" + echo " documentation for details: https://docs.docker.com/go/attack-surface/" + echo + echo "================================================================================" + echo +} + +# Check if this is a forked Linux distro +check_forked() { + + # Check for lsb_release command existence, it usually exists in forked distros + if command_exists lsb_release; then + # Check if the `-u` option is supported + set +e + lsb_release -a -u > /dev/null 2>&1 + lsb_release_exit_code=$? + set -e + + # Check if the command has exited successfully, it means we're in a forked distro + if [ "$lsb_release_exit_code" = "0" ]; then + # Print info about current distro + cat <<-EOF + You're using '$lsb_dist' version '$dist_version'. + EOF + + # Get the upstream release info + lsb_dist=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]') + dist_version=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]') + + # Print info about upstream distro + cat <<-EOF + Upstream release is '$lsb_dist' version '$dist_version'. + EOF + else + if [ -r /etc/debian_version ] && [ "$lsb_dist" != "ubuntu" ] && [ "$lsb_dist" != "raspbian" ]; then + if [ "$lsb_dist" = "osmc" ]; then + # OSMC runs Raspbian + lsb_dist=raspbian + else + # We're Debian and don't even know it! + lsb_dist=debian + fi + dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')" + case "$dist_version" in + 13|14|forky) + dist_version="trixie" + ;; + 12) + dist_version="bookworm" + ;; + 11) + dist_version="bullseye" + ;; + 10) + dist_version="buster" + ;; + 9) + dist_version="stretch" + ;; + 8) + dist_version="jessie" + ;; + esac + fi + fi + fi +} + +do_install() { + echo "# Executing docker install script, commit: $SCRIPT_COMMIT_SHA" + + if command_exists docker; then + cat >&2 <<-'EOF' + Warning: the "docker" command appears to already exist on this system. + + If you already have Docker installed, this script can cause trouble, which is + why we're displaying this warning and provide the opportunity to cancel the + installation. + + If you installed the current Docker package using this script and are using it + again to update Docker, you can ignore this message, but be aware that the + script resets any custom changes in the deb and rpm repo configuration + files to match the parameters passed to the script. + + You may press Ctrl+C now to abort this script. + EOF + ( set -x; sleep 20 ) + fi + + user="$(id -un 2>/dev/null || true)" + + sh_c='sh -c' + if [ "$user" != 'root' ]; then + if command_exists sudo; then + sh_c='sudo -E sh -c' + elif command_exists su; then + sh_c='su -c' + else + cat >&2 <<-'EOF' + Error: this installer needs the ability to run commands as root. + We are unable to find either "sudo" or "su" available to make this happen. + EOF + exit 1 + fi + fi + + if is_dry_run; then + sh_c="echo" + fi + + # perform some very rudimentary platform detection + lsb_dist=$( get_distribution ) + lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')" + + if is_wsl; then + echo + echo "WSL DETECTED: We recommend using Docker Desktop for Windows." + echo "Please get Docker Desktop from https://www.docker.com/products/docker-desktop/" + echo + cat >&2 <<-'EOF' + + You may press Ctrl+C now to abort this script. + EOF + ( set -x; sleep 20 ) + fi + + case "$lsb_dist" in + + ubuntu) + if command_exists lsb_release; then + dist_version="$(lsb_release --codename | cut -f2)" + fi + if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then + dist_version="$(. /etc/lsb-release && echo "$DISTRIB_CODENAME")" + fi + ;; + + debian|raspbian) + dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')" + case "$dist_version" in + 13) + dist_version="trixie" + ;; + 12) + dist_version="bookworm" + ;; + 11) + dist_version="bullseye" + ;; + 10) + dist_version="buster" + ;; + 9) + dist_version="stretch" + ;; + 8) + dist_version="jessie" + ;; + esac + ;; + + centos|rhel) + if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then + dist_version="$(. /etc/os-release && echo "$VERSION_ID")" + fi + ;; + + *) + if command_exists lsb_release; then + dist_version="$(lsb_release --release | cut -f2)" + fi + if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then + dist_version="$(. /etc/os-release && echo "$VERSION_ID")" + fi + ;; + + esac + + # Check if this is a forked Linux distro + check_forked + + # Print deprecation warnings for distro versions that recently reached EOL, + # but may still be commonly used (especially LTS versions). + case "$lsb_dist.$dist_version" in + centos.8|centos.7|rhel.7) + deprecation_notice "$lsb_dist" "$dist_version" + ;; + debian.buster|debian.stretch|debian.jessie) + deprecation_notice "$lsb_dist" "$dist_version" + ;; + raspbian.buster|raspbian.stretch|raspbian.jessie) + deprecation_notice "$lsb_dist" "$dist_version" + ;; + ubuntu.focal|ubuntu.bionic|ubuntu.xenial|ubuntu.trusty) + deprecation_notice "$lsb_dist" "$dist_version" + ;; + ubuntu.oracular|ubuntu.mantic|ubuntu.lunar|ubuntu.kinetic|ubuntu.impish|ubuntu.hirsute|ubuntu.groovy|ubuntu.eoan|ubuntu.disco|ubuntu.cosmic) + deprecation_notice "$lsb_dist" "$dist_version" + ;; + fedora.*) + if [ "$dist_version" -lt 41 ]; then + deprecation_notice "$lsb_dist" "$dist_version" + fi + ;; + esac + + # Run setup for each distro accordingly + case "$lsb_dist" in + ubuntu|debian|raspbian) + pre_reqs="ca-certificates curl" + apt_repo="deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] $DOWNLOAD_URL/linux/$lsb_dist $dist_version $CHANNEL" + ( + if ! is_dry_run; then + set -x + fi + $sh_c 'apt-get -qq update >/dev/null' + $sh_c "DEBIAN_FRONTEND=noninteractive apt-get -y -qq install $pre_reqs >/dev/null" + $sh_c 'install -m 0755 -d /etc/apt/keyrings' + $sh_c "curl -fsSL \"$DOWNLOAD_URL/linux/$lsb_dist/gpg\" -o /etc/apt/keyrings/docker.asc" + $sh_c "chmod a+r /etc/apt/keyrings/docker.asc" + $sh_c "echo \"$apt_repo\" > /etc/apt/sources.list.d/docker.list" + $sh_c 'apt-get -qq update >/dev/null' + ) + + if [ "$REPO_ONLY" = "1" ]; then + exit 0 + fi + + pkg_version="" + if [ -n "$VERSION" ]; then + if is_dry_run; then + echo "# WARNING: VERSION pinning is not supported in DRY_RUN" + else + # Will work for incomplete versions IE (17.12), but may not actually grab the "latest" if in the test channel + pkg_pattern="$(echo "$VERSION" | sed 's/-ce-/~ce~.*/g' | sed 's/-/.*/g')" + search_command="apt-cache madison docker-ce | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3" + pkg_version="$($sh_c "$search_command")" + echo "INFO: Searching repository for VERSION '$VERSION'" + echo "INFO: $search_command" + if [ -z "$pkg_version" ]; then + echo + echo "ERROR: '$VERSION' not found amongst apt-cache madison results" + echo + exit 1 + fi + if version_gte "18.09"; then + search_command="apt-cache madison docker-ce-cli | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3" + echo "INFO: $search_command" + cli_pkg_version="=$($sh_c "$search_command")" + fi + pkg_version="=$pkg_version" + fi + fi + ( + pkgs="docker-ce${pkg_version%=}" + if version_gte "18.09"; then + # older versions didn't ship the cli and containerd as separate packages + pkgs="$pkgs docker-ce-cli${cli_pkg_version%=} containerd.io" + fi + if version_gte "20.10"; then + pkgs="$pkgs docker-compose-plugin docker-ce-rootless-extras$pkg_version" + fi + if version_gte "23.0"; then + pkgs="$pkgs docker-buildx-plugin" + fi + if version_gte "28.2"; then + pkgs="$pkgs docker-model-plugin" + fi + if ! is_dry_run; then + set -x + fi + $sh_c "DEBIAN_FRONTEND=noninteractive apt-get -y -qq install $pkgs >/dev/null" + ) + if [ "$NO_AUTOSTART" != "1" ]; then + start_docker_daemon + fi + echo_docker_as_nonroot + exit 0 + ;; + centos|fedora|rhel) + if [ "$(uname -m)" = "s390x" ]; then + echo "Effective v27.5, please consult RHEL distro statement for s390x support." + exit 1 + fi + repo_file_url="$DOWNLOAD_URL/linux/$lsb_dist/$REPO_FILE" + ( + if ! is_dry_run; then + set -x + fi + if command_exists dnf5; then + $sh_c "dnf -y -q --setopt=install_weak_deps=False install dnf-plugins-core" + $sh_c "dnf5 config-manager addrepo --overwrite --save-filename=docker-ce.repo --from-repofile='$repo_file_url'" + + if [ "$CHANNEL" != "stable" ]; then + $sh_c "dnf5 config-manager setopt \"docker-ce-*.enabled=0\"" + $sh_c "dnf5 config-manager setopt \"docker-ce-$CHANNEL.enabled=1\"" + fi + $sh_c "dnf makecache" + elif command_exists dnf; then + $sh_c "dnf -y -q --setopt=install_weak_deps=False install dnf-plugins-core" + $sh_c "rm -f /etc/yum.repos.d/docker-ce.repo /etc/yum.repos.d/docker-ce-staging.repo" + $sh_c "dnf config-manager --add-repo $repo_file_url" + + if [ "$CHANNEL" != "stable" ]; then + $sh_c "dnf config-manager --set-disabled \"docker-ce-*\"" + $sh_c "dnf config-manager --set-enabled \"docker-ce-$CHANNEL\"" + fi + $sh_c "dnf makecache" + else + $sh_c "yum -y -q install yum-utils" + $sh_c "rm -f /etc/yum.repos.d/docker-ce.repo /etc/yum.repos.d/docker-ce-staging.repo" + $sh_c "yum-config-manager --add-repo $repo_file_url" + + if [ "$CHANNEL" != "stable" ]; then + $sh_c "yum-config-manager --disable \"docker-ce-*\"" + $sh_c "yum-config-manager --enable \"docker-ce-$CHANNEL\"" + fi + $sh_c "yum makecache" + fi + ) + + if [ "$REPO_ONLY" = "1" ]; then + exit 0 + fi + + pkg_version="" + if command_exists dnf; then + pkg_manager="dnf" + pkg_manager_flags="-y -q --best" + else + pkg_manager="yum" + pkg_manager_flags="-y -q" + fi + if [ -n "$VERSION" ]; then + if is_dry_run; then + echo "# WARNING: VERSION pinning is not supported in DRY_RUN" + else + if [ "$lsb_dist" = "fedora" ]; then + pkg_suffix="fc$dist_version" + else + pkg_suffix="el" + fi + pkg_pattern="$(echo "$VERSION" | sed 's/-ce-/\\\\.ce.*/g' | sed 's/-/.*/g').*$pkg_suffix" + search_command="$pkg_manager list --showduplicates docker-ce | grep '$pkg_pattern' | tail -1 | awk '{print \$2}'" + pkg_version="$($sh_c "$search_command")" + echo "INFO: Searching repository for VERSION '$VERSION'" + echo "INFO: $search_command" + if [ -z "$pkg_version" ]; then + echo + echo "ERROR: '$VERSION' not found amongst $pkg_manager list results" + echo + exit 1 + fi + if version_gte "18.09"; then + # older versions don't support a cli package + search_command="$pkg_manager list --showduplicates docker-ce-cli | grep '$pkg_pattern' | tail -1 | awk '{print \$2}'" + cli_pkg_version="$($sh_c "$search_command" | cut -d':' -f 2)" + fi + # Cut out the epoch and prefix with a '-' + pkg_version="-$(echo "$pkg_version" | cut -d':' -f 2)" + fi + fi + ( + pkgs="docker-ce$pkg_version" + if version_gte "18.09"; then + # older versions didn't ship the cli and containerd as separate packages + if [ -n "$cli_pkg_version" ]; then + pkgs="$pkgs docker-ce-cli-$cli_pkg_version containerd.io" + else + pkgs="$pkgs docker-ce-cli containerd.io" + fi + fi + if version_gte "20.10"; then + pkgs="$pkgs docker-compose-plugin docker-ce-rootless-extras$pkg_version" + fi + if version_gte "23.0"; then + pkgs="$pkgs docker-buildx-plugin docker-model-plugin" + fi + if ! is_dry_run; then + set -x + fi + $sh_c "$pkg_manager $pkg_manager_flags install $pkgs" + ) + if [ "$NO_AUTOSTART" != "1" ]; then + start_docker_daemon + fi + echo_docker_as_nonroot + exit 0 + ;; + sles) + echo "Effective v27.5, please consult SLES distro statement for s390x support." + exit 1 + ;; + *) + if [ -z "$lsb_dist" ]; then + if is_darwin; then + echo + echo "ERROR: Unsupported operating system 'macOS'" + echo "Please get Docker Desktop from https://www.docker.com/products/docker-desktop" + echo + exit 1 + fi + fi + echo + echo "ERROR: Unsupported distribution '$lsb_dist'" + echo + exit 1 + ;; + esac + exit 1 +} + +# wrapped up in a function so that we have some protection against only getting +# half the file during "curl | sh" +do_install diff --git a/k8s/README.md b/k8s/README.md new file mode 100644 index 0000000000..1fade15d84 --- /dev/null +++ b/k8s/README.md @@ -0,0 +1,442 @@ +# Lab 09 — Kubernetes Fundamentals + +This report documents the Kubernetes deployment of the Flask-based `devops-info-service` application. + +--- + +## 1. Architecture Overview + +### 1.1 Selected local Kubernetes tool +I selected **minikube** with the **Docker driver** on **Windows + VS Code + WSL + Docker Desktop**. + +**Why this option fits my environment:** +1. It runs completely locally and does not require a cloud provider. +2. It works well with my Windows + VS Code + WSL workflow. +3. Docker Desktop provides the container runtime, and minikube uses the Docker driver directly from WSL. +4. It is practical in my region because the lab can be completed locally after the required images are downloaded. + +### 1.2 Deployment architecture +The main application is deployed with a Kubernetes **Deployment** and exposed with a **NodePort Service**. + +**Main application path:** +- `Deployment/devops-info-service` +- `Service/devops-info-service` +- `3 replicas` by default +- `NodePort 30080` +- container port `5000` + +**Bonus manifests prepared in the repository:** +- `Deployment/devops-info-service-app2` +- `Service/devops-info-service-app2` +- `Ingress/devops-course-ingress` +- host `local.example.com` +- routes `/app1` and `/app2` + +### 1.3 Networking flow +#### Base task +1. A client sends a request to the local cluster. +2. The NodePort Service exposes the application on port `30080`. +3. The Service selects Pods by label `app.kubernetes.io/name=devops-info-service`. +4. Traffic is forwarded to container port `5000`. + +#### Local verification flow actually used +In my WSL + Docker Desktop setup, direct NodePort access through `minikube ip` was not reliable. I verified the application with: + +```bash +kubectl port-forward -n devops-lab09 service/devops-info-service 8080:80 +``` + +This mapped local port `8080` to Service port `80` and allowed stable validation with `curl`. + +### 1.4 Resource allocation strategy +Each container defines conservative lab-friendly resources: +- **Requests:** `100m CPU`, `128Mi memory` +- **Limits:** `250m CPU`, `256Mi memory` + +These values are appropriate for a lightweight Flask service on a local minikube cluster running through WSL and Docker Desktop. + +--- + +## 2. Manifest Files + +### 2.1 `k8s/namespace.yml` +Creates a dedicated namespace `devops-lab09` for logical isolation of all lab resources. + +### 2.2 `k8s/deployment.yml` +Creates the primary Deployment. + +**Key implementation choices:** +1. `replicas: 3` satisfies the requirement for at least three Pod replicas. +2. `RollingUpdate` uses `maxSurge: 1` and `maxUnavailable: 0` to avoid downtime during updates. +3. `readinessProbe` checks `/ready`. +4. `livenessProbe` checks `/health`. +5. Resource requests and limits are defined. +6. Runtime hardening is enabled with `runAsNonRoot`, dropped capabilities, disabled privilege escalation, and `RuntimeDefault` seccomp. + +### 2.3 `k8s/service.yml` +Creates a `NodePort` Service. + +**Key implementation choices:** +1. Service port `80` is user-friendly. +2. `targetPort: http` forwards traffic to container port `5000`. +3. `nodePort: 30080` is fixed explicitly to simplify local testing. + +### 2.4 `k8s/deployment-app2.yml` +Creates a second Deployment for the bonus part, using the same image with different environment values. + +### 2.5 `k8s/service-app2.yml` +Creates a second NodePort Service on `30081` for the bonus application. + +### 2.6 `k8s/ingress.yml` +Defines nginx Ingress with path-based routing and TLS. + +### 2.7 Helper scripts +- `k8s/deploy.sh` — deploys the namespace, main Deployment, and Service. +- `k8s/deploy-bonus.sh` — deploys the bonus resources. +- `k8s/collect-evidence.sh` — saves Kubernetes evidence into `k8s/evidence/`. + +### 2.8 Docker image security fix +The initial deployment failed because Kubernetes could not validate a named non-root user with `runAsNonRoot: true`. + +I fixed this by updating `app_python/Dockerfile` to use a **numeric UID/GID**: + +```dockerfile +RUN addgroup --system --gid 10001 app \ + && adduser --system --uid 10001 --ingroup app --no-create-home app + +USER 10001:10001 +``` + +--- + +## 3. Deployment Evidence + +### 3.1 Cluster setup evidence + +```text +$ kubectl cluster-info +Kubernetes control plane is running at https://127.0.0.1:60412 +CoreDNS is running at https://127.0.0.1:60412/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy + +To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'. +``` + +```text +$ kubectl get nodes -o wide +NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME +minikube Ready control-plane 14m v1.35.1 192.168.49.2 Debian GNU/Linux 12 (bookworm) 5.15.153.1-microsoft-standard-WSL2 docker://29.2.1 +``` + +### 3.2 Deployment evidence + +```text +$ kubectl get all -n devops-lab09 -o wide +NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES +pod/devops-info-service-7b48589b6b-2cf77 1/1 Running 0 5m26s 10.244.0.6 minikube +pod/devops-info-service-7b48589b6b-52j4f 1/1 Running 0 5m9s 10.244.0.8 minikube +pod/devops-info-service-7b48589b6b-wrvvj 1/1 Running 0 5m19s 10.244.0.7 minikube + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR +service/devops-info-service NodePort 10.100.203.165 80:30080/TCP 13m app.kubernetes.io/name=devops-info-service + +NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR +deployment.apps/devops-info-service 3/3 3 3 13m devops-info-service dorley174/devops-info-service:latest app.kubernetes.io/name=devops-info-service +``` + +```text +$ kubectl get pods,svc -n devops-lab09 -o wide +NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES +pod/devops-info-service-7b48589b6b-2cf77 1/1 Running 0 5m27s 10.244.0.6 minikube +pod/devops-info-service-7b48589b6b-52j4f 1/1 Running 0 5m10s 10.244.0.8 minikube +pod/devops-info-service-7b48589b6b-wrvvj 1/1 Running 0 5m20s 10.244.0.7 minikube + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR +service/devops-info-service NodePort 10.100.203.165 80:30080/TCP 13m app.kubernetes.io/name=devops-info-service +``` + +```text +$ kubectl describe deployment devops-info-service -n devops-lab09 +Name: devops-info-service +Namespace: devops-lab09 +Replicas: 3 desired | 3 updated | 3 total | 3 available | 0 unavailable +StrategyType: RollingUpdate +RollingUpdateStrategy: 0 max unavailable, 1 max surge +Image: dorley174/devops-info-service:latest +Liveness: http-get http://:http/health delay=15s timeout=2s period=10s #success=1 #failure=3 +Readiness: http-get http://:http/ready delay=5s timeout=2s period=5s #success=1 #failure=3 +Environment: + PORT: 5000 + DEBUG: False + APP_VARIANT: app1 + APP_MESSAGE: Lab 09 primary deployment + SERVICE_VERSION: lab09-v1 +``` + +### 3.3 Application verification +I verified the running Service with `kubectl port-forward`. + +```bash +kubectl port-forward -n devops-lab09 service/devops-info-service 8080:80 +``` + +```text +$ curl http://127.0.0.1:8080/health +{"status":"healthy","timestamp":"2026-03-26T19:47:24.216Z","uptime_seconds":264,"variant":"app1"} +``` + +```text +$ curl http://127.0.0.1:8080/ready +{"message":"Lab 09 primary deployment","status":"ready","timestamp":"2026-03-26T19:47:24.232Z","uptime_seconds":264,"variant":"app1"} +``` + +```json +$ curl http://127.0.0.1:8080/ | python3 -m json.tool +{ + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "message": "Lab 09 primary deployment", + "name": "devops-info-service", + "variant": "app1", + "version": "lab09-v1" + } +} +``` + +### 3.4 Evidence collection helper + +```text +$ ./k8s/collect-evidence.sh +Evidence saved to k8s/evidence +``` + +The raw evidence files are included in `k8s/evidence/`. + +--- + +## 4. Operations Performed + +### 4.1 Deploy the application +I deployed the namespace, Deployment, and Service declaratively with `kubectl apply` and confirmed that the Deployment reached `3/3` available replicas. + +### 4.2 Scale the Deployment to 5 replicas + +```bash +kubectl scale deployment/devops-info-service -n devops-lab09 --replicas=5 +kubectl rollout status deployment/devops-info-service -n devops-lab09 +kubectl get pods -n devops-lab09 -o wide +``` + +**Result:** the Deployment was successfully scaled from 3 to 5 replicas. + +```text +deployment.apps/devops-info-service scaled +Waiting for deployment "devops-info-service" rollout to finish: 3 of 5 updated replicas are available... +Waiting for deployment "devops-info-service" rollout to finish: 4 of 5 updated replicas are available... +deployment "devops-info-service" successfully rolled out +``` + +```text +NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES +devops-info-service-7b48589b6b-2b97b 1/1 Running 0 8m32s 10.244.0.14 minikube +devops-info-service-7b48589b6b-7fttq 1/1 Running 0 8m25s 10.244.0.15 minikube +devops-info-service-7b48589b6b-jjtsm 1/1 Running 0 8s 10.244.0.17 minikube +devops-info-service-7b48589b6b-wmf6g 1/1 Running 0 7s 10.244.0.18 minikube +devops-info-service-7b48589b6b-zhkpz 1/1 Running 0 8m18s 10.244.0.16 minikube +``` + +### 4.3 Demonstrate a rolling update +Instead of editing the YAML by hand during the live test, I used `kubectl set env` to trigger a Deployment rollout by changing the Pod template environment variables. + +```bash +kubectl set env deployment/devops-info-service -n devops-lab09 \ + APP_MESSAGE="Lab 09 rolling update" \ + SERVICE_VERSION="lab09-v2" +kubectl rollout status deployment/devops-info-service -n devops-lab09 +kubectl rollout history deployment/devops-info-service -n devops-lab09 +``` + +**Result:** rollout completed successfully and the Deployment spec reflected the updated values. + +```text +deployment.apps/devops-info-service env updated +Waiting for deployment "devops-info-service" rollout to finish: 1 out of 5 new replicas have been updated... +... +deployment "devops-info-service" successfully rolled out +``` + +```text +$ kubectl rollout history deployment/devops-info-service -n devops-lab09 +REVISION CHANGE-CAUSE +1 +4 +5 +``` + +```text +$ kubectl get deployment devops-info-service -n devops-lab09 -o yaml | grep -A1 -E 'APP_MESSAGE|SERVICE_VERSION' +- name: APP_MESSAGE + value: Lab 09 rolling update +- name: SERVICE_VERSION + value: lab09-v2 +``` + +### 4.4 Demonstrate rollback + +```bash +kubectl rollout undo deployment/devops-info-service -n devops-lab09 +kubectl rollout status deployment/devops-info-service -n devops-lab09 +kubectl rollout history deployment/devops-info-service -n devops-lab09 +``` + +**Result:** rollback completed successfully and the Deployment returned to the original values. + +```text +deployment.apps/devops-info-service rolled back +Waiting for deployment "devops-info-service" rollout to finish: 1 out of 5 new replicas have been updated... +... +deployment "devops-info-service" successfully rolled out +``` + +```text +$ kubectl rollout history deployment/devops-info-service -n devops-lab09 +REVISION CHANGE-CAUSE +1 +5 +6 +``` + +```text +$ kubectl get deployment devops-info-service -n devops-lab09 -o yaml | grep -A1 -E 'APP_MESSAGE|SERVICE_VERSION' +- name: APP_MESSAGE + value: Lab 09 primary deployment +- name: SERVICE_VERSION + value: lab09-v1 +``` + +Rollback response check: + +```json +$ curl http://127.0.0.1:8082/ | python3 -m json.tool +{ + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "message": "Lab 09 primary deployment", + "name": "devops-info-service", + "variant": "app1", + "version": "lab09-v1" + } +} +``` + +### 4.5 Bonus status +The repository includes bonus manifests for the second application and Ingress, but I did not complete final runtime verification for the bonus part during this execution session. + +--- + +## 5. Production Considerations + +### 5.1 Health checks +Two probes are implemented: +1. **Liveness probe** on `/health` checks whether the process is alive. +2. **Readiness probe** on `/ready` checks whether the Pod is ready to receive traffic. + +### 5.2 Resource limits rationale +The application is lightweight, so the selected requests and limits are sufficient for local development while still demonstrating correct Kubernetes resource configuration. + +### 5.3 Security choices +1. Non-root execution is enforced. +2. Privilege escalation is disabled. +3. All Linux capabilities are dropped. +4. `RuntimeDefault` seccomp is enabled. +5. The Docker image now uses a numeric UID/GID to satisfy Kubernetes non-root validation. + +### 5.4 Suggested production improvements +For a real production deployment, I would additionally introduce: +1. `ConfigMap` and `Secret` resources. +2. `HorizontalPodAutoscaler`. +3. `NetworkPolicy` rules. +4. `PodDisruptionBudget`. +5. immutable image tags instead of `latest`. +6. CI/CD-driven promotion between environments. +7. centralized monitoring, logging, and alerting. + +--- + +## 6. Challenges & Solutions + +### 6.1 Vagrant was more complex than necessary +I initially tried the lab with Vagrant, but the workflow was slower and more fragile than needed for this local setup. I switched to **WSL + Docker Desktop + minikube**, which was simpler and more reliable. + +### 6.2 Docker Desktop / WSL networking behavior +Direct access through `minikube ip` and the NodePort was not reliable in my environment. The stable solution was to verify the application with `kubectl port-forward`. + +### 6.3 `CreateContainerConfigError` +The first deployment failed with: + +```text +Error: container has runAsNonRoot and image has non-numeric user (app), cannot verify user is non-root +``` + +I fixed this by updating the Docker image to use a numeric UID/GID (`10001:10001`) and rebuilding the image inside minikube's Docker environment. + +### 6.4 Rolling update evidence capture +During update verification, one local `port-forward` session was interrupted. To keep the evidence trustworthy, I documented the successful rollout with: +- rollout status +- rollout history +- Deployment environment values after update +- rollback verification with a successful application response + +--- + +## 7. Recommended Local Execution Order + +### 7.1 Prerequisites +- Windows host +- Docker Desktop +- WSL +- `kubectl` installed in WSL +- `minikube` installed in WSL + +### 7.2 Cluster startup +```bash +minikube start --driver=docker +kubectl cluster-info +kubectl get nodes -o wide +``` + +### 7.3 Build and deploy +```bash +eval $(minikube -p minikube docker-env) +docker build -t dorley174/devops-info-service:latest ./app_python +./k8s/deploy.sh +``` + +### 7.4 Verify the application +```bash +kubectl port-forward -n devops-lab09 service/devops-info-service 8080:80 +curl http://127.0.0.1:8080/health +curl http://127.0.0.1:8080/ready +curl http://127.0.0.1:8080/ | python3 -m json.tool +``` + +### 7.5 Scale, update, rollback +Use the commands from Section 4. + +--- + +## 8. Conclusion + +The lab requirements for the base task were completed: +- local Kubernetes cluster started successfully +- application deployed with 3 replicas +- Service exposed through NodePort +- readiness and liveness probes implemented +- resource requests and limits configured +- Deployment scaled to 5 replicas +- rolling update demonstrated +- rollback demonstrated +- evidence collected into `k8s/evidence/` + +The repository also contains prepared bonus manifests for a second app and Ingress. diff --git a/k8s/collect-evidence.sh b/k8s/collect-evidence.sh new file mode 100644 index 0000000000..22d70da958 --- /dev/null +++ b/k8s/collect-evidence.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +set -euo pipefail + +OUT_DIR="k8s/evidence" +NS="devops-lab09" +APP="devops-info-service" + +mkdir -p "$OUT_DIR" + +kubectl cluster-info > "$OUT_DIR/01-cluster-info.txt" +kubectl get nodes -o wide > "$OUT_DIR/02-get-nodes.txt" +kubectl get all -n "$NS" -o wide > "$OUT_DIR/03-get-all.txt" +kubectl get pods,svc -n "$NS" -o wide > "$OUT_DIR/04-get-pods-svc.txt" +kubectl describe deployment "$APP" -n "$NS" > "$OUT_DIR/05-describe-deployment.txt" +kubectl rollout history deployment/"$APP" -n "$NS" > "$OUT_DIR/06-rollout-history.txt" +kubectl get ingress -n "$NS" -o wide > "$OUT_DIR/07-get-ingress.txt" 2>/dev/null || true +kubectl get events -n "$NS" --sort-by=.metadata.creationTimestamp > "$OUT_DIR/08-events.txt" + +echo "Evidence saved to $OUT_DIR" diff --git a/k8s/curl_result.txt b/k8s/curl_result.txt new file mode 100644 index 0000000000..8725f74d33 --- /dev/null +++ b/k8s/curl_result.txt @@ -0,0 +1,55 @@ + % Total % Received % Xferd Average Speed Time Time Time Current + Dload Upload Total Spent Left Speed +100 917 100 917 0 0 20928 0 --:--:-- --:--:-- --:--:-- 21325 +{ + "endpoints": [ + { + "description": "Service information", + "method": "GET", + "path": "/" + }, + { + "description": "Liveness health check", + "method": "GET", + "path": "/health" + }, + { + "description": "Readiness health check", + "method": "GET", + "path": "/ready" + }, + { + "description": "Prometheus metrics", + "method": "GET", + "path": "/metrics" + } + ], + "request": { + "client_ip": "127.0.0.1", + "method": "GET", + "path": "/", + "user_agent": "curl/8.5.0" + }, + "runtime": { + "current_time": "2026-03-26T19:57:23.089Z", + "timezone": "UTC", + "uptime_human": "0 hours, 1 minute", + "uptime_seconds": 61 + }, + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "message": "Lab 09 primary deployment", + "name": "devops-info-service", + "variant": "app1", + "version": "lab09-v1" + }, + "system": { + "architecture": "x86_64", + "cpu_count": 20, + "hostname": "devops-info-service-7b48589b6b-2b97b", + "platform": "Linux", + "platform_version": "Linux-5.15.153.1-microsoft-standard-WSL2-x86_64-with-glibc2.36", + "python_version": "3.13.1" + } +} \ No newline at end of file diff --git a/k8s/deploy-bonus.sh b/k8s/deploy-bonus.sh new file mode 100644 index 0000000000..f099f39332 --- /dev/null +++ b/k8s/deploy-bonus.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +kubectl apply -f k8s/deployment-app2.yml +kubectl apply -f k8s/service-app2.yml +kubectl apply -f k8s/ingress.yml +kubectl rollout status deployment/devops-info-service-app2 -n devops-lab09 +kubectl get ingress,pods,svc -n devops-lab09 -o wide diff --git a/k8s/deploy.sh b/k8s/deploy.sh new file mode 100644 index 0000000000..baef96f8f4 --- /dev/null +++ b/k8s/deploy.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +kubectl apply -f k8s/namespace.yml +kubectl apply -f k8s/deployment.yml +kubectl apply -f k8s/service.yml +kubectl rollout status deployment/devops-info-service -n devops-lab09 +kubectl get pods,svc -n devops-lab09 -o wide diff --git a/k8s/deployment-app2.yml b/k8s/deployment-app2.yml new file mode 100644 index 0000000000..4a0b472fde --- /dev/null +++ b/k8s/deployment-app2.yml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: devops-info-service-app2 + namespace: devops-lab09 + labels: + app.kubernetes.io/name: devops-info-service-app2 + app.kubernetes.io/component: web + app.kubernetes.io/part-of: devops-core-course +spec: + replicas: 2 + revisionHistoryLimit: 10 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app.kubernetes.io/name: devops-info-service-app2 + template: + metadata: + labels: + app.kubernetes.io/name: devops-info-service-app2 + app.kubernetes.io/component: web + app.kubernetes.io/part-of: devops-core-course + spec: + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: devops-info-service-app2 + image: dorley174/devops-info-service:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 5000 + protocol: TCP + env: + - name: PORT + value: "5000" + - name: DEBUG + value: "False" + - name: APP_VARIANT + value: "app2" + - name: APP_MESSAGE + value: "Lab 09 bonus deployment" + - name: SERVICE_VERSION + value: "lab09-bonus" + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "250m" + memory: "256Mi" + readinessProbe: + httpGet: + path: /ready + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 2 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true diff --git a/k8s/deployment.yml b/k8s/deployment.yml new file mode 100644 index 0000000000..283a808676 --- /dev/null +++ b/k8s/deployment.yml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: devops-info-service + namespace: devops-lab09 + labels: + app.kubernetes.io/name: devops-info-service + app.kubernetes.io/component: web + app.kubernetes.io/part-of: devops-core-course +spec: + replicas: 3 + revisionHistoryLimit: 10 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app.kubernetes.io/name: devops-info-service + template: + metadata: + labels: + app.kubernetes.io/name: devops-info-service + app.kubernetes.io/component: web + app.kubernetes.io/part-of: devops-core-course + spec: + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: devops-info-service + image: dorley174/devops-info-service:latest + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 5000 + protocol: TCP + env: + - name: PORT + value: "5000" + - name: DEBUG + value: "False" + - name: APP_VARIANT + value: "app1" + - name: APP_MESSAGE + value: "Lab 09 primary deployment" + - name: SERVICE_VERSION + value: "lab09-v1" + resources: + requests: + cpu: "100m" + memory: "128Mi" + limits: + cpu: "250m" + memory: "256Mi" + readinessProbe: + httpGet: + path: /ready + port: http + initialDelaySeconds: 5 + periodSeconds: 5 + timeoutSeconds: 2 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /health + port: http + initialDelaySeconds: 15 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + runAsNonRoot: true diff --git a/k8s/evidence/01-cluster-info.txt b/k8s/evidence/01-cluster-info.txt new file mode 100644 index 0000000000..378eadb21b --- /dev/null +++ b/k8s/evidence/01-cluster-info.txt @@ -0,0 +1,4 @@ +Kubernetes control plane is running at https://127.0.0.1:60412 +CoreDNS is running at https://127.0.0.1:60412/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy + +To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'. diff --git a/k8s/evidence/02-get-nodes.txt b/k8s/evidence/02-get-nodes.txt new file mode 100644 index 0000000000..d9a2b26a89 --- /dev/null +++ b/k8s/evidence/02-get-nodes.txt @@ -0,0 +1,2 @@ +NAME STATUS ROLES AGE VERSION INTERNAL-IP EXTERNAL-IP OS-IMAGE KERNEL-VERSION CONTAINER-RUNTIME +minikube Ready control-plane 14m v1.35.1 192.168.49.2 Debian GNU/Linux 12 (bookworm) 5.15.153.1-microsoft-standard-WSL2 docker://29.2.1 diff --git a/k8s/evidence/03-get-all.txt b/k8s/evidence/03-get-all.txt new file mode 100644 index 0000000000..ae2d1d6915 --- /dev/null +++ b/k8s/evidence/03-get-all.txt @@ -0,0 +1,14 @@ +NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES +pod/devops-info-service-7b48589b6b-2cf77 1/1 Running 0 5m26s 10.244.0.6 minikube +pod/devops-info-service-7b48589b6b-52j4f 1/1 Running 0 5m9s 10.244.0.8 minikube +pod/devops-info-service-7b48589b6b-wrvvj 1/1 Running 0 5m19s 10.244.0.7 minikube + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR +service/devops-info-service NodePort 10.100.203.165 80:30080/TCP 13m app.kubernetes.io/name=devops-info-service + +NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR +deployment.apps/devops-info-service 3/3 3 3 13m devops-info-service dorley174/devops-info-service:latest app.kubernetes.io/name=devops-info-service + +NAME DESIRED CURRENT READY AGE CONTAINERS IMAGES SELECTOR +replicaset.apps/devops-info-service-7b48589b6b 3 3 3 5m26s devops-info-service dorley174/devops-info-service:latest app.kubernetes.io/name=devops-info-service,pod-template-hash=7b48589b6b +replicaset.apps/devops-info-service-8689cb4bbc 0 0 0 13m devops-info-service dorley174/devops-info-service:latest app.kubernetes.io/name=devops-info-service,pod-template-hash=8689cb4bbc diff --git a/k8s/evidence/04-get-pods-svc.txt b/k8s/evidence/04-get-pods-svc.txt new file mode 100644 index 0000000000..037a7c981e --- /dev/null +++ b/k8s/evidence/04-get-pods-svc.txt @@ -0,0 +1,7 @@ +NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES +pod/devops-info-service-7b48589b6b-2cf77 1/1 Running 0 5m27s 10.244.0.6 minikube +pod/devops-info-service-7b48589b6b-52j4f 1/1 Running 0 5m10s 10.244.0.8 minikube +pod/devops-info-service-7b48589b6b-wrvvj 1/1 Running 0 5m20s 10.244.0.7 minikube + +NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR +service/devops-info-service NodePort 10.100.203.165 80:30080/TCP 13m app.kubernetes.io/name=devops-info-service diff --git a/k8s/evidence/05-describe-deployment.txt b/k8s/evidence/05-describe-deployment.txt new file mode 100644 index 0000000000..1efc8774b5 --- /dev/null +++ b/k8s/evidence/05-describe-deployment.txt @@ -0,0 +1,57 @@ +Name: devops-info-service +Namespace: devops-lab09 +CreationTimestamp: Thu, 26 Mar 2026 22:35:19 +0300 +Labels: app.kubernetes.io/component=web + app.kubernetes.io/name=devops-info-service + app.kubernetes.io/part-of=devops-core-course +Annotations: deployment.kubernetes.io/revision: 2 +Selector: app.kubernetes.io/name=devops-info-service +Replicas: 3 desired | 3 updated | 3 total | 3 available | 0 unavailable +StrategyType: RollingUpdate +MinReadySeconds: 0 +RollingUpdateStrategy: 0 max unavailable, 1 max surge +Pod Template: + Labels: app.kubernetes.io/component=web + app.kubernetes.io/name=devops-info-service + app.kubernetes.io/part-of=devops-core-course + Annotations: kubectl.kubernetes.io/restartedAt: 2026-03-26T22:42:54+03:00 + Containers: + devops-info-service: + Image: dorley174/devops-info-service:latest + Port: 5000/TCP (http) + Host Port: 0/TCP (http) + Limits: + cpu: 250m + memory: 256Mi + Requests: + cpu: 100m + memory: 128Mi + Liveness: http-get http://:http/health delay=15s timeout=2s period=10s #success=1 #failure=3 + Readiness: http-get http://:http/ready delay=5s timeout=2s period=5s #success=1 #failure=3 + Environment: + PORT: 5000 + DEBUG: False + APP_VARIANT: app1 + APP_MESSAGE: Lab 09 primary deployment + SERVICE_VERSION: lab09-v1 + Mounts: + Volumes: + Node-Selectors: + Tolerations: +Conditions: + Type Status Reason + ---- ------ ------ + Available True MinimumReplicasAvailable + Progressing True NewReplicaSetAvailable +OldReplicaSets: devops-info-service-8689cb4bbc (0/0 replicas created) +NewReplicaSet: devops-info-service-7b48589b6b (3/3 replicas created) +Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal ScalingReplicaSet 13m deployment-controller Scaled up replica set devops-info-service-8689cb4bbc from 0 to 3 + Normal ScalingReplicaSet 5m27s deployment-controller Scaled up replica set devops-info-service-7b48589b6b from 0 to 1 + Normal ScalingReplicaSet 5m20s deployment-controller Scaled down replica set devops-info-service-8689cb4bbc from 3 to 2 + Normal ScalingReplicaSet 5m20s deployment-controller Scaled up replica set devops-info-service-7b48589b6b from 1 to 2 + Normal ScalingReplicaSet 5m10s deployment-controller Scaled down replica set devops-info-service-8689cb4bbc from 2 to 1 + Normal ScalingReplicaSet 5m10s deployment-controller Scaled up replica set devops-info-service-7b48589b6b from 2 to 3 + Normal ScalingReplicaSet 5m2s deployment-controller Scaled down replica set devops-info-service-8689cb4bbc from 1 to 0 diff --git a/k8s/evidence/06-rollout-history.txt b/k8s/evidence/06-rollout-history.txt new file mode 100644 index 0000000000..4e3de9fb60 --- /dev/null +++ b/k8s/evidence/06-rollout-history.txt @@ -0,0 +1,5 @@ +deployment.apps/devops-info-service +REVISION CHANGE-CAUSE +1 +2 + diff --git a/k8s/evidence/07-get-ingress.txt b/k8s/evidence/07-get-ingress.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/k8s/evidence/08-events.txt b/k8s/evidence/08-events.txt new file mode 100644 index 0000000000..953684f826 --- /dev/null +++ b/k8s/evidence/08-events.txt @@ -0,0 +1,38 @@ +LAST SEEN TYPE REASON OBJECT MESSAGE +13m Normal SuccessfulCreate replicaset/devops-info-service-8689cb4bbc Created pod: devops-info-service-8689cb4bbc-49c68 +13m Normal Scheduled pod/devops-info-service-8689cb4bbc-gbntr Successfully assigned devops-lab09/devops-info-service-8689cb4bbc-gbntr to minikube +13m Normal ScalingReplicaSet deployment/devops-info-service Scaled up replica set devops-info-service-8689cb4bbc from 0 to 3 +13m Normal Scheduled pod/devops-info-service-8689cb4bbc-49c68 Successfully assigned devops-lab09/devops-info-service-8689cb4bbc-49c68 to minikube +13m Normal SuccessfulCreate replicaset/devops-info-service-8689cb4bbc Created pod: devops-info-service-8689cb4bbc-gbntr +13m Normal SuccessfulCreate replicaset/devops-info-service-8689cb4bbc Created pod: devops-info-service-8689cb4bbc-tmcsb +13m Normal Scheduled pod/devops-info-service-8689cb4bbc-tmcsb Successfully assigned devops-lab09/devops-info-service-8689cb4bbc-tmcsb to minikube +7m19s Warning Failed pod/devops-info-service-8689cb4bbc-49c68 Error: container has runAsNonRoot and image has non-numeric user (app), cannot verify user is non-root (pod: "devops-info-service-8689cb4bbc-49c68_devops-lab09(73e9a23a-7db6-421c-b843-970354f1e086)", container: devops-info-service) +7m19s Normal Pulled pod/devops-info-service-8689cb4bbc-49c68 Container image "dorley174/devops-info-service:latest" already present on machine and can be accessed by the pod +7m21s Normal Pulled pod/devops-info-service-8689cb4bbc-gbntr Container image "dorley174/devops-info-service:latest" already present on machine and can be accessed by the pod +7m18s Warning Failed pod/devops-info-service-8689cb4bbc-tmcsb Error: container has runAsNonRoot and image has non-numeric user (app), cannot verify user is non-root (pod: "devops-info-service-8689cb4bbc-tmcsb_devops-lab09(67a638c8-08d0-4a24-a638-9a1e6b758032)", container: devops-info-service) +7m18s Normal Pulled pod/devops-info-service-8689cb4bbc-tmcsb Container image "dorley174/devops-info-service:latest" already present on machine and can be accessed by the pod +7m21s Warning Failed pod/devops-info-service-8689cb4bbc-gbntr Error: container has runAsNonRoot and image has non-numeric user (app), cannot verify user is non-root (pod: "devops-info-service-8689cb4bbc-gbntr_devops-lab09(6221b7e7-e23d-42bd-9957-c4efe897415f)", container: devops-info-service) +5m29s Normal ScalingReplicaSet deployment/devops-info-service Scaled up replica set devops-info-service-7b48589b6b from 0 to 1 +5m29s Normal SuccessfulCreate replicaset/devops-info-service-7b48589b6b Created pod: devops-info-service-7b48589b6b-2cf77 +5m29s Normal Scheduled pod/devops-info-service-7b48589b6b-2cf77 Successfully assigned devops-lab09/devops-info-service-7b48589b6b-2cf77 to minikube +5m28s Normal Started pod/devops-info-service-7b48589b6b-2cf77 Container started +5m28s Normal Created pod/devops-info-service-7b48589b6b-2cf77 Container created +5m28s Normal Pulled pod/devops-info-service-7b48589b6b-2cf77 Container image "dorley174/devops-info-service:latest" already present on machine and can be accessed by the pod +5m22s Normal SuccessfulDelete replicaset/devops-info-service-8689cb4bbc Deleted pod: devops-info-service-8689cb4bbc-49c68 +5m22s Normal Scheduled pod/devops-info-service-7b48589b6b-wrvvj Successfully assigned devops-lab09/devops-info-service-7b48589b6b-wrvvj to minikube +5m22s Normal SuccessfulCreate replicaset/devops-info-service-7b48589b6b Created pod: devops-info-service-7b48589b6b-wrvvj +5m22s Normal ScalingReplicaSet deployment/devops-info-service Scaled up replica set devops-info-service-7b48589b6b from 1 to 2 +5m22s Normal ScalingReplicaSet deployment/devops-info-service Scaled down replica set devops-info-service-8689cb4bbc from 3 to 2 +5m21s Normal Pulled pod/devops-info-service-7b48589b6b-wrvvj Container image "dorley174/devops-info-service:latest" already present on machine and can be accessed by the pod +5m21s Normal Created pod/devops-info-service-7b48589b6b-wrvvj Container created +5m21s Normal Started pod/devops-info-service-7b48589b6b-wrvvj Container started +5m12s Normal SuccessfulCreate replicaset/devops-info-service-7b48589b6b Created pod: devops-info-service-7b48589b6b-52j4f +5m12s Normal SuccessfulDelete replicaset/devops-info-service-8689cb4bbc Deleted pod: devops-info-service-8689cb4bbc-gbntr +5m12s Normal Scheduled pod/devops-info-service-7b48589b6b-52j4f Successfully assigned devops-lab09/devops-info-service-7b48589b6b-52j4f to minikube +5m12s Normal ScalingReplicaSet deployment/devops-info-service Scaled down replica set devops-info-service-8689cb4bbc from 2 to 1 +5m12s Normal ScalingReplicaSet deployment/devops-info-service Scaled up replica set devops-info-service-7b48589b6b from 2 to 3 +5m11s Normal Started pod/devops-info-service-7b48589b6b-52j4f Container started +5m11s Normal Created pod/devops-info-service-7b48589b6b-52j4f Container created +5m11s Normal Pulled pod/devops-info-service-7b48589b6b-52j4f Container image "dorley174/devops-info-service:latest" already present on machine and can be accessed by the pod +5m4s Normal SuccessfulDelete replicaset/devops-info-service-8689cb4bbc Deleted pod: devops-info-service-8689cb4bbc-tmcsb +5m4s Normal ScalingReplicaSet deployment/devops-info-service Scaled down replica set devops-info-service-8689cb4bbc from 1 to 0 diff --git a/k8s/evidence/09-deployment-before.txt b/k8s/evidence/09-deployment-before.txt new file mode 100644 index 0000000000..1564dc93c0 --- /dev/null +++ b/k8s/evidence/09-deployment-before.txt @@ -0,0 +1,2 @@ +NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR +devops-info-service 3/3 3 3 29m devops-info-service dorley174/devops-info-service:latest app.kubernetes.io/name=devops-info-service diff --git a/k8s/evidence/10-pods-before.txt b/k8s/evidence/10-pods-before.txt new file mode 100644 index 0000000000..69ceb653d0 --- /dev/null +++ b/k8s/evidence/10-pods-before.txt @@ -0,0 +1,4 @@ +NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES +devops-info-service-7b48589b6b-2b97b 1/1 Running 0 8m24s 10.244.0.14 minikube +devops-info-service-7b48589b6b-7fttq 1/1 Running 0 8m17s 10.244.0.15 minikube +devops-info-service-7b48589b6b-zhkpz 1/1 Running 0 8m10s 10.244.0.16 minikube diff --git a/k8s/evidence/11-pods-after-scale.txt b/k8s/evidence/11-pods-after-scale.txt new file mode 100644 index 0000000000..5936628d54 --- /dev/null +++ b/k8s/evidence/11-pods-after-scale.txt @@ -0,0 +1,6 @@ +NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES +devops-info-service-7b48589b6b-2b97b 1/1 Running 0 8m32s 10.244.0.14 minikube +devops-info-service-7b48589b6b-7fttq 1/1 Running 0 8m25s 10.244.0.15 minikube +devops-info-service-7b48589b6b-jjtsm 1/1 Running 0 8s 10.244.0.17 minikube +devops-info-service-7b48589b6b-wmf6g 1/1 Running 0 7s 10.244.0.18 minikube +devops-info-service-7b48589b6b-zhkpz 1/1 Running 0 8m18s 10.244.0.16 minikube diff --git a/k8s/evidence/12-deployment-after-scale.txt b/k8s/evidence/12-deployment-after-scale.txt new file mode 100644 index 0000000000..6f3d84e203 --- /dev/null +++ b/k8s/evidence/12-deployment-after-scale.txt @@ -0,0 +1,2 @@ +NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES SELECTOR +devops-info-service 5/5 5 5 29m devops-info-service dorley174/devops-info-service:latest app.kubernetes.io/name=devops-info-service diff --git a/k8s/evidence/13-rollout-history-after-update.txt b/k8s/evidence/13-rollout-history-after-update.txt new file mode 100644 index 0000000000..76380fc135 --- /dev/null +++ b/k8s/evidence/13-rollout-history-after-update.txt @@ -0,0 +1,5 @@ +deployment.apps/devops-info-service +REVISION CHANGE-CAUSE +1 +4 +5 diff --git a/k8s/evidence/14-env-after-update.txt b/k8s/evidence/14-env-after-update.txt new file mode 100644 index 0000000000..c4222427cf --- /dev/null +++ b/k8s/evidence/14-env-after-update.txt @@ -0,0 +1,4 @@ + - name: APP_MESSAGE + value: Lab 09 rolling update + - name: SERVICE_VERSION + value: lab09-v2 diff --git a/k8s/evidence/15-rollout-history-after-rollback.txt b/k8s/evidence/15-rollout-history-after-rollback.txt new file mode 100644 index 0000000000..e2feb8609c --- /dev/null +++ b/k8s/evidence/15-rollout-history-after-rollback.txt @@ -0,0 +1,5 @@ +deployment.apps/devops-info-service +REVISION CHANGE-CAUSE +1 +5 +6 diff --git a/k8s/evidence/16-env-after-rollback.txt b/k8s/evidence/16-env-after-rollback.txt new file mode 100644 index 0000000000..abd5fafc86 --- /dev/null +++ b/k8s/evidence/16-env-after-rollback.txt @@ -0,0 +1,4 @@ + - name: APP_MESSAGE + value: Lab 09 primary deployment + - name: SERVICE_VERSION + value: lab09-v1 diff --git a/k8s/evidence/17-health.txt b/k8s/evidence/17-health.txt new file mode 100644 index 0000000000..2f5438bd07 --- /dev/null +++ b/k8s/evidence/17-health.txt @@ -0,0 +1 @@ +{"status":"healthy","timestamp":"2026-03-26T19:47:24.216Z","uptime_seconds":264,"variant":"app1"} diff --git a/k8s/evidence/18-ready.txt b/k8s/evidence/18-ready.txt new file mode 100644 index 0000000000..422d3a0176 --- /dev/null +++ b/k8s/evidence/18-ready.txt @@ -0,0 +1 @@ +{"message":"Lab 09 primary deployment","status":"ready","timestamp":"2026-03-26T19:47:24.232Z","uptime_seconds":264,"variant":"app1"} diff --git a/k8s/evidence/19-root-after-rollback.json b/k8s/evidence/19-root-after-rollback.json new file mode 100644 index 0000000000..e618153f29 --- /dev/null +++ b/k8s/evidence/19-root-after-rollback.json @@ -0,0 +1,52 @@ +{ + "endpoints": [ + { + "description": "Service information", + "method": "GET", + "path": "/" + }, + { + "description": "Liveness health check", + "method": "GET", + "path": "/health" + }, + { + "description": "Readiness health check", + "method": "GET", + "path": "/ready" + }, + { + "description": "Prometheus metrics", + "method": "GET", + "path": "/metrics" + } + ], + "request": { + "client_ip": "127.0.0.1", + "method": "GET", + "path": "/", + "user_agent": "curl/8.5.0" + }, + "runtime": { + "current_time": "2026-03-26T19:57:23.089Z", + "timezone": "UTC", + "uptime_human": "0 hours, 1 minute", + "uptime_seconds": 61 + }, + "service": { + "description": "DevOps course info service", + "framework": "Flask", + "message": "Lab 09 primary deployment", + "name": "devops-info-service", + "variant": "app1", + "version": "lab09-v1" + }, + "system": { + "architecture": "x86_64", + "cpu_count": 20, + "hostname": "devops-info-service-7b48589b6b-2b97b", + "platform": "Linux", + "platform_version": "Linux-5.15.153.1-microsoft-standard-WSL2-x86_64-with-glibc2.36", + "python_version": "3.13.1" + } +} diff --git a/k8s/ingress.yml b/k8s/ingress.yml new file mode 100644 index 0000000000..2b6e7d130a --- /dev/null +++ b/k8s/ingress.yml @@ -0,0 +1,32 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: devops-course-ingress + namespace: devops-lab09 + annotations: + nginx.ingress.kubernetes.io/use-regex: "true" + nginx.ingress.kubernetes.io/rewrite-target: /$2 +spec: + ingressClassName: nginx + tls: + - hosts: + - local.example.com + secretName: apps-ingress-tls + rules: + - host: local.example.com + http: + paths: + - path: /app1(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: devops-info-service + port: + number: 80 + - path: /app2(/|$)(.*) + pathType: ImplementationSpecific + backend: + service: + name: devops-info-service-app2 + port: + number: 80 diff --git a/k8s/namespace.yml b/k8s/namespace.yml new file mode 100644 index 0000000000..3bc418d3e7 --- /dev/null +++ b/k8s/namespace.yml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: devops-lab09 + labels: + app.kubernetes.io/part-of: devops-core-course + app.kubernetes.io/managed-by: kubectl diff --git a/k8s/service-app2.yml b/k8s/service-app2.yml new file mode 100644 index 0000000000..14b9aba0df --- /dev/null +++ b/k8s/service-app2.yml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: devops-info-service-app2 + namespace: devops-lab09 + labels: + app.kubernetes.io/name: devops-info-service-app2 + app.kubernetes.io/component: web +spec: + type: NodePort + selector: + app.kubernetes.io/name: devops-info-service-app2 + ports: + - name: http + protocol: TCP + port: 80 + targetPort: http + nodePort: 30081 diff --git a/k8s/service.yml b/k8s/service.yml new file mode 100644 index 0000000000..c27e336f28 --- /dev/null +++ b/k8s/service.yml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: devops-info-service + namespace: devops-lab09 + labels: + app.kubernetes.io/name: devops-info-service + app.kubernetes.io/component: web +spec: + type: NodePort + selector: + app.kubernetes.io/name: devops-info-service + ports: + - name: http + protocol: TCP + port: 80 + targetPort: http + nodePort: 30080