diff --git a/.github/workflows/conditionals.yml b/.github/workflows/conditionals.yml new file mode 100644 index 0000000000..b8f54ea572 --- /dev/null +++ b/.github/workflows/conditionals.yml @@ -0,0 +1,24 @@ +name: Conditionals Demo + +on: push + +jobs: + main-only-job: + runs-on: ubuntu-latest + if: github.event_name == 'push' + steps: + - name: Runs only on main branch + if: github.ref == 'refs/heads/DevOps-practice' + run: echo "This is the main working branch" + + - name: A step that will fail on purpose + id: fail-step + run: exit 1 + continue-on-error: true + + - name: Runs only if previous step failed + if: steps.fail-step.outcome == 'failure' + run: echo "The previous step failed, so this one runs" + + - name: This step continues even after failure + run: echo "Workflow keeps going because of continue-on-error" diff --git a/.github/workflows/day-44-artifact-between-jobs.yml b/.github/workflows/day-44-artifact-between-jobs.yml new file mode 100644 index 0000000000..0032183bc0 --- /dev/null +++ b/.github/workflows/day-44-artifact-between-jobs.yml @@ -0,0 +1,38 @@ +name: Day 44 - Artifact Between Jobs + +on: + push: + branches: + - DevOps-practice + + workflow_dispatch: + +jobs: + generate-file: + runs-on: ubuntu-latest + + steps: + - name: Create artifact file + run: | + mkdir output + echo "This file came from Job 1" > output/message.txt + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: job-message + path: output/message.txt + + use-file: + runs-on: ubuntu-latest + needs: generate-file + + steps: + - name: Download artifact + uses: actions/download-artifact@v4 + with: + name: job-message + path: downloaded + + - name: Print artifact contents + run: cat downloaded/message.txt diff --git a/.github/workflows/day-44-artifacts.yml b/.github/workflows/day-44-artifacts.yml new file mode 100644 index 0000000000..46300f497c --- /dev/null +++ b/.github/workflows/day-44-artifacts.yml @@ -0,0 +1,25 @@ +name: Day 44 - Upload Artifacts + +on: + push: + branches: + - DevOps-practice + + workflow_dispatch: + +jobs: + upload-artifact-demo: + runs-on: ubuntu-latest + + steps: + - name: Generate report file + run: | + mkdir reports + echo "Day 44 test report" > reports/test-report.txt + echo "Workflow completed successfully" >> reports/test-report.txt + + - name: Upload report artifact + uses: actions/upload-artifact@v4 + with: + name: day-44-test-report + path: reports/test-report.txt diff --git a/.github/workflows/day-44-real-tests.yml b/.github/workflows/day-44-real-tests.yml new file mode 100644 index 0000000000..39bc77aa69 --- /dev/null +++ b/.github/workflows/day-44-real-tests.yml @@ -0,0 +1,24 @@ +name: Day 44 - Real Tests + +on: + push: + branches: + - DevOps-practice + + workflow_dispatch: + +jobs: + run-real-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Run test script + run: python scripts/check_day_44.py diff --git a/.github/workflows/day-44-secrets.yml b/.github/workflows/day-44-secrets.yml new file mode 100644 index 0000000000..b1b50b4e2c --- /dev/null +++ b/.github/workflows/day-44-secrets.yml @@ -0,0 +1,27 @@ +name: Day 44 - Secrets + +on: + push: + branches: + - DevOps-practice + + workflow_dispatch: + +jobs: + secrets-demo: + runs-on: ubuntu-latest + + steps: + - name: Check if secret is set + run: | + if [ -n "$MY_SECRET_MESSAGE" ]; then + echo "The secret is set: true" + else + echo "The secret is set: false" + fi + env: + MY_SECRET_MESSAGE: ${{ secrets.MY_SECRET_MESSAGE }} + + - name: Try printing secret directly + run: | + echo "Secret value is ${{ secrets.MY_SECRET_MESSAGE }}" diff --git a/.github/workflows/day-46-docker-test.yml b/.github/workflows/day-46-docker-test.yml new file mode 100644 index 0000000000..d4bc6829eb --- /dev/null +++ b/.github/workflows/day-46-docker-test.yml @@ -0,0 +1,31 @@ +name: Day 46 - Docker Test + +on: + push: + branches: + - main + - DevOps-practice + workflow_dispatch: + +jobs: + docker-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t devops-cicd-lab:test . + + - name: Run container + run: | + docker run -d -p 5000:5000 --name devops-lab devops-cicd-lab:test + sleep 5 + + - name: Test running container + run: curl -f http://localhost:5000 + + - name: Stop container + if: always() + run: docker stop devops-lab diff --git a/.github/workflows/day-47-python-tests.yml b/.github/workflows/day-47-python-tests.yml new file mode 100644 index 0000000000..699f63f47a --- /dev/null +++ b/.github/workflows/day-47-python-tests.yml @@ -0,0 +1,29 @@ +name: Day 47 - Python Tests + +on: + push: + branches: + - main + - DevOps-practice + workflow_dispatch: + +jobs: + python-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests + run: PYTHONPATH=. pytest scripts/test_app.py diff --git a/.github/workflows/day-48-security-scan.yml b/.github/workflows/day-48-security-scan.yml new file mode 100644 index 0000000000..6082247f2a --- /dev/null +++ b/.github/workflows/day-48-security-scan.yml @@ -0,0 +1,26 @@ +name: Day 48 - Docker Security Scan + +on: + push: + branches: + - main + - DevOps-practice + workflow_dispatch: + +jobs: + security-scan: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t devops-cicd-lab:scan . + + - name: Scan Docker image with Trivy + uses: aquasecurity/trivy-action@master + with: + image-ref: devops-cicd-lab:scan + format: table + exit-code: 0 diff --git a/.github/workflows/day-49-final-pipeline.yml b/.github/workflows/day-49-final-pipeline.yml new file mode 100644 index 0000000000..74c29ccc7b --- /dev/null +++ b/.github/workflows/day-49-final-pipeline.yml @@ -0,0 +1,61 @@ +name: Day 49 - Final CI/CD Pipeline + +on: + push: + branches: + - main + - DevOps-practice + workflow_dispatch: + +env: + IMAGE_NAME: devops-cicd-lab + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run Python tests + run: PYTHONPATH=. pytest scripts/test_app.py + + docker: + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set short SHA + run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV + + - name: Build Docker image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest . + docker tag ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:sha-${{ env.SHORT_SHA }} + + - name: Log in to Docker Hub + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/DevOps-practice' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Push Docker image + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/DevOps-practice' + run: | + docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest + docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:sha-${{ env.SHORT_SHA }} diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000000..3a28b4edfc --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,40 @@ +name: Day 45 - Docker Publish + +on: + push: + branches: + - main + - DevOps-practice + workflow_dispatch: + +env: + IMAGE_NAME: devops-cicd-lab + +jobs: + docker-build-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set short SHA + run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV + + - name: Log in to Docker Hub + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/DevOps-practice' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build Docker image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest . + docker tag ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:sha-${{ env.SHORT_SHA }} + + - name: Push Docker image + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/DevOps-practice' + run: | + docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest + docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:sha-${{ env.SHORT_SHA }} diff --git a/.github/workflows/env-vars.yml b/.github/workflows/env-vars.yml new file mode 100644 index 0000000000..12b446b05f --- /dev/null +++ b/.github/workflows/env-vars.yml @@ -0,0 +1,25 @@ +name: Env Vars Demo + +on: push + +env: + APP_NAME: myapp + +jobs: + show-envs: + runs-on: ubuntu-latest + env: + ENVIRONMENT: staging + steps: + - name: Print all variables + env: + VERSION: 1.0.0 + run: | + echo "App name is $APP_NAME" + echo "Environment is $ENVIRONMENT" + echo "Version is $VERSION" + + - name: Print GitHub context info + run: | + echo "Commit SHA is ${{ github.sha }}" + echo "Actor is ${{ github.actor }}" diff --git a/.github/workflows/job-outputs.yml b/.github/workflows/job-outputs.yml new file mode 100644 index 0000000000..d4f407b30e --- /dev/null +++ b/.github/workflows/job-outputs.yml @@ -0,0 +1,20 @@ +name: Job Outputs Demo + +on: push + +jobs: + set-date: + runs-on: ubuntu-latest + outputs: + today: ${{ steps.get-date.outputs.date }} + steps: + - name: Get current date + id: get-date + run: echo "date=$(date)" >> $GITHUB_OUTPUT + + read-date: + runs-on: ubuntu-latest + needs: set-date + steps: + - name: Print the date from the other job + run: echo "The date from set-date job is ${{ needs.set-date.outputs.today }}" diff --git a/.github/workflows/manual.yml b/.github/workflows/manual.yml new file mode 100644 index 0000000000..1054de85e3 --- /dev/null +++ b/.github/workflows/manual.yml @@ -0,0 +1,17 @@ +name: Manual Trigger + +on: + workflow_dispatch: + inputs: + environment: + description: 'Environment to deploy to' + required: true + default: 'staging' + +jobs: + print-env: + runs-on: ubuntu-latest + steps: + - name: Print input + run: echo "Deploying to environment: ${{ github.event.inputs.environment }}" + diff --git a/.github/workflows/matrix.yml b/.github/workflows/matrix.yml new file mode 100644 index 0000000000..704b31f173 --- /dev/null +++ b/.github/workflows/matrix.yml @@ -0,0 +1,21 @@ +name: Matrix Build + +on: push + +jobs: + test: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12"] + os: [ubuntu-latest, windows-latest] + exclude: + - os: windows-latest + python-version: "3.10" + steps: + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Print version + run: python --version diff --git a/.github/workflows/multi-job.yml b/.github/workflows/multi-job.yml new file mode 100644 index 0000000000..d9795f19b9 --- /dev/null +++ b/.github/workflows/multi-job.yml @@ -0,0 +1,24 @@ +name: Multi Job Workflow + +on: push + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Build step + run: echo "Building the app" + + test: + runs-on: ubuntu-latest + needs: build + steps: + - name: Test step + run: echo "Running tests" + + deploy: + runs-on: ubuntu-latest + needs: test + steps: + - name: Deploy step + run: echo "Deploying" diff --git a/.github/workflows/runners.yml b/.github/workflows/runners.yml new file mode 100644 index 0000000000..2adca4317d --- /dev/null +++ b/.github/workflows/runners.yml @@ -0,0 +1,38 @@ +name: Runner Info + +on: push + +jobs: + ubuntu-job: + runs-on: ubuntu-latest + steps: + - name: Print info + run: | + echo "OS: ubuntu-latest" + echo "Hostname: $(hostname)" + echo "Current user: $(whoami)" + + - name: Check installed tools + run: | + docker --version + python --version + node --version + git --version + + windows-job: + runs-on: windows-latest + steps: + - name: Print info + run: | + echo "OS: windows-latest" + echo "Hostname: $env:COMPUTERNAME" + echo "Current user: $env:USERNAME" + + macos-job: + runs-on: macos-latest + steps: + - name: Print info + run: | + echo "OS: macos-latest" + echo "Hostname: $(hostname)" + echo "Current user: $(whoami)" diff --git a/.github/workflows/self-hosted.yml b/.github/workflows/self-hosted.yml new file mode 100644 index 0000000000..085d448d71 --- /dev/null +++ b/.github/workflows/self-hosted.yml @@ -0,0 +1,19 @@ +name: Self-Hosted Job + +on: push + +jobs: + run-on-my-machine: + runs-on: [self-hosted, my-linux-runner] + steps: + - name: Print hostname + run: echo "Hostname is $(hostname)" + + - name: Print working directory + run: echo "Working directory is $(pwd)" + + - name: Create a test file + run: echo "Hello from GitHub Actions" > day42-test.txt + + - name: Show the file was created + run: cat day42-test.txt diff --git a/.github/workflows/smart-pipeline.yml b/.github/workflows/smart-pipeline.yml new file mode 100644 index 0000000000..b80665944e --- /dev/null +++ b/.github/workflows/smart-pipeline.yml @@ -0,0 +1,31 @@ +name: Smart Pipeline + +on: push + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - name: Run lint + run: echo "Linting the code" + + test: + runs-on: ubuntu-latest + steps: + - name: Run tests + run: echo "Running tests" + + summary: + runs-on: ubuntu-latest + needs: [lint, test] + steps: + - name: Print branch type + if: github.ref == 'refs/heads/DevOps-practice' + run: echo "This is a push to the main working branch" + + - name: Print feature branch message + if: github.ref != 'refs/heads/DevOps-practice' + run: echo "This is a push to a feature branch" + + - name: Print commit message + run: echo "Commit message is ${{ github.event.head_commit.message }}" diff --git a/.gitignore b/.gitignore index 269d0f1e86..9c236db29e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ crash.log .pytest_cache/ coverage/ CLAUDE.md +*.tgz diff --git a/2026/day-01/learning-plan.md b/2026/day-01/learning-plan.md new file mode 100644 index 0000000000..e0a1328563 --- /dev/null +++ b/2026/day-01/learning-plan.md @@ -0,0 +1,29 @@ +# My 90 Days DevOps Learning Plan + +## Current Level + +I am a Java and IBM Curam developer with production experience in Kubernetes, OpenShift, AWS, Docker, Linux, and DevOps-related technologies. + +## Why I Am Learning DevOps + +I want to become a strong DevOps engineer and improve my cloud, automation, CI/CD, and Kubernetes skills. + +## My 3 Goals + +1. Deploy applications using Kubernetes +2. Build CI/CD pipelines +3. Improve Linux and troubleshooting skills + +## Core Skills + +- Kubernetes +- AWS +- Shell Scripting + +## Weekly Time Commitment + +2-3 hours daily on weekdays and more time on weekends. + +## Consistency Plan + +I will practice daily, complete tasks consistently, and share my progress publicly. diff --git a/2026/day-02/linux-architecture-notes.md b/2026/day-02/linux-architecture-notes.md new file mode 100644 index 0000000000..7eeced61e3 --- /dev/null +++ b/2026/day-02/linux-architecture-notes.md @@ -0,0 +1,55 @@ +# Linux Architecture Notes + +Author: Asma Saiyed + +# OS + +# What is Linux? + +Linux is Open source operating System (Open Source means code is publicly available ). +Before Linux it was era of UNIX(Limitation of security,error). +1991 Linus Torvaldslaunch Linux, after that 90 percent Applications and servers use linux os. +Ubuntu,Fadora,kali etc these are all falovours of linux. + +# Why DevOps Engineer use Linux? + +DevOps Engineer= Devlopment + Operations(OS-linux different flavous ) +Security high plus mutiuser system + +# How Linux works? + +Ask--Application->Shell->Kernel + +Kernel is a heart of system +its written in C program(written by Lenus torvald) + +Shell is interphase GUI based , when you type command, shell tell to kernal,it will read and compile program 0 or 1. + +# How Internal Process Works in Linux? + +Computer power ON---> electricity provide to motherboard(BIOS) which is hardware(physical part) of computer. +Software start Bootloaders--kernal start process +Kernel first process called Systemd(PID 1) +systems start so all other process also start docker/kuberenetes/ssh +d means daemon(which is running in background) + +# Linux commands: + +ls +cd +which bash +ls -a +pwd +makdir +cat +pwd +echo +date +uptime +df -h +free -h +vim file.txt +echo "Hey"> file.txt +head file.txt -n 1 +head file.txt -n 2 +tail file.txt -n 1 diff --git a/2026/day-03/Screenshot 2026-05-17 015840.png b/2026/day-03/Screenshot 2026-05-17 015840.png new file mode 100644 index 0000000000..f41737d344 Binary files /dev/null and b/2026/day-03/Screenshot 2026-05-17 015840.png differ diff --git a/2026/day-03/Screenshot 2026-05-19 112936.png b/2026/day-03/Screenshot 2026-05-19 112936.png new file mode 100644 index 0000000000..60054bcf7c Binary files /dev/null and b/2026/day-03/Screenshot 2026-05-19 112936.png differ diff --git a/2026/day-03/Screenshot 2026-05-19 114239.png b/2026/day-03/Screenshot 2026-05-19 114239.png new file mode 100644 index 0000000000..60ae4f7e1c Binary files /dev/null and b/2026/day-03/Screenshot 2026-05-19 114239.png differ diff --git a/2026/day-03/Screenshot 2026-05-19 114357.png b/2026/day-03/Screenshot 2026-05-19 114357.png new file mode 100644 index 0000000000..d7297e014b Binary files /dev/null and b/2026/day-03/Screenshot 2026-05-19 114357.png differ diff --git a/2026/day-03/Screenshot 2026-05-19 115914.png b/2026/day-03/Screenshot 2026-05-19 115914.png new file mode 100644 index 0000000000..bfc3df7ab5 Binary files /dev/null and b/2026/day-03/Screenshot 2026-05-19 115914.png differ diff --git a/2026/day-03/Screenshot 2026-05-19 120416.png b/2026/day-03/Screenshot 2026-05-19 120416.png new file mode 100644 index 0000000000..bdd50f4e0f Binary files /dev/null and b/2026/day-03/Screenshot 2026-05-19 120416.png differ diff --git a/2026/day-03/Screenshot 2026-05-19 120852.png b/2026/day-03/Screenshot 2026-05-19 120852.png new file mode 100644 index 0000000000..ad296e2ea3 Binary files /dev/null and b/2026/day-03/Screenshot 2026-05-19 120852.png differ diff --git a/2026/day-03/Screenshot 2026-05-19 122342.png b/2026/day-03/Screenshot 2026-05-19 122342.png new file mode 100644 index 0000000000..0e1fa401d2 Binary files /dev/null and b/2026/day-03/Screenshot 2026-05-19 122342.png differ diff --git a/2026/day-03/Screenshot 2026-05-19 123108.png b/2026/day-03/Screenshot 2026-05-19 123108.png new file mode 100644 index 0000000000..1531326c5b Binary files /dev/null and b/2026/day-03/Screenshot 2026-05-19 123108.png differ diff --git a/2026/day-03/Screenshot 2026-05-19 123405.png b/2026/day-03/Screenshot 2026-05-19 123405.png new file mode 100644 index 0000000000..abb27f2f53 Binary files /dev/null and b/2026/day-03/Screenshot 2026-05-19 123405.png differ diff --git a/2026/day-03/Screenshot 2026-05-19 123601.png b/2026/day-03/Screenshot 2026-05-19 123601.png new file mode 100644 index 0000000000..7394159745 Binary files /dev/null and b/2026/day-03/Screenshot 2026-05-19 123601.png differ diff --git a/2026/day-03/Screenshot 2026-05-19 123739.png b/2026/day-03/Screenshot 2026-05-19 123739.png new file mode 100644 index 0000000000..b35f74d37d Binary files /dev/null and b/2026/day-03/Screenshot 2026-05-19 123739.png differ diff --git a/2026/day-03/Screenshot 2026-05-19 123816.png b/2026/day-03/Screenshot 2026-05-19 123816.png new file mode 100644 index 0000000000..08d2ed7bfe Binary files /dev/null and b/2026/day-03/Screenshot 2026-05-19 123816.png differ diff --git a/2026/day-03/linux-commands-cheatsheet.md b/2026/day-03/linux-commands-cheatsheet.md new file mode 100644 index 0000000000..edd791c04c --- /dev/null +++ b/2026/day-03/linux-commands-cheatsheet.md @@ -0,0 +1,81 @@ +# Process Commands + +# ps aux (display all running prpcess in linux) + +ps means processing status +a means all users +u means detailed user formate +x means show process without terminal/background process +ps aux = show all running process from all user with detailed information +It will shows PID process ID, CPU usage,Mem usage ,STAT process state + +ps process status which shows current shells running process + +# top -> use for real time sysetem monitoring + +checks CPU Usage,Memory Usage,Runnning process , load process +top Live monitoring (shows process updating continously) + +**ps aux static snapshot, take one time snapshot, command finished immediately. good for serching and checkinh (ps aux | grep nginx (to check is nginx running)) +top realtime monitoring , continously refreshes, goog for troubleshootinhg CPU/memory issue(High CPU issue, memory leak,server slownes)** + +# htop | Interactive process viewer | + +# kill PID | Stop process | you have to find out PID process ID whatever you want to kill (ps aux command) + +kill 3200 +it will help to stop stuck process + +# kill -9 PID | Force stop process | + +Forcefully stop/terminate process immediately. +Forcefully destroy process +kill -9 is powerful because: +process cannot ignore it +kernel immediately terminates process But use this command carefullu because unsaved data may be lost +,application may not cleanup properly + +# File Commands + +# | ls | List files | + +# | pwd | Show current directory | + +# | cd | Change directory | + +# | mkdir | Create folder | + +# | rm -rf | Remove files/folders | + +# | cp | Copy files | + +# | mv | Move/rename files | + +# | touch | Create empty file | + +# | cat | View file content | + +# | nano | Edit file | + +# Networking Commnads + +ping google.com -->check connectivity + +ip addr -->show Ip Address + +curl website.com --> test http request + +netstat -tulnp --> check open ports + +ss -tulnp ---> View Listening ports + +dig google.com ---> DNS lookup + +## Log & Service Commands + +| journalctl | View logs | +| journalctl -u nginx | Service logs | +| systemctl status nginx | Service status | +| systemctl restart nginx | Restart service | +| df -h | Disk usage | +| free -m | Memory usage | diff --git a/2026/day-04/Screenshot 2026-05-21 120529.png b/2026/day-04/Screenshot 2026-05-21 120529.png new file mode 100644 index 0000000000..7e3f26aba4 Binary files /dev/null and b/2026/day-04/Screenshot 2026-05-21 120529.png differ diff --git a/2026/day-04/Screenshot 2026-05-21 120837.png b/2026/day-04/Screenshot 2026-05-21 120837.png new file mode 100644 index 0000000000..2605f627cd Binary files /dev/null and b/2026/day-04/Screenshot 2026-05-21 120837.png differ diff --git a/2026/day-04/Screenshot 2026-05-21 124914.png b/2026/day-04/Screenshot 2026-05-21 124914.png new file mode 100644 index 0000000000..ceeedac8c4 Binary files /dev/null and b/2026/day-04/Screenshot 2026-05-21 124914.png differ diff --git a/2026/day-04/Screenshot 2026-05-21 130137.png b/2026/day-04/Screenshot 2026-05-21 130137.png new file mode 100644 index 0000000000..1f947e1f67 Binary files /dev/null and b/2026/day-04/Screenshot 2026-05-21 130137.png differ diff --git a/2026/day-04/Screenshot 2026-05-21 130201.png b/2026/day-04/Screenshot 2026-05-21 130201.png new file mode 100644 index 0000000000..450330ef78 Binary files /dev/null and b/2026/day-04/Screenshot 2026-05-21 130201.png differ diff --git a/2026/day-04/linux-practice.md b/2026/day-04/linux-practice.md new file mode 100644 index 0000000000..4b4e2dca07 --- /dev/null +++ b/2026/day-04/linux-practice.md @@ -0,0 +1,90 @@ +# ps + +Process status because this comamnds shows the status of running processes. +to see which prpgram are currently running in linux chrome running, java running,docker running + +# top + +Because it shows the top resource-consuming processes. +why we use? +CPU usage +Memory usage +Running processes +System performance live + +If server is slow → use top + +# pgrep + +process grep means search +Because it searches processes like grep searches text. + +# systemctl status + +system = Linux system +ctl = control + +Because it controls/manages Linux services. +service running or not +errors +logs +PID + +# systemctl list-units + +list = show +units = services/devices/processes managed by systemd +why we use it? +To display all active units. + +# journalctl -u nginx + +journal = logs +ctl = control/view +-u = unit +Because it views system journal logs. +To troubleshoot service issues. +for example: nginx not starting,Docker failing,Application crashing + +# cron --version + +Cron is a task scheduler. +“Chronos” = time +To run tasks automatically at scheduled times. + +backups +cleanup +scripts +reports + +# sudo service cron status + +sudo = run as admin/root +service = manage Linux services +cron = scheduler service +status = check running state + +# sudo service cron start + +To start cron service manually. + +# crontab -e + +cron = scheduler +tab = table +e = edit +Because it opens cron scheduling table. + +- - - - - echo hello + +# crontab -l + +list +To see all scheduled cron jobs. + +# cat /tmp/test.log + +cat = concatenate +Originally made to join/display file contents. +To read file contents quickly. +path: /tmp/test.log diff --git a/2026/day-05/Screenshot 2026-05-22 113030.png b/2026/day-05/Screenshot 2026-05-22 113030.png new file mode 100644 index 0000000000..8f534e79d3 Binary files /dev/null and b/2026/day-05/Screenshot 2026-05-22 113030.png differ diff --git a/2026/day-05/Screenshot 2026-05-22 113054.png b/2026/day-05/Screenshot 2026-05-22 113054.png new file mode 100644 index 0000000000..e5b0bdef60 Binary files /dev/null and b/2026/day-05/Screenshot 2026-05-22 113054.png differ diff --git a/2026/day-05/Screenshot 2026-05-22 113119.png b/2026/day-05/Screenshot 2026-05-22 113119.png new file mode 100644 index 0000000000..633864c083 Binary files /dev/null and b/2026/day-05/Screenshot 2026-05-22 113119.png differ diff --git a/2026/day-05/Screenshot 2026-05-22 113134.png b/2026/day-05/Screenshot 2026-05-22 113134.png new file mode 100644 index 0000000000..e37b87c2e6 Binary files /dev/null and b/2026/day-05/Screenshot 2026-05-22 113134.png differ diff --git a/2026/day-05/Screenshot 2026-05-22 113146.png b/2026/day-05/Screenshot 2026-05-22 113146.png new file mode 100644 index 0000000000..34324e74d0 Binary files /dev/null and b/2026/day-05/Screenshot 2026-05-22 113146.png differ diff --git a/2026/day-05/Screenshot 2026-05-22 120615.png b/2026/day-05/Screenshot 2026-05-22 120615.png new file mode 100644 index 0000000000..c2a8b41bc7 Binary files /dev/null and b/2026/day-05/Screenshot 2026-05-22 120615.png differ diff --git a/2026/day-05/Screenshot 2026-05-22 120626.png b/2026/day-05/Screenshot 2026-05-22 120626.png new file mode 100644 index 0000000000..7178c815c8 Binary files /dev/null and b/2026/day-05/Screenshot 2026-05-22 120626.png differ diff --git a/2026/day-05/linux-troubleshooting-runbook.md b/2026/day-05/linux-troubleshooting-runbook.md new file mode 100644 index 0000000000..ed341a4297 --- /dev/null +++ b/2026/day-05/linux-troubleshooting-runbook.md @@ -0,0 +1,180 @@ +# Linux Troubleshooting Runbook – SSH Service + +## Target Service + +SSH Service (`ssh`) + +I worked like a DevOps engineer investigating a production Linux server. +The goal was to check if the server was healthy and if the SSH service had any issue. + +# 1. uname -a + +Why I used it: +I first wanted to identify what Linux server I was working on. + +How it works: +This command shows Linux kernel version, architecture, and operating system details. + +What I observed: +The server was running Ubuntu Linux on AWS EC2 with x86_64 architecture. + +Debugging mindset: +Before troubleshooting any issue, DevOps engineers first identify the environment and server details. + +# 2. cat /etc/os-release + +Why I used it: +To check the Ubuntu operating system version. + +How it works: +This file stores Linux distribution information. + +What I observed: +The server was running Ubuntu 26.04 LTS. + +Debugging mindset: +Different Linux versions may behave differently, so checking OS version is important. + +# 3. mkdir /tmp/runbook-demo + +Why I used it: +To verify the filesystem was working correctly. + +How it works: +This command creates a temporary folder. + +What I observed: +The folder was created successfully. + +Debugging mindset: +If the filesystem is damaged or read-only, applications may fail to create files. + +# 4. cp /etc/hosts /tmp/runbook-demo/hosts-copy && ls -l /tmp/runbook-demo + +Why I used it: +To test file copying and permissions. + +How it works: +The command copies a system file into the temporary folder and lists the files. + +What I observed: +The file copied successfully and permissions looked normal. + +Debugging mindset: +This confirms the server can read and write files correctly. + +# 5. top + +Why I used it: +To monitor CPU and memory usage in real time. + +How it works: +This command shows running processes and system resource usage. + +What I observed: +CPU usage was very low and the server was healthy. + +Debugging mindset: +If CPU usage becomes very high, applications and services may slow down or crash. + +--- + +# 6. free -h + +Why I used it: +To check RAM and memory usage. + +How it works: +This command displays total, used, and available memory. + +What I observed: +The server was using low memory and had enough available RAM. + +Debugging mindset: +Low available memory may cause slowness or application failures. + +# 7. df -h + +Why I used it: +To check disk usage and free storage space. + +How it works: +This command displays mounted disks and their usage. + +What I observed: +Only 13% of the root filesystem was used. + +Debugging mindset: +If disk becomes full, logs, applications, and services may stop working. + +--- + +# 8. du -sh /var/log + +Why I used it: +To check how much disk space logs were using. + +How it works: +This command calculates folder size. + +What I observed: +Log folder size was normal. + +Debugging mindset: +Large logs can consume all disk space and cause server problems. + +# 9. ss -tulpn + +Why I used it: +To check listening ports and active services. + +How it works: +This command shows network ports and processes using them. + +What I observed: +SSH service was listening on port 22. + +Debugging mindset: +If SSH is not listening on port 22, remote login will fail. + +# 10. ping google.com + +Why I used it: +To verify network and internet connectivity. + +How it works: +This command sends packets to another server and waits for reply. + +What I observed: +The server successfully reached Google. + +Debugging mindset: +If ping fails, the server may have network or DNS issues. + +# 11. journalctl -u ssh -n 50 + +Why I used it: +To review recent SSH service logs. + +How it works: +This command reads logs generated by the SSH service. + +What I observed: +I noticed multiple failed login attempts and invalid users from public IP addresses. I also saw my successful public key login. + +Debugging mindset: +Logs provide the most important clues during troubleshooting. + +# 12. tail -n 50 /var/log/auth.log + +Why I used it: +To review authentication and login activity. + +How it works: +This command shows the last 50 lines of the authentication log file. + +What I observed: +The logs showed SSH login attempts, authentication failures, and successful sessions. + +Debugging mindset: +Authentication logs help detect login issues and suspicious access attempts. diff --git a/2026/day-06/Screenshot 2026-05-26 112233.png b/2026/day-06/Screenshot 2026-05-26 112233.png new file mode 100644 index 0000000000..d3e1b11fa8 Binary files /dev/null and b/2026/day-06/Screenshot 2026-05-26 112233.png differ diff --git a/2026/day-06/file-io-practice.md b/2026/day-06/file-io-practice.md new file mode 100644 index 0000000000..aab52da03d --- /dev/null +++ b/2026/day-06/file-io-practice.md @@ -0,0 +1,37 @@ +# Day 06 – Linux File Read and Write Practice + +Today I practiced basic Linux file handling commands. I learned how to create a file, write text into it, append new lines, and read the file content using different commands. + +First, I created an empty file using: + +touch notes.txt + +Then I added text into the file using the echo command with `>`: + +echo "Linux is powerful" > notes.txt + +The `>` symbol writes content into the file and replaces old content if the file already has data. + +After that, I added another line using `>>`: + +echo "DevOps uses Linux daily" >> notes.txt + +The `>>` symbol appends new content without deleting existing content. + +Next, I used the `tee` command: + +This command displays output on the terminal and also writes it into the file at the same time. + +To read the full file content, I used: + +cat notes.txt + +To read only the first two lines: + +head -n 2 notes.txt + +To read the last two lines: + +tail -n 2 notes.txt + +This practice helped me understand basic Linux file operations which are very important in DevOps because logs, scripts, and configuration files are all text files. diff --git a/2026/day-07/Screenshot 2026-05-26 114618.png b/2026/day-07/Screenshot 2026-05-26 114618.png new file mode 100644 index 0000000000..34dc69ed62 Binary files /dev/null and b/2026/day-07/Screenshot 2026-05-26 114618.png differ diff --git a/2026/day-07/Screenshot 2026-05-26 114836.png b/2026/day-07/Screenshot 2026-05-26 114836.png new file mode 100644 index 0000000000..d14619c9d8 Binary files /dev/null and b/2026/day-07/Screenshot 2026-05-26 114836.png differ diff --git a/2026/day-07/Screenshot 2026-05-26 114849.png b/2026/day-07/Screenshot 2026-05-26 114849.png new file mode 100644 index 0000000000..e1aeafbc89 Binary files /dev/null and b/2026/day-07/Screenshot 2026-05-26 114849.png differ diff --git a/2026/day-07/Screenshot 2026-05-26 115445.png b/2026/day-07/Screenshot 2026-05-26 115445.png new file mode 100644 index 0000000000..9382dc7ec6 Binary files /dev/null and b/2026/day-07/Screenshot 2026-05-26 115445.png differ diff --git a/2026/day-07/Screenshot 2026-05-26 115811.png b/2026/day-07/Screenshot 2026-05-26 115811.png new file mode 100644 index 0000000000..75433b4e01 Binary files /dev/null and b/2026/day-07/Screenshot 2026-05-26 115811.png differ diff --git a/2026/day-07/Screenshot 2026-05-26 115942.png b/2026/day-07/Screenshot 2026-05-26 115942.png new file mode 100644 index 0000000000..c8bb5ff58e Binary files /dev/null and b/2026/day-07/Screenshot 2026-05-26 115942.png differ diff --git a/2026/day-07/Screenshot 2026-05-26 120350.png b/2026/day-07/Screenshot 2026-05-26 120350.png new file mode 100644 index 0000000000..36d8eada8e Binary files /dev/null and b/2026/day-07/Screenshot 2026-05-26 120350.png differ diff --git a/2026/day-07/Screenshot 2026-05-26 120548.png b/2026/day-07/Screenshot 2026-05-26 120548.png new file mode 100644 index 0000000000..8501412add Binary files /dev/null and b/2026/day-07/Screenshot 2026-05-26 120548.png differ diff --git a/2026/day-07/day-07-linux-fs-and-scenarios.md b/2026/day-07/day-07-linux-fs-and-scenarios.md new file mode 100644 index 0000000000..5ce2f35492 --- /dev/null +++ b/2026/day-07/day-07-linux-fs-and-scenarios.md @@ -0,0 +1,188 @@ +# Day 07 – Linux File System & Troubleshooting Practice + +Today I practiced Linux directories, services, Docker, and file permissions. + +I learned that Linux starts from the root directory `/`. + +I used: + +```bash id="mtc85y" +cd .. +``` + +to move one folder back. + +I used: + +```bash id="jxx10f" +ls +``` + +to see files and folders. + +I used: + +```bash id="b42pwb" +ls -a +``` + +to see hidden files like `.bashrc` and `.ssh`. + +I checked the home directory: + +```bash id="8g6d1o" +cd /home +ls -l +``` + +I saw different user folders like: + +- ubuntu +- yalina +- jamil-mamu + +I learned every user has their own home directory. + +I practiced service troubleshooting: + +```bash id="bpyt2h" +systemctl status myapp +``` + +Output showed: + +```text id="q2trz3" +Unit myapp.service could not be found. +``` + +This means the service does not exist. + +I checked Docker service: + +```bash id="7h93xr" +systemctl status docker +``` + +Docker was also not installed. + +Then I checked available services: + +```bash id="4yrnva" +systemctl list-units --type=service +``` + +I learned: + +- active = running +- failed = crashed +- inactive = stopped +- not found = not installed + +Then I installed Docker. + +```bash id="78m7zi" +sudo apt update +``` + +```bash id="7s11p7" +sudo apt install docker.io -y +``` + +Started Docker: + +```bash id="mo5j58" +sudo systemctl start docker +``` + +Enabled Docker: + +```bash id="1kjn7r" +sudo systemctl enable docker +``` + +Checked Docker status: + +```bash id="y4vkxq" +sudo systemctl status docker +``` + +Checked Docker version: + +```bash id="6j5c8i" +docker --version +``` + +Tested Docker: + +```bash id="ibdfzb" +sudo docker run hello-world +``` + +I also practiced file permissions. + +Created script: + +```bash id="ecxijm" +touch backup.sh +``` + +Checked permissions: + +```bash id="mlkshg" +ls -l backup.sh +``` + +Added execute permission: + +```bash id="lu9bva" +chmod +x backup.sh +``` + +Ran script: + +```bash id="7rlytr" +./backup.sh +``` + +# syslog is the standard protocol for message logging in computing. + +It allows devices such as router, switches , firewall and servers to generate event message and send them to centralized server for storage , analysis and security. + +# grep error /var/log/syslog 2>/dev/null + +grep → search text +error → word to search +/var/log/syslog → log file being checked +2> → redirects errors +/dev/null → black hole in Linux (discards output) + +# du -sh /var/log/\* 2>/dev/null | sort -h | tail -5 + +du -sh /var/log/\* +du → disk usage +-s → summary only +-h → human readable (K, M, G) +Shows size of each file/folder in /var/log + +sort -h it will sort size + +tail -5 shows the last 5 lines +useful for debugging disk full issue, cleaning old logs, monitoring server storage usage + +# cat /etc/hostname + +it will dispaly the sys ip(servername) + +# ls -la ~ + +It shows all files and folders in your home directory, including hidden files, with detailed information. +ls listing file and folder +-l long listing formate (perminssion,owner,size date, filename) -a (shows hidden file ) + +# systemctl list-units nginx + +It shows all active systemd units running on the Linux system. + +# systemctl is-enabled nginx + +Checks whether the nginx service starts automatically when the server boots. diff --git a/2026/day-08/Screenshot 2026-05-28 122510.png b/2026/day-08/Screenshot 2026-05-28 122510.png new file mode 100644 index 0000000000..a8e3c24407 Binary files /dev/null and b/2026/day-08/Screenshot 2026-05-28 122510.png differ diff --git a/2026/day-08/Screenshot 2026-05-28 122654.png b/2026/day-08/Screenshot 2026-05-28 122654.png new file mode 100644 index 0000000000..85726d1dfb Binary files /dev/null and b/2026/day-08/Screenshot 2026-05-28 122654.png differ diff --git a/2026/day-08/Screenshot 2026-05-28 123537.png b/2026/day-08/Screenshot 2026-05-28 123537.png new file mode 100644 index 0000000000..c136e33406 Binary files /dev/null and b/2026/day-08/Screenshot 2026-05-28 123537.png differ diff --git a/2026/day-08/day-08-cloud-deployment.md b/2026/day-08/day-08-cloud-deployment.md new file mode 100644 index 0000000000..bf7fa37ec3 --- /dev/null +++ b/2026/day-08/day-08-cloud-deployment.md @@ -0,0 +1,35 @@ +# Day 08 — Cloud Deployment Notes + +## SSH Connect + +ssh -i day08-key.pem ubuntu@ + +## Update Server + +sudo apt update && sudo apt upgrade -y + +## Install Docker + +sudo apt install docker.io -y + +sudo systemctl start docker + +sudo systemctl enable docker + +docker --version + +Check Docker installed. + +## Install Nginx + +sudo apt install nginx -y + +sudo systemctl start nginx + +sudo systemctl enable nginx + +sudo systemctl status nginx + +## Get Public IP + +curl ifconfig.me diff --git a/2026/day-09/Screenshot 2026-05-28 133428.png b/2026/day-09/Screenshot 2026-05-28 133428.png new file mode 100644 index 0000000000..3437246eff Binary files /dev/null and b/2026/day-09/Screenshot 2026-05-28 133428.png differ diff --git a/2026/day-09/day-09-user-management.md b/2026/day-09/day-09-user-management.md new file mode 100644 index 0000000000..0ec715d377 --- /dev/null +++ b/2026/day-09/day-09-user-management.md @@ -0,0 +1,39 @@ +useradd -m + +Create user with home directory. + +passwd + +Set password. + +groupadd + +Create group. + +usermod -aG + +Add user to group. + +groups username + +Check group membership. + +chmod 775 + +Give: + +owner = rwx +group = rwx +others = r-x +chgrp + +Change group ownership. + +sudo -u username + +Run command as another user. + +# challenges faced + +permission denied while creating files +solved using proper group permission and chmod 775 diff --git a/2026/day-10/README.md b/2026/day-10/README.md index f1c66a14c3..635919980f 100644 --- a/2026/day-10/README.md +++ b/2026/day-10/README.md @@ -1,6 +1,7 @@ # Day 10 – File Permissions & File Operations Challenge ## Task + Master file permissions and basic file operations in Linux. - Create and read files using `touch`, `cat`, `vim` @@ -9,6 +10,7 @@ Master file permissions and basic file operations in Linux. --- ## Expected Output + - A markdown file: `day-10-file-permissions.md` - Screenshots showing permission changes @@ -38,6 +40,7 @@ Master file permissions and basic file operations in Linux. ### Task 3: Understand Permissions (10 minutes) Format: `rwxrwxrwx` (owner-group-others) + - `r` = read (4), `w` = write (2), `x` = execute (1) Check your files: `ls -l devops.txt notes.txt script.sh` @@ -75,27 +78,266 @@ Answer: What are current permissions? Who can read/write/execute? ## Documentation -Create `day-10-file-permissions.md`: +Create `# Day 10 – File Permissions & File Operations Challenge + +## Introduction + +Today I learned Linux file permissions and file operations. I practiced creating files, reading files, changing permissions, and running shell scripts. + +--- + +# Task 1 – Create Files + +## Create Empty File + +```bash id="ypr0m8" +touch devops.txt +``` + +This command created an empty file. + +--- + +## Create notes.txt File + +```bash id="l6zqqj" +echo "Linux permissions are important in DevOps" > notes.txt +``` + +This command created a file and added text inside it. + +--- + +## Create script.sh File + +```bash id="e0o9rp" +vim script.sh +``` + +Added: + +```bash id="sj04u6" +echo "Hello DevOps" +``` + +Saved the file using: + +```bash id="mw70gr" +:wq +``` + +--- + +# Task 2 – Read Files + +## Read notes.txt + +```bash id="18yx8j" +cat notes.txt +``` + +This command displayed file content. + +--- + +## Open script.sh in Read-only Mode + +```bash id="9gk09j" +vim -R script.sh +``` + +This opened the file in read-only mode. + +--- + +## Display First 5 Lines + +```bash id="42mcbf" +head -n 5 /etc/passwd +``` + +This command displayed the first 5 lines of the passwd file. + +--- + +## Display Last 5 Lines + +```bash id="r5jwh0" +tail -n 5 /etc/passwd +``` + +This command displayed the last 5 lines of the passwd file. + +--- + +# Task 3 – Understand Permissions + +## Check Permissions + +```bash id="3e9m0j" +ls -l +``` + +Permission format: + +```text id="jlwmco" +-rw-r--r-- +``` + +Meaning: + +- r = read +- w = write +- x = execute + +Permissions are divided into: + +- Owner +- Group +- Others + +--- + +# Task 4 – Modify Permissions + +## Make script.sh Executable + +```bash id="vfgbks" +chmod +x script.sh +``` + +Run script: + +```bash id="0x0lt9" +./script.sh +``` + +Output: + +```text id="ydh4r3" +Hello DevOps +``` + +--- + +## Make devops.txt Read-only + +```bash id="9j4d8g" +chmod a-w devops.txt +``` + +Removed write permission from all users. + +--- + +## Set notes.txt Permission to 640 + +```bash id="jlwmcs" +chmod 640 notes.txt +``` + +Meaning: + +- Owner → read/write +- Group → read only +- Others → no access + +--- + +## Create project Directory + +```bash id="2w9b2l" +mkdir project +chmod 755 project +``` + +Meaning: + +- Owner → full access +- Group → read and execute +- Others → read and execute + +--- + +# Task 5 – Test Permissions + +## Test Read-only File + +```bash id="ukp9d8" +echo "test" >> devops.txt +``` + +Result: +Permission denied. + +--- + +## Remove Execute Permission + +```bash id="lw67kp" +chmod -x script.sh +./script.sh +``` + +Result: +Permission denied. + +--- + +# Commands Used + +```bash id="b9aym9" +touch devops.txt +echo "Linux permissions are important in DevOps" > notes.txt +vim script.sh +cat notes.txt +vim -R script.sh +head -n 5 /etc/passwd +tail -n 5 /etc/passwd +ls -l +chmod +x script.sh +./script.sh +chmod a-w devops.txt +chmod 640 notes.txt +mkdir project +chmod 755 project +chmod -x script.sh +``` + +--- + +# What I Learned + +1. Linux permissions control file access. +2. chmod command changes file permissions. +3. Execute permission is needed to run scripts. +4. head and tail commands help read files quickly. + `: ```markdown # Day 10 Challenge ## Files Created + [list files] ## Permission Changes + [before/after for each file] ## Commands Used + [your commands] ## What I Learned + [3 key points] ``` --- ## Submission + 1. Navigate to `2026/day-10/` folder 2. Add `day-10-file-permissions.md` with screenshots 3. Commit and push @@ -107,6 +349,7 @@ Create `day-10-file-permissions.md`: Share on LinkedIn about mastering file permissions. Use hashtags: + ``` #90DaysOfDevOps #DevOpsKaJosh diff --git a/2026/day-10/Screenshot 2026-06-01 113611.png b/2026/day-10/Screenshot 2026-06-01 113611.png new file mode 100644 index 0000000000..a1b1420ecf Binary files /dev/null and b/2026/day-10/Screenshot 2026-06-01 113611.png differ diff --git a/2026/day-10/day-10-file-permissions.md b/2026/day-10/day-10-file-permissions.md new file mode 100644 index 0000000000..d5a5672146 --- /dev/null +++ b/2026/day-10/day-10-file-permissions.md @@ -0,0 +1,119 @@ +# Day 10 – File Permissions & File Operations Challenge + +## Introduction + +Today I practiced Linux file permissions and file operations. I learned how to create files, read files, and change permissions in Linux. + +--- + +# Task 1 – Create Files + +First, I created an empty file called devops.txt. + +Then I created notes.txt and added some text inside the file. + +After that, I created script.sh using vim editor and added: +echo "Hello DevOps" + +I checked all files and permissions using ls -l. + +--- + +# Task 2 – Read Files + +I read notes.txt using the cat command. + +Then I opened script.sh in read-only mode. + +I also displayed: + +- first 5 lines of /etc/passwd +- last 5 lines of /etc/passwd + +using head and tail commands. + +--- + +# Task 3 – Understand Permissions + +I learned about Linux permissions: + +- r = read +- w = write +- x = execute + +Permissions are divided into: + +- Owner +- Group +- Others + +Example: +-rw-r--r-- + +This means: + +- Owner can read and write +- Group can read +- Others can read + +--- + +# Task 4 – Modify Permissions + +I made script.sh executable and ran the script successfully. + +Then I changed devops.txt to read-only. + +After that, I changed notes.txt permission to 640. + +I also created a project directory and assigned 755 permission. + +--- + +# Task 5 – Test Permissions + +I tested writing into a read-only file and received a permission denied error. + +Then I removed execute permission from script.sh and tried to run it again. It also showed permission denied. + +--- + +# What I Learned + +1. Linux permissions help control file access. +2. chmod command changes file permissions. +3. Execute permission is needed to run shell scripts. +4. head and tail commands help read large files quickly. + +# Commands Used + +touch devops.txt + +echo "Linux permissions are important in DevOps" > notes.txt + +vim script.sh + +cat notes.txt + +vim -R script.sh + +head -n 5 /etc/passwd + +tail -n 5 /etc/passwd + +ls -l + +chmod +x script.sh + +./script.sh + +chmod a-w devops.txt + +chmod 640 notes.txt + +mkdir project + +chmod 755 project + +chmod -x script.sh diff --git a/2026/day-11/README.md b/2026/day-11/README.md index 128b04f0f1..e45dd8a211 100644 --- a/2026/day-11/README.md +++ b/2026/day-11/README.md @@ -1,6 +1,7 @@ # Day 11 – File Ownership Challenge (chown & chgrp) ## Task + Master file and directory ownership in Linux. - Understand file ownership (user and group) @@ -11,6 +12,7 @@ Master file and directory ownership in Linux. --- ## Expected Output + - A markdown file: `day-11-file-ownership.md` - Screenshots showing ownership changes @@ -39,6 +41,7 @@ Document: What's the difference between owner and group? 5. Verify the changes **Try:** + ```bash sudo chown tokyo devops-file.txt ``` @@ -65,12 +68,12 @@ Using `chown` you can change both owner and group together: 4. Change its owner to `berlin` and group to `heist-team` **Syntax:** `sudo chown owner:group filename` - ---- +sudo chown berlin : heist-team project-config.yml--- ### Task 5: Recursive Ownership (20 minutes) 1. Create directory structure: + ``` mkdir -p heist-project/vault mkdir -p heist-project/plans @@ -95,6 +98,7 @@ Using `chown` you can change both owner and group together: 2. Create groups: `vault-team`, `tech-team` 3. Create directory: `bank-heist/` 4. Create 3 files inside: + ``` touch bank-heist/access-codes.txt touch bank-heist/blueprints.pdf @@ -152,18 +156,23 @@ Create `day-11-file-ownership.md`: # Day 11 Challenge ## Files & Directories Created + [list all files/directories] ## Ownership Changes + [before/after for each file] Example: + - devops-file.txt: user:user → tokyo:heist-team ## Commands Used + [your commands here] ## What I Learned + [3 key points about file ownership] ``` @@ -172,12 +181,15 @@ Example: ## Troubleshooting **Permission denied?** + - Use `sudo` for chown/chgrp operations **Group doesn't exist?** + - Create it first: `sudo groupadd groupname` **User doesn't exist?** + - Create it first: `sudo useradd username` --- @@ -195,6 +207,7 @@ In real DevOps scenarios, you need proper file ownership for: --- ## Submission + 1. Navigate to `2026/day-11/` folder 2. Add `day-11-file-ownership.md` with screenshots 3. Commit and push to your fork @@ -206,6 +219,7 @@ In real DevOps scenarios, you need proper file ownership for: Share on LinkedIn about mastering file ownership. Use hashtags: + ``` #90DaysOfDevOps #DevOpsKaJosh diff --git a/2026/day-11/Screenshot 2026-06-01 131024.png b/2026/day-11/Screenshot 2026-06-01 131024.png new file mode 100644 index 0000000000..97bfc61ae9 Binary files /dev/null and b/2026/day-11/Screenshot 2026-06-01 131024.png differ diff --git a/2026/day-11/Screenshot 2026-06-01 131625.png b/2026/day-11/Screenshot 2026-06-01 131625.png new file mode 100644 index 0000000000..27ccc92923 Binary files /dev/null and b/2026/day-11/Screenshot 2026-06-01 131625.png differ diff --git a/2026/day-11/Screenshot 2026-06-01 132129.png b/2026/day-11/Screenshot 2026-06-01 132129.png new file mode 100644 index 0000000000..b6a7af9fa1 Binary files /dev/null and b/2026/day-11/Screenshot 2026-06-01 132129.png differ diff --git a/2026/day-11/Screenshot 2026-06-01 132324.png b/2026/day-11/Screenshot 2026-06-01 132324.png new file mode 100644 index 0000000000..5cbdaff351 Binary files /dev/null and b/2026/day-11/Screenshot 2026-06-01 132324.png differ diff --git a/2026/day-11/Screenshot 2026-06-01 132840.png b/2026/day-11/Screenshot 2026-06-01 132840.png new file mode 100644 index 0000000000..f3c73e96dd Binary files /dev/null and b/2026/day-11/Screenshot 2026-06-01 132840.png differ diff --git a/2026/day-11/Screenshot 2026-06-01 133348.png b/2026/day-11/Screenshot 2026-06-01 133348.png new file mode 100644 index 0000000000..a60c4c6bcd Binary files /dev/null and b/2026/day-11/Screenshot 2026-06-01 133348.png differ diff --git a/2026/day-11/day-11-file-ownership.md b/2026/day-11/day-11-file-ownership.md new file mode 100644 index 0000000000..b33505d482 --- /dev/null +++ b/2026/day-11/day-11-file-ownership.md @@ -0,0 +1,141 @@ +# Day 11 – File Ownership Notes + +## What I Learned + +Today I learned: + +- file ownership +- group ownership +- recursive ownership + +--- + +# Owner vs Group + +Example: + +-rw-rw-r-- 1 ubuntu ubuntu notes.txt + +- First ubuntu = Owner +- Second ubuntu = Group + +### Difference + +- Owner controls the file +- Group gives access to team users + +--- + +# Important Commands + +## Check Ownership + +ls -l + +--- + +## Change Owner + +sudo chown username filename + +Example: + +sudo chown tokyo devops-file.txt + +--- + +## Change Group + +sudo chgrp groupname filename + +Example: + +sudo chgrp heist-team team-notes.txt + +--- + +## Change Owner and Group Together + +sudo chown owner:group filename + +Example: + +sudo chown professor:heist-team project-config.yaml + +--- + +## Recursive Ownership + +Used for directories and files inside. + +sudo chown -R owner:group directory + +Example: + +sudo chown -R professor:planners geist-project + +--- + +# Common Errors + +## User Does Not Exist + +Error: +invalid user + +Solution: +Create the user first. + +--- + +## Group Does Not Exist + +Solution: +Create the group first. + +--- + +## Wrong Filename + +Example: + +- .yaml +- .yml + +Linux treats them as different files. + +--- + +## Space Around Colon + +Wrong: + +sudo chown professor : planners file.txt + +Correct: + +sudo chown professor:planners file.txt + +--- + +# Why Important in DevOps + +Used for: + +- server security +- shared team access +- Docker containers +- CI/CD pipelines +- log management + +--- + +# Conclusion + +Today I practiced: + +- changing file owners +- changing groups +- recursive ownership + +Now I better understand Linux file management. diff --git a/2026/day-12/Screenshot 2026-06-02 112426.png b/2026/day-12/Screenshot 2026-06-02 112426.png new file mode 100644 index 0000000000..8532ccf65b Binary files /dev/null and b/2026/day-12/Screenshot 2026-06-02 112426.png differ diff --git a/2026/day-12/Screenshot 2026-06-02 112441.png b/2026/day-12/Screenshot 2026-06-02 112441.png new file mode 100644 index 0000000000..e47144da3a Binary files /dev/null and b/2026/day-12/Screenshot 2026-06-02 112441.png differ diff --git a/2026/day-12/Screenshot 2026-06-02 113302.png b/2026/day-12/Screenshot 2026-06-02 113302.png new file mode 100644 index 0000000000..b9ac5341e1 Binary files /dev/null and b/2026/day-12/Screenshot 2026-06-02 113302.png differ diff --git a/2026/day-12/day-12-revision.md b/2026/day-12/day-12-revision.md new file mode 100644 index 0000000000..876739737a --- /dev/null +++ b/2026/day-12/day-12-revision.md @@ -0,0 +1,382 @@ +# Day 12 – Revision Notes (Days 01–11) + +## Goal + +Today I revised all the topics from Days 01–11 and practiced Linux commands again to improve my confidence and understanding. + +This revision helped me remember important Linux basics, file management, permissions, users, groups, troubleshooting, and service management. + +--- + +# Day 01 – Learning Plan + +On Day 01, I created my DevOps learning plan. + +What I learned: + +- Importance of consistency and daily practice +- My goal is to become a DevOps Engineer +- I want strong skills in Linux, Docker, Kubernetes, AWS, and troubleshooting +- Hands-on practice is very important in DevOps + +--- + +# Day 02 – Linux Basics + +I learned basic Linux architecture and terminal usage. + +Topics practiced: + +- Linux directories +- Navigation commands +- Understanding how Linux systems work +- Using terminal commands daily + +Commands practiced: + +- pwd +- ls +- cd +- clear + +What I learned: + +- Linux is command-line based +- Navigation commands are important for daily work + +--- + +# Day 03 – Linux Commands Cheat Sheet + +I practiced important Linux commands. + +Commands practiced: + +- ls -l +- mkdir +- touch +- cp +- mv +- rm +- cat +- history +- whoami + +What I learned: + +- File management commands are used every day +- ls -l is very useful for checking permissions and ownership + +--- + +# Day 04 – Shell Scripting Basics + +I learned simple shell scripting concepts. + +Topics practiced: + +- Variables +- Echo statements +- Simple scripts +- Running script files + +Commands practiced: + +- echo +- nano +- chmod +x + +What I learned: + +- Scripts help automate repetitive tasks +- Linux automation starts with scripting basics + +--- + +# Day 05 – Linux Troubleshooting + +I practiced troubleshooting and process management. + +Commands practiced: + +- ps aux +- top +- systemctl status +- journalctl +- grep + +What I learned: + +- How to check running processes +- How to verify service status +- How to read logs for troubleshooting + +--- + +# Day 06 – File Management + +I practiced working with files and folders. + +Commands practiced: + +- touch +- mkdir +- cp +- mv +- rm + +What I learned: + +- How to create, copy, move, and remove files +- Folder organization is important + +--- + +# Day 07 – File Permissions + +I practiced Linux file permissions. + +Commands practiced: + +- chmod +- ls -l + +What I learned: + +- Read, write, and execute permissions +- Permission numbers like 400, 600, and 755 +- File security is important in Linux + +Example: + +chmod 600 notes.txt + +--- + +# Day 08 – Users and Groups + +I practiced creating users and groups. + +Commands practiced: + +- adduser +- addgroup +- usermod +- id + +What I learned: + +- Linux uses users and groups for access control +- Users can belong to multiple groups + +Example: + +sudo addgroup test + +sudo adduser noumu + +sudo usermod -aG test noumu + +--- + +# Day 09 – Ownership Management + +I practiced ownership commands. + +Commands practiced: + +- chown +- ls -l + +What I learned: + +- chown changes owner and group +- Ownership controls file access + +Example: + +sudo chown noumu:test test-note.txt + +--- + +# Day 10 – Process and Service Management + +I practiced service monitoring and logs. + +Commands practiced: + +- systemctl status nginx +- journalctl -u nginx +- ps aux + +What I learned: + +- How to check if services are healthy +- How to troubleshoot using logs + +--- + +# Day 11 – File Ownership Challenge + +I revised ownership and permissions again. + +Topics practiced: + +- Ownership verification +- User and group management +- Permission checking + +Commands practiced: + +- ls -l +- chown +- chmod +- id + +What I learned: + +- Ownership and permissions work together +- Linux troubleshooting improves with practice + +--- + +# Day 12 – Revision Practice + +Today I revised all previous topics and practiced again. + +Commands practiced: + +- ls -l +- chmod +- chown +- adduser +- addgroup +- usermod +- systemctl +- journalctl +- cp +- mkdir +- touch + +--- + +# User and Group Practice Example + +Today I created a user and group using the example below. + +Commands used: + +sudo addgroup test + +sudo adduser noumu + +sudo usermod -aG test noumu + +id noumu + +touch test-note.txt + +sudo chown noumu:test test-note.txt + +ls -l + +What I learned: + +- How to create users and groups +- How to verify groups using id +- How to change file ownership +- How to check permissions using ls -l + +--- + +# Important Commands I Remember + +1. ls -l + +- Checks permissions, owner, and group + +2. chmod + +- Changes file permissions + +3. chown + +- Changes ownership + +4. systemctl status + +- Checks service health + +5. journalctl + +- Reads logs for troubleshooting + +6. ps aux + +- Shows running processes + +7. cp + +- Copies files + +8. mkdir + +- Creates folders + +--- + +# Mini Self-Check + +## Which commands save me the most time? + +- ls -l +- systemctl status +- journalctl -u + +These commands help quickly troubleshoot Linux systems. + +--- + +## How do I check if a service is healthy? + +Commands used: + +systemctl status nginx + +ps aux | grep nginx + +journalctl -u nginx + +--- + +## How do I safely change ownership and permissions? + +Example: + +sudo chown noumu:test test-note.txt + +chmod 600 test-note.txt + +--- + +## What will I improve in the next 3 days? + +- Linux troubleshooting +- Docker practice +- Kubernetes basics +- Better command confidence +- More hands-on practice + +--- + +# Key Takeaways + +- Repeating commands improves memory +- Linux permissions and ownership are very important +- Troubleshooting mistakes helped me learn better +- Daily practice is increasing my confidence +- Hands-on learning is the best way to improve in DevOps + +#90DaysOfDevOps +#DevOpsKaJosh +#TrainWithShubham diff --git a/2026/day-13/Screenshot 2026-06-02 114645.png b/2026/day-13/Screenshot 2026-06-02 114645.png new file mode 100644 index 0000000000..b844a3d006 Binary files /dev/null and b/2026/day-13/Screenshot 2026-06-02 114645.png differ diff --git a/2026/day-13/Screenshot 2026-06-02 114837.png b/2026/day-13/Screenshot 2026-06-02 114837.png new file mode 100644 index 0000000000..8802b752ce Binary files /dev/null and b/2026/day-13/Screenshot 2026-06-02 114837.png differ diff --git a/2026/day-13/Screenshot 2026-06-02 115021.png b/2026/day-13/Screenshot 2026-06-02 115021.png new file mode 100644 index 0000000000..02827153ef Binary files /dev/null and b/2026/day-13/Screenshot 2026-06-02 115021.png differ diff --git a/2026/day-13/Screenshot 2026-06-02 115113.png b/2026/day-13/Screenshot 2026-06-02 115113.png new file mode 100644 index 0000000000..89325ce661 Binary files /dev/null and b/2026/day-13/Screenshot 2026-06-02 115113.png differ diff --git a/2026/day-13/day-13-lvm.md b/2026/day-13/day-13-lvm.md new file mode 100644 index 0000000000..640e643b2e --- /dev/null +++ b/2026/day-13/day-13-lvm.md @@ -0,0 +1,270 @@ +# Day 13 – Linux Volume Management (LVM) + +## Goal + +Today I learned Linux Volume Management (LVM). + +LVM helps manage storage in a flexible way. It allows creating, extending, and managing storage volumes easily without changing the entire disk. + +--- + +# What is LVM? + +LVM stands for Linux Volume Management. + +Simple flow: + +Disk → Physical Volume → Volume Group → Logical Volume + +Explanation: + +- Physical Volume (PV) = actual storage device +- Volume Group (VG) = storage pool +- Logical Volume (LV) = usable storage space + +--- + +# Step 1 – Check Current Storage + +Commands used: + +lsblk + +pvs + +vgs + +lvs + +df -h + +What I learned: + +- lsblk shows disks and partitions +- pvs shows physical volumes +- vgs shows volume groups +- lvs shows logical volumes +- df -h shows disk usage + +At the beginning, no LVM volumes existed. + +--- + +# Step 2 – Create Fake Virtual Disk + +Command used: + +dd if=/dev/zero of=/tmp/mydisk.img bs=1M count=200 + +Explanation: + +- Created a 200MB virtual disk file for practice +- Safe for learning on EC2 + +--- + +# Step 3 – Attach Virtual Disk + +Commands used: + +losetup -fP /tmp/mydisk.img + +losetup -a + +What I learned: + +- Attached the fake disk to a loop device +- Linux created device like: + +/dev/loop4 + +This loop device behaves like a real disk. + +--- + +# Step 4 – Create Physical Volume (PV) + +Command used: + +pvcreate /dev/loop4 + +Check command: + +pvs + +What I learned: + +- Physical Volume is the first LVM layer +- Linux can now use this storage for LVM + +--- + +# Step 5 – Create Volume Group (VG) + +Command used: + +vgcreate myvg /dev/loop4 + +Check command: + +vgs + +What I learned: + +- Volume Group combines storage into one pool +- Created VG called: + +myvg + +--- + +# Step 6 – Create Logical Volume (LV) + +Command used: + +lvcreate -L 100M -n mylv myvg + +Check command: + +lvs + +What I learned: + +- Logical Volume is usable storage space +- Created LV called: + +mylv + +--- + +# Step 7 – Format the Logical Volume + +Command used: + +mkfs.ext4 /dev/myvg/mylv + +What I learned: + +- Formatting creates filesystem structure +- EXT4 filesystem was created + +--- + +# Step 8 – Mount the Logical Volume + +Commands used: + +mkdir -p /mnt/mydata + +mount /dev/myvg/mylv /mnt/mydata + +Check command: + +df -h /mnt/mydata + +What I learned: + +- Mounting connects storage to a folder +- The logical volume became usable + +--- + +# Step 9 – Create Test File + +Command used: + +echo "Hello LVM Practice" > /mnt/mydata/test.txt + +Check command: + +cat /mnt/mydata/test.txt + +What I learned: + +- Successfully stored data inside mounted LVM storage + +--- + +# Step 10 – Extend the Logical Volume + +Commands used: + +lvextend -L +50M /dev/myvg/mylv + +resize2fs /dev/myvg/mylv + +Check command: + +df -h /mnt/mydata + +What I learned: + +- LVM storage can be increased dynamically +- resize2fs updates filesystem size after extending volume + +--- + +# Important Commands I Learned + +1. lsblk + +- Shows disks and partitions + +2. pvs + +- Shows physical volumes + +3. vgs + +- Shows volume groups + +4. lvs + +- Shows logical volumes + +5. pvcreate + +- Creates physical volume + +6. vgcreate + +- Creates volume group + +7. lvcreate + +- Creates logical volume + +8. mount + +- Attaches storage to folder + +9. lvextend + +- Increases volume size + +10. resize2fs + +- Expands filesystem size + +--- + +# What I Learned Today + +1. LVM makes storage flexible and easier to manage + +2. Logical volumes can be extended without recreating disks + +3. Linux storage management is very important for servers and DevOps + +--- + +# Key Takeaways + +- LVM is used in real Linux servers +- Storage can be increased dynamically +- Mounting connects storage to directories +- Practice helped me understand Linux storage better + +#90DaysOfDevOps +#DevOpsKaJosh +#TrainWithShubham diff --git a/2026/day-14/Screenshot 2026-06-04 105720.png b/2026/day-14/Screenshot 2026-06-04 105720.png new file mode 100644 index 0000000000..2667c7f5be Binary files /dev/null and b/2026/day-14/Screenshot 2026-06-04 105720.png differ diff --git a/2026/day-14/Screenshot 2026-06-04 105746.png b/2026/day-14/Screenshot 2026-06-04 105746.png new file mode 100644 index 0000000000..2cff7e5407 Binary files /dev/null and b/2026/day-14/Screenshot 2026-06-04 105746.png differ diff --git a/2026/day-14/Screenshot 2026-06-04 105832.png b/2026/day-14/Screenshot 2026-06-04 105832.png new file mode 100644 index 0000000000..6152128317 Binary files /dev/null and b/2026/day-14/Screenshot 2026-06-04 105832.png differ diff --git a/2026/day-14/Screenshot 2026-06-04 110215.png b/2026/day-14/Screenshot 2026-06-04 110215.png new file mode 100644 index 0000000000..1a86894f32 Binary files /dev/null and b/2026/day-14/Screenshot 2026-06-04 110215.png differ diff --git a/2026/day-14/Screenshot 2026-06-04 110502.png b/2026/day-14/Screenshot 2026-06-04 110502.png new file mode 100644 index 0000000000..ef76923171 Binary files /dev/null and b/2026/day-14/Screenshot 2026-06-04 110502.png differ diff --git a/2026/day-14/Screenshot 2026-06-04 110714.png b/2026/day-14/Screenshot 2026-06-04 110714.png new file mode 100644 index 0000000000..bfeadb16e1 Binary files /dev/null and b/2026/day-14/Screenshot 2026-06-04 110714.png differ diff --git a/2026/day-14/day-14-networking.md b/2026/day-14/day-14-networking.md new file mode 100644 index 0000000000..de0efa1fbe --- /dev/null +++ b/2026/day-14/day-14-networking.md @@ -0,0 +1,107 @@ +# OSI Model + +Browser -Application Layer +Security-Presentation Layer +Session-Session Layer +Port-Trasport Layer +IP-Network Layer +MAC-Data linked Layer +Cable-Physiscal Layer + +# TCP/IP + +Application +Transport +Internet +Netwrok Access + +# Where Things Sit in Networking + +IP → Network/Internet layer +TCP/UDP → Transport layer +HTTP/HTTPS → Application layer +DNS → Application layer + +# Note: + +`traceroute` was not installed by default in my Ubuntu instance. +I installed it using: + +sudo apt update +sudo apt install traceroute + +# traceroute google.com + +Traceroute to google.com completed successfully in 7 hops. + +Latency was very low (around 1–2 ms), showing good network connectivity from the AWS instance to Google servers. + +One hop showed `*`, which usually means the router does not respond to traceroute packets, but the route still continued successfully. + +# ss -tulpn + +Which services are running +Which ports are open/listening + +The command displayed listening services and ports. +SSH service listening on port 22. + +# nslookup google.com + +The `nslookup google.com` command successfully resolved Google's domain name to IPv4 and IPv6 addresses. + +The local DNS resolver was running on 127.0.0.53 using port 53. + +This confirms DNS name resolution is working correctly. + +# curl -I https://google.com + +The `curl -I https://google.com` command successfully returned HTTP response headers. + +The server responded with HTTP status `301`, which means the request was redirected to `https://www.google.com/`. + +This confirmed HTTPS connectivity and successful communication with Google's web server. + +# netstat -an | head + +The `netstat -an | head` command displayed active network connections and listening services. + +Findings: + +- SSH service was listening on port 22 +- DNS resolver service was listening on port 53 +- One SSH connection was in ESTABLISHED state +- Some connections were in TIME_WAIT state after closing + +LISTEN indicates waiting for connections, while ESTABLISHED indicates active communication. + +# nc -zv localhost 22 + +The `nc -zv localhost 22` command successfully connected to SSH port 22. + +This confirmed the SSH service was reachable on the local machine. + +If the connection had failed, the next checks would be: + +- Verify SSH service status +- Check firewall/security group rules +- Confirm the service is listening on the correct port + +# Reflection + +## Which command gives the fastest signal when something is broken? + +`ping` gives the fastest basic connectivity check. + +## What layer would you inspect if DNS fails? + +I would inspect the Application layer and Network layer. + +## What if HTTP 500 appears? + +I would check the Application layer, web server logs, and backend service. + +## Two follow-up checks in a real incident + +1. Check service status using `systemctl status` +2. Check logs using `journalctl` diff --git a/2026/day-15/Networking -concept(my-understand-quick-revision.md b/2026/day-15/Networking -concept(my-understand-quick-revision.md new file mode 100644 index 0000000000..a92d77a147 --- /dev/null +++ b/2026/day-15/Networking -concept(my-understand-quick-revision.md @@ -0,0 +1,197 @@ +# Simple Networking Concepts Notes + +## Networking + +```text +Networking = How computers talk to each other +``` + +--- + +## IP Address + +```text +IP Address = Home address of computer/server + +Example: +172.31.3.219 +``` + +--- + +## DNS + +```text +DNS = Internet phonebook + +google.com → 142.250.x.x +``` + +--- + +## Port + +```text +Port = Room number inside computer + +22 → SSH +80 → HTTP +443 → HTTPS +3306 → MySQL +``` + +--- + +## TCP + +```text +TCP = Reliable communication + +Checks: +- packet loss +- order +- delivery +``` + +--- + +## UDP + +```text +UDP = Fast communication without checking + +Used for: +- video calls +- gaming +- streaming +``` + +--- + +## HTTP + +```text +HTTP = Website communication rules +``` + +--- + +## HTTPS + +```text +HTTPS = Secure encrypted HTTP +``` + +--- + +## SSH + +```text +SSH = Remote login into server +``` + +--- + +## Ping + +```text +ping = "Are you alive?" +``` + +--- + +## traceroute + +```text +traceroute = "Which path did packet take?" +``` + +--- + +## curl + +```text +curl = Talk to website from terminal +``` + +--- + +## Router + +```text +Router = Traffic controller of internet +``` + +--- + +## Firewall + +```text +Firewall = Security guard of network +``` + +--- + +## Subnet + +```text +Subnet = Smaller divided network +``` + +--- + +## CIDR + +```text +CIDR (/24) = Size of network + +/24 → 254 usable hosts +/16 → very large network +/28 → very small network +``` + +--- + +## Public IP + +```text +Public IP = Accessible from internet + +Example: +8.8.8.8 +``` + +--- + +## Private IP + +```text +Private IP = Internal network IP + +Examples: +10.x.x.x +172.16.x.x - 172.31.x.x +192.168.x.x +``` + +--- + +## Full Networking Flow + +```text +google.com + ↓ +DNS finds IP + ↓ +TCP creates connection + ↓ +HTTPS secures communication + ↓ +Port 443 used + ↓ +Router sends packets + ↓ +Google server responds + ↓ +Browser shows website +``` diff --git a/2026/day-15/Screenshot 2026-06-04 111845.png b/2026/day-15/Screenshot 2026-06-04 111845.png new file mode 100644 index 0000000000..44707f56b7 Binary files /dev/null and b/2026/day-15/Screenshot 2026-06-04 111845.png differ diff --git a/2026/day-15/Screenshot 2026-06-04 112141.png b/2026/day-15/Screenshot 2026-06-04 112141.png new file mode 100644 index 0000000000..019a983ef4 Binary files /dev/null and b/2026/day-15/Screenshot 2026-06-04 112141.png differ diff --git a/2026/day-15/Screenshot 2026-06-04 112432.png b/2026/day-15/Screenshot 2026-06-04 112432.png new file mode 100644 index 0000000000..ffed12aa62 Binary files /dev/null and b/2026/day-15/Screenshot 2026-06-04 112432.png differ diff --git a/2026/day-15/Screenshot 2026-06-04 112800.png b/2026/day-15/Screenshot 2026-06-04 112800.png new file mode 100644 index 0000000000..2be1c005dd Binary files /dev/null and b/2026/day-15/Screenshot 2026-06-04 112800.png differ diff --git a/2026/day-15/day-15-networking-concepts.md b/2026/day-15/day-15-networking-concepts.md new file mode 100644 index 0000000000..05bcbdf0e2 --- /dev/null +++ b/2026/day-15/day-15-networking-concepts.md @@ -0,0 +1,185 @@ +## Task 1: DNS – How Names Become IPs + +### What happens when we type `google.com` in a browser? + +1. The browser first checks DNS to find the IP address of `google.com`. +2. DNS resolves the domain name into an IP address. +3. The browser connects to Google's server using that IP. +4. HTTP/HTTPS communication starts and the webpage loads. + +--- + +### DNS Record Types + +- **A** → Maps a domain name to an IPv4 address +- **AAAA** → Maps a domain name to an IPv6 address +- **CNAME** → Alias of another domain name +- **MX** → Mail server record for email delivery +- **NS** → Name server responsible for the domain + +--- + +### `dig google.com` Output + +A Record: + +```text +142.250.69.46 +``` + +TTL: + +```text +89 seconds +``` + +Observation: +The `dig` command successfully resolved `google.com` to its IPv4 address and showed the DNS TTL value. + +## Task 2: IP Addressing + +### What is an IPv4 address? + +An IPv4 address is a unique network address used to identify devices on a network. + +It consists of 4 numbers separated by dots, for example: + +```text +192.168.1.10 +``` + +Each section ranges from 0–255. + +--- + +### Public vs Private IP + +- **Public IP** → Accessible over the internet + Example: + + ```text + 8.8.8.8 + ``` + +- **Private IP** → Used inside internal/private networks + Example: + + ```text + 192.168.1.10 + ``` + +--- + +### Private IP Ranges + +- `10.x.x.x` +- `172.16.x.x – 172.31.x.x` +- `192.168.x.x` + +--- + +### `ip addr show` Output + +Main network interface: + +```text +ens5 +``` + +Private IP found: + +```text +172.31.3.219/20 +``` + +Observation: +The EC2 instance was using a private IPv4 address from the `172.16.x.x – 172.31.x.x` private network range. + +## Task 3: CIDR & Subnetting + +### What does `/24` mean? + +`/24` means the first 24 bits are used for the network portion of the IP address. + +Example: + +```text +192.168.1.0/24 +``` + +This subnet contains 256 total IP addresses. + +--- + +### Why do we subnet? + +Subnetting helps: + +- Organize networks +- Reduce broadcast traffic +- Improve security +- Better IP address management + +--- + +### CIDR Table + +| CIDR | Subnet Mask | Total IPs | Usable Hosts | +| ---- | --------------- | --------- | ------------ | +| /24 | 255.255.255.0 | 256 | 254 | +| /16 | 255.255.0.0 | 65,536 | 65,534 | +| /28 | 255.255.255.240 | 16 | 14 | + +--- + +### Host Counts + +- `/24` → 254 usable hosts +- `/16` → 65,534 usable hosts +- `/28` → 14 usable hosts + +## Task 4: Ports – The Doors to Services + +### What is a Port? + +A port is a communication endpoint used by services and applications. + +Ports allow multiple services to run on the same IP address. + +For example: + +- SSH uses port 22 +- HTTP uses port 80 +- HTTPS uses port 443 + +--- + +### Common Ports + +| Port | Service | +| ----- | ------- | +| 22 | SSH | +| 80 | HTTP | +| 443 | HTTPS | +| 53 | DNS | +| 3306 | MySQL | +| 6379 | Redis | +| 27017 | MongoDB | + +--- + +### `ss -tulpn` Output + +Listening ports identified: + +| Port | Service | +| ---- | ------- | +| 22 | SSH | +| 53 | DNS | + +Observation: +The SSH service was listening on port 22 and the DNS resolver service was listening on port 53. + +# Running `curl http://google.com` returned an HTTP 301 redirect response. + +This means Google redirects normal HTTP traffic to another URL or secure HTTPS endpoint. diff --git a/2026/day-16/Screenshot 2026-06-04 135732.png b/2026/day-16/Screenshot 2026-06-04 135732.png new file mode 100644 index 0000000000..58b9ab4a89 Binary files /dev/null and b/2026/day-16/Screenshot 2026-06-04 135732.png differ diff --git a/2026/day-16/day-16-shell-scripting.md b/2026/day-16/day-16-shell-scripting.md new file mode 100644 index 0000000000..d09a066e3d --- /dev/null +++ b/2026/day-16/day-16-shell-scripting.md @@ -0,0 +1,270 @@ +# Day 16 – Shell Scripting Basics + +## Task 1: Your First Script + +### hello.sh + +```bash +#!/bin/bash + +echo "Hello, DevOps!" +``` + +### Commands Used + +```bash +chmod +x hello.sh +./hello.sh +``` + +### Output + +```text +Hello, DevOps! +``` + +### What happens if we remove the shebang line? + +The shebang line tells Linux which interpreter should run the script. + +```bash +#!/bin/bash +``` + +Without the shebang, the script may: + +- run incorrectly +- use a different shell +- show interpreter errors + +--- + +# Task 2: Variables + +### variable.sh + +```bash +#!/bin/bash + +NAME="Asma" +ROLE="DevOps Engineer" + +echo "Hello, I am $NAME and I am a $ROLE" +``` + +### Commands Used + +```bash +chmod +x variable.sh +./variable.sh +``` + +### Output + +```text +Hello, I am Asma and I am a DevOps Engineer +``` + +### Single Quotes vs Double Quotes + +Double quotes allow variable expansion: + +```bash +echo "Hello $NAME" +``` + +Single quotes print variables as plain text: + +```bash +echo 'Hello $NAME' +``` + +--- + +# Task 3: User Input with read + +### greet.sh + +```bash +#!/bin/bash + +read -p "Enter your name: " NAME +read -p "Enter your favourite tool: " TOOL + +echo "Hello $NAME, your favourite tool is $TOOL" +``` + +### Commands Used + +```bash +chmod +x greet.sh +./greet.sh +``` + +### Output + +```text +Enter your name: Asma +Enter your favourite tool: Kubernetes + +Hello Asma, your favourite tool is Kubernetes +``` + +### Observation + +The `read` command allows shell scripts to take user input during execution. + +--- + +# Task 4: If-Else Conditions + +### check_numb.sh + +```bash +#!/bin/bash + +read -p "Enter a number: " NUM + +if [ $NUM -gt 0 ] +then + echo "Positive number" + +elif [ $NUM -lt 0 ] +then + echo "Negative number" + +else + echo "Zero" +fi +``` + +### Commands Used + +```bash +chmod +x check_numb.sh +./check_numb.sh +``` + +### Output Examples + +```text +Enter a number: 1 +Positive number +``` + +```text +Enter a number: -1 +Negative number +``` + +```text +Enter a number: 0 +Zero +``` + +### Observation + +The `if-elif-else` condition allows shell scripts to make decisions based on user input. + +--- + +# File Check Script + +### file.sh + +```bash +#!/bin/bash + +read -p "Enter filename: " FILE + +if [ -f "$FILE" ] +then + echo "File exists" + +else + echo "File does not exist" +fi +``` + +### Commands Used + +```bash +chmod +x file.sh +./file.sh +``` + +### Output Examples + +```text +Enter filename: hello.sh +File exists +``` + +```text +Enter filename: abc.txt +File does not exist +``` + +### Observation + +The `-f` condition checks whether a file exists in the current directory. + +--- + +# Task 5: Combine It All + +### server_check.sh + +```bash +#!/bin/bash + +SERVICE="ssh" + +read -p "Do you want to check the service status? (y/n): " CHOICE + +if [ "$CHOICE" = "y" ] +then + systemctl is-active $SERVICE + +else + echo "Skipped" +fi +``` + +### Commands Used + +```bash +chmod +x server_check.sh +./server_check.sh +``` + +### Output Example 1 + +```text +Do you want to check the service status? (y/n): y +active +``` + +### Output Example 2 + +```text +Do you want to check the service status? (y/n): n +Skipped +``` + +### Observation + +The script combined: + +- variables +- user input +- if-else conditions +- systemctl command execution + +--- + +# What I Learned + +1. Shell scripts automate Linux tasks using Bash commands. +2. Variables and `read` help scripts accept dynamic user input. +3. `if-else` conditions allow scripts to make decisions automatically. diff --git a/2026/day-17/Screenshot 2026-06-04 143401.png b/2026/day-17/Screenshot 2026-06-04 143401.png new file mode 100644 index 0000000000..49d7263dca Binary files /dev/null and b/2026/day-17/Screenshot 2026-06-04 143401.png differ diff --git a/2026/day-17/day-17-scripting.md b/2026/day-17/day-17-scripting.md new file mode 100644 index 0000000000..a220ebe8b4 --- /dev/null +++ b/2026/day-17/day-17-scripting.md @@ -0,0 +1,290 @@ +# Day 17 – Shell Scripting: Loops, Arguments & Error Handling + +# Task 1: For Loop + +## for_loop.sh + +```bash +#!/bin/bash + +for fruit in apple banana mango orange grapes +do + echo "Fruit: $fruit" +done +``` + +## Commands Used + +```bash +chmod +x for_loop.sh +./for_loop.sh +``` + +## Output + +```text +Fruit: apple +Fruit: banana +Fruit: mango +Fruit: orange +Fruit: grapes +``` + +## Observation + +The `for` loop repeats commands for every item in a list. + +--- + +# count.sh + +```bash +#!/bin/bash + +for num in {1..10} +do + echo "Number : $num" +done +``` + +## Commands Used + +```bash +chmod +x count.sh +./count.sh +``` + +## Output + +```text +Number : 1 +Number : 2 +Number : 3 +Number : 4 +Number : 5 +Number : 6 +Number : 7 +Number : 8 +Number : 9 +Number : 10 +``` + +## Observation + +The `for` loop can also repeat commands through a sequence of numbers. + +--- + +# Task 2: While Loop + +## countdown.sh + +```bash +#!/bin/bash + +read -p "Enter a number: " NUM + +while [ $NUM -ge 0 ] +do + echo "Countdown: $NUM" + NUM=$((NUM-1)) +done + +echo "Done!" +``` + +## Commands Used + +```bash +chmod +x countdown.sh +./countdown.sh +``` + +## Output Example + +```text +Enter a number: 6 + +Countdown: 6 +Countdown: 5 +Countdown: 4 +Countdown: 3 +Countdown: 2 +Countdown: 1 +Countdown: 0 + +Done! +``` + +## Observation + +The `while` loop keeps running until the condition becomes false. + +--- + +# Task 3: Command-Line Arguments + +## greetAgain.sh + +```bash +#!/bin/bash + +if [ $# -eq 0 ] +then + echo "Usage: ./greet.sh " + +else + echo "Hello, $1!" +fi +``` + +## Commands Used + +```bash +chmod +x greetAgain.sh +./greetAgain.sh +./greetAgain.sh Asma +``` + +## Output Example 1 + +```text +Usage: ./greet.sh +``` + +## Output Example 2 + +```text +Hello, Asma! +``` + +## Observation + +`$1` represents the first command-line argument passed to the script. + +--- + +# args_demo.sh + +```bash +#!/bin/bash + +echo "Script name: $0" +echo "Total arguments: $#" +echo "All arguments: $@" +``` + +## Commands Used + +```bash +chmod +x args_demo.sh +./args_demo.sh apple mango banana +``` + +## Output + +```text +Script name: ./args_demo.sh +Total arguments: 3 +All arguments: apple mango banana +``` + +## Observation + +- `$0` gives the script name +- `$#` gives total argument count +- `$@` gives all arguments passed to the script + +--- + +# Task 4: Install Packages via Script + +## install_packages.sh + +```bash +#!/bin/bash + +PACKAGES="nginx curl wget" + +for pkg in $PACKAGES +do + if dpkg -s $pkg > /dev/null 2>&1 + then + echo "$pkg is already installed" + + else + echo "Installing $pkg..." + sudo apt install -y $pkg + fi +done +``` + +## Commands Used + +```bash +chmod +x install_packages.sh +./install_packages.sh +``` + +## Output Example + +```text +Installing nginx... + +curl is already installed +wget is already installed +``` + +## Observation + +The script: + +- loops through package names +- checks installation status +- installs missing packages automatically + +--- + +# Task 5: Error Handling + +## safe_script.sh + +```bash +#!/bin/bash + +set -e + +mkdir /tmp/devops-test || echo "Directory already exists" + +cd /tmp/devops-test || echo "Cannot enter directory" + +touch testfile.txt || echo "Cannot create file" + +echo "Script completed successfully" +``` + +## Commands Used + +```bash +chmod +x safe_script.sh +./safe_script.sh +``` + +## Output Example + +```text +Script completed successfully +``` + +## Observation + +- `set -e` stops the script if an error occurs +- `||` handles errors gracefully with custom messages + +--- + +# What I Learned + +1. `for` and `while` loops automate repetitive tasks in shell scripts. +2. Command-line arguments like `$1`, `$#`, and `$@` make scripts dynamic and reusable. +3. Error handling with `set -e` and `||` improves script reliability and troubleshooting. diff --git a/2026/day-17/notes-shell-script-memorization.md b/2026/day-17/notes-shell-script-memorization.md new file mode 100644 index 0000000000..2065dd86a4 --- /dev/null +++ b/2026/day-17/notes-shell-script-memorization.md @@ -0,0 +1,991 @@ +# Shell Scripting Commands – Simple Revision Notes + +## What is Shell Scripting? + +Shell scripting means writing Linux commands inside a file so we can run them again and again automatically. + +Example: + +```bash +./hello.sh +``` + +This runs the commands written inside `hello.sh`. + +--- + +# 1. How We Create a Shell Script + +## Create a script file + +```bash +vim hello.sh +``` + +Purpose: + +```text +Creates or opens a shell script file. +``` + +Memory trick: + +```text +vim = write/edit file +``` + +--- + +## Add shebang + +```bash +#!/bin/bash +``` + +Purpose: + +```text +Tells Linux to run this file using Bash shell. +``` + +Memory trick: + +```text +#! = use this interpreter +/bin/bash = Bash location +``` + +--- + +## Make file executable + +```bash +chmod +x hello.sh +``` + +Purpose: + +```text +Gives permission to run the script. +``` + +Memory trick: + +```text +chmod = change mode/permission ++x = add execute permission +``` + +--- + +## Run script + +```bash +./hello.sh +``` + +Purpose: + +```text +Runs the script from current directory. +``` + +Memory trick: + +```text +./ = run from here +``` + +--- + +# 2. echo Command + +```bash +echo "Hello, DevOps!" +``` + +Purpose: + +```text +Prints text on the screen. +``` + +Memory trick: + +```text +echo = say something +``` + +Example: + +```bash +echo "Script completed successfully" +``` + +--- + +# 3. Variables + +```bash +NAME="Asma" +ROLE="DevOps Engineer" +``` + +Purpose: + +```text +Stores values inside names. +``` + +Memory trick: + +```text +Variable = small box storing value +``` + +Example: + +```bash +echo "Hello, I am $NAME and I am a $ROLE" +``` + +Meaning: + +```text +$NAME prints the value stored in NAME. +$ROLE prints the value stored in ROLE. +``` + +Important rule: + +```text +No spaces around = +``` + +Correct: + +```bash +NAME="Asma" +``` + +Wrong: + +```bash +NAME = "Asma" +``` + +--- + +# 4. Single Quotes vs Double Quotes + +## Double quotes + +```bash +echo "Hello $NAME" +``` + +Purpose: + +```text +Shows variable value. +``` + +Output: + +```text +Hello Asma +``` + +--- + +## Single quotes + +```bash +echo 'Hello $NAME' +``` + +Purpose: + +```text +Prints exactly what is written. +``` + +Output: + +```text +Hello $NAME +``` + +Memory trick: + +```text +Double quotes = variable works +Single quotes = variable sleeps +``` + +--- + +# 5. read Command + +```bash +read -p "Enter your name: " NAME +``` + +Purpose: + +```text +Takes input from user and stores it in variable. +``` + +Memory trick: + +```text +read = ask user +-p = prompt/message +``` + +Example: + +```bash +read -p "Enter your favourite tool: " TOOL +echo "Your favourite tool is $TOOL" +``` + +--- + +# 6. If-Else Condition + +```bash +if [ $NUM -gt 0 ] +then + echo "Positive number" +else + echo "Zero or negative" +fi +``` + +Purpose: + +```text +Makes decision inside script. +``` + +Memory trick: + +```text +if = check +then = do this +else = otherwise +fi = finish if +``` + +Important: + +```text +Spaces are required inside [ ] +``` + +Correct: + +```bash +if [ $NUM -gt 0 ] +``` + +Wrong: + +```bash +if [$NUM -gt 0] +``` + +--- + +# 7. Number Conditions + +```bash +-gt +``` + +Purpose: + +```text +Greater than +``` + +Example: + +```bash +if [ $NUM -gt 0 ] +``` + +--- + +```bash +-lt +``` + +Purpose: + +```text +Less than +``` + +Example: + +```bash +if [ $NUM -lt 0 ] +``` + +--- + +```bash +-eq +``` + +Purpose: + +```text +Equal to +``` + +Example: + +```bash +if [ $# -eq 0 ] +``` + +Memory trick: + +```text +gt = greater than +lt = less than +eq = equal +``` + +--- + +# 8. File Check + +```bash +if [ -f "$FILE" ] +then + echo "File exists" +else + echo "File does not exist" +fi +``` + +Purpose: + +```text +Checks if a file exists. +``` + +Memory trick: + +```text +-f = file +``` + +Example: + +```bash +read -p "Enter filename: " FILE +if [ -f "$FILE" ] +then + echo "File exists" +else + echo "File does not exist" +fi +``` + +--- + +# 9. Service Check + +```bash +systemctl is-active ssh +``` + +Purpose: + +```text +Checks if SSH service is active. +``` + +Memory trick: + +```text +systemctl = control system services +is-active = is service running? +``` + +Example: + +```bash +SERVICE="ssh" +systemctl is-active $SERVICE +``` + +--- + +# 10. For Loop + +```bash +for fruit in apple banana mango orange grapes +do + echo "Fruit: $fruit" +done +``` + +Purpose: + +```text +Repeats work for each item in a list. +``` + +Memory trick: + +```text +for = one by one +do = start work +done = finish loop +``` + +Flow: + +```text +apple → print +banana → print +mango → print +orange → print +grapes → print +``` + +--- + +# 11. For Loop with Numbers + +```bash +for num in {1..10} +do + echo "Number: $num" +done +``` + +Purpose: + +```text +Prints numbers from 1 to 10. +``` + +Memory trick: + +```text +{1..10} = range from 1 to 10 +``` + +--- + +# 12. While Loop + +```bash +while [ $NUM -ge 0 ] +do + echo "Countdown: $NUM" + NUM=$((NUM-1)) +done +``` + +Purpose: + +```text +Keeps running while condition is true. +``` + +Memory trick: + +```text +while = as long as +-ge = greater than or equal +``` + +Example: + +```text +If NUM is 5, keep running until it becomes less than 0. +``` + +--- + +# 13. Arithmetic + +```bash +NUM=$((NUM-1)) +``` + +Purpose: + +```text +Subtracts 1 from NUM. +``` + +Memory trick: + +```text +$(( )) = math box +``` + +Example: + +```bash +COUNT=$((COUNT+1)) +``` + +--- + +# 14. Command-Line Arguments + +## First argument + +```bash +$1 +``` + +Purpose: + +```text +First value passed when running script. +``` + +Example: + +```bash +./greetAgain.sh Asma +``` + +Here: + +```text +$1 = Asma +``` + +--- + +## Script name + +```bash +$0 +``` + +Purpose: + +```text +Shows script name. +``` + +Example: + +```bash +echo "Script name: $0" +``` + +--- + +## Total arguments + +```bash +$# +``` + +Purpose: + +```text +Counts how many arguments were passed. +``` + +Example: + +```bash +./args_demo.sh apple mango banana +``` + +Here: + +```text +$# = 3 +``` + +--- + +## All arguments + +```bash +$@ +``` + +Purpose: + +```text +Shows all arguments. +``` + +Example: + +```bash +echo "All arguments: $@" +``` + +Output: + +```text +apple mango banana +``` + +Memory trick: + +```text +$1 = first item +$0 = script name +$# = number of items +$@ = all items +``` + +--- + +# 15. Package Check + +```bash +dpkg -s nginx +``` + +Purpose: + +```text +Checks if nginx package is installed. +``` + +Memory trick: + +```text +dpkg = Debian package manager +-s = status +``` + +Example: + +```bash +if dpkg -s nginx > /dev/null 2>&1 +then + echo "nginx is already installed" +else + echo "Installing nginx" +fi +``` + +--- + +# 16. Hide Output + +```bash +> /dev/null 2>&1 +``` + +Purpose: + +```text +Hides normal output and error output. +``` + +Simple meaning: + +```text +Do the check silently. +``` + +Memory trick: + +```text +/dev/null = dustbin +``` + +--- + +# 17. Install Package + +```bash +sudo apt install -y nginx +``` + +Purpose: + +```text +Installs nginx package. +``` + +Memory trick: + +```text +sudo = admin power +apt install = install software +-y = yes automatically +``` + +--- + +# 18. Error Handling with set -e + +```bash +set -e +``` + +Purpose: + +```text +Stops script when any command fails. +``` + +Memory trick: + +```text +-e = exit on error +``` + +--- + +# 19. Error Handling with || + +```bash +mkdir /tmp/devops-test || echo "Directory already exists" +``` + +Purpose: + +```text +If first command fails, run second command. +``` + +Memory trick: + +```text +|| = or else +``` + +Meaning: + +```text +Create directory OR ELSE print error message. +``` + +--- + +# 20. mkdir Command + +```bash +mkdir /tmp/devops-test +``` + +Purpose: + +```text +Creates a directory. +``` + +Memory trick: + +```text +mkdir = make directory +``` + +--- + +# 21. cd Command + +```bash +cd /tmp/devops-test +``` + +Purpose: + +```text +Moves into a directory. +``` + +Memory trick: + +```text +cd = change directory +``` + +--- + +# 22. touch Command + +```bash +touch testfile.txt +``` + +Purpose: + +```text +Creates an empty file. +``` + +Memory trick: + +```text +touch = touch/create file +``` + +--- + +# 23. ls Command + +```bash +ls +``` + +Purpose: + +```text +Lists files and folders. +``` + +--- + +# 24. ls -l Command + +```bash +ls -l +``` + +Purpose: + +```text +Lists files with permissions, owner, group, size, and date. +``` + +Memory trick: + +```text +-l = long list +``` + +--- + +# 25. chmod Number Example + +```bash +chmod 776 hello.sh +``` + +Purpose: + +```text +Gives permission using numbers. +``` + +Simple meaning: + +```text +Owner = read/write/execute +Group = read/write/execute +Others = read/write +``` + +Better simple command: + +```bash +chmod +x hello.sh +``` + +Purpose: + +```text +Adds execute permission. +``` + +--- + +# Script Creation Flow + +```text +1. Create file +2. Add shebang +3. Write commands +4. Save file +5. Give execute permission +6. Run script +7. Check output +``` + +Commands: + +```bash +vim script.sh +chmod +x script.sh +./script.sh +``` + +--- + +# Simple Memory Tricks Summary + +```text +echo = say/print +read = ask user +NAME=value = store value +$NAME = use stored value +if = check condition +then = do this +else = otherwise +fi = finish if +for = repeat for each item +while = repeat while true +do = start loop work +done = finish loop +$1 = first argument +$0 = script name +$# = argument count +$@ = all arguments +-f = file exists check +-gt = greater than +-lt = less than +-eq = equal +-ge = greater or equal +chmod +x = make executable +./file.sh = run script +set -e = stop on error +|| = or else +``` + +--- + +# Full Simple Example Flow + +When I run: + +```bash +./greetAgain.sh Asma +``` + +Script sees: + +```text +$0 = ./greetAgain.sh +$1 = Asma +$# = 1 +``` + +Then it prints: + +```text +Hello, Asma! +``` + +--- + +# What I Learned + +1. Shell scripting helps automate Linux and DevOps tasks. +2. Variables, loops, and conditions make scripts smarter. +3. Arguments and error handling make scripts reusable and safer. diff --git a/2026/day-18/Screenshot 2026-06-05 112757.png b/2026/day-18/Screenshot 2026-06-05 112757.png new file mode 100644 index 0000000000..80724e9c52 Binary files /dev/null and b/2026/day-18/Screenshot 2026-06-05 112757.png differ diff --git a/2026/day-18/day-18-scripting.md b/2026/day-18/day-18-scripting.md new file mode 100644 index 0000000000..ce5ba41433 --- /dev/null +++ b/2026/day-18/day-18-scripting.md @@ -0,0 +1,128 @@ +# Day 18 – Shell Scripting Functions & Intermediate Concepts + +## Introduction + +In this lab, I learned how to write cleaner and safer shell scripts using functions and strict mode. +I also practiced checking system information like memory, disk usage, uptime, and CPU usage. + +This lab helped me understand how real DevOps engineers organize reusable scripts for automation and monitoring. + +--- + +# Task 1 – Basic Functions + +I created a script called `function.sh`. + +In this script: + +- I created a greeting function +- I created another function to add two numbers +- I learned how functions make scripts reusable and clean + +### What I Learned + +- Functions help avoid repeating code +- Arguments can be passed inside functions +- Functions improve readability + +--- + +# Task 2 – Disk and Memory Check + +I created `disk_check.sh`. + +This script: + +- Checks disk usage +- Checks memory usage +- Displays system resource information + +### What I Learned + +- How to monitor disk usage +- How to check free memory +- Basic monitoring commands used in Linux + +--- + +# Task 3 – Strict Mode + +I created `strict_demo.sh`. + +In this task, I learned about strict mode using: + +- set -e +- set -u +- set -o pipefail + +### Explanation + +### set -e + +Stops the script immediately if a command fails. + +### set -u + +Stops the script if an undefined variable is used. + +### set -o pipefail + +Stops the pipeline if any command inside the pipeline fails. + +### What I Learned + +Strict mode makes scripts safer and helps avoid hidden errors in automation. + +--- + +# Task 4 – Local Variables + +I created `locl_demo.sh`. + +This task helped me understand: + +- Local variables only work inside functions +- Variables do not leak outside the function +- Local variables help keep scripts clean + +### What I Learned + +Using local variables is important for writing organized scripts. + +--- + +# Task 5 – System Information Reporter + +I created `system_info.sh`. + +This script displays: + +- Hostname and OS information +- System uptime +- Disk usage +- Memory usage +- Top CPU-consuming processes + +I used functions for every section to keep the script modular and easy to understand. + +### What I Learned + +- How to organize larger scripts +- How to separate tasks using functions +- Basic Linux system monitoring + +--- + +# Key Learnings + +1. Functions make scripts reusable and easier to manage. + +2. Strict mode helps create safer and more reliable scripts. + +3. Linux monitoring commands are very useful for DevOps and troubleshooting. + +--- + +# Conclusion + +This lab improved my shell scripting knowledge and helped me understand how scripting is used in real DevOps environments for automation and monitoring. diff --git a/2026/day-19/Screenshot 2026-06-05 114701.png b/2026/day-19/Screenshot 2026-06-05 114701.png new file mode 100644 index 0000000000..d0117db40d Binary files /dev/null and b/2026/day-19/Screenshot 2026-06-05 114701.png differ diff --git a/2026/day-19/Screenshot 2026-06-05 114748.png b/2026/day-19/Screenshot 2026-06-05 114748.png new file mode 100644 index 0000000000..926de0b966 Binary files /dev/null and b/2026/day-19/Screenshot 2026-06-05 114748.png differ diff --git a/2026/day-19/Screenshot 2026-06-05 115336.png b/2026/day-19/Screenshot 2026-06-05 115336.png new file mode 100644 index 0000000000..ee8014c630 Binary files /dev/null and b/2026/day-19/Screenshot 2026-06-05 115336.png differ diff --git a/2026/day-19/backup.sh b/2026/day-19/backup.sh new file mode 100644 index 0000000000..e648a35e04 --- /dev/null +++ b/2026/day-19/backup.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +SOURCE_DIR=$1 +BACKUP_DIR=$2 + +DATE=$(date +%Y-%m-%d) + +BACKUP_FILE="backup-$DATE.tar.gz" + +if [ ! -d "$SOURCE_DIR" ]; then + echo "Source folder not found" + exit 1 +fi + +tar -czf "$BACKUP_DIR/$BACKUP_FILE" "$SOURCE_DIR" + +echo "Backup completed" + +echo "Backup file name: $BACKUP_FILE" + +du -h "$BACKUP_DIR/$BACKUP_FILE" diff --git a/2026/day-19/cheatsheet.md b/2026/day-19/cheatsheet.md new file mode 100644 index 0000000000..675e18d6e6 --- /dev/null +++ b/2026/day-19/cheatsheet.md @@ -0,0 +1,160 @@ +# Day 19 Easy Human Cheat Sheet + +## Folder Work + +mkdir += make new folder + +ls += see files and folders + +cd += go inside folder + +pwd += show current location + +rmdir += remove empty folder + +Memory Trick: + +mkdir → make directory +cd → change directory + +--- + +# File Work + +touch += create new file + +nano += open file editor + +cat += show file content + +chmod +x += give execute permission + +./file.sh += run script + +Memory Trick: + +touch → touch new file +cat → show everything + +--- + +# Log Rotation + +find += search files + +gzip += compress log file + +mtime += old file check + +Memory Trick: + +gzip → zip old logs + +--- + +# Backup Commands + +tar += create backup archive + +date += current date and time + +rm += remove file + +Memory Trick: + +tar → pack files together + +--- + +# Crontab + +crontab -l += show scheduled jobs + +crontab -e += edit cron jobs + +Memory Trick: + +cron = automatic timer + +--- + +# Cron Timing + +0 2 \* \* \* += daily 2 AM + +0 3 \* \* 0 += Sunday 3 AM + +_/5 _ \* \* \* += every 5 minutes + +Memory Trick: + +Minute Hour Day Month Week + +--- + +# Monitoring Commands + +df -h += disk usage + +free -h += memory usage + +uptime += server running time + +ps aux += running processes + +Memory Trick: + +df → disk free +free → free memory + +--- + +# Script Safety + +set -e += stop if error + +set -u += stop undefined variable + +set -o pipefail += fail if pipeline fails + +Memory Trick: + +e → error stop +u → undefined stop + +--- + + +Memory Trick: + +add → prepare +commit → save +push → upload diff --git a/2026/day-19/day-19-project.md b/2026/day-19/day-19-project.md new file mode 100644 index 0000000000..f69bad8740 --- /dev/null +++ b/2026/day-19/day-19-project.md @@ -0,0 +1,171 @@ +# Day 19 – Shell Scripting Project + +## Introduction + +In this lab, I practiced real DevOps shell scripting projects. +I created scripts for log rotation, backup automation, and cron scheduling. + +I also learned how Linux servers automatically manage logs and backups using scripts. + +--- + +# Step 1 – Created Practice Folders + +First, I created folders for logs, backups, and source files. + +## Folders Created + +- myapp_logs +- source_data +- backups + +## Commands Used + +mkdir myapp_logs + +mkdir source_data + +mkdir backups + +--- + +# Step 2 – Created Sample Files + +I created sample log files and test files for backup practice. + +## Commands Used + +touch myapp_logs/app.log + +touch myapp_logs/error.log + +touch myapp_logs/old.log + +echo "This is test file" > source_data/file1.txt + +--- + +# Task 1 – Log Rotation Script + +File Created: `log_rotate.sh` + +## What This Script Does + +- checks if log directory exists +- compresses old log files +- deletes old compressed files +- shows messages for completed tasks + +## Commands Used + +- find +- gzip +- if condition +- echo +- exit + +## What I Learned + +- how old logs are managed +- how file cleanup works +- basic automation using shell scripts + +--- + +# Task 2 – Backup Script + +File Created: `backup.sh` + +## What This Script Does + +- creates backup archive +- adds current date in backup name +- stores backup in backup folder +- removes old backups + +## Commands Used + +- tar +- date +- rm +- echo + +## What I Learned + +- how backup automation works +- how archive files are created +- how Linux manages backup files + +--- + +# Task 3 – Crontab + +I learned how to schedule scripts automatically using cron jobs. + +## Commands Used + +crontab -l + +crontab -e + +## Cron Examples + +Run log rotation every day at 2 AM + +0 2 \* \* \* /home/ubuntu/day-19/log_rotate.sh + +Run backup every Sunday at 3 AM + +0 3 \* \* 0 /home/ubuntu/day-19/backup.sh + +Run health check every 5 minutes + +_/5 _ \* \* \* /home/ubuntu/day-19/health_check.sh + +## What I Learned + +- how cron scheduling works +- how Linux runs scripts automatically +- importance of automation in DevOps + +--- + +# Task 4 – Maintenance Script + +File Created: `maintenance.sh` + +## What This Script Does + +- runs backup script +- runs log rotation script +- saves logs with timestamps + +## Commands Used + +- echo +- date +- script calling + +## What I Learned + +- combining multiple scripts +- automating maintenance tasks +- basic monitoring concepts + +--- + +# Key Learnings + +1. Shell scripting helps automate repetitive tasks. + +2. Backup and log rotation are important for server maintenance. + +3. Cron jobs help run scripts automatically without manual work. + +--- + +# Conclusion + +This lab improved my understanding of Linux automation, backup management, log cleanup, and cron scheduling. + +I also learned how DevOps engineers use shell scripts for daily server maintenance and automation tasks. diff --git a/2026/day-19/disk_check.sh b/2026/day-19/disk_check.sh new file mode 100644 index 0000000000..0316a00900 --- /dev/null +++ b/2026/day-19/disk_check.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +check_disk() { + echo "Disk Usage:" + df -h / +} + +check_memory() { + echo "Memory Usage:" + free -h +} + +check_disk +check_memory diff --git a/2026/day-19/function.sh b/2026/day-19/function.sh new file mode 100644 index 0000000000..93c27f96a9 --- /dev/null +++ b/2026/day-19/function.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +greet(){ + echo "Hello, $1!" +} + +add(){ + sum=$(( $1 + $2 )) + echo "Sum is : $sum" +} + +greet "Asma" +add 5 10 diff --git a/2026/day-19/locl_demo.sh b/2026/day-19/locl_demo.sh new file mode 100644 index 0000000000..3dd97da35d --- /dev/null +++ b/2026/day-19/locl_demo.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +my_function() { + local name="Asma" + echo "Inside function: $name" +} + +my_function + +echo "Outside function: $name" diff --git a/2026/day-19/log_rotate.sh b/2026/day-19/log_rotate.sh new file mode 100644 index 0000000000..6b77ba9ea2 --- /dev/null +++ b/2026/day-19/log_rotate.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -euo pipefail + +LOG_DIR=$1 + +if [ ! -d "$LOG_DIR" ]; then + echo "Error : Directory doesn't exist" + exit 1 +fi + +echo "Checking log directory: $LOG_DIR" + +COMPRESSED=$(find "$LOG_DIR" -name "*.log" -mtime +7 -exec gzip {} \; | wc -l) + +DELETED=$(find "$LOG_DIR" -name "*.gz" -mtime +30 -delete | wc -l) + +echo "Compressed files: $COMPRESSED" + +echo "Deleted old compressed files: $DELETED" + +echo "Log rotation completed" diff --git a/2026/day-19/strict_demo.sh b/2026/day-19/strict_demo.sh new file mode 100644 index 0000000000..dacf216ac0 --- /dev/null +++ b/2026/day-19/strict_demo.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +set -euo pipefail + +name="Asma" + +echo $name diff --git a/2026/day-19/system_info.sh b/2026/day-19/system_info.sh new file mode 100644 index 0000000000..de854ddb63 --- /dev/null +++ b/2026/day-19/system_info.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +set -euo pipefail + +system_info() { + echo "SYSTEM INFO" + hostnamectl +} + +uptime_info() { + echo + echo "UPTIME" + uptime +} + +disk_usage() { + echo + echo "DISK USAGE" + du -h / | sort -rh | head -5 +} + +memory_usage() { + echo + echo "MEMORY USAGE" + free -h +} + +cpu_usage() { + echo + echo "TOP CPU PROCESSES" + ps -eo pid,ppid,cmd,%mem,%cpu --sort=-%cpu | head -6 +} + +main() { + system_info + uptime_info + disk_usage + memory_usage + cpu_usage +} + +main diff --git a/2026/day-20/Screenshot 2026-06-05 122310.png b/2026/day-20/Screenshot 2026-06-05 122310.png new file mode 100644 index 0000000000..193a591050 Binary files /dev/null and b/2026/day-20/Screenshot 2026-06-05 122310.png differ diff --git a/2026/day-20/Screenshot 2026-06-05 122534.png b/2026/day-20/Screenshot 2026-06-05 122534.png new file mode 100644 index 0000000000..5873b345b3 Binary files /dev/null and b/2026/day-20/Screenshot 2026-06-05 122534.png differ diff --git a/2026/day-20/Screenshot 2026-06-05 122600.png b/2026/day-20/Screenshot 2026-06-05 122600.png new file mode 100644 index 0000000000..f2b628960e Binary files /dev/null and b/2026/day-20/Screenshot 2026-06-05 122600.png differ diff --git a/2026/day-20/day-20-solution.md b/2026/day-20/day-20-solution.md new file mode 100644 index 0000000000..9fd16da3e8 --- /dev/null +++ b/2026/day-20/day-20-solution.md @@ -0,0 +1,144 @@ +# Day 20 – Log Analyzer and Report Generator + +## Introduction + +In this lab, I created a Bash script to analyze log files and generate a report automatically. + +I used a real Apache log file for testing. +The script checks errors, critical events, and top repeated error messages. + +This lab helped me understand how DevOps engineers analyze server logs for troubleshooting and monitoring. + +--- + +# Task 1 – Input and Validation + +I created `log_analyzer.sh`. + +The script: + +- accepts log file as input +- checks if argument is provided +- checks if file exists +- shows error message if file is missing + +## Commands Used + +grep + +if condition + +exit + +echo + +--- + +# Task 2 – Error Count + +The script counts total error messages from the log file. + +## Command Used + +grep -ic "error" + +## What I Learned + +- how to search logs +- how to count matching lines +- basic log monitoring + +--- + +# Task 3 – Critical Events + +The script searches for critical events and prints line numbers. + +## Command Used + +grep -in "critical" + +## What I Learned + +- how to find important events +- how line numbers help troubleshooting + +--- + +# Task 4 – Top 5 Error Messages + +The script displays the most repeated error messages. + +## Commands Used + +grep + +awk + +sort + +uniq -c + +head -5 + +## What I Learned + +- how repeated errors are identified +- how logs are summarized +- basic production-style log analysis + +--- + +# Task 5 – Summary Report + +The script generates a report file automatically. + +Generated file: + +log_report_2026-06-05.txt + +The report includes: + +- date +- log file name +- total lines +- total error count +- top 5 errors +- critical events + +## What I Learned + +- report generation +- file redirection +- automation using Bash scripts + +--- + +# Sample Output + +Total Error Count: 595 + +Top Errors: + +368 mod_jk child workerEnv in error state 6 + +101 mod_jk child workerEnv in error state 7 + +44 mod_jk child workerEnv in error state 8 + +--- + +# Key Learnings + +1. Bash scripting helps automate log analysis. + +2. grep, awk, sort, and uniq are useful for troubleshooting. + +3. Log analysis is important in DevOps and system monitoring. + +--- + +# Conclusion + +This lab improved my Linux scripting and troubleshooting skills. +I learned how to analyze real log files and generate automated reports using Bash scripting. diff --git a/2026/day-20/log_analyzer.sh b/2026/day-20/log_analyzer.sh new file mode 100644 index 0000000000..2a65c23c7c --- /dev/null +++ b/2026/day-20/log_analyzer.sh @@ -0,0 +1,46 @@ +#!/bin/bash + +LOG_FILE=$1 + +if [ $# -eq 0 ]; then + echo "Please provide log file" + exit 1 +fi + +if [ ! -f "$LOG_FILE" ]; then + echo "Log file not found" + exit 1 +fi + +echo "Analyzing log file: $LOG_FILE" + +echo +echo "Total Error Count:" +grep -ic "error" "$LOG_FILE" + +echo +echo "Critical Events:" +grep -in "critical" "$LOG_FILE" + +echo +echo "Top 5 Errors:" +grep "error" "$LOG_FILE" | awk -F'] ' '{print $3}' | sort | uniq -c | sort -rn | head -5 + +REPORT_FILE="log_report_$(date +%Y-%m-%d).txt" + +{ +echo "Log Analysis Report" +echo "Date: $(date)" +echo "Log File: $LOG_FILE" +echo "Total Lines: $(wc -l < "$LOG_FILE")" +echo "Total Error Count: $(grep -ic "error" "$LOG_FILE")" +echo +echo "Top 5 Errors:" +grep "error" "$LOG_FILE" | awk -F'] ' '{print $3}' | sort | uniq -c | sort -rn | head -5 +echo +echo "Critical Events:" +grep -in "critical" "$LOG_FILE" +} > "$REPORT_FILE" + +echo +echo "Report created: $REPORT_FILE" diff --git a/2026/day-20/log_report_2026-06-05.txt b/2026/day-20/log_report_2026-06-05.txt new file mode 100644 index 0000000000..13265e844d --- /dev/null +++ b/2026/day-20/log_report_2026-06-05.txt @@ -0,0 +1,14 @@ +Log Analysis Report +Date: Fri Jun 5 12:25:05 EDT 2026 +Log File: Apache_2k.log +Total Lines: 1999 +Total Error Count: 595 + +Top 5 Errors: + 368 mod_jk child workerEnv in error state 6 + 101 mod_jk child workerEnv in error state 7 + 44 mod_jk child workerEnv in error state 8 + 20 mod_jk child workerEnv in error state 9 + 12 mod_jk child init 1 -2 + +Critical Events: diff --git a/2026/day-21/command.png b/2026/day-21/command.png new file mode 100644 index 0000000000..d8493553c7 Binary files /dev/null and b/2026/day-21/command.png differ diff --git a/2026/day-21/shell_scripting_cheatsheet.md b/2026/day-21/shell_scripting_cheatsheet.md new file mode 100644 index 0000000000..e8874fd02a --- /dev/null +++ b/2026/day-21/shell_scripting_cheatsheet.md @@ -0,0 +1,408 @@ +# DEVOPS SHELL SCRIPTING DAILY REVISION + +## Script Start + +```bash id="t0xk7g" +#!/bin/bash +``` + +Use bash shell to run script. + +--- + +## Run Script + +```bash id="jlwm31" +chmod +x script.sh +./script.sh +bash script.sh +``` + +- chmod +x → execute permission +- ./script.sh → run script + +--- + +## Variables + +```bash id="jlwm32" +NAME="Asma" +echo $NAME +``` + +No spaces around `=`. + +Wrong: + +```bash id="jlwm33" +NAME = "Asma" +``` + +--- + +## User Input + +```bash id="jlwm34" +read NAME +echo $NAME +``` + +Take user input. + +--- + +## Script Arguments + +```bash id="jlwm35" +./script.sh file.txt +``` + +| Variable | Meaning | +| -------- | ------------------- | +| $0 | script name | +| $1 | first argument | +| $2 | second argument | +| $# | total arguments | +| $? | last command status | + +--- + +## If Condition + +```bash id="jlwm36" +if [ condition ] +then + echo "yes" +fi +``` + +--- + +## Integer Operators + +| Operator | Meaning | +| -------- | ------------- | +| -eq | equal | +| -ne | not equal | +| -gt | greater | +| -lt | less | +| -ge | greater equal | +| -le | less equal | + +Example: + +```bash id="jlwm37" +[ 5 -gt 2 ] +``` + +--- + +## File Checks + +| Check | Meaning | +| ----- | ------------- | +| -f | file exists | +| -d | folder exists | +| -r | readable | +| -w | writable | +| -x | executable | + +--- + +## For Loop + +```bash id="jlwm38" +for i in 1 2 3 +do + echo $i +done +``` + +--- + +## While Loop + +```bash id="jlwm39" +while read line +do + echo $line +done +``` + +--- + +## Function + +```bash id="jlwm40" +greet() { + echo "hello" +} +``` + +Call: + +```bash id="jlwm41" +greet +``` + +--- + +## Local Variable + +```bash id="jlwm42" +local NAME="Asma" +``` + +Works only inside function. + +--- + +## Grep = Search + +```bash id="jlwm43" +grep "error" app.log +``` + +Useful: + +```bash id="jlwm44" +grep -i "error" app.log +grep -c "error" app.log +grep -n "error" app.log +``` + +- -i → ignore case +- -c → count +- -n → line number + +Memory: +grep = search logs + +--- + +## Awk = Columns + +```bash id="jlwm45" +awk '{print $1}' file.txt +``` + +Separator: + +```bash id="jlwm46" +awk -F: '{print $1}' /etc/passwd +``` + +Memory: +awk = columns + +--- + +## Sed = Replace + +```bash id="jlwm47" +sed 's/old/new/g' file.txt +``` + +Inside file: + +```bash id="jlwm48" +sed -i 's/dev/prod/g' config.txt +``` + +Memory: +sed = replace text + +--- + +## Sort + Uniq + +```bash id="jlwm49" +sort file.txt +uniq -c file.txt +``` + +Most used: + +```bash id="jlwm50" +sort file.txt | uniq -c +``` + +Used for repeated logs/errors. + +--- + +## WC = Count + +```bash id="jlwm51" +wc -l file.txt +``` + +- -l → lines +- -w → words +- -c → characters + +--- + +## Head / Tail + +```bash id="jlwm52" +head -5 file.txt +tail -5 file.txt +tail -f app.log +``` + +Memory: +tail -f = live logs + +--- + +## Find Command + +```bash id="jlwm53" +find . -name "*.log" +``` + +Delete old logs: + +```bash id="jlwm54" +find . -mtime +30 -delete +``` + +--- + +## Tar = Backup + +```bash id="jlwm55" +tar -czf backup.tar.gz folder/ +``` + +Memory: +tar = pack files + +--- + +## Cron Jobs + +```bash id="jlwm56" +crontab -e +crontab -l +``` + +Example: + +```bash id="jlwm57" +0 2 * * * script.sh +``` + +Run daily at 2 AM. + +Memory: +Minute Hour Day Month Week + +--- + +## Strict Mode + +```bash id="jlwm58" +set -e +set -u +set -o pipefail +``` + +- set -e → stop on error +- set -u → stop undefined variable +- pipefail → fail pipeline + +--- + +## Debug Mode + +```bash id="jlwm59" +set -x +``` + +Shows script execution step by step. + +--- + +## Useful DevOps Commands + +Disk usage: + +```bash id="jlwm60" +df -h +``` + +Memory usage: + +```bash id="jlwm61" +free -h +``` + +Processes: + +```bash id="jlwm62" +ps aux +``` + +CPU heavy processes: + +```bash id="jlwm63" +ps aux --sort=-%cpu | head +``` + +Live logs: + +```bash id="jlwm64" +tail -f app.log +``` + +--- + +## Real DevOps One Liners + +Live error monitoring: + +```bash id="jlwm65" +tail -f app.log | grep error +``` + +Count errors: + +```bash id="jlwm66" +grep -c "error" app.log +``` + +Top repeated errors: + +```bash id="jlwm67" +grep "error" app.log | sort | uniq -c | sort -rn +``` + +Service status: + +```bash id="jlwm68" +systemctl status nginx +``` + +Restart service: + +```bash id="jlwm69" +systemctl restart nginx +``` + +--- + +## Quick Memory Tricks + +| Command | Easy Meaning | +| ------- | ----------------- | +| grep | search | +| awk | columns | +| sed | replace | +| wc | count | +| tar | archive | +| cron | scheduler | +| chmod | permission | +| uniq | duplicates | +| ps aux | running processes | diff --git a/2026/day-22/day-22-notes.md b/2026/day-22/day-22-notes.md new file mode 100644 index 0000000000..b2eef802e3 --- /dev/null +++ b/2026/day-22/day-22-notes.md @@ -0,0 +1,99 @@ +# Day 22 Notes – Introduction to Git + +--- + +## 1. What is the difference between `git add` and `git commit`? + +`git add` moves changes to the staging area. + +`git commit` saves those staged changes permanently into Git history. + +Example: + +* `git add` = preparing files +* `git commit` = saving snapshot + +--- + +## 2. What does the staging area do? Why doesn't Git commit directly? + +The staging area is a temporary place where we choose which changes should be included in the next commit. + +Git does not commit directly because developers may want to organize changes into separate commits instead of saving everything together. + +It helps keep commit history clean and meaningful. + +--- + +## 3. What information does `git log` show? + +`git log` shows: + +* commit ID +* author name +* date and time +* commit message + +It helps track project history and changes. + +--- + +## 4. What is the `.git/` folder and what happens if you delete it? + +The `.git/` folder stores: + +* commits +* branches +* configuration +* repository history + +It is the heart of the Git repository. + +If we delete the `.git/` folder, the project will no longer be a Git repository and all Git history will be lost. + +--- + +## 5. What is the difference between working directory, staging area, and repository? + +### Working Directory + +The place where we create and edit files normally. + +--- + +### Staging Area + +A temporary area where selected changes are prepared before committing. + +--- + +### Repository + +The place where committed project history is stored permanently by Git. + +--- + +# Commands Practiced Today + +```bash id="q3x10s" +git init +git status +git add . +git commit -m "message" +git log +git log --oneline +git diff +git config --list +``` + +--- + +# What I Learned Today + +* Git is a version control system +* Git tracks file changes over time +* Commits create project history +* `git status` helps understand repository state +* The staging area prepares files before commit +* Git history helps developers collaborate safely + diff --git a/2026/day-22/git-commands.md b/2026/day-22/git-commands.md new file mode 100644 index 0000000000..092202cbd3 --- /dev/null +++ b/2026/day-22/git-commands.md @@ -0,0 +1,192 @@ +# Git Commands Reference + +--- + +# Setup & Config + +## git --version + +Shows the installed Git version. + +Example: + +```bash +git --version +``` + +--- + +## git config --global user.name + +Sets Git username. + +Example: + +```bash +git config --global user.name "Asma Saiyed" +``` + +--- + +## git config --global user.email + +Sets Git email address. + +Example: + +```bash +git config --global user.email "asmasaiyed104@gmail.com" +``` + +--- + +## git config --list + +Shows all Git configuration settings. + +Example: + +```bash +git config --list +``` + +--- + +# Repository Setup + +## git init + +Creates a new Git repository. + +Example: + +```bash +git init +``` + +--- + +## ls -la + +Shows hidden files including `.git`. + +Example: + +```bash +ls -la +``` + +--- + +# Basic Workflow + +## git status + +Shows current repository status. + +Example: + +```bash +git status +``` + +--- + +## git add + +Stages files for commit. + +Example: + +```bash +git add git-commands.md +``` + +--- + +## git add . + +Stages all changed files. + +Example: + +```bash +git add . +``` + +--- + +## git commit + +Creates a snapshot of staged changes. + +Example: + +```bash +git commit -m "Add Git notes" +``` + +--- + +# Viewing Changes + +## git log + +Shows detailed commit history. + +Example: + +```bash +git log +``` + +--- + +## git log --oneline + +Shows compact commit history. + +Example: + +```bash +git log --oneline +``` + +--- + +## git diff + +Shows file changes before commit. + +Example: + +```bash +git diff +``` + +--- + +# Help Commands + +## git help + +Shows Git help documentation. + +Example: + +```bash +git help +``` + +--- + +## git status --help + +Shows help for git status command. + +Example: + +```bash +git status --help +``` + diff --git a/2026/day-22/login.txt b/2026/day-22/login.txt new file mode 100644 index 0000000000..6b572c338c --- /dev/null +++ b/2026/day-22/login.txt @@ -0,0 +1 @@ +Login page added diff --git a/2026/day-23/Screenshot 2026-06-09 121012.png b/2026/day-23/Screenshot 2026-06-09 121012.png new file mode 100644 index 0000000000..abd9384422 Binary files /dev/null and b/2026/day-23/Screenshot 2026-06-09 121012.png differ diff --git a/2026/day-23/Screenshot 2026-06-09 131426.png b/2026/day-23/Screenshot 2026-06-09 131426.png new file mode 100644 index 0000000000..1076558f9e Binary files /dev/null and b/2026/day-23/Screenshot 2026-06-09 131426.png differ diff --git a/2026/day-23/Screenshot 2026-06-09 131539.png b/2026/day-23/Screenshot 2026-06-09 131539.png new file mode 100644 index 0000000000..6fa8c141b8 Binary files /dev/null and b/2026/day-23/Screenshot 2026-06-09 131539.png differ diff --git a/2026/day-23/Screenshot 2026-06-09 132129.png b/2026/day-23/Screenshot 2026-06-09 132129.png new file mode 100644 index 0000000000..45ca4adb59 Binary files /dev/null and b/2026/day-23/Screenshot 2026-06-09 132129.png differ diff --git a/2026/day-23/Screenshot 2026-06-09 132742.png b/2026/day-23/Screenshot 2026-06-09 132742.png new file mode 100644 index 0000000000..75d75d947b Binary files /dev/null and b/2026/day-23/Screenshot 2026-06-09 132742.png differ diff --git a/2026/day-23/day-23-notes.md b/2026/day-23/day-23-notes.md new file mode 100644 index 0000000000..ffc25fc6da --- /dev/null +++ b/2026/day-23/day-23-notes.md @@ -0,0 +1,152 @@ +# Day 23 Notes – Git Branching & Working with GitHub + +--- + +# 1. What is a branch in Git? + +A branch in Git is like creating a separate workspace inside the same project. + +It allows us to work on new features or test changes without affecting the main code. + +--- + +# 2. Why do we use branches instead of committing everything to main? + +If everyone works directly on `main`, it can break the project easily. + +Branches help developers work safely and separately. After testing, changes can be merged into `main`. + +This keeps the project stable and organized. + +--- + +# 3. What is HEAD in Git? + +`HEAD` shows the current branch or current commit where we are working. + +It acts like a pointer in Git. + +For example: + +- if I am on `main`, HEAD points to `main` +- if I switch to `feature-1`, HEAD moves there + +--- + +# 4. What happens to your files when you switch branches? + +When switching branches, Git changes the files in the working directory based on that branch. + +Files created in one branch may disappear in another branch if they were never committed there. + +I tested this using `feature-1` branch. + +--- + +# 5. Difference between git switch and git checkout + +`git checkout` is the older command mostly used for switching branches. + +`git switch` is the newer command made specifically for branch switching. + +I mostly used `git checkout` because I was already comfortable with it. + +Examples: + +```bash id="eqwzbi" +git checkout feature-1 +git checkout main +``` + +Modern alternative: + +```bash id="ptv99y" +git switch feature-1 +git switch main +``` + +--- + +# 6. Difference between origin and upstream + +## origin + +`origin` is my own GitHub repository connected to my local repo. + +## upstream + +`upstream` is the original repository from which a fork was created. + +--- + +# 7. Difference between git fetch and git pull + +## git fetch + +Downloads latest changes from GitHub but does not merge them. + +## git pull + +Downloads changes and merges them automatically into the current branch. + +--- + +# 8. Difference between clone and fork + +## Clone + +Creates a local copy of a repository on my computer. + +## Fork + +Creates my personal copy of someone else’s repository on GitHub. + +--- + +# 9. When would you use clone vs fork? + +I would use: + +- `clone` when working on my own repository +- `fork` when contributing to someone else’s project + +--- + +# 10. How do you keep your fork updated? + +We can connect the original repository as upstream and pull latest changes. + +Example: + +```bash id="9x5h8m" +git remote add upstream +git pull upstream main +``` + +--- + +# Commands I Practiced Today + +```bash id="mo8vii" +git branch +git checkout feature-1 +git checkout main +git checkout -b feature-2 +git branch -d feature-2 +git push -u origin main +git push -u origin feature-1 +git fetch +git pull +git clone +``` + +--- + +# What I Learned Today + +- Branches help isolate work safely +- `git checkout` and `git switch` can both move between branches +- GitHub stores remote repositories +- `git pull` updates local repository from GitHub +- Fork and clone are different concepts +- Feature branches are very important in DevOps workflows diff --git a/2026/day-24/Screenshot 2026-06-09 133502.png b/2026/day-24/Screenshot 2026-06-09 133502.png new file mode 100644 index 0000000000..63cf6e4849 Binary files /dev/null and b/2026/day-24/Screenshot 2026-06-09 133502.png differ diff --git a/2026/day-24/Screenshot 2026-06-09 134216.png b/2026/day-24/Screenshot 2026-06-09 134216.png new file mode 100644 index 0000000000..2bc2f7661e Binary files /dev/null and b/2026/day-24/Screenshot 2026-06-09 134216.png differ diff --git a/2026/day-24/Screenshot 2026-06-09 134226.png b/2026/day-24/Screenshot 2026-06-09 134226.png new file mode 100644 index 0000000000..1ea1305ec2 Binary files /dev/null and b/2026/day-24/Screenshot 2026-06-09 134226.png differ diff --git a/2026/day-24/Screenshot 2026-06-09 134540.png b/2026/day-24/Screenshot 2026-06-09 134540.png new file mode 100644 index 0000000000..7ef636ec00 Binary files /dev/null and b/2026/day-24/Screenshot 2026-06-09 134540.png differ diff --git a/2026/day-24/Screenshot 2026-06-09 135140.png b/2026/day-24/Screenshot 2026-06-09 135140.png new file mode 100644 index 0000000000..35b2898fe4 Binary files /dev/null and b/2026/day-24/Screenshot 2026-06-09 135140.png differ diff --git a/2026/day-24/Screenshot 2026-06-09 135328.png b/2026/day-24/Screenshot 2026-06-09 135328.png new file mode 100644 index 0000000000..8b4a3ae8ab Binary files /dev/null and b/2026/day-24/Screenshot 2026-06-09 135328.png differ diff --git a/2026/day-24/day-24-notes.md b/2026/day-24/day-24-notes.md new file mode 100644 index 0000000000..5bb39e779d --- /dev/null +++ b/2026/day-24/day-24-notes.md @@ -0,0 +1,256 @@ +# Day 24 Notes – Advanced Git + +--- + +# Git Merge + +## What I Did + +I created a branch called `feature-login` and added commits to it. + +Then I merged it into `DevOps-practice`. + +Git performed a fast-forward merge because the main branch did not have any new commits. + +Later, I created another branch called `feature-signup`. + +This time, I also added a commit on `DevOps-practice` before merging. + +When I merged `feature-signup`, Git created a merge commit. + +--- + +## What is a Fast-Forward Merge? + +A fast-forward merge happens when the main branch has no new commits after creating the feature branch. + +Git simply moves the branch pointer forward. + +No extra merge commit is created. + +--- + +## What is a Merge Commit? + +A merge commit happens when both branches have different commits. + +Git creates a special commit to combine both histories together. + +I observed this using: + +```bash +git log --oneline --graph --all +``` + +--- + +## What is a Merge Conflict? + +A merge conflict happens when the same line of the same file is modified differently in two branches. + +Git becomes confused and asks the developer to manually choose the correct changes. + +--- + +# Git Rebase + +## What I Did + +I created a branch called `feature-dashboard` and added multiple commits. + +Then I added another commit on `DevOps-practice`. + +After that, I rebased `feature-dashboard` on top of `DevOps-practice`. + +--- + +## What Does Rebase Do? + +Rebase moves feature branch commits on top of the latest main branch commits. + +It rewrites commit history to create a cleaner and more linear history. + +--- + +## Difference Between Merge and Rebase + +### Merge + +- keeps branch history +- creates merge commits +- history can look more complex + +### Rebase + +- rewrites commit history +- creates cleaner history +- avoids unnecessary merge commits + +--- + +## Why Should We Avoid Rebasing Shared Commits? + +Rebasing changes commit history. + +If commits are already pushed and shared with teammates, rebasing can create confusion and conflicts for others. + +--- + +## When Would I Use Rebase vs Merge? + +I would use: + +- rebase for cleaning my own local branch history +- merge for shared team branches + +--- + +# Squash Merge + +## What I Did + +I created a branch called `feature-profile` and added multiple small commits like: + +- typo fixes +- formatting changes +- profile updates + +Then I used: + +```bash +git merge --squash feature-profile +``` + +--- + +## What Does Squash Merge Do? + +Squash merge combines multiple commits into one single clean commit. + +Instead of adding many small commits to the main branch, Git creates only one commit. + +--- + +## When Would I Use Squash Merge? + +I would use squash merge when: + +- feature branch contains many tiny commits +- I want cleaner commit history +- typo and formatting commits are not important individually + +--- + +## Trade-Off of Squash Merge + +The detailed commit history from the feature branch is lost in the main branch. + +Only one combined commit appears. + +--- + +# Git Stash + +## What I Did + +I modified `git-commands.md` without committing changes. + +Then I used stash to temporarily save my unfinished work. + +After switching branches, I restored my changes using `git stash pop`. + +--- + +## What is Git Stash? + +Git stash temporarily saves unfinished changes without creating a commit. + +It helps when I need to quickly switch branches for another task. + +--- + +## Difference Between git stash pop and git stash apply + +### git stash pop + +- restores stash +- removes stash from stash list + +### git stash apply + +- restores stash +- keeps stash in stash list + +--- + +## When Would I Use Stash? + +I would use stash when: + +- my work is incomplete +- I need to urgently switch branches +- I do not want to create unnecessary commits + +--- + +# Git Cherry-Pick + +## What I Did + +I created a branch called `feature-hotfix` and added three commits. + +Then I used cherry-pick to copy only the second commit into `DevOps-practice`. + +Only that selected commit was applied. + +--- + +## What Does Cherry-Pick Do? + +Cherry-pick copies one specific commit from another branch. + +It does not merge the entire branch. + +--- + +## When Would I Use Cherry-Pick? + +I would use cherry-pick when: + +- I only need one specific bug fix +- I do not want all branch changes +- I need to quickly apply one important commit + +--- + +## What Can Go Wrong with Cherry-Pick? + +Cherry-picking can create conflicts if the selected commit depends on other commits that were not copied. + +--- + +# Commands Practiced Today + +```bash +git merge +git merge --squash +git rebase +git stash +git stash list +git stash pop +git stash apply +git cherry-pick +git log --oneline --graph --all +``` + +--- + +# What I Learned Today + +- Fast-forward merge and merge commit are different +- Rebase creates cleaner history +- Squash merge combines many commits into one +- Stash temporarily saves unfinished work +- Cherry-pick copies only one specific commit +- Git history visualization is very helpful using graph logs +- Advanced Git workflows are very important in DevOps and team collaboration diff --git a/2026/day-24/hotfix2.txt b/2026/day-24/hotfix2.txt new file mode 100644 index 0000000000..4ae6c5e144 --- /dev/null +++ b/2026/day-24/hotfix2.txt @@ -0,0 +1 @@ +Hotfix 2 diff --git a/2026/day-24/main-change.txt b/2026/day-24/main-change.txt new file mode 100644 index 0000000000..8cbc111630 --- /dev/null +++ b/2026/day-24/main-change.txt @@ -0,0 +1 @@ +Main branch chnage diff --git a/2026/day-24/profile.txt b/2026/day-24/profile.txt new file mode 100644 index 0000000000..8c457873a9 --- /dev/null +++ b/2026/day-24/profile.txt @@ -0,0 +1,3 @@ +Profile page added +Fix profile typo +Profile formatting updated diff --git a/2026/day-24/rebase-main.txt b/2026/day-24/rebase-main.txt new file mode 100644 index 0000000000..67d86c3da4 --- /dev/null +++ b/2026/day-24/rebase-main.txt @@ -0,0 +1 @@ +Main change befire rebase diff --git a/2026/day-24/signup.txt b/2026/day-24/signup.txt new file mode 100644 index 0000000000..5dafadfda5 --- /dev/null +++ b/2026/day-24/signup.txt @@ -0,0 +1 @@ +Signup feature added diff --git a/2026/day-25/Screenshot 2026-06-10 104246.png b/2026/day-25/Screenshot 2026-06-10 104246.png new file mode 100644 index 0000000000..635fbfea5c Binary files /dev/null and b/2026/day-25/Screenshot 2026-06-10 104246.png differ diff --git a/2026/day-25/Screenshot 2026-06-10 104500.png b/2026/day-25/Screenshot 2026-06-10 104500.png new file mode 100644 index 0000000000..3a34199b7c Binary files /dev/null and b/2026/day-25/Screenshot 2026-06-10 104500.png differ diff --git a/2026/day-25/Screenshot 2026-06-10 104600.png b/2026/day-25/Screenshot 2026-06-10 104600.png new file mode 100644 index 0000000000..2db3651efd Binary files /dev/null and b/2026/day-25/Screenshot 2026-06-10 104600.png differ diff --git a/2026/day-25/Screenshot 2026-06-10 104751.png b/2026/day-25/Screenshot 2026-06-10 104751.png new file mode 100644 index 0000000000..d6db719f60 Binary files /dev/null and b/2026/day-25/Screenshot 2026-06-10 104751.png differ diff --git a/2026/day-25/Screenshot 2026-06-10 104849.png b/2026/day-25/Screenshot 2026-06-10 104849.png new file mode 100644 index 0000000000..4beee9e61f Binary files /dev/null and b/2026/day-25/Screenshot 2026-06-10 104849.png differ diff --git a/2026/day-25/day-25-notes.md b/2026/day-25/day-25-notes.md new file mode 100644 index 0000000000..639460fe28 --- /dev/null +++ b/2026/day-25/day-25-notes.md @@ -0,0 +1,369 @@ +# Day 25 – Git Reset vs Revert & Branching Strategies + +Today I learned how to safely undo mistakes in Git using reset, revert, and reflog. I also learned different branching strategies used by software teams. + +## Task 1 – Git Reset + +First, I created 3 commits: + +- Commit A +- Commit B +- Commit C + +I checked commit history using: + +```bash id="1lqv7y" +git log --oneline +``` + +--- + +## git reset --soft + +Command used: + +```bash id="lcgg1m" +git reset --soft HEAD~1 +``` + +### What happened + +- Last commit was removed from history +- Changes were still available +- Files stayed staged + +### Understanding + +Soft reset only moves HEAD backward. +It keeps all changes ready for recommit. + +--- + +## git reset --mixed + +Command used: + +```bash id="8lyy2m" +git reset --mixed HEAD~1 +``` + +### What happened + +- Commit was removed +- Changes stayed in files +- Files became unstaged + +### Understanding + +Mixed reset removes commit and unstages changes, but does not delete them. + +--- + +## git reset --hard + +Command used: + +```bash id="mk2kq8" +git reset --hard HEAD~1 +``` + +### What happened + +- Commit was deleted +- File changes were deleted +- Working directory became clean + +### Understanding + +Hard reset is dangerous because it permanently removes changes. + +--- + +## Difference Between Soft, Mixed and Hard + +| Type | Commit Removed | Changes Kept | Staged | +| ----- | -------------- | ------------ | ------ | +| Soft | Yes | Yes | Yes | +| Mixed | Yes | Yes | No | +| Hard | Yes | No | No | + +--- + +## Which Reset is Destructive? + +`git reset --hard` + +Reason: +It deletes commits and file changes permanently. + +--- + +## When to Use Each Reset + +### Soft Reset + +Used when: + +- I want to recommit changes quickly +- I only want to change commit message or combine commits + +### Mixed Reset + +Used when: + +- I want to unstage files +- I want to reorganize commits + +### Hard Reset + +Used when: + +- I want to completely discard changes +- I need a clean repository state + +--- + +## Should Reset Be Used on Pushed Commits? + +No. + +Reason: +It rewrites Git history and can create problems for team members working on shared branches. + +--- + +# Task 2 – Git Revert + +I created: + +- Commit X +- Commit Y +- Commit Z + +Then I reverted commit Y using: + +```bash id="rw2kvf" +git revert +``` + +--- + +## What Happened + +- Git created a new revert commit +- Original commit Y stayed in history +- Changes from Y were safely undone + +I verified history using: + +```bash id="9kjf8f" +git log --oneline +``` + +--- + +# Difference Between Reset and Revert + +## git reset + +- Removes commits from history +- Rewrites history +- Mostly used locally + +## git revert + +- Creates a new commit to undo changes +- Keeps history safe +- Good for shared branches + +--- + +# Why Revert is Safer + +Revert does not rewrite Git history. + +Other developers can continue working without conflicts. + +--- + +# When to Use Reset vs Revert + +### Use Reset + +- Local changes +- Before pushing +- Cleanup work + +### Use Revert + +- Shared repositories +- Pushed commits +- Team collaboration + +--- + +# Reset vs Revert Summary + +| | git reset | git revert | +| ------------------------ | ------------------- | ------------------- | +| What it does | Moves HEAD backward | Creates undo commit | +| Removes history | Yes | No | +| Safe for pushed branches | No | Yes | +| Main use | Local cleanup | Safe undo | + +--- + +# Task 3 – git reflog + +Command used: + +```bash id="t1vgxe" +git reflog +``` + +## Understanding + +`git reflog` is Git’s safety net. + +It helps recover: + +- deleted commits +- hard reset mistakes +- lost changes + +Recovery example: + +```bash id="j9p2h7" +git reset --hard +``` + +--- + +# Task 4 – Branching Strategies + +## 1. GitFlow + +### Structure + +```text id="xg2q5n" +main +develop +feature/* +release/* +hotfix/* +``` + +### Used In + +- Large companies +- Enterprise applications +- Scheduled release systems + +### Pros + +- Organized workflow +- Stable releases + +### Cons + +- Complex for small teams +- More branch management + +--- + +## 2. GitHub Flow + +### Structure + +```text id="0z7m7s" +main -> feature branch -> pull request -> merge +``` + +### Used In + +- Startups +- Fast deployment teams +- CI/CD workflows + +### Pros + +- Simple +- Fast development + +### Cons + +- Less control for large releases + +--- + +## 3. Trunk-Based Development + +### Structure + +```text id="q1prq9" +small branches -> main +``` + +### Used In + +- Modern DevOps teams +- Continuous deployment + +### Pros + +- Fast integration +- Smaller merge conflicts + +### Cons + +- Requires strong testing + +--- + +# Answers + +## Best Strategy for Startup + +GitHub Flow + +Reason: +It is simple and helps teams ship quickly. + +--- + +## Best Strategy for Large Team + +GitFlow + +Reason: +It supports scheduled releases and organized workflows. + +--- + +## Open Source Projects + +Many modern open-source projects use GitHub Flow or Trunk-Based Development because they support fast collaboration and CI/CD practices. + +--- + +# Commands Learned Today + +```bash id="d2t3s6" +git reset --soft HEAD~1 +git reset --mixed HEAD~1 +git reset --hard HEAD~1 +git revert +git reflog +``` + +--- + +# Final Learning + +Today I learned: + +- how to undo mistakes safely in Git +- difference between reset and revert +- why reflog is important +- which reset type is dangerous +- how branching strategies work in real engineering teams diff --git a/2026/day-25/reset-demo.txt b/2026/day-25/reset-demo.txt new file mode 100644 index 0000000000..cf055839c1 --- /dev/null +++ b/2026/day-25/reset-demo.txt @@ -0,0 +1,3 @@ +Commit A + Commit B + Commit C diff --git a/2026/day-26/day-26-notes.md b/2026/day-26/day-26-notes.md new file mode 100644 index 0000000000..e3321f2762 --- /dev/null +++ b/2026/day-26/day-26-notes.md @@ -0,0 +1,393 @@ +# Day 26 – GitHub CLI (gh) + +Today I learned how to use GitHub directly from the terminal using GitHub CLI (`gh`). This helps developers and DevOps engineers manage repositories, pull requests, issues, and workflows without opening the browser repeatedly. + +--- + +# Task 1 – Install and Authenticate GitHub CLI + +## Installed GitHub CLI + +I installed GitHub CLI on Windows and verified installation using: + +```bash +gh --version +``` + +Output showed installed version successfully. + +--- + +## Authentication + +Used command: + +```bash +gh auth login +``` + +Selected: + +- GitHub.com +- HTTPS +- Login with web browser + +Authenticated GitHub account through browser. + +Verified login using: + +```bash +gh auth status +``` + +--- + +# Authentication Methods Supported by gh + +GitHub CLI supports: + +- Browser authentication +- Personal Access Token (PAT) +- SSH authentication +- GitHub Enterprise authentication + +--- + +# Task 2 – Working with Repositories + +## Create Repository from Terminal + +Command used: + +```bash +gh repo create gh-cli-demo --public --clone --add-readme +``` + +This: + +- created repository +- added README +- cloned repository locally + +--- + +## Clone Repository Using gh + +```bash +gh repo clone owner/repository-name +``` + +Example: + +```bash +gh repo clone asmasaiyed104/90DaysOfDevOps +``` + +--- + +## View Repository Details + +```bash +gh repo view +``` + +--- + +## List All Repositories + +```bash +gh repo list +``` + +--- + +## Open Repository in Browser + +```bash +gh repo view --web +``` + +--- + +## Delete Repository + +```bash +gh repo delete gh-cli-demo +``` + +This command permanently deletes repository, so it should be used carefully. + +--- + +# Task 3 – Issues + +## Create Issue + +```bash +gh issue create +``` + +OR directly: + +```bash +gh issue create --title "Test Issue" --body "Testing GitHub CLI issue creation" --label bug +``` + +--- + +## List Issues + +```bash +gh issue list +``` + +--- + +## View Specific Issue + +```bash +gh issue view 1 +``` + +--- + +## Close Issue + +```bash +gh issue close 1 +``` + +--- + +# How gh issue Can Be Used in Automation + +`gh issue` can be used in scripts to: + +- automatically create bug reports +- create deployment incident tickets +- generate DevOps alerts +- automate monitoring workflows + +--- + +# Task 4 – Pull Requests + +## Create Branch + +```bash +git checkout -b feature-gh-cli +``` + +--- + +## Make Changes and Commit + +```bash +git add . +git commit -m "Added GitHub CLI practice" +``` + +--- + +## Push Branch + +```bash +git push origin feature-gh-cli +``` + +--- + +## Create Pull Request from Terminal + +```bash +gh pr create --fill +``` + +This automatically fills PR title and description from commit message. + +--- + +## List Pull Requests + +```bash +gh pr list +``` + +--- + +## View PR Details + +```bash +gh pr view +``` + +--- + +## Merge Pull Request + +```bash +gh pr merge +``` + +--- + +# Merge Methods Supported + +GitHub CLI supports: + +- merge +- squash +- rebase + +Examples: + +```bash +gh pr merge --merge +gh pr merge --squash +gh pr merge --rebase +``` + +--- + +# Reviewing Someone Else’s PR + +Commands used: + +```bash +gh pr checkout +``` + +This downloads PR locally for testing and review. + +Review command: + +```bash +gh pr review +``` + +--- + +# Task 5 – GitHub Actions & Workflows + +## List Workflow Runs + +```bash +gh run list +``` + +Example for public repository: + +```bash +gh run list --repo cli/cli +``` + +--- + +## View Workflow Run Details + +```bash +gh run view +``` + +--- + +# How gh run and gh workflow Help in CI/CD + +These commands help: + +- monitor pipelines +- check deployment status +- rerun failed workflows +- automate CI/CD monitoring +- integrate GitHub Actions into shell scripts + +--- + +# Task 6 – Useful gh Commands + +## GitHub API + +```bash +gh api user +``` + +--- + +## GitHub Gist + +```bash +gh gist create notes.txt +``` + +--- + +## GitHub Release + +```bash +gh release create v1.0 +``` + +--- + +## Create Alias + +```bash +gh alias set co "pr checkout" +``` + +Use alias: + +```bash +gh co 5 +``` + +--- + +## Search GitHub Repositories + +```bash +gh search repos devops +``` + +--- + +# Important Commands Learned Today + +```bash +gh auth login +gh auth status +gh repo create +gh repo clone +gh repo list +gh issue create +gh issue list +gh pr create +gh pr list +gh pr merge +gh run list +gh workflow list +``` + +--- + +# Difference Between Git and GitHub CLI + +| Tool | Purpose | +| ----------------- | --------------------------- | +| Git | Version control system | +| Git Bash | Terminal environment | +| GitHub CLI (`gh`) | Manage GitHub from terminal | + +--- + +# Final Learning + +Today I learned: + +- how to install and configure GitHub CLI +- how to manage repositories from terminal +- how to create issues and pull requests without browser +- how DevOps engineers automate GitHub workflows +- how GitHub CLI helps with CI/CD and automation + +GitHub CLI makes GitHub management faster and more efficient for DevOps workflows. diff --git a/2026/day-27/day-27-notes.md b/2026/day-27/day-27-notes.md new file mode 100644 index 0000000000..9dcd8b5286 --- /dev/null +++ b/2026/day-27/day-27-notes.md @@ -0,0 +1,45 @@ +# Day 27 – GitHub Profile Makeover + +## Objective + +Today I improved my GitHub profile and organized my repositories to make my developer profile more professional. + +--- + +## Changes I Made + +- Updated my GitHub bio +- Added repository descriptions +- Organized my DevOps and Python repositories +- Created `python-for-devops` repository +- Added README documentation +- Updated pinned repositories +- Improved overall GitHub profile appearance + +--- + +## Repositories Updated + +- 90DaysOfDevOps +- DevOps-Practice-Daily +- python-for-devops + +--- + +## Challenges Faced + +- Faced Git repository and nested folder issues +- Learned how to fix Git remote and push issues +- Learned proper GitHub repository structure + +--- + +## Key Learning + +GitHub is not just for storing code — it represents a developer’s learning journey and professional identity. + +--- + +## GitHub Profile + +https://github.com/Asmasaiyed104 diff --git a/2026/day-28/day-28-notes.md b/2026/day-28/day-28-notes.md new file mode 100644 index 0000000000..332e3b5d64 --- /dev/null +++ b/2026/day-28/day-28-notes.md @@ -0,0 +1,89 @@ +# Day 28 – Revision Day + +## Objective + +Today I revised all topics covered from Day 1 to Day 27 to strengthen my DevOps fundamentals and identify areas that need more practice. + +--- + +## Topics Revised + +- Linux fundamentals +- File permissions and ownership +- Process and service management +- Networking basics +- Shell scripting +- Git and GitHub +- Git branching and merge concepts +- GitHub profile and repository organization + +--- + +## Self Assessment + +### Confident Topics + +- Linux basic commands +- Git basic workflow +- GitHub repository management +- Shell scripting basics +- File permissions and ownership +- Networking fundamentals + +### Need More Practice + +- Git rebase and cherry-pick +- LVM concepts +- Advanced shell scripting error handling + +--- + +## Challenges Faced + +- Faced Git repository and nested folder issues +- Practiced fixing Git remote and push errors +- Reviewed merge conflict and Git workflow concepts + +--- + +## Key Revision Points + +### Git + +- Difference between merge and rebase +- git reset vs git revert +- git stash usage +- Branching workflow + +### Linux + +- chmod, chown and chgrp +- Process management +- Disk and memory troubleshooting commands + +### Shell Scripting + +- Loops and conditions +- Functions +- User input and variables +- Basic automation scripts + +--- + +## Teach Back + +### What is Git Branching? + +Git branching allows developers to work on features separately without affecting the main code. Each branch acts like an independent workspace. Once changes are tested, they can be merged back into the main branch. Branching helps teams work safely and efficiently on multiple features at the same time. + +--- + +## Key Learning + +Revision helped me understand which concepts I know confidently and which topics I need to practice more. It also improved my confidence in Git, Linux and shell scripting fundamentals. + +--- + +## GitHub Profile + +https://github.com/Asmasaiyed104 diff --git a/2026/day-29/Screenshot 2026-06-18 114627.png b/2026/day-29/Screenshot 2026-06-18 114627.png new file mode 100644 index 0000000000..e0ed714691 Binary files /dev/null and b/2026/day-29/Screenshot 2026-06-18 114627.png differ diff --git a/2026/day-29/Screenshot 2026-06-18 115333.png b/2026/day-29/Screenshot 2026-06-18 115333.png new file mode 100644 index 0000000000..1d60c5818d Binary files /dev/null and b/2026/day-29/Screenshot 2026-06-18 115333.png differ diff --git a/2026/day-29/Screenshot 2026-06-18 115341.png b/2026/day-29/Screenshot 2026-06-18 115341.png new file mode 100644 index 0000000000..757b38515c Binary files /dev/null and b/2026/day-29/Screenshot 2026-06-18 115341.png differ diff --git a/2026/day-29/Screenshot 2026-06-18 115512.png b/2026/day-29/Screenshot 2026-06-18 115512.png new file mode 100644 index 0000000000..f33f01ab3a Binary files /dev/null and b/2026/day-29/Screenshot 2026-06-18 115512.png differ diff --git a/2026/day-29/Screenshot 2026-06-18 115547.png b/2026/day-29/Screenshot 2026-06-18 115547.png new file mode 100644 index 0000000000..f80542d518 Binary files /dev/null and b/2026/day-29/Screenshot 2026-06-18 115547.png differ diff --git a/2026/day-29/day-29-docker-basics.md b/2026/day-29/day-29-docker-basics.md new file mode 100644 index 0000000000..78ac2e9912 --- /dev/null +++ b/2026/day-29/day-29-docker-basics.md @@ -0,0 +1,177 @@ +What is Docker? + +Docker is a containerization platform used to package applications with all dependencies together so the application runs the same everywhere. + +For example, sometimes a Java app works on my laptop but fails on another machine because Java version or dependencies are different. Docker solves this issue by packaging everything together inside a container. + +What is a Container? + +A container is a lightweight isolated environment where an application runs. + +It contains: + +Application code +Required libraries +Dependencies +Runtime + +Containers are fast because they share the host operating system kernel instead of installing a full OS. + +Why Do We Need Containers? + +Before Docker, developers faced many issues like: + +Application works on developer machine but not in production +Dependency conflicts +Environment mismatch + +Docker helps by creating the same environment everywhere. + +Example: +If I run a Python app in Docker on Windows, the same container can run on Linux server without changing anything. + +Containers vs Virtual Machines + +The main difference is: + +Virtual Machine contains: + +Full operating system +Application +Libraries + +Container contains only: + +Application +Dependencies + +Containers share the host OS kernel, so they are lightweight and start very fast compared to VMs. + +VMs use more RAM and storage, while containers are smaller and faster. + +Docker Architecture + +Docker architecture mainly has 4 parts: + +Docker Client + +This is where we type commands like: + +docker run nginx +Docker Daemon + +Docker daemon runs in the background and manages: + +Images +Containers +Networks +Volumes +Docker Images + +Docker image is like a template. + +Example: + +Ubuntu image +Nginx image +Python image +Docker Container + +Container is a running instance of an image. + +Example: + +docker run nginx + +This creates and starts an Nginx container. + +Docker Registry + +Docker Hub is a registry where Docker images are stored. + +We can: + +Pull images +Push images +My First Docker Container + +To test Docker installation: + +docker run hello-world + +What happens: + +Docker checks local machine for image +If image not found, it downloads from Docker Hub +Docker creates container +Container runs and displays output +Running Nginx Container +docker run -d -p 8080:80 nginx + +Explanation: + +-d means detached mode +-p means port mapping +8080 is host port +80 is container port + +Then I can access Nginx in browser using: + +http://localhost:8080 +Interactive Ubuntu Container +docker run -it ubuntu + +This opens Ubuntu container like a mini Linux machine. + +I can run commands like: + +ls +pwd +apt update + +Exit: + +exit +Useful Docker Commands +List Running Containers +docker ps +List All Containers +docker ps -a +Stop Container +docker stop container_id +Remove Container +docker rm container_id +Detached Mode +docker run -d nginx + +Detached mode runs the container in background. + +Without -d, terminal stays busy. + +Custom Container Name +docker run -d --name my-nginx nginx + +This helps because instead of random container names, I can easily identify containers. + +Check Logs +docker logs my-nginx + +Used for troubleshooting and debugging. + +Execute Command Inside Container +docker exec -it my-nginx bash + +This opens terminal inside running container. + +Why Docker is Important in DevOps + +Docker is widely used in DevOps because: + +Same environment everywhere +Faster deployment +Easy scaling +Works with Kubernetes +Useful in CI/CD pipelines +Supports microservices architecture + +Almost every modern cloud and DevOps project uses Docker. diff --git a/2026/day-30/Screenshot 2026-06-18 122156.png b/2026/day-30/Screenshot 2026-06-18 122156.png new file mode 100644 index 0000000000..48484acc71 Binary files /dev/null and b/2026/day-30/Screenshot 2026-06-18 122156.png differ diff --git a/2026/day-30/Screenshot 2026-06-18 122209.png b/2026/day-30/Screenshot 2026-06-18 122209.png new file mode 100644 index 0000000000..5c6ebdc62a Binary files /dev/null and b/2026/day-30/Screenshot 2026-06-18 122209.png differ diff --git a/2026/day-30/Screenshot 2026-06-18 122221.png b/2026/day-30/Screenshot 2026-06-18 122221.png new file mode 100644 index 0000000000..b7553dd7e4 Binary files /dev/null and b/2026/day-30/Screenshot 2026-06-18 122221.png differ diff --git a/2026/day-30/Screenshot 2026-06-18 122231.png b/2026/day-30/Screenshot 2026-06-18 122231.png new file mode 100644 index 0000000000..69c0346fc0 Binary files /dev/null and b/2026/day-30/Screenshot 2026-06-18 122231.png differ diff --git a/2026/day-30/Screenshot 2026-06-18 122358.png b/2026/day-30/Screenshot 2026-06-18 122358.png new file mode 100644 index 0000000000..d21ef403c1 Binary files /dev/null and b/2026/day-30/Screenshot 2026-06-18 122358.png differ diff --git a/2026/day-30/Screenshot 2026-06-18 123027.png b/2026/day-30/Screenshot 2026-06-18 123027.png new file mode 100644 index 0000000000..c59af30106 Binary files /dev/null and b/2026/day-30/Screenshot 2026-06-18 123027.png differ diff --git a/2026/day-30/Screenshot 2026-06-18 143222.png b/2026/day-30/Screenshot 2026-06-18 143222.png new file mode 100644 index 0000000000..c41f9e3eda Binary files /dev/null and b/2026/day-30/Screenshot 2026-06-18 143222.png differ diff --git a/2026/day-30/Screenshot 2026-06-18 143234.png b/2026/day-30/Screenshot 2026-06-18 143234.png new file mode 100644 index 0000000000..efd10120e9 Binary files /dev/null and b/2026/day-30/Screenshot 2026-06-18 143234.png differ diff --git a/2026/day-30/Screenshot 2026-06-18 143248.png b/2026/day-30/Screenshot 2026-06-18 143248.png new file mode 100644 index 0000000000..202cede4e4 Binary files /dev/null and b/2026/day-30/Screenshot 2026-06-18 143248.png differ diff --git a/2026/day-30/Screenshot 2026-06-18 143304.png b/2026/day-30/Screenshot 2026-06-18 143304.png new file mode 100644 index 0000000000..6025df8d17 Binary files /dev/null and b/2026/day-30/Screenshot 2026-06-18 143304.png differ diff --git a/2026/day-30/Screenshot 2026-06-18 143316.png b/2026/day-30/Screenshot 2026-06-18 143316.png new file mode 100644 index 0000000000..d90565e390 Binary files /dev/null and b/2026/day-30/Screenshot 2026-06-18 143316.png differ diff --git a/2026/day-30/Screenshot 2026-06-18 143328.png b/2026/day-30/Screenshot 2026-06-18 143328.png new file mode 100644 index 0000000000..0bfd9455fd Binary files /dev/null and b/2026/day-30/Screenshot 2026-06-18 143328.png differ diff --git a/2026/day-30/day-30-images.md b/2026/day-30/day-30-images.md new file mode 100644 index 0000000000..4d7ae6bf88 --- /dev/null +++ b/2026/day-30/day-30-images.md @@ -0,0 +1,361 @@ +# Day 30 – Docker Images & Container Lifecycle + +## Docker Images + +Today I worked with Docker images and container lifecycle. + +First, I pulled images from Docker Hub: + +```bash id="jlwm01" +docker pull nginx +docker pull ubuntu +docker pull alpine +``` + +Then I listed all images: + +```bash id="jlwm02" +docker images +``` + +I observed: + +- nginx image was bigger +- ubuntu image medium size +- alpine image very small + +--- + +# Ubuntu vs Alpine + +Ubuntu image contains: + +- more packages +- more tools +- full Linux environment + +That is why Ubuntu image size is larger. + +Alpine image is: + +- lightweight +- minimal Linux distribution +- used for smaller production Docker images + +Alpine image size was around: + +```text id="jlwm03" +3.8 MB +``` + +while Ubuntu was much bigger. + +--- + +# Docker Inspect + +I inspected images using: + +```bash id="jlwm04" +docker inspect nginx +docker inspect ubuntu +docker inspect alpine +``` + +From inspect output I learned: + +- image ID +- environment variables +- exposed ports +- entrypoint +- default command +- architecture +- image layers +- image size + +Example: +Nginx image exposed: + +```text id="jlwm05" +80/tcp +``` + +because nginx runs on port 80. + +--- + +# Docker Image History + +I checked image layers using: + +```bash id="jlwm06" +docker image history nginx +``` + +I learned: + +- Docker images are made of layers +- each Dockerfile instruction creates a layer +- some layers show MB size +- some layers show 0B + +0B layers usually contain metadata changes like: + +- ENV +- CMD +- LABEL +- ENTRYPOINT + +--- + +# Why Docker Uses Layers + +Docker uses layers for: + +- caching +- faster builds +- storage optimization + +If only one layer changes, Docker rebuilds only that layer instead of rebuilding the entire image. + +This improves CI/CD performance. + +--- + +# Container Lifecycle + +I practiced full container lifecycle. + +--- + +## Create Container + +```bash id="jlwm07" +docker create --name lifecycle-nginx nginx +``` + +This created the container without starting it. + +Container state: + +```text id="jlwm08" +Created +``` + +--- + +## Start Container + +```bash id="jlwm09" +docker start lifecycle-nginx +``` + +Docker: + +- mounted image layers +- attached network +- started nginx process + +Container became: + +```text id="jlwm10" +Running +``` + +--- + +## Pause Container + +```bash id="jlwm11" +docker pause lifecycle-nginx +``` + +This temporarily froze container processes. + +Container state: + +```text id="jlwm12" +Paused +``` + +--- + +## Unpause Container + +```bash id="jlwm13" +docker unpause lifecycle-nginx +``` + +This resumed frozen processes. + +--- + +## Stop Container + +```bash id="jlwm14" +docker stop lifecycle-nginx +``` + +Docker performed graceful shutdown using: + +```text id="jlwm15" +SIGTERM +``` + +Container state became: + +```text id="jlwm16" +Exited +``` + +--- + +## Restart Container + +```bash id="jlwm17" +docker restart lifecycle-nginx +``` + +Docker: + +- stopped container +- started it again + +--- + +## Kill Container + +```bash id="jlwm18" +docker kill lifecycle-nginx +``` + +Docker forcefully terminated the container using: + +```text id="jlwm19" +SIGKILL +``` + +Unlike stop, kill does not wait for graceful shutdown. + +--- + +## Remove Container + +```bash id="jlwm20" +docker rm lifecycle-nginx +``` + +This deleted the container completely. + +--- + +# Working with Running Containers + +I started nginx container in detached mode: + +```bash id="jlwm21" +docker run -d -p 8080:80 --name asma-nginx nginx +``` + +I accessed nginx in browser: + +```text id="jlwm22" +http://localhost:8080 +``` + +--- + +# View Logs + +```bash id="jlwm23" +docker logs asma-nginx +``` + +For real-time logs: + +```bash id="jlwm24" +docker logs -f asma-nginx +``` + +--- + +# Exec Into Container + +```bash id="jlwm25" +docker exec -it asma-nginx bash +``` + +I explored container filesystem using: + +- pwd +- ls +- cd + +--- + +# Run Single Command Inside Container + +```bash id="jlwm26" +docker exec asma-nginx ls / +``` + +This executed command inside running container without entering interactive shell. + +--- + +# Inspect Running Container + +```bash id="jlwm27" +docker inspect asma-nginx +``` + +I found: + +- container IP address +- port mappings +- mounts +- network settings + +--- + +# Cleanup Commands + +Stop all running containers: + +```bash id="jlwm28" +docker stop $(docker ps -q) +``` + +Remove stopped containers: + +```bash id="jlwm29" +docker container prune +``` + +Remove unused images: + +```bash id="jlwm30" +docker image prune -a +``` + +Check Docker disk usage: + +```bash id="jlwm31" +docker system df +``` + +--- + +# What I Learned + +Today I learned: + +- difference between images and containers +- Docker image layers +- caching +- container lifecycle +- logs and exec +- inspect command +- cleanup commands + +I also understood how Docker internally manages containers and processes. diff --git a/2026/day-30/impt-not-docker.txt.bak b/2026/day-30/impt-not-docker.txt.bak new file mode 100644 index 0000000000..31659804c3 --- /dev/null +++ b/2026/day-30/impt-not-docker.txt.bak @@ -0,0 +1,9 @@ +alpine:latest 28bd5fe8b56d 13MB 3.93MB +ubuntu:latest f3d28607ddd7 160MB 45.3MB U +nginx:latest 6415da96b72a 241MB 66MB + + + +Docker images are built using multiple layers. Each command in Dockerfile creates a layer. + Docker uses layers for caching, faster builds, and storage optimization. + Some layers show 0B because they only update metadata like ENV or CMD and do not add files. \ No newline at end of file diff --git a/2026/day-30/impt-note-docker.txt b/2026/day-30/impt-note-docker.txt new file mode 100644 index 0000000000..d14f185784 --- /dev/null +++ b/2026/day-30/impt-note-docker.txt @@ -0,0 +1,53 @@ +alpine:latest 28bd5fe8b56d 13MB 3.93MB +ubuntu:latest f3d28607ddd7 160MB 45.3MB U +nginx:latest 6415da96b72a 241MB 66MB + + + +Docker images are built using multiple layers. Each command in Dockerfile creates a layer. +Docker uses layers for caching, faster builds, and storage optimization. +Some layers show 0B because they only update metadata like ENV or CMD and do not add files. + + +docker create --name lifecycle-nginx nginx +this coimmands craete container but not start + + +docker create --name lifecycle-nginx nginx +Docker only: + +created container metadata +prepared filesystem +attached image layers +assigned container ID + + +Why daemon off; Important? + +Normally nginx runs in background. + +But inside Docker: + +main process must stay active + +So: + +daemon off; + +keeps nginx running in foreground. + +If main process stops: + +container stops automatically + +Very important Docker concept. + +docker start starts an existing container that was already created or stopped earlier. Docker mounts the image layers, +attaches networking, creates a writable layer, and starts the container's main process. + In nginx container, Docker starts the nginx process using the CMD instruction from the image + + + Easy Interview Answer + +docker stop performs graceful shutdown by sending SIGTERM signal, so the application gets time to clean up resources properly. But if the application becomes unresponsive or hangs, Docker may not be able to stop it normally. + In that case we use docker kill, which sends SIGKILL and forcefully terminates the container immediately. \ No newline at end of file diff --git a/2026/day-31/.dockerignore b/2026/day-31/.dockerignore new file mode 100644 index 0000000000..38951815c1 --- /dev/null +++ b/2026/day-31/.dockerignore @@ -0,0 +1,4 @@ +node_modules +.git +*.md +.env \ No newline at end of file diff --git a/2026/day-31/cmd-examples/Dockerfile b/2026/day-31/cmd-examples/Dockerfile new file mode 100644 index 0000000000..98c22073c6 --- /dev/null +++ b/2026/day-31/cmd-examples/Dockerfile @@ -0,0 +1,3 @@ +FROM ubuntu + +ENTRYPOINT ["echo"] \ No newline at end of file diff --git a/2026/day-31/day-31-dockerfile.md b/2026/day-31/day-31-dockerfile.md new file mode 100644 index 0000000000..2b9897563b --- /dev/null +++ b/2026/day-31/day-31-dockerfile.md @@ -0,0 +1,435 @@ +# Day 31 – Dockerfile & Custom Images + +## Introduction + +Today I learned how to create my own Docker images using Dockerfile. + +I practiced: + +- building custom images +- Dockerfile instructions +- CMD vs ENTRYPOINT +- running custom website in Docker +- .dockerignore +- Docker build cache + +--- + +# Task 1 – My First Dockerfile + +I created my first Dockerfile using Ubuntu base image. + +Dockerfile: + +```dockerfile +FROM ubuntu + +RUN apt-get update && apt-get install -y curl + +WORKDIR /app + +COPY . . + +CMD ["echo","Hello Friends"] +``` + +--- + +# Build Image + +```bash +docker build -t asma-file . +``` + +This created a custom Docker image. + +--- + +# Run Container + +```bash +docker run asma-file +``` + +Output: + +```text +Hello Friends +``` + +--- + +# What I Learned + +## FROM + +```dockerfile +FROM ubuntu +``` + +Uses Ubuntu as base image. + +--- + +## RUN + +```dockerfile +RUN apt-get update && apt-get install -y curl +``` + +Runs commands during image build time. + +Used to install packages. + +--- + +## WORKDIR + +```dockerfile +WORKDIR /app +``` + +Sets working directory inside container. + +--- + +## COPY + +```dockerfile +COPY . . +``` + +Copies files from host machine into Docker image. + +--- + +## CMD + +```dockerfile +CMD ["echo","Hello Friends"] +``` + +Runs default command when container starts. + +--- + +# Important Understanding + +Container stays alive only while the main process is running. + +Since: + +```dockerfile +CMD ["echo","Hello Friends"] +``` + +finishes quickly, the container exits immediately. + +--- + +# Task 2 – Dockerfile Instructions + +Dockerfile: + +```dockerfile +FROM ubuntu + +RUN apt-get update && apt-get install -y curl + +WORKDIR /app + +COPY message.txt . + +EXPOSE 8080 + +CMD ["cat", "message.txt"] +``` + +--- + +# What I Learned + +## EXPOSE + +```dockerfile +EXPOSE 8080 +``` + +Documents the port used by application inside container. + +--- + +## COPY message.txt . + +Copies file into current working directory inside container. + +--- + +# RUN vs CMD + +| RUN | CMD | +| --------------------------- | --------------------------------- | +| Executes during image build | Executes during container startup | + +--- + +# Task 3 – CMD vs ENTRYPOINT + +## CMD Example + +Dockerfile: + +```dockerfile +FROM ubuntu + +CMD ["echo", "hello from CMD"] +``` + +Run: + +```bash +docker run cmd-demo:v1 +``` + +Output: + +```text +hello from CMD +``` + +Override command: + +```bash +docker run cmd-demo:v1 echo "custom message" +``` + +Output: + +```text +custom message +``` + +I learned: + +- CMD can be overridden. + +--- + +## ENTRYPOINT Example + +Dockerfile: + +```dockerfile +FROM ubuntu + +ENTRYPOINT ["echo"] +``` + +Run: + +```bash +docker run entrypoint-demo:v1 hello from entrypoint +``` + +Output: + +```text +hello from entrypoint +``` + +I learned: + +- ENTRYPOINT acts like fixed executable. +- Additional arguments get appended. + +--- + +# CMD vs ENTRYPOINT + +| CMD | ENTRYPOINT | +| ----------------- | ---------------------- | +| Default command | Fixed executable | +| Easily overridden | Usually not overridden | + +--- + +# Task 4 – Custom Website Using Nginx + +I created a simple HTML website. + +## index.html + +```html + + + + Asma Docker Website + + +

Hello from Asma's custom Docker image!

+

This website is running inside an Nginx container.

+ + +``` + +--- + +# Website Dockerfile + +```dockerfile +FROM nginx:alpine + +COPY index.html /usr/share/nginx/html/index.html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] +``` + +--- + +# Build Image + +```bash +docker build -t my-website:v1 . +``` + +--- + +# Run Container + +```bash +docker run -d -p 8080:80 --name asma-website my-website:v1 +``` + +--- + +# Browser Access + +```text +http://localhost:8080 +``` + +--- + +# What I Learned + +- Nginx serves files from: + +```text +/usr/share/nginx/html/ +``` + +- `daemon off;` keeps nginx running in foreground. +- Long-running process keeps container alive. + +--- + +# Task 5 – .dockerignore + +I created: + +```text +.dockerignore +``` + +Content: + +```text +node_modules +.git +*.md +.env +``` + +--- + +# What I Learned + +`.dockerignore` prevents unnecessary files from being sent to Docker build context. + +Benefits: + +- smaller image size +- faster builds +- better security +- avoids copying secrets + +--- + +# Important Docker Concepts Learned + +## Image + +Read-only template used to create containers. + +--- + +## Container + +Running instance of an image. + +--- + +## Build Context + +Files Docker sends to daemon during image build. + +--- + +## Layers + +Each Dockerfile instruction creates a layer. + +Docker uses layers for: + +- caching +- faster builds +- storage optimization + +--- + +# Interview Questions I Learned + +## What is Dockerfile? + +Dockerfile is a text file containing instructions to build a custom Docker image. + +--- + +## Difference between image and container? + +Image is a template. Container is a running instance of image. + +--- + +## Difference between RUN and CMD? + +RUN executes during image build. CMD executes during container startup. + +--- + +## Why use nginx:alpine? + +Because Alpine is lightweight and reduces image size. + +--- + +## Why container exits automatically sometimes? + +Because container stays alive only while main process is running. + +--- + +# Conclusion + +Today I learned: + +- how to create custom Docker images +- Dockerfile instructions +- CMD vs ENTRYPOINT +- building website containers +- .dockerignore +- Docker caching and optimization + +This lab helped me understand how real Docker images are built and managed in DevOps environments. diff --git a/2026/day-31/dockerfile-instructions/Dockerfile b/2026/day-31/dockerfile-instructions/Dockerfile new file mode 100644 index 0000000000..c83d33aefe --- /dev/null +++ b/2026/day-31/dockerfile-instructions/Dockerfile @@ -0,0 +1,11 @@ +FROM ubuntu + +RUN apt-get update && apt-get install -y curl + +WORKDIR /app + +COPY message.txt . + +EXPOSE 8082 + +CMD ["cat", "message.txt"] \ No newline at end of file diff --git a/2026/day-31/dockerfile-instructions/message.txt b/2026/day-31/dockerfile-instructions/message.txt new file mode 100644 index 0000000000..5736f2dc91 --- /dev/null +++ b/2026/day-31/dockerfile-instructions/message.txt @@ -0,0 +1 @@ +Hello Asma, this file was copied into Docker image. \ No newline at end of file diff --git a/2026/day-31/entrypoint-example/Dockerfile b/2026/day-31/entrypoint-example/Dockerfile new file mode 100644 index 0000000000..98c22073c6 --- /dev/null +++ b/2026/day-31/entrypoint-example/Dockerfile @@ -0,0 +1,3 @@ +FROM ubuntu + +ENTRYPOINT ["echo"] \ No newline at end of file diff --git a/2026/day-31/my-first-image/Dockerfile b/2026/day-31/my-first-image/Dockerfile new file mode 100644 index 0000000000..fc4178b28f --- /dev/null +++ b/2026/day-31/my-first-image/Dockerfile @@ -0,0 +1,14 @@ +# start my image using ubuntu base image +FROM ubuntu + +#this create working dir inside container +WORKDIR /app + +#copy everything from my laptopn to current working container's directory +COPY . . + +#update Ubuntu packages and install curl, run execute while building image, not while starting container +RUN apt-get update && apt-get install -y curl + +# this run when container start +CMD ["echo","Hello Firiends"] \ No newline at end of file diff --git a/2026/day-31/my-website/Dockerfile b/2026/day-31/my-website/Dockerfile new file mode 100644 index 0000000000..0984279756 --- /dev/null +++ b/2026/day-31/my-website/Dockerfile @@ -0,0 +1,7 @@ +FROM nginx:alpine + +COPY index.html /usr/share/nginx/html/index.html + +EXPOSE 80 + +CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file diff --git a/2026/day-31/my-website/index.html b/2026/day-31/my-website/index.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/2026/day-31/note.txt b/2026/day-31/note.txt new file mode 100644 index 0000000000..9147381fb4 --- /dev/null +++ b/2026/day-31/note.txt @@ -0,0 +1,47 @@ +What happened internally? + +Docker read instructions line by line. + +Each instruction created a new layer. + + +t asma-file +craete the tag name image, image name = asma-file +tag = latest + + +. (dot) + +Very important interview question. + +Means: + +Current folder is the build context, Docker sends all files from current folder to Docker daemon. + + +Difference between RUN and CMD? + +Very important. + +Human answer: + +RUN CMD +Executes during image build Executes when container starts + +Example: + +RUN installs curl +CMD prints message + +Container stops because the main process finished execution. In this case echo command completed and container exited. + +Docker uses layers for caching, faster builds, and storage optimization. + +Docker executes Dockerfile instructions one by one and creates layers for each instruction to build the final image. + +Container exited because the main process completed execution. Docker containers stay alive only while the main process is running. +In this case the cat message.txt command finished immediately, so the container stopped automatically. + + +CMD provides a default command that can be overridden during container runtime. ENTRYPOINT defines the main executable for the container and additional arguments passed during docker run are appended to ENTRYPOINT. +CMD is used for default behavior while ENTRYPOINT is used when the container should behave like a dedicated executable. \ No newline at end of file diff --git a/2026/day-31/note.txt.bak b/2026/day-31/note.txt.bak new file mode 100644 index 0000000000..d9b3e0fab5 --- /dev/null +++ b/2026/day-31/note.txt.bak @@ -0,0 +1,38 @@ +What happened internally? + +Docker read instructions line by line. + +Each instruction created a new layer. + + +t asma-file +craete the tag name image, image name = asma-file +tag = latest + + +. (dot) + +Very important interview question. + +Means: + +Current folder is the build context, Docker sends all files from current folder to Docker daemon. + + +Difference between RUN and CMD? + +Very important. + +Human answer: + +RUN CMD +Executes during image build Executes when container starts + +Example: + +RUN installs curl +CMD prints message + +Container stops because the main process finished execution. In this case echo command completed and container exited. + +Docker uses layers for caching, faster builds, and storage optimization. \ No newline at end of file diff --git a/2026/day-32/Screenshot 2026-06-19 113948.png b/2026/day-32/Screenshot 2026-06-19 113948.png new file mode 100644 index 0000000000..3a08137190 Binary files /dev/null and b/2026/day-32/Screenshot 2026-06-19 113948.png differ diff --git a/2026/day-32/Screenshot 2026-06-19 113957.png b/2026/day-32/Screenshot 2026-06-19 113957.png new file mode 100644 index 0000000000..78d22c891d Binary files /dev/null and b/2026/day-32/Screenshot 2026-06-19 113957.png differ diff --git a/2026/day-32/Screenshot 2026-06-19 120102.png b/2026/day-32/Screenshot 2026-06-19 120102.png new file mode 100644 index 0000000000..f6bd82ead3 Binary files /dev/null and b/2026/day-32/Screenshot 2026-06-19 120102.png differ diff --git a/2026/day-32/Screenshot 2026-06-19 120150.png b/2026/day-32/Screenshot 2026-06-19 120150.png new file mode 100644 index 0000000000..67e9420191 Binary files /dev/null and b/2026/day-32/Screenshot 2026-06-19 120150.png differ diff --git a/2026/day-32/Screenshot 2026-06-19 120200.png b/2026/day-32/Screenshot 2026-06-19 120200.png new file mode 100644 index 0000000000..6538fbac75 Binary files /dev/null and b/2026/day-32/Screenshot 2026-06-19 120200.png differ diff --git a/2026/day-32/Screenshot 2026-06-19 120212.png b/2026/day-32/Screenshot 2026-06-19 120212.png new file mode 100644 index 0000000000..909e33e43b Binary files /dev/null and b/2026/day-32/Screenshot 2026-06-19 120212.png differ diff --git a/2026/day-32/Screenshot 2026-06-19 132040.png b/2026/day-32/Screenshot 2026-06-19 132040.png new file mode 100644 index 0000000000..a6120bc023 Binary files /dev/null and b/2026/day-32/Screenshot 2026-06-19 132040.png differ diff --git a/2026/day-32/Screenshot 2026-06-19 134934.png b/2026/day-32/Screenshot 2026-06-19 134934.png new file mode 100644 index 0000000000..ca9b6956a7 Binary files /dev/null and b/2026/day-32/Screenshot 2026-06-19 134934.png differ diff --git a/2026/day-32/Screenshot 2026-06-19 140153.png b/2026/day-32/Screenshot 2026-06-19 140153.png new file mode 100644 index 0000000000..19ec464bbb Binary files /dev/null and b/2026/day-32/Screenshot 2026-06-19 140153.png differ diff --git a/2026/day-32/Screenshot 2026-06-19 140201.png b/2026/day-32/Screenshot 2026-06-19 140201.png new file mode 100644 index 0000000000..9d65c8a65a Binary files /dev/null and b/2026/day-32/Screenshot 2026-06-19 140201.png differ diff --git a/2026/day-32/Screenshot 2026-06-19 140212.png b/2026/day-32/Screenshot 2026-06-19 140212.png new file mode 100644 index 0000000000..af9cc7c93c Binary files /dev/null and b/2026/day-32/Screenshot 2026-06-19 140212.png differ diff --git a/2026/day-32/day-32-volumes-networking.md b/2026/day-32/day-32-volumes-networking.md new file mode 100644 index 0000000000..f82a1543c8 --- /dev/null +++ b/2026/day-32/day-32-volumes-networking.md @@ -0,0 +1,67 @@ +# Day 32 – Docker Volumes & Networking Concepts (/human) + +In this lab, I learned two very important Docker concepts: persistent storage and container networking. + +By default, containers are temporary. If I create a database container and store data inside it, the data remains only inside the container layer. Once the container is removed, all the data is also deleted. This is why Docker volumes are important. + +First, I created a named Docker volume. A volume is like permanent storage managed by Docker. Even if the container is deleted, the volume still exists and keeps the data safe. I attached the volume to the MySQL container using: + +```bash +-v final1:/var/lib/mysql +``` + +Here: + +- `final1` is the Docker volume name +- `/var/lib/mysql` is the MySQL data directory inside the container + +This means MySQL stores all database files inside the Docker volume instead of temporary container storage. + +I also learned bind mounts. In bind mounts, a folder from my local machine is directly connected to a folder inside the container. For example, I created an `index.html` file on my Windows machine and mounted it inside an Nginx container. When I changed the HTML file locally and refreshed the browser, the changes appeared immediately. This is called live file syncing and is mostly used during development. + +Then I learned Docker networking. + +By default, containers run on the default bridge network. Containers can communicate using IP addresses, but name-based communication is not reliable there. + +So I created a custom Docker bridge network: + +```bash +docker network create final1-net +``` + +Then I started two containers on the same custom network: + +```bash +docker run -dit --name app-client --network final1-net ubuntu bash +``` + +and MySQL container: + +```bash +docker run -d --name final1 --network final1-net ... +``` + +Because both containers were connected to the same custom network, Docker automatically provided internal DNS resolution. This allowed containers to communicate using container names instead of IP addresses. + +From inside the app container, I tested connectivity by pinging the MySQL container: + +```bash +ping final1 +``` + +This verified that both containers could communicate over the Docker network. + +I understood that in real production environments: + +- frontend containers talk to backend containers +- backend containers talk to database containers +- communication usually happens using container names, not IP addresses + +I also learned: + +- Volumes provide persistent storage +- Bind mounts provide live file synchronization +- Custom Docker networks provide container-to-container communication +- Docker automatically manages internal DNS for custom networks + +This lab helped me understand the real foundation of modern containerized applications and DevOps environments. diff --git a/2026/day-32/docker concept -knowledge.txt b/2026/day-32/docker concept -knowledge.txt new file mode 100644 index 0000000000..f19b93bcca --- /dev/null +++ b/2026/day-32/docker concept -knowledge.txt @@ -0,0 +1,11 @@ +when you pull image from docker hub , its only read only image layers but when you run docker run nginx , so docker create extra writable layer on top of image layers, +This is called writable container layer + +Overlay Filesystem: Docker image has mutiple layers, example ubuntu base has packages, app files, config, Docker merge them into one virtual filesysetm using Overlays +layer 1 +layer 2 +layer 3 +Writable layer + +Docker overlays them into single filesystem view isnide container +WHy? it saves storage, faster builds ,layer reusue, efficient conatiners \ No newline at end of file diff --git a/2026/day-32/note.txt b/2026/day-32/note.txt new file mode 100644 index 0000000000..f2b083e618 --- /dev/null +++ b/2026/day-32/note.txt @@ -0,0 +1,57 @@ + +docker image pull --create docker volume--mount it + + +first docker run means create and start a container +here Docker creates container , starts MYSQL process +-d means detached mode, container run in background, so your terminal become free gain. +without -d terminal stays attched tp container logs +-name mysql-no-volume means give custom name to container, you can give any name +-e MYSQL_ROOT_PASSWORD=root123 -e means environment variable, +MYSQL_ROOT_PASSWORD sets MySql root user password, Password becomes root123 +-e MYSQL_DATABASE=testdb thia automatically creates database testdb when MySql container starts first time +mysql this is a image , docker checks local images if not found , pulls from Docker Hub +then craete container from image +******************************************************************************************************************************** +WHat internally dodcker do ? +pulls image if image not alredy downloaded , creates writable container layer +, image is read only, container get writable layer , Mysql database files currently store inside writable layer +main process becaome mysqld, Container now has linux filesystem, MySQL binaries,DB directories, temp writable storage +container gets interna; IP, netwrok interface +**************************************************************************************************************************************** + +database data is NOT persistent +Because:data stored inside container writable layer, if container deleted writable layer deleted, database deleted + +so whever container gets deleted data might be lose so docker volume exist to solve this problem + +Without volumes, database data is stored inside the container writable layer. +When the container is removed, the writable layer is deleted and the data is lost. + +********************************************************************************************************* +docker container prune removes all stopped containers and frees Docker storage space. Running containers are not affected. +******************************************************************************************************** +Difference between anonymous volume(withoy name number ) and named volume? +Anonymous volumes are automatically created by Docker with random names, + while named volumes are manually created by users with meaningful names and are easier to manage in production. + +****************************************************************************************************** +I created a named Docker volume called mysql-data and attached it to the MySQL container using: + +-v mysql-data:/var/lib/mysql + +This stores database files inside the Docker volume instead of the temporary container layer. After deleting the container and creating a new one with the same volume, my old database data was still available. + +I learned that Docker volumes provide persistent storage and keep data safe even if containers are removed. +********************************************************************************************************** +I created a local folder with an index.html file and bind mounted it into an Nginx container using: + +docker run -d --name bind-nginx -p 8080:80 -v ${PWD}:/usr/share/ngi +${PWD} represents my current local folder and /usr/share/nginx/html is the Nginx web directory inside the container. + +When I edited the index.html file on my Windows machine and refreshed the browser, the changes appeared immediately without rebuilding the image. + +I learned that bind mounts directly connect the host machine folder with the container folder and are mostly used during development for live file synchronization. +*************************************************************************************************** +I created a custom Docker bridge network so containers could communicate with each other using container names instead of IP addresses. I tested connectivity by pinging one container from another container on the same network. +Docker automatically provided internal DNS resolution on the custom network. \ No newline at end of file diff --git a/2026/day-32/website-files/index.html b/2026/day-32/website-files/index.html new file mode 100644 index 0000000000..be3990f777 --- /dev/null +++ b/2026/day-32/website-files/index.html @@ -0,0 +1 @@ +

Hello from bind mount!

\ No newline at end of file diff --git a/2026/day-33/Screenshot 2026-06-19 173132.png b/2026/day-33/Screenshot 2026-06-19 173132.png new file mode 100644 index 0000000000..5425c5605b Binary files /dev/null and b/2026/day-33/Screenshot 2026-06-19 173132.png differ diff --git a/2026/day-33/Screenshot 2026-06-19 173601.png b/2026/day-33/Screenshot 2026-06-19 173601.png new file mode 100644 index 0000000000..f270db717d Binary files /dev/null and b/2026/day-33/Screenshot 2026-06-19 173601.png differ diff --git a/2026/day-33/Screenshot 2026-06-19 173628.png b/2026/day-33/Screenshot 2026-06-19 173628.png new file mode 100644 index 0000000000..22cf283264 Binary files /dev/null and b/2026/day-33/Screenshot 2026-06-19 173628.png differ diff --git a/2026/day-33/Screenshot 2026-06-19 175123.png b/2026/day-33/Screenshot 2026-06-19 175123.png new file mode 100644 index 0000000000..e684cb8173 Binary files /dev/null and b/2026/day-33/Screenshot 2026-06-19 175123.png differ diff --git a/2026/day-33/Screenshot 2026-06-19 175346.png b/2026/day-33/Screenshot 2026-06-19 175346.png new file mode 100644 index 0000000000..8dc91e5613 Binary files /dev/null and b/2026/day-33/Screenshot 2026-06-19 175346.png differ diff --git a/2026/day-33/compose-basics/docker-compose.yml b/2026/day-33/compose-basics/docker-compose.yml new file mode 100644 index 0000000000..eb740651a7 --- /dev/null +++ b/2026/day-33/compose-basics/docker-compose.yml @@ -0,0 +1,6 @@ +services: + nginx: + image: nginx + container_name: compose-nginx + ports: + - "8080:80" diff --git a/2026/day-33/day-33-compose.md b/2026/day-33/day-33-compose.md new file mode 100644 index 0000000000..c1b6a4511a --- /dev/null +++ b/2026/day-33/day-33-compose.md @@ -0,0 +1,50 @@ +In this lab, I learned Docker Compose. Docker Compose helps us run multiple containers using one YAML configuration file instead of writing long Docker commands again and again. + +First, I checked Docker Compose version using: + +docker compose version + +Then I created a folder called compose-basics and created a file named: + +docker-compose.yml + +Inside the file, I wrote: + +services: +nginx: +image: nginx +container_name: compose-nginx +ports: - "8080:80" + +This file tells Docker Compose: + +use nginx image +create container named compose-nginx +map localhost port 8080 to container port 80 + +Then I started the container using: + +docker compose up + +Internally, Docker Compose automatically: + +pulled nginx image +created container +created network +applied port mapping +started the container + +After that, I opened: + +http://localhost:8080 + +and verified the Nginx page was running. + +Then I stopped and removed everything using: + +docker compose down + +I understood that Docker Compose makes container management easier because all configuration stays inside one YAML file. Instead of manually creating networks, containers, and port mappings, Compose does everything automatically with one command. + +Main understanding: +Docker Compose is used to manage multi-container applications easily using one configuration file. diff --git a/2026/day-33/dns-demo/docker-compose.yml b/2026/day-33/dns-demo/docker-compose.yml new file mode 100644 index 0000000000..446e505042 --- /dev/null +++ b/2026/day-33/dns-demo/docker-compose.yml @@ -0,0 +1,10 @@ +services: + backend: + image: nginx + container_name: backend + + frontend: + image: ubuntu + container_name: frontend + tty: true + diff --git a/2026/day-33/notes-dns.md b/2026/day-33/notes-dns.md new file mode 100644 index 0000000000..e7f5dad50d --- /dev/null +++ b/2026/day-33/notes-dns.md @@ -0,0 +1,67 @@ +In this practical, I understood how multiple Docker containers communicate with each other using Docker Compose internal networking and DNS. + +First, I created a docker-compose.yml file with two services: + +services: + backend: + image: nginx + container_name: backend + + frontend: + image: ubuntu + container_name: frontend + tty: true + +Here: + +backend container runs Nginx server +frontend container runs Ubuntu container + +Then I started both containers using: + +docker compose up -d + +Internally, Docker Compose automatically: + +created both containers +created one shared Docker network +connected both containers to same network +created internal DNS names + +Docker automatically used service names as DNS names: + +backend +frontend + +Then I entered inside frontend container: + +docker exec -it frontend bash + +Inside the container, I installed curl: + +apt update +apt install curl -y + +Here: + +apt update refreshed Ubuntu package list +apt install curl -y installed curl tool inside container + +Curl is an HTTP client tool used to call websites, APIs, or other containers. + +Then I tested container communication using: + +curl backend + +This worked because Docker internal DNS automatically resolved: + +backend → backend container IP address + +Then frontend container sent HTTP request to backend Nginx container and received: + +Welcome to nginx! + +This practical helped me understand that containers on the same Docker Compose network can communicate using service names instead of IP addresses. + +Main understanding: +Docker Compose automatically creates internal networking and DNS between containers, so services can communicate easily using container names. diff --git a/2026/day-34/Screenshot 2026-06-22 141939.png b/2026/day-34/Screenshot 2026-06-22 141939.png new file mode 100644 index 0000000000..4e0804cfc3 Binary files /dev/null and b/2026/day-34/Screenshot 2026-06-22 141939.png differ diff --git a/2026/day-34/app/Dockerfile b/2026/day-34/app/Dockerfile new file mode 100644 index 0000000000..6eeb61076f --- /dev/null +++ b/2026/day-34/app/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 5000 + +CMD ["python", "app.py"] \ No newline at end of file diff --git a/2026/day-34/app/app.py b/2026/day-34/app/app.py new file mode 100644 index 0000000000..4d956a3a36 --- /dev/null +++ b/2026/day-34/app/app.py @@ -0,0 +1,49 @@ +from flask import Flask +import psycopg2 +import redis +import os + +app = Flask(__name__) + +@app.route("/") +def home(): + + db_message = "Database not connected" + redis_message = "Redis not connected" + + try: + conn = psycopg2.connect( + host=os.getenv("DB_HOST"), + database=os.getenv("DB_NAME"), + user=os.getenv("DB_USER"), + password=os.getenv("DB_PASSWORD") + ) + + conn.close() + db_message = "PostgreSQL connected successfully" + + except Exception as e: + db_message = f"Database error: {e}" + + try: + cache = redis.Redis( + host=os.getenv("REDIS_HOST"), + port=6379 + ) + + cache.ping() + redis_message = "Redis connected successfully" + + except Exception as e: + redis_message = f"Redis error: {e}" + + return f""" +

Hello Asma - Day 34 Docker Compose Lab

+ +

{db_message}

+ +

{redis_message}

+ """ + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) \ No newline at end of file diff --git a/2026/day-34/app/requirements.txt b/2026/day-34/app/requirements.txt new file mode 100644 index 0000000000..ed4fd425d6 --- /dev/null +++ b/2026/day-34/app/requirements.txt @@ -0,0 +1,3 @@ +flask +psycopg2-binary +redis \ No newline at end of file diff --git a/2026/day-34/day-34-compose-advanced.md b/2026/day-34/day-34-compose-advanced.md new file mode 100644 index 0000000000..c3e537904e --- /dev/null +++ b/2026/day-34/day-34-compose-advanced.md @@ -0,0 +1,342 @@ +# Docker Compose Notes + +## What is Docker Compose? + +Docker Compose is a tool used to run multiple Docker containers together using one YAML file. + +Instead of writing many `docker run` commands, we write everything inside one file: + +```yaml +compose.yaml +``` + +Then we run: + +```bash +docker compose up +``` + +and all containers start together. + +--- + +# Why We Use Docker Compose + +Docker Compose helps us: + +- Run multiple containers together +- Manage complete applications +- Connect containers easily +- Save time +- Simplify development + +--- + +# Example + +Suppose an application needs: + +- Frontend +- Backend +- Database + +Docker Compose can run all of them together. + +--- + +# Basic Structure + +```yaml +services: +``` + +All containers are written under `services`. + +--- + +# Simple Example + +```yaml +services: + web: + image: nginx + + ports: + - "80:80" + + db: + image: mysql:8 +``` + +This creates: + +- Nginx container +- MySQL container + +--- + +# Important Keywords + +## services + +Defines containers. + +Example: + +```yaml +services: + app: + db: +``` + +--- + +## image + +Downloads image from Docker Hub. + +Example: + +```yaml +image: nginx +``` + +--- + +## build + +Builds image using Dockerfile. + +Example: + +```yaml +build: . +``` + +Meaning: + +- Use Dockerfile from current folder + +--- + +## ports + +Maps laptop port to container port. + +Syntax: + +```yaml +ports: + - "HOST:CONTAINER" +``` + +Example: + +```yaml +ports: + - "5000:5000" +``` + +Meaning: + +- localhost:5000 connects to container port 5000 + +--- + +## container_name + +Gives custom container name. + +Example: + +```yaml +container_name: asma-app +``` + +--- + +## environment + +Used for environment variables. + +Example: + +```yaml +environment: + MYSQL_ROOT_PASSWORD: admin123 +``` + +--- + +## volumes + +Used to save data permanently. + +Without volume: + +- data deletes when container deletes + +With volume: + +- data stays safe + +Example: + +```yaml +volumes: + - mysql-data:/var/lib/mysql +``` + +--- + +## depends_on + +Starts containers in order. + +Example: + +```yaml +depends_on: + - db +``` + +Meaning: + +- app starts after database container + +--- + +## networks + +Containers communicate using service names. + +Example: + +```text +db +``` + +NOT: + +```text +localhost +``` + +--- + +# Full Example + +```yaml +services: + app: + build: . + + container_name: python-app + + ports: + - "5000:5000" + + depends_on: + - db + + db: + image: mysql:8 + + container_name: mysql-db + + environment: + MYSQL_ROOT_PASSWORD: admin123 + + ports: + - "3306:3306" +``` + +--- + +# Common Commands + +## Start Containers + +```bash +docker compose up +``` + +--- + +## Build and Start + +```bash +docker compose up --build +``` + +--- + +## Run in Background + +```bash +docker compose up -d +``` + +--- + +## Stop Containers + +```bash +docker compose down +``` + +--- + +## Check Running Containers + +```bash +docker compose ps +``` + +--- + +## Check Logs + +```bash +docker compose logs +``` + +--- + +# Dockerfile vs Docker Compose + +| Dockerfile | Docker Compose | +| ------------------- | ------------------------- | +| Builds image | Runs containers | +| Single container | Multiple containers | +| Defines image steps | Defines application stack | + +--- + +# Workflow + +1. Create application +2. Write Dockerfile +3. Write compose.yaml +4. Run: + +```bash +docker compose up --build +``` + +5. Application starts successfully + +--- + +# Best Practices + +- Use volumes +- Use environment variables +- Use proper container names +- Keep YAML indentation correct + +--- + +# Summary + +Docker Compose helps developers and DevOps engineers manage multi-container applications easily using one YAML file. diff --git a/2026/day-34/docker-compose.yml b/2026/day-34/docker-compose.yml new file mode 100644 index 0000000000..b81df6d4c8 --- /dev/null +++ b/2026/day-34/docker-compose.yml @@ -0,0 +1,75 @@ +services: + web: + build: ./app + container_name: day34-web + ports: + - "5000:5000" + + environment: + DB_HOST: db + DB_NAME: asma_db + DB_USER: asma + DB_PASSWORD: asma123 + REDIS_HOST: redis + + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + + networks: + - day34-network + + labels: + project: "90DaysOfDevOps" + day: "34" + service: "web" + + db: + image: postgres:16 + container_name: day34-postgres + + restart: always + + environment: + POSTGRES_DB: asma_db + POSTGRES_USER: asma + POSTGRES_PASSWORD: asma123 + + volumes: + - day34-postgres-data:/var/lib/postgresql/data + + networks: + - day34-network + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U asma -d asma_db"] + interval: 10s + timeout: 5s + retries: 5 + + labels: + project: "90DaysOfDevOps" + day: "34" + service: "database" + + redis: + image: redis:7 + container_name: day34-redis + + networks: + - day34-network + + labels: + project: "90DaysOfDevOps" + day: "34" + service: "cache" + +networks: + day34-network: + name: day34-network + +volumes: + day34-postgres-data: + name: day34-postgres-data \ No newline at end of file diff --git a/2026/day-35/Screenshot 2026-06-22 151615.png b/2026/day-35/Screenshot 2026-06-22 151615.png new file mode 100644 index 0000000000..515467ce2b Binary files /dev/null and b/2026/day-35/Screenshot 2026-06-22 151615.png differ diff --git a/2026/day-35/Screenshot 2026-06-22 151804.png b/2026/day-35/Screenshot 2026-06-22 151804.png new file mode 100644 index 0000000000..90450e308a Binary files /dev/null and b/2026/day-35/Screenshot 2026-06-22 151804.png differ diff --git a/2026/day-35/Screenshot 2026-06-22 152318.png b/2026/day-35/Screenshot 2026-06-22 152318.png new file mode 100644 index 0000000000..b8a16be7c6 Binary files /dev/null and b/2026/day-35/Screenshot 2026-06-22 152318.png differ diff --git a/2026/day-35/Screenshot 2026-06-22 152446.png b/2026/day-35/Screenshot 2026-06-22 152446.png new file mode 100644 index 0000000000..45babb3575 Binary files /dev/null and b/2026/day-35/Screenshot 2026-06-22 152446.png differ diff --git a/2026/day-35/day-35-multistage-hub.md b/2026/day-35/day-35-multistage-hub.md new file mode 100644 index 0000000000..26b9fe0801 --- /dev/null +++ b/2026/day-35/day-35-multistage-hub.md @@ -0,0 +1,337 @@ +# Day 35 – Multi-Stage Builds & Docker Hub + +## Introduction + +Today I learned about multi-stage Docker builds and Docker Hub. + +Multi-stage builds help reduce Docker image size by separating the build environment from the final runtime environment. + +Smaller images are: + +- faster +- lightweight +- secure +- easy to deploy + +I also learned how to push Docker images to Docker Hub. + +--- + +# Task 1 – Single Stage Build + +## Go Application + +### main.go + +```go +package main + +import "fmt" + +func main() { + fmt.Println("Hello from Asma Day 35 Multi-Stage Docker App") +} +``` + +--- + +## Single Stage Dockerfile + +### Dockerfile.single + +```dockerfile +FROM golang:1.24 + +WORKDIR /app + +COPY . . + +RUN go build -o asma-app . + +CMD ["./asma-app"] +``` + +--- + +## Build Single Stage Image + +```bash +docker build -f Dockerfile.single -t asma-go-single . +``` + +--- + +## Check Image Size + +```bash +docker images +``` + +### Result + +| Image | Size | +| -------------- | ------ | +| asma-go-single | 1.36GB | + +The image size is large because: + +- Go compiler +- build tools +- source files +- cache + +are included inside the final image. + +--- + +# Task 2 – Multi-Stage Build + +## Multi-Stage Dockerfile + +### Dockerfile.multi + +```dockerfile +FROM golang:1.24 AS builder + +WORKDIR /app + +COPY . . + +RUN go build -o asma-app . + +FROM alpine:3.22 + +WORKDIR /app + +COPY --from=builder /app/asma-app . + +CMD ["./asma-app"] +``` + +--- + +## Build Multi-Stage Image + +```bash +docker build -f Dockerfile.multi -t asma-go-multi . +``` + +--- + +## Check Image Size + +```bash +docker images +``` + +### Result + +| Image | Size | +| ------------- | ------ | +| asma-go-multi | 16.4MB | + +--- + +# Size Comparison + +| Build Type | Size | +| ------------------ | ------ | +| Single Stage Build | 1.36GB | +| Multi-Stage Build | 16.4MB | + +--- + +# Why Multi-Stage Image is Smaller + +The multi-stage image is smaller because: + +- build tools are removed +- Go compiler is removed +- cache files are removed +- only the final application is copied + +This makes the image lightweight and secure. + +--- + +# Run the Container + +```bash +docker run asma-go-multi +``` + +### Output + +```text +Hello from Asma Day 35 Multi-Stage Docker App +``` + +--- + +# Task 3 – Push Image to Docker Hub + +## Login to Docker Hub + +```bash +docker login +``` + +--- + +## Tag Image + +```bash +docker tag asma-go-multi asmasaiyed104/asma-go-multi:v1 +``` + +```bash +docker tag asma-go-multi asmasaiyed104/asma-go-multi:latest +``` + +--- + +## Push Image + +```bash +docker push asmasaiyed104/asma-go-multi:v1 +``` + +```bash +docker push asmasaiyed104/asma-go-multi:latest +``` + +--- + +# Task 4 – Docker Hub Repository + +I checked my Docker Hub repository and explored: + +- image tags +- latest tag +- version tags +- repository description + +--- + +# latest vs Specific Tag + +## latest + +```bash +docker pull nginx:latest +``` + +Downloads the newest image. + +--- + +## Specific Tag + +```bash +docker pull nginx:1.27 +``` + +Downloads a fixed version. + +Specific tags are better because they provide stable versions. + +--- + +# Task 5 – Image Best Practices + +## 1. Minimal Base Image + +Used: + +```dockerfile +FROM alpine:3.22 +``` + +instead of Ubuntu. + +Alpine image is much smaller. + +--- + +## 2. Non-Root User + +```dockerfile +RUN adduser -D appuser + +USER appuser +``` + +This improves security. + +--- + +## 3. Reduce Layers + +Combined commands together. + +Example: + +```dockerfile +RUN apk update && apk add curl +``` + +--- + +## 4. Use Specific Tags + +Good practice: + +```dockerfile +FROM golang:1.24 +``` + +Avoid: + +```dockerfile +FROM golang:latest +``` + +Specific versions make builds stable. + +--- + +# Final Optimized Dockerfile + +```dockerfile +FROM golang:1.24 AS builder + +WORKDIR /app + +COPY . . + +RUN go build -o asma-app . + +FROM alpine:3.22 + +WORKDIR /app + +COPY --from=builder /app/asma-app . + +CMD ["./asma-app"] +``` + +--- + +# What I Learned + +Today I learned: + +- single-stage build +- multi-stage build +- image optimization +- Docker Hub push and pull +- image tagging +- Docker image best practices + +--- + +# Conclusion + +Multi-stage builds are very useful in real DevOps projects because they create smaller and optimized Docker images. + +Docker Hub helps share and distribute images easily across systems and teams. diff --git a/2026/day-35/go-app/Dockerfile.multi b/2026/day-35/go-app/Dockerfile.multi new file mode 100644 index 0000000000..1c07814567 --- /dev/null +++ b/2026/day-35/go-app/Dockerfile.multi @@ -0,0 +1,15 @@ +FROM golang:1.24 AS builder + +WORKDIR /app + +COPY . . + +RUN go build -o asma-app . + +FROM alpine:3.22 + +WORKDIR /app + +COPY --from=builder /app/asma-app . + +CMD ["./asma-app"] \ No newline at end of file diff --git a/2026/day-35/go-app/Dockerfile.single b/2026/day-35/go-app/Dockerfile.single new file mode 100644 index 0000000000..36020118bc --- /dev/null +++ b/2026/day-35/go-app/Dockerfile.single @@ -0,0 +1,9 @@ +FROM golang:1.24 + +WORKDIR /app + +COPY . . + +RUN go build -o asma-app . + +CMD ["./asma-app"] \ No newline at end of file diff --git a/2026/day-35/go-app/go.mod b/2026/day-35/go-app/go.mod new file mode 100644 index 0000000000..086a3a2793 --- /dev/null +++ b/2026/day-35/go-app/go.mod @@ -0,0 +1,3 @@ +module asma-go-app + +go 1.22.0 diff --git a/2026/day-35/go-app/main.go b/2026/day-35/go-app/main.go new file mode 100644 index 0000000000..67bfd770af --- /dev/null +++ b/2026/day-35/go-app/main.go @@ -0,0 +1,7 @@ +package main + +import "fmt" + +func main() { + fmt.Println("Hello from Asma Day 35 Multi-Stage Docker App") +} diff --git a/2026/day-36/Screenshot 2026-06-22 160115.png b/2026/day-36/Screenshot 2026-06-22 160115.png new file mode 100644 index 0000000000..7bb30ee8f5 Binary files /dev/null and b/2026/day-36/Screenshot 2026-06-22 160115.png differ diff --git a/2026/day-36/Screenshot 2026-06-22 160136.png b/2026/day-36/Screenshot 2026-06-22 160136.png new file mode 100644 index 0000000000..b8880213de Binary files /dev/null and b/2026/day-36/Screenshot 2026-06-22 160136.png differ diff --git a/2026/day-36/Screenshot 2026-06-22 160201.png b/2026/day-36/Screenshot 2026-06-22 160201.png new file mode 100644 index 0000000000..b0b8d66b7b Binary files /dev/null and b/2026/day-36/Screenshot 2026-06-22 160201.png differ diff --git a/2026/day-36/day-36-docker-project.md b/2026/day-36/day-36-docker-project.md new file mode 100644 index 0000000000..c29738662b --- /dev/null +++ b/2026/day-36/day-36-docker-project.md @@ -0,0 +1,445 @@ +# Day 36 – Dockerize a Full Application + +# Introduction + +Today I created a complete Dockerized Flask application with PostgreSQL database using Docker Compose. + +This project helped me understand how real applications run using multiple containers together. + +I learned: + +* how to Dockerize a Flask app +* how to connect Flask with PostgreSQL +* how to use Docker Compose +* how to use volumes and networks +* how to push images to Docker Hub + +--- + +# Project Used + +I created a simple Python Flask application with PostgreSQL database. + +The app has: + +* Flask backend +* PostgreSQL database +* Dockerfile +* Docker Compose +* Environment variables +* Docker volume +* Custom Docker network + +--- + +# Project Folder Structure + +```text +day-36/ +├── app/ +│ ├── app.py +│ ├── requirements.txt +│ ├── Dockerfile +│ └── .dockerignore +│ +├── docker-compose.yml +├── .env +├── README.md +└── day-36-docker-project.md +``` + +--- + +# Flask Application + +## app.py + +```python +from flask import Flask +import psycopg2 +import os + +app = Flask(__name__) + +@app.route("/") +def home(): + return "Hello Asma! Day 36 Dockerized Flask App is running." + +@app.route("/db") +def db_check(): + try: + conn = psycopg2.connect( + host=os.getenv("DB_HOST"), + database=os.getenv("DB_NAME"), + user=os.getenv("DB_USER"), + password=os.getenv("DB_PASSWORD") + ) + conn.close() + return "Database connection successful!" + except Exception as e: + return f"Database connection failed: {e}" + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) +``` + +--- + +# requirements.txt + +```text +flask==3.0.3 +psycopg2-binary==2.9.9 +``` + +--- + +# Dockerfile + +```dockerfile +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN useradd -m appuser + +USER appuser + +EXPOSE 5000 + +CMD ["python", "app.py"] +``` + +--- + +# Dockerfile Explanation + +## FROM python:3.12-slim + +Uses lightweight Python image. + +Slim images are smaller and faster. + +--- + +## WORKDIR /app + +Creates working directory inside container. + +--- + +## COPY requirements.txt . + +Copies requirements file into container. + +--- + +## RUN pip install + +Installs Flask and PostgreSQL dependencies. + +--- + +## COPY . . + +Copies project files into container. + +--- + +## RUN useradd -m appuser + +Creates non-root user for better security. + +--- + +## USER appuser + +Runs container using non-root user. + +--- + +## EXPOSE 5000 + +Opens Flask application port. + +--- + +## CMD ["python", "app.py"] + +Starts Flask application. + +--- + +# .dockerignore File + +```text +__pycache__ +*.pyc +.env +.git +.vscode +``` + +This helps reduce image size by ignoring unnecessary files. + +--- + +# Environment Variables + +## .env + +```env +DB_HOST=db +DB_NAME=asma_db +DB_USER=asma +DB_PASSWORD=asma123 + +POSTGRES_DB=asma_db +POSTGRES_USER=asma +POSTGRES_PASSWORD=asma123 +``` + +Environment variables help store configuration separately from code. + +--- + +# Docker Compose File + +## docker-compose.yml + +```yaml +services: + + app: + build: ./app + + container_name: day36-flask-app + + ports: + - "5000:5000" + + env_file: + - .env + + depends_on: + db: + condition: service_healthy + + networks: + - day36-network + + db: + image: postgres:16-alpine + + container_name: day36-postgres-db + + env_file: + - .env + + volumes: + - day36-postgres-data:/var/lib/postgresql/data + + networks: + - day36-network + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U asma -d asma_db"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + day36-postgres-data: + +networks: + day36-network: + driver: bridge +``` + +--- + +# Docker Compose Explanation + +## app service + +Runs Flask application container. + +--- + +## db service + +Runs PostgreSQL database container. + +--- + +## volumes + +Used to save database data permanently. + +Without volume, database data gets deleted when container is removed. + +--- + +## networks + +Creates custom Docker network for container communication. + +--- + +## healthcheck + +Checks if PostgreSQL database is healthy before Flask app starts. + +--- + +# Build and Run Project + +## Build Project + +```bash +docker compose up --build +``` + +--- + +## Run in Background + +```bash +docker compose up -d +``` + +--- + +## Check Running Containers + +```bash +docker compose ps +``` + +--- + +# Application Testing + +## Flask App + +```text +http://localhost:5000 +``` + +Output: + +```text +Hello Asma! Day 36 Dockerized Flask App is running. +``` + +--- + +## Database Connection Test + +```text +http://localhost:5000/db +``` + +Output: + +```text +Database connection successful! +``` + +--- + +# Docker Hub Push + +## Login + +```bash +docker login +``` + +--- + +## Tag Image + +```bash +docker tag day-36-app:latest asmasaiyed104/day-36-flask-app:v1 +``` + +--- + +## Push Image + +```bash +docker push asmasaiyed104/day-36-flask-app:v1 +``` + +--- + +# Challenges I Faced + +## 1. Wrong Folder Structure + +Initially my files were in wrong folders. + +I fixed the structure and moved: + +* docker-compose.yml +* .env + +into the correct directory. + +--- + +## 2. requirements.txt Missing + +Docker build failed because requirements.txt file was missing. + +I created the file and added dependencies. + +--- + +## 3. Typing Mistakes in Dockerfile + +I made spelling mistakes like: + +* requirement.txt +* no-catch-dir + +I corrected them to: + +* requirements.txt +* no-cache-dir + +--- + +# What I Learned + +Today I learned: + +* how to Dockerize a real Flask app +* how to connect Flask with PostgreSQL +* how Docker Compose works +* how containers communicate +* how to use volumes and networks +* how to push images to Docker Hub + +--- + +# Final Result + +My Flask application and PostgreSQL database worked successfully together using Docker Compose. + +The application was successfully Dockerized and pushed to Docker Hub. + +--- + +# Conclusion + +This project gave me real hands-on experience with Docker and Docker Compose. + +I now understand how multi-container applications work in real DevOps environments. diff --git a/2026/day-36/day-36/= b/2026/day-36/day-36/= new file mode 100644 index 0000000000..e69de29bb2 diff --git a/2026/day-36/day-36/CACHED b/2026/day-36/day-36/CACHED new file mode 100644 index 0000000000..e69de29bb2 diff --git a/2026/day-36/day-36/[internal] b/2026/day-36/day-36/[internal] new file mode 100644 index 0000000000..e69de29bb2 diff --git a/2026/day-36/day-36/app/.dockerignore b/2026/day-36/day-36/app/.dockerignore new file mode 100644 index 0000000000..e5ad70cfc1 --- /dev/null +++ b/2026/day-36/day-36/app/.dockerignore @@ -0,0 +1,6 @@ +__pycache__ +*.pyc +.env +.git +.vscode:wq + diff --git a/2026/day-36/day-36/app/Dockerfile b/2026/day-36/day-36/app/Dockerfile new file mode 100644 index 0000000000..9337a98cc6 --- /dev/null +++ b/2026/day-36/day-36/app/Dockerfile @@ -0,0 +1,17 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +RUN useradd -m appuser + +USER appuser + +EXPOSE 5000 + +CMD ["python", "app.py"] \ No newline at end of file diff --git a/2026/day-36/day-36/app/app.py b/2026/day-36/day-36/app/app.py new file mode 100644 index 0000000000..35add33637 --- /dev/null +++ b/2026/day-36/day-36/app/app.py @@ -0,0 +1,26 @@ +from flask import Flask +import psycopg2 +import os + +app = Flask(__name__) + +@app.route("/") +def home(): + return "Hello Asma! Day 36 Dockerized Flask App is running." + +@app.route("/db") +def db_check(): + try: + conn = psycopg2.connect( + host=os.getenv("DB_HOST"), + database=os.getenv("DB_NAME"), + user=os.getenv("DB_USER"), + password=os.getenv("DB_PASSWORD") + ) + conn.close() + return "Database connection successful!" + except Exception as e: + return f"Database connection failed: {e}" + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) diff --git a/2026/day-36/day-36/app/requirements.txt b/2026/day-36/day-36/app/requirements.txt new file mode 100644 index 0000000000..73c0be58c4 --- /dev/null +++ b/2026/day-36/day-36/app/requirements.txt @@ -0,0 +1,2 @@ +flask==3.0.3 +psycopg2-binary==2.9.9 diff --git a/2026/day-36/day-36/docker-compose.yml b/2026/day-36/day-36/docker-compose.yml new file mode 100644 index 0000000000..392a1702ab --- /dev/null +++ b/2026/day-36/day-36/docker-compose.yml @@ -0,0 +1,35 @@ +services: + app: + build: ./app + container_name: day36-flask-app + ports: + - "5000:5000" + env_file: + - .env + depends_on: + db: + condition: service_healthy + networks: + - day36-network + + db: + image: postgres:16-alpine + container_name: day36-postgres-db + env_file: + - .env + volumes: + - day36-postgres-data:/var/lib/postgresql/data + networks: + - day36-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U asma -d asma_db"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + day36-postgres-data: + +networks: + day36-network: + driver: bridge diff --git a/2026/day-36/day-36/exporting b/2026/day-36/day-36/exporting new file mode 100644 index 0000000000..e69de29bb2 diff --git a/2026/day-36/day-36/naming b/2026/day-36/day-36/naming new file mode 100644 index 0000000000..e69de29bb2 diff --git a/2026/day-36/day-36/reading b/2026/day-36/day-36/reading new file mode 100644 index 0000000000..e69de29bb2 diff --git a/2026/day-36/day-36/resolve b/2026/day-36/day-36/resolve new file mode 100644 index 0000000000..e69de29bb2 diff --git a/2026/day-36/day-36/resolving b/2026/day-36/day-36/resolving new file mode 100644 index 0000000000..e69de29bb2 diff --git a/2026/day-36/day-36/transferring b/2026/day-36/day-36/transferring new file mode 100644 index 0000000000..e69de29bb2 diff --git a/2026/day-36/day-36/unpacking b/2026/day-36/day-36/unpacking new file mode 100644 index 0000000000..e69de29bb2 diff --git a/2026/day-37/Docker.png b/2026/day-37/Docker.png new file mode 100644 index 0000000000..c761b8d6e8 Binary files /dev/null and b/2026/day-37/Docker.png differ diff --git a/2026/day-37/day-37-revision.md b/2026/day-37/day-37-revision.md new file mode 100644 index 0000000000..abf89e643c --- /dev/null +++ b/2026/day-37/day-37-revision.md @@ -0,0 +1,775 @@ +# Docker Deep Revision Notes + +# What is Docker? + +Docker is a containerization platform. + +It helps developers package: + +- application +- libraries +- dependencies +- configuration + +inside one container. + +Because of Docker: + +- application works same everywhere +- no dependency issues +- easy deployment +- fast setup + +--- + +# Problem Before Docker + +Before Docker: + +- application worked on one system +- failed on another system +- different library versions +- difficult setup +- manual installation + +Example: + +Developer laptop: + +```text +Python 3.12 +``` + +Server: + +```text +Python 3.8 +``` + +Application may fail. + +Docker solves this problem. + +--- + +# What is an Image? + +Image is a blueprint/template. + +Image contains: + +- application code +- dependencies +- libraries +- runtime +- commands + +Image is READ ONLY. + +Example: + +- nginx image +- python image +- mysql image + +--- + +# What is a Container? + +Container is a running instance of an image. + +Example: + +```docker id="d101" +docker run nginx +``` + +What happens internally: + +1. Docker checks local image +2. If image missing → pulls from Docker Hub +3. Creates writable container layer +4. Starts container process +5. Application starts + +--- + +# Difference Between Image and Container + +| Image | Container | +| ------------------- | ------------------- | +| Blueprint | Running application | +| Read only | Writable | +| Static | Dynamic | +| Cannot run directly | Runs application | + +--- + +# Docker Architecture + +Docker has: + +- Docker Client +- Docker Daemon +- Docker Registry + +--- + +# Docker Client + +Commands we type: + +```docker id="d102" +docker ps +``` + +Client sends request to Docker daemon. + +--- + +# Docker Daemon + +Main Docker engine. + +Responsible for: + +- building images +- running containers +- managing networks +- managing volumes + +--- + +# Docker Registry + +Stores images. + +Example: + +- Docker Hub + +--- + +# Docker Workflow + +```text id="d103" +Dockerfile → Docker Image → Docker Container +``` + +--- + +# What is Dockerfile? + +Dockerfile is a text file used to create Docker images. + +Contains instructions. + +Example: + +```dockerfile id="d104" +FROM python:3.12-slim + +WORKDIR /app + +COPY . . + +RUN pip install flask + +CMD ["python", "app.py"] +``` + +--- + +# FROM + +```dockerfile id="d105" +FROM python:3.12-slim +``` + +Purpose: + +- selects base image + +Internally: + +1. Docker downloads base image +2. Creates image layers + +--- + +# Why Slim or Alpine Images? + +Smaller size: + +- less storage +- faster download +- less vulnerabilities + +Example: + +- Ubuntu → large +- Alpine → very small + +--- + +# WORKDIR + +```dockerfile id="d106" +WORKDIR /app +``` + +Purpose: + +- sets working directory + +Internally: + +- creates folder if missing +- all next commands run inside folder + +--- + +# COPY + +```dockerfile id="d107" +COPY . . +``` + +Purpose: + +- copies files into container + +Internally: + +- Docker copies local files +- creates new image layer + +--- + +# RUN + +```dockerfile id="d108" +RUN pip install flask +``` + +Purpose: + +- executes commands during image build + +Internally: + +1. temporary container starts +2. command executes +3. changes saved into new image layer +4. temporary container removed + +--- + +# Image Layers + +Every Dockerfile instruction creates a layer. + +Example: + +```dockerfile id="d109" +FROM python +COPY . . +RUN pip install flask +``` + +Layers: + +1. Python layer +2. Copy layer +3. Pip install layer + +Benefits: + +- faster builds +- caching +- layer reuse + +--- + +# Docker Cache + +Docker reuses unchanged layers. + +Example: +If only app code changes: + +- dependencies layer reused +- build becomes faster + +--- + +# EXPOSE + +```dockerfile id="d110" +EXPOSE 5000 +``` + +Purpose: + +- documents application port + +Does NOT automatically publish port. + +--- + +# CMD + +```dockerfile id="d111" +CMD ["python", "app.py"] +``` + +Purpose: + +- default startup command + +Can be overridden. + +Example: + +```docker id="d112" +docker run image_name ls +``` + +Overrides CMD. + +--- + +# ENTRYPOINT + +```dockerfile id="d113" +ENTRYPOINT ["python"] +``` + +Purpose: + +- fixed startup command + +More strict than CMD. + +--- + +# CMD vs ENTRYPOINT + +| CMD | ENTRYPOINT | +| ---------------- | --------------------- | +| Default command | Fixed command | +| Easy to override | Difficult to override | + +--- + +# Build Docker Image + +```docker id="d114" +docker build -t my-app . +``` + +Internally: + +1. Docker reads Dockerfile +2. Executes instructions one by one +3. Creates layers +4. Builds final image + +--- + +# Docker Run + +```docker id="d115" +docker run nginx +``` + +Internally: + +1. creates container +2. creates writable layer +3. assigns network +4. starts process + +--- + +# Detached Mode + +```docker id="d116" +docker run -d nginx +``` + +Container runs in background. + +--- + +# Interactive Mode + +```docker id="d117" +docker run -it ubuntu bash +``` + +Purpose: + +- open shell inside container + +Flags: + +- `-i` → interactive +- `-t` → terminal + +--- + +# Port Mapping + +```docker id="d118" +-p 8080:80 +``` + +Meaning: + +- localhost port 8080 +- forwards traffic to container port 80 + +Flow: + +```text id="d119" +Browser → Host Port → Container Port +``` + +--- + +# Docker Volumes + +Problem: +Container data deletes when container removed. + +Solution: +Volumes. + +--- + +# Volume Example + +```docker id="d120" +docker volume create my-volume +``` + +Purpose: + +- persistent storage + +Used for: + +- databases +- logs +- application data + +--- + +# Bind Mount + +```docker id="d121" +-v $(pwd):/app +``` + +Purpose: + +- connects local folder to container folder + +Changes sync instantly. + +Mostly used in development. + +--- + +# Docker Networks + +Purpose: + +- container communication + +Without network: +containers isolated. + +--- + +# Custom Network + +```docker id="d122" +docker network create my-network +``` + +Containers communicate using: + +- service names +- container names + +Example: + +```text id="d123" +db +``` + +instead of localhost. + +--- + +# Docker Compose + +Purpose: +Run multiple containers together. + +Example: + +- app +- database +- redis + +all inside one file. + +--- + +# docker-compose.yml + +Written in YAML format. + +Example: + +```yaml id="d124" +services: + app: + db: +``` + +--- + +# Docker Compose Up + +```docker id="d125" +docker compose up +``` + +Internally: + +1. reads compose file +2. creates network +3. creates volumes +4. builds images +5. starts containers + +--- + +# Docker Compose Down + +```docker id="d126" +docker compose down +``` + +Stops and removes: + +- containers +- networks + +--- + +# Docker Compose Down -v + +```docker id="d127" +docker compose down -v +``` + +Also removes volumes. + +Database data deleted. + +--- + +# Environment Variables + +Stored in: + +```text id="d128" +.env +``` + +Purpose: + +- separate config from code +- secure configuration + +Example: + +- passwords +- database names +- ports + +--- + +# Healthcheck + +Purpose: +checks if container healthy. + +Example: +database ready before app starts. + +--- + +# depends_on + +Controls startup order. + +Example: +app waits for database. + +--- + +# Multi-Stage Build + +Purpose: +reduce image size. + +Example flow: + +```text id="d129" +Builder Stage → Runtime Stage +``` + +Internally: + +- build tools removed +- only final application copied + +Benefits: + +- smaller image +- secure image +- faster deployment + +--- + +# Docker Hub + +Purpose: +store and share images. + +Like GitHub for Docker images. + +--- + +# Tag Image + +```docker id="d130" +docker tag my-app username/my-app:v1 +``` + +Purpose: +gives proper repository name. + +--- + +# Push Image + +```docker id="d131" +docker push username/my-app:v1 +``` + +Uploads image to Docker Hub. + +--- + +# Pull Image + +```docker id="d132" +docker pull nginx +``` + +Downloads image locally. + +--- + +# Cleanup Commands + +## Remove Unused Images + +```docker id="d133" +docker image prune -a +``` + +--- + +## Full Cleanup + +```docker id="d134" +docker system prune -a +``` + +Deletes: + +- unused images +- stopped containers +- unused networks +- cache + +--- + +# Check Docker Storage + +```docker id="d135" +docker system df +``` + +Shows: + +- image usage +- container usage +- cache usage + +--- + +# Why Docker is Important in DevOps + +Docker helps: + +- consistency +- fast deployment +- scalability +- microservices +- CI/CD pipelines +- cloud deployments + +Used with: + +- Kubernetes +- Jenkins +- GitHub Actions +- AWS +- Azure + +--- + +# Final Revision Summary + +Today I revised: + +- Docker architecture +- Images +- Containers +- Dockerfile +- Layers +- Cache +- Networks +- Volumes +- Compose +- Multi-stage builds +- Docker Hub +- Cleanup +- Healthchecks + +--- + +# Conclusion + +Docker simplifies application deployment by packaging everything into containers. + +It helps developers and DevOps engineers build portable, lightweight, and consistent applications across environments. diff --git a/2026/day-37/docker-cheatsheet.md b/2026/day-37/docker-cheatsheet.md new file mode 100644 index 0000000000..d1f422ea97 --- /dev/null +++ b/2026/day-37/docker-cheatsheet.md @@ -0,0 +1,487 @@ +# Docker Cheat Sheet + +# Run Containers + +## Run Container + +```docker +docker run nginx +``` + +Run container. + +--- + +## Run in Background + +```docker +docker run -d nginx +``` + +Run in detached mode. + +--- + +## Interactive Mode + +```docker +docker run -it ubuntu bash +``` + +Open terminal inside container. + +--- + +# Container Commands + +## Running Containers + +```docker +docker ps +``` + +Show running containers. + +--- + +## All Containers + +```docker +docker ps -a +``` + +Show all containers. + +--- + +## Stop Container + +```docker +docker stop container_name +``` + +Stop container. + +--- + +## Start Container + +```docker +docker start container_name +``` + +Start container. + +--- + +## Remove Container + +```docker +docker rm container_name +``` + +Delete container. + +--- + +## Container Logs + +```docker +docker logs container_name +``` + +Show logs. + +--- + +## Access Container + +```docker +docker exec -it container_name bash +``` + +Open shell inside container. + +--- + +# Image Commands + +## Build Image + +```docker +docker build -t my-app . +``` + +Build image. + +--- + +## List Images + +```docker +docker images +``` + +Show images. + +--- + +## Pull Image + +```docker +docker pull nginx +``` + +Download image. + +--- + +## Push Image + +```docker +docker push username/image:v1 +``` + +Push image to Docker Hub. + +--- + +## Tag Image + +```docker +docker tag my-app username/my-app:v1 +``` + +Create image tag. + +--- + +## Remove Image + +```docker +docker rmi image_name +``` + +Delete image. + +--- + +# Volume Commands + +## Create Volume + +```docker +docker volume create my-volume +``` + +Create volume. + +--- + +## List Volumes + +```docker +docker volume ls +``` + +Show volumes. + +--- + +## Inspect Volume + +```docker +docker volume inspect my-volume +``` + +Check volume details. + +--- + +## Remove Volume + +```docker +docker volume rm my-volume +``` + +Delete volume. + +--- + +# Network Commands + +## Create Network + +```docker +docker network create my-network +``` + +Create custom network. + +--- + +## List Networks + +```docker +docker network ls +``` + +Show networks. + +--- + +## Inspect Network + +```docker +docker network inspect my-network +``` + +Check network details. + +--- + +# Docker Compose Commands + +## Start Services + +```docker +docker compose up +``` + +Start services. + +--- + +## Build and Start + +```docker +docker compose up --build +``` + +Build and start containers. + +--- + +## Background Mode + +```docker +docker compose up -d +``` + +Run in background. + +--- + +## Stop Services + +```docker +docker compose down +``` + +Stop services. + +--- + +## Remove Volumes Also + +```docker +docker compose down -v +``` + +Stop services and delete volumes. + +--- + +## Compose Logs + +```docker +docker compose logs +``` + +Show compose logs. + +--- + +## Running Compose Containers + +```docker +docker compose ps +``` + +Show compose containers. + +--- + +# Cleanup Commands + +## Remove Unused Images + +```docker +docker image prune -a +``` + +Delete unused images. + +--- + +## Full Cleanup + +```docker +docker system prune -a +``` + +Delete unused Docker data. + +--- + +## Check Docker Storage + +```docker +docker system df +``` + +Check Docker disk usage. + +--- + +# Dockerfile Instructions + +## FROM + +```docker +FROM python:3.12-slim +``` + +Base image. + +--- + +## WORKDIR + +```docker +WORKDIR /app +``` + +Working directory. + +--- + +## COPY + +```docker +COPY . . +``` + +Copy files. + +--- + +## RUN + +```docker +RUN pip install flask +``` + +Run commands during build. + +--- + +## EXPOSE + +```docker +EXPOSE 5000 +``` + +Open port. + +--- + +## CMD + +```docker +CMD ["python", "app.py"] +``` + +Default container command. + +--- + +## ENTRYPOINT + +```docker +ENTRYPOINT ["python"] +``` + +Fixed startup command. + +--- + +# Important Concepts + +## Image + +Blueprint/template. + +--- + +## Container + +Running instance of image. + +--- + +## Volume + +Persistent storage. + +--- + +## Network + +Allows containers to communicate. + +--- + +## Multi-Stage Build + +Reduces image size by removing unnecessary build tools. + +--- + +## .env File + +Stores environment variables. + +--- + +## Healthcheck + +Checks if container is healthy. + +--- + +# Useful Commands + +## Docker Version + +```docker +docker version +``` + +--- + +## Docker Information + +```docker +docker info +``` + +--- + +## Resource Usage + +```docker +docker stats +``` + +Check CPU and memory usage. diff --git a/2026/day-38/day-38-yaml.md b/2026/day-38/day-38-yaml.md new file mode 100644 index 0000000000..ab6c6c7ef6 --- /dev/null +++ b/2026/day-38/day-38-yaml.md @@ -0,0 +1,127 @@ +# Day 38 – YAML Basics + +## What is YAML? + +YAML is a configuration language used in: + +- GitHub Actions +- Kubernetes +- Docker Compose +- Ansible + +--- + +# Basic YAML Syntax + +name: Asma +role: DevOps Engineer +learning: true + +Format: + +key: value + +--- + +# Lists in YAML + +Block List: + +tools: + +- Docker +- Kubernetes +- GitHub Actions + +Inline List: + +hobbies: [reading, cooking, learning] + +--- + +# Nested Objects + +database: +host: localhost +user: admin + +Indentation creates hierarchy. + +--- + +# Multi-line Strings + +Pipe | + +Keeps line breaks. + +Use for: + +- scripts +- commands + +Fold > + +Converts text into one paragraph. + +Use for: + +- descriptions +- messages + +--- + +# Important YAML Rules + +- YAML uses spaces only +- Never use tabs +- Indentation is very important +- Two spaces are standard + +--- + +# YAML Validation + +Install yamllint: + +pip install yamllint + +Validate files: + +yamllint server.yml + +yamllint person.yml + +--- + +# Common Errors + +Missing document start: + +Add this at top: + +--- + +Trailing spaces: + +Remove extra spaces at end of line. + +--- + +# What I Learned + +1. YAML depends on spaces and indentation. +2. One wrong space can break the file. +3. YAML is used everywhere in DevOps. + +--- + +# Commands Used + +cat person.yml + +cat server.yml + +yamllint person.yml + +yamllint server.yml diff --git a/2026/day-38/person.yml b/2026/day-38/person.yml new file mode 100644 index 0000000000..a7518af880 --- /dev/null +++ b/2026/day-38/person.yml @@ -0,0 +1,14 @@ +name: Asma Saiyed +role: DevOps Engineer +Experience_year: 4 +learning: true + +tools: + - Docker + - Kuberenetes + - GitHub Actions + - Ansible + - GitOps + + +hobbies:[reading, cooking, learning, gardening] diff --git a/2026/day-38/server.yml b/2026/day-38/server.yml new file mode 100644 index 0000000000..b247735b3d --- /dev/null +++ b/2026/day-38/server.yml @@ -0,0 +1,22 @@ +--- +server: + name: dev-server + ip: 192.168.1.10 + port: 8080 + +database: + host: localhost + name: app_db + credentials: + user: admin + password: "admin123" + +startup_script_pipe: | + echo "Starting server..." + docker compose up -d + echo "Server started successfully." + +startup_script_fold: > + echo "Starting server..." + docker compose up -d + echo "Server started successfully." diff --git a/2026/day-39/day-39-cicd-concepts.md b/2026/day-39/day-39-cicd-concepts.md new file mode 100644 index 0000000000..c747d94d5c --- /dev/null +++ b/2026/day-39/day-39-cicd-concepts.md @@ -0,0 +1,325 @@ +# Day 39 – What is CI/CD? + +Today I learned basic CI/CD concepts before creating actual pipelines. + +CI/CD helps automate software delivery and reduce manual deployment problems. + +--- + +# Problems Without CI/CD + +If many developers deploy code manually: + +- code conflicts happen +- production can break +- deployment becomes slow +- human mistakes happen +- troubleshooting becomes difficult + +Manual deployment is risky because every developer may have different environment or process. + +--- + +# “It Works on My Machine” + +This means application works on developer machine but fails on another machine or server. + +Reasons: + +- different software versions +- missing dependencies +- different environment variables +- different OS configuration + +Example: + +Application works on local laptop but fails on production server because Python or Java version is different. + +CI/CD helps maintain same process and consistency everywhere. + +--- + +# Continuous Integration (CI) + +Continuous Integration means developers frequently push code to shared repository. + +When developer pushes code: + +- pipeline automatically starts +- application builds automatically +- automated tests run +- bugs are detected early +- broken code is stopped before deployment + +CI mainly focuses on integrating code changes safely. + +Example: +Developer pushes code to GitHub and GitHub Actions automatically runs build and tests. + +--- + +# What Happens Internally During CI? + +Developer pushes code to GitHub +↓ +GitHub detects new code change +↓ +Workflow YAML file is read +↓ +Runner machine starts automatically +↓ +Project source code is downloaded on runner +↓ +Dependencies are installed +↓ +Build process starts +↓ +Automated tests run +↓ +Linting and code quality checks run +↓ +If everything passes, pipeline becomes successful +↓ +Artifact or Docker image gets created + +If test fails: + +- pipeline stops +- deployment does not happen +- developer fixes issue and pushes again + +This helps catch problems early before production. + +--- + +# Continuous Delivery + +Continuous Delivery means application is always ready for deployment. + +After successful testing: + +- build is packaged +- release is prepared +- deployment needs manual approval + +Main focus: +Application should always stay deployable. + +Example: +Manager or DevOps engineer manually approves production deployment. + +--- + +# Continuous Deployment + +Continuous Deployment automatically deploys application after all tests pass. + +No manual approval is needed. + +Internally: + +- tests pass +- Docker image builds +- deployment automatically starts +- new version goes live automatically + +Example: +Netflix automatically deploys updates after successful testing. + +--- + +# Pipeline Anatomy + +## Trigger + +Starts pipeline automatically. + +Examples: + +- git push +- pull request +- schedule +- manual workflow dispatch + +--- + +## Stage + +Logical phase of pipeline. + +Examples: + +- build +- test +- deploy + +Stages help organize workflow properly. + +--- + +## Job + +A task inside stage. + +Example: + +- run tests +- build Docker image +- scan security vulnerabilities + +Jobs can run sequentially or parallel. + +--- + +## Step + +Single command or action inside job. + +Examples: + +- checkout code +- install dependencies +- run docker build + +Steps are smallest execution unit. + +--- + +## Runner + +Machine that executes pipeline jobs. + +Examples: + +- GitHub-hosted runner +- self-hosted runner +- Jenkins server + +Runner provides CPU, memory, operating system, and environment to execute jobs. + +--- + +## Artifact + +Output generated from pipeline. + +Examples: + +- Docker image +- build package +- compiled application +- test reports + +Artifacts can be reused in later stages. + +--- + +# Simple CI/CD Pipeline Flow + +Developer Writes Code +↓ +Pushes Code to GitHub +↓ +GitHub Detects Change +↓ +Pipeline Starts Automatically +↓ +Runner Machine Starts +↓ +Dependencies Install +↓ +Application Builds +↓ +Tests Execute +↓ +Docker Image Builds +↓ +Artifact Stores +↓ +Deploy to Staging Server +↓ +Deployment Logs Generated + +--- + +# Open Source Workflow Research + +Repository: +FastAPI + +Workflow Folder: +.github/workflows/ + +What triggers workflow? + +- push +- pull_request + +What does workflow do? + +- runs automated tests +- validates code +- checks code quality +- verifies application build + +--- + +# Important Interview Concepts + +## Why CI/CD is important? + +CI/CD improves: + +- automation +- reliability +- consistency +- deployment speed +- early bug detection + +--- + +## Why pipeline failure is good? + +Pipeline failure prevents broken code from reaching production. + +It helps developers fix issues early. + +--- + +## Difference Between CI and CD + +CI: +Focuses on build and testing. + +CD: +Focuses on deployment and release automation. + +--- + +## Why Automation Matters? + +Without automation: + +- deployments become slow +- mistakes increase +- troubleshooting becomes difficult + +Automation reduces manual effort and improves stability. + +--- + +# What I Learned + +1. CI/CD automates software delivery process. +2. Pipeline internally performs many automated tasks. +3. CI catches problems before deployment. +4. Automation improves deployment reliability. +5. YAML workflows control CI/CD pipelines. + +--- + +# Important Note + +Pipeline failure is not bad. + +It means CI/CD detected issue before production deployment and protected production environment. diff --git a/2026/day-40/Asma-motivation-Tracker/.github/workflows/cicd.yml b/2026/day-40/Asma-motivation-Tracker/.github/workflows/cicd.yml new file mode 100644 index 0000000000..155944b4bd --- /dev/null +++ b/2026/day-40/Asma-motivation-Tracker/.github/workflows/cicd.yml @@ -0,0 +1,21 @@ +name: Motivation Tracker CI + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Build Docker Image + run: docker build -t motivation-tracker . + + - name: Confirm Build Success + run: echo "Docker image built successfully" diff --git a/2026/day-40/Asma-motivation-Tracker/Dockerfile b/2026/day-40/Asma-motivation-Tracker/Dockerfile new file mode 100644 index 0000000000..6eeb61076f --- /dev/null +++ b/2026/day-40/Asma-motivation-Tracker/Dockerfile @@ -0,0 +1,13 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 5000 + +CMD ["python", "app.py"] \ No newline at end of file diff --git a/2026/day-40/Asma-motivation-Tracker/app.py b/2026/day-40/Asma-motivation-Tracker/app.py new file mode 100644 index 0000000000..9aa367613c --- /dev/null +++ b/2026/day-40/Asma-motivation-Tracker/app.py @@ -0,0 +1,83 @@ +from flask import Flask, render_template, request, redirect +from database import db +from models import Goal +import os +import random + +app = Flask(__name__) + +DATABASE_URL = os.getenv( + "DATABASE_URL", + "postgresql://postgres:postgres@localhost:5432/motivation_db" +) + +app.config["SQLALCHEMY_DATABASE_URI"] = DATABASE_URL +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + +db.init_app(app) + +quotes = [ + "Small steps every day create big results.", + "Discipline today becomes freedom tomorrow.", + "You are building your future one commit at a time.", + "Do not stop until you are proud.", + "Every failure is feedback. Keep going." +] + + +@app.route("/") +def home(): + goals = Goal.query.order_by(Goal.created_at.desc()).all() + quote = random.choice(quotes) + return render_template("index.html", goals=goals, quote=quote) + + +@app.route("/add", methods=["POST"]) +def add_goal(): + title = request.form.get("title") + description = request.form.get("description") + target_date = request.form.get("target_date") + + new_goal = Goal( + title=title, + description=description, + target_date=target_date + ) + + db.session.add(new_goal) + db.session.commit() + + return redirect("/") + + +@app.route("/update/", methods=["POST"]) +def update_goal(goal_id): + goal = Goal.query.get_or_404(goal_id) + goal.progress = int(request.form.get("progress")) + db.session.commit() + return redirect("/") + + +@app.route("/delete/") +def delete_goal(goal_id): + goal = Goal.query.get_or_404(goal_id) + db.session.delete(goal) + db.session.commit() + return redirect("/") + + +@app.route("/health") +def health(): + return { + "status": "healthy", + "app": "Asma Motivation Tracker", + "database": "connected" + } + + +with app.app_context(): + db.create_all() + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) \ No newline at end of file diff --git a/2026/day-40/Asma-motivation-Tracker/database.py b/2026/day-40/Asma-motivation-Tracker/database.py new file mode 100644 index 0000000000..2e1eeb63ff --- /dev/null +++ b/2026/day-40/Asma-motivation-Tracker/database.py @@ -0,0 +1,3 @@ +from flask_sqlalchemy import SQLAlchemy + +db = SQLAlchemy() \ No newline at end of file diff --git a/2026/day-40/Asma-motivation-Tracker/docker-compose.yml b/2026/day-40/Asma-motivation-Tracker/docker-compose.yml new file mode 100644 index 0000000000..75a53122ca --- /dev/null +++ b/2026/day-40/Asma-motivation-Tracker/docker-compose.yml @@ -0,0 +1,25 @@ +services: + web: + build: . + container_name: motivation_app + ports: + - "5000:5000" + environment: + DATABASE_URL: postgresql://postgres:postgres@db:5432/motivation_db + depends_on: + - db + + db: + image: postgres:16 + container_name: motivation_db + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: motivation_db + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + +volumes: + postgres_data: \ No newline at end of file diff --git a/2026/day-40/Asma-motivation-Tracker/models.py b/2026/day-40/Asma-motivation-Tracker/models.py new file mode 100644 index 0000000000..ecbd780420 --- /dev/null +++ b/2026/day-40/Asma-motivation-Tracker/models.py @@ -0,0 +1,11 @@ +from database import db +from datetime import datetime + + +class Goal(db.Model): + id = db.Column(db.Integer, primary_key=True) + title = db.Column(db.String(150), nullable=False) + description = db.Column(db.Text, nullable=True) + target_date = db.Column(db.String(50), nullable=False) + progress = db.Column(db.Integer, default=0) + created_at = db.Column(db.DateTime, default=datetime.utcnow) \ No newline at end of file diff --git a/2026/day-40/Asma-motivation-Tracker/requirements.txt b/2026/day-40/Asma-motivation-Tracker/requirements.txt new file mode 100644 index 0000000000..36484041dc --- /dev/null +++ b/2026/day-40/Asma-motivation-Tracker/requirements.txt @@ -0,0 +1,4 @@ +Flask==3.0.3 +Flask-SQLAlchemy==3.1.1 +psycopg2-binary==2.9.9 +python-dotenv==1.0.1 \ No newline at end of file diff --git a/2026/day-40/Asma-motivation-Tracker/static/script.js b/2026/day-40/Asma-motivation-Tracker/static/script.js new file mode 100644 index 0000000000..66a8f23db5 --- /dev/null +++ b/2026/day-40/Asma-motivation-Tracker/static/script.js @@ -0,0 +1,50 @@ +console.log("Asma Motivation Tracker Loaded 🚀"); + +document.addEventListener("DOMContentLoaded", () => { + + const quoteBox = document.querySelector(".quote"); + + const extraQuotes = [ + "Success is built one day at a time.", + "Consistency beats motivation.", + "Future DevOps Engineer Loading...", + "Small progress is still progress.", + "Stay disciplined and trust the process." + ]; + + // Change motivational quote every 5 seconds + setInterval(() => { + const randomQuote = + extraQuotes[Math.floor(Math.random() * extraQuotes.length)]; + + if (quoteBox) { + quoteBox.innerText = `"${randomQuote}"`; + } + }, 5000); + + + // Progress color logic + const progressInputs = document.querySelectorAll( + 'input[name="progress"]' + ); + + progressInputs.forEach((input) => { + + input.addEventListener("input", () => { + + const value = parseInt(input.value); + + if (value >= 80) { + input.style.border = "2px solid green"; + } + else if (value >= 40) { + input.style.border = "2px solid orange"; + } + else { + input.style.border = "2px solid red"; + } + }); + + }); + +}); \ No newline at end of file diff --git a/2026/day-40/Asma-motivation-Tracker/static/style.css b/2026/day-40/Asma-motivation-Tracker/static/style.css new file mode 100644 index 0000000000..c56a16c38e --- /dev/null +++ b/2026/day-40/Asma-motivation-Tracker/static/style.css @@ -0,0 +1,147 @@ +* { + box-sizing: border-box; +} + +body { + font-family: Arial, sans-serif; + background: linear-gradient(135deg, #020617, #0f172a, #111827); + color: #e5e7eb; + margin: 0; + padding: 0; +} + +.container { + width: 86%; + max-width: 1200px; + margin: 45px auto; +} + +h1 { + text-align: center; + font-size: 42px; + background: linear-gradient(to right, #3b82f6, #a855f7); + -webkit-background-clip: text; + color: transparent; +} + +.quote { + text-align: center; + font-size: 20px; + color: #fde68a; + background: rgba(255, 255, 255, 0.08); + padding: 20px; + border: 1px solid rgba(253, 230, 138, 0.4); + border-radius: 12px; +} + +.card, +.goal-card { + background: rgba(15, 23, 42, 0.92); + padding: 28px; + margin: 25px 0; + border-radius: 16px; + border: 1px solid rgba(148, 163, 184, 0.25); + box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35); +} + +input, +textarea { + width: 100%; + padding: 14px; + margin: 10px 0; + border: 1px solid #334155; + border-radius: 10px; + background: #020617; + color: #e5e7eb; + font-size: 15px; +} + +input::placeholder, +textarea::placeholder { + color: #94a3b8; +} + +button { + background: linear-gradient(to right, #2563eb, #7c3aed); + color: white; + border: none; + padding: 13px 22px; + border-radius: 10px; + cursor: pointer; + font-size: 15px; +} + +button:hover { + opacity: 0.9; +} + +.section-title { + margin-top: 35px; + font-size: 30px; +} + +.goal-card { + display: flex; + align-items: center; + justify-content: space-between; + gap: 25px; +} + +.goal-content { + flex: 1; +} + +.goal-card h3 { + font-size: 24px; + margin-bottom: 8px; +} + +.date { + color: #22c55e; + font-weight: bold; +} + +.progress-container { + width: 100%; + background: #334155; + border-radius: 20px; + overflow: hidden; + margin-top: 12px; +} + +.progress-bar { + background: linear-gradient(to right, #2563eb, #22c55e); + color: white; + padding: 7px; + text-align: center; + font-weight: bold; +} + +.progress-form { + display: flex; + align-items: center; + gap: 10px; +} + +.progress-form input { + width: 90px; +} + +.delete-btn { + color: #f87171; + border: 1px solid #ef4444; + padding: 12px 18px; + border-radius: 10px; + text-decoration: none; +} + +.delete-btn:hover { + background: #7f1d1d; + color: white; +} + +footer { + text-align: center; + color: #94a3b8; + padding: 25px; +} \ No newline at end of file diff --git a/2026/day-40/Asma-motivation-Tracker/templates/index.html b/2026/day-40/Asma-motivation-Tracker/templates/index.html new file mode 100644 index 0000000000..6822b8f1a1 --- /dev/null +++ b/2026/day-40/Asma-motivation-Tracker/templates/index.html @@ -0,0 +1,62 @@ + + + + Asma Motivation Tracker + + + + + +
+

Asma Motivation Tracker 🚀

+ +

“{{ quote }}”

+ +
+

🎯 Create New Goal

+ + + + + + + + +
+ +

📋 My Goals

+ + {% for goal in goals %} +
+
+

{{ goal.title }}

+

{{ goal.description }}

+ +

+ Target Date: {{ goal.target_date }} +

+ +
+
+ {{ goal.progress }}% +
+
+
+ +
+ + % + +
+ + Delete +
+ {% endfor %} +
+ +
+ © 2026 Asma Motivation Tracker. Keep pushing forward! 💪 +
+ + + \ No newline at end of file diff --git a/2026/day-40/Screenshot 2026-06-25 124646.png b/2026/day-40/Screenshot 2026-06-25 124646.png new file mode 100644 index 0000000000..023c671420 Binary files /dev/null and b/2026/day-40/Screenshot 2026-06-25 124646.png differ diff --git a/2026/day-40/Screenshot 2026-06-25 124838.png b/2026/day-40/Screenshot 2026-06-25 124838.png new file mode 100644 index 0000000000..b899fc3f8f Binary files /dev/null and b/2026/day-40/Screenshot 2026-06-25 124838.png differ diff --git a/2026/day-40/Screenshot 2026-06-25 124846.png b/2026/day-40/Screenshot 2026-06-25 124846.png new file mode 100644 index 0000000000..0d1adf8b07 Binary files /dev/null and b/2026/day-40/Screenshot 2026-06-25 124846.png differ diff --git a/2026/day-40/Screenshot 2026-06-25 124900.png b/2026/day-40/Screenshot 2026-06-25 124900.png new file mode 100644 index 0000000000..bc0511fb7c Binary files /dev/null and b/2026/day-40/Screenshot 2026-06-25 124900.png differ diff --git a/2026/day-40/Screenshot 2026-06-29 112543.png b/2026/day-40/Screenshot 2026-06-29 112543.png new file mode 100644 index 0000000000..e48c98fbfc Binary files /dev/null and b/2026/day-40/Screenshot 2026-06-29 112543.png differ diff --git a/2026/day-40/Screenshot 2026-06-29 112644.png b/2026/day-40/Screenshot 2026-06-29 112644.png new file mode 100644 index 0000000000..0ce74ee950 Binary files /dev/null and b/2026/day-40/Screenshot 2026-06-29 112644.png differ diff --git a/2026/day-40/day-40-first-workflow.md b/2026/day-40/day-40-first-workflow.md new file mode 100644 index 0000000000..55afcfe6fd --- /dev/null +++ b/2026/day-40/day-40-first-workflow.md @@ -0,0 +1,297 @@ +# Day 40 – My First GitHub Actions Workflow + +Today I created my first GitHub Actions workflow and watched the pipeline run successfully in the cloud. + +This was my first real CI/CD automation workflow using GitHub Actions. + +--- + +# Repository Setup + +I created a public repository: + +github-actions-practice + +Then I created the workflow folder: + +.github/workflows/ + +Inside this folder I created: + +hello.yml + +--- + +# My First Workflow YAML + +name: My First Workflow + +on: +push: + +jobs: +greet: +runs-on: ubuntu-latest + +``` +steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Print Hello Message + run: echo "Hello from GitHub Actions!" +``` + +--- + +# What Happens Internally? + +When I push code to GitHub: + +1. GitHub detects code change +2. GitHub reads workflow YAML file +3. Runner machine starts automatically +4. Repository code downloads on runner +5. Steps execute one by one +6. Logs generate in Actions tab +7. Workflow status becomes success or failed + +Everything runs automatically in cloud environment. + +--- + +# Understanding Workflow Anatomy + +## name: + +Defines workflow name shown in Actions tab. + +Example: + +name: My First Workflow + +--- + +## on: + +Defines trigger event. + +Example: + +on: +push: + +This means workflow runs whenever code is pushed. + +--- + +## jobs: + +Contains all jobs inside workflow. + +Example: + +jobs: +greet: + +A workflow can contain multiple jobs. + +--- + +## runs-on: + +Defines operating system for runner machine. + +Example: + +runs-on: ubuntu-latest + +GitHub creates Ubuntu virtual machine to run workflow. + +--- + +## steps: + +Defines sequence of actions executed inside job. + +Example: + +steps: + +Steps execute one by one. + +--- + +## uses: + +Uses prebuilt GitHub Action. + +Example: + +uses: actions/checkout@v4 + +This downloads repository code to runner. + +--- + +## run: + +Executes shell command. + +Example: + +run: echo "Hello" + +Used to run Linux commands or scripts. + +--- + +## Step name + +Helps identify step inside Actions logs. + +Example: + +- name: Print Hello Message + +Makes workflow easier to read. + +--- + +# Additional Workflow Steps + +I later added more steps: + +- print current date and time +- print branch name +- list repository files +- print runner operating system + +This helped me understand runner environment better. + +--- + +# Example Additional Commands + +Print date: + +date + +Print branch name: + +echo ${{ github.ref_name }} + +List files: + +ls -la + +Print operating system: + +uname -a + +--- + +# What is Runner? + +Runner is virtual machine created by GitHub to execute workflow. + +Runner: + +- downloads repository +- installs dependencies +- runs commands +- executes build and tests + +After workflow completes, runner is destroyed automatically. + +--- + +# What is actions/checkout? + +actions/checkout downloads repository code onto runner machine. + +Without checkout: + +- runner cannot access project files + +This is usually first step in most workflows. + +--- + +# Failed Pipeline Understanding + +I intentionally broke workflow using wrong command. + +Example: + +run: wrongcommand + +Pipeline became red and failed. + +I checked: + +- error logs +- failed step +- command output + +Then I fixed issue and pushed again. + +--- + +# What I Learned From Failed Pipeline + +Failed pipeline is useful because: + +- it catches problems early +- prevents broken deployment +- helps debugging + +Pipeline failure is part of CI/CD process. + +--- + +# What I Learned + +1. GitHub Actions automates workflows. +2. GitHub automatically creates runner machine. +3. Workflow steps execute sequentially. +4. Logs help debug pipeline issues. +5. CI/CD becomes real after watching pipeline run. + +--- + +# Important Interview Concepts + +## What happens after git push? + +- GitHub detects event +- workflow starts +- runner machine launches +- repository downloads +- commands execute +- logs generate +- pipeline succeeds or fails + +--- + +## Why GitHub Actions is useful? + +- automates CI/CD +- reduces manual work +- improves consistency +- catches bugs early +- speeds up deployment process + +--- + +# My Understanding + +Today was important because CI/CD became practical instead of only theory. + +Watching first green pipeline run helped me understand: + +- automation +- workflow execution +- runner concept +- pipeline logs +- CI/CD process internally diff --git a/2026/day-41/Screenshot 2026-07-06 130548.png b/2026/day-41/Screenshot 2026-07-06 130548.png new file mode 100644 index 0000000000..9ae2e18167 Binary files /dev/null and b/2026/day-41/Screenshot 2026-07-06 130548.png differ diff --git a/2026/day-41/Screenshot 2026-07-06 131325.png b/2026/day-41/Screenshot 2026-07-06 131325.png new file mode 100644 index 0000000000..1b4f5ee95e Binary files /dev/null and b/2026/day-41/Screenshot 2026-07-06 131325.png differ diff --git a/2026/day-41/Screenshot 2026-07-06 131333.png b/2026/day-41/Screenshot 2026-07-06 131333.png new file mode 100644 index 0000000000..96a0089969 Binary files /dev/null and b/2026/day-41/Screenshot 2026-07-06 131333.png differ diff --git a/2026/day-41/Screenshot 2026-07-06 131358.png b/2026/day-41/Screenshot 2026-07-06 131358.png new file mode 100644 index 0000000000..9c7388e5c7 Binary files /dev/null and b/2026/day-41/Screenshot 2026-07-06 131358.png differ diff --git a/2026/day-41/day-41-triggers.md b/2026/day-41/day-41-triggers.md new file mode 100644 index 0000000000..6cb9e4256a --- /dev/null +++ b/2026/day-41/day-41-triggers.md @@ -0,0 +1,60 @@ +# Day 41 – Triggers & Matrix Builds + +## What I learned today + +Today I learned different ways to start a GitHub Actions workflow, and how to run one job many times at once using a matrix. + +--- + +## Task 1: Pull Request trigger + +I made a file called pr-check.yml. It runs only when someone opens or updates a Pull Request into my branch. It is not run on a normal push. + +What happened: I opened a Pull Request, and this workflow ran by itself. It showed up as a check on the PR page. + +## Task 2: Schedule trigger + +I added a schedule trigger to a workflow. It runs every day at midnight UTC, without me clicking anything. + +Question: What is the cron code for "every Monday at 9 AM"? + +My answer: 0 9 \* \* 1 + +## Task 3: Manual trigger + +I made a file called manual.yml. It does not run by itself. I go to the Actions tab and click "Run workflow" to start it. It also asks me to type an environment name before running. + +What happened: I clicked "Run workflow," typed an environment name, and the logs printed exactly what I typed. + +## Task 4: Matrix Build + +I made a file called matrix.yml. Instead of writing many jobs by hand, I used a matrix. GitHub made one job automatically for every Python version I listed: 3.10, 3.11, and 3.12. + +What happened: 3 jobs ran at the same time, one for each Python version. + +Then I added 2 operating systems to the matrix as well. + +Question: How many total jobs run now? + +My answer: 6 jobs (3 Python versions times 2 operating systems) + +## Task 5: Exclude and fail-fast + +I removed one combination I did not want to test: Python 3.10 on Windows. Now only 5 jobs run instead of 6. + +I also turned off fail-fast, so all jobs keep running even if one of them fails. + +Question: What does fail-fast true do, compared to fail-fast false? + +My answer: fail-fast true is the normal default. If one job fails, GitHub stops all the other jobs right away, even ones still working fine. fail-fast false lets every job finish on its own, no matter what happens to the others. This way I can see full results from every combination. + +## Summary in my own simple words + +- Pull Request trigger: runs only when a PR is opened or changed. +- Schedule trigger: runs by itself at a fixed time, every day or week. +- Manual trigger: runs only when I click a button, and can ask me for input first. +- Matrix build: one job definition that automatically becomes many jobs, one for each combination I list. +- Exclude: removes one unwanted combination from the matrix. +- fail-fast false: lets every job finish, even if one of them fails. + +#90DaysOfDevOps #DevOpsKaJosh #TrainWithShubham diff --git a/2026/day-42/Screenshot 2026-07-06 134557.png b/2026/day-42/Screenshot 2026-07-06 134557.png new file mode 100644 index 0000000000..4e962b53a7 Binary files /dev/null and b/2026/day-42/Screenshot 2026-07-06 134557.png differ diff --git a/2026/day-42/Screenshot 2026-07-06 135614.png b/2026/day-42/Screenshot 2026-07-06 135614.png new file mode 100644 index 0000000000..69a3cec017 Binary files /dev/null and b/2026/day-42/Screenshot 2026-07-06 135614.png differ diff --git a/2026/day-42/Screenshot 2026-07-06 140813.png b/2026/day-42/Screenshot 2026-07-06 140813.png new file mode 100644 index 0000000000..612491d867 Binary files /dev/null and b/2026/day-42/Screenshot 2026-07-06 140813.png differ diff --git a/2026/day-42/Screenshot 2026-07-06 140856.png b/2026/day-42/Screenshot 2026-07-06 140856.png new file mode 100644 index 0000000000..8d34de49d2 Binary files /dev/null and b/2026/day-42/Screenshot 2026-07-06 140856.png differ diff --git a/2026/day-42/Screenshot 2026-07-06 143645.png b/2026/day-42/Screenshot 2026-07-06 143645.png new file mode 100644 index 0000000000..697b423b3d Binary files /dev/null and b/2026/day-42/Screenshot 2026-07-06 143645.png differ diff --git a/2026/day-42/Screenshot 2026-07-06 143828.png b/2026/day-42/Screenshot 2026-07-06 143828.png new file mode 100644 index 0000000000..8fef993c30 Binary files /dev/null and b/2026/day-42/Screenshot 2026-07-06 143828.png differ diff --git a/2026/day-42/Screenshot 2026-07-06 150331.png b/2026/day-42/Screenshot 2026-07-06 150331.png new file mode 100644 index 0000000000..11f61ca08e Binary files /dev/null and b/2026/day-42/Screenshot 2026-07-06 150331.png differ diff --git a/2026/day-42/day-42-runners.md b/2026/day-42/day-42-runners.md new file mode 100644 index 0000000000..aa6294717d --- /dev/null +++ b/2026/day-42/day-42-runners.md @@ -0,0 +1,73 @@ +# Day 42 – Runners: GitHub-Hosted and Self-Hosted + +## What I learned today + +Today I learned what a runner is. A runner is the machine that actually runs my workflow jobs. I learned about two kinds: GitHub-hosted runners, and my own self-hosted runner. + +--- + +## Task 1: GitHub-Hosted Runners + +I made one workflow with 3 jobs. Each job ran on a different operating system: ubuntu-latest, windows-latest, and macos-latest. Each job printed its OS name, its hostname, and the current user running it. + +What happened: all 3 jobs ran at the same time, in parallel, and each one printed its own details correctly. + +Question: What is a GitHub-hosted runner? Who manages it? + +My answer: A GitHub-hosted runner is a virtual machine that GitHub creates fresh for every workflow run, then deletes right after it finishes. GitHub manages it completely. I do not set it up, maintain it, or pay for any hardware. I just use it. + +## Task 2: What Comes Pre-installed + +On the ubuntu-latest runner, I added a step that checked the versions of Docker, Python, Node, and Git. + +What happened: all four tools were already installed, with no setup needed from me. It printed real version numbers for each one. + +Question: Why does it matter that runners come with tools pre-installed? + +My answer: It saves time. If runners came empty, every single workflow would need extra steps to install Docker, Python, Node, and Git before doing any real work. This would make every run slower and more repetitive. + +## Task 3: Setting Up My Own Self-Hosted Runner + +I went to my repo settings, then Actions, then Runners, and added a new self-hosted runner. I chose Linux since I am using WSL on my Windows machine. I downloaded the runner, configured it with my repo token, and started it. + +What happened: my runner showed up in the Runners list with a green dot, marked as Idle. This means it was online and ready to accept jobs. + +## Task 4: Using My Self-Hosted Runner + +I made a new file called self-hosted.yml. It uses runs-on self-hosted, so the job runs on my own machine instead of a GitHub cloud machine. The job printed the hostname, printed the working directory, created a small test file, and then showed the contents of that file. + +What happened: the job ran successfully, and I confirmed the test file was really created on my own machine, not somewhere in the cloud. + +## Task 5: Labels + +I added a custom label called my-linux-runner to my self-hosted runner. Then I updated my workflow to require both labels together: self-hosted and my-linux-runner. + +What happened: once my runner had that exact label, the job matched correctly and ran successfully on my machine again. + +Question: Why are labels useful when you have multiple self-hosted runners? + +My answer: Labels let me target one specific runner, or a specific group of runners, instead of just any random one. For example, if one runner has more memory, or a GPU, or a certain tool installed, labels let my workflow say "only run this job on the runner that has this label," instead of it landing on whichever machine happens to be free. + +--- + +## Task 6: GitHub-Hosted vs Self-Hosted + +| | GitHub-Hosted | Self-Hosted | +| ------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| Who manages it? | GitHub | Me | +| Cost | Free minutes included, then billed per minute | Free to use, but I pay for my own hardware and electricity | +| Pre-installed tools | Comes with Docker, Python, Node, Git already installed | Nothing pre-installed, I set up everything myself | +| Good for | Quick, simple jobs, and public open-source projects | Special hardware needs like a GPU, private networks, custom software | +| Security concern | Low, since GitHub gives a fresh, isolated machine every time | Higher, especially on public repos, since a bad Pull Request could run harmful code on my real machine | + +--- + +## Summary in my own simple words + +- GitHub-hosted runner: a temporary machine that GitHub creates and manages for me. +- Self-hosted runner: my own machine, registered to my repo, that I fully control. +- Pre-installed tools save time because I do not need to install them every run. +- Labels let me choose exactly which runner should pick up a job. +- Self-hosted runners give me more control, but also more responsibility for security. + +#90DaysOfDevOps #DevOpsKaJosh #TrainWithShubham diff --git a/2026/day-43/Screenshot 2026-07-07 105635.png b/2026/day-43/Screenshot 2026-07-07 105635.png new file mode 100644 index 0000000000..d6082c2496 Binary files /dev/null and b/2026/day-43/Screenshot 2026-07-07 105635.png differ diff --git a/2026/day-43/Screenshot 2026-07-07 110449.png b/2026/day-43/Screenshot 2026-07-07 110449.png new file mode 100644 index 0000000000..f0c350ecca Binary files /dev/null and b/2026/day-43/Screenshot 2026-07-07 110449.png differ diff --git a/2026/day-43/Screenshot 2026-07-07 110914.png b/2026/day-43/Screenshot 2026-07-07 110914.png new file mode 100644 index 0000000000..a31026b62d Binary files /dev/null and b/2026/day-43/Screenshot 2026-07-07 110914.png differ diff --git a/2026/day-43/Screenshot 2026-07-07 111322.png b/2026/day-43/Screenshot 2026-07-07 111322.png new file mode 100644 index 0000000000..bdf3127f57 Binary files /dev/null and b/2026/day-43/Screenshot 2026-07-07 111322.png differ diff --git a/2026/day-43/Screenshot 2026-07-07 111626.png b/2026/day-43/Screenshot 2026-07-07 111626.png new file mode 100644 index 0000000000..eb5376224d Binary files /dev/null and b/2026/day-43/Screenshot 2026-07-07 111626.png differ diff --git a/2026/day-43/day-43-jobs-steps.md b/2026/day-43/day-43-jobs-steps.md new file mode 100644 index 0000000000..c0ea450539 --- /dev/null +++ b/2026/day-43/day-43-jobs-steps.md @@ -0,0 +1,83 @@ +# Day 43 – Jobs, Steps, Env Vars and Conditionals + +## What I learned today + +Today I learned how to control the flow of my pipeline. I learned how to make jobs run in a certain order, how to use environment variables at different levels, how to pass data between jobs, and how to run steps only when certain conditions are true. + +--- + +## Task 1: Multi-Job Workflow + +I made a file called multi-job.yml with three jobs: build, test, and deploy. I used needs so that test only starts after build finishes successfully, and deploy only starts after test finishes successfully. + +What happened: the Actions graph showed a clean chain, build then test then deploy, connected one after another, all green. + +In my own words: needs tells a job to wait for another job to finish successfully before it starts. Without needs, all jobs would try to run at the same time. + +--- + +## Task 2: Environment Variables + +I used environment variables at three different levels in one workflow. + +Workflow level: APP_NAME, set once at the top, available to every job in the file. + +Job level: ENVIRONMENT, set inside one job, only available to that job. + +Step level: VERSION, set inside one step, only available to that step. + +I printed all three together in one step, and they all worked. I also printed two GitHub context variables: the commit SHA and the actor, which is the username of whoever triggered the run. + +What happened: all three variables printed correctly, along with the real commit SHA and my GitHub username. + +--- + +## Task 3: Job Outputs + +I made one job called set-date that creates a value, today's date, and saves it as an output. I made a second job called read-date that waits for the first job, then reads and prints that same value. + +What happened: the second job printed the exact same date that the first job generated, proving the value passed correctly between them. + +Question: Why would you pass outputs between jobs? + +My answer: Each job runs on its own separate machine, so jobs do not share memory or files automatically. If one job creates a value that a later job needs, like a version number or a generated ID, outputs are the proper way to hand that value over. + +--- + +## Task 4: Conditionals + +In one workflow, I added four things: + +A step that only runs when the branch is my main working branch. + +A step that only runs when the previous step failed. I forced a step to fail on purpose to test this. + +A job that only runs on a push event, not on a pull request. + +A step with continue-on-error set to true. + +What happened: the step that failed on purpose showed a red error message, but the whole job still finished successfully, because continue-on-error let it move on instead of stopping everything. The very next step correctly detected that failure and ran because of it. + +Question: What does continue-on-error true do? + +My answer: It lets one step fail without stopping the rest of the job or marking the whole workflow as failed. This is useful for optional steps where a failure should not block the rest of the pipeline. + +--- + +## Task 5: Putting It Together + +I made a file called smart-pipeline.yml. It has a lint job and a test job that run at the same time, in parallel, since neither one needs the other. Then it has a summary job that waits for both of them to finish using needs, and prints whether the push was to my main branch or a feature branch, plus the actual commit message. + +What happened: the graph showed lint and test side by side, both connecting into summary. The summary job correctly printed the branch message and the real commit message. + +--- + +## Summary in my own simple words + +- needs: makes one job wait for another job to finish successfully before it starts. +- Environment variables can be set at the workflow level, job level, or step level, and lower levels do not remove the ones above them, they all work together. +- outputs: lets one job pass a value to another job, since jobs run separately and do not share data on their own. +- if: lets a step or job run only when a certain condition is true, like a specific branch or a certain event type. +- continue-on-error: true lets a step fail without stopping the whole job. + +#90DaysOfDevOps #DevOpsKaJosh #TrainWithShubham diff --git a/2026/day-44/Screenshot 2026-07-14 124313.png b/2026/day-44/Screenshot 2026-07-14 124313.png new file mode 100644 index 0000000000..a5d7e90cf7 Binary files /dev/null and b/2026/day-44/Screenshot 2026-07-14 124313.png differ diff --git a/2026/day-44/Screenshot 2026-07-14 124757.png b/2026/day-44/Screenshot 2026-07-14 124757.png new file mode 100644 index 0000000000..6d5eecae29 Binary files /dev/null and b/2026/day-44/Screenshot 2026-07-14 124757.png differ diff --git a/2026/day-44/Screenshot 2026-07-14 125339.png b/2026/day-44/Screenshot 2026-07-14 125339.png new file mode 100644 index 0000000000..c1a931b92e Binary files /dev/null and b/2026/day-44/Screenshot 2026-07-14 125339.png differ diff --git a/2026/day-44/day-44-secrets-artifacts.md b/2026/day-44/day-44-secrets-artifacts.md new file mode 100644 index 0000000000..e14996815a --- /dev/null +++ b/2026/day-44/day-44-secrets-artifacts.md @@ -0,0 +1,135 @@ +Day 44 - Secrets, Artifacts and Real Tests in CI + +## Objective + +In this lab, I learned how to use GitHub Actions for real CI work. I practiced storing secrets safely, using secrets as environment variables, uploading and downloading artifacts, running a real test script, and using cache to speed up dependency installation. + +## Repository Secrets + +I created repository secrets in GitHub from: + +`Settings > Secrets and variables > Actions > New repository secret` + +Secrets created: + +- `MY_SECRET_MESSAGE` +- `DOCKER_USERNAME` +- `DOCKER_TOKEN` + +I used `MY_SECRET_MESSAGE` in a workflow without printing the actual secret value. + +The workflow printed: + +```text +The secret is set: true +``` + +When I tried to print the secret directly, GitHub masked it in the logs as: + +```text +*** +``` + +## Why Secrets Should Not Be Printed + +Secrets should never be printed in CI logs because logs can be viewed by other people, saved for later, or shared in screenshots. Even though GitHub masks secrets automatically, it is safer to avoid exposing secret values in any command output. + +## Using Secrets as Environment Variables + +I passed secrets into workflow steps using environment variables. + +Example: + +```yaml +env: + MY_SECRET_MESSAGE: ${{ secrets.MY_SECRET_MESSAGE }} +``` + +This keeps sensitive values out of the code and allows the workflow to use them securely. + +## Docker Secrets + +I added these Docker-related secrets for the next lab: + +- `DOCKER_USERNAME` +- `DOCKER_TOKEN` + +`DOCKER_USERNAME` stores my Docker Hub username. + +`DOCKER_TOKEN` stores my Docker Hub personal access token. + +I did not hardcode these values in the workflow files. + +## Uploading Artifacts + +I created a workflow that generated a report file and uploaded it using: + +```yaml +uses: actions/upload-artifact@v4 +``` + +The artifact can be downloaded from the GitHub Actions workflow run page. + +Screenshot: + +```text +Add artifact download screenshot here +``` + +## Downloading Artifacts Between Jobs + +I created another workflow with two jobs: + +- Job 1 generated a file and uploaded it as an artifact. +- Job 2 downloaded the artifact and printed its contents. + +This showed how files can be passed between jobs in a CI pipeline. + +## When Artifacts Are Useful + +Artifacts are useful in real pipelines for saving: + +- Test reports +- Build outputs +- Log files +- Coverage reports +- Screenshots +- Compiled packages + +They are helpful when one job creates a file and another job needs to use it. + +## Running Real Tests in CI + +I added a Python test script in the repo: + +```text +scripts/check_day_44.py +``` + +The workflow checks out the code, sets up Python, and runs the script. + +If the script exits with a non-zero code, the CI pipeline fails. After fixing the script, the workflow passes successfully. + +Screenshot: + +```text +Add passing test run screenshot here +``` + +## Caching + +I used `actions/cache@v4` to cache Python pip dependencies. + +The cache path was: + +```text +~/.cache/pip +``` + +The cache is stored by GitHub Actions and reused in future workflow runs when the cache key matches. This can make dependency installation faster. + +## What I Learned + +In this lab, I learned that GitHub Actions can do more than run simple commands. It can securely use secrets, store workflow output files as artifacts, pass files between jobs, run real tests, and use caching to improve performance. + +This was my first step toward building a more realistic CI pipeline. diff --git a/2026/day-45/Screenshot 2026-07-14 140142.png b/2026/day-45/Screenshot 2026-07-14 140142.png new file mode 100644 index 0000000000..1df76ed5bb Binary files /dev/null and b/2026/day-45/Screenshot 2026-07-14 140142.png differ diff --git a/2026/day-45/day-45-docker-publish.md b/2026/day-45/day-45-docker-publish.md new file mode 100644 index 0000000000..6c8fcc29cf --- /dev/null +++ b/2026/day-45/day-45-docker-publish.md @@ -0,0 +1,80 @@ +# Day 45 - Docker Build And Push + +Create this workflow file: + +```text +.github/workflows/docker-publish.yml +``` + +Paste this: + +```yaml +name: Day 45 - Docker Publish + +on: + push: + branches: + - main + - DevOps-practice + workflow_dispatch: + +env: + IMAGE_NAME: devops-cicd-lab + +jobs: + docker-build-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set short SHA + run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV + + - name: Log in to Docker Hub + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/DevOps-practice' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build Docker image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest . + docker tag ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:sha-${{ env.SHORT_SHA }} + + - name: Push Docker image + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/DevOps-practice' + run: | + docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest + docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:sha-${{ env.SHORT_SHA }} +``` + +## Simple Explanation + +This workflow builds a Docker image and pushes it to Docker Hub. + +It uses these GitHub secrets: + +```text +DOCKER_USERNAME +DOCKER_TOKEN +``` + +It creates two image tags: + +```text +latest +sha-shortcommit +``` + +## Screenshots To Take + +```text +1. GitHub Actions workflow passed +2. Docker Hub image page +3. latest tag on Docker Hub +4. sha tag on Docker Hub +5. Local app running at localhost:5001 +``` diff --git a/2026/day-46/Screenshot 2026-07-14 140542.png b/2026/day-46/Screenshot 2026-07-14 140542.png new file mode 100644 index 0000000000..7a96e31786 Binary files /dev/null and b/2026/day-46/Screenshot 2026-07-14 140542.png differ diff --git a/2026/day-46/Screenshot 2026-07-14 140557.png b/2026/day-46/Screenshot 2026-07-14 140557.png new file mode 100644 index 0000000000..dbbc555e63 Binary files /dev/null and b/2026/day-46/Screenshot 2026-07-14 140557.png differ diff --git a/2026/day-46/day-46-docker-test.md b/2026/day-46/day-46-docker-test.md new file mode 100644 index 0000000000..990dc948e0 --- /dev/null +++ b/2026/day-46/day-46-docker-test.md @@ -0,0 +1,62 @@ +# Day 46 - Docker Container Test + +Create this workflow file: + +```text +.github/workflows/day-46-docker-test.yml +``` + +Paste this: + +```yaml +name: Day 46 - Docker Test + +on: + push: + branches: + - main + - DevOps-practice + workflow_dispatch: + +jobs: + docker-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t devops-cicd-lab:test . + + - name: Run container + run: | + docker run -d -p 5000:5000 --name devops-lab devops-cicd-lab:test + sleep 5 + + - name: Test running container + run: curl -f http://localhost:5000 + + - name: Stop container + if: always() + run: docker stop devops-lab +``` + +## Simple Explanation + +This workflow checks if the Docker container really works. + +It builds the image, runs the container, and tests the app with: + +```text +curl http://localhost:5000 +``` + +If the app does not respond, the workflow fails. + +## Screenshots To Take + +```text +1. Day 46 Docker Test workflow passed +2. Logs showing curl test worked +``` diff --git a/2026/day-47/Screenshot 2026-07-14 140452.png b/2026/day-47/Screenshot 2026-07-14 140452.png new file mode 100644 index 0000000000..024ac3aa4e Binary files /dev/null and b/2026/day-47/Screenshot 2026-07-14 140452.png differ diff --git a/2026/day-47/day-47-python-tests.md b/2026/day-47/day-47-python-tests.md new file mode 100644 index 0000000000..f6e6a328db --- /dev/null +++ b/2026/day-47/day-47-python-tests.md @@ -0,0 +1,61 @@ +# Day 47 - Python Tests + +Create this workflow file: + +```text +.github/workflows/day-47-python-tests.yml +``` + +Paste this: + +```yaml +name: Day 47 - Python Tests + +on: + push: + branches: + - main + - DevOps-practice + workflow_dispatch: + +jobs: + python-tests: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests + run: pytest scripts/test_app.py +``` + +## Simple Explanation + +This workflow runs Python tests in GitHub Actions. + +It installs the app dependencies and runs: + +```text +pytest scripts/test_app.py +``` + +If any test fails, the workflow fails. + +## Screenshots To Take + +```text +1. Day 47 Python Tests workflow passed +2. Logs showing pytest passed +``` + diff --git a/2026/day-48/Screenshot 2026-07-14 140142.png b/2026/day-48/Screenshot 2026-07-14 140142.png new file mode 100644 index 0000000000..1df76ed5bb Binary files /dev/null and b/2026/day-48/Screenshot 2026-07-14 140142.png differ diff --git a/2026/day-48/day-48-security-scan.md b/2026/day-48/day-48-security-scan.md new file mode 100644 index 0000000000..f688764d01 --- /dev/null +++ b/2026/day-48/day-48-security-scan.md @@ -0,0 +1,60 @@ +# Day 48 - Docker Security Scan + +Create this workflow file: + +```text +.github/workflows/day-48-security-scan.yml +``` + +Paste this: + +```yaml +name: Day 48 - Docker Security Scan + +on: + push: + branches: + - main + - DevOps-practice + workflow_dispatch: + +jobs: + security-scan: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Build Docker image + run: docker build -t devops-cicd-lab:scan . + + - name: Scan Docker image with Trivy + uses: aquasecurity/trivy-action@master + with: + image-ref: devops-cicd-lab:scan + format: table + exit-code: 0 +``` + +## Simple Explanation + +This workflow scans the Docker image using Trivy. + +Trivy checks the image for security issues and prints the result in the workflow logs. + +I used: + +```text +exit-code: 0 +``` + +This keeps the lab workflow green while still showing the scan report. + +## Screenshots To Take + +```text +1. Day 48 Security Scan workflow passed +2. Trivy scan result in logs +``` + diff --git a/2026/day-49/Screenshot 2026-07-14 140040.png b/2026/day-49/Screenshot 2026-07-14 140040.png new file mode 100644 index 0000000000..12f4af2836 Binary files /dev/null and b/2026/day-49/Screenshot 2026-07-14 140040.png differ diff --git a/2026/day-49/day-49-final-pipeline.md b/2026/day-49/day-49-final-pipeline.md new file mode 100644 index 0000000000..8b90a78ecd --- /dev/null +++ b/2026/day-49/day-49-final-pipeline.md @@ -0,0 +1,98 @@ +# Day 49 - Final CI/CD Pipeline + +Create this workflow file: + +```text +.github/workflows/day-49-final-pipeline.yml +``` + +Paste this: + +```yaml +name: Day 49 - Final CI/CD Pipeline + +on: + push: + branches: + - main + - DevOps-practice + workflow_dispatch: + +env: + IMAGE_NAME: devops-cicd-lab + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run Python tests + run: pytest scripts/test_app.py + + docker: + runs-on: ubuntu-latest + needs: test + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set short SHA + run: echo "SHORT_SHA=${GITHUB_SHA::7}" >> $GITHUB_ENV + + - name: Build Docker image + run: | + docker build -t ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest . + docker tag ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:sha-${{ env.SHORT_SHA }} + + - name: Log in to Docker Hub + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/DevOps-practice' + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Push Docker image + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/DevOps-practice' + run: | + docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest + docker push ${{ secrets.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:sha-${{ env.SHORT_SHA }} +``` + +## Simple Explanation + +This is the final CI/CD pipeline. + +It has two jobs: + +```text +test +docker +``` + +The Docker job runs only after the test job passes. + +That means the image is pushed only when the tests are successful. + +## Screenshots To Take + +```text +1. Day 49 Final Pipeline workflow passed +2. Test job passed +3. Docker job passed +4. Docker Hub image updated +``` + diff --git a/2026/day-50/Screenshot 2026-07-24 114135.png b/2026/day-50/Screenshot 2026-07-24 114135.png new file mode 100644 index 0000000000..9cebfd0542 Binary files /dev/null and b/2026/day-50/Screenshot 2026-07-24 114135.png differ diff --git a/2026/day-50/Screenshot 2026-07-24 114156.png b/2026/day-50/Screenshot 2026-07-24 114156.png new file mode 100644 index 0000000000..50e9845576 Binary files /dev/null and b/2026/day-50/Screenshot 2026-07-24 114156.png differ diff --git a/2026/day-50/Screenshot 2026-07-24 120252.png b/2026/day-50/Screenshot 2026-07-24 120252.png new file mode 100644 index 0000000000..6994b18804 Binary files /dev/null and b/2026/day-50/Screenshot 2026-07-24 120252.png differ diff --git a/2026/day-50/Screenshot 2026-07-24 134449.png b/2026/day-50/Screenshot 2026-07-24 134449.png new file mode 100644 index 0000000000..c20293f821 Binary files /dev/null and b/2026/day-50/Screenshot 2026-07-24 134449.png differ diff --git a/2026/day-50/Screenshot 2026-07-24 134626.png b/2026/day-50/Screenshot 2026-07-24 134626.png new file mode 100644 index 0000000000..f885351d33 Binary files /dev/null and b/2026/day-50/Screenshot 2026-07-24 134626.png differ diff --git a/2026/day-50/Screenshot 2026-07-24 134841.png b/2026/day-50/Screenshot 2026-07-24 134841.png new file mode 100644 index 0000000000..7dd27a4419 Binary files /dev/null and b/2026/day-50/Screenshot 2026-07-24 134841.png differ diff --git a/2026/day-50/Screenshot 2026-07-24 135228.png b/2026/day-50/Screenshot 2026-07-24 135228.png new file mode 100644 index 0000000000..f6e0d26ff8 Binary files /dev/null and b/2026/day-50/Screenshot 2026-07-24 135228.png differ diff --git a/2026/day-50/day-50-k8s-setup.md b/2026/day-50/day-50-k8s-setup.md new file mode 100644 index 0000000000..f975ced866 --- /dev/null +++ b/2026/day-50/day-50-k8s-setup.md @@ -0,0 +1,217 @@ +# Why was Kubernetes created? + +Kubernetes was created because managing thousands of containers across many servers manually is very difficult. If a container or server fails, Kubernetes automatically restarts containers, schedules them on healthy servers, scales applications based on demand, and manages networking. It also works with persistent storage so application data is not lost when a container is recreated. + +# Who created Kubernetes and what inspired it? + +Kubernetes was created by Google. It was inspired by Google's internal container management system called Borg. Later, Google donated Kubernetes to the Cloud Native Computing Foundation (CNCF), where it became an open-source project. + +# What does Kubernetes mean? + +Kubernetes is a Greek word that means "helmsman" or "captain." Just like a captain manages many ships, Kubernetes manages many containers. + +# Why can't we just use Docker? + +Docker is used to create and run containers. Kubernetes is used to manage containers across multiple machines. It automatically handles deployment, scaling, networking, load balancing, self-healing, and high availability. + +# Which part actually runs the application + +The Worker Nodes run the application. The Control Plane manages the cluster and tells the Worker Nodes what to do. + +# What is Control Plane? + +he Control Plane is the brain of the Kubernetes cluster. It receives requests from users through the API Server, stores the cluster state in etcd, schedules Pods, and makes sure the cluster stays in the desired state. It manages the entire cluster but does not run application containers. + +# What is Worker Node? + +A Worker Node is the machine where Pods and containers actually run. Each Worker Node contains kubelet, kube-proxy, and a container runtime like containerd. The Worker Node follows instructions from the Control Plane to run and manage applications. + +# What is the API Server? + +The API Server is the front door of the Kubernetes cluster. It receives all requests from users and Kubernetes components, validates them, stores the cluster state in etcd, and coordinates communication between all cluster components. + +# What happens if etcd is lost? + +etcd stores the entire Kubernetes cluster state. If etcd is lost, Kubernetes forgets its configuration, Deployments, Services, Secrets, ConfigMaps, and other resources. Running Pods may continue working for some time, but the Control Plane cannot properly manage the cluster. That is why etcd backups are very important. + +# Why do we use Namespaces? + +Namespaces are used to logically separate resources inside a Kubernetes cluster. They help organize applications, avoid naming conflicts, and make it easier to manage access and resources for different teams or environments. + +# Why do we use Deployments instead of ReplicaSets? + +We use Deployments because they provide rolling updates, rollbacks, scaling, and manage ReplicaSets automatically. ReplicaSets only ensure the required number of Pods are running. + +# Day 50 – Kubernetes Architecture and Cluster Setup + +## Kubernetes History + +Kubernetes was created to manage many containers running on multiple servers. Docker can run containers, but it cannot manage containers across many servers easily. + +Kubernetes was created by Google and inspired by Google's Borg system. + +The word **Kubernetes** means **Captain** in Greek. + +--- + +# Kubernetes Architecture + +## Control Plane + +- API Server – Receives all requests from kubectl. +- etcd – Stores all cluster information. +- Scheduler – Selects the best node for a Pod. +- Controller Manager – Keeps the cluster in the desired state. + +## Worker Node + +- kubelet – Manages Pods on the node. +- kube-proxy – Handles Pod networking. +- containerd – Runs containers. + +### Request Flow + +``` +kubectl + ↓ +API Server + ↓ +etcd + ↓ +Controller Manager + ↓ +ReplicaSet + ↓ +Scheduler + ↓ +API Server + ↓ +kubelet + ↓ +containerd + ↓ +Running Pod +``` + +--- + +# What happens when I run kubectl apply? + +1. kubectl sends the request to the API Server. +2. API Server stores the desired state in etcd. +3. Controller Manager creates the required resources. +4. Scheduler selects the best Worker Node. +5. kubelet starts the Pod. +6. containerd runs the container. +7. Pod becomes Running. + +--- + +# What happens if the API Server goes down? + +- kubectl cannot communicate with the cluster. +- No new resources can be created. +- Existing Pods continue running. + +--- + +# What happens if a Worker Node goes down? + +- Kubernetes detects the failure. +- Scheduler chooses another healthy node (if available). +- ReplicaSet creates new Pods. + +--- + +# Local Kubernetes Cluster + +Tool Used: **Kind** + +Why I chose Kind: + +- Easy to install. +- Uses Docker. +- Fast to create a cluster. +- Good for learning Kubernetes. + +--- + +# Commands I Used + +kubectl version --client + +kind version + +kubectl cluster-info + +kubectl get nodes + +kubectl get namespaces + +kubectl get pods -A + +kubectl describe node tws-cluster-control-plane + +kind get clusters + +kind delete cluster --name tws-cluster + +kind create cluster --name tws-cluster + +kubectl get nodes + +kubectl config current-context + +kubectl config get-contexts + +kubectl config view + +--- + +# kube-system Pods + +| Pod | Purpose | +| ----------------------- | ---------------------------- | +| kube-apiserver | Receives Kubernetes requests | +| etcd | Stores cluster data | +| kube-scheduler | Selects the best node | +| kube-controller-manager | Maintains desired state | +| CoreDNS | DNS for Pods | +| kube-proxy | Handles networking | +| kindnet | Creates Pod network | + +--- + +# kubeconfig + +A kubeconfig file stores cluster connection details. + +Default location: + +~/.kube/config + +It contains: + +- Cluster information +- User information +- Contexts + +--- + +--- + +## Kubernetes System Pods + +--- + +# What I Learned + +- Kubernetes manages containers across multiple servers. +- API Server is the main entry point. +- etcd stores cluster information. +- Scheduler selects the best node. +- Controller Manager keeps the cluster healthy. +- kubelet manages Pods. +- containerd runs containers. +- Kind is a simple local Kubernetes cluster. +- kubectl is the command-line tool used to manage Kubernetes. diff --git a/2026/day-51/Screenshot 2026-07-24 152257.png b/2026/day-51/Screenshot 2026-07-24 152257.png new file mode 100644 index 0000000000..309be5f382 Binary files /dev/null and b/2026/day-51/Screenshot 2026-07-24 152257.png differ diff --git a/2026/day-51/Screenshot 2026-07-24 152414.png b/2026/day-51/Screenshot 2026-07-24 152414.png new file mode 100644 index 0000000000..9fdb9cea81 Binary files /dev/null and b/2026/day-51/Screenshot 2026-07-24 152414.png differ diff --git a/2026/day-51/Screenshot 2026-07-24 152620.png b/2026/day-51/Screenshot 2026-07-24 152620.png new file mode 100644 index 0000000000..6bbc748b0f Binary files /dev/null and b/2026/day-51/Screenshot 2026-07-24 152620.png differ diff --git a/2026/day-51/Screenshot 2026-07-24 152642.png b/2026/day-51/Screenshot 2026-07-24 152642.png new file mode 100644 index 0000000000..c0d6f5c0a4 Binary files /dev/null and b/2026/day-51/Screenshot 2026-07-24 152642.png differ diff --git a/2026/day-51/Screenshot 2026-07-24 152725.png b/2026/day-51/Screenshot 2026-07-24 152725.png new file mode 100644 index 0000000000..46648e56ef Binary files /dev/null and b/2026/day-51/Screenshot 2026-07-24 152725.png differ diff --git a/2026/day-51/day-51-pods.md b/2026/day-51/day-51-pods.md new file mode 100644 index 0000000000..695da3c2ee --- /dev/null +++ b/2026/day-51/day-51-pods.md @@ -0,0 +1,138 @@ +Day 51 – My First Kubernetes Pod +Objective + +Today I learned how to create and manage my first Pod in Kubernetes using a YAML manifest. + +What is a Pod? + +A Pod is the smallest unit in Kubernetes. It can contain one or more containers. Usually, one Pod runs one application container. + +Example: + +One Pod → One Nginx container + +What is a Manifest? + +A manifest is a YAML file that tells Kubernetes what resource to create and how it should be configured. + +Kubernetes reads the YAML file and creates the resource. + +Main Parts of a Pod Manifest +apiVersion + +It tells Kubernetes which API version is used for the resource. + +Example: + +apiVersion: v1 + +Pods use v1. + +kind + +It tells Kubernetes what resource to create. + +Example: + +kind: Pod + +metadata + +Metadata gives information about the resource such as its name and labels. + +Example: + +Name: nginx-pod +Label: app=nginx + +Labels help organize and identify Kubernetes resources. + +spec + +The spec section describes the desired state of the Pod. + +Inside spec, we define: + +Container name +Docker image +Container port + +The containers field must always be inside the spec section. + +apiVersion: v1 +kind: Pod + +metadata: +name: nginx-pod +labels: +app: nginx + +spec: +containers: - name: nginx +image: nginx:latest +ports: - containerPort: 80 + +# Error I Faced + +I received the following error: + +"unknown field 'containers'" + +Reason: + +The containers section was not inside the spec section because of incorrect YAML indentation. + +The API Server validates every manifest before creating resources. + +Since the YAML structure was incorrect, the API Server rejected the request. + +After fixing the indentation, the Pod was created successfully. + +# Pod Creation Flow + +kubectl sends the YAML file to the API Server. +The API Server validates the manifest. +The Pod information is stored in etcd. +The Scheduler selects a node. +The kubelet receives the request. +The container runtime downloads the image if needed. +The container starts. +The Pod status becomes Running. + +# Commands I Learned + +Create a Pod +Validate a manifest using dry run +List Pods +View Pod details +View Pod logs +Enter a running container +Delete a Pod + +# What I Learned from Logs + +kubectl logs displays the output generated by the application running inside the container. + +It is mainly used for troubleshooting and debugging applications. + +# What I Learned from kubectl exec + +kubectl exec allows us to enter a running container and execute commands. + +Inside the nginx container, I verified: + +Current working directory +Files and folders +Installed Nginx version + +# Pod Deletion + +After deleting the Pod, it was not recreated automatically. + +Reason: + +The Pod was created directly from a Pod manifest. + +There was no Deployment or ReplicaSet managing the Pod. + +Only Deployments and ReplicaSets automatically recreate Pods. diff --git a/2026/day-51/day50-51.png b/2026/day-51/day50-51.png new file mode 100644 index 0000000000..e44c353028 Binary files /dev/null and b/2026/day-51/day50-51.png differ diff --git a/2026/day-51/nginx-pod.yml b/2026/day-51/nginx-pod.yml new file mode 100644 index 0000000000..5a99a930d3 --- /dev/null +++ b/2026/day-51/nginx-pod.yml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod + +metadata: + name: nginx-pod + labels: + app: nginx + +spec: + containers: + - name: nginx + image: nginx:latest + ports: + - containerPort: 80 diff --git a/2026/day-53/Screenshot 2026-07-27 141631.png b/2026/day-53/Screenshot 2026-07-27 141631.png new file mode 100644 index 0000000000..e68cec9f31 Binary files /dev/null and b/2026/day-53/Screenshot 2026-07-27 141631.png differ diff --git a/2026/day-53/Screenshot 2026-07-27 141735.png b/2026/day-53/Screenshot 2026-07-27 141735.png new file mode 100644 index 0000000000..21449882eb Binary files /dev/null and b/2026/day-53/Screenshot 2026-07-27 141735.png differ diff --git a/2026/day-53/Screenshot 2026-07-27 141902.png b/2026/day-53/Screenshot 2026-07-27 141902.png new file mode 100644 index 0000000000..33a92e02b2 Binary files /dev/null and b/2026/day-53/Screenshot 2026-07-27 141902.png differ diff --git a/2026/day-53/Screenshot 2026-07-27 142104.png b/2026/day-53/Screenshot 2026-07-27 142104.png new file mode 100644 index 0000000000..b66d41d4fd Binary files /dev/null and b/2026/day-53/Screenshot 2026-07-27 142104.png differ diff --git a/2026/day-53/Screenshot 2026-07-27 142800.png b/2026/day-53/Screenshot 2026-07-27 142800.png new file mode 100644 index 0000000000..5cf76bdf0c Binary files /dev/null and b/2026/day-53/Screenshot 2026-07-27 142800.png differ diff --git a/2026/day-53/Screenshot 2026-07-27 143000.png b/2026/day-53/Screenshot 2026-07-27 143000.png new file mode 100644 index 0000000000..aadb8c3d20 Binary files /dev/null and b/2026/day-53/Screenshot 2026-07-27 143000.png differ diff --git a/2026/day-53/Screenshot 2026-07-27 143212.png b/2026/day-53/Screenshot 2026-07-27 143212.png new file mode 100644 index 0000000000..b246bb492f Binary files /dev/null and b/2026/day-53/Screenshot 2026-07-27 143212.png differ diff --git a/2026/day-53/Screenshot 2026-07-27 143339.png b/2026/day-53/Screenshot 2026-07-27 143339.png new file mode 100644 index 0000000000..df0566b798 Binary files /dev/null and b/2026/day-53/Screenshot 2026-07-27 143339.png differ diff --git a/2026/day-53/Screenshot 2026-07-27 143413.png b/2026/day-53/Screenshot 2026-07-27 143413.png new file mode 100644 index 0000000000..b99b9eb6cc Binary files /dev/null and b/2026/day-53/Screenshot 2026-07-27 143413.png differ diff --git a/2026/day-53/day-53-kubernetes-deployment-lab.md b/2026/day-53/day-53-kubernetes-deployment-lab.md new file mode 100644 index 0000000000..5662ea60fc --- /dev/null +++ b/2026/day-53/day-53-kubernetes-deployment-lab.md @@ -0,0 +1,150 @@ +# Day 53 -- Kubernetes Deployment Lab + +## Objective + +Deploy an Nginx application using a Kubernetes Deployment and understand +how Deployments, ReplicaSets, and Pods work together. + +## Architecture + + Deployment + │ + ▼ + ReplicaSet + │ + ▼ + Pods + +## Deployment YAML + +``` yaml +apiVersion: apps/v1 +kind: Deployment + +metadata: + name: backend-deployment + +spec: + replicas: 3 + + selector: + matchLabels: + app: backend + + template: + metadata: + labels: + app: backend + + spec: + containers: + - name: backend + image: nginx:latest + ports: + - containerPort: 80 +``` + +## Commands Used + +### Apply Deployment + +``` bash +kubectl apply -f deployment.yml +``` + +### Verify Resources + +``` bash +kubectl get deployments +kubectl get replicasets +kubectl get pods +kubectl describe deployment backend-deployment +``` + +### Self-Healing Test + +``` bash +kubectl delete pod +kubectl get pods +``` + +Result: - ReplicaSet detected only 2 Pods. - ReplicaSet created a new +Pod from the Pod Template. - Scheduler selected a worker node. - kubelet +started the container. + +### Scale Up + +``` bash +kubectl scale deployment backend-deployment --replicas=5 +kubectl get pods +``` + +Result: - Desired State = 5 - ReplicaSet created 2 additional Pods. + +### Scale Down + +``` bash +kubectl scale deployment backend-deployment --replicas=2 +kubectl get pods +``` + +Result: - ReplicaSet terminated 3 Pods. - 2 Pods remained running. + +### Cleanup + +``` bash +kubectl delete deployment backend-deployment +kubectl get deployments +kubectl get replicasets +kubectl get pods +``` + +## Interview Questions + +### What is a Deployment? + +A Deployment manages ReplicaSets and provides scaling, rolling updates, +rollback, and self-healing. + +### Why do we need a ReplicaSet? + +It keeps the current number of Pods equal to the desired number of +replicas. + +### Why do selector and template labels match? + +The ReplicaSet uses the selector to identify and manage the Pods created +from the template. + +### What is the Pod Template? + +It is the blueprint used to create new Pods. + +### What happens when a Pod is deleted? + +1. ReplicaSet detects fewer Pods than desired. +2. ReplicaSet creates a new Pod using the template. +3. API Server stores the new Pod. +4. Scheduler assigns a node. +5. kubelet starts the container. + +### What happens when scaling from 3 to 5 replicas? + +The Deployment updates the desired state, the ReplicaSet creates two new +Pods, the Scheduler assigns nodes, and the kubelet starts the +containers. + +### What happens when scaling from 5 to 2 replicas? + +The ReplicaSet terminates three Pods and keeps two Pods running. + +## Key Learnings + +- Deployment manages ReplicaSets. +- ReplicaSet manages Pods. +- Pod Template is the blueprint for Pods. +- Labels and selectors connect Deployments, ReplicaSets, Services, and + Pods. +- Self-healing recreates failed Pods automatically. +- Scaling changes the desired number of replicas. +- Kubernetes always works toward the desired state. diff --git a/2026/day-53/deployment.png b/2026/day-53/deployment.png new file mode 100644 index 0000000000..7360c325ff Binary files /dev/null and b/2026/day-53/deployment.png differ diff --git a/2026/day-53/deployment.yml b/2026/day-53/deployment.yml new file mode 100644 index 0000000000..60ca41d70f --- /dev/null +++ b/2026/day-53/deployment.yml @@ -0,0 +1,25 @@ +apiVersion: apps/v1 +kind: Deployment + +metadata: + name: backend-deployment + +spec: + replicas: 3 + + selector: + matchLabels: + app: backend + + template: # ← Same indentation as selector + metadata: + labels: + app: backend + + spec: + containers: + - name: backend + image: nginx:latest + ports: + - containerPort: 80 + diff --git a/2026/day-54/Concept.png b/2026/day-54/Concept.png new file mode 100644 index 0000000000..3de39f968e Binary files /dev/null and b/2026/day-54/Concept.png differ diff --git a/2026/day-54/Day-54-Kubernetes-ConfigMaps-and-Secrets.md b/2026/day-54/Day-54-Kubernetes-ConfigMaps-and-Secrets.md new file mode 100644 index 0000000000..900aeb0ed9 --- /dev/null +++ b/2026/day-54/Day-54-Kubernetes-ConfigMaps-and-Secrets.md @@ -0,0 +1,146 @@ +# Day 54 -- Kubernetes ConfigMaps & Secrets (Hands-on Lab) + +## Objective + +- Create and use ConfigMaps +- Inject ConfigMaps as environment variables +- Update ConfigMaps +- Understand why running Pods don't automatically receive updated + values +- Create Kubernetes Secrets +- Inject Secrets into Pods +- Understand Base64 encoding vs encryption + +## Create ConfigMap + +``` bash +kubectl create configmap app-config \ + --from-literal=APP_ENV=production \ + --from-literal=APP_DEBUG=false \ + --from-literal=APP_PORT=8080 +``` + +Verify: + +``` bash +kubectl get configmap +kubectl describe configmap app-config +kubectl get configmap app-config -o yaml +``` + +## ConfigMap from File + +Create `app.properties`: + +``` properties +APP_ENV=production +APP_DEBUG=false +APP_PORT=8080 +``` + +``` bash +kubectl create configmap app-config-file --from-file=app.properties +``` + +## Inject ConfigMap + +``` yaml +apiVersion: v1 +kind: Pod +metadata: + name: configmap-pod +spec: + containers: + - name: app-container + image: nginx + envFrom: + - configMapRef: + name: app-config +``` + +``` bash +kubectl apply -f configmap-pod.yaml +kubectl exec -it configmap-pod -- env | grep APP +``` + +## Update ConfigMap + +``` bash +kubectl edit configmap app-config +``` + +Change APP_ENV from production to staging, save with `Esc` then `:wq`. + +Recreate the Pod: + +``` bash +kubectl delete pod configmap-pod +kubectl apply -f configmap-pod.yaml +kubectl exec -it configmap-pod -- env | grep APP_ENV +``` + +Expected: + +``` text +APP_ENV=staging +``` + +## Create Secret + +``` bash +kubectl create secret generic db-secret \ + --from-literal=DB_USER=admin \ + --from-literal=DB_PASSWORD=myPassword123 +``` + +View: + +``` bash +kubectl describe secret db-secret +kubectl get secret db-secret -o yaml +``` + +Decode: + +``` bash +echo "YWRtaW4=" | base64 --decode +echo "bXlQYXNzd29yZDEyMw==" | base64 --decode +``` + +## Inject Secret + +``` yaml +apiVersion: v1 +kind: Pod +metadata: + name: secret-pod +spec: + containers: + - name: app-container + image: nginx + envFrom: + - secretRef: + name: db-secret +``` + +``` bash +kubectl apply -f secret-pod.yaml +kubectl exec -it secret-pod -- env | grep DB +``` + +Expected: + +``` text +DB_USER=admin +DB_PASSWORD=myPassword123 +``` + +## Interview Notes + +- ConfigMap stores non-sensitive configuration. +- Secret stores sensitive information. +- Secrets are Base64 encoded, not encrypted by default. +- Updating a ConfigMap does not update running Pods automatically. +- Restart or recreate Pods to load updated ConfigMap values. + +## Day 54 Complete ✅ diff --git a/2026/day-54/Screenshot 2026-07-29 160017.png b/2026/day-54/Screenshot 2026-07-29 160017.png new file mode 100644 index 0000000000..d10b0c20cb Binary files /dev/null and b/2026/day-54/Screenshot 2026-07-29 160017.png differ diff --git a/2026/day-54/Screenshot 2026-07-29 160412.png b/2026/day-54/Screenshot 2026-07-29 160412.png new file mode 100644 index 0000000000..c0c40e1e79 Binary files /dev/null and b/2026/day-54/Screenshot 2026-07-29 160412.png differ diff --git a/2026/day-54/Screenshot 2026-07-29 160514.png b/2026/day-54/Screenshot 2026-07-29 160514.png new file mode 100644 index 0000000000..78134f347f Binary files /dev/null and b/2026/day-54/Screenshot 2026-07-29 160514.png differ diff --git a/2026/day-54/Screenshot 2026-07-29 161313.png b/2026/day-54/Screenshot 2026-07-29 161313.png new file mode 100644 index 0000000000..efb7c0d5cd Binary files /dev/null and b/2026/day-54/Screenshot 2026-07-29 161313.png differ diff --git a/2026/day-54/Screenshot 2026-07-29 162201.png b/2026/day-54/Screenshot 2026-07-29 162201.png new file mode 100644 index 0000000000..46447af0a5 Binary files /dev/null and b/2026/day-54/Screenshot 2026-07-29 162201.png differ diff --git a/2026/day-54/app.properties b/2026/day-54/app.properties new file mode 100644 index 0000000000..03e1549bd4 --- /dev/null +++ b/2026/day-54/app.properties @@ -0,0 +1,3 @@ +APP_ENV=production +APP_DEBUG=false +APP_PORT=8080 diff --git a/2026/day-54/configmap-pod.yaml b/2026/day-54/configmap-pod.yaml new file mode 100644 index 0000000000..a7ce44e8cd --- /dev/null +++ b/2026/day-54/configmap-pod.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod + +metadata: + name: configmap-pod + +spec: + containers: + - name: app-container + image: nginx + envFrom: + - configMapRef: + name: app-config diff --git a/2026/day-54/secret-pod.yaml b/2026/day-54/secret-pod.yaml new file mode 100644 index 0000000000..8334f43662 --- /dev/null +++ b/2026/day-54/secret-pod.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: secret-pod + +spec: + containers: + - name: app-container + image: nginx + envFrom: + - secretRef: + name: db-secret diff --git a/2026/day-55/PV and PVC.png b/2026/day-55/PV and PVC.png new file mode 100644 index 0000000000..396a311eaf Binary files /dev/null and b/2026/day-55/PV and PVC.png differ diff --git a/2026/day-55/Screenshot 2026-07-30 125040.png b/2026/day-55/Screenshot 2026-07-30 125040.png new file mode 100644 index 0000000000..78926b6a26 Binary files /dev/null and b/2026/day-55/Screenshot 2026-07-30 125040.png differ diff --git a/2026/day-55/Screenshot 2026-07-30 125351.png b/2026/day-55/Screenshot 2026-07-30 125351.png new file mode 100644 index 0000000000..4c4fd5973d Binary files /dev/null and b/2026/day-55/Screenshot 2026-07-30 125351.png differ diff --git a/2026/day-55/Screenshot 2026-07-30 131130.png b/2026/day-55/Screenshot 2026-07-30 131130.png new file mode 100644 index 0000000000..25111d744d Binary files /dev/null and b/2026/day-55/Screenshot 2026-07-30 131130.png differ diff --git a/2026/day-55/Screenshot 2026-07-30 134724.png b/2026/day-55/Screenshot 2026-07-30 134724.png new file mode 100644 index 0000000000..5243e2f5d8 Binary files /dev/null and b/2026/day-55/Screenshot 2026-07-30 134724.png differ diff --git a/2026/day-55/Screenshot 2026-07-30 135048.png b/2026/day-55/Screenshot 2026-07-30 135048.png new file mode 100644 index 0000000000..86014edb83 Binary files /dev/null and b/2026/day-55/Screenshot 2026-07-30 135048.png differ diff --git a/2026/day-55/Screenshot 2026-07-30 135320.png b/2026/day-55/Screenshot 2026-07-30 135320.png new file mode 100644 index 0000000000..e7a8744f1c Binary files /dev/null and b/2026/day-55/Screenshot 2026-07-30 135320.png differ diff --git a/2026/day-55/Screenshot 2026-07-30 135336.png b/2026/day-55/Screenshot 2026-07-30 135336.png new file mode 100644 index 0000000000..5f3fc194cf Binary files /dev/null and b/2026/day-55/Screenshot 2026-07-30 135336.png differ diff --git a/2026/day-55/day-55-persistent-volumes.md b/2026/day-55/day-55-persistent-volumes.md new file mode 100644 index 0000000000..a4f95f4652 --- /dev/null +++ b/2026/day-55/day-55-persistent-volumes.md @@ -0,0 +1,155 @@ +# Day 55 - Kubernetes Persistent Volumes (PV) and Persistent Volume Claims (PVC) + +## Objective +Learn why containers need persistent storage and how Kubernetes uses Persistent Volumes (PV), Persistent Volume Claims (PVC), and StorageClasses. + +## Why Containers Need Persistent Storage +Containers are ephemeral. When a Pod is deleted, everything stored inside the container is lost. + +- `emptyDir` stores data only while the Pod exists. +- Deleting the Pod deletes the `emptyDir` volume and its data. +- Persistent storage keeps data even after Pods are recreated. + +## Persistent Volume (PV) +A PersistentVolume is a Kubernetes resource that represents storage. + +Example configuration: +- Capacity: 1Gi +- Access Mode: ReadWriteOnce (RWO) +- Reclaim Policy: Retain +- hostPath: /tmp/k8s-pv-data + +PV lifecycle: +Available → Bound → Released + +## Persistent Volume Claim (PVC) +A PersistentVolumeClaim requests storage from Kubernetes. + +Example request: +- Storage: 500Mi +- Access Mode: ReadWriteOnce + +The PVC does not know where the storage exists. Kubernetes finds a matching PV. + +## Architecture + +Application + | + v + Pod + | + v + PVC + | + v + PV + | + v +Physical Storage + +Pods use PVCs, not PVs directly. + +## Static Provisioning +Administrator creates the PV manually. +Developer creates the PVC. +Kubernetes binds the PVC to the PV. + +Lab resources: +- PV: my-pv +- PVC: my-pvc + +## Dynamic Provisioning +Developer creates only the PVC. +StorageClass automatically creates the PV. + +StorageClass observed: +- Name: standard +- Provisioner: rancher.io/local-path +- Reclaim Policy: Delete +- Volume Binding Mode: WaitForFirstConsumer + +Because the binding mode was WaitForFirstConsumer, the PVC stayed Pending until a Pod used it. + +## Access Modes +- ReadWriteOnce (RWO): One node can read/write. +- ReadOnlyMany (ROX): Many nodes can read only. +- ReadWriteMany (RWX): Many nodes can read/write. + +## Reclaim Policies +Retain +- Keeps the PV after the PVC is deleted. +- PV status becomes Released. + +Delete +- Deletes the dynamically created PV after the PVC is deleted. + +## Lab Summary +### Task 1 +- Demonstrated data loss with emptyDir. + +### Task 2 +- Created a static PersistentVolume. + +### Task 3 +- Created a PersistentVolumeClaim. +- Fixed Pending state using: + storageClassName: "" + +### Task 4 +- Mounted the PVC in a Pod. +- Verified data persisted after Pod recreation. + +### Task 5 +- Explored the default StorageClass. + +### Task 6 +- Created a dynamic PVC. +- Created a Pod using it. +- Verified Kubernetes automatically provisioned a PV. + +### Task 7 +- Deleted Pods. +- Deleted PVCs. +- Observed: + - Manual PV → Released + - Dynamic PV → Deleted automatically +- Deleted the remaining manual PV. + +## Common Troubleshooting +PVC Pending: +- StorageClass mismatch +- Capacity mismatch +- Access mode mismatch +- WaitForFirstConsumer waiting for a Pod + +Pod cannot mount PVC: +- PVC not Bound +- Wrong claimName +- No available PV + +## Interview Questions + +Why not use emptyDir for databases? +Because emptyDir is deleted when the Pod is deleted. + +Difference between PV and PVC? +- PV = Actual storage. +- PVC = Request for storage. + +Static vs Dynamic Provisioning? +- Static: Administrator creates the PV. +- Dynamic: Kubernetes creates the PV automatically through a StorageClass. + +What is a StorageClass? +A StorageClass defines how Kubernetes dynamically provisions storage. + +## Key Takeaways +- Containers are ephemeral. +- emptyDir is temporary. +- PV provides persistent storage. +- PVC requests storage. +- Pods use PVCs, not PVs directly. +- StorageClasses enable dynamic provisioning. +- WaitForFirstConsumer delays provisioning until a Pod consumes the PVC. +- Retain keeps storage. +- Delete removes dynamically created storage. diff --git a/2026/day-55/dynamic-pod.yaml b/2026/day-55/dynamic-pod.yaml new file mode 100644 index 0000000000..675708e3ed --- /dev/null +++ b/2026/day-55/dynamic-pod.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Pod + +metadata: + name: dynamic-pod + +spec: + containers: + - name: app + image: busybox + command: + - sh + - -c + - sleep 3600 + + volumeMounts: + - name: storage + mountPath: /data + + volumes: + - name: storage + persistentVolumeClaim: + claimName: dynamic-pvc diff --git a/2026/day-55/dynamic-pvc.yaml b/2026/day-55/dynamic-pvc.yaml new file mode 100644 index 0000000000..fc05a0d0e0 --- /dev/null +++ b/2026/day-55/dynamic-pvc.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: PersistentVolumeClaim + +metadata: + name: dynamic-pvc + +spec: + storageClassName: standard + + accessModes: + - ReadWriteOnce + + resources: + requests: + storage: 500Mi diff --git a/2026/day-55/pod.yaml b/2026/day-55/pod.yaml new file mode 100644 index 0000000000..dd5f9f15d5 --- /dev/null +++ b/2026/day-55/pod.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Pod +metadata: + name: emptydir-pod + +spec: + containers: + - name: app + image: busybox + command: + - sh + - -c + - sleep 3600 + + volumeMounts: + - name: data-volume + mountPath: /data + + volumes: + - name: data-volume + persistentVolumeClaim: + claimName: my-pvc diff --git a/2026/day-55/pv.yaml b/2026/day-55/pv.yaml new file mode 100644 index 0000000000..62377d1ce3 --- /dev/null +++ b/2026/day-55/pv.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolume + +metadata: + name: my-pv + +spec: + capacity: + storage: 1Gi + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain + hostPath: + path: /tmp/k8s-pv-data diff --git a/2026/day-55/pvc.yaml b/2026/day-55/pvc.yaml new file mode 100644 index 0000000000..eab018fcf1 --- /dev/null +++ b/2026/day-55/pvc.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: PersistentVolumeClaim + +metadata: + name: my-pvc + +spec: + storageClassName: "" + accessModes: + - ReadWriteOnce + + resources: + requests: + storage: 500Mi diff --git a/2026/day-56/Screenshot 2026-07-30 164713.png b/2026/day-56/Screenshot 2026-07-30 164713.png new file mode 100644 index 0000000000..9c296ebede Binary files /dev/null and b/2026/day-56/Screenshot 2026-07-30 164713.png differ diff --git a/2026/day-56/Screenshot 2026-07-30 170042.png b/2026/day-56/Screenshot 2026-07-30 170042.png new file mode 100644 index 0000000000..fa1fadcc02 Binary files /dev/null and b/2026/day-56/Screenshot 2026-07-30 170042.png differ diff --git a/2026/day-56/Screenshot 2026-07-30 170213.png b/2026/day-56/Screenshot 2026-07-30 170213.png new file mode 100644 index 0000000000..9f21968c74 Binary files /dev/null and b/2026/day-56/Screenshot 2026-07-30 170213.png differ diff --git a/2026/day-56/Screenshot 2026-07-30 170320.png b/2026/day-56/Screenshot 2026-07-30 170320.png new file mode 100644 index 0000000000..09e9684cea Binary files /dev/null and b/2026/day-56/Screenshot 2026-07-30 170320.png differ diff --git a/2026/day-56/Screenshot 2026-07-30 170425.png b/2026/day-56/Screenshot 2026-07-30 170425.png new file mode 100644 index 0000000000..2c5c87b3dc Binary files /dev/null and b/2026/day-56/Screenshot 2026-07-30 170425.png differ diff --git a/2026/day-56/day-56-statefulset-simple-notes.md b/2026/day-56/day-56-statefulset-simple-notes.md new file mode 100644 index 0000000000..c37111c93b --- /dev/null +++ b/2026/day-56/day-56-statefulset-simple-notes.md @@ -0,0 +1,113 @@ +# Day 56 - Kubernetes StatefulSet (Simple Notes) + +## What is a StatefulSet? + +A StatefulSet is used for applications that need: - Stable Pod names - +Stable network identity - Stable storage + +Examples: - PostgreSQL - MySQL - MongoDB - Kafka - Cassandra + +## Deployment vs StatefulSet + +Deployment - Random Pod names - Good for stateless applications + +StatefulSet - Fixed Pod names - Good for databases + +Example: - Deployment: demo-app-abc123 - StatefulSet: nginx-0, nginx-1, +nginx-2 + +## Headless Service + +``` yaml +clusterIP: None +``` + +Meaning: - No Cluster IP - DNS record for every Pod + +Examples: - nginx-0.nginx-headless.default.svc.cluster.local - +nginx-1.nginx-headless.default.svc.cluster.local + +## Normal Service + +Client -\> Cluster IP -\> Any matching Pod + +## Headless Service + +Client -\> DNS -\> Specific Pod + +## StatefulSet YAML + +``` yaml +apiVersion: apps/v1 +kind: StatefulSet + +metadata: + name: nginx + +spec: + serviceName: nginx-headless + replicas: 3 + + selector: + matchLabels: + app: nginx + + template: + metadata: + labels: + app: nginx + + spec: + containers: + - name: nginx + image: nginx +``` + +## Commands + +Create: + +``` bash +kubectl apply -f statefulset.yaml +``` + +Watch: + +``` bash +kubectl get pods -w +``` + +Delete: + +``` bash +kubectl delete pod nginx-1 +``` + +## What You Observed + +- Pods: nginx-0, nginx-1, nginx-2 +- Deleting nginx-1 recreates nginx-1 +- Scale to 5 creates nginx-3 and nginx-4 +- Scale to 2 deletes nginx-4, nginx-3 and nginx-2 +- Pods are created in order. +- Pods are deleted in reverse order. + +## Interview Answers + +Why StatefulSet? - Databases need stable Pod names, network identity and +storage. + +Why Headless Service? - It creates DNS names for each Pod instead of one +Cluster IP. + +What happens if nginx-1 is deleted? - Kubernetes recreates nginx-1 with +the same name. + +## Key Points + +- StatefulSet is for stateful applications. +- Pod names stay the same. +- Headless Service uses clusterIP: None. +- DNS connects to a specific Pod. +- Ordered creation. +- Ordered deletion. diff --git a/2026/day-56/day-56.png b/2026/day-56/day-56.png new file mode 100644 index 0000000000..853f940a77 Binary files /dev/null and b/2026/day-56/day-56.png differ diff --git a/2026/day-56/deployment.yaml b/2026/day-56/deployment.yaml new file mode 100644 index 0000000000..75dbd9f6e2 --- /dev/null +++ b/2026/day-56/deployment.yaml @@ -0,0 +1,22 @@ +apiVersion: apps/v1 +kind: Deployment + +metadata: + name: demo-app + +spec: + replicas: 3 + + selector: + matchLabels: + app: demo + + template: + metadata: + labels: + app: demo + + spec: + containers: + - name: nginx + image: nginx diff --git a/2026/day-56/headless-service.yaml b/2026/day-56/headless-service.yaml new file mode 100644 index 0000000000..773f366675 --- /dev/null +++ b/2026/day-56/headless-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service + +metadata: + name: nginx-headless + +spec: + clusterIP: None + + selector: + app: demo + + ports: + - port: 80 + targetPort: 80 diff --git a/2026/day-56/statefulset.yaml b/2026/day-56/statefulset.yaml new file mode 100644 index 0000000000..07812eeb46 --- /dev/null +++ b/2026/day-56/statefulset.yaml @@ -0,0 +1,23 @@ +apiVersion: apps/v1 +kind: StatefulSet + +metadata: + name: nginx + +spec: + serviceName: nginx-headless + replicas: 3 + + selector: + matchLabels: + app: nginx + + template: + metadata: + labels: + app: nginx + + spec: + containers: + - name: nginx + image: nginx diff --git a/2026/day-57/Concept.png b/2026/day-57/Concept.png new file mode 100644 index 0000000000..f7f813536d Binary files /dev/null and b/2026/day-57/Concept.png differ diff --git a/2026/day-57/Screenshot 2026-07-31 135646.png b/2026/day-57/Screenshot 2026-07-31 135646.png new file mode 100644 index 0000000000..a3936f11eb Binary files /dev/null and b/2026/day-57/Screenshot 2026-07-31 135646.png differ diff --git a/2026/day-57/Screenshot 2026-07-31 140012.png b/2026/day-57/Screenshot 2026-07-31 140012.png new file mode 100644 index 0000000000..1e720d92ae Binary files /dev/null and b/2026/day-57/Screenshot 2026-07-31 140012.png differ diff --git a/2026/day-57/Screenshot 2026-07-31 140104.png b/2026/day-57/Screenshot 2026-07-31 140104.png new file mode 100644 index 0000000000..2c8a8ebbd1 Binary files /dev/null and b/2026/day-57/Screenshot 2026-07-31 140104.png differ diff --git a/2026/day-57/Screenshot 2026-07-31 140513.png b/2026/day-57/Screenshot 2026-07-31 140513.png new file mode 100644 index 0000000000..8baccf28ca Binary files /dev/null and b/2026/day-57/Screenshot 2026-07-31 140513.png differ diff --git a/2026/day-57/Screenshot 2026-07-31 140941.png b/2026/day-57/Screenshot 2026-07-31 140941.png new file mode 100644 index 0000000000..381d6274fb Binary files /dev/null and b/2026/day-57/Screenshot 2026-07-31 140941.png differ diff --git a/2026/day-57/Screenshot 2026-07-31 141029.png b/2026/day-57/Screenshot 2026-07-31 141029.png new file mode 100644 index 0000000000..dad2968776 Binary files /dev/null and b/2026/day-57/Screenshot 2026-07-31 141029.png differ diff --git a/2026/day-57/day-57-kubernetes-scheduling-resources-health-probes.md b/2026/day-57/day-57-kubernetes-scheduling-resources-health-probes.md new file mode 100644 index 0000000000..a0b11c6a0c --- /dev/null +++ b/2026/day-57/day-57-kubernetes-scheduling-resources-health-probes.md @@ -0,0 +1,102 @@ +# Day 57 -- Kubernetes Scheduling, Resources & Health Probes + +## What I Learned + +Today I learned how Kubernetes schedules Pods, manages CPU and memory, +and checks application health. + +## 1. Resource Requests + +Requests tell Kubernetes the minimum CPU and memory a Pod needs. +Example: + +``` yaml +resources: + requests: + cpu: "100m" + memory: "128Mi" +``` + +Scheduler uses requests to choose a node. + +## 2. Resource Limits + +Limits tell Kubernetes the maximum CPU and memory a container can use. + +``` yaml +resources: + limits: + cpu: "250m" + memory: "256Mi" +``` + +If memory exceeds the limit, Kubernetes kills the container (OOMKilled). + +## 3. Requests vs Limits + + Requests Limits + ------------------- --------------------- + Minimum resources Maximum resources + Used by Scheduler Enforced by Kubelet + +## 4. QoS Classes + +- Guaranteed: Requests = Limits +- Burstable: Requests and Limits are different +- BestEffort: No requests or limits + +## 5. OOMKilled + +The container uses more memory than its memory limit, so Kubernetes +kills it. + +## 6. Pending Pod + +A Pod stays Pending when no node has enough CPU or memory. Example +message: + + Insufficient cpu + Insufficient memory + +## 7. Liveness Probe + +Checks whether the application is still alive. If it fails repeatedly, +Kubernetes restarts the container. + +## 8. Readiness Probe + +Checks whether the application is ready to receive traffic. If it fails, +the Pod keeps running but the Service does not send traffic to it. + +## 9. Startup Probe + +Used for slow-starting applications. Kubernetes waits for the +application to finish starting before running liveness and readiness +probes. + +## Interview Questions + +### Difference between Requests and Limits? + +- Requests: Minimum resources, used by Scheduler. +- Limits: Maximum resources, enforced by Kubernetes. + +### Difference between Liveness and Readiness? + +- Liveness restarts the container. +- Readiness stops traffic but does not restart the container. + +### When do we use Startup Probe? + +For applications that take a long time to start, such as Java +applications or databases. + +## Summary + +- Scheduler uses Requests. +- Limits prevent overuse. +- OOMKilled happens when memory exceeds the limit. +- Pending Pods cannot be scheduled. +- Liveness restarts unhealthy containers. +- Readiness controls traffic. +- Startup Probe waits until the application is fully started. diff --git a/2026/day-57/liveness-pod.yaml b/2026/day-57/liveness-pod.yaml new file mode 100644 index 0000000000..2dfa42c2ec --- /dev/null +++ b/2026/day-57/liveness-pod.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: Pod + +metadata: + name: liveness-pod + +spec: + containers: + - name: app + image: busybox + + command: + - sh + - -c + - | + touch /tmp/healthy + sleep 30 + rm /tmp/healthy + sleep 3600 + + livenessProbe: + exec: + command: + - cat + - /tmp/healthy + periodSeconds: 5 + failureThreshold: 3 diff --git a/2026/day-57/oom-pod.yaml b/2026/day-57/oom-pod.yaml new file mode 100644 index 0000000000..9afe34a1ac --- /dev/null +++ b/2026/day-57/oom-pod.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Pod + +metadata: + name: oom-pod + +spec: + containers: + - name: stress + image: polinux/stress + + resources: + limits: + memory: 100Mi + + command: + - stress + + args: + - --vm + - "1" + - --vm-bytes + - 200M + - --vm-hang + - "1" + diff --git a/2026/day-57/pending-pod.yaml b/2026/day-57/pending-pod.yaml new file mode 100644 index 0000000000..34383735ac --- /dev/null +++ b/2026/day-57/pending-pod.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Pod +metadata: + name: pending-pod +spec: + containers: + - name: nginx + image: nginx + resources: + requests: + cpu: "100" + memory: 128Gi diff --git a/2026/day-57/readiness-pod.yaml b/2026/day-57/readiness-pod.yaml new file mode 100644 index 0000000000..560674d88f --- /dev/null +++ b/2026/day-57/readiness-pod.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: Pod + +metadata: + name: readiness-pod + +spec: + containers: + - name: app + image: busybox + + command: + - sh + - -c + - | + sleep 20 + touch /tmp/ready + sleep 3600 + + readinessProbe: + exec: + command: + - cat + - /tmp/ready + periodSeconds: 5 diff --git a/2026/day-57/resource-pod.yaml b/2026/day-57/resource-pod.yaml new file mode 100644 index 0000000000..cdb5479398 --- /dev/null +++ b/2026/day-57/resource-pod.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Pod + +metadata: + name: resource-pod + +spec: + containers: + - name: nginx + image: nginx + + resources: + requests: + cpu: 100m + memory: 128Mi + + limits: + cpu: 250m + memory: 256Mi diff --git a/2026/day-57/startup-pod.yaml b/2026/day-57/startup-pod.yaml new file mode 100644 index 0000000000..b5407a3345 --- /dev/null +++ b/2026/day-57/startup-pod.yaml @@ -0,0 +1,26 @@ +apiVersion: v1 +kind: Pod + +metadata: + name: startup-pod + +spec: + containers: + - name: app + image: busybox + + command: + - sh + - -c + - | + sleep 30 + touch /tmp/started + sleep 3600 + + startupProbe: + exec: + command: + - cat + - /tmp/started + periodSeconds: 5 + failureThreshold: 10 diff --git a/2026/day-58/Screenshot 2026-08-04 130430.png b/2026/day-58/Screenshot 2026-08-04 130430.png new file mode 100644 index 0000000000..63686694e9 Binary files /dev/null and b/2026/day-58/Screenshot 2026-08-04 130430.png differ diff --git a/2026/day-58/Screenshot 2026-08-04 132824.png b/2026/day-58/Screenshot 2026-08-04 132824.png new file mode 100644 index 0000000000..0376b2efc2 Binary files /dev/null and b/2026/day-58/Screenshot 2026-08-04 132824.png differ diff --git a/2026/day-58/Screenshot 2026-08-04 135341.png b/2026/day-58/Screenshot 2026-08-04 135341.png new file mode 100644 index 0000000000..ec7a2a54a9 Binary files /dev/null and b/2026/day-58/Screenshot 2026-08-04 135341.png differ diff --git a/2026/day-58/Screenshot 2026-08-04 135446.png b/2026/day-58/Screenshot 2026-08-04 135446.png new file mode 100644 index 0000000000..64e4f2f49a Binary files /dev/null and b/2026/day-58/Screenshot 2026-08-04 135446.png differ diff --git a/2026/day-58/Screenshot 2026-08-04 135756.png b/2026/day-58/Screenshot 2026-08-04 135756.png new file mode 100644 index 0000000000..6dc59e3376 Binary files /dev/null and b/2026/day-58/Screenshot 2026-08-04 135756.png differ diff --git a/2026/day-58/Screenshot 2026-08-04 140033.png b/2026/day-58/Screenshot 2026-08-04 140033.png new file mode 100644 index 0000000000..a64f8f20ab Binary files /dev/null and b/2026/day-58/Screenshot 2026-08-04 140033.png differ diff --git a/2026/day-58/Screenshot 2026-08-04 140426.png b/2026/day-58/Screenshot 2026-08-04 140426.png new file mode 100644 index 0000000000..e41973ee2e Binary files /dev/null and b/2026/day-58/Screenshot 2026-08-04 140426.png differ diff --git a/2026/day-58/day-58-metrics-hpa.md b/2026/day-58/day-58-metrics-hpa.md new file mode 100644 index 0000000000..435d3e828e --- /dev/null +++ b/2026/day-58/day-58-metrics-hpa.md @@ -0,0 +1,128 @@ +# Day 58 -- Metrics Server & Horizontal Pod Autoscaler (HPA) + +## Objective + +Learn how Kubernetes automatically scales applications based on CPU +usage using the Metrics Server and Horizontal Pod Autoscaler (HPA). + +## What I Learned + +### Metrics Server + +- Collects CPU and memory metrics. +- Enables `kubectl top` and HPA. +- HPA cannot work without Metrics Server. + +### kubectl top + +Commands: + +``` bash +kubectl top nodes +kubectl top pods -A +kubectl top pods -A --sort-by=cpu +``` + +- `kubectl top` shows actual resource usage. +- `kubectl describe pod` shows configured requests and limits. + +### Deployment + +Created a Deployment using: - Image: `registry.k8s.io/hpa-example` - CPU +request: `200m` + +``` yaml +resources: + requests: + cpu: 200m +``` + +HPA uses CPU **requests**, not limits. + +### Service + +``` bash +kubectl expose deployment php-apache --port=80 +``` + +### HPA (Imperative) + +``` bash +kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10 +``` + +Target CPU: 50% Minimum Pods: 1 Maximum Pods: 10 + +### Load Test + +``` bash +kubectl run load-generator --image=busybox:1.36 --restart=Never -- /bin/sh -c "while true; do wget -q -O- http://php-apache; done" +``` + +Observed: - CPU usage increased. - HPA scaled Pods automatically. - +Replicas increased from **1 to 8**. + +### HPA (Declarative) + +Used `autoscaling/v2`. + +``` yaml +behavior: + scaleUp: + stabilizationWindowSeconds: 0 + scaleDown: + stabilizationWindowSeconds: 300 +``` + +- Scale up immediately. +- Wait 300 seconds before scaling down. + +## HPA Formula + + desiredReplicas = ceil(currentReplicas × currentUsage / targetUsage) + +## autoscaling/v1 vs autoscaling/v2 + + v1 v2 + ------------- ------------------------------ + CPU only CPU, Memory & Custom Metrics + Basic Advanced behavior + No behavior Supports behavior + +## Important Commands + +``` bash +kubectl top nodes +kubectl top pods -A +kubectl get hpa +kubectl describe hpa php-apache +kubectl get hpa --watch +kubectl delete hpa php-apache +kubectl delete deployment php-apache +kubectl delete service php-apache +kubectl delete pod load-generator +``` + +## Key Takeaways + +- Metrics Server provides metrics. +- HPA needs CPU requests. +- HPA scales automatically. +- `kubectl top` shows live usage. +- `autoscaling/v2` supports advanced scaling behavior. + +## Interview Questions + +1. Why is Metrics Server needed? + - It provides metrics for `kubectl top` and HPA. +2. Does HPA use CPU requests or CPU limits? + - CPU requests. +3. Why did TARGETS show ``? + - Metrics were not yet available. +4. Why didn't HPA scale down immediately? + - Because of the 300-second stabilization window. +5. What did I complete? + - Installed Metrics Server, created Deployment and Service, + configured HPA, generated load, observed automatic scaling from + 1 to 8 Pods, created HPA using `autoscaling/v2`, and cleaned up + the lab. diff --git a/2026/day-58/day-58.png b/2026/day-58/day-58.png new file mode 100644 index 0000000000..36ab453a2e Binary files /dev/null and b/2026/day-58/day-58.png differ diff --git a/2026/day-58/php-apache-deployment.yaml b/2026/day-58/php-apache-deployment.yaml new file mode 100644 index 0000000000..a5dd6ed1ad --- /dev/null +++ b/2026/day-58/php-apache-deployment.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment + +metadata: + name: php-apache + +spec: + replicas: 1 + + selector: + matchLabels: + app: php-apache + + template: + metadata: + labels: + app: php-apache + + spec: + containers: + - name: php-apache + image: registry.k8s.io/hpa-example + + resources: + requests: + cpu: 200m diff --git a/2026/day-58/php-apache-hpa.yaml b/2026/day-58/php-apache-hpa.yaml new file mode 100644 index 0000000000..c83fc81f3f --- /dev/null +++ b/2026/day-58/php-apache-hpa.yaml @@ -0,0 +1,29 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler + +metadata: + name: php-apache + +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: php-apache + + minReplicas: 1 + maxReplicas: 10 + + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 + + behavior: + scaleUp: + stabilizationWindowSeconds: 0 + + scaleDown: + stabilizationWindowSeconds: 300 diff --git a/2026/day-59/Screenshot 2026-08-04 140721.png b/2026/day-59/Screenshot 2026-08-04 140721.png new file mode 100644 index 0000000000..25cafd5225 Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-04 140721.png differ diff --git a/2026/day-59/Screenshot 2026-08-04 161435.png b/2026/day-59/Screenshot 2026-08-04 161435.png new file mode 100644 index 0000000000..59160fe488 Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-04 161435.png differ diff --git a/2026/day-59/Screenshot 2026-08-04 162814.png b/2026/day-59/Screenshot 2026-08-04 162814.png new file mode 100644 index 0000000000..f3cbc352b6 Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-04 162814.png differ diff --git a/2026/day-59/Screenshot 2026-08-06 105604.png b/2026/day-59/Screenshot 2026-08-06 105604.png new file mode 100644 index 0000000000..4f1b650952 Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-06 105604.png differ diff --git a/2026/day-59/Screenshot 2026-08-06 105815.png b/2026/day-59/Screenshot 2026-08-06 105815.png new file mode 100644 index 0000000000..a594c63c9a Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-06 105815.png differ diff --git a/2026/day-59/Screenshot 2026-08-06 110015.png b/2026/day-59/Screenshot 2026-08-06 110015.png new file mode 100644 index 0000000000..28ab42e1ac Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-06 110015.png differ diff --git a/2026/day-59/Screenshot 2026-08-06 110223.png b/2026/day-59/Screenshot 2026-08-06 110223.png new file mode 100644 index 0000000000..e0f6efd729 Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-06 110223.png differ diff --git a/2026/day-59/Screenshot 2026-08-06 110400.png b/2026/day-59/Screenshot 2026-08-06 110400.png new file mode 100644 index 0000000000..d90077f299 Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-06 110400.png differ diff --git a/2026/day-59/Screenshot 2026-08-06 110931.png b/2026/day-59/Screenshot 2026-08-06 110931.png new file mode 100644 index 0000000000..3bd6ef02c2 Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-06 110931.png differ diff --git a/2026/day-59/Screenshot 2026-08-06 111154.png b/2026/day-59/Screenshot 2026-08-06 111154.png new file mode 100644 index 0000000000..e961b9353a Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-06 111154.png differ diff --git a/2026/day-59/Screenshot 2026-08-06 111323.png b/2026/day-59/Screenshot 2026-08-06 111323.png new file mode 100644 index 0000000000..5ef4f42853 Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-06 111323.png differ diff --git a/2026/day-59/Screenshot 2026-08-06 111457.png b/2026/day-59/Screenshot 2026-08-06 111457.png new file mode 100644 index 0000000000..68fbcd9abf Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-06 111457.png differ diff --git a/2026/day-59/Screenshot 2026-08-06 111551.png b/2026/day-59/Screenshot 2026-08-06 111551.png new file mode 100644 index 0000000000..4a8343b27b Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-06 111551.png differ diff --git a/2026/day-59/Screenshot 2026-08-06 111724.png b/2026/day-59/Screenshot 2026-08-06 111724.png new file mode 100644 index 0000000000..cd70a3df06 Binary files /dev/null and b/2026/day-59/Screenshot 2026-08-06 111724.png differ diff --git a/2026/day-59/custom-values.yaml b/2026/day-59/custom-values.yaml new file mode 100644 index 0000000000..64dcfb58ce --- /dev/null +++ b/2026/day-59/custom-values.yaml @@ -0,0 +1,13 @@ +replicaCount: 3 + +service: + type: NodePort + +resources: + requests: + cpu: 100m + memory: 128Mi + + limits: + cpu: 200m + memory: 256Mi diff --git a/2026/day-59/day-59-Helm concept.png b/2026/day-59/day-59-Helm concept.png new file mode 100644 index 0000000000..df535ec59a Binary files /dev/null and b/2026/day-59/day-59-Helm concept.png differ diff --git a/2026/day-59/day-59-helm.md b/2026/day-59/day-59-helm.md new file mode 100644 index 0000000000..bbaaca9b55 --- /dev/null +++ b/2026/day-59/day-59-helm.md @@ -0,0 +1,153 @@ +# Day 59 -- Helm (Kubernetes Package Manager) + +## Objective + +Learn how Helm simplifies Kubernetes deployments by packaging multiple +Kubernetes manifests into reusable charts. + +## What is Helm? + +Helm is the package manager for Kubernetes, similar to apt for Ubuntu. + +### Three Core Concepts + +- **Chart**: A package of Kubernetes templates. +- **Release**: An installed instance of a chart. +- **Repository**: A collection of charts. + +## Commands Practiced + +### Install Helm + +``` bash +helm version +helm env +``` + +### Add Repository + +``` bash +helm repo add bitnami https://charts.bitnami.com/bitnami +helm repo update +helm search repo nginx +helm search repo bitnami +``` + +### Install a Chart + +``` bash +helm install my-nginx oci://registry-1.docker.io/bitnamicharts/nginx +``` + +### Customize with --set + +``` bash +helm install my-nginx-nodeport oci://registry-1.docker.io/bitnamicharts/nginx --set replicaCount=3 --set service.type=NodePort +``` + +### Customize with values.yaml + +``` yaml +replicaCount: 3 + +service: + type: NodePort + +resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 200m + memory: 256Mi +``` + +Install: + +``` bash +helm install my-nginx-custom oci://registry-1.docker.io/bitnamicharts/nginx -f custom-values.yaml +``` + +### Upgrade + +``` bash +helm upgrade my-nginx oci://registry-1.docker.io/bitnamicharts/nginx --set replicaCount=5 +``` + +### Rollback + +``` bash +helm rollback my-nginx 1 +``` + +Rollback creates a new revision instead of replacing the previous one. + +## Create Your Own Chart + +``` bash +helm create my-app +helm lint my-app +helm template my-release ./my-app +helm install my-release ./my-app +helm upgrade my-release ./my-app --set replicaCount=5 +``` + +Chart structure: + +``` text +my-app/ +├── Chart.yaml +├── values.yaml +├── charts/ +└── templates/ +``` + +Go template examples: + +``` text +{{ .Values.replicaCount }} +{{ .Chart.Name }} +{{ .Release.Name }} +``` + +## Cleanup + +``` bash +helm uninstall my-nginx +helm uninstall my-nginx-custom +helm uninstall my-nginx-nodeport +helm uninstall my-release +helm list +``` + +## What I Learned + +- Helm is the package manager for Kubernetes. +- Charts package Kubernetes resources. +- Releases are installed chart instances. +- Repositories store charts. +- values.yaml customizes deployments. +- helm upgrade updates applications. +- helm rollback restores previous revisions. +- helm template renders manifests without installing. +- helm lint validates charts before deployment. + +## Cheat Sheet + +``` bash +helm version +helm env +helm repo add bitnami https://charts.bitnami.com/bitnami +helm repo update +helm search repo nginx +helm install my-nginx oci://registry-1.docker.io/bitnamicharts/nginx +helm list +helm history my-nginx +helm upgrade my-nginx oci://registry-1.docker.io/bitnamicharts/nginx --set replicaCount=5 +helm rollback my-nginx 1 +helm create my-app +helm lint my-app +helm template my-release ./my-app +helm install my-release ./my-app +helm uninstall my-release +``` diff --git a/2026/day-59/my-app/.helmignore b/2026/day-59/my-app/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/2026/day-59/my-app/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/2026/day-59/my-app/Chart.yaml b/2026/day-59/my-app/Chart.yaml new file mode 100644 index 0000000000..f2e9f20317 --- /dev/null +++ b/2026/day-59/my-app/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: my-app +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/2026/day-59/my-app/templates/NOTES.txt b/2026/day-59/my-app/templates/NOTES.txt new file mode 100644 index 0000000000..20289187c0 --- /dev/null +++ b/2026/day-59/my-app/templates/NOTES.txt @@ -0,0 +1,35 @@ +1. Get the application URL by running these commands: +{{- if .Values.httpRoute.enabled }} +{{- if .Values.httpRoute.hostnames }} + export APP_HOSTNAME={{ .Values.httpRoute.hostnames | first }} +{{- else }} + export APP_HOSTNAME=$(kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o jsonpath="{.spec.listeners[0].hostname}") + {{- end }} +{{- if and .Values.httpRoute.rules (first .Values.httpRoute.rules).matches (first (first .Values.httpRoute.rules).matches).path.value }} + echo "Visit http://$APP_HOSTNAME{{ (first (first .Values.httpRoute.rules).matches).path.value }} to use your application" + + NOTE: Your HTTPRoute depends on the listener configuration of your gateway and your HTTPRoute rules. + The rules can be set for path, method, header and query parameters. + You can check the gateway configuration with 'kubectl get --namespace {{(first .Values.httpRoute.parentRefs).namespace | default .Release.Namespace }} gateway/{{ (first .Values.httpRoute.parentRefs).name }} -o yaml' +{{- end }} +{{- else if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "my-app.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "my-app.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "my-app.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "my-app.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/2026/day-59/my-app/templates/_helpers.tpl b/2026/day-59/my-app/templates/_helpers.tpl new file mode 100644 index 0000000000..da4015bf33 --- /dev/null +++ b/2026/day-59/my-app/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "my-app.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "my-app.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "my-app.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "my-app.labels" -}} +helm.sh/chart: {{ include "my-app.chart" . }} +{{ include "my-app.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "my-app.selectorLabels" -}} +app.kubernetes.io/name: {{ include "my-app.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "my-app.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "my-app.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/2026/day-59/my-app/templates/deployment.yaml b/2026/day-59/my-app/templates/deployment.yaml new file mode 100644 index 0000000000..4921ff349b --- /dev/null +++ b/2026/day-59/my-app/templates/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "my-app.fullname" . }} + labels: + {{- include "my-app.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "my-app.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "my-app.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "my-app.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/2026/day-59/my-app/templates/hpa.yaml b/2026/day-59/my-app/templates/hpa.yaml new file mode 100644 index 0000000000..b87d33975a --- /dev/null +++ b/2026/day-59/my-app/templates/hpa.yaml @@ -0,0 +1,32 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "my-app.fullname" . }} + labels: + {{- include "my-app.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "my-app.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/2026/day-59/my-app/templates/httproute.yaml b/2026/day-59/my-app/templates/httproute.yaml new file mode 100644 index 0000000000..f457a4bdcd --- /dev/null +++ b/2026/day-59/my-app/templates/httproute.yaml @@ -0,0 +1,38 @@ +{{- if .Values.httpRoute.enabled -}} +{{- $fullName := include "my-app.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: {{ $fullName }} + labels: + {{- include "my-app.labels" . | nindent 4 }} + {{- with .Values.httpRoute.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + parentRefs: + {{- with .Values.httpRoute.parentRefs }} + {{- toYaml . | nindent 4 }} + {{- end }} + {{- with .Values.httpRoute.hostnames }} + hostnames: + {{- toYaml . | nindent 4 }} + {{- end }} + rules: + {{- range .Values.httpRoute.rules }} + {{- with .matches }} + - matches: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .filters }} + filters: + {{- toYaml . | nindent 8 }} + {{- end }} + backendRefs: + - name: {{ $fullName }} + port: {{ $svcPort }} + weight: 1 + {{- end }} +{{- end }} diff --git a/2026/day-59/my-app/templates/ingress.yaml b/2026/day-59/my-app/templates/ingress.yaml new file mode 100644 index 0000000000..094732d609 --- /dev/null +++ b/2026/day-59/my-app/templates/ingress.yaml @@ -0,0 +1,43 @@ +{{- if .Values.ingress.enabled -}} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "my-app.fullname" . }} + labels: + {{- include "my-app.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- with .Values.ingress.className }} + ingressClassName: {{ . }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- with .pathType }} + pathType: {{ . }} + {{- end }} + backend: + service: + name: {{ include "my-app.fullname" $ }} + port: + number: {{ $.Values.service.port }} + {{- end }} + {{- end }} +{{- end }} diff --git a/2026/day-59/my-app/templates/service.yaml b/2026/day-59/my-app/templates/service.yaml new file mode 100644 index 0000000000..2c92457760 --- /dev/null +++ b/2026/day-59/my-app/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "my-app.fullname" . }} + labels: + {{- include "my-app.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "my-app.selectorLabels" . | nindent 4 }} diff --git a/2026/day-59/my-app/templates/serviceaccount.yaml b/2026/day-59/my-app/templates/serviceaccount.yaml new file mode 100644 index 0000000000..af75921008 --- /dev/null +++ b/2026/day-59/my-app/templates/serviceaccount.yaml @@ -0,0 +1,13 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "my-app.serviceAccountName" . }} + labels: + {{- include "my-app.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.serviceAccount.automount }} +{{- end }} diff --git a/2026/day-59/my-app/templates/tests/test-connection.yaml b/2026/day-59/my-app/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..9e68b40388 --- /dev/null +++ b/2026/day-59/my-app/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "my-app.fullname" . }}-test-connection" + labels: + {{- include "my-app.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "my-app.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/2026/day-59/my-app/values.yaml b/2026/day-59/my-app/values.yaml new file mode 100644 index 0000000000..6ef8aa52a7 --- /dev/null +++ b/2026/day-59/my-app/values.yaml @@ -0,0 +1,161 @@ +# Default values for my-app. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 3 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: nginx + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "1.25" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created. + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account. + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template. + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 80 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +# -- Expose the service via gateway-api HTTPRoute +# Requires Gateway API resources and suitable controller installed within the cluster +# (see: https://gateway-api.sigs.k8s.io/guides/) +httpRoute: + # HTTPRoute enabled. + enabled: false + # HTTPRoute annotations. + annotations: {} + # Which Gateways this Route is attached to. + parentRefs: + - name: gateway + sectionName: http + # namespace: default + # Hostnames matching HTTP header. + hostnames: + - chart-example.local + # List of rules and filters applied. + rules: + - matches: + - path: + type: PathPrefix + value: /headers + # filters: + # - type: RequestHeaderModifier + # requestHeaderModifier: + # set: + # - name: My-Overwrite-Header + # value: this-is-the-only-value + # remove: + # - User-Agent + # - matches: + # - path: + # type: PathPrefix + # value: /echo + # headers: + # - name: version + # value: v2 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] + # - name: foo + # secret: + # secretName: mysecret + # optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] + # - name: foo + # mountPath: "/etc/foo" + # readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/2026/day-60/Screenshot 2026-08-06 121527.png b/2026/day-60/Screenshot 2026-08-06 121527.png new file mode 100644 index 0000000000..31ea8ab626 Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 121527.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 121827.png b/2026/day-60/Screenshot 2026-08-06 121827.png new file mode 100644 index 0000000000..f6079da949 Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 121827.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 122402.png b/2026/day-60/Screenshot 2026-08-06 122402.png new file mode 100644 index 0000000000..779d0e0072 Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 122402.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 122813.png b/2026/day-60/Screenshot 2026-08-06 122813.png new file mode 100644 index 0000000000..1c1cab4e79 Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 122813.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 123022.png b/2026/day-60/Screenshot 2026-08-06 123022.png new file mode 100644 index 0000000000..387a506e17 Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 123022.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 123537.png b/2026/day-60/Screenshot 2026-08-06 123537.png new file mode 100644 index 0000000000..4945ddfce1 Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 123537.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 123642.png b/2026/day-60/Screenshot 2026-08-06 123642.png new file mode 100644 index 0000000000..8dc602123b Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 123642.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 124036.png b/2026/day-60/Screenshot 2026-08-06 124036.png new file mode 100644 index 0000000000..5c5dc7e144 Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 124036.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 124106.png b/2026/day-60/Screenshot 2026-08-06 124106.png new file mode 100644 index 0000000000..5316fd403c Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 124106.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 124355.png b/2026/day-60/Screenshot 2026-08-06 124355.png new file mode 100644 index 0000000000..a3ce8d56a3 Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 124355.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 124446.png b/2026/day-60/Screenshot 2026-08-06 124446.png new file mode 100644 index 0000000000..137e541902 Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 124446.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 124807.png b/2026/day-60/Screenshot 2026-08-06 124807.png new file mode 100644 index 0000000000..1f2f553809 Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 124807.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 124901.png b/2026/day-60/Screenshot 2026-08-06 124901.png new file mode 100644 index 0000000000..6c269d8776 Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 124901.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 124923.png b/2026/day-60/Screenshot 2026-08-06 124923.png new file mode 100644 index 0000000000..a5751439bf Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 124923.png differ diff --git a/2026/day-60/Screenshot 2026-08-06 125138.png b/2026/day-60/Screenshot 2026-08-06 125138.png new file mode 100644 index 0000000000..e422ae5777 Binary files /dev/null and b/2026/day-60/Screenshot 2026-08-06 125138.png differ diff --git a/2026/day-60/day-60-capstone.md b/2026/day-60/day-60-capstone.md new file mode 100644 index 0000000000..1f14419869 --- /dev/null +++ b/2026/day-60/day-60-capstone.md @@ -0,0 +1,123 @@ +# Day 60 – Kubernetes Capstone (WordPress + MySQL) + +## Objective +Deploy a real-world **WordPress + MySQL** application on Kubernetes using the core concepts learned during Days 52–60. + +## Architecture + +```text +Browser + | +kubectl port-forward + | +WordPress NodePort Service + | +WordPress Deployment (2 Replicas) + | +ConfigMap + Secret + | +MySQL Headless Service + | +MySQL StatefulSet + | +Persistent Volume Claim (1Gi) +``` + +## Kubernetes Resources Used + +- Namespace +- Secret +- ConfigMap +- Headless Service +- StatefulSet +- Persistent Volume Claim (PVC) +- Deployment +- NodePort Service +- Resource Requests & Limits +- Liveness Probe +- Readiness Probe +- Horizontal Pod Autoscaler (HPA) + +## Implementation Summary + +### Task 1 – Namespace +- Created `capstone` namespace. +- Set it as the default namespace. + +### Task 2 – MySQL +- Created MySQL Secret. +- Created Headless Service. +- Deployed MySQL StatefulSet. +- Attached a 1Gi PVC. +- Verified the `wordpress` database using: +```bash +kubectl exec -it mysql-0 -- mysql -u wpuser -pwordpress123 -e "SHOW DATABASES;" +``` + +### Task 3 – WordPress +- Created ConfigMap. +- Created Deployment with 2 replicas. +- Added CPU/Memory requests and limits. +- Configured liveness and readiness probes. + +### Task 4 – Expose Application +- Created NodePort Service. +- Accessed WordPress with: +```bash +kubectl port-forward svc/wordpress 8080:80 +``` +- Completed the WordPress installation. + +### Task 5 – Self-Healing & Persistence +- Deleted a WordPress pod and Kubernetes recreated it automatically. +- Deleted the MySQL pod and StatefulSet recreated it. +- PVC remained bound and data persisted. + +### Task 6 – HPA +Configured: +- Min Pods: 2 +- Max Pods: 10 +- CPU Target: 50% + +Verified: +``` +cpu: 1% / 50% +``` + +## Concept Mapping + +| Concept | Day | +|---|---:| +| Namespace | 52 | +| Deployment | 52 | +| Service | 53 | +| Secret | 54 | +| ConfigMap | 54 | +| Persistent Volume | 55 | +| StatefulSet | 56 | +| Resource Limits | 57 | +| Liveness & Readiness Probes | 57 | +| HPA | 58 | +| Helm | 59 | +| Capstone | 60 | + +## Results + +- Namespace created successfully. +- MySQL StatefulSet running. +- PVC Bound. +- WordPress running with two replicas. +- NodePort Service working. +- WordPress installed successfully. +- Blog post created. +- Self-healing verified. +- Persistent storage verified. +- HPA configured successfully. + +## Reflection + +This capstone project combined all major Kubernetes concepts into one real-world application. I learned how Deployments, StatefulSets, Services, ConfigMaps, Secrets, PVCs, probes, and autoscaling work together. The most valuable lesson was seeing Kubernetes automatically recover failed pods while preserving application data through persistent storage. + +## Conclusion + +The WordPress + MySQL application was successfully deployed and tested using Kubernetes best practices including StatefulSets, Deployments, Secrets, ConfigMaps, Services, Persistent Storage, Health Probes, Resource Management, and Horizontal Pod Autoscaling. diff --git a/2026/day-60/day-60-concept.png b/2026/day-60/day-60-concept.png new file mode 100644 index 0000000000..cb76593cf5 Binary files /dev/null and b/2026/day-60/day-60-concept.png differ diff --git a/2026/day-60/mysql-headless-service.yaml b/2026/day-60/mysql-headless-service.yaml new file mode 100644 index 0000000000..97b902ad3d --- /dev/null +++ b/2026/day-60/mysql-headless-service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service + +metadata: + name: mysql + +spec: + clusterIP: None + + selector: + app: mysql + + ports: + - port: 3306 + targetPort: 3306 diff --git a/2026/day-60/mysql-secret.yaml b/2026/day-60/mysql-secret.yaml new file mode 100644 index 0000000000..71c67132b4 --- /dev/null +++ b/2026/day-60/mysql-secret.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: Secret +metadata: + name: mysql-secret +type: Opaque + +stringData: + MYSQL_ROOT_PASSWORD: root123 + MYSQL_DATABASE: wordpress + MYSQL_USER: wpuser + MYSQL_PASSWORD: wordpress123 diff --git a/2026/day-60/mysql-statefulset.yaml b/2026/day-60/mysql-statefulset.yaml new file mode 100644 index 0000000000..f59ca6fc7d --- /dev/null +++ b/2026/day-60/mysql-statefulset.yaml @@ -0,0 +1,54 @@ +apiVersion: apps/v1 +kind: StatefulSet + +metadata: + name: mysql + +spec: + serviceName: mysql + replicas: 1 + + selector: + matchLabels: + app: mysql + + template: + metadata: + labels: + app: mysql + + spec: + containers: + - name: mysql + image: mysql:8.0 + + envFrom: + - secretRef: + name: mysql-secret + + ports: + - containerPort: 3306 + + resources: + requests: + cpu: "250m" + memory: "512Mi" + limits: + cpu: "500m" + memory: "1Gi" + + volumeMounts: + - name: mysql-storage + mountPath: /var/lib/mysql + + volumeClaimTemplates: + - metadata: + name: mysql-storage + + spec: + accessModes: + - ReadWriteOnce + + resources: + requests: + storage: 1Gi diff --git a/2026/day-60/wordpress-configmap.yaml b/2026/day-60/wordpress-configmap.yaml new file mode 100644 index 0000000000..32c1c29ced --- /dev/null +++ b/2026/day-60/wordpress-configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap + +metadata: + name: wordpress-config + +data: + WORDPRESS_DB_HOST: mysql-0.mysql.capstone.svc.cluster.local:3306 + WORDPRESS_DB_NAME: wordpress diff --git a/2026/day-60/wordpress-deployment.yaml b/2026/day-60/wordpress-deployment.yaml new file mode 100644 index 0000000000..00b270bbb2 --- /dev/null +++ b/2026/day-60/wordpress-deployment.yaml @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: Deployment + +metadata: + name: wordpress + +spec: + replicas: 2 + + selector: + matchLabels: + app: wordpress + + template: + metadata: + labels: + app: wordpress + + spec: + containers: + - name: wordpress + image: wordpress:latest + + ports: + - containerPort: 80 + + envFrom: + - configMapRef: + name: wordpress-config + + env: + - name: WORDPRESS_DB_USER + valueFrom: + secretKeyRef: + name: mysql-secret + key: MYSQL_USER + + - name: WORDPRESS_DB_PASSWORD + valueFrom: + secretKeyRef: + name: mysql-secret + key: MYSQL_PASSWORD + + resources: + requests: + cpu: "250m" + memory: "256Mi" + limits: + cpu: "500m" + memory: "512Mi" + + livenessProbe: + httpGet: + path: /wp-login.php + port: 80 + initialDelaySeconds: 60 + periodSeconds: 10 + + readinessProbe: + httpGet: + path: /wp-login.php + port: 80 + initialDelaySeconds: 30 + periodSeconds: 10 diff --git a/2026/day-60/wordpress-hpa.yaml b/2026/day-60/wordpress-hpa.yaml new file mode 100644 index 0000000000..245d26676b --- /dev/null +++ b/2026/day-60/wordpress-hpa.yaml @@ -0,0 +1,18 @@ +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: wordpress-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: wordpress + minReplicas: 2 + maxReplicas: 10 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 50 diff --git a/2026/day-60/wordpress-service.yaml b/2026/day-60/wordpress-service.yaml new file mode 100644 index 0000000000..c1902360cd --- /dev/null +++ b/2026/day-60/wordpress-service.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Service +metadata: + name: wordpress +spec: + type: NodePort + selector: + app: wordpress + ports: + - port: 80 + targetPort: 80 + nodePort: 30080 diff --git a/2026/day-61/Screenshot 2026-08-14 140221.png b/2026/day-61/Screenshot 2026-08-14 140221.png new file mode 100644 index 0000000000..85e603b8b7 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 140221.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 140447.png b/2026/day-61/Screenshot 2026-08-14 140447.png new file mode 100644 index 0000000000..2408da38f8 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 140447.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 140610.png b/2026/day-61/Screenshot 2026-08-14 140610.png new file mode 100644 index 0000000000..b57998005d Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 140610.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 141057.png b/2026/day-61/Screenshot 2026-08-14 141057.png new file mode 100644 index 0000000000..3e854f6e13 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 141057.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 141231.png b/2026/day-61/Screenshot 2026-08-14 141231.png new file mode 100644 index 0000000000..899fe1ae0d Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 141231.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 141443.png b/2026/day-61/Screenshot 2026-08-14 141443.png new file mode 100644 index 0000000000..33dd2d8e5e Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 141443.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 142049.png b/2026/day-61/Screenshot 2026-08-14 142049.png new file mode 100644 index 0000000000..164580dde6 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 142049.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 142231.png b/2026/day-61/Screenshot 2026-08-14 142231.png new file mode 100644 index 0000000000..2a2599384f Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 142231.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 142406.png b/2026/day-61/Screenshot 2026-08-14 142406.png new file mode 100644 index 0000000000..4b130e69a9 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 142406.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 142711.png b/2026/day-61/Screenshot 2026-08-14 142711.png new file mode 100644 index 0000000000..d3828ea5a5 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 142711.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 142851.png b/2026/day-61/Screenshot 2026-08-14 142851.png new file mode 100644 index 0000000000..d31570cdae Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 142851.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 143214.png b/2026/day-61/Screenshot 2026-08-14 143214.png new file mode 100644 index 0000000000..d65a242d0c Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 143214.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 143333.png b/2026/day-61/Screenshot 2026-08-14 143333.png new file mode 100644 index 0000000000..cb0011f2df Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 143333.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 143447.png b/2026/day-61/Screenshot 2026-08-14 143447.png new file mode 100644 index 0000000000..b5413f8a4b Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 143447.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 143710.png b/2026/day-61/Screenshot 2026-08-14 143710.png new file mode 100644 index 0000000000..bd29f945b1 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 143710.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 143924.png b/2026/day-61/Screenshot 2026-08-14 143924.png new file mode 100644 index 0000000000..7cb283b0be Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 143924.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 144101.png b/2026/day-61/Screenshot 2026-08-14 144101.png new file mode 100644 index 0000000000..f881269b69 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 144101.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 144426.png b/2026/day-61/Screenshot 2026-08-14 144426.png new file mode 100644 index 0000000000..912e212c51 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 144426.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 144537.png b/2026/day-61/Screenshot 2026-08-14 144537.png new file mode 100644 index 0000000000..e3e95694c7 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 144537.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 144651.png b/2026/day-61/Screenshot 2026-08-14 144651.png new file mode 100644 index 0000000000..2cb087afc7 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 144651.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 144753.png b/2026/day-61/Screenshot 2026-08-14 144753.png new file mode 100644 index 0000000000..d7045dc230 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 144753.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 144942.png b/2026/day-61/Screenshot 2026-08-14 144942.png new file mode 100644 index 0000000000..9e12f3f89a Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 144942.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 145006.png b/2026/day-61/Screenshot 2026-08-14 145006.png new file mode 100644 index 0000000000..91cb4e338a Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 145006.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 145027.png b/2026/day-61/Screenshot 2026-08-14 145027.png new file mode 100644 index 0000000000..879909f8e5 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 145027.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 145206.png b/2026/day-61/Screenshot 2026-08-14 145206.png new file mode 100644 index 0000000000..d26efb9e8b Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 145206.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 145604.png b/2026/day-61/Screenshot 2026-08-14 145604.png new file mode 100644 index 0000000000..40a8c9fc9d Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 145604.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 145732.png b/2026/day-61/Screenshot 2026-08-14 145732.png new file mode 100644 index 0000000000..2cfbfd4f29 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 145732.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 145824.png b/2026/day-61/Screenshot 2026-08-14 145824.png new file mode 100644 index 0000000000..3c8cf78446 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 145824.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 145947.png b/2026/day-61/Screenshot 2026-08-14 145947.png new file mode 100644 index 0000000000..2f46e1517d Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 145947.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 145958.png b/2026/day-61/Screenshot 2026-08-14 145958.png new file mode 100644 index 0000000000..d0b07f87d6 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 145958.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 150007.png b/2026/day-61/Screenshot 2026-08-14 150007.png new file mode 100644 index 0000000000..87fbd24690 Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 150007.png differ diff --git a/2026/day-61/Screenshot 2026-08-14 150024.png b/2026/day-61/Screenshot 2026-08-14 150024.png new file mode 100644 index 0000000000..ab19d12a8a Binary files /dev/null and b/2026/day-61/Screenshot 2026-08-14 150024.png differ diff --git a/2026/day-61/day-61-terraform-intro.md b/2026/day-61/day-61-terraform-intro.md new file mode 100644 index 0000000000..10a6239b70 --- /dev/null +++ b/2026/day-61/day-61-terraform-intro.md @@ -0,0 +1,322 @@ +# Day 61 - Introduction to Terraform and My First AWS Infrastructure + +## What I Learned + +Today I started learning Terraform and Infrastructure as Code (IaC). I +used Terraform with AWS to create an S3 bucket and an EC2 instance. I +also learned how Terraform keeps track of resources using the state +file. Finally, I modified my EC2 instance tag and destroyed the +resources using Terraform. + +## Task 1 - Infrastructure as Code (IaC) + +### What is Infrastructure as Code? + +Infrastructure as Code means creating and managing infrastructure by +writing code instead of creating everything manually in the AWS Console. + +For example, instead of manually creating an EC2 instance or S3 bucket, +I can describe them in a Terraform file and Terraform creates them for +me. + +IaC is useful in DevOps because infrastructure can be repeated, +reviewed, shared, and automated. + +### What problems does IaC solve? + +IaC helps us create infrastructure faster, repeat the same setup, reduce +manual mistakes, review changes before applying them, and recreate +environments when needed. + +### Terraform vs Other Tools + +- **Terraform:** Mainly used to provision infrastructure and works + with many cloud providers. +- **AWS CloudFormation:** Infrastructure as Code mainly for AWS. +- **Ansible:** Mainly used for configuration management and + automation. +- **Pulumi:** Lets us define infrastructure using programming + languages. + +### Declarative and cloud-agnostic + +Terraform is **declarative** because I describe the infrastructure I +want, and Terraform decides what actions are needed. + +Terraform is **cloud-agnostic** because it can work with different +providers such as AWS, Azure, and Google Cloud. + +## Task 2 - Terraform and AWS CLI Setup + +I verified Terraform: + +``` bash +terraform --version +``` + +My version was: + +``` text +Terraform v1.15.8 +on linux_amd64 +``` + +I verified AWS CLI with: + +``` bash +aws --version +aws sts get-caller-identity +``` + +The AWS identity command returned my IAM identity, confirming that my +AWS credentials were working. + +## Task 3 - Create an S3 Bucket + +I created my project directory: + +``` bash +mkdir terraform-basics +cd terraform-basics +``` + +My `main.tf` contained the AWS provider and S3 bucket: + +``` hcl +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + } + } +} + +provider "aws" { + region = "ca-central-1" +} + +resource "aws_s3_bucket" "day61_bucket_asma" { + bucket = "asma-terraweek-day61-2026" +} +``` + +I ran: + +``` bash +terraform fmt +terraform init +terraform plan +terraform apply +``` + +`terraform init` downloaded the AWS provider. In my lab, Terraform +installed `hashicorp/aws v6.60.0`. + +The `.terraform/` directory contains downloaded provider files that +Terraform needs to work with AWS. + +Terraform also created `.terraform.lock.hcl`, which records the selected +provider versions. + +The S3 bucket was created successfully. + +## Task 4 - Create an EC2 Instance + +I found an Amazon Linux AMI for my AWS region, `ca-central-1`, and added +an EC2 resource: + +``` hcl +resource "aws_instance" "day61_ec2" { + ami = "ami-0f82f408f5558e47b" + instance_type = "t2.micro" + + tags = { + Name = "TerraWeek-Day1" + } +} +``` + +Then I ran: + +``` bash +terraform fmt +terraform plan +terraform apply +``` + +Terraform showed: + +``` text +Plan: 1 to add, 0 to change, 0 to destroy. +``` + +Only the EC2 instance needed to be created because Terraform already +knew about the S3 bucket from its state. + +## Task 5 - Understanding Terraform State + +Terraform uses `terraform.tfstate` to keep track of the resources it +manages. + +I practiced: + +``` bash +terraform show +terraform state list +terraform state show aws_s3_bucket.day61_bucket_asma +terraform state show aws_instance.day61_ec2 +``` + +After creating both resources, `terraform state list` showed: + +``` text +aws_instance.day61_ec2 +aws_s3_bucket.day61_bucket_asma +``` + +The state stores information such as resource IDs, names, ARNs, region, +tags, network information, and other resource attributes. + +I should not manually edit the state file because Terraform depends on +it to understand the real infrastructure. + +State files should not normally be committed to a public Git repository +because they may contain infrastructure details or sensitive values. + +## Task 6 - Modify the EC2 Instance + +I changed the EC2 Name tag from: + +``` text +TerraWeek-Day1 +``` + +to: + +``` text +TerraWeek-Modified +``` + +Then I ran: + +``` bash +terraform plan +``` + +Terraform showed: + +``` text +Plan: 0 to add, 1 to change, 0 to destroy. +``` + +It also showed `~ update in-place`, meaning Terraform could change the +tag without replacing the EC2 instance. + +I ran: + +``` bash +terraform apply +``` + +The result was: + +``` text +Apply complete! Resources: 0 added, 1 changed, 0 destroyed. +``` + +I verified the EC2 tag with AWS CLI and confirmed: + +``` text +Name TerraWeek-Modified +``` + +### Terraform plan symbols + +- `+` = create a resource +- `~` = modify an existing resource +- `-` = destroy a resource + +## Destroying the Infrastructure + +Finally, I ran: + +``` bash +terraform destroy +``` + +Terraform showed: + +``` text +Plan: 0 to add, 0 to change, 2 to destroy. +``` + +After I entered `yes`, Terraform destroyed the S3 bucket and EC2 +instance. + +``` text +Destroy complete! Resources: 2 destroyed. +``` + +## Terraform Commands I Practiced + + Command What it does + ------------------------ ------------------------------------------------- + `terraform init` Initializes the project and downloads providers + `terraform fmt` Formats Terraform code + `terraform validate` Checks the Terraform configuration + `terraform plan` Previews infrastructure changes + `terraform apply` Creates or changes infrastructure + `terraform show` Shows the current state in readable form + `terraform state list` Lists resources Terraform manages + `terraform state show` Shows details for one managed resource + `terraform destroy` Removes Terraform-managed resources + +## Screenshots + +I will add my screenshots here before pushing to GitHub. + +### S3 Bucket Created + +![S3 Apply](./screenshots/terraform-s3-apply.png) + +### EC2 Instance Created + +![EC2 Apply](./screenshots/terraform-ec2-apply.png) + +### EC2 Tag Modified + +![Terraform Modify](./screenshots/terraform-modify.png) + +### Resources Destroyed + +![Terraform Destroy](./screenshots/terraform-destroy.png) + +## `.gitignore` + +I should add these local Terraform files to `.gitignore`: + +``` gitignore +.terraform/ +*.tfstate +*.tfstate.* +``` + +I can keep `.terraform.lock.hcl` in Git so the provider dependency +selections remain consistent. + +## My Day 61 Takeaway + +Today I learned the basic Terraform workflow: + +**Write configuration -\> Init -\> Plan -\> Apply -\> Check State -\> +Modify -\> Apply -\> Destroy** + +The most important thing I learned is that Terraform remembers the +resources it manages through its state file. When I added EC2 after +creating the S3 bucket, Terraform knew the bucket already existed and +only created the EC2 instance. + +I also learned that Terraform can update some resources in place without +destroying and recreating them. + +**Day 61 completed!** diff --git a/2026/day-61/terraform-61-Day.png b/2026/day-61/terraform-61-Day.png new file mode 100644 index 0000000000..ed6a66c202 Binary files /dev/null and b/2026/day-61/terraform-61-Day.png differ diff --git a/2026/day-61/terraform-basics/.terraform.lock.hcl b/2026/day-61/terraform-basics/.terraform.lock.hcl new file mode 100644 index 0000000000..322c0a5320 --- /dev/null +++ b/2026/day-61/terraform-basics/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.60.0" + hashes = [ + "h1:VF6oe4urgR2lRZuCAytMHvUZHtqcZU99TGw915LdCL0=", + "zh:1a29660d9038c38e9e306194bd3e7de7017148f3491af7fdfa434d7d8c82b3d4", + "zh:268d711a0a9c18459e09a837405244e5d459bb4ebb5d34a91c4fcbabe3e25d26", + "zh:2d7058c285484e7cccf4f5b1973c10b404ae77532f77ea95115915dd341fbf5b", + "zh:2e1cddc362497b569ba528f013d0cf770465047b8185e8180f4ccabafa082577", + "zh:5210c99d5946dd4985a6cff7460422e257037679b35df0a54982e3f675d1a8d5", + "zh:60703aa30b23ed5e5cfabf2614aeb465cf2aa4b848eb8afb2723b6b9d358ccd0", + "zh:904f0651e9bfbf08b16f4d13c85e7841c2d1b5a670087cb11f8d4ad06249cf0c", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:b4268c98208f725ea28afb38dde6cb4b7536de577c36ee919fa44c3d8d5faf70", + "zh:caf78d1713cc323383facae063c61584758ea697428f328600227902f3a9980f", + "zh:cb349f8a43653bfc78867909d6342c56073f9805bdbd9dc32a8e1d18302257d4", + "zh:e087e5e6e3130980838552022e022a62a52a851cf93a0029cb5d410a34f6106b", + "zh:e94318f9ebf29d06aa821da73a7fc7a637d7d13e535e40585c6242cea4477262", + "zh:effc513566df2f5b5921a5df884817b1de57790db1c485eece566c5d4c55c4ee", + "zh:f2be12b976d10a91289835056ff962721e65f47f8e52b368211c4bd3ce9e20e6", + "zh:fb7ef7e405b05789cec32c8ea06a353789c6e3a336b876508cf2ac98984f0558", + ] +} diff --git a/2026/day-61/terraform-basics/main.tf b/2026/day-61/terraform-basics/main.tf new file mode 100644 index 0000000000..2a3b2437b2 --- /dev/null +++ b/2026/day-61/terraform-basics/main.tf @@ -0,0 +1,21 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + } + } +} +provider "aws" { + region = "ca-central-1" +} +resource "aws_s3_bucket" "day61_bucket_asma" { + bucket = "asma-terraweek-day61-2026" +} +resource "aws_instance" "day61_ec2" { + ami = "ami-0f82f408f5558e47b" + instance_type = "t2.micro" + + tags = { + Name = "TerraWeek-Modified" + } +} diff --git a/2026/day-62/.terraform.lock.hcl b/2026/day-62/.terraform.lock.hcl new file mode 100644 index 0000000000..39a6fbd36d --- /dev/null +++ b/2026/day-62/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:edXOJWE4ORX8Fm+dpVpICzMZJat4AX0VRCAy/xkcOc0=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/2026/day-62/Day-62-concept.png b/2026/day-62/Day-62-concept.png new file mode 100644 index 0000000000..7b27ab94f8 Binary files /dev/null and b/2026/day-62/Day-62-concept.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 121148.png b/2026/day-62/Screenshot 2026-08-17 121148.png new file mode 100644 index 0000000000..dab9198683 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 121148.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 121306.png b/2026/day-62/Screenshot 2026-08-17 121306.png new file mode 100644 index 0000000000..04d2c35000 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 121306.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 121604.png b/2026/day-62/Screenshot 2026-08-17 121604.png new file mode 100644 index 0000000000..18e470531d Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 121604.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 123229.png b/2026/day-62/Screenshot 2026-08-17 123229.png new file mode 100644 index 0000000000..e0e6f101d7 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 123229.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 123607.png b/2026/day-62/Screenshot 2026-08-17 123607.png new file mode 100644 index 0000000000..a627fd4aa0 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 123607.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 123808.png b/2026/day-62/Screenshot 2026-08-17 123808.png new file mode 100644 index 0000000000..fb911e544c Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 123808.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 124004.png b/2026/day-62/Screenshot 2026-08-17 124004.png new file mode 100644 index 0000000000..1e2bd6143b Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 124004.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 124022.png b/2026/day-62/Screenshot 2026-08-17 124022.png new file mode 100644 index 0000000000..eb9709897d Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 124022.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 124035.png b/2026/day-62/Screenshot 2026-08-17 124035.png new file mode 100644 index 0000000000..c8733fca5f Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 124035.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 124200.png b/2026/day-62/Screenshot 2026-08-17 124200.png new file mode 100644 index 0000000000..5dbff4dbd2 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 124200.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 124449.png b/2026/day-62/Screenshot 2026-08-17 124449.png new file mode 100644 index 0000000000..ac537a9c77 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 124449.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 124648.png b/2026/day-62/Screenshot 2026-08-17 124648.png new file mode 100644 index 0000000000..c0dc3fae39 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 124648.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 124739.png b/2026/day-62/Screenshot 2026-08-17 124739.png new file mode 100644 index 0000000000..e573eac3e2 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 124739.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 124912.png b/2026/day-62/Screenshot 2026-08-17 124912.png new file mode 100644 index 0000000000..c91db4b5d4 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 124912.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 125003.png b/2026/day-62/Screenshot 2026-08-17 125003.png new file mode 100644 index 0000000000..01e76370bd Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 125003.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 125129.png b/2026/day-62/Screenshot 2026-08-17 125129.png new file mode 100644 index 0000000000..632e78d834 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 125129.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 125243.png b/2026/day-62/Screenshot 2026-08-17 125243.png new file mode 100644 index 0000000000..cf475a765d Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 125243.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 125929.png b/2026/day-62/Screenshot 2026-08-17 125929.png new file mode 100644 index 0000000000..9dc5ff2958 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 125929.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 130010.png b/2026/day-62/Screenshot 2026-08-17 130010.png new file mode 100644 index 0000000000..096626c48e Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 130010.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 130138.png b/2026/day-62/Screenshot 2026-08-17 130138.png new file mode 100644 index 0000000000..45c2748957 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 130138.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 130154.png b/2026/day-62/Screenshot 2026-08-17 130154.png new file mode 100644 index 0000000000..d63307d069 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 130154.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 130426.png b/2026/day-62/Screenshot 2026-08-17 130426.png new file mode 100644 index 0000000000..593aa6ebc6 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 130426.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 130543.png b/2026/day-62/Screenshot 2026-08-17 130543.png new file mode 100644 index 0000000000..6d00928eb9 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 130543.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 130652.png b/2026/day-62/Screenshot 2026-08-17 130652.png new file mode 100644 index 0000000000..d7a4d2d581 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 130652.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 130908.png b/2026/day-62/Screenshot 2026-08-17 130908.png new file mode 100644 index 0000000000..11aa076c1d Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 130908.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 131319.png b/2026/day-62/Screenshot 2026-08-17 131319.png new file mode 100644 index 0000000000..7df1d5d70d Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 131319.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 133719.png b/2026/day-62/Screenshot 2026-08-17 133719.png new file mode 100644 index 0000000000..0e8ac56490 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 133719.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 133809.png b/2026/day-62/Screenshot 2026-08-17 133809.png new file mode 100644 index 0000000000..40ecfaeda2 Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 133809.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 134037.png b/2026/day-62/Screenshot 2026-08-17 134037.png new file mode 100644 index 0000000000..ced5066f5c Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 134037.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 134157.png b/2026/day-62/Screenshot 2026-08-17 134157.png new file mode 100644 index 0000000000..f0ea1e860b Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 134157.png differ diff --git a/2026/day-62/Screenshot 2026-08-17 134234.png b/2026/day-62/Screenshot 2026-08-17 134234.png new file mode 100644 index 0000000000..8d856c3b9d Binary files /dev/null and b/2026/day-62/Screenshot 2026-08-17 134234.png differ diff --git a/2026/day-62/Terraform_Complete_Concept_Notes_Simple.docx b/2026/day-62/Terraform_Complete_Concept_Notes_Simple.docx new file mode 100644 index 0000000000..b185545b7a Binary files /dev/null and b/2026/day-62/Terraform_Complete_Concept_Notes_Simple.docx differ diff --git a/2026/day-62/day-62-terraform-lifecycle-dependencies.md b/2026/day-62/day-62-terraform-lifecycle-dependencies.md new file mode 100644 index 0000000000..2bd430b778 --- /dev/null +++ b/2026/day-62/day-62-terraform-lifecycle-dependencies.md @@ -0,0 +1,198 @@ +# Day 62 -- Terraform Lifecycle and Dependencies + +## What I Learned + +Today I practiced Terraform with AWS and learned how Terraform manages +resources, dependencies, state, and lifecycle rules. + +## What I Created + +Using Terraform, I created: + +- VPC +- Public Subnet +- Internet Gateway +- Route Table +- Route Table Association +- Security Group +- EC2 Instance +- S3 Bucket + +## Commands I Practiced + +### Format Terraform Code + +`terraform fmt` + +This command formats the Terraform code and makes it clean and readable. + +### Validate Configuration + +`terraform validate` + +I received: + +**Success! The configuration is valid.** + +This confirmed that my Terraform configuration was correct. + +### Check the Plan + +`terraform plan` + +This showed me what Terraform was going to create, change, or destroy +before making changes in AWS. + +### Create Infrastructure + +`terraform apply` + +Terraform created my AWS resources successfully. + +### Check Terraform State + +I used Terraform state to inspect my EC2 instance. + +`terraform state show aws_instance.main` + +I checked values such as: + +- Public IP +- Instance type +- Subnet ID + +My EC2 instance used `t3.micro`. + +## Terraform Dependency + +I created an S3 bucket for application logs and used: + +`depends_on = [aws_instance.main]` + +This tells Terraform that the S3 bucket has an explicit dependency on +the EC2 instance. + +## Terraform Graph + +I generated a Terraform dependency graph with: + +`terraform graph | dot -Tpng > graph.png` + +This helped me understand how Terraform resources are connected. + +## Lifecycle Rule + +I practiced this lifecycle rule inside the EC2 resource: + +``` hcl +lifecycle { + create_before_destroy = true +} +``` + +This tells Terraform to create the replacement resource before +destroying the old resource when replacement is required. + +## AMI Replacement Test + +I checked available Amazon Linux AMIs using the AWS CLI. + +Then I changed the AMI in the EC2 configuration. + +Terraform showed: + +`Plan: 1 to add, 0 to change, 1 to destroy.` + +This taught me that changing the AMI can require Terraform to replace +the EC2 instance. + +I did not need to apply this replacement because the goal was to +understand and verify the lifecycle behavior. + +## Errors I Faced + +### EC2 Instance Type Error + +At first I tried to use `t2.micro`. + +AWS returned an error because that instance type was not supported in +the selected Availability Zone. + +I checked the available free-tier-eligible instance types and changed +the EC2 instance to: + +`t3.micro` + +After this change, the EC2 instance was created successfully. + +### Lifecycle Block Error + +I received: + +`Error: Unsupported block type` + +The lifecycle block was placed in the wrong location. + +I moved it inside the `aws_instance` resource: + +``` hcl +resource "aws_instance" "main" { + # EC2 configuration + + lifecycle { + create_before_destroy = true + } +} +``` + +After fixing the block placement: + +`terraform validate` + +returned: + +**Success! The configuration is valid.** + +## Final Cleanup + +After completing the lab, I removed the AWS resources using: + +`terraform destroy` + +Final result: + +**Destroy complete! Resources: 8 destroyed.** + +This was important because leaving cloud resources running can cause +unnecessary AWS charges. + +## Screenshots + +Add screenshots here for: + +1. Terraform validate success +2. Terraform apply success +3. AWS VPC created +4. EC2 state/public IP +5. Terraform lifecycle replacement plan +6. Terraform destroy success + +## Key Takeaways + +- Terraform can create and manage AWS infrastructure as code. +- `terraform plan` lets me review changes before applying them. +- Terraform automatically understands many resource dependencies. +- `depends_on` can be used for an explicit dependency. +- Terraform state keeps track of managed infrastructure. +- `terraform graph` helps visualize resource relationships. +- Lifecycle rules control how Terraform creates and replaces + resources. +- `create_before_destroy` can reduce downtime during resource + replacement. +- Changing an EC2 AMI can cause the instance to be replaced. +- `terraform destroy` safely cleans up Terraform-managed lab + resources. + +## Day 62 Status + +**Completed ✅** diff --git a/2026/day-62/graph.png b/2026/day-62/graph.png new file mode 100644 index 0000000000..f424b661a9 Binary files /dev/null and b/2026/day-62/graph.png differ diff --git a/2026/day-62/imp-62.png b/2026/day-62/imp-62.png new file mode 100644 index 0000000000..78565bc88b Binary files /dev/null and b/2026/day-62/imp-62.png differ diff --git a/2026/day-62/main.tf b/2026/day-62/main.tf new file mode 100644 index 0000000000..d6e41cc9e1 --- /dev/null +++ b/2026/day-62/main.tf @@ -0,0 +1,103 @@ +# Create the main VPC +resource "aws_vpc" "main" { + cidr_block = "10.0.0.0/16" + + tags = { + Name = "TerraWeek-VPC" + } +} +# Create a public subnet inside the VPC +resource "aws_subnet" "public" { + vpc_id = aws_vpc.main.id + cidr_block = "10.0.1.0/24" + map_public_ip_on_launch = true + + tags = { + Name = "TerraWeek-Public-Subnet" + } +} +# Create an Internet Gateway and attach it to the VPC +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id + + tags = { + Name = "TerraWeek-IGW" + } +} +# Create a route table for internet access +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.main.id + } + + tags = { + Name = "TerraWeek-Public-RT" + } +} +# Associate the public subnet with the public route table +resource "aws_route_table_association" "public" { + subnet_id = aws_subnet.public.id + route_table_id = aws_route_table.public.id +} +# Security Group for the EC2 server +resource "aws_security_group" "main" { + name = "TerraWeek-SG" + description = "Allow SSH and HTTP traffic" + vpc_id = aws_vpc.main.id + + ingress { + description = "SSH" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + ingress { + description = "HTTP" + from_port = 80 + to_port = 80 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "TerraWeek-SG" + } +} +# Create EC2 instance in the public subnet +resource "aws_instance" "main" { + ami = "ami-0c1613580c06f8c5d" + instance_type = "t3.micro" + subnet_id = aws_subnet.public.id + vpc_security_group_ids = [aws_security_group.main.id] + associate_public_ip_address = true + + lifecycle { + create_before_destroy = true + } + + tags = { + Name = "TerraWeek-Server" + } +} +# S3 bucket for application logs +resource "aws_s3_bucket" "logs" { + bucket_prefix = "terraweek-app-logs-" + + depends_on = [aws_instance.main] + + tags = { + Name = "TerraWeek-App-Logs" + } +} + diff --git a/2026/day-62/providers.tf b/2026/day-62/providers.tf new file mode 100644 index 0000000000..dad35b5346 --- /dev/null +++ b/2026/day-62/providers.tf @@ -0,0 +1,11 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~>5.0" + } + } +} +provider "aws" { + region = "ca-central-1" +} diff --git a/2026/day-63/.terraform.lock.hcl b/2026/day-63/.terraform.lock.hcl new file mode 100644 index 0000000000..39a6fbd36d --- /dev/null +++ b/2026/day-63/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:edXOJWE4ORX8Fm+dpVpICzMZJat4AX0VRCAy/xkcOc0=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/2026/day-63/Day-63-Terraform-Interview-Study-Notes.docx b/2026/day-63/Day-63-Terraform-Interview-Study-Notes.docx new file mode 100644 index 0000000000..f0fa0ad5a6 Binary files /dev/null and b/2026/day-63/Day-63-Terraform-Interview-Study-Notes.docx differ diff --git a/2026/day-63/Day-63-concept.png b/2026/day-63/Day-63-concept.png new file mode 100644 index 0000000000..b7be11a1cb Binary files /dev/null and b/2026/day-63/Day-63-concept.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 150059.png b/2026/day-63/Screenshot 2026-08-18 150059.png new file mode 100644 index 0000000000..5a6055638f Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 150059.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 150713.png b/2026/day-63/Screenshot 2026-08-18 150713.png new file mode 100644 index 0000000000..77505e7b5d Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 150713.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 150904.png b/2026/day-63/Screenshot 2026-08-18 150904.png new file mode 100644 index 0000000000..df07fe2e7a Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 150904.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 152442.png b/2026/day-63/Screenshot 2026-08-18 152442.png new file mode 100644 index 0000000000..ddfb272227 Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 152442.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 153306.png b/2026/day-63/Screenshot 2026-08-18 153306.png new file mode 100644 index 0000000000..04b244d968 Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 153306.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 153430.png b/2026/day-63/Screenshot 2026-08-18 153430.png new file mode 100644 index 0000000000..37518749b1 Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 153430.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 153442.png b/2026/day-63/Screenshot 2026-08-18 153442.png new file mode 100644 index 0000000000..0d553804d2 Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 153442.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 153456.png b/2026/day-63/Screenshot 2026-08-18 153456.png new file mode 100644 index 0000000000..1bbb85c8ff Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 153456.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 153636.png b/2026/day-63/Screenshot 2026-08-18 153636.png new file mode 100644 index 0000000000..31e8be1eb8 Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 153636.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 153700.png b/2026/day-63/Screenshot 2026-08-18 153700.png new file mode 100644 index 0000000000..ecd0afb180 Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 153700.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 154332.png b/2026/day-63/Screenshot 2026-08-18 154332.png new file mode 100644 index 0000000000..2add46f9f6 Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 154332.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 154642.png b/2026/day-63/Screenshot 2026-08-18 154642.png new file mode 100644 index 0000000000..e3691118d7 Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 154642.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 154948.png b/2026/day-63/Screenshot 2026-08-18 154948.png new file mode 100644 index 0000000000..ac5414c7bc Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 154948.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 155226.png b/2026/day-63/Screenshot 2026-08-18 155226.png new file mode 100644 index 0000000000..169169e73e Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 155226.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 155322.png b/2026/day-63/Screenshot 2026-08-18 155322.png new file mode 100644 index 0000000000..10f65174b5 Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 155322.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 155509.png b/2026/day-63/Screenshot 2026-08-18 155509.png new file mode 100644 index 0000000000..c9ca0c9f52 Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 155509.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 155905.png b/2026/day-63/Screenshot 2026-08-18 155905.png new file mode 100644 index 0000000000..8f8dd3c97a Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 155905.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 155936.png b/2026/day-63/Screenshot 2026-08-18 155936.png new file mode 100644 index 0000000000..40614eef89 Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 155936.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 160205.png b/2026/day-63/Screenshot 2026-08-18 160205.png new file mode 100644 index 0000000000..bcd990a82f Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 160205.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 161115.png b/2026/day-63/Screenshot 2026-08-18 161115.png new file mode 100644 index 0000000000..26e64f90b3 Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 161115.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 161131.png b/2026/day-63/Screenshot 2026-08-18 161131.png new file mode 100644 index 0000000000..122c43bfab Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 161131.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 161227.png b/2026/day-63/Screenshot 2026-08-18 161227.png new file mode 100644 index 0000000000..795baf608a Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 161227.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 161335.png b/2026/day-63/Screenshot 2026-08-18 161335.png new file mode 100644 index 0000000000..269ff8e52e Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 161335.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 161426.png b/2026/day-63/Screenshot 2026-08-18 161426.png new file mode 100644 index 0000000000..eb1d35f84d Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 161426.png differ diff --git a/2026/day-63/Screenshot 2026-08-18 161601.png b/2026/day-63/Screenshot 2026-08-18 161601.png new file mode 100644 index 0000000000..a9850897d3 Binary files /dev/null and b/2026/day-63/Screenshot 2026-08-18 161601.png differ diff --git a/2026/day-63/day-63-terraform.md b/2026/day-63/day-63-terraform.md new file mode 100644 index 0000000000..197b0b5d05 --- /dev/null +++ b/2026/day-63/day-63-terraform.md @@ -0,0 +1,208 @@ +# Day 63 -- Terraform Variables, Outputs, Data Sources and Locals + +## Overview + +Today I practiced Terraform by creating AWS infrastructure and making +the configuration more reusable. + +## What I Practiced + +- Terraform variables and `terraform.tfvars` +- Command-line variable overrides +- Outputs and JSON outputs +- AWS data sources +- Locals and common tags +- `terraform fmt`, `validate`, `plan`, `apply`, state, and `destroy` + +## AWS Resources Created + +- VPC +- Public Subnet +- Internet Gateway +- Route Table +- Route Table Association +- Security Group +- EC2 Instance +- S3 Bucket + +## Terraform Variables + +I created `variables.tf` so values do not need to be hardcoded in +`main.tf`. + +Variables included: - `region` - `vpc_cidr` - `subnet_cidr` - +`instance_type` - `project_name` - `environment` - `allowed_ports` - +`extra_tags` + +This makes the configuration easier to reuse. + +## Variable Values + +I practiced using `terraform.tfvars` and also overriding a value from +the command line: + +``` bash +terraform plan -var="instance_type=t2.nano" +``` + +This showed me that Terraform can receive variable values from different +places. + +## Terraform Outputs + +I created outputs for: - EC2 instance ID - EC2 public IP - EC2 public +DNS - Security Group ID - Subnet ID - VPC ID + +Commands used: + +``` bash +terraform output +terraform output -json +``` + +Outputs are useful for getting important information after resources are +created. + +## Data Sources + +I used AWS data sources to read information instead of hardcoding +everything. + +I used: - `data.aws_ami.amazon_linux` - +`data.aws_availability_zones.available` + +I learned that a data source reads existing information. It does not +create a resource. + +## Locals and Common Tags + +I used locals to make resource names and tags consistent. + +Common tags included: + +``` text +Environment = dev +ManagedBy = Terraform +Project = terraweek +``` + +Terraform updated 7 existing resources in place: + +``` text +Plan: 0 to add, 7 to change, 0 to destroy. +``` + +After applying: + +``` text +Apply complete! Resources: 0 added, 7 changed, 0 destroyed. +``` + +## Validation and Planning + +I used: + +``` bash +terraform fmt +terraform validate +terraform plan +``` + +Validation returned: + +``` text +Success! The configuration is valid. +``` + +At final verification, Terraform returned: + +``` text +No changes. Your infrastructure matches the configuration. +``` + +This confirmed that my configuration, Terraform state, and AWS +infrastructure matched. + +## Terraform State + +I checked managed resources using: + +``` bash +terraform state list +``` + +The state contained the AWS resources and data sources used in the lab. + +## Problems I Faced + +### 1. Unsupported block type + +I accidentally wrote `variablle` instead of `variable`. I corrected the +spelling and validated again. + +### 2. Missing AWS provider + +Terraform reported a missing provider. I fixed it with: + +``` bash +terraform init +``` + +### 3. EC2 Availability Zone problem + +The requested EC2 instance type was not supported in the selected +Availability Zone. I changed to a supported Availability Zone. + +### 4. Subnet CIDR conflict + +AWS reported that `10.0.1.0/24` conflicted with another subnet while the +subnet was being replaced. After fixing the subnet situation, Terraform +successfully created the infrastructure. + +These errors helped me practice reading Terraform and AWS error messages +and troubleshooting step by step. + +## Cleanup + +Because this was a practice lab, I cleaned up the AWS resources: + +``` bash +terraform destroy +``` + +Terraform showed: + +``` text +Plan: 0 to add, 0 to change, 8 to destroy. +``` + +Final result: + +``` text +Destroy complete! Resources: 8 destroyed. +``` + +I then ran: + +``` bash +terraform state list +``` + +No resources remained in the state. + +## Key Takeaways + +- Variables make Terraform reusable. +- `.tfvars` keeps values separate from configuration. +- Command-line values can override defaults. +- Outputs expose useful resource information. +- Data sources read existing AWS information. +- Locals reduce repetition and improve naming. +- Tags help organize AWS resources. +- Always run `terraform validate` and review `terraform plan`. +- Terraform state tracks managed infrastructure. +- Clean up practice resources with `terraform destroy`. + +## Day 63 Status + +**Completed successfully ✅** diff --git a/2026/day-63/locals.tf b/2026/day-63/locals.tf new file mode 100644 index 0000000000..3670c8fc83 --- /dev/null +++ b/2026/day-63/locals.tf @@ -0,0 +1,9 @@ +locals { + name_prefix = "${var.project_name}-${var.environment}" + + common_tags = { + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + } +} diff --git a/2026/day-63/main.tf b/2026/day-63/main.tf new file mode 100644 index 0000000000..bea9e2a82d --- /dev/null +++ b/2026/day-63/main.tf @@ -0,0 +1,134 @@ +# Create the main VPC +data "aws_ami" "amazon_linux" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["amzn2-ami-hvm-*-x86_64-gp2"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + filter { + name = "root-device-type" + values = ["ebs"] + } +} + +data "aws_availability_zones" "available" { + state = "available" +} +resource "aws_vpc" "main" { + cidr_block = var.vpc_cidr + + tags = merge( + local.common_tags, + var.extra_tags, + { + Name = "${local.name_prefix}-vpc" + } + ) +} +# Create a public subnet inside the VPC +resource "aws_subnet" "public" { + vpc_id = aws_vpc.main.id + cidr_block = var.subnet_cidr + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = true + + + tags = merge( + local.common_tags, + var.extra_tags, + { + Name = "${local.name_prefix}-subnet" + } + ) +} +# Create an Internet Gateway and attach it to the VPC +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id + + tags = merge(local.common_tags, var.extra_tags, { + Name = "${local.name_prefix}-igw" + }) +} +# Create a route table for internet access +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = var.internet_cidr + gateway_id = aws_internet_gateway.main.id + } + + tags = merge(local.common_tags, var.extra_tags, { + Name = "${local.name_prefix}-route-table" + }) +} +# Associate the public subnet with the public route table +resource "aws_route_table_association" "public" { + subnet_id = aws_subnet.public.id + route_table_id = aws_route_table.public.id +} +# Security Group for the EC2 server +resource "aws_security_group" "main" { + name = "TerraWeek-SG" + description = "Allow SSH and HTTP traffic" + vpc_id = aws_vpc.main.id + + dynamic "ingress" { + for_each = var.allowed_ports + + content { + description = "Allow port ${ingress.value}" + from_port = ingress.value + to_port = ingress.value + protocol = "tcp" + cidr_blocks = [var.internet_cidr] + } + } + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = [var.internet_cidr] + } + + tags = merge(local.common_tags, var.extra_tags, { + Name = "${local.name_prefix}-sg" + }) +} +# Create EC2 instance in the public subnet +resource "aws_instance" "main" { + ami = data.aws_ami.amazon_linux.id + instance_type = var.instance_type + subnet_id = aws_subnet.public.id + vpc_security_group_ids = [aws_security_group.main.id] + associate_public_ip_address = true + + + + tags = merge( + local.common_tags, + var.extra_tags, + { + Name = "${local.name_prefix}-server" + } + ) +} +# S3 bucket for application logs +resource "aws_s3_bucket" "logs" { + bucket_prefix = "terraweek-app-logs-" + + depends_on = [aws_instance.main] + + tags = merge(local.common_tags, var.extra_tags, { + Name = "${local.name_prefix}-logs" + }) +} + diff --git a/2026/day-63/outputs.tf b/2026/day-63/outputs.tf new file mode 100644 index 0000000000..586159416c --- /dev/null +++ b/2026/day-63/outputs.tf @@ -0,0 +1,29 @@ +output "vpc_id" { + description = "VPC ID" + value = aws_vpc.main.id +} + +output "subnet_id" { + description = "Public subnet ID" + value = aws_subnet.public.id +} + +output "instance_id" { + description = "EC2 instance ID" + value = aws_instance.main.id +} + +output "instance_public_ip" { + description = "Public IP of EC2 instance" + value = aws_instance.main.public_ip +} + +output "instance_public_dns" { + description = "Public DNS of EC2 instance" + value = aws_instance.main.public_dns +} + +output "security_group_id" { + description = "Security group ID" + value = aws_security_group.main.id +} diff --git a/2026/day-63/prod.tfvars b/2026/day-63/prod.tfvars new file mode 100644 index 0000000000..531b1e4171 --- /dev/null +++ b/2026/day-63/prod.tfvars @@ -0,0 +1,5 @@ +project_name = "terraweek" +environment = "prod" +instance_type = "t3.small" +vpc_cidr = "10.1.0.0/16" +subnet_cidr = "10.1.1.0/24" diff --git a/2026/day-63/providers.tf b/2026/day-63/providers.tf new file mode 100644 index 0000000000..fdcae32663 --- /dev/null +++ b/2026/day-63/providers.tf @@ -0,0 +1,11 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~>5.0" + } + } +} +provider "aws" { + region = var.region +} diff --git a/2026/day-63/terraform.tfvars b/2026/day-63/terraform.tfvars new file mode 100644 index 0000000000..2b0e4208fd --- /dev/null +++ b/2026/day-63/terraform.tfvars @@ -0,0 +1,3 @@ +project_name = "terraweek" +environment = "dev" +instance_type = "t2.micro" diff --git a/2026/day-63/variables.tf b/2026/day-63/variables.tf new file mode 100644 index 0000000000..0ea76f08ad --- /dev/null +++ b/2026/day-63/variables.tf @@ -0,0 +1,51 @@ +variable "region" { + description = "AWS region" + type = string + default = "ca-central-1" +} + +variable "vpc_cidr" { + description = "VPC CIDR block" + type = string + default = "10.0.0.0/16" +} + +variable "subnet_cidr" { + description = "Subnet CIDR block" + type = string + default = "10.0.1.0/24" +} + +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t2.micro" +} + +variable "project_name" { + description = "Project name" + type = string +} + +variable "environment" { + description = "Environment name" + type = string + default = "dev" +} + +variable "allowed_ports" { + description = "Allowed ports" + type = list(number) + default = [22, 80, 443] +} + +variable "extra_tags" { + description = "Extra resource tags" + type = map(string) + default = {} +} +variable "internet_cidr" { + description = "CIDR block for internet access" + type = string + default = "0.0.0.0/0" +} diff --git a/2026/day-64/.terraform.lock.hcl b/2026/day-64/.terraform.lock.hcl new file mode 100644 index 0000000000..39a6fbd36d --- /dev/null +++ b/2026/day-64/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:edXOJWE4ORX8Fm+dpVpICzMZJat4AX0VRCAy/xkcOc0=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/2026/day-64/Day-64-Concept.png b/2026/day-64/Day-64-Concept.png new file mode 100644 index 0000000000..0d386380bc Binary files /dev/null and b/2026/day-64/Day-64-Concept.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 101247.png b/2026/day-64/Screenshot 2026-08-19 101247.png new file mode 100644 index 0000000000..305d7d62a8 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 101247.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 101310.png b/2026/day-64/Screenshot 2026-08-19 101310.png new file mode 100644 index 0000000000..271e838292 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 101310.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 101713.png b/2026/day-64/Screenshot 2026-08-19 101713.png new file mode 100644 index 0000000000..4fffc31054 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 101713.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 103029.png b/2026/day-64/Screenshot 2026-08-19 103029.png new file mode 100644 index 0000000000..a7a30bb7c7 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 103029.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 103119.png b/2026/day-64/Screenshot 2026-08-19 103119.png new file mode 100644 index 0000000000..cfa072bb09 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 103119.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 103221.png b/2026/day-64/Screenshot 2026-08-19 103221.png new file mode 100644 index 0000000000..e551e64d66 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 103221.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 103356.png b/2026/day-64/Screenshot 2026-08-19 103356.png new file mode 100644 index 0000000000..f92c8c4e81 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 103356.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 103525.png b/2026/day-64/Screenshot 2026-08-19 103525.png new file mode 100644 index 0000000000..66b35deab7 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 103525.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 103615.png b/2026/day-64/Screenshot 2026-08-19 103615.png new file mode 100644 index 0000000000..b44139195b Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 103615.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 104414.png b/2026/day-64/Screenshot 2026-08-19 104414.png new file mode 100644 index 0000000000..0514c1d701 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 104414.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 104702.png b/2026/day-64/Screenshot 2026-08-19 104702.png new file mode 100644 index 0000000000..f044d0005f Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 104702.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 104822.png b/2026/day-64/Screenshot 2026-08-19 104822.png new file mode 100644 index 0000000000..c21d5c60ab Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 104822.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 105030.png b/2026/day-64/Screenshot 2026-08-19 105030.png new file mode 100644 index 0000000000..e1cb45fa4a Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 105030.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 105324.png b/2026/day-64/Screenshot 2026-08-19 105324.png new file mode 100644 index 0000000000..3ba29a5c15 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 105324.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 105339.png b/2026/day-64/Screenshot 2026-08-19 105339.png new file mode 100644 index 0000000000..068f4d0618 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 105339.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 105421.png b/2026/day-64/Screenshot 2026-08-19 105421.png new file mode 100644 index 0000000000..50d56ebc70 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 105421.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 105605.png b/2026/day-64/Screenshot 2026-08-19 105605.png new file mode 100644 index 0000000000..8cdf905802 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 105605.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 105715.png b/2026/day-64/Screenshot 2026-08-19 105715.png new file mode 100644 index 0000000000..e66045eeda Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 105715.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 105904.png b/2026/day-64/Screenshot 2026-08-19 105904.png new file mode 100644 index 0000000000..81745747b4 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 105904.png differ diff --git a/2026/day-64/Screenshot 2026-08-19 105949.png b/2026/day-64/Screenshot 2026-08-19 105949.png new file mode 100644 index 0000000000..bada9fa234 Binary files /dev/null and b/2026/day-64/Screenshot 2026-08-19 105949.png differ diff --git a/2026/day-64/backend.tf b/2026/day-64/backend.tf new file mode 100644 index 0000000000..5236afe868 --- /dev/null +++ b/2026/day-64/backend.tf @@ -0,0 +1,9 @@ +terraform { + backend "s3" { + bucket = "terraweek-state-asma-2026" + key = "dev/terraform.tfstate" + region = "ca-central-1" + dynamodb_table = "terraweek-state-lock" + encrypt = true + } +} diff --git a/2026/day-64/day-64-state-management.md b/2026/day-64/day-64-state-management.md new file mode 100644 index 0000000000..f50fa6a27d --- /dev/null +++ b/2026/day-64/day-64-state-management.md @@ -0,0 +1,723 @@ +# Day 64 --- Terraform State Management and Remote Backends + +## Goal + +Learn how Terraform state works, why remote state is important, how +state locking protects teams, how to import existing AWS resources, how +to use state commands, and how Terraform detects infrastructure drift. + +------------------------------------------------------------------------ + +## 1. Why Terraform State Is Important + +Terraform state is the map between my Terraform code and the real +resources in AWS. + +Example: + +``` text +aws_instance.main + ↓ +Real EC2 instance in AWS + ↓ +i-043a2341a057e18a3 +``` + +Terraform needs state so it remembers which real AWS resource belongs to +each Terraform resource. + +When I run `terraform plan`, Terraform uses my configuration, state +information, and refreshed information from AWS to determine what needs +to change. + +### Simple interview answer + +**Terraform state keeps track of the infrastructure Terraform manages +and maps Terraform resource addresses to real cloud resources.** + +------------------------------------------------------------------------ + +## 2. Task 1 --- Inspecting State + +I reused my Day 63 Terraform configuration. + +Useful commands: + +``` bash +terraform show +terraform state list +terraform state show aws_instance.main +terraform state show aws_vpc.main +``` + +`terraform show` displays the current state in readable form. + +`terraform state list` lists the addresses Terraform knows about. + +My state included resources/data sources such as: + +``` text +data.aws_ami.amazon_linux +data.aws_availability_zones.available +aws_instance.main +aws_internet_gateway.main +aws_route_table.public +aws_route_table_association.public +aws_s3_bucket.logs +aws_security_group.main +aws_subnet.public +aws_vpc.main +``` + +`terraform state show aws_instance.main` shows many EC2 attributes, +including values that AWS/provider calculated rather than values I +manually typed in the resource block. + +Examples include instance ID, AMI, instance type, subnet, security +groups, IP information, ARN, tags, and other computed attributes. + +------------------------------------------------------------------------ + +## 3. State Serial Number + +I checked: + +``` bash +grep '"serial"' terraform.tfstate +``` + +My lab showed: + +``` text +"serial": 9 +``` + +The serial is a revision counter for Terraform state. It changes as +Terraform writes newer state snapshots. + +**Easy memory:** `serial = state revision number`. + +------------------------------------------------------------------------ + +## 4. Why Local State Is Risky + +A local state file normally exists as: + +``` text +terraform.tfstate +``` + +Keeping important team state only on one laptop is risky because the +file can be lost, team members can end up with different copies, and +collaboration becomes difficult. + +A remote backend gives the team a shared state location. + +``` text +LOCAL + +Developer + | +terraform.tfstate + + +REMOTE + +Developer 1 ----Developer 2 -----+----> Remote Backend ----> State +CI/CD -----------/ +``` + +------------------------------------------------------------------------ + +## 5. Task 2 --- S3 Remote Backend + +I created an S3 bucket: + +``` text +terraweek-state-asma-2026 +``` + +Region: + +``` text +ca-central-1 +``` + +I enabled S3 versioning. + +### Why S3? + +S3 provides remote storage for the Terraform state. + +### Why versioning? + +Versioning keeps previous object versions, which can help with recovery +if a state object is accidentally changed or deleted. + +**Easy memory:**\ +`S3 = remote state storage`\ +`Versioning = previous state versions/recovery` + +------------------------------------------------------------------------ + +## 6. State Locking + +For the lab I created the DynamoDB table: + +``` text +terraweek-state-lock +``` + +with: + +``` text +LockID +``` + +The table reached: + +``` text +ACTIVE +``` + +State locking prevents two Terraform operations from writing to the same +state at the same time. + +``` text +Engineer A ---> gets lock ---> Terraform operation +Engineer B ---> blocked until lock is available +``` + +This is critical in team environments because concurrent state writes +can cause conflicts or corrupt state. + +------------------------------------------------------------------------ + +## 7. Migrating State + +I configured the S3 backend and ran: + +``` bash +terraform init -migrate-state +``` + +Terraform asked: + +``` text +Do you want to copy existing state to the new backend? +``` + +I entered: + +``` text +yes +``` + +Terraform successfully configured the S3 backend. + +Then I ran: + +``` bash +terraform plan +``` + +Result: + +``` text +No changes. Your infrastructure matches the configuration. +``` + +This confirmed that the migration preserved Terraform's knowledge of my +infrastructure. + +------------------------------------------------------------------------ + +## 8. Deprecation Warning Seen in My Lab + +My Terraform version displayed: + +``` text +Warning: Deprecated Parameter + +The parameter "dynamodb_table" is deprecated. +Use parameter "use_lockfile" instead. +``` + +This is useful interview knowledge. + +The exercise demonstrated DynamoDB-based locking, while my installed +Terraform version warned that this backend parameter is deprecated and +recommended `use_lockfile`. + +------------------------------------------------------------------------ + +## 9. Task 3 --- Testing State Locking + +I opened two terminals in the same Terraform project. + +One Terraform operation held the state lock. In the second terminal I +ran another Terraform command. + +Terraform returned: + +``` text +Error: Error acquiring the state lock +``` + +The output also contained: + +``` text +ConditionalCheckFailedException: +The conditional request failed +``` + +and displayed a Lock ID. + +This proved the state locking mechanism was working. + +### Why locking matters + +Without locking, two engineers could try to modify the same state +simultaneously. Locking allows only one state-writing operation at a +time. + +### force-unlock + +``` bash +terraform force-unlock +``` + +Use this only for a stale lock and only after confirming that no +legitimate Terraform operation is still running. + +------------------------------------------------------------------------ + +## 10. Task 4 --- Import Existing Resource + +Sometimes an AWS resource already exists before Terraform starts +managing it. + +I created the S3 bucket: + +``` text +terraweek-import-test-asma-2026 +``` + +Then I imported it: + +``` bash +terraform import -var="project_name=terraweek" aws_s3_bucket.imported terraweek-import-test-asma-2026 +``` + +Terraform returned: + +``` text +Import successful! +``` + +Afterward: + +``` bash +terraform plan -var="project_name=terraweek" +``` + +returned: + +``` text +No changes. +``` + +### Import vs create + +**Create from scratch:** + +``` text +Terraform code + ↓ +terraform apply + ↓ +Terraform creates AWS resource +``` + +**Import:** + +``` text +Existing AWS resource + ↓ +terraform import + ↓ +Terraform associates it with a state address +``` + +Import is useful when a company already has manually created +infrastructure that now needs to be managed through Terraform. + +------------------------------------------------------------------------ + +## 11. Task 5 --- State Surgery + +I practiced: + +``` bash +terraform state mv +terraform state rm +terraform import +``` + +### terraform state mv + +I moved: + +``` text +aws_s3_bucket.imported +``` + +to: + +``` text +aws_s3_bucket.logs_bucket +``` + +Command: + +``` bash +terraform state mv aws_s3_bucket.imported aws_s3_bucket.logs_bucket +``` + +I updated my Terraform resource name to match. + +**Purpose:** move/rename a Terraform state address without recreating +the real resource. + +Typical use: refactoring or renaming Terraform resources. + +### terraform state rm + +I ran: + +``` bash +terraform state rm aws_s3_bucket.logs_bucket +``` + +This removed the resource from Terraform state but did **not** directly +delete the real S3 bucket. + +**Easy memory:** +`state rm = Terraform forgets the object; AWS object remains.` + +Then I re-imported the bucket so Terraform managed it again. + +------------------------------------------------------------------------ + +## 12. Task 6 --- State Drift + +Drift happens when real infrastructure is changed outside the intended +Terraform workflow. + +My Terraform configuration expected the EC2 Name tag to be: + +``` text +terraweek-dev-server +``` + +I manually changed it to: + +``` text +ManuallyChanged +``` + +Then I ran: + +``` bash +terraform plan -var="project_name=terraweek" +``` + +Terraform detected: + +``` text +"ManuallyChanged" -> "terraweek-dev-server" +``` + +and showed: + +``` text +Plan: 0 to add, 1 to change, 0 to destroy. +``` + +That was a real example of drift. + +------------------------------------------------------------------------ + +## 13. Fixing Drift + +There are two general choices: + +**Option A:** Keep Terraform configuration as the desired truth and +apply it so AWS returns to that desired configuration. + +**Option B:** If the manual change was intentional and approved, update +the Terraform configuration accordingly. + +For my lab I chose Option A. + +Terraform showed: + +``` text +Apply complete! Resources: 0 added, 1 changed, 0 destroyed. +``` + +I ran another plan and got: + +``` text +No changes. Your infrastructure matches the configuration. +``` + +Drift was resolved. + +### How teams reduce drift + +Teams commonly restrict unnecessary console changes, manage +infrastructure through Terraform, use pull requests/code reviews, +control IAM access, and run infrastructure changes through CI/CD. + +------------------------------------------------------------------------ + +## 14. Refresh / Refresh-Only + +A refresh updates Terraform's knowledge using information from real +infrastructure. + +A modern explicit workflow is: + +``` bash +terraform apply -refresh-only +``` + +A refresh-only operation updates state to reflect remote changes without +performing the normal infrastructure reconciliation of a regular apply. + +------------------------------------------------------------------------ + +## 15. Final Cleanup + +After the lab I destroyed the temporary Terraform-managed +infrastructure. + +Terraform returned: + +``` text +Destroy complete! Resources: 9 destroyed. +``` + +I also deleted the DynamoDB locking table. + +AWS showed: + +``` text +TableStatus: DELETING +``` + +------------------------------------------------------------------------ + +## 16. S3 Versioning Cleanup Lesson + +When I tried to delete the backend bucket with: + +``` bash +aws s3 rb s3://terraweek-state-asma-2026 --force +``` + +AWS returned: + +``` text +BucketNotEmpty +``` + +This happened because versioning was enabled. Removing the current +object was not enough; historical versions/delete markers still existed. + +After removing the versions and delete marker, I deleted the bucket +successfully: + +``` text +remove_bucket: terraweek-state-asma-2026 +``` + +This was a useful practical lesson about how S3 versioning preserves +historical objects. + +------------------------------------------------------------------------ + +## 17. Important Commands + + ----------------------------------------------------------------------- + Command Purpose + ----------------------------------- ----------------------------------- + `terraform show` Show state in readable form + + `terraform state list` List objects/addresses in state + + `terraform state show
` Inspect one state object + + `terraform init -migrate-state` Initialize backend and migrate + state + + `terraform import` Associate an existing resource with + Terraform state + + `terraform state mv` Move/rename a state address + + `terraform state rm` Remove an object from state without + directly destroying it + + `terraform force-unlock` Remove a stale lock + + `terraform apply -refresh-only` Refresh recorded state without + normal reconciliation + + `terraform plan` Preview differences/actions + + `terraform apply` Reconcile infrastructure with + desired configuration + + `terraform destroy` Destroy Terraform-managed resources + ----------------------------------------------------------------------- + +------------------------------------------------------------------------ + +# Interview Questions + +## What is Terraform state? + +Terraform state keeps Terraform's record of managed infrastructure and +maps Terraform resource addresses to real cloud objects. + +## Why use remote state? + +It gives teams a shared state location and is safer for collaboration +than separate local state files. + +## Why use S3 versioning? + +It preserves previous state object versions and can help with recovery. + +## What is state locking? + +It prevents concurrent Terraform operations from writing to the same +state. + +## What happens if another operation already has the lock? + +Terraform returns a state-lock error instead of allowing unsafe +concurrent state modification. + +## What is terraform import? + +It associates an already-existing infrastructure resource with a +Terraform resource address/state. + +## Does terraform state rm destroy AWS infrastructure? + +No. It removes the object from Terraform state; it does not directly +destroy the real resource. + +## Why use terraform state mv? + +To move or rename an existing state address during refactoring without +unnecessarily recreating the real infrastructure. + +## What is state drift? + +It is a difference between the intended Terraform configuration and the +real infrastructure, often caused by out-of-band/manual changes. + +## How did I test drift? + +I changed my EC2 Name tag to `ManuallyChanged`. Terraform plan detected +it and proposed restoring `terraweek-dev-server`. + +## What is force-unlock? + +It manually removes a stale Terraform state lock. It should only be used +after confirming no active operation owns the lock. + +## What does state serial mean? + +It is a revision counter for Terraform state snapshots. + +------------------------------------------------------------------------ + +# My Day 64 Lab Flow + +``` text +Reuse Day 63 infrastructure + ↓ +Inspect Terraform state + ↓ +Check serial number + ↓ +Create S3 backend bucket + ↓ +Enable S3 versioning + ↓ +Create DynamoDB lock table + ↓ +Configure remote backend + ↓ +terraform init -migrate-state + ↓ +Copy existing state to S3 + ↓ +terraform plan → No changes + ↓ +Test locking using two terminals + ↓ +Error acquiring state lock + ↓ +Create/import existing S3 bucket + ↓ +Import successful + ↓ +terraform plan → No changes + ↓ +terraform state mv + ↓ +terraform state rm + ↓ +Re-import bucket + ↓ +Change EC2 Name manually + ↓ +terraform plan detects drift + ↓ +terraform apply fixes drift + ↓ +terraform plan → No changes + ↓ +terraform destroy + ↓ +9 resources destroyed + ↓ +Delete DynamoDB table + ↓ +Remove S3 versions/delete marker + ↓ +Delete backend S3 bucket +``` + +# Five Things to Remember Before an Interview + +``` text +STATE = Terraform's map/record of managed infrastructure +S3 = Remote state storage +LOCK = Prevent concurrent state writes +IMPORT = Bring an existing resource under Terraform management +DRIFT = Real infrastructure differs from desired Terraform configuration +``` + +## Day 64 Completed ✅ + +I successfully practiced Terraform state inspection, remote S3 state, +versioning, state migration, locking, importing existing infrastructure, +`state mv`, `state rm`, re-importing, drift detection, drift correction, +and complete AWS cleanup. diff --git a/2026/day-64/locals.tf b/2026/day-64/locals.tf new file mode 100644 index 0000000000..3670c8fc83 --- /dev/null +++ b/2026/day-64/locals.tf @@ -0,0 +1,9 @@ +locals { + name_prefix = "${var.project_name}-${var.environment}" + + common_tags = { + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + } +} diff --git a/2026/day-64/main.tf b/2026/day-64/main.tf new file mode 100644 index 0000000000..9f02658e7e --- /dev/null +++ b/2026/day-64/main.tf @@ -0,0 +1,134 @@ +# Create the main VPC +data "aws_ami" "amazon_linux" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["amzn2-ami-hvm-*-x86_64-gp2"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } + + filter { + name = "root-device-type" + values = ["ebs"] + } +} + +data "aws_availability_zones" "available" { + state = "available" +} +resource "aws_vpc" "main" { + cidr_block = var.vpc_cidr + + tags = merge( + local.common_tags, + var.extra_tags, + { + Name = "${local.name_prefix}-vpc" + } + ) +} +# Create a public subnet inside the VPC +resource "aws_subnet" "public" { + vpc_id = aws_vpc.main.id + cidr_block = var.subnet_cidr + availability_zone = data.aws_availability_zones.available.names[0] + map_public_ip_on_launch = true + + + tags = merge( + local.common_tags, + var.extra_tags, + { + Name = "${local.name_prefix}-subnet" + } + ) +} +# Create an Internet Gateway and attach it to the VPC +resource "aws_internet_gateway" "main" { + vpc_id = aws_vpc.main.id + + tags = merge(local.common_tags, var.extra_tags, { + Name = "${local.name_prefix}-igw" + }) +} +# Create a route table for internet access +resource "aws_route_table" "public" { + vpc_id = aws_vpc.main.id + + route { + cidr_block = var.internet_cidr + gateway_id = aws_internet_gateway.main.id + } + + tags = merge(local.common_tags, var.extra_tags, { + Name = "${local.name_prefix}-route-table" + }) +} +# Associate the public subnet with the public route table +resource "aws_route_table_association" "public" { + subnet_id = aws_subnet.public.id + route_table_id = aws_route_table.public.id +} +# Security Group for the EC2 server +resource "aws_security_group" "main" { + name = "TerraWeek-SG" + description = "Allow SSH and HTTP traffic" + vpc_id = aws_vpc.main.id + + dynamic "ingress" { + for_each = var.allowed_ports + + content { + description = "Allow port ${ingress.value}" + from_port = ingress.value + to_port = ingress.value + protocol = "tcp" + cidr_blocks = [var.internet_cidr] + } + } + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = [var.internet_cidr] + } + + tags = merge(local.common_tags, var.extra_tags, { + Name = "${local.name_prefix}-sg" + }) +} +# Create EC2 instance in the public subnet +resource "aws_instance" "main" { + ami = data.aws_ami.amazon_linux.id + instance_type = var.instance_type + subnet_id = aws_subnet.public.id + vpc_security_group_ids = [aws_security_group.main.id] + associate_public_ip_address = true + tags = merge( + local.common_tags, + var.extra_tags, + { + Name = "${local.name_prefix}-server" + TestLock = "true" + } + ) +} +# S3 bucket for application logs +resource "aws_s3_bucket" "logs" { + bucket_prefix = "terraweek-app-logs-" + + depends_on = [aws_instance.main] + + tags = merge(local.common_tags, var.extra_tags, { + Name = "${local.name_prefix}-logs" + }) +} +resource "aws_s3_bucket" "logs_bucket" { + bucket = "terraweek-import-test-asma-2026" +} diff --git a/2026/day-64/outputs.tf b/2026/day-64/outputs.tf new file mode 100644 index 0000000000..586159416c --- /dev/null +++ b/2026/day-64/outputs.tf @@ -0,0 +1,29 @@ +output "vpc_id" { + description = "VPC ID" + value = aws_vpc.main.id +} + +output "subnet_id" { + description = "Public subnet ID" + value = aws_subnet.public.id +} + +output "instance_id" { + description = "EC2 instance ID" + value = aws_instance.main.id +} + +output "instance_public_ip" { + description = "Public IP of EC2 instance" + value = aws_instance.main.public_ip +} + +output "instance_public_dns" { + description = "Public DNS of EC2 instance" + value = aws_instance.main.public_dns +} + +output "security_group_id" { + description = "Security group ID" + value = aws_security_group.main.id +} diff --git a/2026/day-64/providers.tf b/2026/day-64/providers.tf new file mode 100644 index 0000000000..fdcae32663 --- /dev/null +++ b/2026/day-64/providers.tf @@ -0,0 +1,11 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~>5.0" + } + } +} +provider "aws" { + region = var.region +} diff --git a/2026/day-64/variables.tf b/2026/day-64/variables.tf new file mode 100644 index 0000000000..0ea76f08ad --- /dev/null +++ b/2026/day-64/variables.tf @@ -0,0 +1,51 @@ +variable "region" { + description = "AWS region" + type = string + default = "ca-central-1" +} + +variable "vpc_cidr" { + description = "VPC CIDR block" + type = string + default = "10.0.0.0/16" +} + +variable "subnet_cidr" { + description = "Subnet CIDR block" + type = string + default = "10.0.1.0/24" +} + +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t2.micro" +} + +variable "project_name" { + description = "Project name" + type = string +} + +variable "environment" { + description = "Environment name" + type = string + default = "dev" +} + +variable "allowed_ports" { + description = "Allowed ports" + type = list(number) + default = [22, 80, 443] +} + +variable "extra_tags" { + description = "Extra resource tags" + type = map(string) + default = {} +} +variable "internet_cidr" { + description = "CIDR block for internet access" + type = string + default = "0.0.0.0/0" +} diff --git a/2026/day-65/.terraform.lock.hcl b/2026/day-65/.terraform.lock.hcl new file mode 100644 index 0000000000..39a6fbd36d --- /dev/null +++ b/2026/day-65/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:edXOJWE4ORX8Fm+dpVpICzMZJat4AX0VRCAy/xkcOc0=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/2026/day-65/Screenshot 2026-08-25 132256.png b/2026/day-65/Screenshot 2026-08-25 132256.png new file mode 100644 index 0000000000..ad4c83b19b Binary files /dev/null and b/2026/day-65/Screenshot 2026-08-25 132256.png differ diff --git a/2026/day-65/Screenshot 2026-08-25 134215.png b/2026/day-65/Screenshot 2026-08-25 134215.png new file mode 100644 index 0000000000..67421eb8b5 Binary files /dev/null and b/2026/day-65/Screenshot 2026-08-25 134215.png differ diff --git a/2026/day-65/Screenshot 2026-08-25 134936.png b/2026/day-65/Screenshot 2026-08-25 134936.png new file mode 100644 index 0000000000..abb3e2dce7 Binary files /dev/null and b/2026/day-65/Screenshot 2026-08-25 134936.png differ diff --git a/2026/day-65/Screenshot 2026-08-25 135503.png b/2026/day-65/Screenshot 2026-08-25 135503.png new file mode 100644 index 0000000000..0f4738fe72 Binary files /dev/null and b/2026/day-65/Screenshot 2026-08-25 135503.png differ diff --git a/2026/day-65/Screenshot 2026-08-25 140241.png b/2026/day-65/Screenshot 2026-08-25 140241.png new file mode 100644 index 0000000000..f8ae8f496d Binary files /dev/null and b/2026/day-65/Screenshot 2026-08-25 140241.png differ diff --git a/2026/day-65/Screenshot 2026-08-25 140830.png b/2026/day-65/Screenshot 2026-08-25 140830.png new file mode 100644 index 0000000000..bcd41d4ab3 Binary files /dev/null and b/2026/day-65/Screenshot 2026-08-25 140830.png differ diff --git a/2026/day-65/Screenshot 2026-08-25 142339.png b/2026/day-65/Screenshot 2026-08-25 142339.png new file mode 100644 index 0000000000..d44d834b14 Binary files /dev/null and b/2026/day-65/Screenshot 2026-08-25 142339.png differ diff --git a/2026/day-65/Screenshot 2026-08-25 142738.png b/2026/day-65/Screenshot 2026-08-25 142738.png new file mode 100644 index 0000000000..b33b6e5547 Binary files /dev/null and b/2026/day-65/Screenshot 2026-08-25 142738.png differ diff --git a/2026/day-65/Screenshot 2026-08-25 142813.png b/2026/day-65/Screenshot 2026-08-25 142813.png new file mode 100644 index 0000000000..439b906113 Binary files /dev/null and b/2026/day-65/Screenshot 2026-08-25 142813.png differ diff --git a/2026/day-65/Screenshot 2026-08-25 142859.png b/2026/day-65/Screenshot 2026-08-25 142859.png new file mode 100644 index 0000000000..aa4783587a Binary files /dev/null and b/2026/day-65/Screenshot 2026-08-25 142859.png differ diff --git a/2026/day-65/Screenshot 2026-08-25 142955.png b/2026/day-65/Screenshot 2026-08-25 142955.png new file mode 100644 index 0000000000..4d1a255e43 Binary files /dev/null and b/2026/day-65/Screenshot 2026-08-25 142955.png differ diff --git a/2026/day-65/Screenshot 2026-08-25 143141.png b/2026/day-65/Screenshot 2026-08-25 143141.png new file mode 100644 index 0000000000..45089ad16d Binary files /dev/null and b/2026/day-65/Screenshot 2026-08-25 143141.png differ diff --git a/2026/day-65/Terraform-Modules-concept.png b/2026/day-65/Terraform-Modules-concept.png new file mode 100644 index 0000000000..1f0f934a42 Binary files /dev/null and b/2026/day-65/Terraform-Modules-concept.png differ diff --git a/2026/day-65/day-65-modules.md b/2026/day-65/day-65-modules.md new file mode 100644 index 0000000000..6637aa752d --- /dev/null +++ b/2026/day-65/day-65-modules.md @@ -0,0 +1,469 @@ +# Day 65 -- Terraform Modules: Build Reusable Infrastructure + +## What I Learned + +Today I learned how Terraform **modules** help organize and reuse +infrastructure code. + +Instead of keeping every resource in one large `main.tf`, I separated +reusable resources into modules. I built my own EC2 and Security Group +modules and also used a public VPC module from the Terraform Registry. + +A module feels similar to a function in programming: define it once, +pass different inputs, and reuse it. + +------------------------------------------------------------------------ + +## 1. Module Structure + +I created this project structure: + +``` text +day-65/ +├── main.tf +├── variables.tf +├── outputs.tf +├── providers.tf +└── modules/ + ├── ec2-instance/ + │ ├── main.tf + │ ├── variables.tf + │ └── outputs.tf + └── security-group/ + ├── main.tf + ├── variables.tf + └── outputs.tf +``` + +### Root Module vs Child Module + +- **Root module** -- the Terraform configuration where I run commands + such as `terraform plan` and `terraform apply`. +- **Child module** -- a reusable module called by the root module, + such as my EC2 or Security Group module. + +------------------------------------------------------------------------ + +## 2. Custom EC2 Module + +I created a reusable EC2 module in: + +``` text +modules/ec2-instance/ +``` + +### variables.tf + +``` hcl +variable "ami_id" { + description = "AMI ID for the EC2 instance" + type = string +} + +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t2.micro" +} + +variable "subnet_id" { + description = "Subnet ID" + type = string +} + +variable "security_group_ids" { + description = "Security group IDs" + type = list(string) +} + +variable "instance_name" { + description = "Name of the EC2 instance" + type = string +} + +variable "tags" { + description = "Additional tags" + type = map(string) + default = {} +} +``` + +### main.tf + +``` hcl +resource "aws_instance" "this" { + ami = var.ami_id + instance_type = var.instance_type + subnet_id = var.subnet_id + vpc_security_group_ids = var.security_group_ids + associate_public_ip_address = true + + tags = merge( + var.tags, + { + Name = var.instance_name + } + ) +} +``` + +### outputs.tf + +``` hcl +output "instance_id" { + value = aws_instance.this.id +} + +output "public_ip" { + value = aws_instance.this.public_ip +} + +output "private_ip" { + value = aws_instance.this.private_ip +} +``` + +I called the same EC2 module twice to create two different servers: + +- `terraweek-web` +- `terraweek-api` + +This showed me that I do not need to copy the EC2 resource code every +time. + +------------------------------------------------------------------------ + +## 3. Custom Security Group Module + +I also created: + +``` text +modules/security-group/ +``` + +The module accepts a list of ports and uses a Terraform `dynamic` block +to create ingress rules. + +For this lab I used: + +``` hcl +ingress_ports = [22, 80, 443] +``` + +The important concept was that a `dynamic` block can generate repeated +nested blocks from a collection instead of writing each ingress rule +manually. + +The Security Group output was then passed into both EC2 module calls: + +``` hcl +security_group_ids = [module.web_sg.sg_id] +``` + +So both servers shared the same Security Group. + +------------------------------------------------------------------------ + +## 4. Reusing the EC2 Module + +The root module called the same child module twice with different names: + +``` hcl +module "web_server" { + source = "./modules/ec2-instance" + + ami_id = data.aws_ami.amazon_linux.id + instance_type = "t3.micro" + subnet_id = module.vpc.public_subnets[0] + security_group_ids = [module.web_sg.sg_id] + instance_name = "terraweek-web" + tags = local.common_tags +} + +module "api_server" { + source = "./modules/ec2-instance" + + ami_id = data.aws_ami.amazon_linux.id + instance_type = "t3.micro" + subnet_id = module.vpc.public_subnets[0] + security_group_ids = [module.web_sg.sg_id] + instance_name = "terraweek-api" + tags = local.common_tags +} +``` + +After fixing the lab issues, Terraform successfully created both +instances and returned public IP addresses. + +``` text +api_server_ip = "99.79.123.227" +web_server_ip = "15.223.68.171" +``` + +------------------------------------------------------------------------ + +## 5. Terraform Registry VPC Module + +Instead of continuing with my hand-written VPC resources from the +earlier lab, I replaced them with the public AWS VPC module: + +``` hcl +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 5.0" + + name = "terraweek-vpc" + cidr = "10.0.0.0/16" + + azs = [ + "ca-central-1a", + "ca-central-1b" + ] + + public_subnets = [ + "10.0.1.0/24", + "10.0.2.0/24" + ] + + private_subnets = [ + "10.0.3.0/24", + "10.0.4.0/24" + ] + + enable_nat_gateway = false + enable_dns_hostnames = true + + tags = local.common_tags +} +``` + +The Registry module created much more networking infrastructure +automatically than my simple hand-written VPC configuration, including +public/private subnets, route tables, route-table associations, Internet +Gateway, and related VPC resources. + +I also learned that Terraform downloads Registry modules under: + +``` text +.terraform/modules/ +``` + +I verified it with: + +``` bash +ls .terraform/modules +``` + +and saw: + +``` text +modules.json vpc +``` + +------------------------------------------------------------------------ + +## 6. Terraform State with Modules + +I ran: + +``` bash +terraform state list +``` + +The state showed module-specific addresses such as: + +``` text +module.api_server.aws_instance.this +module.web_server.aws_instance.this +module.web_sg.aws_security_group.this +module.vpc.aws_vpc.this[0] +module.vpc.aws_subnet.public[0] +module.vpc.aws_subnet.public[1] +module.vpc.aws_subnet.private[0] +module.vpc.aws_subnet.private[1] +``` + +This helped me understand that Terraform keeps track of which module +owns each resource. + +------------------------------------------------------------------------ + +## 7. Real Troubleshooting During the Lab + +This lab included several problems that helped me understand Terraform +better. + +### Duplicate VPC Module + +I accidentally had two blocks named: + +``` hcl +module "vpc" +``` + +Terraform returned: + +``` text +Error: Duplicate module call +``` + +I removed the duplicate and kept only one Registry VPC module. + +### Old VPC References + +After moving to the Registry module, the configuration still contained +old hand-written networking resources and references. + +I removed the old VPC, subnet, Internet Gateway, route table, and +route-table association configuration and updated module references to: + +``` hcl +module.vpc.vpc_id +module.vpc.public_subnets[0] +``` + +### EC2 Instance Type / Availability Zone Problem + +Initially I used: + +``` text +t2.micro +``` + +AWS returned an error because `t2.micro` was not supported in the +selected `ca-central-1d` Availability Zone. + +I changed the instance type to: + +``` text +t3.micro +``` + +and used the VPC module with: + +``` text +ca-central-1a +ca-central-1b +``` + +The instances were then created successfully. + +### Public IP Output Was Empty + +The first successful deployment showed: + +``` text +api_server_ip = "" +web_server_ip = "" +``` + +I added: + +``` hcl +associate_public_ip_address = true +``` + +to my EC2 module. + +Terraform replaced both EC2 instances, and the new instances received +public IP addresses. + +This was a useful example of seeing how a configuration change can cause +Terraform to replace existing infrastructure. + +------------------------------------------------------------------------ + +## 8. Hand-Written VPC vs Registry Module + +### Hand-Written VPC + +In my earlier Terraform work, I manually defined resources such as: + +- VPC +- subnet +- Internet Gateway +- route table +- route-table association + +This gave me direct control, but required more Terraform code. + +### Registry VPC Module + +For Day 65, I supplied the VPC configuration as module inputs, and the +module created the required networking resources internally. + +During the Registry-module deployment, Terraform reported: + +``` text +Apply complete! Resources: 20 added, 0 changed, 8 destroyed. +``` + +This showed me how much infrastructure a reusable module can manage +behind a relatively small module call. + +------------------------------------------------------------------------ + +## 9. Module Best Practices + +Five module practices I learned: + +1. Pin Registry module versions so upgrades are controlled. +2. Keep each custom module focused on one responsibility. +3. Use variables so the module can be reused with different + configurations. +4. Define outputs so other modules can use important resource values. +5. Add documentation such as a `README.md` so other engineers + understand how to use the module. + +------------------------------------------------------------------------ + +## 10. Commands Practiced + +``` bash +terraform fmt -recursive +terraform init +terraform init -upgrade +terraform validate +terraform plan +terraform apply +terraform state list +ls .terraform/modules +terraform destroy +``` + +------------------------------------------------------------------------ + +## Final Cleanup + +After completing and verifying the lab, I destroyed the AWS +infrastructure: + +``` bash +terraform destroy +``` + +Final result: + +``` text +Destroy complete! Resources: 20 destroyed. +``` + +This ensured the temporary lab infrastructure was removed after testing. + +------------------------------------------------------------------------ + +## My Main Takeaway + +Before this lab, I was writing Terraform resources directly in the root +configuration. + +Now I understand how to package infrastructure into reusable modules, +pass values into modules through variables, expose values through +outputs, connect modules together, call the same module multiple times, +and use an existing Registry module instead of rebuilding common +infrastructure. + +The troubleshooting was also useful because I had to fix duplicate +module definitions, old resource references, an unsupported EC2 instance +type/Availability Zone combination, and missing public IPs before the +final deployment worked. diff --git a/2026/day-65/main.tf b/2026/day-65/main.tf new file mode 100644 index 0000000000..8cba06f4aa --- /dev/null +++ b/2026/day-65/main.tf @@ -0,0 +1,87 @@ +# Find latest Amazon Linux 2 AMI +data "aws_ami" "amazon_linux" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["amzn2-ami-hvm-*-x86_64-gp2"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } +} + +# Common tags +locals { + common_tags = { + Project = var.project_name + ManagedBy = "Terraform" + } +} + +# VPC using Terraform Registry module +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 5.0" + + name = "terraweek-vpc" + cidr = "10.0.0.0/16" + + azs = [ + "ca-central-1a", + "ca-central-1b" + ] + + public_subnets = [ + "10.0.1.0/24", + "10.0.2.0/24" + ] + + private_subnets = [ + "10.0.3.0/24", + "10.0.4.0/24" + ] + + enable_nat_gateway = false + enable_dns_hostnames = true + + tags = local.common_tags +} + +# Security Group module +module "web_sg" { + source = "./modules/security-group" + + vpc_id = module.vpc.vpc_id + sg_name = "terraweek-web-sg" + ingress_ports = [22, 80, 443] + tags = local.common_tags +} + +# Web Server +module "web_server" { + source = "./modules/ec2-instance" + + ami_id = data.aws_ami.amazon_linux.id + instance_type = "t3.micro" + subnet_id = module.vpc.public_subnets[0] + security_group_ids = [module.web_sg.sg_id] + instance_name = "terraweek-web" + tags = local.common_tags +} + +# API Server +module "api_server" { + source = "./modules/ec2-instance" + + ami_id = data.aws_ami.amazon_linux.id + instance_type = "t3.micro" + subnet_id = module.vpc.public_subnets[0] + security_group_ids = [module.web_sg.sg_id] + instance_name = "terraweek-api" + tags = local.common_tags +} + diff --git a/2026/day-65/modules/ec2-instance/.terraform.lock.hcl b/2026/day-65/modules/ec2-instance/.terraform.lock.hcl new file mode 100644 index 0000000000..3313d20954 --- /dev/null +++ b/2026/day-65/modules/ec2-instance/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.61.0" + hashes = [ + "h1:Kl91YvWRdorx6POwgdFf6ZBsHHa86GhazsqOwPvunyo=", + "zh:216566f0fbc506e107d3a961c87d88aed052ec2b4ced13388394d3cff30425ac", + "zh:4700ad141d0ad96465ac35a3900f2ff7c91a1e5528428ac687c8b5825c0be718", + "zh:6b79d2fa6550fc51e2fac04f1066c5cc96e957e7634ad86d2250240e637db205", + "zh:76f6227bf2bcd7422cf29d9868d8b517c8220ec3edd4e7c8434b867a71c03334", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:a7be5814cf94a8e3869ec9fa7f5182223a11373a4510ce68b9bab431f9b83746", + "zh:b65392e24506fe99f8df7bd2b5d3940d7a234b64b79a890f1aabe2be91a827e5", + "zh:b68a5092203cf5a9d1a5f01f9f2e96fded3eb7cacfd49ae851c50b87bc610fe9", + "zh:b8fedfa62bac16592519e1bae0c82dd7cd1544b0637543d72e3597e46ad7db32", + "zh:bba8a35212c07e68b6c881aa011c4b26a284e42b971059579aca9ffb63a8faef", + "zh:bbd0391e3e2f21c8930872829df7cf7f4e35f84c4d6d7b7619a94f8078fe6f3d", + "zh:c9c921e8e466281c04cada8d336ec1ce978128ee153e9df7b451847fbea49d18", + "zh:c9cb0be3097f4392b977fa254d28efd0909d008f572e74a41ead6c0d84c0daaa", + "zh:cc7aeb23fa3775816e391d4668a9865f787993f9f8d6f5b7a9f7e4effdffb3cb", + "zh:e59e487220cec8999bbd120ba8f97b5e04b010fb9149e47d4c9032961614d7c7", + "zh:fa19a7571a99397120f2f2bdcf33bbcc2f39091b9b0c879cd7e5f9ebb1966c40", + ] +} diff --git a/2026/day-65/modules/ec2-instance/main.tf b/2026/day-65/modules/ec2-instance/main.tf new file mode 100644 index 0000000000..1da5779812 --- /dev/null +++ b/2026/day-65/modules/ec2-instance/main.tf @@ -0,0 +1,14 @@ +resource "aws_instance" "this" { + ami = var.ami_id + instance_type = var.instance_type + subnet_id = var.subnet_id + vpc_security_group_ids = var.security_group_ids + associate_public_ip_address = true + + tags = merge( + var.tags, + { + Name = var.instance_name + } + ) +} diff --git a/2026/day-65/modules/ec2-instance/outputs.tf b/2026/day-65/modules/ec2-instance/outputs.tf new file mode 100644 index 0000000000..8b168c2327 --- /dev/null +++ b/2026/day-65/modules/ec2-instance/outputs.tf @@ -0,0 +1,11 @@ +output "instance_id" { + value = aws_instance.this.id +} + +output "public_ip" { + value = aws_instance.this.public_ip +} + +output "private_ip" { + value = aws_instance.this.private_ip +} diff --git a/2026/day-65/modules/ec2-instance/variables.tf b/2026/day-65/modules/ec2-instance/variables.tf new file mode 100644 index 0000000000..3efac4639e --- /dev/null +++ b/2026/day-65/modules/ec2-instance/variables.tf @@ -0,0 +1,31 @@ +variable "ami_id" { + description = "AMI ID for the EC2 instance" + type = string +} + +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t2.micro" +} + +variable "subnet_id" { + description = "Subnet ID for the EC2 instance" + type = string +} + +variable "security_group_ids" { + description = "Security groups for the EC2 instance" + type = list(string) +} + +variable "instance_name" { + description = "Name of the EC2 instance" + type = string +} + +variable "tags" { + description = "Additional tags" + type = map(string) + default = {} +} diff --git a/2026/day-65/modules/security-group/main.tf b/2026/day-65/modules/security-group/main.tf new file mode 100644 index 0000000000..48ee981a1b --- /dev/null +++ b/2026/day-65/modules/security-group/main.tf @@ -0,0 +1,30 @@ +resource "aws_security_group" "this" { + name = var.sg_name + description = "Security group managed by Terraform module" + vpc_id = var.vpc_id + + dynamic "ingress" { + for_each = var.ingress_ports + + content { + from_port = ingress.value + to_port = ingress.value + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = merge( + var.tags, + { + Name = var.sg_name + } + ) +} diff --git a/2026/day-65/modules/security-group/outputs.tf b/2026/day-65/modules/security-group/outputs.tf new file mode 100644 index 0000000000..94ee4aefd4 --- /dev/null +++ b/2026/day-65/modules/security-group/outputs.tf @@ -0,0 +1,3 @@ +output "sg_id" { + value = aws_security_group.this.id +} diff --git a/2026/day-65/modules/security-group/variables.tf b/2026/day-65/modules/security-group/variables.tf new file mode 100644 index 0000000000..d407282642 --- /dev/null +++ b/2026/day-65/modules/security-group/variables.tf @@ -0,0 +1,21 @@ +variable "vpc_id" { + description = "VPC ID for the security group" + type = string +} + +variable "sg_name" { + description = "Security group name" + type = string +} + +variable "ingress_ports" { + description = "Ports allowed for inbound traffic" + type = list(number) + default = [22, 80] +} + +variable "tags" { + description = "Additional tags" + type = map(string) + default = {} +} diff --git a/2026/day-65/outputs.tf b/2026/day-65/outputs.tf new file mode 100644 index 0000000000..da916a1fbb --- /dev/null +++ b/2026/day-65/outputs.tf @@ -0,0 +1,7 @@ +output "web_server_ip" { + value = module.web_server.public_ip +} + +output "api_server_ip" { + value = module.api_server.public_ip +} diff --git a/2026/day-65/providers.tf b/2026/day-65/providers.tf new file mode 100644 index 0000000000..a68e2003de --- /dev/null +++ b/2026/day-65/providers.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + required_version = ">= 1.0" +} + +provider "aws" { + region = var.aws_region +} diff --git a/2026/day-65/variables.tf b/2026/day-65/variables.tf new file mode 100644 index 0000000000..f17f607bc6 --- /dev/null +++ b/2026/day-65/variables.tf @@ -0,0 +1,11 @@ +variable "aws_region" { + description = "AWS region" + type = string + default = "ca-central-1" +} + +variable "project_name" { + description = "Project name" + type = string + default = "terraweek" +} diff --git a/2026/day-66/EKS+Terraform.png b/2026/day-66/EKS+Terraform.png new file mode 100644 index 0000000000..013efd26ca Binary files /dev/null and b/2026/day-66/EKS+Terraform.png differ diff --git a/2026/day-66/Screenshot 2026-08-26 121854.png b/2026/day-66/Screenshot 2026-08-26 121854.png new file mode 100644 index 0000000000..f9601d3d9f Binary files /dev/null and b/2026/day-66/Screenshot 2026-08-26 121854.png differ diff --git a/2026/day-66/Screenshot 2026-08-26 122221.png b/2026/day-66/Screenshot 2026-08-26 122221.png new file mode 100644 index 0000000000..4587334a56 Binary files /dev/null and b/2026/day-66/Screenshot 2026-08-26 122221.png differ diff --git a/2026/day-66/Screenshot 2026-08-26 122306.png b/2026/day-66/Screenshot 2026-08-26 122306.png new file mode 100644 index 0000000000..4c42b0be63 Binary files /dev/null and b/2026/day-66/Screenshot 2026-08-26 122306.png differ diff --git a/2026/day-66/Screenshot 2026-08-26 122504.png b/2026/day-66/Screenshot 2026-08-26 122504.png new file mode 100644 index 0000000000..6b49cb9d51 Binary files /dev/null and b/2026/day-66/Screenshot 2026-08-26 122504.png differ diff --git a/2026/day-66/Screenshot 2026-08-26 123821.png b/2026/day-66/Screenshot 2026-08-26 123821.png new file mode 100644 index 0000000000..451961178a Binary files /dev/null and b/2026/day-66/Screenshot 2026-08-26 123821.png differ diff --git a/2026/day-66/Screenshot 2026-08-26 123908.png b/2026/day-66/Screenshot 2026-08-26 123908.png new file mode 100644 index 0000000000..0e55ac1b8a Binary files /dev/null and b/2026/day-66/Screenshot 2026-08-26 123908.png differ diff --git a/2026/day-66/Screenshot 2026-08-26 123942.png b/2026/day-66/Screenshot 2026-08-26 123942.png new file mode 100644 index 0000000000..85d3779ea6 Binary files /dev/null and b/2026/day-66/Screenshot 2026-08-26 123942.png differ diff --git a/2026/day-66/Screenshot 2026-08-26 124000.png b/2026/day-66/Screenshot 2026-08-26 124000.png new file mode 100644 index 0000000000..34ddf44da4 Binary files /dev/null and b/2026/day-66/Screenshot 2026-08-26 124000.png differ diff --git a/2026/day-66/Screenshot 2026-08-26 124141.png b/2026/day-66/Screenshot 2026-08-26 124141.png new file mode 100644 index 0000000000..9ac348c2ce Binary files /dev/null and b/2026/day-66/Screenshot 2026-08-26 124141.png differ diff --git a/2026/day-66/Screenshot 2026-08-26 124217.png b/2026/day-66/Screenshot 2026-08-26 124217.png new file mode 100644 index 0000000000..1da6818fd9 Binary files /dev/null and b/2026/day-66/Screenshot 2026-08-26 124217.png differ diff --git a/2026/day-66/Screenshot 2026-08-26 124622.png b/2026/day-66/Screenshot 2026-08-26 124622.png new file mode 100644 index 0000000000..ddeaf2cc9c Binary files /dev/null and b/2026/day-66/Screenshot 2026-08-26 124622.png differ diff --git a/2026/day-66/Screenshot 2026-08-26 124915.png b/2026/day-66/Screenshot 2026-08-26 124915.png new file mode 100644 index 0000000000..db2a02850d Binary files /dev/null and b/2026/day-66/Screenshot 2026-08-26 124915.png differ diff --git a/2026/day-66/Screenshot 2026-08-26 125018.png b/2026/day-66/Screenshot 2026-08-26 125018.png new file mode 100644 index 0000000000..4f0a37f620 Binary files /dev/null and b/2026/day-66/Screenshot 2026-08-26 125018.png differ diff --git a/2026/day-66/day-66-eks-terraform.md b/2026/day-66/day-66-eks-terraform.md new file mode 100644 index 0000000000..23ea796bd2 --- /dev/null +++ b/2026/day-66/day-66-eks-terraform.md @@ -0,0 +1,318 @@ +# Day 66 -- Provision an EKS Cluster with Terraform Modules + +## Objective + +Today I provisioned an AWS EKS cluster using Terraform Registry modules. +The goal was to create the networking and EKS infrastructure with +Terraform, connect `kubectl`, deploy an Nginx workload, verify it, and +clean up the AWS resources after the lab. + +## Project Structure + +``` text +terraform-eks/ +├── eks.tf +├── outputs.tf +├── providers.tf +├── terraform.tfvars +├── variables.tf +├── vpc.tf +└── k8s/ + └── nginx-deployment.yaml +``` + +## What I Created + +### 1. Terraform Providers + +I configured the AWS provider and Kubernetes provider. The AWS region +used for this lab was: + +``` text +ca-central-1 +``` + +I also used `terraform.tfvars` to provide the region value. + +### 2. VPC with Terraform Module + +I used the Terraform Registry VPC module: + +``` text +terraform-aws-modules/vpc/aws +``` + +The VPC included: + +- 2 Availability Zones +- 2 public subnets +- 2 private subnets +- NAT Gateway +- DNS hostnames +- EKS subnet tags + +EKS uses private subnets for the worker nodes, while public subnets can +be used for public-facing AWS load balancers. + +The subnet tags help Kubernetes/AWS identify which subnets can be used +for public or internal load balancers. + +### 3. EKS Cluster + +I used the Terraform EKS module: + +``` text +terraform-aws-modules/eks/aws +``` + +My configuration created: + +- EKS cluster: `terraweek-eks` +- Kubernetes version: `1.31` +- Managed node group: `terraweek_nodes` +- Instance type: `t3.medium` +- Desired worker nodes: `2` +- Public EKS API endpoint +- IAM roles and security resources + +The EKS worker nodes were placed in the private subnets created by the +VPC module. + +## Terraform Validation and Plan + +Before creating the infrastructure, I ran: + +``` bash +terraform fmt +terraform validate +terraform plan +``` + +Terraform validation completed successfully. + +The final plan showed: + +``` text +Plan: 55 to add, 0 to change, 0 to destroy. +``` + +## Terraform Apply + +I created the infrastructure with: + +``` bash +terraform apply +``` + +The apply completed successfully: + +``` text +Apply complete! Resources: 55 added, 0 changed, 0 destroyed. +``` + +### Screenshot + +Add the screenshot showing the successful `terraform apply` here. + +## Connecting kubectl to EKS + +I updated my local kubeconfig using: + +``` bash +aws eks update-kubeconfig --name terraweek-eks --region ca-central-1 +``` + +The EKS context was added to: + +``` text +/home/asma/.kube/config +``` + +Initially, `kubectl` returned a credentials error. I enabled cluster +creator administrator permissions in the EKS Terraform module: + +``` hcl +enable_cluster_creator_admin_permissions = true +``` + +Terraform then added the required EKS access resources: + +``` text +Apply complete! Resources: 2 added, 0 changed, 0 destroyed. +``` + +After updating kubeconfig again, `kubectl` successfully connected to the +cluster. + +## Verify Worker Nodes + +I ran: + +``` bash +kubectl get nodes +``` + +Both managed worker nodes were in `Ready` state: + +``` text +ip-10-0-11-218.ca-central-1.compute.internal Ready +ip-10-0-12-6.ca-central-1.compute.internal Ready +``` + +### Screenshot + +Add the screenshot showing both EKS nodes in `Ready` state here. + +## Deploy Nginx + +I created: + +``` text +k8s/nginx-deployment.yaml +``` + +The workload contained: + +- Nginx Deployment +- 3 replicas +- Nginx container on port 80 +- Kubernetes Service of type `LoadBalancer` + +I deployed it using: + +``` bash +kubectl apply -f k8s/nginx-deployment.yaml +``` + +Then I verified the pods: + +``` bash +kubectl get pods +``` + +All three Nginx pods were running successfully. + +I also checked the service: + +``` bash +kubectl get svc nginx-service +``` + +AWS successfully created an external LoadBalancer for the Nginx service. + +### Screenshot + +Add the screenshot showing the 3 running Nginx pods and the LoadBalancer +service here. + +## Cleanup + +Because EKS, EC2 worker nodes, NAT Gateway, and LoadBalancer resources +can generate AWS charges, cleanup is an important part of this lab. + +First, I removed the Kubernetes resources: + +``` bash +kubectl delete -f k8s/nginx-deployment.yaml +``` + +I verified that `nginx-service` was removed: + +``` bash +kubectl get svc +``` + +Only the default Kubernetes `ClusterIP` service remained. + +Next, the Terraform-managed infrastructure should be removed with: + +``` bash +terraform destroy +``` + +After destruction, verify in AWS that the following lab resources are +gone: + +- EKS cluster +- EC2 worker-node instances +- VPC +- NAT Gateway +- Elastic IP +- AWS LoadBalancer + +### Destroy Screenshot + +Add the final `Destroy complete!` screenshot here after cleanup is +complete. + +> **Cleanup status:** At the time this document was created, the +> Kubernetes Nginx workload and LoadBalancer service had been deleted. +> Record the final Terraform destroy result here after +> `terraform destroy` completes. + +## What I Learned + +This lab helped me understand how Terraform modules can connect +together. + +``` text +VPC Module + ↓ +Private/Public Subnets + ↓ +EKS Module + ↓ +EKS Cluster + ↓ +Managed Node Group + ↓ +EC2 Worker Nodes + ↓ +Kubernetes Pods +``` + +One important concept was using an output from one module as an input to +another: + +``` hcl +vpc_id = module.vpc.vpc_id +subnet_ids = module.vpc.private_subnets +``` + +Instead of manually creating every AWS resource, Terraform Registry +modules created the VPC, networking, IAM, security, EKS cluster, and +managed node group in a repeatable way. + +## EKS vs kind/minikube + +On Day 50, I practiced Kubernetes using a local cluster such as +kind/minikube. That was useful for learning Kubernetes objects and +commands. + +In this lab, EKS was different because it created real AWS +infrastructure. AWS managed the Kubernetes control plane, while the +managed node group provided EC2 worker nodes. + +The biggest lesson for me was: + +**kind/minikube is useful for local Kubernetes practice, while EKS with +Terraform demonstrates how Kubernetes infrastructure can be provisioned +in AWS using Infrastructure as Code.** + +## Result + +I successfully: + +- Created AWS networking using a Terraform VPC module +- Provisioned an EKS cluster using a Terraform EKS module +- Created 2 managed EC2 worker nodes +- Connected `kubectl` to EKS +- Fixed EKS access permissions +- Verified both nodes were `Ready` +- Deployed 3 Nginx pods +- Created and verified an AWS LoadBalancer +- Deleted the Kubernetes workload and LoadBalancer service +- Prepared the infrastructure for complete Terraform cleanup + +This was a practical example of using **Terraform + AWS + EKS + +Kubernetes** together. diff --git a/2026/day-66/terraform-eks/.terraform.lock.hcl b/2026/day-66/terraform-eks/.terraform.lock.hcl new file mode 100644 index 0000000000..e4c9fba882 --- /dev/null +++ b/2026/day-66/terraform-eks/.terraform.lock.hcl @@ -0,0 +1,128 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0, >= 5.79.0" + hashes = [ + "h1:edXOJWE4ORX8Fm+dpVpICzMZJat4AX0VRCAy/xkcOc0=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} + +provider "registry.terraform.io/hashicorp/cloudinit" { + version = "2.4.0" + constraints = ">= 2.0.0" + hashes = [ + "h1:Bx3XQkBSY3RAGwLZb8hyi8AhvahPNlt4mlyZhW9guOI=", + "zh:1b0fe71b8e87a068f7cd9faaa733100ab72ab61ce812b8bd2b8e3e6ea3907b2d", + "zh:2aa9631ad64cfda1eb58f147619b631dadedfaf9453b422aa5ada2d3861183c1", + "zh:2c5f35463bdfb2f87d3576b81e62c30f8109e67bb6f21ffcbc46a855811455c0", + "zh:5970bcad151ea236bd262ada1a5a23bfbc1716f94a4e8b16ab2bcdda91d6a671", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:79a0676909732b6ec0441a733af6383513cde3bd2cef5c1ad0a74131e1286a04", + "zh:818f16141481a1202b3977becd19a12d4d46cd2e3f5753f5d0d0049adacf8f8c", + "zh:948d98716831087e69eca99f91ed7964cc537f3aca279f7494645ee56c9dc4ec", + "zh:a75e78889565a51df3e8e3af207e36e5ddb25e47ce1780a784c82dc3c3109b67", + "zh:a9c6e455d52b1bba5272bd87a35cfabcfd6d903dcbe42e2de926228dbb1e39b2", + "zh:b846805d8c2f5d1d6c2ffeeaf32109d9af7db7fa3c56929bfc1dcfaadf9c8bd8", + "zh:c3e5279756b46c4f49a6f4c81347fbe2fffebb2bf18a5c24664830304a1f6a8e", + "zh:c8be7b31893163d0046b0137a6100533f07e8efd192a1903b6bb4c42be12dceb", + ] +} + +provider "registry.terraform.io/hashicorp/kubernetes" { + version = "3.2.1" + hashes = [ + "h1:XccFuTe/eJ94vkoZNYUL3wsdXJwlxHguIY36jildRQ8=", + "zh:067fe16a852d42e0f571712e36cb3e71855f917ea2041415e155f56ebc480d7f", + "zh:2815e174f8f0f032ea3a64f2196740ad000a39f88ae5646e7061bf15ed589f62", + "zh:2f94f6b689c59c43e596e724228f2861095d02c2a2ac2a257a4619667135ac75", + "zh:3e807310c84f11561b9ba06b978f03c46cfeca2e84ad0803d34d5d30a8a637cb", + "zh:5cba6f92202c60cac6898141356420709f5341b80ae4c360725cc647f86188ff", + "zh:72b841b6f0820d8f87c3d7c5a3611c35121ab9a4c1db4ea7a98b0319f209e474", + "zh:74770b892ee9b04829d92318d9e8ca96f8143b0c6c766e4141901908173fd01d", + "zh:7a723c8ebf9e218d0f7a0cfe6c0437f2b5eeb7ae015a14fad16e0f7fd9ef79ab", + "zh:a0f5073b2636a3894d4e9dd1b6853d5f324dd78728313bff79b842e5e9eca96f", + "zh:c13241cba993ef63a537beb6a1caf00e233bb045b50d197349530de0ee3276d5", + "zh:d52826f4b0227b7db99ea4a1d48f49a0bfb440563c92ebd2f8faec273c856c2d", + "zh:dc1cf5505a39a264a650b0830f74150ad02368787e5ead89e4007034f8f47831", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.3.1" + constraints = ">= 3.0.0" + hashes = [ + "h1:TuxJq10DVnRP7c5HBZPyyvQGcckNVfijyU1eXEu5e4M=", + "zh:08c59776542ea16e5a8545752787b17ff412922182b4cfabe16139197be8ac44", + "zh:123109cc7e5ed6d515787fbc212f2a3fd5e75647bb24ab7c801ccd4d4ed42451", + "zh:14b3fa4372754b54844b41d5dbd4671a292d8d6828b90169061feb4d7b15dd05", + "zh:56a4daaa3212f57b764bf3d1f333141c6610c5f21abb240e0111221f7c7fa4d4", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7e888a026dbacd2474a42264227ae35f639780f0f0c613529d10a95cd61988b3", + "zh:85a53646267e87d600df7124e4767ffde9bba3b6356d45d961618bdd68131cc7", + "zh:8ffa0e9c7c39b2ab0905b472465d6e35ef0b776b3f6273bb34c150340b61bff1", + "zh:9846510a1841530d4403f4818e233f91e3b3bade7441047599fbf800742f65be", + "zh:afa98d44860875f037c6def0a7e6ff208e042712ba771f620482b143cd336891", + "zh:bdca130d9ef27488ae0b13bc8fd8019e8bbdd4f2ceff29da066bd333165d68c5", + "zh:cb3b94cbca88210dd0d1f11e2b8a89333f48c3857faf8f70f589072ce7c28610", + "zh:f0c0ba87925fe32f84b80f7513b1efb1b0866f51f899ba825e95ad59ff09b018", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.14.1" + constraints = ">= 0.9.0" + hashes = [ + "h1:GJig5pIwiKDsiF73KLs7vWvDs76/x6DeNSxKrfqlA40=", + "zh:0837ca5b057e5cff94dff7de2fcccafb4abaa33c45de193fe2853e684818a267", + "zh:15a122f72d9e0f34fc5384cc7ec089319641fee5c319748a3aa02fc42f459969", + "zh:342fb83093a280ea7ee0654feae1f5867c62eb8eebc1ab46f9a7ab0b4c878a62", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:99f169834d3370b8341381c6a9c7a8b01fb26027531faa38e6fb49cc23916f68", + "zh:9f482917c7a28cf2436578be7aa9f04f8c811aba8b5949e0223ea987a2757a91", + "zh:ac6b5b8732826f2d1129a8a4a038ac7a7a9ca7b77d2a4608e5703be1a1e2bff0", + "zh:c54782a27d58ce04f6696c6fc0b2cf1e2fba6bed239fb520521a7bce7d7193cb", + "zh:c8d0ddc8f575ecb44f025d54edbfe118e26397fe328a67be62325766f31eb6e7", + "zh:d043b96f204edd2353bf6b2a34e645ffdee2e9634d9bb747331320444810a538", + "zh:e32c288501ca9a6c9d22b52e839dd391fc7083d54ee6b8dc296ce0e6bd3e57ef", + "zh:e47fcc7bb4e9ab5cc522c3b06e4fa9c0bf94b84be8210bc6b1655c44acb2addc", + "zh:f61bf218322bcbe0bd2d56bba738e7fa485e9b54244e13aa12de741b37d450c0", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.3.0" + constraints = ">= 3.0.0" + hashes = [ + "h1:j/BqLS2N2AScZyotd9nZpHdieJ7e5S8y+A+ZfIu8kL8=", + "zh:0ab58d6f8991d436c7d2dbd89ed814709b949b07ac5a54ee53b0aec1fa772a8b", + "zh:60b347abcb56f45d97c56f14d895069cd15a83993f199777f571b79fea3642ee", + "zh:6889be32640349230de3f23856e6f04e0e9ced4a84a27d3f552fa54684448218", + "zh:73f8e1ecf7135033165fb14b7e8bf4d656f3ce13065ec35762ea0481975328c7", + "zh:94ce25ee253eca0b42cae9c856b36bca8103b6453012d1b279c3623c805f2d42", + "zh:96bc6de9fd67bc446fd11257872e1ffb1029a996ed1d65a3f6b43f6d408ad9ab", + "zh:97c609a310a51bfd504d704e036d72064a84bf0bdb36cc08cd4cc66098212b41", + "zh:a12c16e94533c5bd123f75032576b9dc91dd5d5ccd5f7cf331d0f2e1adc55cf8", + "zh:c4f014f876adf7af57188795050bda5b0029d8c7d7773031102b6c36dcf1fc21", + "zh:d9b0a21583aaa3df3a95394fb949a3c515ff71c2ff5a1fc4a73d364aa90bfca5", + "zh:da510d22f0c6d71ad19a76406f106b782448f512375787ecfabb338ed1e311a7", + "zh:f0e9447a9ce3a24cdaa113089e65663c836d8b9bfdb915a1c0284e0112cab5c0", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/2026/day-66/terraform-eks/eks.tf b/2026/day-66/terraform-eks/eks.tf new file mode 100644 index 0000000000..b8586b2848 --- /dev/null +++ b/2026/day-66/terraform-eks/eks.tf @@ -0,0 +1,31 @@ +module "eks" { + source = "terraform-aws-modules/eks/aws" + version = "~> 20.0" + + cluster_name = var.cluster_name + cluster_version = var.cluster_version + + enable_cluster_creator_admin_permissions = true + + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + + cluster_endpoint_public_access = true + + eks_managed_node_groups = { + terraweek_nodes = { + ami_type = "AL2_x86_64" + instance_types = [var.node_instance_type] + + min_size = 1 + max_size = 3 + desired_size = var.node_desired_count + } + } + + tags = { + Environment = "dev" + Project = "TerraWeek" + ManagedBy = "Terraform" + } +} diff --git a/2026/day-66/terraform-eks/k8s/nginx-deployment.yaml b/2026/day-66/terraform-eks/k8s/nginx-deployment.yaml new file mode 100644 index 0000000000..144286d40a --- /dev/null +++ b/2026/day-66/terraform-eks/k8s/nginx-deployment.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-terraweek + labels: + app: nginx + +spec: + replicas: 3 + + selector: + matchLabels: + app: nginx + + template: + metadata: + labels: + app: nginx + + spec: + containers: + - name: nginx + image: nginx:latest + ports: + - containerPort: 80 + +--- +apiVersion: v1 +kind: Service +metadata: + name: nginx-service + +spec: + type: LoadBalancer + + selector: + app: nginx + + ports: + - port: 80 + targetPort: 80 diff --git a/2026/day-66/terraform-eks/outputs.tf b/2026/day-66/terraform-eks/outputs.tf new file mode 100644 index 0000000000..f734f910de --- /dev/null +++ b/2026/day-66/terraform-eks/outputs.tf @@ -0,0 +1,11 @@ +output "cluster_name" { + value = module.eks.cluster_name +} + +output "cluster_endpoint" { + value = module.eks.cluster_endpoint +} + +output "cluster_region" { + value = var.region +} diff --git a/2026/day-66/terraform-eks/providers.tf b/2026/day-66/terraform-eks/providers.tf new file mode 100644 index 0000000000..bef676d257 --- /dev/null +++ b/2026/day-66/terraform-eks/providers.tf @@ -0,0 +1,18 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + + kubernetes = { + source = "hashicorp/kubernetes" + } + } + + required_version = ">= 1.0" +} + +provider "aws" { + region = var.region +} diff --git a/2026/day-66/terraform-eks/terraform.tfvars b/2026/day-66/terraform-eks/terraform.tfvars new file mode 100644 index 0000000000..56ee129e1b --- /dev/null +++ b/2026/day-66/terraform-eks/terraform.tfvars @@ -0,0 +1 @@ +region = "ca-central-1" diff --git a/2026/day-66/terraform-eks/variables.tf b/2026/day-66/terraform-eks/variables.tf new file mode 100644 index 0000000000..efd48f12ee --- /dev/null +++ b/2026/day-66/terraform-eks/variables.tf @@ -0,0 +1,34 @@ +variable "region" { + description = "AWS region" + type = string +} + +variable "cluster_name" { + description = "Name of the EKS cluster" + type = string + default = "terraweek-eks" +} + +variable "cluster_version" { + description = "Kubernetes version for EKS" + type = string + default = "1.31" +} + +variable "node_instance_type" { + description = "EC2 instance type for EKS worker nodes" + type = string + default = "t3.medium" +} + +variable "node_desired_count" { + description = "Desired number of EKS worker nodes" + type = number + default = 2 +} + +variable "vpc_cidr" { + description = "CIDR block for the VPC" + type = string + default = "10.0.0.0/16" +} diff --git a/2026/day-66/terraform-eks/vpc.tf b/2026/day-66/terraform-eks/vpc.tf new file mode 100644 index 0000000000..e0a60bb71b --- /dev/null +++ b/2026/day-66/terraform-eks/vpc.tf @@ -0,0 +1,36 @@ +data "aws_availability_zones" "available" { + state = "available" +} + +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 5.0" + + name = "terraweek-vpc" + cidr = var.vpc_cidr + + azs = slice(data.aws_availability_zones.available.names, 0, 2) + + public_subnets = [ + "10.0.1.0/24", + "10.0.2.0/24" + ] + + private_subnets = [ + "10.0.11.0/24", + "10.0.12.0/24" + ] + + enable_nat_gateway = true + single_nat_gateway = true + + enable_dns_hostnames = true + + public_subnet_tags = { + "kubernetes.io/role/elb" = 1 + } + + private_subnet_tags = { + "kubernetes.io/role/internal-elb" = 1 + } +} diff --git a/2026/day-67/Screenshot 2026-08-27 142734.png b/2026/day-67/Screenshot 2026-08-27 142734.png new file mode 100644 index 0000000000..00bf828d38 Binary files /dev/null and b/2026/day-67/Screenshot 2026-08-27 142734.png differ diff --git a/2026/day-67/Screenshot 2026-08-27 142958.png b/2026/day-67/Screenshot 2026-08-27 142958.png new file mode 100644 index 0000000000..bb350bc201 Binary files /dev/null and b/2026/day-67/Screenshot 2026-08-27 142958.png differ diff --git a/2026/day-67/Screenshot 2026-08-27 143219.png b/2026/day-67/Screenshot 2026-08-27 143219.png new file mode 100644 index 0000000000..3981cfd6d1 Binary files /dev/null and b/2026/day-67/Screenshot 2026-08-27 143219.png differ diff --git a/2026/day-67/Screenshot 2026-08-27 143350.png b/2026/day-67/Screenshot 2026-08-27 143350.png new file mode 100644 index 0000000000..2aac7a482a Binary files /dev/null and b/2026/day-67/Screenshot 2026-08-27 143350.png differ diff --git a/2026/day-67/Screenshot 2026-08-27 143543.png b/2026/day-67/Screenshot 2026-08-27 143543.png new file mode 100644 index 0000000000..b92260cd8f Binary files /dev/null and b/2026/day-67/Screenshot 2026-08-27 143543.png differ diff --git a/2026/day-67/Screenshot 2026-08-27 143755.png b/2026/day-67/Screenshot 2026-08-27 143755.png new file mode 100644 index 0000000000..86ab4a5e04 Binary files /dev/null and b/2026/day-67/Screenshot 2026-08-27 143755.png differ diff --git a/2026/day-67/Screenshot 2026-08-27 143908.png b/2026/day-67/Screenshot 2026-08-27 143908.png new file mode 100644 index 0000000000..62cace1391 Binary files /dev/null and b/2026/day-67/Screenshot 2026-08-27 143908.png differ diff --git a/2026/day-67/Terraform-Capstone-concept.png b/2026/day-67/Terraform-Capstone-concept.png new file mode 100644 index 0000000000..b2fb8e01fb Binary files /dev/null and b/2026/day-67/Terraform-Capstone-concept.png differ diff --git a/2026/day-67/terraweek-capstone/.gitignore b/2026/day-67/terraweek-capstone/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/2026/day-67/terraweek-capstone/.terraform.lock.hcl b/2026/day-67/terraweek-capstone/.terraform.lock.hcl new file mode 100644 index 0000000000..39a6fbd36d --- /dev/null +++ b/2026/day-67/terraweek-capstone/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:edXOJWE4ORX8Fm+dpVpICzMZJat4AX0VRCAy/xkcOc0=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/2026/day-67/terraweek-capstone/dev.tfvars b/2026/day-67/terraweek-capstone/dev.tfvars new file mode 100644 index 0000000000..a8d23308dd --- /dev/null +++ b/2026/day-67/terraweek-capstone/dev.tfvars @@ -0,0 +1,4 @@ +vpc_cidr = "10.0.0.0/16" +subnet_cidr = "10.0.1.0/24" +instance_type = "t2.micro" +ingress_ports = [22, 80] diff --git a/2026/day-67/terraweek-capstone/locals.tf b/2026/day-67/terraweek-capstone/locals.tf new file mode 100644 index 0000000000..7c16f89f39 --- /dev/null +++ b/2026/day-67/terraweek-capstone/locals.tf @@ -0,0 +1,11 @@ +locals { + environment = terraform.workspace + name_prefix = "${var.project_name}-${local.environment}" + + common_tags = { + Project = var.project_name + Environment = local.environment + ManagedBy = "Terraform" + Workspace = terraform.workspace + } +} diff --git a/2026/day-67/terraweek-capstone/main.tf b/2026/day-67/terraweek-capstone/main.tf new file mode 100644 index 0000000000..1aa59b8c1b --- /dev/null +++ b/2026/day-67/terraweek-capstone/main.tf @@ -0,0 +1,44 @@ +# Find the latest Amazon Linux 2 AMI +data "aws_ami" "amazon_linux" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["amzn2-ami-hvm-*-x86_64-gp2"] + } + + filter { + name = "virtualization-type" + values = ["hvm"] + } +} + +module "vpc" { + source = "./modules/vpc" + + cidr = var.vpc_cidr + public_subnet_cidr = var.subnet_cidr + environment = local.environment + project_name = var.project_name +} + +module "security_group" { + source = "./modules/security-group" + + vpc_id = module.vpc.vpc_id + ingress_ports = var.ingress_ports + environment = local.environment + project_name = var.project_name +} + +module "ec2_instance" { + source = "./modules/ec2-instance" + + ami_id = data.aws_ami.amazon_linux.id + instance_type = var.instance_type + subnet_id = module.vpc.subnet_id + security_group_ids = [module.security_group.sg_id] + environment = local.environment + project_name = var.project_name +} diff --git a/2026/day-67/terraweek-capstone/modules/ec2-instance/main.tf b/2026/day-67/terraweek-capstone/modules/ec2-instance/main.tf new file mode 100644 index 0000000000..b7d5ea3ff7 --- /dev/null +++ b/2026/day-67/terraweek-capstone/modules/ec2-instance/main.tf @@ -0,0 +1,13 @@ +resource "aws_instance" "this" { + ami = var.ami_id + instance_type = var.instance_type + subnet_id = var.subnet_id + vpc_security_group_ids = var.security_group_ids + + tags = { + Name = "${var.project_name}-${var.environment}-server" + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + } +} diff --git a/2026/day-67/terraweek-capstone/modules/ec2-instance/outputs.tf b/2026/day-67/terraweek-capstone/modules/ec2-instance/outputs.tf new file mode 100644 index 0000000000..c20fb27ec3 --- /dev/null +++ b/2026/day-67/terraweek-capstone/modules/ec2-instance/outputs.tf @@ -0,0 +1,9 @@ +output "instance_id" { + description = "EC2 instance ID" + value = aws_instance.this.id +} + +output "public_ip" { + description = "EC2 public IP" + value = aws_instance.this.public_ip +} diff --git a/2026/day-67/terraweek-capstone/modules/ec2-instance/variables.tf b/2026/day-67/terraweek-capstone/modules/ec2-instance/variables.tf new file mode 100644 index 0000000000..34835969a3 --- /dev/null +++ b/2026/day-67/terraweek-capstone/modules/ec2-instance/variables.tf @@ -0,0 +1,29 @@ +variable "ami_id" { + description = "AMI ID for the EC2 instance" + type = string +} + +variable "instance_type" { + description = "EC2 instance type" + type = string +} + +variable "subnet_id" { + description = "Subnet ID" + type = string +} + +variable "security_group_ids" { + description = "Security group IDs" + type = list(string) +} + +variable "environment" { + description = "Environment name" + type = string +} + +variable "project_name" { + description = "Project name" + type = string +} diff --git a/2026/day-67/terraweek-capstone/modules/security-group/main.tf b/2026/day-67/terraweek-capstone/modules/security-group/main.tf new file mode 100644 index 0000000000..7249514e2c --- /dev/null +++ b/2026/day-67/terraweek-capstone/modules/security-group/main.tf @@ -0,0 +1,29 @@ +resource "aws_security_group" "this" { + name = "${var.project_name}-${var.environment}-sg" + vpc_id = var.vpc_id + + dynamic "ingress" { + for_each = var.ingress_ports + + content { + from_port = ingress.value + to_port = ingress.value + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.project_name}-${var.environment}-sg" + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + } +} diff --git a/2026/day-67/terraweek-capstone/modules/security-group/outputs.tf b/2026/day-67/terraweek-capstone/modules/security-group/outputs.tf new file mode 100644 index 0000000000..6cba367242 --- /dev/null +++ b/2026/day-67/terraweek-capstone/modules/security-group/outputs.tf @@ -0,0 +1,4 @@ +output "sg_id" { + description = "Security group ID" + value = aws_security_group.this.id +} diff --git a/2026/day-67/terraweek-capstone/modules/security-group/variables.tf b/2026/day-67/terraweek-capstone/modules/security-group/variables.tf new file mode 100644 index 0000000000..d45b49902a --- /dev/null +++ b/2026/day-67/terraweek-capstone/modules/security-group/variables.tf @@ -0,0 +1,19 @@ +variable "vpc_id" { + description = "VPC ID" + type = string +} + +variable "ingress_ports" { + description = "Allowed inbound ports" + type = list(number) +} + +variable "environment" { + description = "Environment name" + type = string +} + +variable "project_name" { + description = "Project name" + type = string +} diff --git a/2026/day-67/terraweek-capstone/modules/vpc/main.tf b/2026/day-67/terraweek-capstone/modules/vpc/main.tf new file mode 100644 index 0000000000..0e78d47df6 --- /dev/null +++ b/2026/day-67/terraweek-capstone/modules/vpc/main.tf @@ -0,0 +1,57 @@ +resource "aws_vpc" "this" { + cidr_block = var.cidr + enable_dns_support = true + enable_dns_hostnames = true + + tags = { + Name = "${var.project_name}-${var.environment}-vpc" + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + } +} + +resource "aws_subnet" "public" { + vpc_id = aws_vpc.this.id + cidr_block = var.public_subnet_cidr + map_public_ip_on_launch = true + + tags = { + Name = "${var.project_name}-${var.environment}-public-subnet" + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + } +} + +resource "aws_internet_gateway" "this" { + vpc_id = aws_vpc.this.id + + tags = { + Name = "${var.project_name}-${var.environment}-igw" + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + } +} + +resource "aws_route_table" "public" { + vpc_id = aws_vpc.this.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.this.id + } + + tags = { + Name = "${var.project_name}-${var.environment}-public-rt" + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + } +} + +resource "aws_route_table_association" "public" { + subnet_id = aws_subnet.public.id + route_table_id = aws_route_table.public.id +} diff --git a/2026/day-67/terraweek-capstone/modules/vpc/outputs.tf b/2026/day-67/terraweek-capstone/modules/vpc/outputs.tf new file mode 100644 index 0000000000..6c955ca950 --- /dev/null +++ b/2026/day-67/terraweek-capstone/modules/vpc/outputs.tf @@ -0,0 +1,9 @@ +output "vpc_id" { + description = "ID of the VPC" + value = aws_vpc.this.id +} + +output "subnet_id" { + description = "ID of the public subnet" + value = aws_subnet.public.id +} diff --git a/2026/day-67/terraweek-capstone/modules/vpc/variables.tf b/2026/day-67/terraweek-capstone/modules/vpc/variables.tf new file mode 100644 index 0000000000..d6ea60f5c9 --- /dev/null +++ b/2026/day-67/terraweek-capstone/modules/vpc/variables.tf @@ -0,0 +1,19 @@ +variable "cidr" { + description = "CIDR block for the VPC" + type = string +} + +variable "public_subnet_cidr" { + description = "CIDR block for the public subnet" + type = string +} + +variable "environment" { + description = "Environment name" + type = string +} + +variable "project_name" { + description = "Project name" + type = string +} diff --git a/2026/day-67/terraweek-capstone/outputs.tf b/2026/day-67/terraweek-capstone/outputs.tf new file mode 100644 index 0000000000..1d9822ae91 --- /dev/null +++ b/2026/day-67/terraweek-capstone/outputs.tf @@ -0,0 +1,15 @@ +output "environment" { + value = local.environment +} + +output "vpc_id" { + value = module.vpc.vpc_id +} + +output "instance_id" { + value = module.ec2_instance.instance_id +} + +output "public_ip" { + value = module.ec2_instance.public_ip +} diff --git a/2026/day-67/terraweek-capstone/prod.tfvars b/2026/day-67/terraweek-capstone/prod.tfvars new file mode 100644 index 0000000000..4979c23e36 --- /dev/null +++ b/2026/day-67/terraweek-capstone/prod.tfvars @@ -0,0 +1,4 @@ +vpc_cidr = "10.2.0.0/16" +subnet_cidr = "10.2.1.0/24" +instance_type = "t3.small" +ingress_ports = [80, 443] diff --git a/2026/day-67/terraweek-capstone/providers.tf b/2026/day-67/terraweek-capstone/providers.tf new file mode 100644 index 0000000000..ab83af6618 --- /dev/null +++ b/2026/day-67/terraweek-capstone/providers.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + required_version = ">= 1.0" +} + +provider "aws" { + region = "ca-central-1" +} diff --git a/2026/day-67/terraweek-capstone/staging.tfvars b/2026/day-67/terraweek-capstone/staging.tfvars new file mode 100644 index 0000000000..a39eeca5bf --- /dev/null +++ b/2026/day-67/terraweek-capstone/staging.tfvars @@ -0,0 +1,4 @@ +vpc_cidr = "10.1.0.0/16" +subnet_cidr = "10.1.1.0/24" +instance_type = "t2.small" +ingress_ports = [22, 80, 443] diff --git a/2026/day-67/terraweek-capstone/variables.tf b/2026/day-67/terraweek-capstone/variables.tf new file mode 100644 index 0000000000..8a78531378 --- /dev/null +++ b/2026/day-67/terraweek-capstone/variables.tf @@ -0,0 +1,21 @@ +variable "project_name" { + type = string + default = "terraweek" +} + +variable "vpc_cidr" { + type = string +} + +variable "subnet_cidr" { + type = string +} + +variable "instance_type" { + type = string +} + +variable "ingress_ports" { + type = list(number) + default = [22, 80] +} diff --git a/2026/day-68/.terraform.lock.hcl b/2026/day-68/.terraform.lock.hcl new file mode 100644 index 0000000000..fef4fcbeba --- /dev/null +++ b/2026/day-68/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.66.0" + hashes = [ + "h1:hBEaeBm9nm7A/u1nnD0nfolTPP55/BoKRFWk8zG8/fk=", + "zh:156fe7164a3d26ef6b35734c43e99fb198df90575ed897d1182b8e930b8cd523", + "zh:1af52b22b35be00f8d16e3ebebff9fa699ec4db2ef69e6032ba5c536f80c03d9", + "zh:2545a8478bd551fdc9694f6cc1a1ad24617f6736f8bde0ad6cae90987c65380f", + "zh:4070db1ee369ccb41cb610bfd887386bc0a9b9ecad60aeb4dbce58443d2519dd", + "zh:53da7d3c1840ef875c7d34e967732502a64fe677af0e78824773d4c15a8fe740", + "zh:576a93a28bf611a4de2a2e6ced697a41d5126b8fd31d30782b16797e410a9706", + "zh:58fed5fa9a033355b9d4f3092c817b70d934100e0d8678d6e4c93f3c9493d4e4", + "zh:6a9ca2f24e2ee9156dd785d159a850b35d190e9cf7eca21cb9582970c2db80cd", + "zh:729edd30f99cc16009deba5c013265b0c81eda261a3d0821cbd011d3287fd230", + "zh:7ae460049b75bd4aefee465ef7c53a01ac2df46d4d3e3ac00824afa8b5cb83fb", + "zh:9051fa85c8034ade8a57a5c6f232fd33da28f3800bb5aa40bc8625dbc5e27632", + "zh:906547e4319805e7acf7fbdf2bac28a4b1a7370790a2a430c7adb1b29bb934eb", + "zh:998f27410a66158a35ee5ed142c27e5b21fe8601941da55da2157f8042d6dcca", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9c1804eff1dda0446dc2d215231015bb65a2fc6c3b7ba24584fe45f1ddd3fa9f", + "zh:b03ff5efdee310502aaaeb460144dc059bce72a0d8217e6b989099ef8aef9283", + ] +} diff --git a/2026/day-68/Screenshot 2026-09-23 164856.png b/2026/day-68/Screenshot 2026-09-23 164856.png new file mode 100644 index 0000000000..16e4016bc7 Binary files /dev/null and b/2026/day-68/Screenshot 2026-09-23 164856.png differ diff --git a/2026/day-68/Screenshot 2026-09-23 164920.png b/2026/day-68/Screenshot 2026-09-23 164920.png new file mode 100644 index 0000000000..1a9ad3b9b5 Binary files /dev/null and b/2026/day-68/Screenshot 2026-09-23 164920.png differ diff --git a/2026/day-68/Screenshot 2026-09-23 165037.png b/2026/day-68/Screenshot 2026-09-23 165037.png new file mode 100644 index 0000000000..428430e921 Binary files /dev/null and b/2026/day-68/Screenshot 2026-09-23 165037.png differ diff --git a/2026/day-68/Screenshot 2026-09-23 165147.png b/2026/day-68/Screenshot 2026-09-23 165147.png new file mode 100644 index 0000000000..c01a3ce23b Binary files /dev/null and b/2026/day-68/Screenshot 2026-09-23 165147.png differ diff --git a/2026/day-68/Screenshot 2026-09-23 165257.png b/2026/day-68/Screenshot 2026-09-23 165257.png new file mode 100644 index 0000000000..d2272713a0 Binary files /dev/null and b/2026/day-68/Screenshot 2026-09-23 165257.png differ diff --git a/2026/day-68/Screenshot 2026-09-23 165452.png b/2026/day-68/Screenshot 2026-09-23 165452.png new file mode 100644 index 0000000000..71198ed46b Binary files /dev/null and b/2026/day-68/Screenshot 2026-09-23 165452.png differ diff --git a/2026/day-68/Screenshot 2026-09-23 165650.png b/2026/day-68/Screenshot 2026-09-23 165650.png new file mode 100644 index 0000000000..cfad70f46a Binary files /dev/null and b/2026/day-68/Screenshot 2026-09-23 165650.png differ diff --git a/2026/day-68/Screenshot 2026-09-23 165923.png b/2026/day-68/Screenshot 2026-09-23 165923.png new file mode 100644 index 0000000000..23695534f0 Binary files /dev/null and b/2026/day-68/Screenshot 2026-09-23 165923.png differ diff --git a/2026/day-68/Screenshot 2026-09-23 170057.png b/2026/day-68/Screenshot 2026-09-23 170057.png new file mode 100644 index 0000000000..82a8e8ab2a Binary files /dev/null and b/2026/day-68/Screenshot 2026-09-23 170057.png differ diff --git a/2026/day-68/Screenshot 2026-09-23 170317.png b/2026/day-68/Screenshot 2026-09-23 170317.png new file mode 100644 index 0000000000..cdd74f1943 Binary files /dev/null and b/2026/day-68/Screenshot 2026-09-23 170317.png differ diff --git a/2026/day-68/Screenshot 2026-09-23 170538.png b/2026/day-68/Screenshot 2026-09-23 170538.png new file mode 100644 index 0000000000..043ce00697 Binary files /dev/null and b/2026/day-68/Screenshot 2026-09-23 170538.png differ diff --git a/2026/day-68/Screenshot 2026-09-23 170745.png b/2026/day-68/Screenshot 2026-09-23 170745.png new file mode 100644 index 0000000000..ba79ff498d Binary files /dev/null and b/2026/day-68/Screenshot 2026-09-23 170745.png differ diff --git a/2026/day-68/ansible-68.png b/2026/day-68/ansible-68.png new file mode 100644 index 0000000000..d8bf4554ea Binary files /dev/null and b/2026/day-68/ansible-68.png differ diff --git a/2026/day-68/ansible.cfg b/2026/day-68/ansible.cfg new file mode 100644 index 0000000000..8f61b0e186 --- /dev/null +++ b/2026/day-68/ansible.cfg @@ -0,0 +1,5 @@ +[defaults] +inventory = inventory.ini +host_key_checking = False +remote_user = ec2-user +private_key_file = ~/.ssh/terra-server-key.pem diff --git a/2026/day-68/day-68-ansible-intro.md b/2026/day-68/day-68-ansible-intro.md new file mode 100644 index 0000000000..780632ad09 --- /dev/null +++ b/2026/day-68/day-68-ansible-intro.md @@ -0,0 +1,292 @@ +# Day 68 -- Introduction to Ansible and Inventory Setup + +## Overview + +Today I practiced the basics of Ansible and learned how to manage +multiple servers from one control node. + +I used **Terraform** to create three EC2 instances in AWS. After the +servers were ready, I installed **Ansible on my WSL machine** and used +SSH to manage all three EC2 instances. + +Ansible is **agentless**, so I did not install an Ansible agent on each +EC2 instance. Ansible connected to the managed nodes using SSH. + +## Ansible Architecture + +``` text +WSL / Laptop (Control Node) + | + | Ansible + SSH + | + +----> Web Server (EC2) + +----> App Server (EC2) + +----> DB Server (EC2) +``` + +- **Control Node:** My WSL Ubuntu machine where Ansible was installed. +- **Managed Nodes:** Three AWS EC2 instances managed by Ansible. +- **Inventory:** The file that tells Ansible which servers to manage. +- **Modules:** Units of work such as `ping`, `command`, `yum`, and + `copy`. +- **Playbooks:** YAML files for repeatable Ansible tasks. This lab + focused mainly on ad-hoc commands. + +## Lab Environment + +I used Terraform to provision three Amazon Linux EC2 instances using +`t2.micro`, an SSH security group, and an existing AWS key pair. + +The servers were: + +``` text +web-server +app-server +db-server +``` + +Terraform showed: + +``` text +Plan: 4 to add, 0 to change, 0 to destroy. +``` + +After applying: + +``` text +Apply complete! Resources: 4 added, 0 changed, 0 destroyed. +``` + +## SSH Verification + +Before using Ansible, I tested SSH access to all three EC2 instances: + +``` bash +ssh -i ~/.ssh/terra-server-key.pem ec2-user@ +``` + +All three connections were successful. + +## Installing Ansible + +On my WSL Ubuntu control node: + +``` bash +sudo apt update +sudo apt install ansible -y +ansible --version +``` + +Installed version: + +``` text +ansible [core 2.16.3] +``` + +Ansible was only installed on the control node because it manages the +EC2 servers remotely through SSH. + +## Inventory Setup + +I created `inventory.ini`. The public IPs are redacted for public +documentation. + +``` ini +[web] +web-server ansible_host= + +[app] +app-server ansible_host= + +[db] +db-server ansible_host= + +[all:vars] +ansible_user=ec2-user +ansible_ssh_private_key_file=~/.ssh/terra-server-key.pem +``` + +I tested all servers: + +``` bash +ansible all -i inventory.ini -m ping +``` + +All three returned `SUCCESS` and `"ping": "pong"`. + +## Ad-Hoc Commands + +### 1. Check uptime on all servers + +``` bash +ansible all -i inventory.ini -m command -a "uptime" +``` + +### 2. Check memory on the web server + +``` bash +ansible web -i inventory.ini -m command -a "free -h" +``` + +### 3. Check disk usage on all servers + +``` bash +ansible all -i inventory.ini -m command -a "df -h" +``` + +### 4. Install Git on the web server + +``` bash +ansible web -i inventory.ini -m yum -a "name=git state=present" --become +``` + +`--become` gives elevated privileges, similar to `sudo`. It is useful +for tasks such as installing packages and managing system services. + +### 5. Copy and verify a file + +``` bash +echo "Hello from Ansible" > hello.txt +ansible all -i inventory.ini -m copy -a "src=hello.txt dest=/tmp/hello.txt" +ansible all -i inventory.ini -m command -a "cat /tmp/hello.txt" +``` + +All three servers returned: + +``` text +Hello from Ansible +``` + +## Inventory Groups and Patterns + +I added groups of groups: + +``` ini +[application:children] +web +app + +[all_servers:children] +application +db +``` + +I tested them with: + +``` bash +ansible application -i inventory.ini -m ping +ansible all_servers -i inventory.ini -m ping +``` + +I also practiced patterns: + +``` bash +ansible 'web:app' -i inventory.ini -m ping +ansible 'all:!db' -i inventory.ini -m ping +``` + +The first targets web or app. The second targets all servers except the +db group. + +## ansible.cfg + +I created: + +``` ini +[defaults] +inventory = inventory.ini +host_key_checking = False +remote_user = ec2-user +private_key_file = ~/.ssh/terra-server-key.pem +``` + +Because the project was in a Windows-mounted WSL directory, Ansible +initially ignored the local config due to the directory being +world-writable. I explicitly selected it: + +``` bash +export ANSIBLE_CONFIG="$PWD/ansible.cfg" +``` + +Then the shorter command worked: + +``` bash +ansible all -m ping +``` + +## `command` vs `shell` + +The **`command` module** runs simple commands directly on the remote +server. + +``` bash +ansible all -m command -a "uptime" +``` + +The **`shell` module** runs through a shell and supports shell features +such as pipes and redirects. + +``` bash +ansible all -m shell -a "ps aux | grep ssh" +``` + +For simple commands, I would use `command`. I would use `shell` when +shell features are required. + +## What I Learned + +My basic workflow was: + +``` text +Terraform infrastructure + ↓ +SSH verification + ↓ +Install Ansible on control node + ↓ +Create inventory + ↓ +Ansible ping + ↓ +Ad-hoc commands + ↓ +Groups and patterns + ↓ +ansible.cfg +``` + +The main thing I learned is that Ansible lets me manage multiple servers +from one control node instead of manually SSHing into each server and +repeating the same tasks. + +## Cleanup + +After completing the lab and taking screenshots, I destroyed the AWS +infrastructure: + +``` bash +terraform destroy +``` + +Terraform confirmed: + +``` text +Destroy complete! Resources: 4 destroyed. +``` + +This removed the three EC2 instances and the Terraform-managed security +group. + +## Screenshots + +Add your screenshots here before publishing: + +- AWS EC2 instances created by Terraform +- Successful `ansible all -m ping` +- Ad-hoc command outputs +- Inventory group/pattern tests +- Successful `terraform destroy` + +------------------------------------------------------------------------ + +**Day 68 completed -- Introduction to Ansible and Inventory Setup** diff --git a/2026/day-68/hello.txt b/2026/day-68/hello.txt new file mode 100644 index 0000000000..0e2f4c3cf1 --- /dev/null +++ b/2026/day-68/hello.txt @@ -0,0 +1 @@ +Hello from Ansible diff --git a/2026/day-68/inventory.ini b/2026/day-68/inventory.ini new file mode 100644 index 0000000000..be3538720e --- /dev/null +++ b/2026/day-68/inventory.ini @@ -0,0 +1,20 @@ +[web] +web-server ansible_host=3.99.161.184 + +[app] +app-server ansible_host=15.223.63.241 + +[db] +db-server ansible_host=15.223.65.125 + +[all:vars] +ansible_user=ec2-user +ansible_ssh_private_key_file=~/.ssh/terra-server-key.pem + +[application:children] +web +app + +[all_servers:children] +application +db diff --git a/2026/day-68/main.tf b/2026/day-68/main.tf new file mode 100644 index 0000000000..0e50bac0e8 --- /dev/null +++ b/2026/day-68/main.tf @@ -0,0 +1,47 @@ +data "aws_ami" "amazon_linux" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["al2023-ami-2023.*-x86_64"] + } + filter { + name = "virtualization-type" + values = ["hvm"] + } +} +resource "aws_security_group" "ansible_ssh" { + name = "ansible-lab-ssh" + description = "Allow SSH for Ansible lab" + + ingress { + description = "SSH" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + +} + +resource "aws_instance" "ansible_nodes" { + count = length(var.instance_names) + + ami = data.aws_ami.amazon_linux.id + instance_type = var.instance_type + key_name = var.key_name + associate_public_ip_address = true + vpc_security_group_ids = [aws_security_group.ansible_ssh.id] + + tags = { + Name = var.instance_names[count.index] + } +} diff --git a/2026/day-68/terraform.tfvars b/2026/day-68/terraform.tfvars new file mode 100644 index 0000000000..8e14b136da --- /dev/null +++ b/2026/day-68/terraform.tfvars @@ -0,0 +1 @@ +key_name = "terra-server-key" diff --git a/2026/day-68/terraform/providers.tf b/2026/day-68/terraform/providers.tf new file mode 100644 index 0000000000..f7170922fb --- /dev/null +++ b/2026/day-68/terraform/providers.tf @@ -0,0 +1,11 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = " ~>5.0" + } + } +} +providers "aws" { + region = "ca-central-1" +} diff --git a/2026/day-68/variables.tf b/2026/day-68/variables.tf new file mode 100644 index 0000000000..1f742d43ac --- /dev/null +++ b/2026/day-68/variables.tf @@ -0,0 +1,23 @@ +variable "aws_region" { + description = "Aws region fo Ansible" + type = string + default = "ca-central-1" +} +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t2.micro" +} + +variable "instance_names" { + description = "Names of EC2 instances maanged by Ansible" + type = list(string) + + + default = [ + "web-server", "app-server", "db-server"] +} +variable "key_name" { + description = "AWS key pair namr for SSH" + type = string +} diff --git a/2026/day-69/.terraform.lock.hcl b/2026/day-69/.terraform.lock.hcl new file mode 100644 index 0000000000..fef4fcbeba --- /dev/null +++ b/2026/day-69/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.66.0" + hashes = [ + "h1:hBEaeBm9nm7A/u1nnD0nfolTPP55/BoKRFWk8zG8/fk=", + "zh:156fe7164a3d26ef6b35734c43e99fb198df90575ed897d1182b8e930b8cd523", + "zh:1af52b22b35be00f8d16e3ebebff9fa699ec4db2ef69e6032ba5c536f80c03d9", + "zh:2545a8478bd551fdc9694f6cc1a1ad24617f6736f8bde0ad6cae90987c65380f", + "zh:4070db1ee369ccb41cb610bfd887386bc0a9b9ecad60aeb4dbce58443d2519dd", + "zh:53da7d3c1840ef875c7d34e967732502a64fe677af0e78824773d4c15a8fe740", + "zh:576a93a28bf611a4de2a2e6ced697a41d5126b8fd31d30782b16797e410a9706", + "zh:58fed5fa9a033355b9d4f3092c817b70d934100e0d8678d6e4c93f3c9493d4e4", + "zh:6a9ca2f24e2ee9156dd785d159a850b35d190e9cf7eca21cb9582970c2db80cd", + "zh:729edd30f99cc16009deba5c013265b0c81eda261a3d0821cbd011d3287fd230", + "zh:7ae460049b75bd4aefee465ef7c53a01ac2df46d4d3e3ac00824afa8b5cb83fb", + "zh:9051fa85c8034ade8a57a5c6f232fd33da28f3800bb5aa40bc8625dbc5e27632", + "zh:906547e4319805e7acf7fbdf2bac28a4b1a7370790a2a430c7adb1b29bb934eb", + "zh:998f27410a66158a35ee5ed142c27e5b21fe8601941da55da2157f8042d6dcca", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9c1804eff1dda0446dc2d215231015bb65a2fc6c3b7ba24584fe45f1ddd3fa9f", + "zh:b03ff5efdee310502aaaeb460144dc059bce72a0d8217e6b989099ef8aef9283", + ] +} diff --git a/2026/day-69/Screenshot 2026-09-24 174519.png b/2026/day-69/Screenshot 2026-09-24 174519.png new file mode 100644 index 0000000000..5a27a680aa Binary files /dev/null and b/2026/day-69/Screenshot 2026-09-24 174519.png differ diff --git a/2026/day-69/Screenshot 2026-09-24 174536.png b/2026/day-69/Screenshot 2026-09-24 174536.png new file mode 100644 index 0000000000..b6928057f4 Binary files /dev/null and b/2026/day-69/Screenshot 2026-09-24 174536.png differ diff --git a/2026/day-69/Screenshot 2026-09-24 174556.png b/2026/day-69/Screenshot 2026-09-24 174556.png new file mode 100644 index 0000000000..21c33cea33 Binary files /dev/null and b/2026/day-69/Screenshot 2026-09-24 174556.png differ diff --git a/2026/day-69/Screenshot 2026-09-24 174823.png b/2026/day-69/Screenshot 2026-09-24 174823.png new file mode 100644 index 0000000000..f17a741ba2 Binary files /dev/null and b/2026/day-69/Screenshot 2026-09-24 174823.png differ diff --git a/2026/day-69/ansible.cfg b/2026/day-69/ansible.cfg new file mode 100644 index 0000000000..8f61b0e186 --- /dev/null +++ b/2026/day-69/ansible.cfg @@ -0,0 +1,5 @@ +[defaults] +inventory = inventory.ini +host_key_checking = False +remote_user = ec2-user +private_key_file = ~/.ssh/terra-server-key.pem diff --git a/2026/day-69/day-69-playbooks.md b/2026/day-69/day-69-playbooks.md new file mode 100644 index 0000000000..333c34309d --- /dev/null +++ b/2026/day-69/day-69-playbooks.md @@ -0,0 +1,420 @@ +# Day 69 -- Ansible Playbooks and Modules + +## Overview + +Today I moved from Ansible ad-hoc commands to **Ansible Playbooks**. My +goal was to understand how playbooks are structured and how they can +configure servers in a repeatable way. + +I used **WSL Ubuntu as my Ansible control node** and AWS EC2 instances +as managed nodes. My inventory had three server groups: `web`, `app`, +and `db`. + +## What I Practiced + +- Writing and running Ansible playbooks +- Plays, tasks, modules, and arguments +- Package and service management +- Copying files and creating directories +- `command`, `shell`, `register`, and `debug` +- `lineinfile` +- Handlers and `notify` +- Idempotency +- Check mode, diff mode, verbosity, and limits +- Multi-play playbooks + +## Lab Setup + +- **Control Node:** WSL Ubuntu +- **Managed Nodes:** 3 AWS EC2 instances +- **OS:** Amazon Linux +- **Connection:** SSH +- **Groups:** `web`, `app`, `db` +- **Privilege escalation:** `become: true` + +I verified connectivity with: + +``` bash +ansible all -m ping +``` + +All three servers returned `SUCCESS` and `pong`. + +> I intentionally did not include public IP addresses or SSH key details +> in this documentation. + +## Task 1 -- First Playbook + +I created `install-nginx.yml` to configure the web server. + +``` yaml +--- +- name: Install and start Nginx on web servers + hosts: web + become: true + + tasks: + - name: Install Nginx + yum: + name: nginx + state: present + + - name: Start and enable Nginx + service: + name: nginx + state: started + enabled: true + + - name: Create a custom index page + copy: + content: "

Deployed by Ansible - TerraWeek Server

" + dest: /usr/share/nginx/html/index.html +``` + +I checked the syntax and then ran it: + +``` bash +ansible-playbook install-nginx.yml --syntax-check +ansible-playbook install-nginx.yml +``` + +First run: + +``` text +web-server : ok=4 changed=3 unreachable=0 failed=0 +``` + +I ran the same playbook again: + +``` text +web-server : ok=4 changed=0 unreachable=0 failed=0 +``` + +This helped me understand **idempotency**. Ansible checks the current +state first. If the server is already in the required state, it does not +make the same change again. + +## Task 2 -- Understanding Playbook Structure + +I learned the basic parts of a playbook: + +- **Playbook:** the complete YAML automation file. +- **Play:** defines the hosts and tasks to run. +- **Task:** one specific job. +- **Module:** the Ansible tool that performs the job. +- **Arguments:** values/settings given to a module. +- **`become: true`:** allows tasks that require elevated privileges. + +For example: + +``` yaml +- name: Install Nginx + yum: + name: nginx + state: present +``` + +Here, `Install Nginx` is the task and `yum` is the module. + +## Task 3 -- Essential Ansible Modules + +I created `essential-modules.yml` and a sample configuration file: + +``` bash +mkdir -p files +echo "This is my Ansible app configuration" > files/app.conf +``` + +I practiced important modules including: + +- `yum` +- `copy` +- `file` +- `command` +- `shell` +- `debug` +- `lineinfile` +- `service` in my Day 69 playbooks + +### Register and Debug + +I used `command` to check disk space: + +``` yaml +command: df -h +register: disk_output +``` + +Then displayed the saved result: + +``` yaml +debug: + var: disk_output.stdout_lines +``` + +This showed me how `register` saves task output into a variable. + +### Command vs Shell + +I used: + +``` yaml +command: df -h +``` + +for a normal command. + +I used: + +``` yaml +shell: ps aux | wc -l +``` + +when I needed a shell pipe. + +My simple rule is: + +- `command` → normal command without shell features. +- `shell` → when I need pipes, redirects, or other shell features. + +### Troubleshooting Curl + +I faced a real package issue during this task. Amazon Linux already had +`curl-minimal`, and installing the regular `curl` package caused a +package conflict. + +For this lab, I kept the existing curl capability and removed the +conflicting regular `curl` package from the package installation list. + +### Troubleshooting YAML + +I also had a YAML indentation error: + +``` text +did not find expected '-' indicator +``` + +I fixed the spacing/indentation and ran the playbook again. + +The final result was successful: + +``` text +app-server : ok=9 changed=6 unreachable=0 failed=0 +db-server : ok=9 changed=6 unreachable=0 failed=0 +web-server : ok=9 changed=6 unreachable=0 failed=0 +``` + +This reminded me that YAML indentation is very important in Ansible. + +## Task 4 -- Handlers and Notify + +I created `nginx-config.yml` to practice handlers. + +The task used: + +``` yaml +notify: Restart Nginx +``` + +The handler used: + +``` yaml +handlers: + - name: Restart Nginx + service: + name: nginx + state: restarted +``` + +On the first run, the configuration changed and the handler ran: + +``` text +TASK [Update Nginx configuration] +changed: [web-server] + +RUNNING HANDLER [Restart Nginx] +changed: [web-server] +``` + +Result: + +``` text +web-server : ok=3 changed=2 unreachable=0 failed=0 +``` + +I ran the same playbook again. This time: + +``` text +TASK [Update Nginx configuration] +ok: [web-server] +``` + +Result: + +``` text +web-server : ok=2 changed=0 unreachable=0 failed=0 +``` + +The handler did not run the second time because nothing changed. This +showed me that handlers can avoid unnecessary service restarts. + +## Task 5 -- Testing and Debugging + +I practiced these commands: + +### Check Mode + +``` bash +ansible-playbook install-nginx.yml --check +``` + +This previews what the playbook would do without normally applying the +changes. + +### Check and Diff + +``` bash +ansible-playbook nginx-config.yml --check --diff +``` + +`--diff` helps show file differences before applying a change. + +### Verbosity + +``` bash +ansible-playbook install-nginx.yml -v +``` + +Other levels include: + +``` bash +-v +-vv +-vvv +``` + +More `v` characters give more troubleshooting information. + +### Limit a Run + +``` bash +ansible-playbook install-nginx.yml --limit web-server +``` + +### List Hosts + +``` bash +ansible-playbook install-nginx.yml --list-hosts +``` + +### List Tasks + +``` bash +ansible-playbook install-nginx.yml --list-tasks +``` + +I learned that `--check --diff` is useful before production changes +because I can review expected changes before applying them. + +## Task 6 -- Multi-Play Playbook + +I created `multi-play.yml` with separate plays for different server +roles. + +The structure was: + +``` text +multi-play.yml +├── Web Play → Nginx +├── App Play → gcc, make, /opt/app +└── DB Play → database client, /var/lib/appdata +``` + +This helped me understand that one playbook can contain multiple plays +and configure different groups differently. + +> I did not save/verify the final Task 6 PLAY RECAP before destroying +> the EC2 lab, so I am not claiming a successful Task 6 execution result +> here. + +## AWS Cleanup + +After finishing the lab work, I destroyed the Terraform-managed AWS +resources: + +``` bash +terraform destroy +``` + +Terraform showed: + +``` text +Plan: 0 to add, 0 to change, 4 to destroy. +``` + +After confirmation, the result was: + +``` text +Destroy complete! Resources: 4 destroyed. +``` + +I verified the state: + +``` bash +terraform state list +``` + +It returned nothing, confirming that Terraform had no managed Day 69 +resources left. + +## What I Learned + +This lab helped me understand that Ansible playbooks make server +configuration repeatable and easier to manage. + +My main takeaways are: + +1. Playbooks describe the configuration I want. +2. Plays target specific hosts or groups. +3. Tasks are individual jobs. +4. Modules perform those jobs. +5. Idempotency prevents unnecessary repeated changes. +6. Handlers run actions such as service restarts only when notified by + a change. +7. `register` and `debug` help capture and inspect task output. +8. Check and diff modes help review changes before applying them. +9. One playbook can contain multiple plays for different server roles. +10. Real automation work includes troubleshooting YAML and + operating-system package issues. + +## Useful Day 69 Commands + +``` bash +ansible all -m ping + +ansible-playbook install-nginx.yml --syntax-check +ansible-playbook install-nginx.yml + +ansible-playbook install-nginx.yml --check +ansible-playbook nginx-config.yml --check --diff + +ansible-playbook install-nginx.yml -v +ansible-playbook install-nginx.yml --limit web-server +ansible-playbook install-nginx.yml --list-hosts +ansible-playbook install-nginx.yml --list-tasks + +terraform destroy +terraform state list +``` + +## Final Thoughts + +Day 69 was useful because I did more than just write YAML. I saw how +Ansible behaves when a server is already configured, how handlers +prevent unnecessary restarts, and how real errors can be troubleshooted. + +The biggest lesson for me was that automation is not only about writing +a playbook. I also need to validate it, test it, understand the output, +troubleshoot failures, and clean up the infrastructure when I finish. + +**Day 69 -- Ansible Playbooks and Modules** diff --git a/2026/day-69/day-69.png b/2026/day-69/day-69.png new file mode 100644 index 0000000000..602ed0d3f5 Binary files /dev/null and b/2026/day-69/day-69.png differ diff --git a/2026/day-69/essential-modules.yml b/2026/day-69/essential-modules.yml new file mode 100644 index 0000000000..87504df4f8 --- /dev/null +++ b/2026/day-69/essential-modules.yml @@ -0,0 +1,50 @@ +--- +- name: Practice essential Ansible modules + hosts: all + become: true + + tasks: + - name: Install packages + yum: + name: + - git + - wget + - tree + state: present + + - name: Copy config file + copy: + src: files/app.conf + dest: /etc/app.conf + owner: root + group: root + mode: '0644' + + - name: Create application directory + file: + path: /opt/myapp + state: directory + owner: ec2-user + mode: '0755' + + - name: Check disk space + command: df -h + register: disk_output + + - name: Print disk space + debug: + var: disk_output.stdout_lines + + - name: Count running processes + shell: ps aux | wc -l + register: process_count + + - name: Show process count + debug: + msg: "Total processes: {{ process_count.stdout }}" + + - name: Set timezone in environment + lineinfile: + path: /etc/environment + line: 'TZ=Asia/Kolkata' + create: true diff --git a/2026/day-69/files/app.conf b/2026/day-69/files/app.conf new file mode 100644 index 0000000000..77707f6ba8 --- /dev/null +++ b/2026/day-69/files/app.conf @@ -0,0 +1 @@ +This is my Ansible app configuration diff --git a/2026/day-69/hello.txt b/2026/day-69/hello.txt new file mode 100644 index 0000000000..0e2f4c3cf1 --- /dev/null +++ b/2026/day-69/hello.txt @@ -0,0 +1 @@ +Hello from Ansible diff --git a/2026/day-69/install-nginx.yml b/2026/day-69/install-nginx.yml new file mode 100644 index 0000000000..6bf109751e --- /dev/null +++ b/2026/day-69/install-nginx.yml @@ -0,0 +1,21 @@ +--- +- name: Install and start Nginx on web servers + hosts: web + become: true + + tasks: + - name: Install Nginx + yum: + name: nginx + state: present + + - name: Start and enable Nginx + service: + name: nginx + state: started + enabled: true + + - name: Create a custom index page + copy: + content: "

Deployed by Ansible - TerraWeek Server

" + dest: /usr/share/nginx/html/index.html diff --git a/2026/day-69/inventory.ini b/2026/day-69/inventory.ini new file mode 100644 index 0000000000..8577349081 --- /dev/null +++ b/2026/day-69/inventory.ini @@ -0,0 +1,20 @@ +[web] +web-server ansible_host=99.79.123.46 + +[app] +app-server ansible_host=3.99.139.7 + +[db] +db-server ansible_host=15.223.183.251 + +[all:vars] +ansible_user=ec2-user +ansible_ssh_private_key_file=~/.ssh/terra-server-key.pem + +[application:children] +web +app + +[all_servers:children] +application +db diff --git a/2026/day-69/main.tf b/2026/day-69/main.tf new file mode 100644 index 0000000000..0e50bac0e8 --- /dev/null +++ b/2026/day-69/main.tf @@ -0,0 +1,47 @@ +data "aws_ami" "amazon_linux" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["al2023-ami-2023.*-x86_64"] + } + filter { + name = "virtualization-type" + values = ["hvm"] + } +} +resource "aws_security_group" "ansible_ssh" { + name = "ansible-lab-ssh" + description = "Allow SSH for Ansible lab" + + ingress { + description = "SSH" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + +} + +resource "aws_instance" "ansible_nodes" { + count = length(var.instance_names) + + ami = data.aws_ami.amazon_linux.id + instance_type = var.instance_type + key_name = var.key_name + associate_public_ip_address = true + vpc_security_group_ids = [aws_security_group.ansible_ssh.id] + + tags = { + Name = var.instance_names[count.index] + } +} diff --git a/2026/day-69/multi-play.yml b/2026/day-69/multi-play.yml new file mode 100644 index 0000000000..77c8883467 --- /dev/null +++ b/2026/day-69/multi-play.yml @@ -0,0 +1,52 @@ +--- +- name: Configure web servers + hosts: web + become: true + + tasks: + - name: Install Nginx + yum: + name: nginx + state: present + + - name: Start Nginx + service: + name: nginx + state: started + enabled: true + + +- name: Configure application servers + hosts: app + become: true + + tasks: + - name: Install development tools + yum: + name: + - gcc + - make + state: present + + - name: Create application directory + file: + path: /opt/app + state: directory + mode: '0755' + + +- name: Configure database servers + hosts: db + become: true + + tasks: + - name: Install MySQL client + yum: + name: mariadb105 + state: present + + - name: Create application data directory + file: + path: /var/lib/appdata + state: directory + mode: '0755' diff --git a/2026/day-69/nginx-config.yml b/2026/day-69/nginx-config.yml new file mode 100644 index 0000000000..e6e94599df --- /dev/null +++ b/2026/day-69/nginx-config.yml @@ -0,0 +1,25 @@ +--- +- name: Configure Nginx with handler + hosts: web + become: true + + tasks: + - name: Update Nginx configuration + copy: + content: | + server { + listen 80; + server_name _; + location / { + root /usr/share/nginx/html; + index index.html; + } + } + dest: /etc/nginx/conf.d/custom.conf + notify: Restart Nginx + + handlers: + - name: Restart Nginx + service: + name: nginx + state: restarted diff --git a/2026/day-69/terraform.tfvars b/2026/day-69/terraform.tfvars new file mode 100644 index 0000000000..8e14b136da --- /dev/null +++ b/2026/day-69/terraform.tfvars @@ -0,0 +1 @@ +key_name = "terra-server-key" diff --git a/2026/day-69/terraform/providers.tf b/2026/day-69/terraform/providers.tf new file mode 100644 index 0000000000..f7170922fb --- /dev/null +++ b/2026/day-69/terraform/providers.tf @@ -0,0 +1,11 @@ +terraform { + required_providers { + aws = { + source = "hashicorp/aws" + version = " ~>5.0" + } + } +} +providers "aws" { + region = "ca-central-1" +} diff --git a/2026/day-69/variables.tf b/2026/day-69/variables.tf new file mode 100644 index 0000000000..1f742d43ac --- /dev/null +++ b/2026/day-69/variables.tf @@ -0,0 +1,23 @@ +variable "aws_region" { + description = "Aws region fo Ansible" + type = string + default = "ca-central-1" +} +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t2.micro" +} + +variable "instance_names" { + description = "Names of EC2 instances maanged by Ansible" + type = list(string) + + + default = [ + "web-server", "app-server", "db-server"] +} +variable "key_name" { + description = "AWS key pair namr for SSH" + type = string +} diff --git a/2026/day-70/.terraform.lock.hcl b/2026/day-70/.terraform.lock.hcl new file mode 100644 index 0000000000..fef4fcbeba --- /dev/null +++ b/2026/day-70/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.66.0" + hashes = [ + "h1:hBEaeBm9nm7A/u1nnD0nfolTPP55/BoKRFWk8zG8/fk=", + "zh:156fe7164a3d26ef6b35734c43e99fb198df90575ed897d1182b8e930b8cd523", + "zh:1af52b22b35be00f8d16e3ebebff9fa699ec4db2ef69e6032ba5c536f80c03d9", + "zh:2545a8478bd551fdc9694f6cc1a1ad24617f6736f8bde0ad6cae90987c65380f", + "zh:4070db1ee369ccb41cb610bfd887386bc0a9b9ecad60aeb4dbce58443d2519dd", + "zh:53da7d3c1840ef875c7d34e967732502a64fe677af0e78824773d4c15a8fe740", + "zh:576a93a28bf611a4de2a2e6ced697a41d5126b8fd31d30782b16797e410a9706", + "zh:58fed5fa9a033355b9d4f3092c817b70d934100e0d8678d6e4c93f3c9493d4e4", + "zh:6a9ca2f24e2ee9156dd785d159a850b35d190e9cf7eca21cb9582970c2db80cd", + "zh:729edd30f99cc16009deba5c013265b0c81eda261a3d0821cbd011d3287fd230", + "zh:7ae460049b75bd4aefee465ef7c53a01ac2df46d4d3e3ac00824afa8b5cb83fb", + "zh:9051fa85c8034ade8a57a5c6f232fd33da28f3800bb5aa40bc8625dbc5e27632", + "zh:906547e4319805e7acf7fbdf2bac28a4b1a7370790a2a430c7adb1b29bb934eb", + "zh:998f27410a66158a35ee5ed142c27e5b21fe8601941da55da2157f8042d6dcca", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9c1804eff1dda0446dc2d215231015bb65a2fc6c3b7ba24584fe45f1ddd3fa9f", + "zh:b03ff5efdee310502aaaeb460144dc059bce72a0d8217e6b989099ef8aef9283", + ] +} diff --git a/2026/day-70/Day-70.png b/2026/day-70/Day-70.png new file mode 100644 index 0000000000..362551a748 Binary files /dev/null and b/2026/day-70/Day-70.png differ diff --git a/2026/day-70/Screenshot 2026-09-25 130351.png b/2026/day-70/Screenshot 2026-09-25 130351.png new file mode 100644 index 0000000000..6f53265029 Binary files /dev/null and b/2026/day-70/Screenshot 2026-09-25 130351.png differ diff --git a/2026/day-70/Screenshot 2026-09-25 131354.png b/2026/day-70/Screenshot 2026-09-25 131354.png new file mode 100644 index 0000000000..6a36195fc2 Binary files /dev/null and b/2026/day-70/Screenshot 2026-09-25 131354.png differ diff --git a/2026/day-70/Screenshot 2026-09-25 131407.png b/2026/day-70/Screenshot 2026-09-25 131407.png new file mode 100644 index 0000000000..581f1e5e2e Binary files /dev/null and b/2026/day-70/Screenshot 2026-09-25 131407.png differ diff --git a/2026/day-70/Screenshot 2026-09-25 135220.png b/2026/day-70/Screenshot 2026-09-25 135220.png new file mode 100644 index 0000000000..eff8c66299 Binary files /dev/null and b/2026/day-70/Screenshot 2026-09-25 135220.png differ diff --git a/2026/day-70/Screenshot 2026-09-25 135355.png b/2026/day-70/Screenshot 2026-09-25 135355.png new file mode 100644 index 0000000000..4ebc7baee0 Binary files /dev/null and b/2026/day-70/Screenshot 2026-09-25 135355.png differ diff --git a/2026/day-70/ansible.cfg b/2026/day-70/ansible.cfg new file mode 100644 index 0000000000..8f61b0e186 --- /dev/null +++ b/2026/day-70/ansible.cfg @@ -0,0 +1,5 @@ +[defaults] +inventory = inventory.ini +host_key_checking = False +remote_user = ec2-user +private_key_file = ~/.ssh/terra-server-key.pem diff --git a/2026/day-70/day-70-variables-loops.md b/2026/day-70/day-70-variables-loops.md new file mode 100644 index 0000000000..4a3e036c87 --- /dev/null +++ b/2026/day-70/day-70-variables-loops.md @@ -0,0 +1,270 @@ +# Day 70 – Variables, Facts, Conditionals and Loops + +## Objective +Practice Ansible variables, `group_vars`, `host_vars`, facts, conditionals, loops, `register`, and `debug` using three AWS EC2 managed nodes: `web-server`, `app-server`, and `db-server` on Amazon Linux 2023. + +## Task 1 – Variables +Created `variables-demo.yml` with reusable values such as `app_name`, `app_port`, `app_dir`, and a package list. `app_dir` used `"/opt/{{ app_name }}"`. + +Tested an override with: +```bash +ansible-playbook variables-demo.yml -e "app_name=my-custom-app app_port=9090" +``` +Result: `app_name` became `my-custom-app`, port became `9090`, and the directory automatically became `/opt/my-custom-app`. + +**Lesson:** A variable stores a value. `{{ variable_name }}` uses its current value. `-e` can override values for a playbook run. + +## Task 2 – group_vars and host_vars +Final structure: +```text +day-70/ +├── inventory.ini +├── ansible.cfg +├── group_vars/ +│ ├── all.yml +│ ├── web.yml +│ └── db.yml +├── host_vars/ +│ └── web-server.yml +└── playbooks/ + └── site.yml +``` + +`group_vars/all.yml`: +```yaml +--- +ntp_server: pool.ntp.org +app_env: development +common_packages: + - vim + - htop + - tree +``` + +`group_vars/web.yml`: +```yaml +--- +http_port: 80 +max_connections: 1000 +web_packages: + - nginx +``` + +`group_vars/db.yml`: +```yaml +--- +db_port: 3306 +db_packages: + - mysql-server +``` + +`host_vars/web-server.yml`: +```yaml +--- +max_connections: 2000 +custom_message: "This is the primary web server" +``` + +Verified: +```bash +ansible web-server -m debug -a "var=http_port" +ansible web-server -m debug -a "var=max_connections" +``` +Results: `http_port = 80` and `max_connections = 2000`. + +Successful output included: +```text +Environment: development +HTTP port: 80, Max connections: 2000 +This is the primary web server +``` + +**Lesson:** `group_vars` stores values for groups; `host_vars` stores values for a specific host. The host-specific `max_connections: 2000` was used for `web-server` instead of the group value `1000`. + +## Task 3 – Ansible Facts +Facts are information Ansible automatically discovers from managed nodes. + +Commands used: +```bash +ansible web-server -m setup -a "filter=ansible_distribution*" +ansible web-server -m setup -a "filter=ansible_memtotal_mb" +ansible web-server -m setup -a "filter=ansible_default_ipv4" +``` + +Useful facts discovered: +- `ansible_distribution`: Amazon +- `ansible_distribution_version`: 2023 +- `ansible_memtotal_mb`: 957 MB +- `ansible_default_ipv4.address`: web server private IP `172.31.24.63` +- `ansible_interfaces`: network interfaces such as `lo` and `enX0` + +The `facts-demo.yml` playbook displayed hostname, OS, version, memory, private IP, and interfaces for all three servers. + +**Lesson:** Variables are information we give Ansible. Facts are information Ansible discovers from the server. + +## Task 4 – Conditionals +Used `when` to run tasks only when conditions were true. + +Examples: +```yaml +when: "'web' in group_names" +``` +```yaml +when: ansible_memtotal_mb < 1024 +``` +```yaml +when: ansible_distribution == "Amazon" +``` +```yaml +when: + - "'web' in group_names" + - app_env == "production" +``` +```yaml +when: "'web' in group_names or 'app' in group_names" +``` + +Observed results: +- Nginx task executed only on `web-server`. +- Database conditional executed only on `db-server`. +- Low-memory warning ran because each server reported 957 MB RAM. +- Amazon Linux message executed. +- Ubuntu message was skipped. +- Production check was skipped because `app_env` was `development`. +- Web-or-app condition executed on `web-server` and `app-server`. + +The original DB package task returned `No package mysql-server available` on Amazon Linux 2023. For this conditional exercise, it was replaced with a `debug` task so the DB-group condition could be tested successfully. + +**Lesson:** `when` means run the task only if the condition is true. Otherwise Ansible reports `skipped`. + +## Task 5 – Loops +Used loops to avoid repeating tasks. + +Users: +```yaml +users: + - alice + - bob + - charlie +``` + +Directories: +```yaml +directories: + - /opt/app + - /opt/app/logs + - /opt/app/config +``` + +Example: +```yaml +- name: Create multiple users + user: + name: "{{ item }}" + state: present + loop: "{{ users }}" +``` + +A product loop generated user-directory combinations: +```yaml +loop: "{{ users | product(directories) | list }}" +``` + +Examples included: +```text +User alice -> Directory /opt/app +User bob -> Directory /opt/app/logs +User charlie -> Directory /opt/app/config +``` + +**Lesson:** A loop runs one task for multiple items. `item` is the current value being processed. + +## Task 6 – Register, Debug and Server Health Report +Created `server-report.yml`. + +Example: +```yaml +- name: Check disk space + command: df -h / + register: disk_result + +- name: Check memory + command: free -m + register: memory_result +``` + +Flow: +```text +command runs → register saves result → debug displays it → when checks it → report is saved +``` + +Example verified web-server report: +```text +Server: web-server +OS: Amazon 2023 +IP: 172.31.24.63 +RAM: 957MB +Disk: Filesystem Size Used Avail Use% Mounted on +/dev/xvda1 8.0G 1.8G 6.2G 23% / +Checked at: 2026-09-25T17:51:22Z +``` + +The disk alert was skipped because usage was 23%, below the critical threshold. + +Report files created: +```text +/tmp/server-report-app-server.txt +/tmp/server-report-web-server.txt +/tmp/server-report-db-server.txt +``` + +Verified with: +```bash +ansible all -m shell -a "ls -l /tmp/server-report-*.txt" +ansible web-server -m command -a "cat /tmp/server-report-web-server.txt" +``` + +## Troubleshooting +### curl conflict +Amazon Linux 2023 already had `curl-minimal`; installing regular `curl` caused a conflict. The package list was changed to use `curl-minimal`. + +### Undefined variables +Some variable files existed but were empty because their Vim contents had not been saved. `cat` and `find` were used to verify files, then the YAML was saved correctly. + +### MySQL package +The DB conditional correctly targeted `db-server`, but Amazon Linux 2023 reported `No package mysql-server available`. The task was changed to `debug` for the conditional-focused exercise. + +## Important Commands Practiced +```bash +ansible all -m ping +ansible web-server -m setup -a "filter=ansible_distribution*" +ansible web-server -m setup -a "filter=ansible_memtotal_mb" +ansible web-server -m setup -a "filter=ansible_default_ipv4" +ansible web-server -m debug -a "var=http_port" +ansible web-server -m debug -a "var=max_connections" +ansible-playbook variables-demo.yml +ansible-playbook variables-demo.yml -e "app_name=my-custom-app app_port=9090" +ansible-playbook playbooks/site.yml +ansible-playbook playbooks/facts-demo.yml +ansible-playbook playbooks/conditional-demo.yml +ansible-playbook playbooks/loops-demo.yml +ansible-playbook playbooks/server-report.yml +``` + +## Interview Notes +**What are Ansible variables?** Variables store reusable values that can be referenced inside playbooks. + +**What are Ansible facts?** Facts are information Ansible automatically discovers about managed nodes, such as OS, memory, IP address, and network interfaces. + +**What is a conditional?** A conditional uses `when` to run a task only when a condition is true. + +**What is a loop?** A loop runs the same task for multiple items and avoids repeated YAML. + +**What does register do?** `register` captures a task's result into a variable so later tasks can display, check, or use it. + +**What is variable precedence?** Variable precedence decides which value Ansible uses when the same variable is defined in more than one place. + +## Day 70 Result +Successfully practiced variables, `-e` overrides, `group_vars`, `host_vars`, variable precedence, facts, `when`, AND/OR conditions, loops, `item`, product loops, `register`, `debug`, health reporting, conditional alerts, and report-file creation. + +All final Day 70 playbook runs completed with `failed=0` after troubleshooting. diff --git a/2026/day-70/group_vars/all.yml b/2026/day-70/group_vars/all.yml new file mode 100644 index 0000000000..4a45a495b7 --- /dev/null +++ b/2026/day-70/group_vars/all.yml @@ -0,0 +1,7 @@ +--- +ntp_server: pool.ntp.org +app_env: development +common_packages: + - vim + - htop + - tree diff --git a/2026/day-70/group_vars/db.yml b/2026/day-70/group_vars/db.yml new file mode 100644 index 0000000000..04c2fa4587 --- /dev/null +++ b/2026/day-70/group_vars/db.yml @@ -0,0 +1,4 @@ +--- +db_port: 3306 +db_packages: + - mysql-server diff --git a/2026/day-70/group_vars/web.yml b/2026/day-70/group_vars/web.yml new file mode 100644 index 0000000000..0df5657056 --- /dev/null +++ b/2026/day-70/group_vars/web.yml @@ -0,0 +1,5 @@ +--- +http_port: 80 +max_connections: 1000 +web_packages: + - nginx diff --git a/2026/day-70/host_vars/web-server.yml b/2026/day-70/host_vars/web-server.yml new file mode 100644 index 0000000000..8fb2732933 --- /dev/null +++ b/2026/day-70/host_vars/web-server.yml @@ -0,0 +1,3 @@ +--- +max_connections: 2000 +custom_message: "This is the primary web server" diff --git a/2026/day-70/inventory.ini b/2026/day-70/inventory.ini new file mode 100644 index 0000000000..1966da8d89 --- /dev/null +++ b/2026/day-70/inventory.ini @@ -0,0 +1,21 @@ +[web] +web-server ansible_host=3.96.127.95 + +[app] +app-server ansible_host=16.52.169.142 + +[db] +db-server ansible_host=15.222.1.156 + +[all:vars] +ansible_user=ec2-user +ansible_ssh_private_key_file=~/.ssh/terra-server-key.pem + +[application:children] +web +app + +[all_servers:children] +application +db + diff --git a/2026/day-70/main.tf b/2026/day-70/main.tf new file mode 100644 index 0000000000..0e50bac0e8 --- /dev/null +++ b/2026/day-70/main.tf @@ -0,0 +1,47 @@ +data "aws_ami" "amazon_linux" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["al2023-ami-2023.*-x86_64"] + } + filter { + name = "virtualization-type" + values = ["hvm"] + } +} +resource "aws_security_group" "ansible_ssh" { + name = "ansible-lab-ssh" + description = "Allow SSH for Ansible lab" + + ingress { + description = "SSH" + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + +} + +resource "aws_instance" "ansible_nodes" { + count = length(var.instance_names) + + ami = data.aws_ami.amazon_linux.id + instance_type = var.instance_type + key_name = var.key_name + associate_public_ip_address = true + vpc_security_group_ids = [aws_security_group.ansible_ssh.id] + + tags = { + Name = var.instance_names[count.index] + } +} diff --git a/2026/day-70/playbooks/conditional-demo.yml b/2026/day-70/playbooks/conditional-demo.yml new file mode 100644 index 0000000000..11d3c601c6 --- /dev/null +++ b/2026/day-70/playbooks/conditional-demo.yml @@ -0,0 +1,43 @@ +--- +- name: Conditional demo + hosts: all + become: true + + tasks: + - name: Install Nginx only on web servers + yum: + name: nginx + state: present + when: "'web' in group_names" + + - name: Database server conditional test + debug: + msg: "This task runs only on the database server" + when: "'db' in group_names" + + - name: Warn about low memory + debug: + msg: "WARNING: This server has less than 1GB RAM" + when: ansible_memtotal_mb < 1024 + + - name: Message for Amazon Linux + debug: + msg: "This server is running Amazon Linux" + when: ansible_distribution == "Amazon" + + - name: Message for Ubuntu + debug: + msg: "This server is running Ubuntu" + when: ansible_distribution == "Ubuntu" + + - name: Production web server check + debug: + msg: "This is a production web server" + when: + - "'web' in group_names" + - app_env == "production" + + - name: Web or app server check + debug: + msg: "This is a web or application server" + when: "'web' in group_names or 'app' in group_names" diff --git a/2026/day-70/playbooks/facts-demo.yml b/2026/day-70/playbooks/facts-demo.yml new file mode 100644 index 0000000000..6efdfd24c6 --- /dev/null +++ b/2026/day-70/playbooks/facts-demo.yml @@ -0,0 +1,14 @@ +--- +- name: Facts demo + hosts: all + + tasks: + - name: Show server facts + debug: + msg: + - "Hostname: {{ ansible_hostname }}" + - "OS: {{ ansible_distribution }}" + - "OS Version: {{ ansible_distribution_version }}" + - "Memory: {{ ansible_memtotal_mb }} MB" + - "IP Address: {{ ansible_default_ipv4.address }}" + - "Interfaces: {{ ansible_interfaces }}" diff --git a/2026/day-70/playbooks/loops-demo.yml b/2026/day-70/playbooks/loops-demo.yml new file mode 100644 index 0000000000..d24a214211 --- /dev/null +++ b/2026/day-70/playbooks/loops-demo.yml @@ -0,0 +1,43 @@ +--- +- name: Loops demo + hosts: all + become: true + + vars: + users: + - alice + - bob + - charlie + + directories: + - /opt/app + - /opt/app/logs + - /opt/app/config + + tasks: + - name: Create multiple users + user: + name: "{{ item }}" + state: present + loop: "{{ users }}" + + - name: Create multiple directories + file: + path: "{{ item }}" + state: directory + mode: '0755' + loop: "{{ directories }}" + + - name: Install multiple packages + yum: + name: "{{ item }}" + state: present + loop: + - git + - wget + - tree + + - name: Show user and directory combinations + debug: + msg: "User {{ item.0 }} -> Directory {{ item.1 }}" + loop: "{{ users | product(directories) | list }}" diff --git a/2026/day-70/playbooks/server-report.yml b/2026/day-70/playbooks/server-report.yml new file mode 100644 index 0000000000..c1dcff5c4a --- /dev/null +++ b/2026/day-70/playbooks/server-report.yml @@ -0,0 +1,44 @@ +--- +- name: Server Health Report + hosts: all + + tasks: + - name: Check disk space + command: df -h / + register: disk_result + + - name: Check memory + command: free -m + register: memory_result + + - name: Check running services + shell: systemctl list-units --type=service --state=running | head -20 + register: services_result + + - name: Generate report + debug: + msg: + - "========== {{ inventory_hostname }} ==========" + - "OS: {{ ansible_distribution }} {{ ansible_distribution_version }}" + - "IP: {{ ansible_default_ipv4.address }}" + - "RAM: {{ ansible_memtotal_mb }}MB" + - "Disk: {{ disk_result.stdout_lines[1] }}" + - "Running services (first 20): {{ services_result.stdout_lines | length }}" + + - name: Flag if disk is critically low + debug: + msg: "ALERT: Check disk space on {{ inventory_hostname }}" + when: "'9[0-9]%' in disk_result.stdout or '100%' in disk_result.stdout" + + - name: Save report to file + copy: + content: | + Server: {{ inventory_hostname }} + OS: {{ ansible_distribution }} {{ ansible_distribution_version }} + IP: {{ ansible_default_ipv4.address }} + RAM: {{ ansible_memtotal_mb }}MB + Disk: {{ disk_result.stdout }} + Checked at: {{ ansible_date_time.iso8601 }} + dest: "/tmp/server-report-{{ inventory_hostname }}.txt" + become: true + diff --git a/2026/day-70/playbooks/site.yml b/2026/day-70/playbooks/site.yml new file mode 100644 index 0000000000..5caeadce34 --- /dev/null +++ b/2026/day-70/playbooks/site.yml @@ -0,0 +1,27 @@ +--- +- name: Apply common config + hosts: all + become: true + + tasks: + - name: Install common packages + yum: + name: "{{ common_packages }}" + state: present + + - name: Show environment + debug: + msg: "Environment: {{ app_env }}" + +- name: Configure web servers + hosts: web + become: true + + tasks: + - name: Show web config + debug: + msg: "HTTP port: {{ http_port }}, Max connections: {{ max_connections }}" + + - name: Show host-specific message + debug: + msg: "{{ custom_message }}" diff --git a/2026/day-70/terraform.tfvars b/2026/day-70/terraform.tfvars new file mode 100644 index 0000000000..8e14b136da --- /dev/null +++ b/2026/day-70/terraform.tfvars @@ -0,0 +1 @@ +key_name = "terra-server-key" diff --git a/2026/day-70/variables-demo.yml b/2026/day-70/variables-demo.yml new file mode 100644 index 0000000000..a06c33d24d --- /dev/null +++ b/2026/day-70/variables-demo.yml @@ -0,0 +1,29 @@ +--- +- name: Variable demo + hosts: all + become: true + + vars: + app_name: terraweek-app + app_port: 8080 + app_dir: "/opt/{{ app_name }}" + packages: + - git + - curl-minimal + - wget + + tasks: + - name: Print app details + debug: + msg: "Deploying {{ app_name }} on port {{ app_port }} to {{ app_dir }}" + + - name: Create application directory + file: + path: "{{ app_dir }}" + state: directory + mode: '0755' + + - name: Install required packages + yum: + name: "{{ packages }}" + state: present diff --git a/2026/day-70/variables.tf b/2026/day-70/variables.tf new file mode 100644 index 0000000000..1f742d43ac --- /dev/null +++ b/2026/day-70/variables.tf @@ -0,0 +1,23 @@ +variable "aws_region" { + description = "Aws region fo Ansible" + type = string + default = "ca-central-1" +} +variable "instance_type" { + description = "EC2 instance type" + type = string + default = "t2.micro" +} + +variable "instance_names" { + description = "Names of EC2 instances maanged by Ansible" + type = list(string) + + + default = [ + "web-server", "app-server", "db-server"] +} +variable "key_name" { + description = "AWS key pair namr for SSH" + type = string +} diff --git a/2026/day-71/.gitignore b/2026/day-71/.gitignore new file mode 100644 index 0000000000..b36779c412 --- /dev/null +++ b/2026/day-71/.gitignore @@ -0,0 +1 @@ +.vault_pass diff --git a/2026/day-71/Day-71.png b/2026/day-71/Day-71.png new file mode 100644 index 0000000000..407b1be2f9 Binary files /dev/null and b/2026/day-71/Day-71.png differ diff --git a/2026/day-71/Screenshot 2026-09-28 114616.png b/2026/day-71/Screenshot 2026-09-28 114616.png new file mode 100644 index 0000000000..421c8b7ae8 Binary files /dev/null and b/2026/day-71/Screenshot 2026-09-28 114616.png differ diff --git a/2026/day-71/Screenshot 2026-09-28 114635.png b/2026/day-71/Screenshot 2026-09-28 114635.png new file mode 100644 index 0000000000..7dcca18a86 Binary files /dev/null and b/2026/day-71/Screenshot 2026-09-28 114635.png differ diff --git a/2026/day-71/Screenshot 2026-09-28 114647.png b/2026/day-71/Screenshot 2026-09-28 114647.png new file mode 100644 index 0000000000..8323828b5b Binary files /dev/null and b/2026/day-71/Screenshot 2026-09-28 114647.png differ diff --git a/2026/day-71/complete-site.yml b/2026/day-71/complete-site.yml new file mode 100644 index 0000000000..0f229dc276 --- /dev/null +++ b/2026/day-71/complete-site.yml @@ -0,0 +1,26 @@ +--- +- name: Configure web servers + hosts: web + become: true + roles: + - role: webserver + vars: + app_name: terraweek + http_port: 80 + +- name: Configure app servers with Docker + hosts: app + become: true + roles: + - geerlingguy.docker + +- name: Configure database servers + hosts: db + become: true + tasks: + - name: Create DB config with secrets + template: + src: templates/db-config.j2 + dest: /etc/db-config.env + owner: root + mode: '0600' diff --git a/2026/day-71/day-71-ansible-roles-templates-galaxy-vault.md b/2026/day-71/day-71-ansible-roles-templates-galaxy-vault.md new file mode 100644 index 0000000000..ee5d237e45 --- /dev/null +++ b/2026/day-71/day-71-ansible-roles-templates-galaxy-vault.md @@ -0,0 +1,891 @@ +# Day 71 -- Ansible Roles, Jinja2 Templates, Galaxy & Vault + +## Goal of Day 71 + +Day 71 teaches us how to make Ansible code: + +- Organized +- Reusable +- Dynamic +- Secure + +The four main concepts are: + +1. **Roles** -- organize Ansible code +2. **Jinja2 Templates** -- create dynamic configuration files +3. **Ansible Galaxy** -- reuse roles created by others +4. **Ansible Vault** -- protect passwords and secrets + +------------------------------------------------------------------------ + +# 1. Quick Ansible Revision + +## Control Node + +The **Control Node** is the main machine where Ansible is installed. + +We run Ansible commands from this machine. + +Example: + +``` text +Control Node + | + | SSH + | + +----> Web Server + +----> App Server + +----> DB Server +``` + +The control node can use an SSH private key, such as a `.pem` file, to +connect to managed servers. + +## Managed Nodes + +Managed nodes are the servers that Ansible manages. + +Examples: + +- Web server +- Application server +- Database server + +Ansible normally connects to Linux managed nodes using **SSH**. + +Ansible is called **agentless** because we normally do not install a +special Ansible agent on every managed node. + +------------------------------------------------------------------------ + +# 2. Inventory + +The inventory tells Ansible **which servers exist** and how they are +grouped. + +Example: + +``` ini +[web] +web-server + +[app] +app-server + +[db] +db-server +``` + +Here we have three groups: + +``` text +web --> web-server +app --> app-server +db --> db-server +``` + +------------------------------------------------------------------------ + +# 3. Modules + +A module is like a **tool** Ansible uses to perform an action. + +Examples: + +``` text +ping --> test connectivity +package --> manage packages +service --> manage services +template --> create a file from a Jinja2 template +copy --> copy files +command --> run a command +``` + +Easy memory: + +> Module = tool that performs an action. + +------------------------------------------------------------------------ + +# 4. Ad-Hoc Commands + +An ad-hoc command is a quick, one-time Ansible command. + +Example: + +``` bash +ansible all -i inventory.ini -m ping +``` + +Meaning: + +``` text +ansible + | + +-- all --> target all hosts + +-- -i inventory.ini --> use this inventory + +-- -m ping --> use the ping module +``` + +Ad-hoc commands are useful for quick testing or simple actions. + +For repeatable automation, we normally use a **playbook**. + +------------------------------------------------------------------------ + +# 5. Playbook + +A playbook is a YAML file containing automation instructions. + +Think: + +> Playbook = Manager + +The playbook mainly decides: + +``` text +WHO should be configured? +WHAT automation should run? +``` + +Example: + +``` yaml +--- +- name: Configure web servers + hosts: web + become: true + + roles: + - webserver +``` + +Meaning: + +``` text +hosts: web + | + +--> WHO? Web servers + +roles: + - webserver + | + +--> WHAT? Run the webserver role +``` + +So remember: + +> Playbook = WHO + WHAT + +------------------------------------------------------------------------ + +# 6. Why Do We Need Roles? + +Imagine one playbook contains: + +``` text +Install Nginx +Create directories +Create configuration +Create webpage +Start Nginx +Restart Nginx +Set variables +Copy files +``` + +As the project becomes larger, one playbook becomes difficult to manage. + +A **Role** separates the automation into organized folders. + +Think: + +> Role = Department + +Example: + +``` text +roles/ +└── webserver/ + ├── tasks/ + │ └── main.yml + ├── handlers/ + │ └── main.yml + ├── defaults/ + │ └── main.yml + └── templates/ + ├── nginx.conf.j2 + └── index.html.j2 +``` + +Easy memory: + +``` text +Playbook = Manager +Role = Department +Tasks = Workers +Template = Custom form/document +``` + +------------------------------------------------------------------------ + +# 7. Tasks + +Tasks contain the actual steps Ansible performs. + +Example: + +``` yaml +- name: Install Nginx + package: + name: nginx + state: present +``` + +Another task could deploy a template: + +``` yaml +- name: Create Nginx configuration + template: + src: nginx.conf.j2 + dest: /etc/nginx/nginx.conf + notify: Restart Nginx +``` + +Think: + +> Task = Worker doing the actual job. + +------------------------------------------------------------------------ + +# 8. Handlers + +A handler is a special task that normally runs **only when notified by +another changed task**. + +Example task: + +``` yaml +- name: Create Nginx configuration + template: + src: nginx.conf.j2 + dest: /etc/nginx/nginx.conf + notify: Restart Nginx +``` + +Handler: + +``` yaml +- name: Restart Nginx + service: + name: nginx + state: restarted +``` + +Flow: + +``` text +Template changed? + | + YES + | + v +notify handler + | + v +Restart Nginx +``` + +If nothing changed, the handler normally does not need to restart Nginx. + +------------------------------------------------------------------------ + +# 9. Jinja2 Templates + +Jinja2 creates **dynamic files**. + +Suppose we write: + +``` nginx +listen 80; +``` + +The value `80` is hard-coded. + +Instead, Jinja2 can use: + +``` jinja2 +listen {{ http_port }}; +``` + +`{{ http_port }}` is a variable placeholder. + +If: + +``` yaml +http_port: 80 +``` + +Ansible renders: + +``` text +listen {{ http_port }}; + | + v + 80 + | + v +listen 80; +``` + +Important: + +> We do NOT create one Jinja2 template for every server. + +We can use one template for many servers, and Ansible inserts the +correct values for each host. + +------------------------------------------------------------------------ + +# 10. Variables + +A variable is a **named box containing a value**. + +Example: + +``` yaml +http_port: 80 +app_name: terraweek +``` + +Think: + +``` text +Variable Value + +http_port -------> 80 +app_name -------> terraweek +``` + +Jinja2 can use these values: + +``` jinja2 +listen {{ http_port }}; +``` + +------------------------------------------------------------------------ + +# 11. Where Can Variables Come From? + +For now, remember these common places: + +``` text +Role defaults +group_vars +host_vars +Playbook variables +``` + +## Role Defaults + +Example: + +``` text +roles/webserver/defaults/main.yml +``` + +``` yaml +http_port: 80 +``` + +This means: + +> Use port 80 as the role's default unless a higher-precedence variable +> provides another value. + +Think: + +> defaults = fallback value + +------------------------------------------------------------------------ + +## group_vars + +Suppose the inventory contains: + +``` ini +[web] +web1 +web2 +web3 +``` + +We can create: + +``` text +group_vars/web.yml +``` + +Example: + +``` yaml +http_port: 8080 +``` + +This can give the `web` group that value: + +``` text + web group + | + http_port = 8080 + | + +--------+--------+ + | | | + web1 web2 web3 + 8080 8080 8080 +``` + +Think: + +> group_vars = variables for a group of hosts. + +------------------------------------------------------------------------ + +## host_vars + +Suppose only `web2` needs port `9090`. + +Create: + +``` text +host_vars/web2.yml +``` + +``` yaml +http_port: 9090 +``` + +Conceptually: + +``` text +web1 --> group/default value +web2 --> 9090 (host-specific value) +web3 --> group/default value +``` + +Think: + +> host_vars = variables for one particular host. + +------------------------------------------------------------------------ + +# 12. How Variables and Jinja2 Work Together + +Template: + +``` jinja2 +listen {{ http_port }}; +``` + +Ansible determines the value for the current host. + +Then Jinja2 uses it. + +Flow: + +``` text +Variable source + | + v +http_port = 80 + | + v +Jinja2 Template + | + v +listen {{ http_port }}; + | + v +Final configuration + | + v +listen 80; +``` + +Important: + +> Jinja2 does not perform the server action itself. + +An **Ansible task** uses the template module to render and place the +file. + +------------------------------------------------------------------------ + +# 13. Complete Playbook → Role → Task → Template Flow + +This is one of the most important pictures for Day 71: + +``` text +PLAYBOOK +Manager + | + | WHO + WHAT + v +ROLE +Department + | + | HOW + v +TASKS +Workers + | + | may use + v +JINJA2 TEMPLATE +Dynamic document + | + | uses + v +VARIABLES +Values + | + v +FINAL CONFIG FILE +``` + +Example: + +``` text +Playbook + | + v +"Configure web group" + | + v +webserver Role + | + v +Install Nginx task + | + v +Template task + | + v +nginx.conf.j2 + | + v +Insert variables + | + v +/etc/nginx/nginx.conf + | + v +Notify Handler if changed + | + v +Restart Nginx +``` + +------------------------------------------------------------------------ + +# 14. Ansible Galaxy + +Sometimes we do not want to build every role ourselves. + +**Ansible Galaxy** provides reusable Ansible content, including roles +and collections. + +Example: + +``` bash +ansible-galaxy role install geerlingguy.docker +``` + +Concept: + +``` text +Our own role +roles/webserver/ + | + +--> We created it + + +Community role +geerlingguy.docker + | + +--> Reused from Galaxy +``` + +Think: + +> Galaxy = reuse Ansible automation created by others. + +In the Day 71 lab, the Docker Galaxy role had compatibility/package +issues with Amazon Linux 2023, so Docker was installed using Amazon +Linux's available package instead. The important learning point was +still understanding how Galaxy roles are obtained and called. + +------------------------------------------------------------------------ + +# 15. Ansible Vault + +Passwords and secrets should not normally be stored as readable plain +text in a repository. + +Bad example: + +``` yaml +db_password: MyRealPassword123 +``` + +Ansible Vault can encrypt sensitive Ansible data. + +Example command: + +``` bash +ansible-vault create group_vars/db/vault.yml +``` + +An encrypted Vault file looks similar to: + +``` text +$ANSIBLE_VAULT;1.1;AES256 +6634663331313061... +``` + +Humans cannot directly read the secret values from the encrypted +content. + +With the correct Vault password, Ansible can decrypt and use the +variables when running automation. + +Think: + +> Vault = protect sensitive Ansible data. + +------------------------------------------------------------------------ + +# 16. Vault + Jinja2 + +This is an important Day 71 connection. + +Suppose Vault contains encrypted variables such as: + +``` yaml +vault_db_password: example_secret +``` + +A Jinja2 template can reference: + +``` jinja2 +DB_PASSWORD={{ vault_db_password }} +``` + +Flow: + +``` text +vault.yml +Encrypted secret + | + | Ansible decrypts it + v +vault_db_password + | + v +Jinja2 template + | + | renders configuration + v +DB configuration file +``` + +This allows Ansible to use a secret without storing that secret as +normal readable YAML in the repository. + +------------------------------------------------------------------------ + +# 17. Day 71 Complete Architecture + +``` text + ANSIBLE + | + v + Inventory + | + +-----------+-----------+ + | | | + v v v + [web] [app] [db] + | | | + v v v + Playbook Playbook Playbook + | | | + v v v + Webserver Role Galaxy Role Vault + | | + +------+------+ | + | | | | + v v v v + Tasks Handlers Defaults Encrypted Secrets + | + v + Templates + | + v + Jinja2 + | + v +Dynamic configuration +``` + +------------------------------------------------------------------------ + +# 18. What Happened in Our Day 71 Lab? + +We worked with: + +``` text +Web Server + | + +--> Custom webserver role + +--> Nginx + +--> Jinja2 templates + +--> Variables + +--> Handlers + +App Server + | + +--> Galaxy Docker role + +--> Docker + +DB Server + | + +--> Ansible Vault + +--> Encrypted DB variables + +--> Jinja2 DB configuration +``` + +We also verified Docker: + +``` text +Docker version 25.0.14 +``` + +And the DB play successfully created the configuration using +Vault-protected variables. + +------------------------------------------------------------------------ + +# 19. Most Important Interview Definitions + +## What is an Ansible Playbook? + +A playbook is a YAML file containing automation instructions. It tells +Ansible which hosts to target and what automation to perform. + +## What is an Ansible Role? + +A role organizes Ansible automation into reusable folders such as tasks, +handlers, templates, and defaults. + +## What is Jinja2? + +Jinja2 is a templating engine used by Ansible to generate dynamic files +using variables. + +## What is a Handler? + +A handler is a special task that is normally triggered by a notification +when another task reports a change, such as restarting Nginx after its +configuration changes. + +## What is Ansible Galaxy? + +Ansible Galaxy is a service and ecosystem for finding and sharing +reusable Ansible content such as roles and collections. + +## What is Ansible Vault? + +Ansible Vault encrypts sensitive Ansible data such as passwords, keys, +and secret variables. + +## What is a Variable? + +A variable stores a value that Ansible can reuse in playbooks, roles, +and templates. + +------------------------------------------------------------------------ + +# 20. Easy Memory Trick + +Remember Day 71 like this: + +``` text +PLAYBOOK += Manager += WHO + WHAT + +ROLE += Department += Organizes HOW + +TASK += Worker += Performs the action + +JINJA2 += Custom form/document += Creates dynamic files + +VARIABLE += Value that fills the form + +HANDLER += Special action triggered when notified after a change + +GALAXY += Reuse automation from others + +VAULT += Protect secrets +``` + +------------------------------------------------------------------------ + +# 21. Final Day 71 Flow to Memorize + +``` text +Inventory + | + v +Playbook + | + v +Role + | + v +Tasks + | + +----> Templates ----> Jinja2 ----> Variables + | + +----> Notify Handler when needed + | + v +Managed Server + + +Galaxy + | + +----> Reusable external roles + + +Vault + | + +----> Encrypted sensitive variables +``` + +## One-Line Summary + +**Day 71 teaches us to organize automation with Roles, create dynamic +files with Jinja2 and variables, reuse community automation with Galaxy, +and protect sensitive data with Vault.** diff --git a/2026/day-71/group_vars/db/vault.yml b/2026/day-71/group_vars/db/vault.yml new file mode 100644 index 0000000000..299c199f83 --- /dev/null +++ b/2026/day-71/group_vars/db/vault.yml @@ -0,0 +1,11 @@ +$ANSIBLE_VAULT;1.1;AES256 +63613232653530663033643562383563303662623930663931303135643136636332646362343261 +3438663632663866633062313231366266343934636437660a616233396435343162346463376162 +64623965633565633132656163303534656434663365643032373961616638326166636637373666 +3365636334626165380a623561643765303962323762356361643238393935316234323837666339 +64396138663036353835393564373562616166633862386333653632376531396236636530643062 +37636233326464333832383663343865626263653066616163613739633066626166656434646266 +38313535333734303538343339366162663462326431386334653932663731613035663462366435 +65623830366463613965396132363836346162656265396165383532643462396133633564313465 +33353238656531396662663161396330303666616334366630393565646437323630386266373238 +6536396561343063306339616235363536633261343939396239 diff --git a/2026/day-71/requirements.yml b/2026/day-71/requirements.yml new file mode 100644 index 0000000000..63ab0a01b8 --- /dev/null +++ b/2026/day-71/requirements.yml @@ -0,0 +1,6 @@ +--- +roles: + - name: geerlingguy.docker + version: "7.4.1" + + - name: geerlingguy.ntp diff --git a/2026/day-71/roles/webserver/README.md b/2026/day-71/roles/webserver/README.md new file mode 100644 index 0000000000..225dd44b9f --- /dev/null +++ b/2026/day-71/roles/webserver/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/2026/day-71/roles/webserver/defaults/main.yml b/2026/day-71/roles/webserver/defaults/main.yml new file mode 100644 index 0000000000..23ae3c4d9d --- /dev/null +++ b/2026/day-71/roles/webserver/defaults/main.yml @@ -0,0 +1,6 @@ +--- +# defaults file for roles/webserver +# --- +http_port: 80 +app_name: myapp +max_connections: 512 diff --git a/2026/day-71/roles/webserver/handlers/main.yml b/2026/day-71/roles/webserver/handlers/main.yml new file mode 100644 index 0000000000..516ff081fe --- /dev/null +++ b/2026/day-71/roles/webserver/handlers/main.yml @@ -0,0 +1,6 @@ +--- +# handlers file for roles/webserver +- name: Restart Nginx + service: + name: nginx + state: restarted diff --git a/2026/day-71/roles/webserver/meta/main.yml b/2026/day-71/roles/webserver/meta/main.yml new file mode 100644 index 0000000000..c572acc9f8 --- /dev/null +++ b/2026/day-71/roles/webserver/meta/main.yml @@ -0,0 +1,52 @@ +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/2026/day-71/roles/webserver/tasks/main.yml b/2026/day-71/roles/webserver/tasks/main.yml new file mode 100644 index 0000000000..51514006de --- /dev/null +++ b/2026/day-71/roles/webserver/tasks/main.yml @@ -0,0 +1,39 @@ +--- +# tasks file for roles/webserver + +- name: Install Nginx + yum: + name: nginx + state: present + +- name: Deploy Nginx configuration + template: + src: nginx.conf.j2 + dest: /etc/nginx/nginx.conf + mode: '0644' + notify: Restart Nginx + +- name: Create web root + file: + path: "/var/www/{{ app_name }}" + state: directory + mode: '0755' + +- name: Deploy virtual host + template: + src: nginx-vhost.conf.j2 + dest: "/etc/nginx/conf.d/{{ app_name }}.conf" + mode: '0644' + notify: Restart Nginx + +- name: Deploy index page + template: + src: index.html.j2 + dest: "/var/www/{{ app_name }}/index.html" + mode: '0644' + +- name: Start and enable Nginx + service: + name: nginx + state: started + enabled: true diff --git a/2026/day-71/roles/webserver/templates/index.html.j2 b/2026/day-71/roles/webserver/templates/index.html.j2 new file mode 100644 index 0000000000..32fbf0a7ba --- /dev/null +++ b/2026/day-71/roles/webserver/templates/index.html.j2 @@ -0,0 +1,4 @@ +

{{ app_name }}

+

Host: {{ ansible_hostname }}

+

IP: {{ ansible_default_ipv4.address }}

+

Environment: {{ env | default('development') }}

diff --git a/2026/day-71/roles/webserver/templates/nginx-vhost.conf.j2 b/2026/day-71/roles/webserver/templates/nginx-vhost.conf.j2 new file mode 100644 index 0000000000..1910bd3bd3 --- /dev/null +++ b/2026/day-71/roles/webserver/templates/nginx-vhost.conf.j2 @@ -0,0 +1,14 @@ +server { + listen {{ http_port }}; + server_name {{ ansible_hostname }}; + + root /var/www/{{ app_name }}; + index index.html; + + location / { + try_files $uri $uri/ =404; + } + + access_log /var/log/nginx/{{ app_name }}_access.log; + error_log /var/log/nginx/{{ app_name }}_error.log; +} diff --git a/2026/day-71/roles/webserver/templates/nginx.conf.j2 b/2026/day-71/roles/webserver/templates/nginx.conf.j2 new file mode 100644 index 0000000000..a5a4db85a7 --- /dev/null +++ b/2026/day-71/roles/webserver/templates/nginx.conf.j2 @@ -0,0 +1,16 @@ +user nginx; +worker_processes auto; + +events { + worker_connections {{ max_connections }}; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + sendfile on; + keepalive_timeout 65; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/2026/day-71/roles/webserver/tests/inventory b/2026/day-71/roles/webserver/tests/inventory new file mode 100644 index 0000000000..878877b077 --- /dev/null +++ b/2026/day-71/roles/webserver/tests/inventory @@ -0,0 +1,2 @@ +localhost + diff --git a/2026/day-71/roles/webserver/tests/test.yml b/2026/day-71/roles/webserver/tests/test.yml new file mode 100644 index 0000000000..2b64e0f523 --- /dev/null +++ b/2026/day-71/roles/webserver/tests/test.yml @@ -0,0 +1,5 @@ +--- +- hosts: localhost + remote_user: root + roles: + - roles/webserver diff --git a/2026/day-71/roles/webserver/vars/main.yml b/2026/day-71/roles/webserver/vars/main.yml new file mode 100644 index 0000000000..de2e93113a --- /dev/null +++ b/2026/day-71/roles/webserver/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for roles/webserver diff --git a/2026/day-71/site.yml b/2026/day-71/site.yml new file mode 100644 index 0000000000..86c59bfeed --- /dev/null +++ b/2026/day-71/site.yml @@ -0,0 +1,10 @@ +--- +- name: Configure web server using role + hosts: web + become: true + + roles: + - role: webserver + vars: + app_name: terraweek + http_port: 80 diff --git a/2026/day-71/template-demo.yml b/2026/day-71/template-demo.yml new file mode 100644 index 0000000000..eb13da82e8 --- /dev/null +++ b/2026/day-71/template-demo.yml @@ -0,0 +1,39 @@ +--- +- name: Deploy Nginx with template + hosts: web + become: true + + vars: + app_name: terraweek-app + http_port: 80 + + tasks: + - name: Install Nginx + yum: + name: nginx + state: present + + - name: Create web root + file: + path: "/var/www/{{ app_name }}" + state: directory + mode: '0755' + + - name: Deploy vhost config from template + template: + src: templates/nginx-vhost.conf.j2 + dest: "/etc/nginx/conf.d/{{ app_name }}.conf" + owner: root + mode: '0644' + notify: Restart Nginx + + - name: Deploy index page + copy: + content: "

{{ app_name }}

Host: {{ ansible_hostname }} | IP: {{ ansible_default_ipv4.address }}

" + dest: "/var/www/{{ app_name }}/index.html" + + handlers: + - name: Restart Nginx + service: + name: nginx + state: restarted diff --git a/2026/day-71/templates/db-config.j2 b/2026/day-71/templates/db-config.j2 new file mode 100644 index 0000000000..e9171875b8 --- /dev/null +++ b/2026/day-71/templates/db-config.j2 @@ -0,0 +1,5 @@ +# Database Configuration -- Managed by Ansible +DB_HOST={{ ansible_default_ipv4.address }} +DB_PORT={{ db_port | default(3306) }} +DB_PASSWORD={{ vault_db_password }} +DB_ROOT_PASSWORD={{ vault_db_root_password }} diff --git a/2026/day-71/templates/nginx-vhost.conf.j2 b/2026/day-71/templates/nginx-vhost.conf.j2 new file mode 100644 index 0000000000..7bc9eedc82 --- /dev/null +++ b/2026/day-71/templates/nginx-vhost.conf.j2 @@ -0,0 +1,13 @@ +server { +listen {{ http_port | default(80)}}; +server_name {{ ansible_hostname }}; + +root /var/www/{{app_name}}; +index index.html; + +location / { + try_files $uri $uri/ =404; +} +access_log /var/log/nginx/{{app_name}}_access.log; +error_log /var/log/nginx/{{app_name}}_error.log; +} diff --git a/2026/day-72/Day-72.png b/2026/day-72/Day-72.png new file mode 100644 index 0000000000..7b841a216f Binary files /dev/null and b/2026/day-72/Day-72.png differ diff --git a/2026/day-72/ansible-docker-project/ansible.cfg b/2026/day-72/ansible-docker-project/ansible.cfg new file mode 100644 index 0000000000..4dd424d735 --- /dev/null +++ b/2026/day-72/ansible-docker-project/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +inventory = inventory.ini +host_key_checking = False diff --git a/2026/day-72/ansible-docker-project/group_vars/all.yml b/2026/day-72/ansible-docker-project/group_vars/all.yml new file mode 100644 index 0000000000..fe4062d678 --- /dev/null +++ b/2026/day-72/ansible-docker-project/group_vars/all.yml @@ -0,0 +1,13 @@ +--- +timezone: America/Toronto +project_name: devops-app +app_env: development + +common_packages: + - vim + - wget + - git + - htop + - tree + - jq + - unzip diff --git a/2026/day-72/ansible-docker-project/group_vars/web/vault.yml b/2026/day-72/ansible-docker-project/group_vars/web/vault.yml new file mode 100644 index 0000000000..3c6ac2b53c --- /dev/null +++ b/2026/day-72/ansible-docker-project/group_vars/web/vault.yml @@ -0,0 +1,10 @@ +$ANSIBLE_VAULT;1.1;AES256 +36353464383664613932666661653266636132636233393538343836383137653035383863316361 +3561366265663762363533363466616166316263363866320a633236393263346231303666313531 +36626664373363623462643430303563353331393337323265373531303337356633633836666566 +3836323563393736610a393763343064356162623665326534633164353661643432346239653764 +61666230363435353634656338653361303536323030653865643261653735353464373736623665 +65623335616436346262363230633262613733323830383162656235323936643634363139613562 +66373866356562383561363632656536336335323730333866383939393265363165636233396331 +31316232653439373764306639656261323466616434323831306632636438366533613332633136 +30383232613134303631336132366536303731613033366432656234396464366362 diff --git a/2026/day-72/ansible-docker-project/inventory.ini b/2026/day-72/ansible-docker-project/inventory.ini new file mode 100644 index 0000000000..1966da8d89 --- /dev/null +++ b/2026/day-72/ansible-docker-project/inventory.ini @@ -0,0 +1,21 @@ +[web] +web-server ansible_host=3.96.127.95 + +[app] +app-server ansible_host=16.52.169.142 + +[db] +db-server ansible_host=15.222.1.156 + +[all:vars] +ansible_user=ec2-user +ansible_ssh_private_key_file=~/.ssh/terra-server-key.pem + +[application:children] +web +app + +[all_servers:children] +application +db + diff --git a/2026/day-72/ansible-docker-project/roles/common/README.md b/2026/day-72/ansible-docker-project/roles/common/README.md new file mode 100644 index 0000000000..225dd44b9f --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/common/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/2026/day-72/ansible-docker-project/roles/common/defaults/main.yml b/2026/day-72/ansible-docker-project/roles/common/defaults/main.yml new file mode 100644 index 0000000000..28cf403f3c --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/common/defaults/main.yml @@ -0,0 +1,2 @@ +--- +# defaults file for roles/common diff --git a/2026/day-72/ansible-docker-project/roles/common/handlers/main.yml b/2026/day-72/ansible-docker-project/roles/common/handlers/main.yml new file mode 100644 index 0000000000..d29b8e9413 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/common/handlers/main.yml @@ -0,0 +1,2 @@ +--- +# handlers file for roles/common diff --git a/2026/day-72/ansible-docker-project/roles/common/meta/main.yml b/2026/day-72/ansible-docker-project/roles/common/meta/main.yml new file mode 100644 index 0000000000..c572acc9f8 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/common/meta/main.yml @@ -0,0 +1,52 @@ +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/2026/day-72/ansible-docker-project/roles/common/tasks/main.yml b/2026/day-72/ansible-docker-project/roles/common/tasks/main.yml new file mode 100644 index 0000000000..c1a41b6013 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/common/tasks/main.yml @@ -0,0 +1,29 @@ +--- +- name: Update package cache + yum: + update_cache: true + tags: common + +- name: Install common packages + yum: + name: "{{ common_packages }}" + state: present + tags: common + +- name: Set hostname + hostname: + name: "{{ inventory_hostname }}" + tags: common + +- name: Set timezone + timezone: + name: "{{ timezone }}" + tags: common + +- name: Create deploy user + user: + name: deploy + groups: wheel + shell: /bin/bash + state: present + tags: common diff --git a/2026/day-72/ansible-docker-project/roles/common/tests/inventory b/2026/day-72/ansible-docker-project/roles/common/tests/inventory new file mode 100644 index 0000000000..878877b077 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/common/tests/inventory @@ -0,0 +1,2 @@ +localhost + diff --git a/2026/day-72/ansible-docker-project/roles/common/tests/test.yml b/2026/day-72/ansible-docker-project/roles/common/tests/test.yml new file mode 100644 index 0000000000..1cbebf5c26 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/common/tests/test.yml @@ -0,0 +1,5 @@ +--- +- hosts: localhost + remote_user: root + roles: + - roles/common diff --git a/2026/day-72/ansible-docker-project/roles/common/vars/main.yml b/2026/day-72/ansible-docker-project/roles/common/vars/main.yml new file mode 100644 index 0000000000..f83eed7710 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/common/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for roles/common diff --git a/2026/day-72/ansible-docker-project/roles/docker/README.md b/2026/day-72/ansible-docker-project/roles/docker/README.md new file mode 100644 index 0000000000..225dd44b9f --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/docker/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/2026/day-72/ansible-docker-project/roles/docker/defaults/main.yml b/2026/day-72/ansible-docker-project/roles/docker/defaults/main.yml new file mode 100644 index 0000000000..07b6bdb51f --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/docker/defaults/main.yml @@ -0,0 +1,6 @@ +--- +docker_app_image: nginx +docker_app_tag: latest +docker_app_name: myapp +docker_app_port: 8080 +docker_container_port: 80 diff --git a/2026/day-72/ansible-docker-project/roles/docker/handlers/main.yml b/2026/day-72/ansible-docker-project/roles/docker/handlers/main.yml new file mode 100644 index 0000000000..65cec5201c --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/docker/handlers/main.yml @@ -0,0 +1,5 @@ +--- +- name: Restart Docker + service: + name: docker + state: restarted diff --git a/2026/day-72/ansible-docker-project/roles/docker/meta/main.yml b/2026/day-72/ansible-docker-project/roles/docker/meta/main.yml new file mode 100644 index 0000000000..c572acc9f8 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/docker/meta/main.yml @@ -0,0 +1,52 @@ +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/2026/day-72/ansible-docker-project/roles/docker/tasks/main.yml b/2026/day-72/ansible-docker-project/roles/docker/tasks/main.yml new file mode 100644 index 0000000000..792acb615c --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/docker/tasks/main.yml @@ -0,0 +1,56 @@ +--- +- name: Install Docker + dnf: + name: docker + state: present + tags: docker + +- name: Start and enable Docker + service: + name: docker + state: started + enabled: true + tags: docker + +- name: Add deploy user to docker group + user: + name: deploy + groups: docker + append: true + tags: docker + +- name: Log in to Docker Hub + community.docker.docker_login: + username: "{{ vault_docker_username }}" + password: "{{ vault_docker_password }}" + become_user: deploy + when: vault_docker_username is defined + no_log: true + tags: docker + +- name: Pull application image + community.docker.docker_image: + name: "{{ docker_app_image }}" + tag: "{{ docker_app_tag }}" + source: pull + tags: docker + +- name: Run application container + community.docker.docker_container: + name: "{{ docker_app_name }}" + image: "{{ docker_app_image }}:{{ docker_app_tag }}" + state: started + restart_policy: always + ports: + - "{{ docker_app_port }}:{{ docker_container_port }}" + tags: docker + +- name: Wait for container to respond + uri: + url: "http://localhost:{{ docker_app_port }}" + status_code: 200 + register: health_check + until: health_check.status == 200 + retries: 5 + delay: 3 + tags: docker diff --git a/2026/day-72/ansible-docker-project/roles/docker/tests/inventory b/2026/day-72/ansible-docker-project/roles/docker/tests/inventory new file mode 100644 index 0000000000..878877b077 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/docker/tests/inventory @@ -0,0 +1,2 @@ +localhost + diff --git a/2026/day-72/ansible-docker-project/roles/docker/tests/test.yml b/2026/day-72/ansible-docker-project/roles/docker/tests/test.yml new file mode 100644 index 0000000000..c5ea91121b --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/docker/tests/test.yml @@ -0,0 +1,5 @@ +--- +- hosts: localhost + remote_user: root + roles: + - roles/docker diff --git a/2026/day-72/ansible-docker-project/roles/docker/vars/main.yml b/2026/day-72/ansible-docker-project/roles/docker/vars/main.yml new file mode 100644 index 0000000000..5277412b2b --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/docker/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for roles/docker diff --git a/2026/day-72/ansible-docker-project/roles/nginx/README.md b/2026/day-72/ansible-docker-project/roles/nginx/README.md new file mode 100644 index 0000000000..225dd44b9f --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/nginx/README.md @@ -0,0 +1,38 @@ +Role Name +========= + +A brief description of the role goes here. + +Requirements +------------ + +Any pre-requisites that may not be covered by Ansible itself or the role should be mentioned here. For instance, if the role uses the EC2 module, it may be a good idea to mention in this section that the boto package is required. + +Role Variables +-------------- + +A description of the settable variables for this role should go here, including any variables that are in defaults/main.yml, vars/main.yml, and any variables that can/should be set via parameters to the role. Any variables that are read from other roles and/or the global scope (ie. hostvars, group vars, etc.) should be mentioned here as well. + +Dependencies +------------ + +A list of other roles hosted on Galaxy should go here, plus any details in regards to parameters that may need to be set for other roles, or variables that are used from other roles. + +Example Playbook +---------------- + +Including an example of how to use your role (for instance, with variables passed in as parameters) is always nice for users too: + + - hosts: servers + roles: + - { role: username.rolename, x: 42 } + +License +------- + +BSD + +Author Information +------------------ + +An optional section for the role authors to include contact information, or a website (HTML is not allowed). diff --git a/2026/day-72/ansible-docker-project/roles/nginx/defaults/main.yml b/2026/day-72/ansible-docker-project/roles/nginx/defaults/main.yml new file mode 100644 index 0000000000..a2f4a284f1 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/nginx/defaults/main.yml @@ -0,0 +1,4 @@ +--- +nginx_http_port: 80 +nginx_upstream_port: 8080 +nginx_server_name: "_" diff --git a/2026/day-72/ansible-docker-project/roles/nginx/handlers/main.yml b/2026/day-72/ansible-docker-project/roles/nginx/handlers/main.yml new file mode 100644 index 0000000000..f2b3b16289 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/nginx/handlers/main.yml @@ -0,0 +1,10 @@ +--- +- name: Reload Nginx + service: + name: nginx + state: reloaded + +- name: Restart Nginx + service: + name: nginx + state: restarted diff --git a/2026/day-72/ansible-docker-project/roles/nginx/meta/main.yml b/2026/day-72/ansible-docker-project/roles/nginx/meta/main.yml new file mode 100644 index 0000000000..c572acc9f8 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/nginx/meta/main.yml @@ -0,0 +1,52 @@ +galaxy_info: + author: your name + description: your role description + company: your company (optional) + + # If the issue tracker for your role is not on github, uncomment the + # next line and provide a value + # issue_tracker_url: http://example.com/issue/tracker + + # Choose a valid license ID from https://spdx.org - some suggested licenses: + # - BSD-3-Clause (default) + # - MIT + # - GPL-2.0-or-later + # - GPL-3.0-only + # - Apache-2.0 + # - CC-BY-4.0 + license: license (GPL-2.0-or-later, MIT, etc) + + min_ansible_version: 2.1 + + # If this a Container Enabled role, provide the minimum Ansible Container version. + # min_ansible_container_version: + + # + # Provide a list of supported platforms, and for each platform a list of versions. + # If you don't wish to enumerate all versions for a particular platform, use 'all'. + # To view available platforms and versions (or releases), visit: + # https://galaxy.ansible.com/api/v1/platforms/ + # + # platforms: + # - name: Fedora + # versions: + # - all + # - 25 + # - name: SomePlatform + # versions: + # - all + # - 1.0 + # - 7 + # - 99.99 + + galaxy_tags: [] + # List tags for your role here, one per line. A tag is a keyword that describes + # and categorizes the role. Users find roles by searching for tags. Be sure to + # remove the '[]' above, if you add tags to this list. + # + # NOTE: A tag is limited to a single word comprised of alphanumeric characters. + # Maximum 20 tags per role. + +dependencies: [] + # List your role dependencies here, one per line. Be sure to remove the '[]' above, + # if you add dependencies to this list. diff --git a/2026/day-72/ansible-docker-project/roles/nginx/tasks/main.yml b/2026/day-72/ansible-docker-project/roles/nginx/tasks/main.yml new file mode 100644 index 0000000000..337cc2ff19 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/nginx/tasks/main.yml @@ -0,0 +1,28 @@ +--- +- name: Install Nginx + dnf: + name: nginx + state: present + tags: nginx + +- name: Deploy reverse proxy configuration + template: + src: app-proxy.conf.j2 + dest: /etc/nginx/conf.d/app-proxy.conf + owner: root + group: root + mode: '0644' + notify: Reload Nginx + tags: nginx + +- name: Test Nginx configuration + command: nginx -t + changed_when: false + tags: nginx + +- name: Start and enable Nginx + service: + name: nginx + state: started + enabled: true + tags: nginx diff --git a/2026/day-72/ansible-docker-project/roles/nginx/templates/app-proxy.conf.j2 b/2026/day-72/ansible-docker-project/roles/nginx/templates/app-proxy.conf.j2 new file mode 100644 index 0000000000..62341ee382 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/nginx/templates/app-proxy.conf.j2 @@ -0,0 +1,21 @@ +upstream app_backend { + server 127.0.0.1:{{ nginx_upstream_port }}; +} + +server { + listen {{ nginx_http_port }}; + server_name {{ nginx_server_name }}; + + location / { + proxy_pass http://app_backend; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /health { + proxy_pass http://app_backend; + } +} diff --git a/2026/day-72/ansible-docker-project/roles/nginx/tests/inventory b/2026/day-72/ansible-docker-project/roles/nginx/tests/inventory new file mode 100644 index 0000000000..878877b077 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/nginx/tests/inventory @@ -0,0 +1,2 @@ +localhost + diff --git a/2026/day-72/ansible-docker-project/roles/nginx/tests/test.yml b/2026/day-72/ansible-docker-project/roles/nginx/tests/test.yml new file mode 100644 index 0000000000..6750534da0 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/nginx/tests/test.yml @@ -0,0 +1,5 @@ +--- +- hosts: localhost + remote_user: root + roles: + - roles/nginx diff --git a/2026/day-72/ansible-docker-project/roles/nginx/vars/main.yml b/2026/day-72/ansible-docker-project/roles/nginx/vars/main.yml new file mode 100644 index 0000000000..4d5dfe2d70 --- /dev/null +++ b/2026/day-72/ansible-docker-project/roles/nginx/vars/main.yml @@ -0,0 +1,2 @@ +--- +# vars file for roles/nginx diff --git a/2026/day-72/ansible-docker-project/site.yml b/2026/day-72/ansible-docker-project/site.yml new file mode 100644 index 0000000000..6b3c3b5b61 --- /dev/null +++ b/2026/day-72/ansible-docker-project/site.yml @@ -0,0 +1,27 @@ +--- +- name: Apply common configuration + hosts: all + become: true + + roles: + - common + + tags: common + +- name: Install Docker and run containers + hosts: web + become: true + + roles: + - docker + + tags: docker + +- name: Configure Nginx reverse proxy + hosts: web + become: true + + roles: + - nginx + + tags: nginx diff --git a/2026/day-72/day-72-ansible-project.md b/2026/day-72/day-72-ansible-project.md new file mode 100644 index 0000000000..645a500cc3 --- /dev/null +++ b/2026/day-72/day-72-ansible-project.md @@ -0,0 +1,806 @@ +# Day 72 -- Ansible Project: Automate Docker and Nginx Deployment + +## Project Goal + +This project combines the Ansible concepts from the previous days into +one complete deployment. + +The automation: + +- Applies common server configuration. +- Installs and starts Docker on the web server. +- Logs in to Docker Hub using credentials protected by Ansible Vault. +- Pulls and runs an Nginx Docker container. +- Maps host port `8080` to container port `80`. +- Installs Nginx on the EC2 host. +- Configures host Nginx as a reverse proxy. +- Sends traffic from host port `80` to the Docker application on port + `8080`. +- Uses Ansible roles, variables, templates, handlers, tags, and Vault. + +## Architecture + +``` text +Ansible Control Node + | + | SSH + v +Web EC2 Server ++------------------------------------+ +| | +| Host Nginx | +| Port 80 | +| | | +| | reverse proxy | +| v | +| 127.0.0.1:8080 | +| | | +| v | +| Docker Container: myapp | +| nginx:latest | +| Container Port 80 | +| | ++------------------------------------+ + +Request flow: + +Client -> Nginx :80 -> localhost:8080 -> Docker -> Container :80 +``` + +## Project Directory Structure + +``` text +ansible-docker-project/ +├── ansible.cfg +├── inventory.ini +├── site.yml +├── group_vars/ +│ ├── all.yml +│ └── web/ +│ └── vault.yml +└── roles/ + ├── common/ + │ ├── defaults/ + │ ├── handlers/ + │ ├── tasks/ + │ │ └── main.yml + │ └── templates/ + ├── docker/ + │ ├── defaults/ + │ │ └── main.yml + │ ├── handlers/ + │ │ └── main.yml + │ ├── tasks/ + │ │ └── main.yml + │ └── templates/ + └── nginx/ + ├── defaults/ + │ └── main.yml + ├── handlers/ + │ └── main.yml + ├── tasks/ + │ └── main.yml + └── templates/ + └── app-proxy.conf.j2 +``` + +The role skeletons were created with: + +``` bash +mkdir -p ansible-docker-project/roles +cd ansible-docker-project + +ansible-galaxy init roles/common +ansible-galaxy init roles/docker +ansible-galaxy init roles/nginx +``` + +## Inventory + +``` ini +[web] +web-server ansible_host=3.96.127.95 + +[app] +app-server ansible_host=16.52.169.142 + +[db] +db-server ansible_host=15.222.1.156 + +[all:vars] +ansible_user=ec2-user +ansible_ssh_private_key_file=~/.ssh/terra-server-key.pem + +[application:children] +web +app + +[all_servers:children] +application +db +``` + +> Note: Public IP addresses can change if EC2 instances are +> stopped/recreated. Update the inventory when required. + +## Global Variables + +File: `group_vars/all.yml` + +``` yaml +--- +timezone: America/Toronto +project_name: devops-app +app_env: development + +common_packages: + - vim + - wget + - git + - htop + - tree + - jq + - unzip +``` + +### Environment Note + +The lab servers use Amazon Linux 2023. The full `curl` package was not +added because the system already had `curl-minimal`, which caused a DNF +package conflict when attempting to install full `curl`. + +## Common Role + +File: `roles/common/tasks/main.yml` + +``` yaml +--- +- name: Update package cache + yum: + update_cache: true + tags: common + +- name: Install common packages + yum: + name: "{{ common_packages }}" + state: present + tags: common + +- name: Set hostname + hostname: + name: "{{ inventory_hostname }}" + tags: common + +- name: Set timezone + timezone: + name: "{{ timezone }}" + tags: common + +- name: Create deploy user + user: + name: deploy + groups: wheel + shell: /bin/bash + state: present + tags: common +``` + +The common role runs against all managed servers. + +A second run returned `changed=0`, showing that the role is idempotent. + +## Docker Role + +### Docker Defaults + +File: `roles/docker/defaults/main.yml` + +``` yaml +--- +docker_app_image: nginx +docker_app_tag: latest +docker_app_name: myapp +docker_app_port: 8080 +docker_container_port: 80 +``` + +The port mapping means: + +``` text +EC2 host port 8080 -> Docker container port 80 +``` + +### Docker Tasks + +File: `roles/docker/tasks/main.yml` + +``` yaml +--- +- name: Install Docker + dnf: + name: docker + state: present + tags: docker + +- name: Start and enable Docker + service: + name: docker + state: started + enabled: true + tags: docker + +- name: Add deploy user to docker group + user: + name: deploy + groups: docker + append: true + tags: docker + +- name: Log in to Docker Hub + community.docker.docker_login: + username: "{{ vault_docker_username }}" + password: "{{ vault_docker_password }}" + become_user: deploy + when: vault_docker_username is defined + no_log: true + tags: docker + +- name: Pull application image + community.docker.docker_image: + name: "{{ docker_app_image }}" + tag: "{{ docker_app_tag }}" + source: pull + tags: docker + +- name: Run application container + community.docker.docker_container: + name: "{{ docker_app_name }}" + image: "{{ docker_app_image }}:{{ docker_app_tag }}" + state: started + restart_policy: always + ports: + - "{{ docker_app_port }}:{{ docker_container_port }}" + tags: docker + +- name: Wait for container to respond + uri: + url: "http://localhost:{{ docker_app_port }}" + status_code: 200 + register: health_check + until: health_check.status == 200 + retries: 5 + delay: 3 + tags: docker +``` + +### Amazon Linux 2023 Adaptation + +The project source describes installing Docker CE using the Docker CE +repository. For this lab, the managed nodes are Amazon Linux 2023, so +the native `docker` package was installed with `dnf`. + +This was an environment-specific adaptation. + +### Docker Handler + +File: `roles/docker/handlers/main.yml` + +``` yaml +--- +- name: Restart Docker + service: + name: docker + state: restarted +``` + +### Docker Collection + +The Docker collection was checked with: + +``` bash +ansible-galaxy collection install community.docker +``` + +It was already installed. + +## Nginx Role + +### Nginx Defaults + +File: `roles/nginx/defaults/main.yml` + +``` yaml +--- +nginx_http_port: 80 +nginx_upstream_port: 8080 +nginx_server_name: "_" +``` + +Meaning: + +``` text +nginx_http_port: 80 -> clients connect here +nginx_upstream_port: 8080 -> Docker application is here +nginx_server_name: "_" -> catch-all server name +``` + +### Reverse Proxy Jinja2 Template + +File: `roles/nginx/templates/app-proxy.conf.j2` + +``` nginx +upstream app_backend { + server 127.0.0.1:{{ nginx_upstream_port }}; +} + +server { + listen {{ nginx_http_port }}; + server_name {{ nginx_server_name }}; + + location / { + proxy_pass http://app_backend; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /health { + proxy_pass http://app_backend; + } +} +``` + +Jinja2 changes: + +``` text +{{ nginx_http_port }} -> 80 +{{ nginx_upstream_port }} -> 8080 +{{ nginx_server_name }} -> _ +``` + +The rendered Nginx configuration therefore sends incoming port 80 +traffic to `127.0.0.1:8080`. + +### Nginx Tasks + +File: `roles/nginx/tasks/main.yml` + +``` yaml +--- +- name: Install Nginx + dnf: + name: nginx + state: present + tags: nginx + +- name: Deploy reverse proxy configuration + template: + src: app-proxy.conf.j2 + dest: /etc/nginx/conf.d/app-proxy.conf + owner: root + group: root + mode: '0644' + notify: Reload Nginx + tags: nginx + +- name: Test Nginx configuration + command: nginx -t + changed_when: false + tags: nginx + +- name: Start and enable Nginx + service: + name: nginx + state: started + enabled: true + tags: nginx +``` + +### Nginx Handlers + +File: `roles/nginx/handlers/main.yml` + +``` yaml +--- +- name: Reload Nginx + service: + name: nginx + state: reloaded + +- name: Restart Nginx + service: + name: nginx + state: restarted +``` + +When the Jinja2 configuration changes, the template task notifies the +`Reload Nginx` handler. + +This avoids unnecessary reloads when the configuration has not changed. + +## Master Playbook + +File: `site.yml` + +``` yaml +--- +- name: Apply common configuration + hosts: all + become: true + + roles: + - common + + tags: common + +- name: Install Docker and run containers + hosts: web + become: true + + roles: + - docker + + tags: docker + +- name: Configure Nginx reverse proxy + hosts: web + become: true + + roles: + - nginx + + tags: nginx +``` + +The master playbook gives one entry point for the complete deployment. + +## Ansible Vault + +A Vault password file was stored in the Linux home directory: + +``` text +~/.vault_pass +``` + +Permissions were restricted: + +``` bash +chmod 600 ~/.vault_pass +``` + +The encrypted variables file is: + +``` text +group_vars/web/vault.yml +``` + +It was created with: + +``` bash +ansible-vault create group_vars/web/vault.yml \ + --vault-password-file ~/.vault_pass +``` + +The encrypted file begins with: + +``` text +$ANSIBLE_VAULT;1.1;AES256 +``` + +The plaintext structure before encryption contains variables like: + +``` yaml +vault_docker_username: "" +vault_docker_password: "" +``` + +**Never commit the Vault password file or expose real credentials in +documentation.** + +To edit the encrypted file: + +``` bash +ansible-vault edit group_vars/web/vault.yml \ + --vault-password-file ~/.vault_pass +``` + +The Docker login task also uses: + +``` yaml +no_log: true +``` + +This helps prevent sensitive task output from appearing in Ansible logs. + +## WSL / Windows Mount Note + +This project is located under: + +``` text +/mnt/c/Users/asmas/OneDrive/Desktop/90DaysOfDevOps/2026/day-72/ansible-docker-project +``` + +Ansible reports: + +``` text +Ansible is being run in a world writable directory... +ignoring it as an ansible.cfg source. +``` + +Because the local `ansible.cfg` is ignored in this environment, commands +explicitly specify the inventory and Vault password file: + +``` bash +ansible-playbook -i inventory.ini site.yml \ + --vault-password-file ~/.vault_pass +``` + +The Vault password file was intentionally kept in the Linux home +directory instead of the Windows-mounted project directory. + +## Using Tags + +Tags allow only part of the project to run. + +Run only the common role: + +``` bash +ansible-playbook -i inventory.ini site.yml \ + --tags common \ + --vault-password-file ~/.vault_pass +``` + +Run only Docker: + +``` bash +ansible-playbook -i inventory.ini site.yml \ + --tags docker \ + --vault-password-file ~/.vault_pass +``` + +Run only Nginx: + +``` bash +ansible-playbook -i inventory.ini site.yml \ + --tags nginx \ + --vault-password-file ~/.vault_pass +``` + +This is useful when only one part of the server needs to be updated. + +## Verification + +### Docker Container + +The container was verified with: + +``` bash +ansible web -i inventory.ini -b -m command -a "docker ps" +``` + +The running container showed: + +``` text +myapp +0.0.0.0:8080->80/tcp +``` + +This proves that host port `8080` maps to container port `80`. + +### Direct Container Test + +``` bash +ansible web -i inventory.ini \ + -m uri \ + -a "url=http://localhost:8080 status_code=200" +``` + +Result: + +``` text +status: 200 +msg: OK +``` + +### Nginx Reverse Proxy Test + +``` bash +ansible web -i inventory.ini \ + -m uri \ + -a "url=http://localhost:80 status_code=200" +``` + +Result: + +``` text +status: 200 +msg: OK +``` + +This proves the complete path: + +``` text +Port 80 + | + v +Host Nginx + | + v +localhost:8080 + | + v +Docker myapp + | + v +Container port 80 +``` + +## Docker Hub + Vault Verification + +The first login attempts failed because example credentials were still +stored in the Vault. + +After editing the encrypted Vault with valid Docker Hub credentials, the +Docker deployment succeeded: + +``` text +TASK [docker : Install Docker] ok +TASK [docker : Start and enable Docker] ok +TASK [docker : Add deploy user to docker group] ok +TASK [docker : Log in to Docker Hub] changed +TASK [docker : Pull application image] ok +TASK [docker : Run application container] ok +TASK [docker : Wait for container to respond] ok + +failed=0 +``` + +This proves: + +``` text +Encrypted Vault + | + v +Ansible decrypts credentials + | + v +Docker Hub login + | + v +Pull image + | + v +Run container +``` + +## Full Deployment + +Run the complete project with: + +``` bash +ansible-playbook -i inventory.ini site.yml \ + --vault-password-file ~/.vault_pass +``` + +This runs: + +``` text +site.yml + | + +-- common role -> all servers + | + +-- docker role -> web server + | + +-- nginx role -> web server +``` + +## Idempotency + +Idempotency means: + +> Run the same automation again and Ansible only changes something when +> a change is actually needed. + +The common role was run twice. On the second run: + +``` text +app-server : changed=0 failed=0 +db-server : changed=0 failed=0 +web-server : changed=0 failed=0 +``` + +The Docker role was also rerun after deployment. Existing Docker +installation, service state, image, container, and health check returned +`ok`. + +For the final project proof, run the complete playbook twice: + +``` bash +ansible-playbook -i inventory.ini site.yml \ + --vault-password-file ~/.vault_pass +``` + +Run the same command again and capture the second `PLAY RECAP`. + +## Screenshots to Add Before Submission + +Add your own terminal screenshots in this section before pushing the +final documentation. + +### Screenshot 1 -- Full End-to-End Playbook + +``` text +[ADD SCREENSHOT: ansible-playbook site.yml running successfully] +``` + +### Screenshot 2 -- Idempotency + +``` text +[ADD SCREENSHOT: second complete run showing mostly/all ok] +``` + +### Screenshot 3 -- Docker Container + +``` text +[ADD SCREENSHOT: docker ps showing myapp and 8080->80] +``` + +### Screenshot 4 -- Nginx Port 80 Test + +``` text +[ADD SCREENSHOT: URI/curl test returning HTTP 200 through port 80] +``` + +## What I Learned + +During Day 72 I combined the main Ansible concepts into one project: + +- **Inventory** tells Ansible which servers to manage. +- **Playbooks** control the overall deployment. +- **Roles** organize automation into reusable sections. +- **Tasks** perform the actual work. +- **Variables** keep values reusable and easy to change. +- **Jinja2 templates** generate configuration dynamically. +- **Handlers** reload or restart services only when needed. +- **Tags** let me run only selected parts of the deployment. +- **Ansible Vault** protects sensitive credentials. +- **Docker modules** manage images and containers. +- **Nginx** works as a reverse proxy in front of the container. +- **Idempotency** means repeated runs do not make unnecessary changes. + +## Final Result + +The final architecture is: + +``` text +Ansible + | + v +EC2 Web Server + | + +-- Host Nginx :80 + | | + | +-- reverse proxy --> localhost:8080 + | + +-- Docker + | + +-- myapp + nginx:latest + container :80 +``` + +The application was successfully tested directly through Docker on port +`8080` and through the host Nginx reverse proxy on port `80`. + +## Submission + +Save this documentation at: + +``` text +2026/day-72/day-72-ansible-project.md +``` + +Then commit and push it to the 90DaysOfDevOps repository after adding +the required screenshots. diff --git a/2026/day-73/Day-73.png b/2026/day-73/Day-73.png new file mode 100644 index 0000000000..a3ec9289f1 Binary files /dev/null and b/2026/day-73/Day-73.png differ diff --git a/2026/day-73/Screenshot 2026-09-29 151524.png b/2026/day-73/Screenshot 2026-09-29 151524.png new file mode 100644 index 0000000000..47e9bbabcc Binary files /dev/null and b/2026/day-73/Screenshot 2026-09-29 151524.png differ diff --git a/2026/day-73/Screenshot 2026-09-29 151537.png b/2026/day-73/Screenshot 2026-09-29 151537.png new file mode 100644 index 0000000000..b296cc2c1f Binary files /dev/null and b/2026/day-73/Screenshot 2026-09-29 151537.png differ diff --git a/2026/day-73/Screenshot 2026-09-29 151752.png b/2026/day-73/Screenshot 2026-09-29 151752.png new file mode 100644 index 0000000000..6ea4e9a406 Binary files /dev/null and b/2026/day-73/Screenshot 2026-09-29 151752.png differ diff --git a/2026/day-73/Screenshot 2026-09-29 152248.png b/2026/day-73/Screenshot 2026-09-29 152248.png new file mode 100644 index 0000000000..304f8c20c1 Binary files /dev/null and b/2026/day-73/Screenshot 2026-09-29 152248.png differ diff --git a/2026/day-73/Screenshot 2026-09-29 153222.png b/2026/day-73/Screenshot 2026-09-29 153222.png new file mode 100644 index 0000000000..35a212f353 Binary files /dev/null and b/2026/day-73/Screenshot 2026-09-29 153222.png differ diff --git a/2026/day-73/Screenshot 2026-09-29 153240.png b/2026/day-73/Screenshot 2026-09-29 153240.png new file mode 100644 index 0000000000..d116148446 Binary files /dev/null and b/2026/day-73/Screenshot 2026-09-29 153240.png differ diff --git a/2026/day-73/Screenshot 2026-09-29 153517.png b/2026/day-73/Screenshot 2026-09-29 153517.png new file mode 100644 index 0000000000..d3f936f02a Binary files /dev/null and b/2026/day-73/Screenshot 2026-09-29 153517.png differ diff --git a/2026/day-73/Screenshot 2026-09-29 153702.png b/2026/day-73/Screenshot 2026-09-29 153702.png new file mode 100644 index 0000000000..96e7b5d039 Binary files /dev/null and b/2026/day-73/Screenshot 2026-09-29 153702.png differ diff --git a/2026/day-73/Screenshot 2026-09-29 154058.png b/2026/day-73/Screenshot 2026-09-29 154058.png new file mode 100644 index 0000000000..376f8e4376 Binary files /dev/null and b/2026/day-73/Screenshot 2026-09-29 154058.png differ diff --git a/2026/day-73/day-73-observability-prometheus.md b/2026/day-73/day-73-observability-prometheus.md new file mode 100644 index 0000000000..4a8310a064 --- /dev/null +++ b/2026/day-73/day-73-observability-prometheus.md @@ -0,0 +1,280 @@ +# Day 73 -- Introduction to Observability and Prometheus + +## Overview + +Today I started the observability block and learned the three pillars of +observability: metrics, logs, and traces. I also set up Prometheus with +Docker, practiced PromQL, added a sample application, debugged a scrape +issue, and explored Prometheus TSDB storage. + +## Three Pillars of Observability + +### Metrics + +Numerical measurements over time, such as CPU usage, memory usage, +request count, and error rate. Prometheus is mainly used for metrics. + +### Logs + +Timestamped records of events such as application errors and database +failures. Logs help explain why something happened. + +### Traces + +The journey of a request across services, for example: + +`Frontend → API → Payment Service → Database` + +Simple way I remember them: - Metrics → What happened? - Logs → Why did +it happen? - Traces → Where did it happen? + +## Monitoring vs Observability + +Monitoring tells me when something is wrong, for example `CPU > 90%`. + +Observability helps me investigate why something is wrong using metrics, +logs, and traces. + +## Architecture + +``` text +[Your App] --> metrics --> [Prometheus] --> [Grafana Dashboards] +[Your App] --> logs --> [Promtail] --> [Loki] --> [Grafana] +[Your App] --> traces --> [OTEL Collector] --> [Grafana/Debug] +[Host] --> metrics --> [Node Exporter] --> [Prometheus] +[Docker] --> metrics --> [cAdvisor] --> [Prometheus] +``` + +## Project Structure + +``` text +day-73/ +└── observability-stack/ + ├── docker-compose.yml + └── prometheus.yml +``` + +## Prometheus Configuration + +``` yaml +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] + + - job_name: "notes-app" + static_configs: + - targets: ["notes-app:8000"] +``` + +Prometheus was configured to scrape targets every 15 seconds. + +## Docker Compose + +``` yaml +services: + prometheus: + image: prom/prometheus:latest + container_name: prometheus + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + restart: unless-stopped + + notes-app: + image: trainwithshubham/notes-app:latest + container_name: notes-app + ports: + - "8000:8000" + restart: unless-stopped + +volumes: + prometheus_data: +``` + +Started with: + +``` bash +docker compose up -d +``` + +## Prometheus Concepts + +- Counter → normally increases. Example: total HTTP requests. +- Gauge → can increase and decrease. Example: current memory usage. +- Histogram → groups measurements into buckets, such as request + duration. +- Summary → similar to a histogram but calculates percentiles on the + client side. +- Labels → key-value information attached to metrics. +- Time series → unique metric name plus label combination. + +## PromQL Queries Practiced + +``` promql +up +process_resident_memory_bytes +process_resident_memory_bytes / 1024 / 1024 +count({__name__=~".+"}) +prometheus_http_requests_total +prometheus_http_requests_total[5m] +rate(prometheus_http_requests_total[5m]) +sum(rate(prometheus_http_requests_total[5m])) +prometheus_http_requests_total{code="200"} +prometheus_http_requests_total{code!="200"} +topk(5, prometheus_http_requests_total) +rate(prometheus_http_requests_total{code!="200"}[5m]) +``` + +During the lab, `count({__name__=~".+"})` returned `936`. + +## Counter vs Gauge + +``` text +Counter → normally keeps increasing +Example → total HTTP requests + +Gauge → can increase and decrease +Example → current memory usage +``` + +## Debugging -- Notes App Target Was DOWN + +After adding the Notes App, Prometheus initially did not show the new +target. I restarted Prometheus so it would load the updated +configuration: + +``` bash +docker compose restart prometheus +``` + +The Notes App then appeared, but Prometheus showed: + +``` text +DOWN +Error scraping target: server returned HTTP status 404 Not Found +``` + +Prometheus was trying: + +``` text +http://notes-app:8000/metrics +``` + +I tested it: + +``` bash +curl -i http://localhost:8000/metrics +``` + +Result: + +``` text +HTTP/1.1 404 Not Found +``` + +Then I tested the application: + +``` bash +curl -i http://localhost:8000/ +``` + +Result: + +``` text +HTTP/1.1 200 OK +``` + +This confirmed: + +``` text +Notes App container → Running +Port 8000 → Reachable +Application / → 200 OK +Application /metrics → 404 Not Found +Prometheus target → DOWN +``` + +The application was healthy, but this version did not expose a +Prometheus `/metrics` endpoint. + +Important lesson: + +`Application running does not automatically mean Prometheus metrics are available.` + +## Prometheus Storage and TSDB + +I checked Prometheus storage: + +``` bash +docker exec prometheus du -sh /prometheus +``` + +It increased from: + +``` text +544.0K → 548.0K +``` + +This showed that Prometheus was continuing to store metric samples. + +In TSDB Status I observed: + +``` text +Number of Series: 1040 +Number of Chunks: 1040 +Number of Label Pairs: 541 +``` + +Prometheus uses a local TSDB and the lab notes a default retention of 15 +days. Example retention configuration: + +``` yaml +command: + - '--config.file=/etc/prometheus/prometheus.yml' + - '--storage.tsdb.retention.time=30d' + - '--storage.tsdb.retention.size=1GB' +``` + +The `prometheus_data` volume keeps metric data outside the container so +it can survive container recreation while the volume exists. + +## Cleanup + +After completing the lab: + +``` bash +docker compose down -v +``` + +This removed the Prometheus container, Notes App container, Compose +network, and Prometheus data volume. My existing Kind Kubernetes +container was left untouched. + +## What I Learned + +``` text +Application / System + ↓ +Expose Metrics + ↓ +Prometheus Scrapes Metrics + ↓ +Prometheus TSDB + ↓ +PromQL + ↓ +Understand System Behaviour +``` + +Day 73 helped me understand Prometheus scraping, metric types, labels, +time series, PromQL, storage, retention, and real troubleshooting when +an application does not expose a `/metrics` endpoint. diff --git a/2026/day-73/observability-stack/docker-compose.yml b/2026/day-73/observability-stack/docker-compose.yml new file mode 100644 index 0000000000..d7c6859dad --- /dev/null +++ b/2026/day-73/observability-stack/docker-compose.yml @@ -0,0 +1,22 @@ +services: + prometheus: + image: prom/prometheus:latest + container_name: prometheus + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + restart: unless-stopped + + notes-app: + image: trainwithshubham/notes-app:latest + container_name: notes-app + ports: + - "8000:8000" + restart: unless-stopped + +volumes: + prometheus_data: diff --git a/2026/day-73/observability-stack/prometheus.yml b/2026/day-73/observability-stack/prometheus.yml new file mode 100644 index 0000000000..b9ebe1bb45 --- /dev/null +++ b/2026/day-73/observability-stack/prometheus.yml @@ -0,0 +1,12 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] + + - job_name: "notes-app" + static_configs: + - targets: ["notes-app:8000"] diff --git a/2026/day-74/Screenshot 2026-09-29 215029.png b/2026/day-74/Screenshot 2026-09-29 215029.png new file mode 100644 index 0000000000..18cc805fcb Binary files /dev/null and b/2026/day-74/Screenshot 2026-09-29 215029.png differ diff --git a/2026/day-74/Screenshot 2026-09-29 215302.png b/2026/day-74/Screenshot 2026-09-29 215302.png new file mode 100644 index 0000000000..c46d0df3e0 Binary files /dev/null and b/2026/day-74/Screenshot 2026-09-29 215302.png differ diff --git a/2026/day-74/Screenshot 2026-09-29 215347.png b/2026/day-74/Screenshot 2026-09-29 215347.png new file mode 100644 index 0000000000..84c5d5fcf0 Binary files /dev/null and b/2026/day-74/Screenshot 2026-09-29 215347.png differ diff --git a/2026/day-74/Screenshot 2026-09-29 215950.png b/2026/day-74/Screenshot 2026-09-29 215950.png new file mode 100644 index 0000000000..024174684d Binary files /dev/null and b/2026/day-74/Screenshot 2026-09-29 215950.png differ diff --git a/2026/day-74/Screenshot 2026-09-29 220151.png b/2026/day-74/Screenshot 2026-09-29 220151.png new file mode 100644 index 0000000000..c75745a487 Binary files /dev/null and b/2026/day-74/Screenshot 2026-09-29 220151.png differ diff --git a/2026/day-74/Screenshot 2026-09-29 220223.png b/2026/day-74/Screenshot 2026-09-29 220223.png new file mode 100644 index 0000000000..64c8840179 Binary files /dev/null and b/2026/day-74/Screenshot 2026-09-29 220223.png differ diff --git a/2026/day-74/Screenshot 2026-09-29 220239.png b/2026/day-74/Screenshot 2026-09-29 220239.png new file mode 100644 index 0000000000..b9c899f9c3 Binary files /dev/null and b/2026/day-74/Screenshot 2026-09-29 220239.png differ diff --git a/2026/day-74/Screenshot 2026-09-29 222808.png b/2026/day-74/Screenshot 2026-09-29 222808.png new file mode 100644 index 0000000000..68286df01f Binary files /dev/null and b/2026/day-74/Screenshot 2026-09-29 222808.png differ diff --git a/2026/day-74/Screenshot 2026-09-29 223159.png b/2026/day-74/Screenshot 2026-09-29 223159.png new file mode 100644 index 0000000000..6f4f787d06 Binary files /dev/null and b/2026/day-74/Screenshot 2026-09-29 223159.png differ diff --git a/2026/day-74/Screenshot 2026-09-29 223528.png b/2026/day-74/Screenshot 2026-09-29 223528.png new file mode 100644 index 0000000000..802a30114b Binary files /dev/null and b/2026/day-74/Screenshot 2026-09-29 223528.png differ diff --git a/2026/day-74/Screenshot 2026-09-29 224041.png b/2026/day-74/Screenshot 2026-09-29 224041.png new file mode 100644 index 0000000000..8e59952a09 Binary files /dev/null and b/2026/day-74/Screenshot 2026-09-29 224041.png differ diff --git a/2026/day-74/Screenshot 2026-09-29 224250.png b/2026/day-74/Screenshot 2026-09-29 224250.png new file mode 100644 index 0000000000..07d278421c Binary files /dev/null and b/2026/day-74/Screenshot 2026-09-29 224250.png differ diff --git a/2026/day-74/day-74-exporters-grafana.md b/2026/day-74/day-74-exporters-grafana.md new file mode 100644 index 0000000000..237b4df4a3 --- /dev/null +++ b/2026/day-74/day-74-exporters-grafana.md @@ -0,0 +1,352 @@ +# Day 74 -- Exporters and Grafana + +## Overview + +Today I continued the observability lab by adding exporters and Grafana +to the Prometheus setup. + +I worked with: - Prometheus - Node Exporter - cAdvisor - Grafana - +Docker Compose - PromQL - Notes App + +The goal was to collect host and container metrics with exporters, query +them with Prometheus, and visualize them in Grafana. + +## Architecture + +``` text +Host / WSL + | + +--> Node Exporter :9100 ----+ + | | +Docker Containers | + | v + +--> cAdvisor :8080 ----> Prometheus :9090 + | + v + Grafana :3000 +``` + +Node Exporter provides host/system metrics. cAdvisor provides container +metrics. Prometheus scrapes the metrics, and Grafana displays them. + +## Docker Compose Stack + +The completed lab used: - Prometheus -- port 9090 - Notes App -- port +8000 - Node Exporter -- port 9100 - cAdvisor -- port 8080 - Grafana -- +port 3000 + +## Prometheus Configuration + +``` yaml +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] + + - job_name: "notes-app" + static_configs: + - targets: ["notes-app:8000"] + + - job_name: "node-exporter" + static_configs: + - targets: ["node-exporter:9100"] + + - job_name: "cadvisor" + static_configs: + - targets: ["cadvisor:8080"] +``` + +## Node Exporter + +Node Exporter exposed host metrics such as CPU, memory, filesystem, +network, and system load. + +I verified it with: + +``` bash +curl http://localhost:9100/metrics | head -20 +``` + +Prometheus showed `node-exporter` as UP. + +### CPU + +``` promql +node_cpu_seconds_total{mode="idle"} +``` + +CPU usage percentage: + +``` promql +100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) +``` + +### Memory + +``` promql +node_memory_MemAvailable_bytes +``` + +Memory usage percentage: + +``` promql +(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 +``` + +## cAdvisor + +cAdvisor collected Docker/container metrics and Prometheus showed +`cadvisor:8080` as UP. + +The cAdvisor interface displayed CPU, memory, and network information. + +### cAdvisor Label Debugging + +The lab-style query: + +``` promql +container_memory_usage_bytes{name!=""} +``` + +returned no data in my environment. + +I tested the base metric: + +``` promql +container_memory_usage_bytes +``` + +and Prometheus returned many results. The returned metrics had labels +such as `id`, `instance`, and `job`, but no usable `name` label. + +So I adapted the query to the labels actually available: + +``` promql +container_memory_usage_bytes / 1024 / 1024 +``` + +For container CPU: + +``` promql +rate(container_cpu_usage_seconds_total[5m]) * 100 +``` + +This taught me to inspect the real metric labels before assuming an +exporter is broken. + +## Prometheus Targets + +During the lab: + +``` text +Prometheus UP +Node Exporter UP +cAdvisor UP +Notes App DOWN +``` + +The Notes App returned HTTP 404 for `/metrics`. The application itself +was reachable, but that image did not expose a Prometheus metrics +endpoint. + +Important lesson: + +``` text +Application running != Prometheus metrics endpoint available +``` + +## Grafana + +Grafana ran at: + +``` text +http://localhost:3000 +``` + +Prometheus was configured inside Grafana with: + +``` text +http://prometheus:9090 +``` + +The Windows browser uses `http://localhost:9090`, while the Grafana +container uses `http://prometheus:9090` because Docker service names +resolve inside the Docker network. + +## Grafana Dashboard + +Dashboard name: + +``` text +DevOps Observability Overview +``` + +### CPU Usage % + +``` promql +100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) +``` + +Visualization: Gauge + +### Memory Usage % + +``` promql +(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 +``` + +Visualization: Gauge + +### Container CPU Usage + +``` promql +rate(container_cpu_usage_seconds_total[5m]) * 100 +``` + +Visualization: Time series + +### Container Memory (MB) + +``` promql +container_memory_usage_bytes / 1024 / 1024 +``` + +Visualization: Bar chart + +### Disk Usage % + +``` promql +(1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 +``` + +Visualization: Stat + +## Grafana Provisioning + +I created: + +``` text +grafana/ +└── provisioning/ + ├── datasources/ + └── dashboards/ +``` + +Prometheus datasource provisioning: + +``` yaml +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false +``` + +This lets Grafana configure the Prometheus datasource automatically. + +## Imported Node Exporter Dashboard + +I imported Grafana dashboard ID `1860` (Node Exporter Full). + +It successfully displayed: - CPU Busy - System Load - RAM Used - Swap +Used - Root filesystem usage - CPU cores - Total RAM - Uptime - CPU +graphs - Memory graphs - Network traffic - Disk usage + +This confirmed: + +``` text +Host + ↓ +Node Exporter + ↓ +Prometheus + ↓ +Grafana + ↓ +Dashboard +``` + +## Useful Commands + +Start: + +``` bash +docker compose up -d +``` + +Validate: + +``` bash +docker compose config +``` + +Check containers: + +``` bash +docker compose ps +``` + +Check Node Exporter: + +``` bash +curl http://localhost:9100/metrics | head -20 +``` + +Restart Prometheus after configuration changes: + +``` bash +docker compose restart prometheus +``` + +Destroy the Day 74 stack and its volumes: + +``` bash +docker compose down -v +``` + +## What I Learned + +I learned that exporters expose metrics for Prometheus. Node Exporter +provides host metrics, while cAdvisor provides container metrics. + +Prometheus collects and queries the metrics, and Grafana visualizes +them. + +I also learned an important troubleshooting lesson: a PromQL query can +return no data because the labels available in my environment differ +from the labels expected by the example query. I should inspect the raw +metric and its labels first. + +The complete observability flow is: + +``` text +Infrastructure / Containers + ↓ + Exporters + ↓ + Prometheus + ↓ + Grafana + ↓ + Dashboards +``` + +## Cleanup + +After the lab: + +``` bash +docker compose down -v +``` + +This removes the Day 74 containers, Compose network, and volumes. The +separate Kind Kubernetes container is unrelated to this lab and should +not be removed as part of Day 74 cleanup. diff --git a/2026/day-74/day-74.png b/2026/day-74/day-74.png new file mode 100644 index 0000000000..2e6684dae5 Binary files /dev/null and b/2026/day-74/day-74.png differ diff --git a/2026/day-74/observability-stack/docker-compose.yml b/2026/day-74/observability-stack/docker-compose.yml new file mode 100644 index 0000000000..3b0b943b05 --- /dev/null +++ b/2026/day-74/observability-stack/docker-compose.yml @@ -0,0 +1,64 @@ +services: + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + ports: + - "9090:9090" + volumes: + - prometheus_data:/prometheus + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + command: + - '--config.file=/etc/prometheus/prometheus.yml' + restart: unless-stopped + + notes-app: + image: trainwithshubham/notes-app:latest + container_name: notes-app + ports: + - "8000:8000" + restart: unless-stopped + + node-exporter: + image: prom/node-exporter:latest + container_name: node-exporter + ports: + - "9100:9100" + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--path.rootfs=/rootfs' + - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' + restart: unless-stopped + + cadvisor: + image: gcr.io/cadvisor/cadvisor:latest + container_name: cadvisor + ports: + - "8080:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + restart: unless-stopped + + grafana: + image: grafana/grafana-enterprise:latest + container_name: grafana + ports: + - "3000:3000" + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin123 + restart: unless-stopped + +volumes: + prometheus_data: + grafana_data: diff --git a/2026/day-74/observability-stack/grafana/provisioning/datasources/datasources.yml b/2026/day-74/observability-stack/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 0000000000..bb009bb21d --- /dev/null +++ b/2026/day-74/observability-stack/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,9 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false diff --git a/2026/day-74/observability-stack/prometheus.yml b/2026/day-74/observability-stack/prometheus.yml new file mode 100644 index 0000000000..8098b0f191 --- /dev/null +++ b/2026/day-74/observability-stack/prometheus.yml @@ -0,0 +1,20 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] + + - job_name: "notes-app" + static_configs: + - targets: ["notes-app:8000"] + + - job_name: "node-exporter" + static_configs: + - targets: ["node-exporter:9100"] + + - job_name: "cadvisor" + static_configs: + - targets: ["cadvisor:8080"] diff --git a/2026/day-75/Screenshot 2026-09-30 133534.png b/2026/day-75/Screenshot 2026-09-30 133534.png new file mode 100644 index 0000000000..8e9ba520b9 Binary files /dev/null and b/2026/day-75/Screenshot 2026-09-30 133534.png differ diff --git a/2026/day-75/Screenshot 2026-09-30 133932.png b/2026/day-75/Screenshot 2026-09-30 133932.png new file mode 100644 index 0000000000..26e71ab0cc Binary files /dev/null and b/2026/day-75/Screenshot 2026-09-30 133932.png differ diff --git a/2026/day-75/Screenshot 2026-09-30 134038.png b/2026/day-75/Screenshot 2026-09-30 134038.png new file mode 100644 index 0000000000..53b47b7fec Binary files /dev/null and b/2026/day-75/Screenshot 2026-09-30 134038.png differ diff --git a/2026/day-75/Screenshot 2026-09-30 134223.png b/2026/day-75/Screenshot 2026-09-30 134223.png new file mode 100644 index 0000000000..630ec21805 Binary files /dev/null and b/2026/day-75/Screenshot 2026-09-30 134223.png differ diff --git a/2026/day-75/Screenshot 2026-09-30 134310.png b/2026/day-75/Screenshot 2026-09-30 134310.png new file mode 100644 index 0000000000..fe3caada92 Binary files /dev/null and b/2026/day-75/Screenshot 2026-09-30 134310.png differ diff --git a/2026/day-75/Screenshot 2026-09-30 135208.png b/2026/day-75/Screenshot 2026-09-30 135208.png new file mode 100644 index 0000000000..fa17d3f672 Binary files /dev/null and b/2026/day-75/Screenshot 2026-09-30 135208.png differ diff --git a/2026/day-75/day-75-loki-promtail.md b/2026/day-75/day-75-loki-promtail.md new file mode 100644 index 0000000000..8c8bb684b1 --- /dev/null +++ b/2026/day-75/day-75-loki-promtail.md @@ -0,0 +1,229 @@ +# Day 75 -- Log Management with Loki and Promtail + +## Goal + +Day 75 adds centralized log management to the observability stack. + +**Metrics tell us what is happening. Logs help us understand why it is +happening.** + +## Architecture + +``` text +Docker Containers + ↓ + Promtail + ↓ + Loki + ↓ + Grafana +``` + +Promtail collects Docker logs, Loki stores and queries them, and Grafana +displays Loki logs together with Prometheus metrics. + +## Project Structure + +``` text +day-75/ +├── README.md +├── docker-compose.yml +├── prometheus.yml +├── loki/ +│ └── loki-config.yml +├── promtail/ +│ └── promtail-config.yml +└── grafana/ + └── provisioning/ + └── datasources/ + └── datasources.yml +``` + +## Services + +The stack used Prometheus, Node Exporter, cAdvisor, Grafana, Notes App, +Loki, and Promtail. + +## Loki + +Loki listens on port `3100`. Readiness was tested with: + +``` bash +curl http://localhost:3100/ready +``` + +The first response showed that the ingester was still waiting. After +startup completed, Loki returned: + +``` text +ready +``` + +## Promtail + +Promtail reads Docker JSON logs from: + +``` text +/var/lib/docker/containers/*/*-json.log +``` + +and sends them to: + +``` text +http://loki:3100/loki/api/v1/push +``` + +The positions file acts like a bookmark so Promtail remembers how far it +has read. + +## Grafana Data Sources + +Grafana was provisioned with: + +``` text +Prometheus → http://prometheus:9090 → metrics +Loki → http://loki:3100 → logs +``` + +Prometheus remained the default data source. + +## Validation + +Docker Compose configuration was checked with: + +``` bash +docker compose config +``` + +The stack was started with: + +``` bash +docker compose up -d +``` + +and verified with: + +``` bash +docker compose ps +``` + +All seven services started. + +## Troubleshooting: Old Docker Logs + +Promtail initially received a Loki `400 Bad Request` because some +existing Docker log entries had timestamps that were too old. + +Fresh application requests were generated: + +``` bash +for i in $(seq 1 20); do + curl -s http://localhost:8000 > /dev/null +done +``` + +A direct Loki query returned successful log streams afterward, proving +that the Promtail → Loki pipeline worked. + +## LogQL Practiced + +All Docker logs: + +``` logql +{job="docker"} +``` + +Logs containing `error`: + +``` logql +{job="docker"} |= "error" +``` + +Exclude logs containing `health`: + +``` logql +{job="docker"} != "health" +``` + +Count logs over five minutes: + +``` logql +count_over_time({job="docker"}[5m]) +``` + +Log rate: + +``` logql +rate({job="docker"}[5m]) +``` + +Count error-containing logs over five minutes: + +``` logql +count_over_time({job="docker"} |= "error" [5m]) +``` + +## Metrics + Logs Together + +The final Grafana dashboard displayed both data types side by side. + +### Container CPU --- Prometheus + +``` promql +rate(container_cpu_usage_seconds_total[5m]) * 100 +``` + +### Docker Logs --- Loki + +``` logql +{job="docker"} +``` + +Final flow: + +``` text + Grafana + / \ + / \ + Prometheus Loki + ↑ ↑ + Metrics Logs + ↑ ↑ + cAdvisor Promtail + ↑ + Docker Containers +``` + +This allows troubleshooting using the same time range: + +``` text +Prometheus shows a metric problem + ↓ +Check the same timestamp + ↓ +Loki shows the related logs + ↓ +Investigate why it happened +``` + +## Interview Explanation + +**Prometheus tells me what is happening through metrics. Loki helps me +understand why it may be happening through logs. Grafana lets me view +both together and correlate them using the same time range.** + +Prometheus uses **PromQL** for metrics. Loki uses **LogQL** for logs. + +## Day 75 Result + +- Loki configured and running +- Promtail collecting Docker logs +- Loki readiness verified +- Promtail → Loki pipeline verified +- Prometheus and Loki available in Grafana +- LogQL filtering and counting practiced +- Error logs investigated +- Prometheus metrics and Loki logs displayed together +- Metrics/log correlation completed + +**Day 75 completed successfully.** diff --git a/2026/day-75/day-75.png b/2026/day-75/day-75.png new file mode 100644 index 0000000000..c689dd7ab1 Binary files /dev/null and b/2026/day-75/day-75.png differ diff --git a/2026/day-75/docker-compose.yml b/2026/day-75/docker-compose.yml new file mode 100644 index 0000000000..630da2d1f5 --- /dev/null +++ b/2026/day-75/docker-compose.yml @@ -0,0 +1,102 @@ +networks: + monitoring: + driver: bridge + +volumes: + prometheus_data: {} + grafana_data: {} + loki_data: {} + +services: + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + ports: + - "9090:9090" + volumes: + - prometheus_data:/prometheus + - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro + networks: + - monitoring + restart: unless-stopped + + notes-app: + image: trainwithshubham/notes-app:latest + container_name: notes-app + ports: + - "8000:8000" + networks: + - monitoring + restart: unless-stopped + + node-exporter: + image: prom/node-exporter:latest + container_name: node-exporter + ports: + - "9100:9100" + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--path.rootfs=/rootfs' + - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' + networks: + - monitoring + restart: unless-stopped + + cadvisor: + image: gcr.io/cadvisor/cadvisor:latest + container_name: cadvisor + ports: + - "8080:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + networks: + - monitoring + restart: unless-stopped + + grafana: + image: grafana/grafana-enterprise:latest + container_name: grafana + ports: + - "3000:3000" + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin123 + networks: + - monitoring + restart: unless-stopped + + loki: + image: grafana/loki:latest + container_name: loki + ports: + - "3100:3100" + volumes: + - ./loki/loki-config.yml:/etc/loki/loki-config.yml + - loki_data:/loki + command: -config.file=/etc/loki/loki-config.yml + networks: + - monitoring + restart: unless-stopped + + promtail: + image: grafana/promtail:latest + container_name: promtail + volumes: + - ./promtail/promtail-config.yml:/etc/promtail/promtail-config.yml + - /var/lib/docker/containers:/var/lib/docker/containers:ro + - /var/run/docker.sock:/var/run/docker.sock + command: -config.file=/etc/promtail/promtail-config.yml + networks: + - monitoring + restart: unless-stopped diff --git a/2026/day-75/grafana/provisioning/datasources/datasources.yml b/2026/day-75/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 0000000000..2c0808d808 --- /dev/null +++ b/2026/day-75/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,15 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false + + - name: Loki + type: loki + access: proxy + url: http://loki:3100 + editable: false diff --git a/2026/day-75/loki/loki-config.yml b/2026/day-75/loki/loki-config.yml new file mode 100644 index 0000000000..056f9fcb9b --- /dev/null +++ b/2026/day-75/loki/loki-config.yml @@ -0,0 +1,26 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + +common: + ring: + instance_addr: 127.0.0.1 + kvstore: + store: inmemory + replication_factor: 1 + path_prefix: /loki + +schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +storage_config: + filesystem: + directory: /loki/chunks diff --git a/2026/day-75/prometheus.yml b/2026/day-75/prometheus.yml new file mode 100644 index 0000000000..db5bc355dd --- /dev/null +++ b/2026/day-75/prometheus.yml @@ -0,0 +1,20 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] + + - job_name: "notes-app" + static_configs: + - targets: ["notes-app:8000"] + + - job_name: "node-exporter" + static_configs: + - targets: ["node-exporter:9100"] + + - job_name: "cadvisor" + static_configs: + - targets: ["cadvisor:8080"] diff --git a/2026/day-75/promtail/promtail-config.yml b/2026/day-75/promtail/promtail-config.yml new file mode 100644 index 0000000000..53d69b1584 --- /dev/null +++ b/2026/day-75/promtail/promtail-config.yml @@ -0,0 +1,20 @@ +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://loki:3100/loki/api/v1/push + +scrape_configs: + - job_name: docker + static_configs: + - targets: + - localhost + labels: + job: docker + __path__: /var/lib/docker/containers/*/*-json.log + pipeline_stages: + - docker: {} diff --git a/2026/day-76/Screenshot 2026-09-30 144206.png b/2026/day-76/Screenshot 2026-09-30 144206.png new file mode 100644 index 0000000000..cb26e1069f Binary files /dev/null and b/2026/day-76/Screenshot 2026-09-30 144206.png differ diff --git a/2026/day-76/Screenshot 2026-09-30 144238.png b/2026/day-76/Screenshot 2026-09-30 144238.png new file mode 100644 index 0000000000..61f884b032 Binary files /dev/null and b/2026/day-76/Screenshot 2026-09-30 144238.png differ diff --git a/2026/day-76/Screenshot 2026-09-30 144343.png b/2026/day-76/Screenshot 2026-09-30 144343.png new file mode 100644 index 0000000000..6d8f9d2009 Binary files /dev/null and b/2026/day-76/Screenshot 2026-09-30 144343.png differ diff --git a/2026/day-76/Screenshot 2026-09-30 144435.png b/2026/day-76/Screenshot 2026-09-30 144435.png new file mode 100644 index 0000000000..b6f925a671 Binary files /dev/null and b/2026/day-76/Screenshot 2026-09-30 144435.png differ diff --git a/2026/day-76/Screenshot 2026-09-30 144735.png b/2026/day-76/Screenshot 2026-09-30 144735.png new file mode 100644 index 0000000000..3bbfe9c525 Binary files /dev/null and b/2026/day-76/Screenshot 2026-09-30 144735.png differ diff --git a/2026/day-76/Screenshot 2026-09-30 144742.png b/2026/day-76/Screenshot 2026-09-30 144742.png new file mode 100644 index 0000000000..f61c7cc440 Binary files /dev/null and b/2026/day-76/Screenshot 2026-09-30 144742.png differ diff --git a/2026/day-76/Screenshot 2026-09-30 145424.png b/2026/day-76/Screenshot 2026-09-30 145424.png new file mode 100644 index 0000000000..08ed4f0727 Binary files /dev/null and b/2026/day-76/Screenshot 2026-09-30 145424.png differ diff --git a/2026/day-76/Screenshot 2026-09-30 145636.png b/2026/day-76/Screenshot 2026-09-30 145636.png new file mode 100644 index 0000000000..51b121e30e Binary files /dev/null and b/2026/day-76/Screenshot 2026-09-30 145636.png differ diff --git a/2026/day-76/Screenshot 2026-09-30 145701.png b/2026/day-76/Screenshot 2026-09-30 145701.png new file mode 100644 index 0000000000..2453c5edcd Binary files /dev/null and b/2026/day-76/Screenshot 2026-09-30 145701.png differ diff --git a/2026/day-76/Screenshot 2026-09-30 145719.png b/2026/day-76/Screenshot 2026-09-30 145719.png new file mode 100644 index 0000000000..702e5a5db7 Binary files /dev/null and b/2026/day-76/Screenshot 2026-09-30 145719.png differ diff --git a/2026/day-76/Screenshot 2026-09-30 150046.png b/2026/day-76/Screenshot 2026-09-30 150046.png new file mode 100644 index 0000000000..4a02335ebd Binary files /dev/null and b/2026/day-76/Screenshot 2026-09-30 150046.png differ diff --git a/2026/day-76/alert-rules.yml b/2026/day-76/alert-rules.yml new file mode 100644 index 0000000000..5f8e33bde6 --- /dev/null +++ b/2026/day-76/alert-rules.yml @@ -0,0 +1,47 @@ +groups: + - name: system-alerts + rules: + - alert: HighCPUUsage + expr: 100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) > 80 + for: 2m + labels: + severity: warning + annotations: + summary: "High CPU usage detected" + description: "CPU usage has been above 80% for more than 2 minutes. Current value: {{ $value }}%" + + - alert: HighMemoryUsage + expr: (1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 > 85 + for: 2m + labels: + severity: warning + annotations: + summary: "High memory usage detected" + description: "Memory usage is above 85%. Current value: {{ $value }}%" + + - alert: ContainerDown + expr: absent(container_last_seen{name="notes-app"}) + for: 1m + labels: + severity: critical + annotations: + summary: "Container is down" + description: "The notes-app container has not been seen for over 1 minute" + + - alert: TargetDown + expr: up == 0 + for: 1m + labels: + severity: critical + annotations: + summary: "Scrape target is down" + description: "{{ $labels.job }} target {{ $labels.instance }} is unreachable" + + - alert: HighDiskUsage + expr: (1 - node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"}) * 100 > 90 + for: 5m + labels: + severity: critical + annotations: + summary: "Disk space running low" + description: "Root filesystem usage is above 90%. Current value: {{ $value }}%" diff --git a/2026/day-76/day-76-otel-alerting.md b/2026/day-76/day-76-otel-alerting.md new file mode 100644 index 0000000000..6fc4a6c944 --- /dev/null +++ b/2026/day-76/day-76-otel-alerting.md @@ -0,0 +1,412 @@ +# Day 76 -- OpenTelemetry and Alerting + +## Goal + +Day 76 adds the third pillar of observability --- **traces** --- using +OpenTelemetry (OTEL), plus Prometheus and Grafana alerting. + +The completed stack covers: - **Metrics** → Prometheus - **Logs** → +Loki - **Traces** → OpenTelemetry Collector - **Visualization and +alerts** → Grafana + +## OpenTelemetry in Simple Words + +OpenTelemetry (OTEL) is a vendor-neutral framework for generating, +collecting, processing, and exporting telemetry data. + +The Collector follows this flow: + +``` text +Application → Receiver → Processor → Exporter → Backend +``` + +- **Receiver:** accepts telemetry. +- **Processor:** prepares telemetry; this lab uses batching. +- **Exporter:** sends telemetry to another system. + +Ports used: - `4317` --- OTLP gRPC - `4318` --- OTLP HTTP - `8889` --- +Prometheus-compatible metrics endpoint + +## Trace and Span + +A **trace** is the complete journey of one request. A **span** is one +step inside that journey. + +``` text +TRACE: User request +├── Span 1 → API +├── Span 2 → Authentication +└── Span 3 → Database +``` + +## Project Structure + +``` text +day-76/ +├── README.md +├── alert-rules.yml +├── docker-compose.yml +├── prometheus.yml +├── otel-collector/ +│ └── otel-collector-config.yml +├── loki/ +│ └── loki-config.yml +├── promtail/ +│ └── promtail-config.yml +├── grafana/ +│ └── provisioning/ +│ └── datasources/ +│ └── datasources.yml +└── day-76-otel-alerting.md +``` + +## OTEL Collector Configuration + +``` yaml +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + +exporters: + prometheus: + endpoint: "0.0.0.0:8889" + debug: + verbosity: detailed + +service: + pipelines: + metrics: + receivers: [otlp] + processors: [batch] + exporters: [prometheus] + traces: + receivers: [otlp] + processors: [batch] + exporters: [debug] + logs: + receivers: [otlp] + processors: [batch] + exporters: [debug] +``` + +Metrics flow: + +``` text +OTLP → OTEL Collector → :8889 → Prometheus +``` + +Trace flow: + +``` text +OTLP → OTEL Collector → Debug output +``` + +## Services + + Service Port Purpose + ---------------- ---------------- ----------------------------- + Prometheus 9090 Metrics storage/querying + Node Exporter 9100 Host metrics + cAdvisor 8080 Container metrics + Grafana 3000 Dashboards and alerting + Loki 3100 Log storage + Promtail 9080 internal Docker log collection + OTEL Collector 4317/4318/8889 Telemetry collection/export + Notes App 8000 Sample application + +Final `docker compose ps` verification showed all eight services +running, with cAdvisor healthy. + +## OTEL Collector Verification + +Command: + +``` bash +docker logs otel-collector 2>&1 | tail -10 +``` + +Observed: + +``` text +Starting GRPC server ... :4317 +Starting HTTP server ... :4318 +Everything is ready. Begin running and processing data. +``` + +Prometheus Targets showed `cadvisor`, `node-exporter`, `otel-collector`, +and `prometheus` UP. The OTEL scrape endpoint was `otel-collector:8889`. + +## Test Trace + +A sample OTLP trace was POSTed to: + +``` text +http://localhost:4318/v1/traces +``` + +Verification: + +``` bash +docker logs otel-collector 2>&1 | grep -A 15 "test-span" +``` + +Observed: + +``` text +Name: test-span +Kind: Internal +http.method: GET +http.status_code: 200 +otelcol.signal: traces +``` + +This proved: + +``` text +curl → OTLP HTTP :4318 → Receiver → Batch Processor → Debug Exporter → docker logs +``` + +## Test OTLP Metric + +A metric named `test_requests_total` with value `42` was sent through +OTLP. + +PromQL: + +``` promql +test_requests_total +``` + +Result: + +``` text +test_requests_total = 42 +``` + +Labels included `exported_job="my-test-service"`, +`instance="otel-collector:8889"`, and `job="otel-collector"`. + +Flow: + +``` text +curl → OTEL :4318 → Prometheus exporter :8889 → Prometheus → PromQL → 42 +``` + +## Prometheus Alert Rules + +Five rules were configured: + +- **HighCPUUsage:** CPU above 80% for 2 minutes; severity `warning`. +- **HighMemoryUsage:** memory above 85% for 2 minutes; severity + `warning`. +- **ContainerDown:** expected Notes App cAdvisor series absent for 1 + minute; severity `critical`. +- **TargetDown:** `up == 0` for 1 minute; severity `critical`. +- **HighDiskUsage:** root disk above 90% for 5 minutes; severity + `critical`. + +Alert lifecycle: + +``` text +INACTIVE → condition true → PENDING → "for:" duration passes → FIRING +``` + +When the condition clears, the alert returns to inactive. + +## TargetDown Test + +Node Exporter was deliberately stopped: + +``` bash +docker compose stop node-exporter +``` + +Prometheus could not scrape `node-exporter:9100`, so `up == 0` became +true. + +Observed: + +``` text +TargetDown: INACTIVE → PENDING → FIRING +``` + +Node Exporter was restored: + +``` bash +docker compose start node-exporter +``` + +This successfully demonstrated a real Prometheus alert lifecycle. + +## Debugging Lesson: ContainerDown + +`ContainerDown` fired even though Notes App was running. + +Rule: + +``` promql +absent(container_last_seen{name="notes-app"}) +``` + +The query returned `1`, meaning the matching time series was absent. We +also found: + +``` promql +container_memory_usage_bytes{name!=""} +``` + +returned no data, while unfiltered `container_memory_usage_bytes` +returned data. + +The cAdvisor data in this environment did not expose the expected +`name="notes-app"` label. Therefore the alert query did not match the +real labels. + +**Lesson:** a firing alert does not automatically prove an application +is broken. Check the PromQL expression and actual metric labels. + +## Grafana Contact Point + +Created: + +``` text +Name: DevOps Team +Integration: Email +``` + +Flow: + +``` text +Alert → Notification policy → Contact point → Email/Slack/PagerDuty/etc. +``` + +Actual email delivery from a local Docker Grafana setup also requires +suitable SMTP configuration. + +## Grafana Alert Rule + +Created: + +``` text +High Container Memory +``` + +Datasource: `Prometheus` + +Working query for this environment: + +``` promql +container_memory_usage_bytes / 1024 / 1024 +``` + +Configuration: + +``` text +Threshold: IS ABOVE 100 MB +Evaluation interval: 1m +Pending period: 2m +Label: severity=warning +Contact point: DevOps Team +Folder: DevOps Alerts +Evaluation group: DevOps Evaluation +``` + +The saved rule displayed `High Container Memory → Normal`. + +## Prometheus Alerts vs Grafana Alerts + +**Prometheus alerts** evaluate PromQL conditions directly against +metrics. In this lab, Prometheus displayed the alert state. Without +Alertmanager, the Prometheus setup did not provide the notification +routing used here. + +**Grafana alerts** can evaluate metric conditions and route firing +alerts to configured contact points. + +## Full Architecture + +``` text + METRICS +Node Exporter ─────┐ +cAdvisor ──────────┼──→ Prometheus ──→ Grafana +OTEL :8889 ────────┘ │ + └──→ Alert Rules + + LOGS +Docker Containers → Promtail → Loki → Grafana + + TRACES +Application/curl → OTLP :4318 → OTEL Collector → Debug Output + └──→ Future: Jaeger/Tempo +``` + +## Three Pillars + +- **Metrics:** WHAT happened? Example: CPU 85%, memory 90%. +- **Logs:** WHY did it happen? Example: ERROR, HTTP 404. +- **Traces:** WHERE did it happen in the request journey? + +``` text +Metrics → WHAT? +Logs → WHY? +Traces → WHERE? +``` + +## Useful Commands + +``` bash +docker compose up -d +docker compose ps +docker logs otel-collector +docker logs otel-collector 2>&1 | grep -A 15 "test-span" +docker compose stop node-exporter +docker compose start node-exporter +docker compose down -v +``` + +## Interview Explanation + +> I built a Docker Compose observability stack using Prometheus, +> Grafana, Loki, Promtail, Node Exporter, cAdvisor, and OpenTelemetry. +> Prometheus collected metrics, Loki stored logs, and OpenTelemetry +> handled OTLP telemetry and traces. I also created Prometheus and +> Grafana alerts and tested a TargetDown alert by stopping Node +> Exporter. + +OpenTelemetry: \> The OpenTelemetry Collector receives telemetry, +processes it, and exports it. In my lab it received OTLP data on ports +4317 and 4318. Metrics were exposed for Prometheus on port 8889, while +traces were sent to the debug exporter. + +Alert lifecycle: \> An alert starts inactive. When its condition becomes +true, it can become pending. If the condition stays true for the +configured pending period, it becomes firing. When the condition clears, +it returns to normal. + +## Final Result + +``` text +OpenTelemetry Collector DONE +OTLP gRPC :4317 DONE +OTLP HTTP :4318 DONE +Prometheus exporter :8889 DONE +Test trace DONE +test-span in OTEL logs DONE +OTLP test metric DONE +test_requests_total = 42 DONE +Prometheus alert rules DONE +TargetDown test DONE +Grafana contact point DONE +Grafana memory alert DONE +8-service stack verification DONE +``` + +Day 76 successfully added **traces and alerting** to the existing +metrics-and-logs observability stack. diff --git a/2026/day-76/day-76.png b/2026/day-76/day-76.png new file mode 100644 index 0000000000..a43a0fbe40 Binary files /dev/null and b/2026/day-76/day-76.png differ diff --git a/2026/day-76/docker-compose.yml b/2026/day-76/docker-compose.yml new file mode 100644 index 0000000000..2ccd8dcf45 --- /dev/null +++ b/2026/day-76/docker-compose.yml @@ -0,0 +1,118 @@ +networks: + monitoring: + driver: bridge + +volumes: + prometheus_data: {} + grafana_data: {} + loki_data: {} + +services: + + prometheus: + image: prom/prometheus:latest + container_name: prometheus + ports: + - "9090:9090" + volumes: + - ./prometheus.yml:/etc/prometheus/prometheus.yml + - ./alert-rules.yml:/etc/prometheus/alert-rules.yml + - prometheus_data:/prometheus + command: + - '--config.file=/etc/prometheus/prometheus.yml' + networks: + - monitoring + restart: unless-stopped + + notes-app: + image: trainwithshubham/notes-app:latest + container_name: notes-app + ports: + - "8000:8000" + networks: + - monitoring + restart: unless-stopped + + node-exporter: + image: prom/node-exporter:latest + container_name: node-exporter + ports: + - "9100:9100" + volumes: + - /proc:/host/proc:ro + - /sys:/host/sys:ro + - /:/rootfs:ro + command: + - '--path.procfs=/host/proc' + - '--path.sysfs=/host/sys' + - '--path.rootfs=/rootfs' + - '--collector.filesystem.mount-points-exclude=^/(sys|proc|dev|host|etc)($$|/)' + networks: + - monitoring + restart: unless-stopped + + cadvisor: + image: gcr.io/cadvisor/cadvisor:latest + container_name: cadvisor + ports: + - "8080:8080" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /sys:/sys:ro + - /var/lib/docker/:/var/lib/docker:ro + networks: + - monitoring + restart: unless-stopped + + grafana: + image: grafana/grafana-enterprise:latest + container_name: grafana + ports: + - "3000:3000" + volumes: + - grafana_data:/var/lib/grafana + - ./grafana/provisioning:/etc/grafana/provisioning + environment: + - GF_SECURITY_ADMIN_USER=admin + - GF_SECURITY_ADMIN_PASSWORD=admin123 + networks: + - monitoring + restart: unless-stopped + + loki: + image: grafana/loki:latest + container_name: loki + ports: + - "3100:3100" + volumes: + - ./loki/loki-config.yml:/etc/loki/loki-config.yml + - loki_data:/loki + command: -config.file=/etc/loki/loki-config.yml + networks: + - monitoring + restart: unless-stopped + + promtail: + image: grafana/promtail:latest + container_name: promtail + volumes: + - ./promtail/promtail-config.yml:/etc/promtail/promtail-config.yml + - /var/lib/docker/containers:/var/lib/docker/containers:ro + - /var/run/docker.sock:/var/run/docker.sock + command: -config.file=/etc/promtail/promtail-config.yml + networks: + - monitoring + restart: unless-stopped + + otel-collector: + image: otel/opentelemetry-collector-contrib:latest + container_name: otel-collector + ports: + - "4317:4317" + - "4318:4318" + - "8889:8889" + volumes: + - ./otel-collector/otel-collector-config.yml:/etc/otelcol-contrib/config.yaml + networks: + - monitoring + restart: unless-stopped \ No newline at end of file diff --git a/2026/day-76/grafana/provisioning/datasources/datasources.yml b/2026/day-76/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 0000000000..2c0808d808 --- /dev/null +++ b/2026/day-76/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,15 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true + editable: false + + - name: Loki + type: loki + access: proxy + url: http://loki:3100 + editable: false diff --git a/2026/day-76/loki/loki-config.yml b/2026/day-76/loki/loki-config.yml new file mode 100644 index 0000000000..056f9fcb9b --- /dev/null +++ b/2026/day-76/loki/loki-config.yml @@ -0,0 +1,26 @@ +auth_enabled: false + +server: + http_listen_port: 3100 + +common: + ring: + instance_addr: 127.0.0.1 + kvstore: + store: inmemory + replication_factor: 1 + path_prefix: /loki + +schema_config: + configs: + - from: 2020-10-24 + store: tsdb + object_store: filesystem + schema: v13 + index: + prefix: index_ + period: 24h + +storage_config: + filesystem: + directory: /loki/chunks diff --git a/2026/day-76/otel-collector/otel-collector-config.yml b/2026/day-76/otel-collector/otel-collector-config.yml new file mode 100644 index 0000000000..2aaaf999dd --- /dev/null +++ b/2026/day-76/otel-collector/otel-collector-config.yml @@ -0,0 +1,33 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: + +exporters: + prometheus: + endpoint: "0.0.0.0:8889" + debug: + verbosity: detailed + +service: + pipelines: + metrics: + receivers: [otlp] + processors: [batch] + exporters: [prometheus] + + traces: + receivers: [otlp] + processors: [batch] + exporters: [debug] + + logs: + receivers: [otlp] + processors: [batch] + exporters: [debug] diff --git a/2026/day-76/prometheus.yml b/2026/day-76/prometheus.yml new file mode 100644 index 0000000000..148afb9af3 --- /dev/null +++ b/2026/day-76/prometheus.yml @@ -0,0 +1,23 @@ +global: + scrape_interval: 15s + evaluation_interval: 15s + +rule_files: + - /etc/prometheus/alert-rules.yml + +scrape_configs: + - job_name: "prometheus" + static_configs: + - targets: ["localhost:9090"] + + - job_name: "node-exporter" + static_configs: + - targets: ["node-exporter:9100"] + + - job_name: "cadvisor" + static_configs: + - targets: ["cadvisor:8080"] + + - job_name: "otel-collector" + static_configs: + - targets: ["otel-collector:8889"] diff --git a/2026/day-76/promtail/promtail-config.yml b/2026/day-76/promtail/promtail-config.yml new file mode 100644 index 0000000000..53d69b1584 --- /dev/null +++ b/2026/day-76/promtail/promtail-config.yml @@ -0,0 +1,20 @@ +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /tmp/positions.yaml + +clients: + - url: http://loki:3100/loki/api/v1/push + +scrape_configs: + - job_name: docker + static_configs: + - targets: + - localhost + labels: + job: docker + __path__: /var/lib/docker/containers/*/*-json.log + pipeline_stages: + - docker: {} diff --git a/2026/day-77/Day-77.png b/2026/day-77/Day-77.png new file mode 100644 index 0000000000..6cb22d0028 Binary files /dev/null and b/2026/day-77/Day-77.png differ diff --git a/2026/day-77/Screenshot 2026-09-30 143951.png b/2026/day-77/Screenshot 2026-09-30 143951.png new file mode 100644 index 0000000000..ce36c092e1 Binary files /dev/null and b/2026/day-77/Screenshot 2026-09-30 143951.png differ diff --git a/2026/day-77/Screenshot 2026-09-30 144111.png b/2026/day-77/Screenshot 2026-09-30 144111.png new file mode 100644 index 0000000000..0735d26702 Binary files /dev/null and b/2026/day-77/Screenshot 2026-09-30 144111.png differ diff --git a/2026/day-77/Screenshot 2026-09-30 151801.png b/2026/day-77/Screenshot 2026-09-30 151801.png new file mode 100644 index 0000000000..a67f468d6a Binary files /dev/null and b/2026/day-77/Screenshot 2026-09-30 151801.png differ diff --git a/2026/day-77/Screenshot 2026-09-30 151909.png b/2026/day-77/Screenshot 2026-09-30 151909.png new file mode 100644 index 0000000000..415e0ab4b8 Binary files /dev/null and b/2026/day-77/Screenshot 2026-09-30 151909.png differ diff --git a/2026/day-77/Screenshot 2026-09-30 152034.png b/2026/day-77/Screenshot 2026-09-30 152034.png new file mode 100644 index 0000000000..238b606b5a Binary files /dev/null and b/2026/day-77/Screenshot 2026-09-30 152034.png differ diff --git a/2026/day-77/Screenshot 2026-09-30 152539.png b/2026/day-77/Screenshot 2026-09-30 152539.png new file mode 100644 index 0000000000..2216dbb3a8 Binary files /dev/null and b/2026/day-77/Screenshot 2026-09-30 152539.png differ diff --git a/2026/day-77/Screenshot 2026-09-30 152652.png b/2026/day-77/Screenshot 2026-09-30 152652.png new file mode 100644 index 0000000000..f4ea21142b Binary files /dev/null and b/2026/day-77/Screenshot 2026-09-30 152652.png differ diff --git a/2026/day-77/Screenshot 2026-09-30 153135.png b/2026/day-77/Screenshot 2026-09-30 153135.png new file mode 100644 index 0000000000..f31a5c6a98 Binary files /dev/null and b/2026/day-77/Screenshot 2026-09-30 153135.png differ diff --git a/2026/day-77/Screenshot 2026-09-30 153942.png b/2026/day-77/Screenshot 2026-09-30 153942.png new file mode 100644 index 0000000000..0fadd69461 Binary files /dev/null and b/2026/day-77/Screenshot 2026-09-30 153942.png differ diff --git a/2026/day-77/day-77-full-observability-stack.md b/2026/day-77/day-77-full-observability-stack.md new file mode 100644 index 0000000000..05d6480f20 --- /dev/null +++ b/2026/day-77/day-77-full-observability-stack.md @@ -0,0 +1,414 @@ +# Day 77 -- Observability Project: Full Stack with Docker Compose + +## Goal + +Build and verify a complete observability stack using Docker Compose. + +The lab combines the concepts practiced in Days 73--76: + +- Prometheus for metrics +- Node Exporter for host metrics +- cAdvisor for container metrics +- Loki for log storage/querying +- Promtail for collecting Docker logs +- OpenTelemetry Collector for telemetry/traces +- Grafana for visualization +- Notes App as the sample application + +------------------------------------------------------------------------ + +## Architecture + +``` text +Host Machine + | + +--> Node Exporter --------+ + | +Docker Containers v + +--> cAdvisor --------> Prometheus ----+ + | +Docker Logs --> Promtail --> Loki ---------+--> Grafana + | +Application --> OTEL Collector ------------+ + | + Traces +``` + +Simple meaning: + +- **Metrics** tell us WHAT is happening. +- **Logs** help explain WHY it may be happening. +- **Traces** show WHERE a request travelled. +- **Grafana** gives us one place to view observability data. + +------------------------------------------------------------------------ + +## Project Setup + +Repository used: + +``` bash +git clone https://github.com/LondheShubham153/observability-for-devops.git +cd observability-for-devops +``` + +Project structure: + +``` text +. +├── README.md +├── assets +├── docker-compose.yml +├── grafana +│ └── provisioning +│ ├── dashboards +│ └── datasources +├── loki +│ └── loki-config.yml +├── notes-app +├── otel-collector +│ └── otel-collector-config.yml +├── prometheus.yml +└── promtail + └── promtail-config.yml +``` + +------------------------------------------------------------------------ + +## Start the Stack + +``` bash +docker compose up -d +``` + +Verify: + +``` bash +docker compose ps +``` + +Services verified running: + +``` text +cadvisor +grafana +loki +node-exporter +notes-app +otel-collector +prometheus +promtail +``` + +------------------------------------------------------------------------ + +## Prometheus Target Verification + +Opened Prometheus and checked target health. + +The following targets were **UP**: + +``` text +docker / cAdvisor -> cadvisor:8080 +node-exporter -> node-exporter:9100 +otel-collector -> otel-collector:8889 +prometheus -> localhost:9090 +``` + +This proves Prometheus can successfully scrape the configured metric +exporters. + +------------------------------------------------------------------------ + +## Metric Testing + +### Host CPU + +``` promql +node_cpu_seconds_total +``` + +This returned CPU metrics from Node Exporter. + +### Container Memory + +``` promql +container_memory_usage_bytes +``` + +This returned container resource metrics from cAdvisor. + +Important lesson: + +> Node Exporter monitors the host machine, while cAdvisor monitors +> containers. + +------------------------------------------------------------------------ + +## OpenTelemetry Collector + +Collector configuration: + +``` yaml +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +processors: + batch: {} + +exporters: + prometheus: + endpoint: 0.0.0.0:8889 + debug: + verbosity: basic + +service: + pipelines: + metrics: + receivers: [otlp] + processors: [batch] + exporters: [prometheus] + + traces: + receivers: [otlp] + processors: [batch] + exporters: [debug] + + logs: + receivers: [otlp] + processors: [batch] + exporters: [debug] +``` + +### Trace Test + +A fresh OTLP trace was sent to: + +``` text +http://localhost:4318/v1/traces +``` + +Then the collector logs were checked: + +``` bash +docker logs otel-collector --since=1m 2>&1 | tail -50 +``` + +Successful output included: + +``` text +Traces +otelcol.signal: "traces" +resource spans: 1 +spans: 1 +``` + +This proves: + +``` text +Trace + ↓ +OTLP HTTP Receiver :4318 + ↓ +OpenTelemetry Collector + ↓ +Batch Processor + ↓ +Debug Exporter + ↓ +Trace processed successfully +``` + +In this lab, traces were verified through the collector's debug +exporter. A dedicated trace backend was not used for storing and +visualizing traces. + +------------------------------------------------------------------------ + +## Loki and Promtail + +Log flow: + +``` text +Docker Containers + ↓ + Promtail + ↓ + Loki + ↓ + Grafana +``` + +Grafana used Loki as the datasource for Docker logs. + +LogQL query: + +``` logql +{job="docker"} +``` + +Optional error filtering: + +``` logql +{job="docker"} |= "error" +``` + +------------------------------------------------------------------------ + +## Final Grafana Dashboard + +A unified Grafana dashboard was created. + +### Panel 1 -- CPU Usage % + +Datasource: **Prometheus** + +``` promql +100 - (avg(rate(node_cpu_seconds_total{mode="idle"}[5m])) * 100) +``` + +### Panel 2 -- Memory Usage % + +Datasource: **Prometheus** + +``` promql +(1 - node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes) * 100 +``` + +### Panel 3 -- Container Memory MB + +Datasource: **Prometheus** + +``` promql +container_memory_usage_bytes / 1024 / 1024 +``` + +### Panel 4 -- Container CPU % + +Datasource: **Prometheus** + +``` promql +rate(container_cpu_usage_seconds_total[5m]) * 100 +``` + +### Panel 5 -- Docker Logs + +Datasource: **Loki** + +``` logql +{job="docker"} +``` + +Final dashboard layout: + +``` text ++------------------------+------------------------+ +| Container Memory MB | CPU Usage % | +| cAdvisor + Prometheus | Node Exporter | ++------------------------+------------------------+ +| Container CPU % | Memory Usage % | +| cAdvisor + Prometheus | Node Exporter | ++------------------------+------------------------+ +| Docker Logs | +| Promtail + Loki | ++-------------------------------------------------+ +``` + +------------------------------------------------------------------------ + +## What Each Tool Does + + ----------------------------------------------------------------------- + Tool Simple Purpose + ----------------------------------- ----------------------------------- + Prometheus Collects and stores metrics + + Node Exporter Exposes host CPU, RAM, disk and + network metrics + + cAdvisor Exposes Docker container resource + metrics + + Promtail Collects Docker logs + + Loki Stores and queries logs + + OpenTelemetry Collector Receives and processes telemetry + such as traces + + Grafana Visualizes and correlates + observability data + ----------------------------------------------------------------------- + +------------------------------------------------------------------------ + +## Days 73--77 Learning Flow + +``` text +Day 73 +Prometheus + PromQL + ↓ +Day 74 +Node Exporter + cAdvisor + Grafana + ↓ +Day 75 +Loki + Promtail + LogQL + ↓ +Day 76 +OpenTelemetry + Alerting + ↓ +Day 77 +Full Observability Integration +``` + +------------------------------------------------------------------------ + +## Interview Explanation + +> I built a Docker Compose observability stack. Node Exporter and +> cAdvisor expose infrastructure and container metrics to Prometheus. +> Promtail collects Docker logs and sends them to Loki. OpenTelemetry +> Collector receives telemetry such as traces. Grafana is used to +> visualize and correlate the observability data. + +Short version: + +> Prometheus gives me metrics, Loki gives me logs, OpenTelemetry handles +> telemetry and traces, and Grafana helps me view and correlate the +> information. + +------------------------------------------------------------------------ + +## Cleanup + +After completing and saving the dashboard: + +``` bash +docker compose down -v +``` + +This removes the lab containers, network, and Compose volumes. + +------------------------------------------------------------------------ + +## Final Result + +Day 77 completed successfully. + +Verified: + +- Docker Compose observability stack running +- Prometheus targets UP +- Node Exporter host metrics +- cAdvisor container metrics +- Loki/Promtail Docker logs +- OpenTelemetry trace processing +- Unified Grafana dashboard +- Metrics + logs + traces concepts understood + +**Day 77 -- Complete** diff --git a/2026/day-77/monitoring_observability.pdf b/2026/day-77/monitoring_observability.pdf new file mode 100644 index 0000000000..0f201178ea Binary files /dev/null and b/2026/day-77/monitoring_observability.pdf differ diff --git a/2026/day-77/observability-for-devops b/2026/day-77/observability-for-devops new file mode 160000 index 0000000000..5be5f75110 --- /dev/null +++ b/2026/day-77/observability-for-devops @@ -0,0 +1 @@ +Subproject commit 5be5f751109bb2e185c2ac85dd3cda8015e8c60b diff --git a/2026/day-78/AI-BankApp-DevOps/.github/workflows/gitops-ci.yml b/2026/day-78/AI-BankApp-DevOps/.github/workflows/gitops-ci.yml new file mode 100644 index 0000000000..987549c66c --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/.github/workflows/gitops-ci.yml @@ -0,0 +1,75 @@ +name: GitOps CI - Build & Push to DockerHub + +on: + push: + branches: [feat/gitops] + paths: + - 'src/**' + - 'pom.xml' + - 'Dockerfile' + workflow_dispatch: + +permissions: + contents: write + +env: + DOCKERHUB_REPO: trainwithshubham/ai-bankapp-eks + +jobs: + build-and-push: + name: Build, Test & Push Docker Image + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + + - name: Build with Maven + run: ./mvnw clean package -DskipTests -B + + - name: Run tests + run: ./mvnw test -B + continue-on-error: true + + - name: Set image tag + id: tag + run: echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and Push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ${{ env.DOCKERHUB_REPO }}:latest + ${{ env.DOCKERHUB_REPO }}:${{ steps.tag.outputs.sha_short }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Update Kubernetes deployment manifest + run: | + sed -i "s|image: ${{ env.DOCKERHUB_REPO }}:.*|image: ${{ env.DOCKERHUB_REPO }}:${{ steps.tag.outputs.sha_short }}|" k8s/bankapp-deployment.yml + + - name: Commit updated manifest + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add k8s/bankapp-deployment.yml + git diff --staged --quiet || git commit -m "ci: update bankapp image to ${{ steps.tag.outputs.sha_short }} [skip ci]" + git push diff --git a/2026/day-78/AI-BankApp-DevOps/.gitignore b/2026/day-78/AI-BankApp-DevOps/.gitignore new file mode 100644 index 0000000000..d1b1c95d21 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/.gitignore @@ -0,0 +1,45 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Terraform ### +.terraform/ +*.tfstate +*.tfstate.backup +*.tfplan + +### Claude ### +CLAUDE.md + +### Speaker Notes (personal, not for repo) ### +SPEAKER_NOTES.md diff --git a/2026/day-78/AI-BankApp-DevOps/.mvn/wrapper/maven-wrapper.properties b/2026/day-78/AI-BankApp-DevOps/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..8f96f52c66 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/2026/day-78/AI-BankApp-DevOps/DEPLOYMENT.md b/2026/day-78/AI-BankApp-DevOps/DEPLOYMENT.md new file mode 100644 index 0000000000..f585c23a2e --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/DEPLOYMENT.md @@ -0,0 +1,276 @@ +# Deployment Playbook — AI BankApp on EKS + +Step-by-step commands to deploy the full stack. Run these in order. + +## Prerequisites + +- AWS CLI configured (`aws configure`) +- Terraform >= 1.5.7 +- kubectl +- Helm 3 (`brew install helm`) +- Docker (for local image builds) +- GitHub repo secrets: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN` + +## Step 1: Provision Infrastructure + +```bash +cd terraform +terraform init +terraform plan +terraform apply +# Takes ~15 minutes — creates VPC, EKS cluster (3 nodes), ArgoCD +``` + +## Step 2: Configure kubectl + +```bash +aws eks update-kubeconfig --name bankapp-eks --region us-west-2 +kubectl get nodes +# Should show 3 nodes in Ready state +``` + +## Step 3: Verify ArgoCD + +```bash +# Check all pods are Running +kubectl get pods -n argocd + +# Get ArgoCD URL +kubectl get svc argocd-server -n argocd \ + -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' + +# Get admin password +kubectl get secret argocd-initial-admin-secret -n argocd \ + -o jsonpath='{.data.password}' | base64 -d; echo + +# Login: admin / +``` + +## Step 4: Install Gateway API + Envoy Gateway + +```bash +# Install Gateway API CRDs — MUST use --server-side +# Without it, Envoy Gateway helm install fails with CRD ownership conflicts +kubectl apply --server-side \ + -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml + +# Install Envoy Gateway — MUST use --skip-crds since Gateway API CRDs already exist +helm install eg oci://docker.io/envoyproxy/gateway-helm \ + --version v1.2.6 \ + -n envoy-gateway-system \ + --create-namespace \ + --skip-crds \ + --wait + +# --skip-crds also skips Envoy Gateway's own extension CRDs, install them separately +helm pull oci://docker.io/envoyproxy/gateway-helm --version v1.2.6 --untar -d /tmp/eg-chart +kubectl apply --server-side -f /tmp/eg-chart/gateway-helm/crds/generated/ +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system + +# Verify +kubectl get gatewayclass +``` + +## Step 5: Install cert-manager (TLS/HTTPS) + +```bash +helm install cert-manager oci://quay.io/jetstack/charts/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --set crds.enabled=true \ + --set config.enableGatewayAPI=true \ + --wait + +# Verify +kubectl get pods -n cert-manager +``` + +After ArgoCD syncs the ClusterIssuer and updated Gateway (with HTTPS listener), cert-manager automatically provisions a Let's Encrypt TLS certificate. + +**Prerequisite:** Create a CNAME in GoDaddy: +- `bankapp.trainwithshubham.com` → `` + +```bash +# Check certificate status +kubectl get certificate -n bankapp +kubectl get secret bankapp-tls -n bankapp +``` + +## Step 6: Install kube-prometheus-stack + +```bash +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +helm repo update prometheus-community + +helm install kube-prometheus prometheus-community/kube-prometheus-stack \ + -n monitoring \ + --create-namespace \ + --set grafana.service.type=LoadBalancer \ + --wait + +# Get Grafana URL +kubectl get svc kube-prometheus-grafana -n monitoring \ + -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' + +# Get Grafana password +kubectl get secret kube-prometheus-grafana -n monitoring \ + -o jsonpath='{.data.admin-password}' | base64 -d; echo + +# Login: admin / +``` + +## Step 7: Build & Push Docker Image + +> CI does this automatically on push to `feat/gitops`. This step is only needed for the **first deploy** (image doesn't exist on DockerHub yet). + +```bash +# IMPORTANT: EKS runs amd64 nodes. If building on Apple Silicon (M1/M2/M3), +# you MUST specify --platform linux/amd64 or pods will fail with: +# "no match for platform in manifest: not found" + +docker buildx build --platform linux/amd64 \ + -t trainwithshubham/ai-bankapp-eks:latest \ + --push . +``` + +## Step 8: Deploy via ArgoCD + +```bash +kubectl apply -f argocd/application.yml + +# Watch sync progress +kubectl get application bankapp -n argocd -w +``` + +## Step 9: Verify Everything + +```bash +# All pods should be Running +kubectl get pods -n bankapp + +# Check PVCs are Bound +kubectl get pvc -n bankapp + +# Check Gateway has an address +kubectl get gateway -n bankapp + +# Get the app URL (NLB created by Envoy Gateway) +kubectl get svc -n envoy-gateway-system \ + -l gateway.envoyproxy.io/owning-gateway-name=bankapp-gateway \ + -o jsonpath='{.items[0].status.loadBalancer.ingress[0].hostname}' + +# Test — should return 302 (Spring Security redirect to /login) +curl -s -o /dev/null -w "%{http_code}" http:/// + +# Test login page — should return 200 +curl -s -o /dev/null -w "%{http_code}" -L http:///login +``` + +## Step 10: Pull Ollama Model + +```bash +# Ollama starts empty — pull the AI model +kubectl exec -n bankapp deploy/ollama -- ollama pull tinyllama +``` + +## Cleanup + +> **ORDER MATTERS.** Helm-installed resources (Envoy Gateway, Grafana) create AWS Load Balancers +> and Security Groups outside of Terraform. If you run `terraform destroy` first, the EKS cluster +> is gone but those orphaned resources block VPC deletion. Always clean up in this order: + +```bash +# 1. Delete ArgoCD app (removes Gateway → deletes Envoy Gateway NLB) +kubectl delete -f argocd/application.yml + +# 2. Uninstall all Helm releases that created LoadBalancers/resources +helm uninstall cert-manager -n cert-manager +helm uninstall kube-prometheus -n monitoring +helm uninstall eg -n envoy-gateway-system + +# 3. Delete Gateway API CRDs +kubectl delete -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml + +# 4. Wait for Load Balancers to fully terminate (~30-60 seconds) +echo "Waiting for LBs to terminate..." +sleep 60 + +# 5. Verify no Load Balancers remain in the VPC +aws elb describe-load-balancers --region us-west-2 \ + --query 'LoadBalancerDescriptions[*].LoadBalancerName' --output text +# Should be empty. If not, delete manually: +# aws elb delete-load-balancer --load-balancer-name --region us-west-2 + +# 6. Destroy infrastructure +cd terraform +terraform destroy +``` + +**If `terraform destroy` gets stuck on VPC deletion**, orphaned resources remain: +```bash +# Find and delete orphaned security groups +aws ec2 describe-security-groups --region us-west-2 \ + --filters Name=vpc-id,Values= \ + --query 'SecurityGroups[?GroupName!=`default`].[GroupId,GroupName]' --output table +aws ec2 delete-security-group --group-id --region us-west-2 + +# Then delete the VPC manually +aws ec2 delete-vpc --vpc-id --region us-west-2 + +# Re-run terraform destroy to clean the state +terraform destroy +``` + +--- + +## Gotchas We Hit + +### 1. Gateway API CRDs + Envoy Gateway Conflict +**Symptom:** `helm install` for Envoy Gateway fails with `conflict with "kubectl-client-side-apply"`. +**Cause:** `kubectl apply` uses client-side apply for CRDs. When Envoy Gateway tries to install the same CRDs via server-side apply, ownership conflicts. +**Fix:** Use `kubectl apply --server-side` for CRDs, then `--skip-crds` on helm install. + +### 2. Docker Image Platform Mismatch +**Symptom:** Pods show `ErrImagePull` with error `no match for platform in manifest: not found`. +**Cause:** Building on Apple Silicon (arm64) produces arm64 images. EKS nodes are amd64. +**Fix:** Use `docker buildx build --platform linux/amd64 --push`. This does NOT apply to GitHub Actions CI (runs on ubuntu/amd64). + +### 3. Image Doesn't Exist on First Deploy +**Symptom:** Pods show `ImagePullBackOff` with `repository does not exist or may require authorization`. +**Cause:** ArgoCD deploys immediately but CI hasn't pushed the first image to DockerHub yet. +**Fix:** Build and push the image manually (Step 6) before or right after applying the ArgoCD application. + +### 4. Envoy Gateway Extension CRDs Missing After `--skip-crds` +**Symptom:** `BackendTrafficPolicy` resource fails with `no matches for kind`. +**Cause:** `--skip-crds` skips ALL CRDs, including Envoy Gateway's own extension CRDs (BackendTrafficPolicy, SecurityPolicy, etc.) — not just the Gateway API CRDs. +**Fix:** +```bash +helm pull oci://docker.io/envoyproxy/gateway-helm --version v1.2.6 --untar -d /tmp/eg-chart +kubectl apply --server-side -f /tmp/eg-chart/gateway-helm/crds/generated/ +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +### 5. Login Redirect Loop with Multiple Replicas Behind Envoy Gateway +**Symptom:** Login page keeps redirecting back to itself. No error message shown. +**Cause:** Envoy Gateway bypasses K8s Service `sessionAffinity: ClientIP` and load-balances directly to pod endpoints. With in-memory sessions, GET /login hits pod A (creates CSRF token), POST /login hits pod B (CSRF mismatch → silent redirect to /login). +**Fix:** Add `BackendTrafficPolicy` with cookie-based consistent hashing (in `k8s/gateway.yml`) and `SERVER_FORWARD_HEADERS_STRATEGY=native` in configmap. + +### 6. `terraform destroy` Stuck on VPC Deletion +**Symptom:** `terraform destroy` hangs on VPC delete with `DependencyViolation`. +**Cause:** Helm-installed resources (Envoy Gateway, Grafana LB) created AWS Load Balancers and Security Groups outside Terraform. When EKS is destroyed first, these orphan and block VPC deletion. +**Fix:** Always uninstall Helm releases and delete the ArgoCD app BEFORE running `terraform destroy`. If already stuck, delete orphaned ELBs and SGs via AWS CLI (see Cleanup section above). + +### 7. ArgoCD Shows OutOfSync After Manual Rollout Restart +**Symptom:** ArgoCD status shows `OutOfSync` even though app is healthy. +**Cause:** `kubectl rollout restart` adds a restartedAt annotation that doesn't match the Git manifest. +**Fix:** Not a problem — next CI push updates the manifest and ArgoCD syncs to it, resolving the drift. + +--- + +## Access Summary + +| Service | URL Command | Credentials | +|---------|------------|-------------| +| **BankApp** | `kubectl get svc -n envoy-gateway-system -l gateway.envoyproxy.io/owning-gateway-name=bankapp-gateway -o jsonpath='{.items[0].status.loadBalancer.ingress[0].hostname}'` | App login | +| **ArgoCD** | `kubectl get svc argocd-server -n argocd -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'` | `admin` / see Step 3 | +| **Grafana** | `kubectl get svc kube-prometheus-grafana -n monitoring -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'` | `admin` / see Step 5 | diff --git a/2026/day-78/AI-BankApp-DevOps/Dockerfile b/2026/day-78/AI-BankApp-DevOps/Dockerfile new file mode 100644 index 0000000000..92c47d42b1 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/Dockerfile @@ -0,0 +1,22 @@ +# Build stage +FROM eclipse-temurin:21-jdk-alpine AS build +WORKDIR /app +COPY . . +RUN chmod +x mvnw && ./mvnw clean package -DskipTests -B + +# Run stage - alpine has significantly fewer CVEs than ubuntu/jammy +FROM eclipse-temurin:21-jre-alpine +WORKDIR /app + +# Pull latest security patches for OS libraries +RUN apk update && apk upgrade --no-cache + +# Create a non-root user for security (Alpine uses addgroup/adduser instead of groupadd/useradd) +RUN addgroup -S devsecops && adduser -S -G devsecops devsecops +USER devsecops + +# Copy only the built artifact +COPY --from=build /app/target/*.jar app.jar + +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/2026/day-78/AI-BankApp-DevOps/LICENSE b/2026/day-78/AI-BankApp-DevOps/LICENSE new file mode 100644 index 0000000000..58006f3ebf --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 TrainWithShubham + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/2026/day-78/AI-BankApp-DevOps/README.md b/2026/day-78/AI-BankApp-DevOps/README.md new file mode 100644 index 0000000000..094689956f --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/README.md @@ -0,0 +1,160 @@ +
+ +# AI BankApp + +### End-to-End GitOps on Amazon EKS + +A modern banking application with an integrated AI chatbot, deployed on AWS EKS using Terraform, ArgoCD, Gateway API, and Prometheus monitoring. + +[![Java](https://img.shields.io/badge/Java-21-ED8B00?logo=openjdk&logoColor=white)](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html) +[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4.1-6DB33F?logo=springboot&logoColor=white)](https://spring.io/projects/spring-boot) +[![Kubernetes](https://img.shields.io/badge/EKS-1.35-326CE5?logo=kubernetes&logoColor=white)](https://aws.amazon.com/eks/) +[![ArgoCD](https://img.shields.io/badge/ArgoCD-GitOps-EF7B4D?logo=argo&logoColor=white)](https://argo-cd.readthedocs.io/) +[![Terraform](https://img.shields.io/badge/Terraform-IaC-844FBA?logo=terraform&logoColor=white)](https://www.terraform.io/) +[![Docker](https://img.shields.io/badge/Docker-Hub-2496ED?logo=docker&logoColor=white)](https://hub.docker.com/) + +--- + +![Dashboard](screenshots/dashboard.png) + +![AI Chatbot](screenshots/ai-chatbot.png) + +
+ +--- + +## Features + +- **Banking Operations** — Deposit, withdraw, transfer funds between accounts +- **AI Chatbot** — Context-aware financial assistant powered by Ollama (TinyLlama), self-hosted on Kubernetes +- **Dark/Light Mode** — Glassmorphism UI with theme toggle and localStorage persistence +- **Spring Security** — BCrypt password hashing, CSRF protection, form-based authentication +- **Prometheus Metrics** — Built-in `/actuator/prometheus` endpoint for monitoring + +--- + +## Architecture + +
+ +![GitOps Pipeline](screenshots/gitops-pipeline.gif) + +
+ +| Layer | Tool | +|-------|------| +| **Infrastructure** | Terraform (VPC + EKS + ArgoCD) | +| **CI Pipeline** | GitHub Actions → DockerHub | +| **GitOps / CD** | ArgoCD (auto-sync from `k8s/` manifests) | +| **Ingress** | Gateway API + Envoy Gateway (AWS NLB) | +| **TLS** | cert-manager + Let's Encrypt (auto-provisioned) | +| **Monitoring** | kube-prometheus-stack (Prometheus + Grafana) | +| **AI Chatbot** | Ollama (TinyLlama) on EKS | +| **Storage** | EBS CSI Driver (gp3 dynamic provisioning) | + +--- + +## What Gets Deployed + +| Resource | Details | +|----------|---------| +| **EKS Cluster** | Kubernetes 1.35, 3x `t3.medium` across 3 AZs | +| **BankApp** | 2 replicas with HPA (scales to 4), rolling updates | +| **MySQL 8.0** | Persistent EBS volume (gp3) | +| **Ollama AI** | TinyLlama model with persistent storage | +| **Gateway** | HTTPS with Let's Encrypt TLS, session persistence | +| **Monitoring** | Prometheus + Grafana dashboards | +| **ArgoCD** | Auto-sync, self-heal, prune | + +--- + +## Quick Start + +> Full step-by-step commands with troubleshooting: [`DEPLOYMENT.md`](DEPLOYMENT.md) + +```bash +# 1. Provision infrastructure (~15 min) +cd terraform && terraform init && terraform apply + +# 2. Configure kubectl +aws eks update-kubeconfig --name bankapp-eks --region us-west-2 + +# 3. Install Envoy Gateway + cert-manager + Prometheus +# (see DEPLOYMENT.md Steps 4-6) + +# 4. Deploy via ArgoCD +kubectl apply -f argocd/application.yml + +# 5. Pull AI model +kubectl exec -n bankapp deploy/ollama -- ollama pull tinyllama +``` + +--- + +## CI/CD — GitOps Flow + +``` +Code Push → GitHub Actions → Build & Push to DockerHub → Update k8s manifest → ArgoCD auto-sync → EKS +``` + +1. Push code changes to `feat/gitops` +2. **GitHub Actions** builds the app, pushes Docker image to DockerHub with commit SHA tag +3. Workflow updates `k8s/bankapp-deployment.yml` with new image tag and commits back +4. **ArgoCD** detects the manifest change and auto-syncs to EKS +5. **EKS** performs a rolling update with zero downtime + +**GitHub Secrets Required:** `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN` + +--- + +## Project Structure + +``` +. +├── terraform/ # Infrastructure as Code (VPC + EKS + ArgoCD) +├── k8s/ # Kubernetes manifests (ArgoCD watches this) +│ ├── bankapp-deployment.yml +│ ├── mysql-deployment.yml +│ ├── ollama-deployment.yml +│ ├── service.yml +│ ├── gateway.yml # Gateway API + HTTPS + session persistence +│ ├── cert-manager.yml # Let's Encrypt ClusterIssuer +│ ├── hpa.yml # Horizontal Pod Autoscaler +│ └── ... # namespace, configmap, secrets, pv, pvc +├── argocd/ +│ └── application.yml # ArgoCD Application +├── .github/workflows/ +│ └── gitops-ci.yml # CI → DockerHub → manifest update +└── DEPLOYMENT.md # Step-by-step deployment playbook +``` + +--- + +## Documentation + +| Document | Purpose | +|----------|---------| +| [`DEPLOYMENT.md`](DEPLOYMENT.md) | Step-by-step deployment commands + gotchas | +| [`terraform/README.md`](terraform/README.md) | Detailed infrastructure setup + troubleshooting | + +--- + +## Tech Stack + +**Backend:** Java 21, Spring Boot 3.4.1, Spring Security, Thymeleaf, Actuator + +**Frontend:** Bootstrap 5, glassmorphism dark/light UI, CSS custom properties + +**AI:** Ollama with TinyLlama — self-hosted, zero cost, runs as a Kubernetes pod + +**Database:** MySQL 8.0 with EBS gp3 persistent volumes + +**DevOps:** Terraform, GitHub Actions, ArgoCD, Envoy Gateway, cert-manager, kube-prometheus-stack + +--- + +
+ +**TrainWithShubham** — Happy Learning + +
diff --git a/2026/day-78/AI-BankApp-DevOps/argocd/application.yml b/2026/day-78/AI-BankApp-DevOps/argocd/application.yml new file mode 100644 index 0000000000..6b6b741b04 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/argocd/application.yml @@ -0,0 +1,21 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: bankapp + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/TrainWithShubham/AI-BankApp-DevOps.git + targetRevision: feat/gitops + path: k8s + destination: + server: https://kubernetes.default.svc + namespace: bankapp + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/2026/day-78/AI-BankApp-DevOps/docker-compose.yml b/2026/day-78/AI-BankApp-DevOps/docker-compose.yml new file mode 100644 index 0000000000..2220669840 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/docker-compose.yml @@ -0,0 +1,77 @@ +services: + mysql: + image: mysql:8.0 + container_name: bankapp-mysql + environment: + MYSQL_ROOT_PASSWORD: Test@123 + MYSQL_DATABASE: bankappdb + ports: + - "3306:3306" + volumes: + - mysql-data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - bankapp-net + + ollama: + image: ollama/ollama + container_name: bankapp-ollama + ports: + - "11434:11434" + volumes: + - ollama-data:/root/.ollama + healthcheck: + test: ["CMD", "ollama", "list"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - bankapp-net + + ollama-pull-model: + image: ollama/ollama + container_name: ollama-pull-model + environment: + - OLLAMA_HOST=ollama + volumes: + - ollama-data:/root/.ollama + depends_on: + ollama: + condition: service_healthy + entrypoint: /bin/sh + command: -c "ollama pull tinyllama" + networks: + - bankapp-net + + bankapp: + build: . + container_name: bankapp + ports: + - "8080:8080" + environment: + MYSQL_HOST: mysql + MYSQL_PORT: 3306 + MYSQL_DATABASE: bankappdb + MYSQL_USER: root + MYSQL_PASSWORD: Test@123 + OLLAMA_URL: http://ollama:11434 + depends_on: + mysql: + condition: service_healthy + ollama-pull-model: + condition: service_completed_successfully + networks: + - bankapp-net + +volumes: + mysql-data: + ollama-data: + +networks: + bankapp-net: + driver: bridge diff --git a/2026/day-78/AI-BankApp-DevOps/k8s/bankapp-deployment.yml b/2026/day-78/AI-BankApp-DevOps/k8s/bankapp-deployment.yml new file mode 100644 index 0000000000..11b0ee6e1b --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/k8s/bankapp-deployment.yml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bankapp + namespace: bankapp +spec: + replicas: 4 + selector: + matchLabels: + app: bankapp + template: + metadata: + labels: + app: bankapp + spec: + initContainers: + - name: wait-for-mysql + image: busybox:1.36 + command: ["/bin/sh", "-c", "until nc -z mysql-service 3306; do sleep 2; done"] + resources: + requests: + memory: "32Mi" + cpu: "50m" + limits: + memory: "64Mi" + cpu: "100m" + - name: wait-for-ollama + image: busybox:1.36 + command: ["/bin/sh", "-c", "until nc -z ollama-service 11434; do sleep 2; done"] + resources: + requests: + memory: "32Mi" + cpu: "50m" + limits: + memory: "64Mi" + cpu: "100m" + containers: + - name: bankapp + image: trainwithshubham/ai-bankapp-eks:1c7cb0e + imagePullPolicy: Always + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: bankapp-config + - secretRef: + name: bankapp-secret + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + readinessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 30 + failureThreshold: 15 + livenessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 5 diff --git a/2026/day-78/AI-BankApp-DevOps/k8s/cert-manager.yml b/2026/day-78/AI-BankApp-DevOps/k8s/cert-manager.yml new file mode 100644 index 0000000000..64aa7f0f2e --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/k8s/cert-manager.yml @@ -0,0 +1,22 @@ +# cert-manager ClusterIssuer for Let's Encrypt TLS certificates. +# Uses HTTP-01 validation via Gateway API — cert-manager creates a temporary +# HTTPRoute to serve the ACME challenge on port 80. +# Prerequisites: cert-manager installed with --set config.enableGatewayAPI=true +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: mentor@trainwithshubham.com + privateKeySecretRef: + name: letsencrypt-account-key + solvers: + - http01: + gatewayHTTPRoute: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: bankapp-gateway + namespace: bankapp diff --git a/2026/day-78/AI-BankApp-DevOps/k8s/configmap.yml b/2026/day-78/AI-BankApp-DevOps/k8s/configmap.yml new file mode 100644 index 0000000000..28f7a16020 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/k8s/configmap.yml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: bankapp-config + namespace: bankapp +data: + MYSQL_HOST: mysql-service + MYSQL_PORT: "3306" + MYSQL_DATABASE: bankappdb + OLLAMA_URL: http://ollama-service:11434 + SERVER_FORWARD_HEADERS_STRATEGY: native diff --git a/2026/day-78/AI-BankApp-DevOps/k8s/gateway.yml b/2026/day-78/AI-BankApp-DevOps/k8s/gateway.yml new file mode 100644 index 0000000000..ed74cf25e7 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/k8s/gateway.yml @@ -0,0 +1,94 @@ +# Requires: Gateway API CRDs + Envoy Gateway installed on the cluster +# On EKS, Envoy Gateway creates an AWS NLB for external access automatically. +# Install steps documented in terraform/README.md +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: envoy-gateway +spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: bankapp-gateway + namespace: bankapp + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + gatewayClassName: envoy-gateway + listeners: + # HTTP — needed for cert-manager ACME HTTP-01 challenge validation. + # cert-manager creates a temporary HTTPRoute in its own namespace to serve + # the challenge, so allowedRoutes must permit all namespaces. + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + # HTTPS — TLS terminated at Envoy Gateway using cert-manager certificate + - name: https + protocol: HTTPS + port: 443 + hostname: 52.43.73.130.nip.io + tls: + mode: Terminate + certificateRefs: + - group: "" + kind: Secret + name: bankapp-tls + allowedRoutes: + namespaces: + from: Same +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: bankapp-route + namespace: bankapp +spec: + hostnames: + - 52.43.73.130.nip.io + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: bankapp-gateway + sectionName: https + - group: gateway.networking.k8s.io + kind: Gateway + name: bankapp-gateway + sectionName: http + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - group: "" + kind: Service + name: bankapp-service + port: 8080 + weight: 1 +--- +# Session persistence: Envoy Gateway bypasses K8s Service sessionAffinity and +# load-balances directly to pod endpoints. Without this policy, requests from +# the same browser can hit different pods, breaking in-memory sessions (CSRF +# tokens, login state). Cookie-based consistent hashing pins a browser to one pod. +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: bankapp-session + namespace: bankapp +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: bankapp-route + loadBalancer: + type: ConsistentHash + consistentHash: + type: Cookie + cookie: + name: BANKAPP_AFFINITY + ttl: 3600s diff --git a/2026/day-78/AI-BankApp-DevOps/k8s/hpa.yml b/2026/day-78/AI-BankApp-DevOps/k8s/hpa.yml new file mode 100644 index 0000000000..a9e0b0eb50 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/k8s/hpa.yml @@ -0,0 +1,37 @@ +# ─── Horizontal Pod Autoscaler — BankApp ───────────────────────── +# Requires metrics-server to be installed in the cluster. +# Sized for t3.medium (2 vCPU / 4 GiB): max 4 replicas keeps +# total workload requests under node capacity. +# ───────────────────────────────────────────────────────────────── +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: bankapp-hpa + namespace: bankapp +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: bankapp + minReplicas: 2 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + behavior: + scaleUp: + stabilizationWindowSeconds: 30 + policies: + - type: Pods + value: 2 + periodSeconds: 60 + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Pods + value: 1 + periodSeconds: 60 diff --git a/2026/day-78/AI-BankApp-DevOps/k8s/mysql-deployment.yml b/2026/day-78/AI-BankApp-DevOps/k8s/mysql-deployment.yml new file mode 100644 index 0000000000..f5de98f330 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/k8s/mysql-deployment.yml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql + namespace: bankapp +spec: + selector: + matchLabels: + app: mysql + strategy: + type: Recreate + template: + metadata: + labels: + app: mysql + spec: + containers: + - name: mysql + image: mysql:8.0 + ports: + - containerPort: 3306 + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: bankapp-secret + key: MYSQL_ROOT_PASSWORD + - name: MYSQL_DATABASE + valueFrom: + configMapKeyRef: + name: bankapp-config + key: MYSQL_DATABASE + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + volumeMounts: + - name: mysql-storage + mountPath: /var/lib/mysql + readinessProbe: + exec: + command: ["mysqladmin", "ping", "-h", "localhost"] + initialDelaySeconds: 15 + failureThreshold: 10 + livenessProbe: + exec: + command: ["mysqladmin", "ping", "-h", "localhost"] + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 5 + volumes: + - name: mysql-storage + persistentVolumeClaim: + claimName: mysql-pvc diff --git a/2026/day-78/AI-BankApp-DevOps/k8s/namespace.yml b/2026/day-78/AI-BankApp-DevOps/k8s/namespace.yml new file mode 100644 index 0000000000..0ea3fd2d9e --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/k8s/namespace.yml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: bankapp diff --git a/2026/day-78/AI-BankApp-DevOps/k8s/ollama-deployment.yml b/2026/day-78/AI-BankApp-DevOps/k8s/ollama-deployment.yml new file mode 100644 index 0000000000..b64e286cfe --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/k8s/ollama-deployment.yml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ollama + namespace: bankapp +spec: + selector: + matchLabels: + app: ollama + strategy: + type: Recreate + template: + metadata: + labels: + app: ollama + spec: + containers: + - name: ollama + image: ollama/ollama:latest + ports: + - containerPort: 11434 + resources: + requests: + memory: "2Gi" + cpu: "900m" + limits: + memory: "2.5Gi" + cpu: "1500m" + volumeMounts: + - name: ollama-storage + mountPath: /root/.ollama + lifecycle: + postStart: + exec: + command: + - /bin/sh + - -c + - | + until ollama list > /dev/null 2>&1; do sleep 2; done + ollama pull tinyllama + readinessProbe: + exec: + command: ["/bin/sh", "-c", "ollama list | grep -q tinyllama"] + initialDelaySeconds: 30 + failureThreshold: 30 + livenessProbe: + httpGet: + path: / + port: 11434 + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 5 + volumes: + - name: ollama-storage + persistentVolumeClaim: + claimName: ollama-pvc diff --git a/2026/day-78/AI-BankApp-DevOps/k8s/pv.yml b/2026/day-78/AI-BankApp-DevOps/k8s/pv.yml new file mode 100644 index 0000000000..095b06bf8c --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/k8s/pv.yml @@ -0,0 +1,16 @@ +# EKS uses dynamic provisioning via EBS CSI Driver. +# No manual PersistentVolumes needed — PVCs reference the StorageClass directly. +# This file defines the gp3 StorageClass for EKS. +# NOTE: Not set as default to avoid conflict with EKS built-in gp2 default. +# PVCs explicitly reference storageClassName: gp3 +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: gp3 +provisioner: ebs.csi.aws.com +parameters: + type: gp3 + fsType: ext4 +reclaimPolicy: Delete +volumeBindingMode: WaitForFirstConsumer +allowVolumeExpansion: true diff --git a/2026/day-78/AI-BankApp-DevOps/k8s/pvc.yml b/2026/day-78/AI-BankApp-DevOps/k8s/pvc.yml new file mode 100644 index 0000000000..fbcaa1e425 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/k8s/pvc.yml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-pvc + namespace: bankapp +spec: + storageClassName: gp3 + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ollama-pvc + namespace: bankapp +spec: + storageClassName: gp3 + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi diff --git a/2026/day-78/AI-BankApp-DevOps/k8s/secrets.yml b/2026/day-78/AI-BankApp-DevOps/k8s/secrets.yml new file mode 100644 index 0000000000..4edf117533 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/k8s/secrets.yml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: bankapp-secret + namespace: bankapp +type: Opaque +data: + MYSQL_ROOT_PASSWORD: VGVzdEAxMjM= # Test@123 + MYSQL_USER: cm9vdA== # root + MYSQL_PASSWORD: VGVzdEAxMjM= # Test@123 diff --git a/2026/day-78/AI-BankApp-DevOps/k8s/service.yml b/2026/day-78/AI-BankApp-DevOps/k8s/service.yml new file mode 100644 index 0000000000..36d04255c3 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/k8s/service.yml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: Service +metadata: + name: mysql-service + namespace: bankapp +spec: + selector: + app: mysql + ports: + - port: 3306 +--- +apiVersion: v1 +kind: Service +metadata: + name: ollama-service + namespace: bankapp +spec: + selector: + app: ollama + ports: + - port: 11434 +--- +apiVersion: v1 +kind: Service +metadata: + name: bankapp-service + namespace: bankapp +spec: + type: ClusterIP + sessionAffinity: ClientIP + sessionAffinityConfig: + clientIP: + timeoutSeconds: 3600 + selector: + app: bankapp + ports: + - port: 8080 + targetPort: 8080 diff --git a/2026/day-78/AI-BankApp-DevOps/mvnw b/2026/day-78/AI-BankApp-DevOps/mvnw new file mode 100644 index 0000000000..d7c358e5a2 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/2026/day-78/AI-BankApp-DevOps/mvnw.cmd b/2026/day-78/AI-BankApp-DevOps/mvnw.cmd new file mode 100644 index 0000000000..6f779cff20 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/2026/day-78/AI-BankApp-DevOps/mysql-values.yaml b/2026/day-78/AI-BankApp-DevOps/mysql-values.yaml new file mode 100644 index 0000000000..542e574bcd --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/mysql-values.yaml @@ -0,0 +1,20 @@ +auth: + rootPassword: Test@123 + database: bankappdb + +primary: + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 250m + memory: 256Mi + persistence: + size: 5Gi + storageClass: "" + +metrics: + enabled: true + serviceMonitor: + enabled: false diff --git a/2026/day-78/AI-BankApp-DevOps/pom.xml b/2026/day-78/AI-BankApp-DevOps/pom.xml new file mode 100644 index 0000000000..ebe89b78d8 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.13 + + + com.example + bankapp + 0.0.1-SNAPSHOT + bankapp + Banking Web Application + + + + + + + + + + + + + + + 21 + + 10.1.52 + + 2.18.6 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-validation + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity6 + + + io.micrometer + micrometer-registry-prometheus + + + + com.mysql + mysql-connector-j + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026/day-78/AI-BankApp-DevOps/screenshots/ai-chatbot.png b/2026/day-78/AI-BankApp-DevOps/screenshots/ai-chatbot.png new file mode 100644 index 0000000000..9a472bd6fe Binary files /dev/null and b/2026/day-78/AI-BankApp-DevOps/screenshots/ai-chatbot.png differ diff --git a/2026/day-78/AI-BankApp-DevOps/screenshots/dashboard.png b/2026/day-78/AI-BankApp-DevOps/screenshots/dashboard.png new file mode 100644 index 0000000000..b0c823a7ac Binary files /dev/null and b/2026/day-78/AI-BankApp-DevOps/screenshots/dashboard.png differ diff --git a/2026/day-78/AI-BankApp-DevOps/screenshots/gitops-pipeline.gif b/2026/day-78/AI-BankApp-DevOps/screenshots/gitops-pipeline.gif new file mode 100644 index 0000000000..ffc9cb4695 Binary files /dev/null and b/2026/day-78/AI-BankApp-DevOps/screenshots/gitops-pipeline.gif differ diff --git a/2026/day-78/AI-BankApp-DevOps/setup-k8s/README.md b/2026/day-78/AI-BankApp-DevOps/setup-k8s/README.md new file mode 100644 index 0000000000..a5f29a2b43 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/setup-k8s/README.md @@ -0,0 +1,154 @@ +# Kind Cluster Setup + +## Prerequisites + +Install these three tools if you don't have them already. + +**Docker** — Kind runs clusters inside Docker containers. + +```bash +# macOS +brew install --cask docker + +# Ubuntu/Debian +sudo apt-get update && sudo apt-get install -y docker.io +sudo usermod -aG docker $USER && newgrp docker +``` + +**kubectl** — CLI to talk to your cluster. + +```bash +# macOS +brew install kubectl + +# Linux +curl -LO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" +chmod +x kubectl && sudo mv kubectl /usr/local/bin/ +``` + +**Kind** — runs K8s clusters locally using Docker. + +```bash +# macOS +brew install kind + +# Linux +curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 +chmod +x kind && sudo mv kind /usr/local/bin/ +``` + +## 1. Build the BankApp image + +```bash +docker build -t trainwithshubham/bankapp:k8s . +``` + +## 2. Create the cluster + +```bash +kind create cluster --config setup-k8s/kind-config.yml +``` + +## 3. Load the image into Kind + +Kind can't pull from your local Docker daemon — you need to explicitly load images into the cluster. + +```bash +kind load docker-image trainwithshubham/bankapp:k8s --name tws-cluster +``` + +## 4. Install metrics-server + +HPA needs real CPU/memory numbers. Without this, `kubectl top` and autoscaling won't work. + +```bash +kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml +``` + +Kind uses self-signed certs, so metrics-server can't verify kubelet TLS — patch it: + +```bash +kubectl patch deployment metrics-server -n kube-system \ + --type='json' \ + -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]' +``` + +Wait for it to be ready: + +```bash +kubectl rollout status deployment/metrics-server -n kube-system --timeout=120s +``` + +## 5. Install Kubernetes Dashboard + +```bash +kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml +kubectl apply -f k8s/dashboard-admin.yml +``` + +Generate a login token: + +```bash +kubectl -n kubernetes-dashboard create token dashboard-admin +``` + +Start the proxy and open the dashboard: + +```bash +kubectl proxy +``` + +``` +http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ +``` + +Select **Token**, paste the token, and sign in. + +## 6. Apply manifests + +Order matters — namespace and config first, then storage, then workloads. + +```bash +kubectl apply -f k8s/namespace.yml +kubectl apply -f k8s/configmap.yml +kubectl apply -f k8s/secrets.yml +kubectl apply -f k8s/pv.yml +kubectl apply -f k8s/pvc.yml +kubectl apply -f k8s/mysql-deployment.yml +kubectl apply -f k8s/ollama-deployment.yml +kubectl apply -f k8s/bankapp-deployment.yml +kubectl apply -f k8s/service.yml +kubectl apply -f k8s/hpa.yml +``` + +## 7. Verify + +```bash +kubectl get all -n bankapp +kubectl get hpa -n bankapp # wait ~60s for metrics to populate +kubectl top pods -n bankapp +``` + +App will be at **http://localhost:8080** once pods are ready. + +## 8. Test HPA (autoscaling) + +Generate CPU load on bankapp to trigger scale-up: + +```bash +kubectl run load-test --image=busybox:1.36 -n bankapp -- sh -c "while true; do wget -q -O- http://bankapp-service:8080/actuator/health >/dev/null 2>&1; done" +``` + +Watch replicas scale from 2 → 4 (takes ~1-2 min for metrics to kick in): + +```bash +kubectl get hpa -n bankapp -w +``` + +Clean up when done: + +```bash +kubectl delete pod load-test -n bankapp +``` + +Scale-down happens automatically after 5 minutes of low CPU. diff --git a/2026/day-78/AI-BankApp-DevOps/setup-k8s/kind-config.yml b/2026/day-78/AI-BankApp-DevOps/setup-k8s/kind-config.yml new file mode 100644 index 0000000000..eeadbdd7cc --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/setup-k8s/kind-config.yml @@ -0,0 +1,17 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +name: tws-cluster + +nodes: +- role: control-plane + image: kindest/node:v1.35.0 + extraPortMappings: + - containerPort: 30080 + hostPort: 8080 + protocol: TCP + +- role: worker + image: kindest/node:v1.35.0 + +- role: worker + image: kindest/node:v1.35.0 \ No newline at end of file diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/BankappApplication.java b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/BankappApplication.java new file mode 100644 index 0000000000..8453398612 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/BankappApplication.java @@ -0,0 +1,13 @@ +package com.example.bankapp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BankappApplication { + + public static void main(String[] args) { + SpringApplication.run(BankappApplication.class, args); + } + +} diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/config/SecurityConfig.java b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/config/SecurityConfig.java new file mode 100644 index 0000000000..f7c0a8cf4b --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/config/SecurityConfig.java @@ -0,0 +1,40 @@ +package com.example.bankapp.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .requestMatchers("/register", "/login", "/css/**", "/js/**", "/actuator/**").permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .loginPage("/login") + .defaultSuccessUrl("/dashboard", true) + .permitAll() + ) + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/controller/BankController.java b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/controller/BankController.java new file mode 100644 index 0000000000..94ee89230b --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/controller/BankController.java @@ -0,0 +1,86 @@ +package com.example.bankapp.controller; + +import com.example.bankapp.model.Account; +import com.example.bankapp.service.AccountService; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import java.math.BigDecimal; + +@Controller +public class BankController { + + private final AccountService accountService; + + public BankController(AccountService accountService) { + this.accountService = accountService; + } + + @GetMapping("/login") + public String loginPage() { + return "login"; + } + + @GetMapping("/register") + public String registerPage() { + return "register"; + } + + @PostMapping("/register") + public String register(@RequestParam String username, + @RequestParam String password, + Model model) { + if (accountService.registerAccount(username, password)) { + return "redirect:/login?registered"; + } + model.addAttribute("error", true); + return "register"; + } + + @GetMapping("/dashboard") + public String dashboard(@AuthenticationPrincipal Account account, Model model) { + model.addAttribute("account", account); + return "dashboard"; + } + + @PostMapping("/deposit") + public String deposit(@AuthenticationPrincipal Account account, + @RequestParam BigDecimal amount, + RedirectAttributes redirectAttributes) { + accountService.deposit(account, amount); + return "redirect:/dashboard"; + } + + @PostMapping("/withdraw") + public String withdraw(@AuthenticationPrincipal Account account, + @RequestParam BigDecimal amount, + RedirectAttributes redirectAttributes) { + if (!accountService.withdraw(account, amount)) { + redirectAttributes.addFlashAttribute("error", "Insufficient funds."); + } + return "redirect:/dashboard"; + } + + @PostMapping("/transfer") + public String transfer(@AuthenticationPrincipal Account account, + @RequestParam String toUsername, + @RequestParam BigDecimal amount, + RedirectAttributes redirectAttributes) { + String error = accountService.transferAmount(account, toUsername, amount); + if (error != null) { + redirectAttributes.addFlashAttribute("error", error); + } + return "redirect:/dashboard"; + } + + @GetMapping("/transactions") + public String transactions(@AuthenticationPrincipal Account account, Model model) { + model.addAttribute("transactions", accountService.getTransactionHistory(account)); + return "transactions"; + } +} diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/controller/ChatController.java b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/controller/ChatController.java new file mode 100644 index 0000000000..df8ba2c43f --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/controller/ChatController.java @@ -0,0 +1,27 @@ +package com.example.bankapp.controller; + +import com.example.bankapp.model.Account; +import com.example.bankapp.service.ChatService; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api") +public class ChatController { + + private final ChatService chatService; + + public ChatController(ChatService chatService) { + this.chatService = chatService; + } + + @PostMapping("/chat") + public Map chat(@AuthenticationPrincipal Account account, + @RequestBody Map request) { + String message = request.getOrDefault("message", ""); + String reply = chatService.chat(account, message); + return Map.of("reply", reply); + } +} diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/model/Account.java b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/model/Account.java new file mode 100644 index 0000000000..a85b5c5c42 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/model/Account.java @@ -0,0 +1,112 @@ +package com.example.bankapp.model; + +import jakarta.persistence.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Entity +@Table(name = "accounts") +public class Account implements UserDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String username; + + @Column(nullable = false) + private String password; + + @Column(nullable = false, precision = 19, scale = 2) + private BigDecimal balance = BigDecimal.ZERO; + + @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List transactions = new ArrayList<>(); + + public Account() { + } + + public Account(String username, String password) { + this.username = username; + this.password = password; + this.balance = BigDecimal.ZERO; + } + + // Getters and setters + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public BigDecimal getBalance() { + return balance; + } + + public void setBalance(BigDecimal balance) { + this.balance = balance; + } + + public List getTransactions() { + return transactions; + } + + public void setTransactions(List transactions) { + this.transactions = transactions; + } + + // UserDetails implementation + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/model/Transaction.java b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/model/Transaction.java new file mode 100644 index 0000000000..799de0602e --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/model/Transaction.java @@ -0,0 +1,78 @@ +package com.example.bankapp.model; + +import jakarta.persistence.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Table(name = "transactions") +public class Transaction { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, precision = 19, scale = 2) + private BigDecimal amount; + + @Column(nullable = false) + private String type; + + @Column(nullable = false) + private LocalDateTime timestamp; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id") + private Account account; + + public Transaction() { + } + + public Transaction(BigDecimal amount, String type, LocalDateTime timestamp, Account account) { + this.amount = amount; + this.type = type; + this.timestamp = timestamp; + this.account = account; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } +} diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/repository/AccountRepository.java b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/repository/AccountRepository.java new file mode 100644 index 0000000000..72553370e9 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/repository/AccountRepository.java @@ -0,0 +1,10 @@ +package com.example.bankapp.repository; + +import com.example.bankapp.model.Account; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface AccountRepository extends JpaRepository { + Optional findByUsername(String username); +} diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/repository/TransactionRepository.java b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/repository/TransactionRepository.java new file mode 100644 index 0000000000..8940815105 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/repository/TransactionRepository.java @@ -0,0 +1,10 @@ +package com.example.bankapp.repository; + +import com.example.bankapp.model.Transaction; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TransactionRepository extends JpaRepository { + List findByAccountIdOrderByTimestampDesc(Long accountId); +} diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/service/AccountService.java b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/service/AccountService.java new file mode 100644 index 0000000000..5fbffbf965 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/service/AccountService.java @@ -0,0 +1,98 @@ +package com.example.bankapp.service; + +import com.example.bankapp.model.Account; +import com.example.bankapp.model.Transaction; +import com.example.bankapp.repository.AccountRepository; +import com.example.bankapp.repository.TransactionRepository; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class AccountService implements UserDetailsService { + + private final AccountRepository accountRepository; + private final TransactionRepository transactionRepository; + private final PasswordEncoder passwordEncoder; + + public AccountService(AccountRepository accountRepository, + TransactionRepository transactionRepository, + PasswordEncoder passwordEncoder) { + this.accountRepository = accountRepository; + this.transactionRepository = transactionRepository; + this.passwordEncoder = passwordEncoder; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return accountRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); + } + + public boolean registerAccount(String username, String password) { + if (accountRepository.findByUsername(username).isPresent()) { + return false; + } + Account account = new Account(username, passwordEncoder.encode(password)); + accountRepository.save(account); + return true; + } + + @Transactional + public void deposit(Account account, BigDecimal amount) { + account.setBalance(account.getBalance().add(amount)); + accountRepository.save(account); + + Transaction transaction = new Transaction(amount, "Deposit", LocalDateTime.now(), account); + transactionRepository.save(transaction); + } + + @Transactional + public boolean withdraw(Account account, BigDecimal amount) { + if (account.getBalance().compareTo(amount) < 0) { + return false; + } + account.setBalance(account.getBalance().subtract(amount)); + accountRepository.save(account); + + Transaction transaction = new Transaction(amount, "Withdrawal", LocalDateTime.now(), account); + transactionRepository.save(transaction); + return true; + } + + @Transactional + public String transferAmount(Account from, String toUsername, BigDecimal amount) { + if (from.getUsername().equals(toUsername)) { + return "Cannot transfer to yourself."; + } + if (from.getBalance().compareTo(amount) < 0) { + return "Insufficient funds."; + } + Account to = accountRepository.findByUsername(toUsername).orElse(null); + if (to == null) { + return "Recipient not found."; + } + + from.setBalance(from.getBalance().subtract(amount)); + to.setBalance(to.getBalance().add(amount)); + accountRepository.save(from); + accountRepository.save(to); + + LocalDateTime now = LocalDateTime.now(); + transactionRepository.save(new Transaction(amount, "Transfer Out", now, from)); + transactionRepository.save(new Transaction(amount, "Transfer In", now, to)); + + return null; // null means success + } + + public List getTransactionHistory(Account account) { + return transactionRepository.findByAccountIdOrderByTimestampDesc(account.getId()); + } +} diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/service/ChatService.java b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/service/ChatService.java new file mode 100644 index 0000000000..143b00a1d2 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/java/com/example/bankapp/service/ChatService.java @@ -0,0 +1,79 @@ +package com.example.bankapp.service; + +import com.example.bankapp.model.Account; +import com.example.bankapp.model.Transaction; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.Map; + +@Service +public class ChatService { + + @Value("${ollama.url}") + private String ollamaUrl; + + @Value("${ollama.model}") + private String model; + + private final RestTemplate restTemplate = new RestTemplate(); + private final AccountService accountService; + + public ChatService(AccountService accountService) { + this.accountService = accountService; + } + + public String chat(Account account, String userMessage) { + List recent = accountService.getTransactionHistory(account); + String context = buildContext(account, recent); + + Map request = Map.of( + "model", model, + "messages", List.of( + Map.of("role", "system", "content", context), + Map.of("role", "user", "content", userMessage) + ), + "stream", false + ); + + try { + Map response = restTemplate.postForObject( + ollamaUrl + "/api/chat", request, Map.class + ); + if (response != null && response.containsKey("message")) { + Map message = (Map) response.get("message"); + return message.get("content"); + } + return "Sorry, I couldn't process that."; + } catch (Exception e) { + return "AI assistant is unavailable. Please make sure Ollama is running."; + } + } + + private String buildContext(Account account, List transactions) { + StringBuilder sb = new StringBuilder(); + sb.append("You are a helpful banking assistant for BankApp. "); + sb.append("Keep answers short and friendly (2-3 sentences max). "); + sb.append("\n\nCustomer details:"); + sb.append("\n- Username: ").append(account.getUsername()); + sb.append("\n- Balance: $").append(account.getBalance()); + sb.append("\n- Account ID: ").append(account.getId()); + + if (!transactions.isEmpty()) { + sb.append("\n\nRecent transactions:"); + int limit = Math.min(transactions.size(), 5); + for (int i = 0; i < limit; i++) { + Transaction t = transactions.get(i); + sb.append("\n- ").append(t.getType()) + .append(": $").append(t.getAmount()) + .append(" on ").append(t.getTimestamp().toLocalDate()); + } + } else { + sb.append("\n\nNo transactions yet."); + } + + return sb.toString(); + } +} diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/resources/application.properties b/2026/day-78/AI-BankApp-DevOps/src/main/resources/application.properties new file mode 100644 index 0000000000..10318ff9e7 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/resources/application.properties @@ -0,0 +1,26 @@ +spring.application.name=bankapp + +# MySQL Database configuration +spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:bankappdb}?useSSL=false&allowPublicKeyRetrieval=true +spring.datasource.username=${MYSQL_USER:root} +spring.datasource.password=${MYSQL_PASSWORD:Test@123} +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# JPA & Hibernate configuration +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect + +# Virtual threads (Java 21) +spring.threads.virtual.enabled=true + +# Actuator - expose health + prometheus for kube-prometheus-stack scraping +management.endpoints.web.exposure.include=health,prometheus +management.endpoint.health.show-details=when-authorized + +# Ollama AI +ollama.url=${OLLAMA_URL:http://localhost:11434} +ollama.model=tinyllama + +# Structured logging +logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/resources/static/css/bankapp.css b/2026/day-78/AI-BankApp-DevOps/src/main/resources/static/css/bankapp.css new file mode 100644 index 0000000000..fd4886e8af --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/resources/static/css/bankapp.css @@ -0,0 +1,639 @@ +/* ===== CSS Custom Properties ===== */ +:root { + --bg-primary: #0a0a0a; + --bg-surface: rgba(255, 255, 255, 0.03); + --text-primary: #f1f5f9; + --text-muted: #94a3b8; + --accent: #3b82f6; + --accent-hover: #2563eb; + --accent-glow: rgba(59, 130, 246, 0.4); + --success: #22c55e; + --danger: #ef4444; + --border-color: rgba(255, 255, 255, 0.08); + --border-radius: 16px; + --border-radius-sm: 10px; + --transition-speed: 0.3s; + --glass-bg: rgba(255, 255, 255, 0.05); + --glass-border: rgba(255, 255, 255, 0.1); + --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + --gradient-bg: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%); + --font-stack: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +[data-theme="light"] { + --bg-primary: #f8fafc; + --bg-surface: rgba(0, 0, 0, 0.02); + --text-primary: #0f172a; + --text-muted: #64748b; + --border-color: rgba(0, 0, 0, 0.08); + --glass-bg: rgba(255, 255, 255, 0.7); + --glass-border: rgba(0, 0, 0, 0.08); + --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + --gradient-bg: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #dbeafe 100%); +} + +/* ===== Reset & Base ===== */ +*, *::before, *::after { + box-sizing: border-box; +} + +body { + font-family: var(--font-stack); + background: var(--gradient-bg); + color: var(--text-primary); + margin: 0; + padding: 0; + min-height: 100vh; + transition: background var(--transition-speed), color var(--transition-speed); +} + +a { + color: var(--accent); + text-decoration: none; + transition: color var(--transition-speed); +} + +a:hover { + color: var(--accent-hover); +} + +/* ===== Navbar ===== */ +.navbar-app { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid var(--glass-border); + padding: 0.75rem 1.5rem; + position: sticky; + top: 0; + z-index: 1000; +} + +.navbar-app .navbar-brand { + font-weight: 700; + font-size: 1.25rem; + color: var(--text-primary) !important; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.navbar-app .nav-link { + color: var(--text-muted) !important; + font-weight: 500; + padding: 0.5rem 1rem !important; + border-radius: var(--border-radius-sm); + transition: all var(--transition-speed); +} + +.navbar-app .nav-link:hover, +.navbar-app .nav-link.active { + color: var(--text-primary) !important; + background: var(--bg-surface); +} + +.theme-toggle { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + color: var(--text-primary); + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all var(--transition-speed); + font-size: 1.1rem; +} + +.theme-toggle:hover { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +/* ===== Glass Card ===== */ +.glass-card { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + border-radius: var(--border-radius); + box-shadow: var(--glass-shadow); + padding: 2rem; + transition: transform var(--transition-speed), box-shadow var(--transition-speed); +} + +.glass-card:hover { + transform: translateY(-2px); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); +} + +[data-theme="light"] .glass-card:hover { + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); +} + +/* ===== Auth Pages (Login/Register) ===== */ +.auth-wrapper { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.auth-container { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1rem; +} + +.auth-card { + width: 100%; + max-width: 440px; + animation: fadeInUp 0.5s ease-out; +} + +.auth-card h2 { + font-weight: 700; + margin-bottom: 0.5rem; +} + +.auth-card .text-muted { + color: var(--text-muted) !important; +} + +/* ===== Form Styles ===== */ +.form-floating > .form-control { + background: var(--bg-surface); + border: 1px solid var(--border-color); + color: var(--text-primary); + border-radius: var(--border-radius-sm); + transition: border-color var(--transition-speed), box-shadow var(--transition-speed); +} + +.form-floating > .form-control:focus { + background: var(--bg-surface); + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); + color: var(--text-primary); +} + +.form-floating > label { + color: var(--text-muted); +} + +.form-floating > .form-control:focus ~ label, +.form-floating > .form-control:not(:placeholder-shown) ~ label { + color: var(--accent); +} + +/* ===== Buttons ===== */ +.btn-accent { + background: var(--accent); + color: #fff; + border: none; + border-radius: var(--border-radius-sm); + padding: 0.75rem 1.5rem; + font-weight: 600; + transition: all var(--transition-speed); +} + +.btn-accent:hover { + background: var(--accent-hover); + color: #fff; + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--accent-glow); +} + +.btn-glass { + background: var(--glass-bg); + color: var(--text-primary); + border: 1px solid var(--glass-border); + border-radius: var(--border-radius-sm); + padding: 0.75rem 1.5rem; + font-weight: 500; + transition: all var(--transition-speed); +} + +.btn-glass:hover { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +/* ===== Password Toggle ===== */ +.input-group-password { + position: relative; +} + +.input-group-password .form-control { + padding-right: 3rem; +} + +.password-toggle { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + z-index: 5; + padding: 0.25rem; +} + +.password-toggle:hover { + color: var(--accent); +} + +/* ===== Dashboard ===== */ +.dashboard-wrapper { + padding: 2rem 1rem; + max-width: 1200px; + margin: 0 auto; +} + +.balance-card { + text-align: center; + padding: 2.5rem 2rem; + margin-bottom: 2rem; +} + +.balance-label { + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-muted); + margin-bottom: 0.5rem; +} + +.balance-amount { + font-size: 3rem; + font-weight: 800; + color: var(--text-primary); + line-height: 1; +} + +.balance-amount .currency { + color: var(--accent); +} + +.account-info { + display: flex; + justify-content: center; + gap: 2rem; + margin-top: 1rem; + color: var(--text-muted); + font-size: 0.875rem; +} + +.action-cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.action-card { + padding: 1.5rem; +} + +.action-card h5 { + font-weight: 600; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.action-card .form-control { + background: var(--bg-surface); + border: 1px solid var(--border-color); + color: var(--text-primary); + border-radius: var(--border-radius-sm); + padding: 0.75rem 1rem; +} + +.action-card .form-control:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); +} + +.action-card .form-label { + color: var(--text-muted); + font-size: 0.875rem; + font-weight: 500; +} + +/* ===== Transactions Table ===== */ +.transactions-wrapper { + padding: 2rem 1rem; + max-width: 900px; + margin: 0 auto; +} + +.table-glass { + width: 100%; + border-collapse: separate; + border-spacing: 0; +} + +.table-glass thead th { + background: var(--bg-surface); + color: var(--text-muted); + font-weight: 600; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.table-glass tbody td { + padding: 1rem; + border-bottom: 1px solid var(--border-color); + color: var(--text-primary); + vertical-align: middle; +} + +.table-glass tbody tr { + transition: background var(--transition-speed); +} + +.table-glass tbody tr:hover { + background: var(--bg-surface); +} + +.amount-positive { + color: var(--success); + font-weight: 600; +} + +.amount-negative { + color: var(--danger); + font-weight: 600; +} + +.badge-type { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; +} + +.badge-deposit { + background: rgba(34, 197, 94, 0.15); + color: var(--success); +} + +.badge-withdrawal { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); +} + +.badge-transfer-out { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); +} + +.badge-transfer-in { + background: rgba(34, 197, 94, 0.15); + color: var(--success); +} + +.empty-state { + text-align: center; + padding: 4rem 2rem; + color: var(--text-muted); +} + +.empty-state i { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.5; +} + +/* ===== Alert Styles ===== */ +.alert-glass { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + color: var(--danger); + border-radius: var(--border-radius-sm); + padding: 0.75rem 1rem; +} + +.alert-glass-success { + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.3); + color: var(--success); +} + +/* ===== Footer ===== */ +.footer-app { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-top: 1px solid var(--glass-border); + padding: 1.25rem; + text-align: center; + color: var(--text-muted); + font-size: 0.85rem; + margin-top: auto; +} + +.footer-app a { + color: var(--accent); + font-weight: 500; +} + +/* ===== Animations ===== */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.fade-in { + animation: fadeInUp 0.5s ease-out; +} + +.fade-in-delay-1 { animation-delay: 0.1s; animation-fill-mode: both; } +.fade-in-delay-2 { animation-delay: 0.2s; animation-fill-mode: both; } +.fade-in-delay-3 { animation-delay: 0.3s; animation-fill-mode: both; } + +/* ===== Chat Widget ===== */ +.chat-fab { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + width: 56px; + height: 56px; + border-radius: 50%; + background: var(--accent); + color: #fff; + border: none; + font-size: 1.5rem; + cursor: pointer; + box-shadow: 0 4px 16px var(--accent-glow); + transition: all var(--transition-speed); + z-index: 1100; + display: flex; + align-items: center; + justify-content: center; +} + +.chat-fab:hover { + background: var(--accent-hover); + transform: scale(1.1); +} + +.chat-panel { + position: fixed; + bottom: 5.5rem; + right: 1.5rem; + width: 360px; + max-height: 480px; + border-radius: var(--border-radius); + background: var(--glass-bg); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow); + display: none; + flex-direction: column; + z-index: 1100; + animation: fadeInUp 0.3s ease-out; +} + +.chat-panel.open { + display: flex; +} + +.chat-header { + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--border-color); + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + min-height: 200px; + max-height: 320px; +} + +.chat-msg { + padding: 0.625rem 0.875rem; + border-radius: 12px; + font-size: 0.875rem; + line-height: 1.4; + max-width: 85%; + word-wrap: break-word; +} + +.chat-msg.user { + background: var(--accent); + color: #fff; + align-self: flex-end; + border-bottom-right-radius: 4px; +} + +.chat-msg.bot { + background: var(--bg-surface); + border: 1px solid var(--border-color); + align-self: flex-start; + border-bottom-left-radius: 4px; +} + +.chat-msg.bot.loading { + color: var(--text-muted); + font-style: italic; +} + +.chat-input-area { + padding: 0.75rem; + border-top: 1px solid var(--border-color); + display: flex; + gap: 0.5rem; +} + +.chat-input-area input { + flex: 1; + background: var(--bg-surface); + border: 1px solid var(--border-color); + color: var(--text-primary); + border-radius: var(--border-radius-sm); + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + outline: none; +} + +.chat-input-area input:focus { + border-color: var(--accent); +} + +.chat-input-area button { + background: var(--accent); + color: #fff; + border: none; + border-radius: var(--border-radius-sm); + padding: 0.5rem 0.75rem; + cursor: pointer; + transition: background var(--transition-speed); +} + +.chat-input-area button:hover { + background: var(--accent-hover); +} + +/* ===== Responsive ===== */ +@media (max-width: 768px) { + .balance-amount { + font-size: 2.25rem; + } + + .account-info { + flex-direction: column; + gap: 0.5rem; + } + + .action-cards { + grid-template-columns: 1fr; + } + + .glass-card { + padding: 1.5rem; + } + + .table-glass thead th, + .table-glass tbody td { + padding: 0.75rem 0.5rem; + font-size: 0.875rem; + } +} + +@media (max-width: 480px) { + .balance-amount { + font-size: 1.75rem; + } + + .auth-card { + padding: 1.5rem; + } +} diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/resources/static/js/theme.js b/2026/day-78/AI-BankApp-DevOps/src/main/resources/static/js/theme.js new file mode 100644 index 0000000000..e27c1f749e --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/resources/static/js/theme.js @@ -0,0 +1,33 @@ +(function () { + const STORAGE_KEY = 'bankapp-theme'; + + function getPreferred() { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) return stored; + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + + function apply(theme) { + document.documentElement.setAttribute('data-theme', theme); + localStorage.setItem(STORAGE_KEY, theme); + const icon = document.getElementById('themeIcon'); + if (icon) { + icon.className = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill'; + } + } + + // Apply immediately to prevent flash + apply(getPreferred()); + + document.addEventListener('DOMContentLoaded', function () { + const btn = document.getElementById('themeToggle'); + if (btn) { + btn.addEventListener('click', function () { + const current = document.documentElement.getAttribute('data-theme') || 'dark'; + apply(current === 'dark' ? 'light' : 'dark'); + }); + } + // Re-apply to update icon after DOM is ready + apply(getPreferred()); + }); +})(); diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/resources/static/mysql/SQLScript.txt b/2026/day-78/AI-BankApp-DevOps/src/main/resources/static/mysql/SQLScript.txt new file mode 100644 index 0000000000..69a8e5c7e8 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/resources/static/mysql/SQLScript.txt @@ -0,0 +1 @@ +CREATE DATABASE bankappdb; \ No newline at end of file diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/dashboard.html b/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/dashboard.html new file mode 100644 index 0000000000..9ee685e66d --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/dashboard.html @@ -0,0 +1,162 @@ + + + + +
+ + +
+ +
+
Total Balance
+
+ $0.00 +
+ +
+ + +
+ +
+ + +
+ +
+
Deposit
+
+
+ + +
+ +
+
+ + +
+
Withdraw
+
+
+ + +
+ +
+
+ + +
+
Transfer
+
+
+ + +
+
+ + +
+ +
+
+
+
+ +
+
+ + + + +
+
+ AI Assistant +
+
+
Hi! I'm your banking assistant. Ask me about your balance, transactions, or anything else.
+
+
+ + +
+
+ + + + + diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/fragments/layout.html b/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/fragments/layout.html new file mode 100644 index 0000000000..3cafa18c59 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/fragments/layout.html @@ -0,0 +1,66 @@ + + + + + + + + + + BankApp + + + + + + + + + + + + + +
+

© 2026 BankApp. Built with Spring Boot.

+
+ + diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/login.html b/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/login.html new file mode 100644 index 0000000000..362983c1c5 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/login.html @@ -0,0 +1,72 @@ + + + + +
+ + +
+
+
+

Welcome back Josh Batch 10

+

Sign in to your account

+
+ +
+ Invalid username or password. +
+ +
+ You have been logged out. +
+ +
+ Registration successful. Please sign in. +
+ +
+
+ + +
+ +
+ + + +
+ + +
+ +

+ Don't have an account? Create one +

+
+
+ +
+
+ + + + + diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/register.html b/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/register.html new file mode 100644 index 0000000000..a54ca3a3ad --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/register.html @@ -0,0 +1,95 @@ + + + + +
+ + +
+
+
+

Create Account

+

Join BankApp today

+
+ +
+ Username already taken. Please choose another. +
+ +
+
+ + +
+ +
+ + + +
+ +
+
+
+
+ +
+ + +
+ +

+ Already have an account? Sign in +

+
+
+ +
+
+ + + + + diff --git a/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/transactions.html b/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/transactions.html new file mode 100644 index 0000000000..e0e48bbe62 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/main/resources/templates/transactions.html @@ -0,0 +1,64 @@ + + + + +
+ + +
+
+

Transaction History

+ + Dashboard + +
+ +
+ +
+
+ + + + + + + + + + + + + + + + + +
IDTypeAmountDate
+ + +
+
+
+ + +
+ +
No transactions yet
+

Your transaction history will appear here once you make your first deposit.

+ + Make a Deposit + +
+
+
+ +
+
+ + + + diff --git a/2026/day-78/AI-BankApp-DevOps/src/test/java/com/example/bankapp/BankappApplicationTests.java b/2026/day-78/AI-BankApp-DevOps/src/test/java/com/example/bankapp/BankappApplicationTests.java new file mode 100644 index 0000000000..63c64e9d7c --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/src/test/java/com/example/bankapp/BankappApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.bankapp; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BankappApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/2026/day-78/AI-BankApp-DevOps/terraform/.terraform.lock.hcl b/2026/day-78/AI-BankApp-DevOps/terraform/.terraform.lock.hcl new file mode 100644 index 0000000000..fff0fb1ed0 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/terraform/.terraform.lock.hcl @@ -0,0 +1,125 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.40.0" + constraints = ">= 4.0.0, >= 6.0.0, ~> 6.0, >= 6.28.0" + hashes = [ + "h1:Li1WiMXyFcGfuph9iGIdTScJFNjhkSR/MkuvpapGYYg=", + "zh:05171de71e4236b41740c6a4d4145cc38399b344535e7768e5380d0fdcb95609", + "zh:17910a059ac677c21b61ed5da94cffba40f7cb13ddd78c818458d122cf3ef9a9", + "zh:2f072f335d3f422bbe0f3fd46eba70a52caf43e09ee97599c26f713dbac48fed", + "zh:316da833674982910718aae754f4db0fcc89f0466ca2ca66219fe7fa435950be", + "zh:3fcdde0076bebfaeb0c61960b3e03cf3e2c5a978f7a4ebb0aa42e6b36209044a", + "zh:48cecf9748a3354ccd08275da8b34a42b3367247fc2de398e03ae4e8463299aa", + "zh:4ae0f7e76c61a4820405bd3f340b156f42f7f4480715865b008636c61fbbfa30", + "zh:5add497a7ff7063425c65460c6bc40701cf0e59e09b379e7f535fa37c8ebd80a", + "zh:7a26b4507c99382cefba2b189d4c5ebed939de8fb663a193b65d37934603804d", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:bad959d58660f6a0b0f950b038c76f378cfe045ba8560a0c3bbf8886f62d81be", + "zh:cf7af6468288a36b02d009045dd4a67c24ad99bbcad406bab0f9c12776d5d99c", + "zh:cfb683320d6d8bc1a5640538af62a5f9da285e28d0daca9e91d667d6e25585d9", + "zh:de7d26ac15362942f590175e425160a2fbf0fe0960afec40b4af78076d4fb9fb", + "zh:fe2fd136b68ea1941da60d8991d3857dd721b180d49502121f3ea8688a812704", + ] +} + +provider "registry.terraform.io/hashicorp/cloudinit" { + version = "2.3.7" + constraints = ">= 2.0.0" + hashes = [ + "h1:M9TpQxKAE/hyOwytdX9MUNZw30HoD/OXqYIug5fkqH8=", + "zh:06f1c54e919425c3139f8aeb8fcf9bceca7e560d48c9f0c1e3bb0a8ad9d9da1e", + "zh:0e1e4cf6fd98b019e764c28586a386dc136129fef50af8c7165a067e7e4a31d5", + "zh:1871f4337c7c57287d4d67396f633d224b8938708b772abfc664d1f80bd67edd", + "zh:2b9269d91b742a71b2248439d5e9824f0447e6d261bfb86a8a88528609b136d1", + "zh:3d8ae039af21426072c66d6a59a467d51f2d9189b8198616888c1b7fc42addc7", + "zh:3ef4e2db5bcf3e2d915921adced43929214e0946a6fb11793085d9a48995ae01", + "zh:42ae54381147437c83cbb8790cc68935d71b6357728a154109d3220b1beb4dc9", + "zh:4496b362605ae4cbc9ef7995d102351e2fe311897586ffc7a4a262ccca0c782a", + "zh:652a2401257a12706d32842f66dac05a735693abcb3e6517d6b5e2573729ba13", + "zh:7406c30806f5979eaed5f50c548eced2ea18ea121e01801d2f0d4d87a04f6a14", + "zh:7848429fd5a5bcf35f6fee8487df0fb64b09ec071330f3ff240c0343fe2a5224", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + ] +} + +provider "registry.terraform.io/hashicorp/helm" { + version = "2.17.0" + constraints = "~> 2.17" + hashes = [ + "h1:kQMkcPVvHOguOqnxoEU2sm1ND9vCHiT8TvZ2x6v/Rsw=", + "zh:06fb4e9932f0afc1904d2279e6e99353c2ddac0d765305ce90519af410706bd4", + "zh:104eccfc781fc868da3c7fec4385ad14ed183eb985c96331a1a937ac79c2d1a7", + "zh:129345c82359837bb3f0070ce4891ec232697052f7d5ccf61d43d818912cf5f3", + "zh:3956187ec239f4045975b35e8c30741f701aa494c386aaa04ebabffe7749f81c", + "zh:66a9686d92a6b3ec43de3ca3fde60ef3d89fb76259ed3313ca4eb9bb8c13b7dd", + "zh:88644260090aa621e7e8083585c468c8dd5e09a3c01a432fb05da5c4623af940", + "zh:a248f650d174a883b32c5b94f9e725f4057e623b00f171936dcdcc840fad0b3e", + "zh:aa498c1f1ab93be5c8fbf6d48af51dc6ef0f10b2ea88d67bcb9f02d1d80d3930", + "zh:bf01e0f2ec2468c53596e027d376532a2d30feb72b0b5b810334d043109ae32f", + "zh:c46fa84cc8388e5ca87eb575a534ebcf68819c5a5724142998b487cb11246654", + "zh:d0c0f15ffc115c0965cbfe5c81f18c2e114113e7a1e6829f6bfd879ce5744fbb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.4" + constraints = ">= 3.0.0" + hashes = [ + "h1:L5V05xwp/Gto1leRryuesxjMfgZwjb7oool4WS1UEFQ=", + "zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43", + "zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a", + "zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991", + "zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f", + "zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e", + "zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615", + "zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442", + "zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5", + "zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f", + "zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.13.1" + constraints = ">= 0.9.0" + hashes = [ + "h1:ZT5ppCNIModqk3iOkVt5my8b8yBHmDpl663JtXAIRqM=", + "zh:02cb9aab1002f0f2a94a4f85acec8893297dc75915f7404c165983f720a54b74", + "zh:04429b2b31a492d19e5ecf999b116d396dac0b24bba0d0fb19ecaefe193fdb8f", + "zh:26f8e51bb7c275c404ba6028c1b530312066009194db721a8427a7bc5cdbc83a", + "zh:772ff8dbdbef968651ab3ae76d04afd355c32f8a868d03244db3f8496e462690", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:898db5d2b6bd6ca5457dccb52eedbc7c5b1a71e4a4658381bcbb38cedbbda328", + "zh:8de913bf09a3fa7bedc29fec18c47c571d0c7a3d0644322c46f3aa648cf30cd8", + "zh:9402102c86a87bdfe7e501ffbb9c685c32bbcefcfcf897fd7d53df414c36877b", + "zh:b18b9bb1726bb8cfbefc0a29cf3657c82578001f514bcf4c079839b6776c47f0", + "zh:b9d31fdc4faecb909d7c5ce41d2479dd0536862a963df434be4b16e8e4edc94d", + "zh:c951e9f39cca3446c060bd63933ebb89cedde9523904813973fbc3d11863ba75", + "zh:e5b773c0d07e962291be0e9b413c7a22c044b8c7b58c76e8aa91d1659990dfb5", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.2.1" + constraints = ">= 4.0.0" + hashes = [ + "h1:akFNuHwvrtnYMBofieoeXhPJDhYZzJVu/Q/BgZK2fgg=", + "zh:0d1e7d07ac973b97fa228f46596c800de830820506ee145626f079dd6bbf8d8a", + "zh:5c7e3d4348cb4861ab812973ef493814a4b224bdd3e9d534a7c8a7c992382b86", + "zh:7c6d4a86cd7a4e9c1025c6b3a3a6a45dea202af85d870cddbab455fb1bd568ad", + "zh:7d0864755ba093664c4b2c07c045d3f5e3d7c799dda1a3ef33d17ed1ac563191", + "zh:83734f57950ab67c0d6a87babdb3f13c908cbe0a48949333f489698532e1391b", + "zh:951e3c285218ebca0cf20eaa4265020b4ef042fea9c6ade115ad1558cfe459e5", + "zh:b9543955b4297e1d93b85900854891c0e645d936d8285a190030475379c5c635", + "zh:bb1bd9e86c003d08c30c1b00d44118ed5bbbf6b1d2d6f7eaac4fa5c6ebea5933", + "zh:c9477bfe00653629cd77ddac3968475f7ad93ac3ca8bc45b56d1d9efb25e4a6e", + "zh:d4cfda8687f736d0cba664c22ec49dae1188289e214ef57f5afe6a7217854fed", + "zh:dc77ee066cf96532a48f0578c35b1eaf6dc4d8ddd0e3ae8e029a3b10676dd5d3", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/2026/day-78/AI-BankApp-DevOps/terraform/README.md b/2026/day-78/AI-BankApp-DevOps/terraform/README.md new file mode 100644 index 0000000000..8913721447 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/terraform/README.md @@ -0,0 +1,340 @@ +# AI BankApp - End-to-End GitOps on EKS + +Complete guide to deploy AI BankApp on AWS EKS using Terraform, ArgoCD, Gateway API, and Prometheus monitoring. + +## Architecture + +``` +Developer Push → GitHub Actions CI → DockerHub → ArgoCD → EKS Cluster + ↑ + watches k8s/ manifests +``` + +``` + ┌─────────────────────────────────────────────────┐ + │ VPC (10.0.0.0/16) │ + │ │ + │ ┌──────────────┐ ┌──────────────┐ ┌────────┐ │ + │ │ Public Sub │ │ Public Sub │ │Pub Sub │ │ + │ │ 10.0.1.0/24 │ │ 10.0.2.0/24 │ │.3.0/24 │ │ + │ │ (us-west-2a) │ │ (us-west-2b) │ │ (2c) │ │ + │ └──────┬───────┘ └──────┬───────┘ └───┬────┘ │ + │ │ NAT GW │ │ + │ ┌──────┴───────┐ ┌──────────────┐ ┌───┴────┐ │ + │ │ Private Sub │ │ Private Sub │ │Priv Sub│ │ + │ │ 10.0.4.0/24 │ │ 10.0.5.0/24 │ │.6.0/24 │ │ + │ │ (Worker Nodes)│ │ (Worker Nodes)│ │(Nodes) │ │ + │ └──────────────┘ └──────────────┘ └────────┘ │ + │ ┌──────────────┐ ┌──────────────┐ ┌────────┐ │ + │ │ Intra Sub │ │ Intra Sub │ │Intra │ │ + │ │ 10.0.7.0/24 │ │ 10.0.8.0/24 │ │.9.0/24│ │ + │ │ (Ctrl Plane) │ │ (Ctrl Plane) │ │(Ctrl) │ │ + │ └──────────────┘ └──────────────┘ └────────┘ │ + └─────────────────────────────────────────────────┘ +``` + +| Component | Tool | +|-----------|------| +| Infrastructure | Terraform (VPC + EKS + ArgoCD) | +| CI Pipeline | GitHub Actions → DockerHub | +| GitOps / CD | ArgoCD (auto-sync from `k8s/` folder) | +| Ingress | Gateway API + Envoy Gateway (AWS NLB) | +| Monitoring | kube-prometheus-stack (Prometheus + Grafana) | +| AI Chatbot | Ollama (tinyllama) on EKS | +| Storage | EBS CSI Driver (gp3 dynamic provisioning) | + +## What Gets Created + +| Resource | Details | +|---|---| +| **VPC** | `10.0.0.0/16` across 3 AZs with public, private, and intra subnets | +| **NAT Gateway** | Single NAT for private subnet internet access | +| **EKS Cluster** | `bankapp-eks` running Kubernetes **1.35** | +| **Node Group** | `bankapp-ng` — 3x `t3.medium` instances on AL2023 | +| **Add-ons** | CoreDNS, kube-proxy, VPC-CNI, Pod Identity Agent, EBS CSI Driver | +| **ArgoCD** | Installed via Helm, exposed as LoadBalancer | + +## Prerequisites + +- [Terraform](https://developer.hashicorp.com/terraform/install) >= 1.5.7 +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) v2 (configured with `aws configure`) +- [kubectl](https://kubernetes.io/docs/tasks/tools/) +- [Helm 3](https://helm.sh/docs/intro/install/) +- GitHub repo secrets: + - `DOCKERHUB_USERNAME` — DockerHub username + - `DOCKERHUB_TOKEN` — DockerHub access token + +## 1. Provision EKS Cluster + +```bash +cd terraform +terraform init +terraform plan +terraform apply # ~15 minutes +``` + +This creates the VPC, EKS cluster, node group, add-ons, and ArgoCD. + +## 2. Configure kubectl + +```bash +aws eks update-kubeconfig --name bankapp-eks --region us-west-2 +kubectl get nodes +``` + +## 3. Access ArgoCD + +```bash +# Get the ArgoCD server URL (AWS NLB) +kubectl get svc argocd-server -n argocd -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' + +# Get the initial admin password +kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath='{.data.password}' | base64 -d +``` + +Login: username = `admin`, password = output from above. + +## 4. Install Gateway API + Envoy Gateway + +```bash +# Install Gateway API CRDs (must use --server-side to avoid ownership conflicts with Envoy Gateway) +kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml + +# Install Envoy Gateway via Helm (--skip-crds since Gateway API CRDs are already installed above) +helm install eg oci://docker.io/envoyproxy/gateway-helm \ + --version v1.2.6 \ + -n envoy-gateway-system \ + --create-namespace \ + --skip-crds \ + --wait + +# --skip-crds also skips Envoy Gateway extension CRDs (BackendTrafficPolicy etc.) +# Install them separately, then restart the controller to pick them up +helm pull oci://docker.io/envoyproxy/gateway-helm --version v1.2.6 --untar -d /tmp/eg-chart +kubectl apply --server-side -f /tmp/eg-chart/gateway-helm/crds/generated/ +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system + +# Verify GatewayClass is registered +kubectl get gatewayclass +``` + +On EKS, Envoy Gateway automatically provisions an AWS NLB when a Gateway resource is created. + +## 5. Install cert-manager (HTTPS/TLS) + +```bash +helm install cert-manager oci://quay.io/jetstack/charts/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --set crds.enabled=true \ + --set config.enableGatewayAPI=true \ + --wait +``` + +cert-manager integrates with Gateway API to auto-provision Let's Encrypt TLS certificates. +The `ClusterIssuer` and HTTPS listener are defined in `k8s/cert-manager.yml` and `k8s/gateway.yml` — ArgoCD syncs them automatically. + +**DNS setup (GoDaddy):** Create a CNAME record: +- `bankapp.trainwithshubham.com` → `` + +```bash +# Verify cert-manager +kubectl get pods -n cert-manager + +# Check certificate (after ArgoCD syncs and DNS propagates) +kubectl get certificate -n bankapp +kubectl describe certificate -n bankapp +``` + +## 6. Install kube-prometheus-stack + +```bash +# Add Helm repo +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +helm repo update + +# Install kube-prometheus-stack +helm install kube-prometheus prometheus-community/kube-prometheus-stack \ + -n monitoring \ + --create-namespace \ + --set grafana.service.type=LoadBalancer \ + --wait +``` + +Access Grafana: + +```bash +# Get Grafana URL +kubectl get svc kube-prometheus-grafana -n monitoring -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' + +# Get admin password +kubectl get secret kube-prometheus-grafana -n monitoring -o jsonpath='{.data.admin-password}' | base64 -d; echo +# Login: admin / +``` + +Access Prometheus (port-forward): + +```bash +kubectl port-forward svc/kube-prometheus-kube-prome-prometheus 9090:9090 -n monitoring +# Open http://localhost:9090 +``` + +## 7. Deploy Application via ArgoCD + +### Option A: CLI + +```bash +kubectl apply -f argocd/application.yml +``` + +### Option B: ArgoCD UI + +1. Open ArgoCD UI (URL from Step 3) +2. Click **New App** → fill in: + - **Name:** `bankapp` + - **Project:** `default` + - **Repo URL:** `https://github.com/TrainWithShubham/AI-BankApp-DevOps.git` + - **Revision:** `feat/gitops` + - **Path:** `k8s` + - **Cluster:** `https://kubernetes.default.svc` + - **Namespace:** `bankapp` +3. Enable **Auto-Sync** + **Self-Heal** + **Prune** +4. Click **Create** + +ArgoCD deploys everything from `k8s/`: + +| Resource | What it does | +|----------|-------------| +| `namespace.yml` | Creates `bankapp` namespace | +| `pv.yml` | gp3 StorageClass (EBS CSI dynamic provisioning) | +| `pvc.yml` | PVCs for MySQL (5Gi) and Ollama (10Gi) | +| `configmap.yml` | DB host, port, Ollama URL | +| `secrets.yml` | DB credentials | +| `mysql-deployment.yml` | MySQL 8.0 with EBS volume | +| `ollama-deployment.yml` | Ollama AI with tinyllama model | +| `bankapp-deployment.yml` | BankApp (2 replicas, HPA enabled) | +| `service.yml` | ClusterIP services for all components | +| `gateway.yml` | Gateway API → external access via NLB | +| `hpa.yml` | Auto-scale BankApp 2-4 replicas | + +## 8. Verify Deployment + +```bash +# Pods +kubectl get pods -n bankapp + +# Services +kubectl get svc -n bankapp + +# Gateway + NLB +kubectl get gateway -n bankapp + +# Get the app URL +export APP_URL=$(kubectl get svc -n envoy-gateway-system \ + -l gateway.envoyproxy.io/owning-gateway-name=bankapp-gateway \ + -o jsonpath='{.items[0].status.loadBalancer.ingress[0].hostname}') +echo "BankApp: http://$APP_URL" +``` + +## 9. CI/CD Pipeline (GitOps Flow) + +``` +Code Push → GitHub Actions → Build & Push to DockerHub → Update k8s manifest → ArgoCD auto-sync → EKS +``` + +1. **Developer** pushes code changes to `feat/gitops` +2. **GitHub Actions** (`.github/workflows/gitops-ci.yml`): + - Builds the Spring Boot app with Maven + - Builds Docker image + - Pushes `trainwithshubham/ai-bankapp-eks:` to DockerHub + - Updates `k8s/bankapp-deployment.yml` with new image tag + - Commits with `[skip ci]` +3. **ArgoCD** detects the manifest change → auto-syncs to EKS +4. **EKS** performs rolling update with zero downtime + +### GitHub Secrets Required + +Set in repo → Settings → Secrets → Actions: + +| Secret | Value | +|--------|-------| +| `DOCKERHUB_USERNAME` | DockerHub username | +| `DOCKERHUB_TOKEN` | DockerHub access token | + +## 10. ArgoCD Add-ons + +| Add-on | Purpose | Install | +|--------|---------|---------| +| **Image Updater** | Auto-update image tags from registries | `helm install argocd-image-updater argo/argocd-image-updater -n argocd` | +| **Notifications** | Slack/Teams alerts on sync events | Built into argo-cd chart, configure via ConfigMap | +| **Rollouts** | Blue/Green and Canary deployments | `kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml` | +| **ApplicationSets** | Template-driven multi-cluster apps | Built into ArgoCD 2.x | + +## 11. Cleanup + +> **Order matters** — Helm releases create AWS Load Balancers outside Terraform. +> Delete them first or `terraform destroy` will hang on VPC deletion. + +```bash +# 1. Delete ArgoCD app (removes Gateway → NLB) +kubectl delete -f argocd/application.yml + +# 2. Uninstall Helm releases +helm uninstall cert-manager -n cert-manager +helm uninstall kube-prometheus -n monitoring +helm uninstall eg -n envoy-gateway-system + +# 3. Wait for LBs to terminate +sleep 60 + +# 4. Destroy infrastructure +cd terraform +terraform destroy +``` + +See [`DEPLOYMENT.md`](../DEPLOYMENT.md#cleanup) for detailed cleanup steps and troubleshooting if VPC deletion gets stuck. + +## Project Structure + +``` +. +├── terraform/ +│ ├── provider.tf # Terraform + AWS + Helm providers +│ ├── variables.tf # Configurable inputs (region, cluster, nodes) +│ ├── vpc.tf # VPC module — public, private, intra subnets +│ ├── eks.tf # EKS module — cluster, node group, add-ons +│ ├── argocd.tf # ArgoCD Helm release +│ ├── outputs.tf # Cluster info, kubectl command, ArgoCD password +│ └── terraform.tfvars # Default variable values +├── k8s/ # Application manifests (ArgoCD watches this) +├── argocd/ +│ └── application.yml # ArgoCD Application pointing to k8s/ +└── .github/workflows/ + └── gitops-ci.yml # CI → DockerHub → manifest update +``` + +## Module Versions + +| Module | Version | Registry | +|---|---|---| +| terraform-aws-modules/eks/aws | ~> 21.0 | [Terraform Registry](https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/latest) | +| terraform-aws-modules/vpc/aws | ~> 6.0 | [Terraform Registry](https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest) | +| hashicorp/aws provider | ~> 6.0 | [Terraform Registry](https://registry.terraform.io/providers/hashicorp/aws/latest) | +| argoproj/argo-cd (Helm) | latest | [Artifact Hub](https://artifacthub.io/packages/helm/argo/argo-cd) | + +## Troubleshooting + +```bash +# ArgoCD app status +kubectl get applications -n argocd +kubectl describe application bankapp -n argocd + +# Pod logs +kubectl logs -l app=bankapp -n bankapp --tail=50 + +# EBS PVC status (should be Bound) +kubectl get pvc -n bankapp + +# Gateway status +kubectl get gateway,httproute -n bankapp +``` diff --git a/2026/day-78/AI-BankApp-DevOps/terraform/argocd.tf b/2026/day-78/AI-BankApp-DevOps/terraform/argocd.tf new file mode 100644 index 0000000000..7cecabbe15 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/terraform/argocd.tf @@ -0,0 +1,27 @@ +# ArgoCD — Installed via Helm into the EKS cluster +# Exposed as LoadBalancer (AWS NLB) for easy access + +resource "helm_release" "argocd" { + name = "argocd" + repository = "https://argoproj.github.io/argo-helm" + chart = "argo-cd" + namespace = "argocd" + create_namespace = true + + values = [ + yamlencode({ + server = { + service = { + type = "LoadBalancer" + } + } + configs = { + params = { + "server.insecure" = true + } + } + }) + ] + + depends_on = [module.eks] +} diff --git a/2026/day-78/AI-BankApp-DevOps/terraform/eks.tf b/2026/day-78/AI-BankApp-DevOps/terraform/eks.tf new file mode 100644 index 0000000000..6d35f9dc8c --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/terraform/eks.tf @@ -0,0 +1,81 @@ +# EKS Cluster — Module v21.x with Kubernetes 1.35 +# Uses EKS Pod Identity, access_entries, AL2023 AMI, and AWS provider v6.0+ + +module "eks" { + source = "terraform-aws-modules/eks/aws" + version = "~> 21.0" + + # v21 parameter names (renamed from v20) + name = local.name + kubernetes_version = var.cluster_version + + endpoint_public_access = true + endpoint_private_access = true + + # Cluster creator gets admin access via access_entries + enable_cluster_creator_admin_permissions = true + + # EKS Add-ons (latest versions auto-resolved) + addons = { + coredns = { + most_recent = true + } + kube-proxy = { + most_recent = true + } + vpc-cni = { + most_recent = true + before_compute = true + } + eks-pod-identity-agent = { + most_recent = true + before_compute = true + } + aws-ebs-csi-driver = { + most_recent = true + service_account_role_arn = module.ebs_csi_irsa.iam_role_arn + } + metrics-server = { + most_recent = true + } + } + + # Networking + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + control_plane_subnet_ids = module.vpc.intra_subnets + + # Managed Node Group + eks_managed_node_groups = { + bankapp-ng = { + instance_types = [var.node_instance_type] + desired_size = var.node_desired_count + min_size = var.node_desired_count + max_size = var.node_max_count + + tags = { + NodeGroup = "bankapp" + } + } + } + + tags = local.tags +} + +# IRSA for EBS CSI Driver (needed to create/attach EBS volumes) +module "ebs_csi_irsa" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + version = "~> 5.0" + + role_name = "${local.name}-ebs-csi" + attach_ebs_csi_policy = true + + oidc_providers = { + main = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["kube-system:ebs-csi-controller-sa"] + } + } + + tags = local.tags +} diff --git a/2026/day-78/AI-BankApp-DevOps/terraform/outputs.tf b/2026/day-78/AI-BankApp-DevOps/terraform/outputs.tf new file mode 100644 index 0000000000..aa9dc8a30c --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/terraform/outputs.tf @@ -0,0 +1,53 @@ +# Cluster +output "cluster_name" { + description = "EKS cluster name" + value = module.eks.cluster_name +} + +output "cluster_endpoint" { + description = "EKS cluster API endpoint" + value = module.eks.cluster_endpoint +} + +output "cluster_version" { + description = "Kubernetes version running on the cluster" + value = module.eks.cluster_version +} + +output "cluster_certificate_authority_data" { + description = "Base64 encoded certificate data for cluster authentication" + value = module.eks.cluster_certificate_authority_data + sensitive = true +} + +output "oidc_provider_arn" { + description = "OIDC provider ARN (for IRSA if needed)" + value = module.eks.oidc_provider_arn +} + +# Networking +output "vpc_id" { + description = "VPC ID" + value = module.vpc.vpc_id +} + +output "private_subnets" { + description = "Private subnet IDs (worker nodes)" + value = module.vpc.private_subnets +} + +output "public_subnets" { + description = "Public subnet IDs (load balancers)" + value = module.vpc.public_subnets +} + +# Quick commands +output "configure_kubectl" { + description = "Command to configure kubectl" + value = "aws eks update-kubeconfig --name ${module.eks.cluster_name} --region ${local.region}" +} + +output "argocd_initial_password" { + description = "Command to get ArgoCD initial admin password" + value = "kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath='{.data.password}' | base64 -d" +} diff --git a/2026/day-78/AI-BankApp-DevOps/terraform/provider.tf b/2026/day-78/AI-BankApp-DevOps/terraform/provider.tf new file mode 100644 index 0000000000..9ba9d855be --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/terraform/provider.tf @@ -0,0 +1,54 @@ +terraform { + required_version = ">= 1.5.7" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.17" + } + } +} + +locals { + region = var.aws_region + name = var.cluster_name + vpc_cidr = "10.0.0.0/16" + azs = slice(data.aws_availability_zones.available.names, 0, 3) + public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] + private_subnets = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"] + intra_subnets = ["10.0.7.0/24", "10.0.8.0/24", "10.0.9.0/24"] + + tags = { + Project = "AI-BankApp" + ManagedBy = "terraform" + Environment = "production" + } +} + +data "aws_availability_zones" "available" { + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +provider "aws" { + region = local.region +} + +provider "helm" { + kubernetes { + host = module.eks.cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) + + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "aws" + args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name] + } + } +} diff --git a/2026/day-78/AI-BankApp-DevOps/terraform/terraform.tfvars b/2026/day-78/AI-BankApp-DevOps/terraform/terraform.tfvars new file mode 100644 index 0000000000..4ee8cf47ea --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/terraform/terraform.tfvars @@ -0,0 +1,6 @@ +aws_region = "us-west-2" +cluster_name = "bankapp-eks" +cluster_version = "1.35" +node_instance_type = "t3.medium" +node_desired_count = 3 +node_max_count = 5 diff --git a/2026/day-78/AI-BankApp-DevOps/terraform/variables.tf b/2026/day-78/AI-BankApp-DevOps/terraform/variables.tf new file mode 100644 index 0000000000..5874833f56 --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/terraform/variables.tf @@ -0,0 +1,35 @@ +variable "aws_region" { + description = "AWS region for EKS cluster" + type = string + default = "us-west-2" +} + +variable "cluster_name" { + description = "Name of the EKS cluster" + type = string + default = "bankapp-eks" +} + +variable "cluster_version" { + description = "Kubernetes version for EKS" + type = string + default = "1.35" +} + +variable "node_instance_type" { + description = "EC2 instance type for EKS worker nodes" + type = string + default = "t3.medium" +} + +variable "node_desired_count" { + description = "Desired number of worker nodes" + type = number + default = 3 +} + +variable "node_max_count" { + description = "Maximum number of worker nodes" + type = number + default = 5 +} diff --git a/2026/day-78/AI-BankApp-DevOps/terraform/vpc.tf b/2026/day-78/AI-BankApp-DevOps/terraform/vpc.tf new file mode 100644 index 0000000000..f3f1d3768f --- /dev/null +++ b/2026/day-78/AI-BankApp-DevOps/terraform/vpc.tf @@ -0,0 +1,23 @@ +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 6.0" + + name = local.name + cidr = local.vpc_cidr + azs = local.azs + public_subnets = local.public_subnets + private_subnets = local.private_subnets + intra_subnets = local.intra_subnets + + enable_nat_gateway = true + + public_subnet_tags = { + "kubernetes.io/role/elb" = 1 + } + + private_subnet_tags = { + "kubernetes.io/role/internal-elb" = 1 + } + + tags = local.tags +} diff --git a/2026/day-78/Handwritten Helm and Kubernetes Study Guide.png b/2026/day-78/Handwritten Helm and Kubernetes Study Guide.png new file mode 100644 index 0000000000..c2a47d6c50 Binary files /dev/null and b/2026/day-78/Handwritten Helm and Kubernetes Study Guide.png differ diff --git a/2026/day-78/Screenshot 2026-10-03 020601.png b/2026/day-78/Screenshot 2026-10-03 020601.png new file mode 100644 index 0000000000..5039c12775 Binary files /dev/null and b/2026/day-78/Screenshot 2026-10-03 020601.png differ diff --git a/2026/day-78/Screenshot 2026-10-03 020615.png b/2026/day-78/Screenshot 2026-10-03 020615.png new file mode 100644 index 0000000000..76fb2c0eca Binary files /dev/null and b/2026/day-78/Screenshot 2026-10-03 020615.png differ diff --git a/2026/day-78/Screenshot 2026-10-03 020628.png b/2026/day-78/Screenshot 2026-10-03 020628.png new file mode 100644 index 0000000000..330c2061ca Binary files /dev/null and b/2026/day-78/Screenshot 2026-10-03 020628.png differ diff --git a/2026/day-78/Screenshot 2026-10-03 020754.png b/2026/day-78/Screenshot 2026-10-03 020754.png new file mode 100644 index 0000000000..92ee43e4c4 Binary files /dev/null and b/2026/day-78/Screenshot 2026-10-03 020754.png differ diff --git a/2026/day-78/Screenshot 2026-10-03 020903.png b/2026/day-78/Screenshot 2026-10-03 020903.png new file mode 100644 index 0000000000..55144ce2c6 Binary files /dev/null and b/2026/day-78/Screenshot 2026-10-03 020903.png differ diff --git a/2026/day-78/day-78-helm-intro.md b/2026/day-78/day-78-helm-intro.md new file mode 100644 index 0000000000..dc11d42d5c --- /dev/null +++ b/2026/day-78/day-78-helm-intro.md @@ -0,0 +1,380 @@ +# Day 78 -- Introduction to Helm and Chart Basics + +## Objective + +Learn the basic concepts of Helm and use Helm to deploy and manage MySQL +on a Kubernetes Kind cluster for the AI-BankApp project. + +## Project Setup + +Repository: + +`TrainWithShubham/AI-BankApp-DevOps` + +Branch: + +`feat/gitops` + +The existing project contains Kubernetes manifests under `k8s/` and a +Kind cluster configuration under `setup-k8s/`. + +## 1. Helm Concepts + +Helm is a **package manager for Kubernetes**, similar to `apt` or `yum` +in Linux. + +Important Helm terms: + +- **Chart** -- A reusable package containing Kubernetes templates and + configuration. +- **Repository** -- A location where Helm charts are stored and + shared. +- **Values** -- Configuration used to customize a chart. +- **Release** -- An installed instance of a Helm chart inside + Kubernetes. + +Simple flow: + +`Helm Repository → Chart → Values → Templates → Kubernetes Resources → Release` + +Helm does not replace Kubernetes. Helm generates/manages the Kubernetes +configuration, while Kubernetes still runs and manages the actual +workloads. + +## 2. Kind Cluster + +The project Kind configuration created: + +- 1 control-plane node +- 2 worker nodes +- Kubernetes v1.35.0 +- Host port `8080` mapped to container port `30080` + +Command: + +``` bash +kind create cluster --config setup-k8s/kind-config.yml +``` + +Verification: + +``` bash +kubectl get nodes +``` + +All three nodes reached `Ready` status. + +## 3. Verify Helm + +Helm was already installed. + +``` bash +helm version +helm list +``` + +`helm list` initially showed no releases. + +## 4. Add Bitnami Helm Repository + +``` bash +helm repo add bitnami https://charts.bitnami.com/bitnami +helm repo update +helm search repo bitnami/mysql +``` + +MySQL chart found: + +- Chart version: `14.0.3` +- Application version: `9.4.0` + +The chart version represents the Helm package version, while the +application version represents the MySQL version packaged by the chart. + +## 5. Inspect Default Values + +``` bash +helm show values bitnami/mysql | head -80 +``` + +This displays the default configurable settings of the chart without +deploying anything. + +## 6. Install MySQL with Helm + +``` bash +helm install bankapp-mysql bitnami/mysql \ + --set auth.rootPassword=Test@123 \ + --set auth.database=bankappdb \ + --set primary.resources.requests.memory=256Mi \ + --set primary.resources.requests.cpu=250m \ + --set primary.resources.limits.memory=512Mi \ + --set primary.resources.limits.cpu=500m \ + --set primary.persistence.size=5Gi +``` + +The Helm release was created as: + +- Release: `bankapp-mysql` +- Namespace: `default` +- Chart: `mysql-14.0.3` +- App version: `9.4.0` + +## 7. Troubleshooting ImagePullBackOff + +The MySQL Pod initially failed with: + +`Init:ImagePullBackOff` + +`kubectl describe pod bankapp-mysql-0` showed that Kubernetes could not +pull: + +`docker.io/bitnami/mysql:9.4.0-debian-12-r1` + +For this lab environment, the release was updated to use the available +legacy image repository: + +``` bash +helm upgrade bankapp-mysql bitnami/mysql \ + --reuse-values \ + --set image.repository=bitnamilegacy/mysql \ + --set global.security.allowInsecureImages=true +``` + +The StatefulSet template was checked and showed: + +`docker.io/bitnamilegacy/mysql:9.4.0-debian-12-r1` + +The old stuck Pod was then deleted so the StatefulSet could recreate it: + +``` bash +kubectl delete pod bankapp-mysql-0 +``` + +The recreated Pod became: + +`1/1 Running` + +> Note: This image substitution was used for the local learning lab. +> Helm displayed security warnings indicating that substituted images +> are not the chart's normal validated production configuration. + +## 8. Verify Persistent Storage + +``` bash +kubectl get pvc -l app.kubernetes.io/instance=bankapp-mysql +``` + +Result: + +- PVC status: `Bound` +- Capacity: `5Gi` +- Access mode: `RWO` +- StorageClass: `standard` + +This confirmed that the Helm persistence value was applied. + +## 9. Verify Kubernetes Secret + +``` bash +kubectl get secret -l app.kubernetes.io/instance=bankapp-mysql +``` + +Helm created an `Opaque` Kubernetes Secret for the MySQL release. + +## 10. Verify Database + +``` bash +kubectl exec -it bankapp-mysql-0 -- \ + mysql -uroot -pTest@123 -e "SHOW DATABASES;" +``` + +The output included: + +`bankappdb` + +This proved that the Helm value: + +`auth.database=bankappdb` + +was successfully passed to the MySQL application. + +## 11. Use a Values File + +Instead of providing many `--set` arguments, a reusable values file was +created. + +File: `mysql-values.yaml` + +``` yaml +auth: + rootPassword: Test@123 + database: bankappdb + +primary: + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 250m + memory: 256Mi + persistence: + size: 5Gi + storageClass: "" + +metrics: + enabled: true + serviceMonitor: + enabled: false +``` + +A second release was installed using: + +``` bash +helm install bankapp-mysql-v2 bitnami/mysql \ + -f mysql-values.yaml \ + --set image.repository=bitnamilegacy/mysql \ + --set global.security.allowInsecureImages=true +``` + +This demonstrated that the **same chart can create multiple independent +releases** and that `-f` can load reusable configuration from a values +file. + +## 12. Upgrade a Helm Release + +The original release was upgraded: + +``` bash +helm upgrade bankapp-mysql bitnami/mysql \ + --reuse-values \ + --set metrics.enabled=true +``` + +Helm created a new revision instead of replacing the release history. + +## 13. Helm History + +``` bash +helm history bankapp-mysql +``` + +The lab produced multiple revisions: + +- Revision 1 -- Initial installation +- Revision 2 -- Image repository fix +- Revision 3 -- Upgrade +- Revision 4 -- Upgrade with metrics enabled +- Revision 5 -- Rollback to Revision 2 + +Older revisions showed `superseded`, while the active revision showed +`deployed`. + +## 14. Rollback + +Because Revision 1 contained the image configuration that had failed in +this environment, the lab safely rolled back to Revision 2: + +``` bash +helm rollback bankapp-mysql 2 +``` + +Helm reported: + +`Rollback was a success! Happy Helming!` + +The rollback created **Revision 5** with the description: + +`Rollback to 2` + +Important lesson: a Helm rollback does not delete later history. It +creates a new revision using the configuration from an older revision. + +## 15. Inspect a Helm Chart + +The Bitnami MySQL chart was downloaded and extracted: + +``` bash +helm pull bitnami/mysql --untar +ls mysql/ +``` + +Important files/directories included: + +- `Chart.yaml` -- chart metadata and version information +- `values.yaml` -- default chart configuration +- `templates/` -- Kubernetes resource templates +- `charts/` -- chart dependencies +- `README.md` +- `values.schema.json` + +Templates were inspected with: + +``` bash +ls mysql/templates/ +``` + +Examples included: + +- `_helpers.tpl` +- `NOTES.txt` +- `secrets.yaml` +- `serviceaccount.yaml` +- `networkpolicy.yaml` +- `primary/` + +The downloaded chart directory was then removed: + +``` bash +rm -rf mysql/ +``` + +## 16. Cleanup + +The MySQL release can be removed with: + +``` bash +helm uninstall bankapp-mysql +``` + +Verification: + +``` bash +helm list +kubectl get pods +``` + +## Key Learning + +Before Helm, Kubernetes applications may require many separate YAML +files. + +With Helm: + +`Chart + Values → Rendered Kubernetes YAML → Kubernetes API → Resources` + +Helm makes Kubernetes application configuration reusable and provides +release management features such as: + +- Install +- Upgrade +- Revision history +- Rollback +- Uninstall + +### Interview Summary + +**Helm is a package manager for Kubernetes. A Helm chart contains +reusable Kubernetes templates, `values.yaml` provides configuration, and +installing a chart creates a release. Helm makes it easier to deploy, +upgrade, rollback, and reuse Kubernetes application configurations +across environments.** + +## Day 78 Status + +**Completed successfully.** + +The next step is **Day 79 -- Creating a Custom Helm Chart for +AI-BankApp**, where the existing Kubernetes manifests will be converted +into our own reusable Helm chart. diff --git a/2026/day-79/Day 79_ Custom Helm Chart Infographic.png b/2026/day-79/Day 79_ Custom Helm Chart Infographic.png new file mode 100644 index 0000000000..cc2270fbd6 Binary files /dev/null and b/2026/day-79/Day 79_ Custom Helm Chart Infographic.png differ diff --git a/2026/day-79/Screenshot 2026-10-05 122438.png b/2026/day-79/Screenshot 2026-10-05 122438.png new file mode 100644 index 0000000000..f93af70a1b Binary files /dev/null and b/2026/day-79/Screenshot 2026-10-05 122438.png differ diff --git a/2026/day-79/Screenshot 2026-10-05 122526.png b/2026/day-79/Screenshot 2026-10-05 122526.png new file mode 100644 index 0000000000..e8734b5146 Binary files /dev/null and b/2026/day-79/Screenshot 2026-10-05 122526.png differ diff --git a/2026/day-79/Screenshot 2026-10-05 122728.png b/2026/day-79/Screenshot 2026-10-05 122728.png new file mode 100644 index 0000000000..034ac01f6e Binary files /dev/null and b/2026/day-79/Screenshot 2026-10-05 122728.png differ diff --git a/2026/day-79/day-79-custom-charts.md b/2026/day-79/day-79-custom-charts.md new file mode 100644 index 0000000000..ffa72cb588 --- /dev/null +++ b/2026/day-79/day-79-custom-charts.md @@ -0,0 +1,441 @@ +# Day 79 -- Creating a Custom Helm Chart for AI-BankApp + +## Objective + +Convert the AI-BankApp Kubernetes manifests into a reusable custom Helm +chart and deploy the complete Spring Boot + MySQL + Ollama stack using +one Helm release. + +## 1. Create the Chart + +``` bash +mkdir -p helm-chart +cd helm-chart +helm create bankapp +rm -rf bankapp/templates/*.yaml bankapp/templates/tests/ +``` + +We kept `_helpers.tpl` for reusable naming/label helpers and customized +`NOTES.txt`. + +## 2. Chart Metadata + +`Chart.yaml`: + +``` yaml +apiVersion: v2 +name: bankapp +description: AI-BankApp -- Spring Boot banking application with MySQL and Ollama AI chatbot +type: application +version: 0.1.0 +appVersion: "1.0.0" +maintainers: + - name: TrainWithShubham + url: https://github.com/TrainWithShubham +keywords: + - bankapp + - spring-boot + - mysql + - ollama + - ai +``` + +`version` is the Helm chart version. `appVersion` is the application +version. + +## 3. values.yaml + +Configuration was moved out of hardcoded Kubernetes manifests into +`values.yaml`. + +Main sections: + +- `bankapp` -- Spring Boot application settings +- `mysql` -- MySQL image, resources, and persistence +- `ollama` -- Ollama image, TinyLlama model, resources, and + persistence +- `config` -- shared application configuration +- `secrets` -- database credentials +- `storageClass` -- storage configuration +- `gateway` -- optional gateway configuration + +Important examples: + +``` yaml +bankapp: + replicaCount: 4 + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 4 + targetCPUUtilization: 70 + +mysql: + enabled: true + persistence: + size: 5Gi + storageClass: gp3 + +ollama: + enabled: true + model: tinyllama + persistence: + size: 10Gi + storageClass: gp3 + +config: + mysqlDatabase: bankappdb +``` + +## 4. ConfigMap Template + +Created `templates/configmap.yaml`. + +Example: + +``` yaml +MYSQL_DATABASE: {{ .Values.config.mysqlDatabase | quote }} +``` + +Helm rendered it as: + +``` yaml +MYSQL_DATABASE: "bankappdb" +``` + +This demonstrates the main Helm flow: + +``` text +values.yaml + ↓ +Helm template {{ ... }} + ↓ +helm template + ↓ +Normal Kubernetes YAML +``` + +## 5. Secret Template + +Created `templates/secret.yaml`. + +Example: + +``` yaml +MYSQL_ROOT_PASSWORD: {{ .Values.secrets.mysqlRootPassword | b64enc | quote }} +``` + +`b64enc` Base64-encodes the value for the Kubernetes Secret. + +For production, secrets should normally be stored in a dedicated +secret-management solution rather than committed as plain text in +`values.yaml`. + +## 6. Persistent Storage + +Created `templates/storage.yaml` containing: + +- Optional StorageClass +- MySQL PVC -- 5Gi +- Ollama PVC -- 10Gi + +Conditional resources use syntax such as: + +``` yaml +{{- if .Values.ollama.enabled }} +``` + +This allows Ollama-related resources to disappear automatically when +Ollama is disabled. + +## 7. Deployments + +Three Deployment templates were created. + +### BankApp + +`templates/bankapp-deployment.yaml` + +Important features: + +- Spring Boot container +- ConfigMap and Secret via `envFrom` +- Resource requests/limits +- Readiness and liveness probes +- Init container waiting for MySQL +- Init container waiting for Ollama +- HPA-aware replica configuration + +### MySQL + +`templates/mysql-deployment.yaml` + +Important features: + +- MySQL 8.0 +- Password from Secret +- Database name from ConfigMap +- Persistent volume at `/var/lib/mysql` +- Readiness/liveness probes +- Recreate strategy + +### Ollama + +`templates/ollama-deployment.yaml` + +Important features: + +- Ollama container +- Persistent storage +- TinyLlama model +- `postStart` lifecycle hook +- Readiness/liveness probes + +The lifecycle hook runs `ollama pull tinyllama`. + +## 8. Services + +Created `templates/services.yaml`. + + Service Port + --------- ------- + MySQL 3306 + Ollama 11434 + BankApp 8080 + +All are ClusterIP services in this lab. + +## 9. Horizontal Pod Autoscaler + +Created `templates/hpa.yaml`. + +``` text +Target CPU: 70% +Minimum BankApp Pods: 2 +Maximum BankApp Pods: 4 +``` + +Because the minimum is 2, Kubernetes created two BankApp Pods. + +## 10. Validate the Chart + +``` bash +helm lint bankapp/ +``` + +Result: + +``` text +1 chart(s) linted, 0 chart(s) failed +``` + +Render locally: + +``` bash +helm template my-bankapp bankapp/ > /tmp/bankapp-rendered.yaml +grep "^kind:" /tmp/bankapp-rendered.yaml +``` + +The chart rendered 12 resources: + +``` text +Secret +ConfigMap +StorageClass +PersistentVolumeClaim +PersistentVolumeClaim +Service +Service +Service +Deployment +Deployment +Deployment +HorizontalPodAutoscaler +``` + +`helm template` is useful because it renders final Kubernetes YAML +without installing anything. + +## 11. Troubleshooting NOTES.txt + +Initially `helm template` failed because the starter `NOTES.txt` +referenced: + +``` text +.Values.httpRoute.enabled +``` + +Our custom `values.yaml` did not contain that old starter setting. We +replaced the stale starter `NOTES.txt` content with BankApp-specific +release notes. + +Debugging lesson: + +1. Read the file name in the Helm error. +2. Find the failing `.Values` reference. +3. Compare it with `values.yaml`. +4. Fix the stale/incorrect template rather than adding unrelated + values. + +## 12. Install on Kind + +AWS/EKS uses the configured `gp3` storage, but the local Kind lab used +its `standard` StorageClass. + +``` bash +helm install my-bankapp bankapp/ -n bankapp --create-namespace --set storageClass.create=false --set mysql.persistence.storageClass=standard --set ollama.persistence.storageClass=standard +``` + +The Helm release was successfully deployed: + +``` text +NAME NAMESPACE REVISION STATUS CHART APP VERSION +my-bankapp bankapp 1 deployed bankapp-0.1.0 1.0.0 +``` + +## 13. Verify Storage and Resources + +Both PVCs became Bound: + +``` text +my-bankapp-mysql-pvc Bound 5Gi RWO standard +my-bankapp-ollama-pvc Bound 10Gi RWO standard +``` + +The three Services and HPA were also created successfully. + +## 14. Ollama Startup Troubleshooting + +Ollama initially stayed in `ContainerCreating`. + +We inspected it with: + +``` bash +kubectl describe pod my-bankapp-ollama-55bb44df54-nmzgr -n bankapp +``` + +The Events showed Kubernetes was downloading the large +`ollama/ollama:latest` image. It took approximately 2 minutes 35 +seconds, and the reported image size was approximately 3.78 GB. + +After the download: + +``` text +State: Running +Ready: True +ContainersReady: True +Restart Count: 0 +``` + +So the Helm chart was not broken; the large image simply needed time to +download. + +## 15. Final Pod Status + +``` bash +kubectl get pods -n bankapp +``` + +Final result: + +``` text +my-bankapp-7567587896-7vcrl 1/1 Running 0 +my-bankapp-7567587896-tcpv8 1/1 Running 0 +my-bankapp-mysql-6788576b64-qmrmj 1/1 Running 0 +my-bankapp-ollama-55bb44df54-nmzgr 1/1 Running 0 +``` + +Architecture: + +``` text + Helm Release + my-bankapp + | + +--------------+--------------+ + | | | + v v v + BankApp MySQL Ollama + 2 Pods 1 Pod 1 Pod + | | | + | v v + | 5Gi PVC 10Gi PVC + | + +------ HPA: min 2 / max 4 +``` + +All four Pods were healthy with zero restarts. + +## 16. Useful Verification Commands + +``` bash +helm list -n bankapp +kubectl get pods -n bankapp +kubectl get svc -n bankapp +kubectl get pvc -n bankapp +kubectl get hpa -n bankapp +``` + +## 17. Cleanup + +``` bash +helm uninstall my-bankapp -n bankapp +helm list -n bankapp +kubectl get pods -n bankapp +``` + +## Key Helm Concepts Learned + +**Chart:** Package containing Kubernetes templates, values, metadata, +and helpers. + +**Values:** Configuration supplied to templates. + +**Template:** Kubernetes YAML containing Helm expressions. + +**Release:** An installed instance of a Helm chart. + +**Conditional:** `{{- if .Values.ollama.enabled }}` enables/disables +resources. + +**Helper:** `{{ include "bankapp.fullname" . }}` reuses naming logic +from `_helpers.tpl`. + +**b64enc:** Base64-encodes Secret values. + +**toYaml + nindent:** Renders nested YAML cleanly. + +## Interview Summary + +> I converted the AI-BankApp Kubernetes manifests into a reusable custom +> Helm chart. I moved configurable settings into `values.yaml` and +> created templates for ConfigMaps, Secrets, persistent storage, +> Deployments, Services, and HPA. I used Helm helpers, conditionals, +> Base64 encoding, and templated resource configuration. I validated the +> chart using `helm lint` and `helm template`, then deployed the +> complete Spring Boot, MySQL, and Ollama stack to a Kind Kubernetes +> cluster as one Helm release. I also verified the Pods, Services, PVCs, +> and HPA and troubleshot Ollama startup by inspecting Kubernetes Pod +> events. + +## Final Result + +``` text +Custom Helm Chart DONE +Chart.yaml DONE +values.yaml DONE +ConfigMap DONE +Secret DONE +Persistent Storage DONE +BankApp Deployment DONE +MySQL Deployment DONE +Ollama Deployment DONE +Services DONE +HPA DONE +helm lint PASSED +helm template PASSED +Helm Release DEPLOYED +PVCs BOUND +All Pods RUNNING +``` + +**Day 79 -- Custom Helm Chart: COMPLETE** diff --git a/2026/day-79/helm-chart/bankapp/.helmignore b/2026/day-79/helm-chart/bankapp/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/2026/day-79/helm-chart/bankapp/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/2026/day-79/helm-chart/bankapp/Chart.yaml b/2026/day-79/helm-chart/bankapp/Chart.yaml new file mode 100644 index 0000000000..044c6b7bbf --- /dev/null +++ b/2026/day-79/helm-chart/bankapp/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: bankapp +description: AI-BankApp -- Spring Boot banking application with MySQL and Ollama AI chatbot +type: application +version: 0.1.0 +appVersion: "1.0.0" +maintainers: + - name: TrainWithShubham + url: https://github.com/TrainWithShubham +keywords: + - bankapp + - spring-boot + - mysql + - ollama + - ai diff --git a/2026/day-79/helm-chart/bankapp/templates/NOTES.txt b/2026/day-79/helm-chart/bankapp/templates/NOTES.txt new file mode 100644 index 0000000000..354012b3f2 --- /dev/null +++ b/2026/day-79/helm-chart/bankapp/templates/NOTES.txt @@ -0,0 +1,9 @@ +AI-BankApp Helm Release + +Release Name: {{ .Release.Name }} +Namespace: {{ .Release.Namespace }} + +BankApp Service: +{{ include "bankapp.fullname" . }}-service + +Thank you for installing AI-BankApp. diff --git a/2026/day-79/helm-chart/bankapp/templates/_helpers.tpl b/2026/day-79/helm-chart/bankapp/templates/_helpers.tpl new file mode 100644 index 0000000000..375881a9b4 --- /dev/null +++ b/2026/day-79/helm-chart/bankapp/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "bankapp.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bankapp.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "bankapp.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "bankapp.labels" -}} +helm.sh/chart: {{ include "bankapp.chart" . }} +{{ include "bankapp.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "bankapp.selectorLabels" -}} +app.kubernetes.io/name: {{ include "bankapp.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "bankapp.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "bankapp.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/2026/day-79/helm-chart/bankapp/templates/bankapp-deployment.yaml b/2026/day-79/helm-chart/bankapp/templates/bankapp-deployment.yaml new file mode 100644 index 0000000000..1fd777c8bc --- /dev/null +++ b/2026/day-79/helm-chart/bankapp/templates/bankapp-deployment.yaml @@ -0,0 +1,62 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "bankapp.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + {{- if not .Values.bankapp.autoscaling.enabled }} + replicas: {{ .Values.bankapp.replicaCount }} + {{- end }} + selector: + matchLabels: + app: {{ include "bankapp.fullname" . }} + template: + metadata: + labels: + app: {{ include "bankapp.fullname" . }} + spec: + initContainers: + - name: wait-for-mysql + image: busybox:1.36 + command: ["/bin/sh", "-c", "until nc -z {{ include "bankapp.fullname" . }}-mysql 3306; do sleep 2; done"] + resources: + requests: { memory: "32Mi", cpu: "50m" } + limits: { memory: "64Mi", cpu: "100m" } + {{- if .Values.ollama.enabled }} + - name: wait-for-ollama + image: busybox:1.36 + command: ["/bin/sh", "-c", "until nc -z {{ include "bankapp.fullname" . }}-ollama 11434; do sleep 2; done"] + resources: + requests: { memory: "32Mi", cpu: "50m" } + limits: { memory: "64Mi", cpu: "100m" } + {{- end }} + containers: + - name: bankapp + image: "{{ .Values.bankapp.image.repository }}:{{ .Values.bankapp.image.tag }}" + imagePullPolicy: {{ .Values.bankapp.image.pullPolicy }} + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: {{ include "bankapp.fullname" . }}-config + - secretRef: + name: {{ include "bankapp.fullname" . }}-secret + {{- with .Values.bankapp.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + readinessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 30 + failureThreshold: 15 + livenessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 5 diff --git a/2026/day-79/helm-chart/bankapp/templates/configmap.yaml b/2026/day-79/helm-chart/bankapp/templates/configmap.yaml new file mode 100644 index 0000000000..27ea8f3934 --- /dev/null +++ b/2026/day-79/helm-chart/bankapp/templates/configmap.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "bankapp.fullname" . }}-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +data: + MYSQL_HOST: {{ include "bankapp.fullname" . }}-mysql + MYSQL_PORT: "3306" + MYSQL_DATABASE: {{ .Values.config.mysqlDatabase | quote }} + OLLAMA_URL: {{ default (printf "http://%s-ollama:11434" (include "bankapp.fullname" .)) .Values.config.ollamaUrl | quote }} + SERVER_FORWARD_HEADERS_STRATEGY: "native" diff --git a/2026/day-79/helm-chart/bankapp/templates/hpa.yaml b/2026/day-79/helm-chart/bankapp/templates/hpa.yaml new file mode 100644 index 0000000000..c3e149e5d9 --- /dev/null +++ b/2026/day-79/helm-chart/bankapp/templates/hpa.yaml @@ -0,0 +1,36 @@ +{{- if .Values.bankapp.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "bankapp.fullname" . }}-hpa + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "bankapp.fullname" . }} + minReplicas: {{ .Values.bankapp.autoscaling.minReplicas }} + maxReplicas: {{ .Values.bankapp.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.bankapp.autoscaling.targetCPUUtilization }} + behavior: + scaleUp: + stabilizationWindowSeconds: 30 + policies: + - type: Pods + value: 2 + periodSeconds: 60 + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Pods + value: 1 + periodSeconds: 60 +{{- end }} diff --git a/2026/day-79/helm-chart/bankapp/templates/mysql-deployment.yaml b/2026/day-79/helm-chart/bankapp/templates/mysql-deployment.yaml new file mode 100644 index 0000000000..54e7702659 --- /dev/null +++ b/2026/day-79/helm-chart/bankapp/templates/mysql-deployment.yaml @@ -0,0 +1,58 @@ +{{- if .Values.mysql.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "bankapp.fullname" . }}-mysql + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + app: {{ include "bankapp.fullname" . }}-mysql + strategy: + type: Recreate + template: + metadata: + labels: + app: {{ include "bankapp.fullname" . }}-mysql + spec: + containers: + - name: mysql + image: "{{ .Values.mysql.image.repository }}:{{ .Values.mysql.image.tag }}" + ports: + - containerPort: 3306 + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "bankapp.fullname" . }}-secret + key: MYSQL_ROOT_PASSWORD + - name: MYSQL_DATABASE + valueFrom: + configMapKeyRef: + name: {{ include "bankapp.fullname" . }}-config + key: MYSQL_DATABASE + {{- with .Values.mysql.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: mysql-storage + mountPath: /var/lib/mysql + readinessProbe: + exec: + command: ["mysqladmin", "ping", "-h", "localhost"] + initialDelaySeconds: 15 + failureThreshold: 10 + livenessProbe: + exec: + command: ["mysqladmin", "ping", "-h", "localhost"] + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 5 + volumes: + - name: mysql-storage + persistentVolumeClaim: + claimName: {{ include "bankapp.fullname" . }}-mysql-pvc +{{- end }} diff --git a/2026/day-79/helm-chart/bankapp/templates/ollama-deployment.yaml b/2026/day-79/helm-chart/bankapp/templates/ollama-deployment.yaml new file mode 100644 index 0000000000..1b0b65bf12 --- /dev/null +++ b/2026/day-79/helm-chart/bankapp/templates/ollama-deployment.yaml @@ -0,0 +1,57 @@ +{{- if .Values.ollama.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "bankapp.fullname" . }}-ollama + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + app: {{ include "bankapp.fullname" . }}-ollama + strategy: + type: Recreate + template: + metadata: + labels: + app: {{ include "bankapp.fullname" . }}-ollama + spec: + containers: + - name: ollama + image: "{{ .Values.ollama.image.repository }}:{{ .Values.ollama.image.tag }}" + ports: + - containerPort: 11434 + {{- with .Values.ollama.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: ollama-storage + mountPath: /root/.ollama + lifecycle: + postStart: + exec: + command: + - /bin/sh + - -c + - | + until ollama list > /dev/null 2>&1; do sleep 2; done + ollama pull {{ .Values.ollama.model }} + readinessProbe: + exec: + command: ["/bin/sh", "-c", "ollama list | grep -q {{ .Values.ollama.model }}"] + initialDelaySeconds: 30 + failureThreshold: 30 + livenessProbe: + httpGet: + path: / + port: 11434 + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 5 + volumes: + - name: ollama-storage + persistentVolumeClaim: + claimName: {{ include "bankapp.fullname" . }}-ollama-pvc +{{- end }} diff --git a/2026/day-79/helm-chart/bankapp/templates/secret.yaml b/2026/day-79/helm-chart/bankapp/templates/secret.yaml new file mode 100644 index 0000000000..d03135ad23 --- /dev/null +++ b/2026/day-79/helm-chart/bankapp/templates/secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "bankapp.fullname" . }}-secret + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +type: Opaque +data: + MYSQL_ROOT_PASSWORD: {{ .Values.secrets.mysqlRootPassword | b64enc | quote }} + MYSQL_USER: {{ .Values.secrets.mysqlUser | b64enc | quote }} + MYSQL_PASSWORD: {{ .Values.secrets.mysqlPassword | b64enc | quote }} diff --git a/2026/day-79/helm-chart/bankapp/templates/services.yaml b/2026/day-79/helm-chart/bankapp/templates/services.yaml new file mode 100644 index 0000000000..6c88f468f4 --- /dev/null +++ b/2026/day-79/helm-chart/bankapp/templates/services.yaml @@ -0,0 +1,40 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "bankapp.fullname" . }}-mysql + namespace: {{ .Release.Namespace }} +spec: + selector: + app: {{ include "bankapp.fullname" . }}-mysql + ports: + - port: 3306 +--- +{{- if .Values.ollama.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "bankapp.fullname" . }}-ollama + namespace: {{ .Release.Namespace }} +spec: + selector: + app: {{ include "bankapp.fullname" . }}-ollama + ports: + - port: 11434 +{{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "bankapp.fullname" . }}-service + namespace: {{ .Release.Namespace }} +spec: + type: {{ .Values.bankapp.service.type }} + sessionAffinity: ClientIP + sessionAffinityConfig: + clientIP: + timeoutSeconds: 3600 + selector: + app: {{ include "bankapp.fullname" . }} + ports: + - port: {{ .Values.bankapp.service.port }} + targetPort: 8080 diff --git a/2026/day-79/helm-chart/bankapp/templates/storage.yaml b/2026/day-79/helm-chart/bankapp/templates/storage.yaml new file mode 100644 index 0000000000..f34725a46c --- /dev/null +++ b/2026/day-79/helm-chart/bankapp/templates/storage.yaml @@ -0,0 +1,47 @@ +{{- if .Values.storageClass.create }} +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: {{ .Values.storageClass.name }} +provisioner: {{ .Values.storageClass.provisioner }} +parameters: + type: gp3 + fsType: ext4 +reclaimPolicy: Delete +volumeBindingMode: WaitForFirstConsumer +allowVolumeExpansion: true +{{- end }} +--- +{{- if .Values.mysql.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "bankapp.fullname" . }}-mysql-pvc + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + storageClassName: {{ .Values.mysql.persistence.storageClass }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.mysql.persistence.size }} +{{- end }} +--- +{{- if .Values.ollama.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "bankapp.fullname" . }}-ollama-pvc + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + storageClassName: {{ .Values.ollama.persistence.storageClass }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.ollama.persistence.size }} +{{- end }} diff --git a/2026/day-79/helm-chart/bankapp/values.yaml b/2026/day-79/helm-chart/bankapp/values.yaml new file mode 100644 index 0000000000..5dd2fbcd3f --- /dev/null +++ b/2026/day-79/helm-chart/bankapp/values.yaml @@ -0,0 +1,81 @@ +# BankApp configuration +bankapp: + replicaCount: 4 + image: + repository: trainwithshubham/ai-bankapp-eks + tag: "latest" + pullPolicy: Always + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + service: + type: ClusterIP + port: 8080 + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 4 + targetCPUUtilization: 70 + +# MySQL configuration +mysql: + enabled: true + image: + repository: mysql + tag: "8.0" + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + persistence: + size: 5Gi + storageClass: gp3 + +# Ollama AI configuration +ollama: + enabled: true + image: + repository: ollama/ollama + tag: "latest" + model: tinyllama + resources: + requests: + memory: "2Gi" + cpu: "900m" + limits: + memory: "2.5Gi" + cpu: "1500m" + persistence: + size: 10Gi + storageClass: gp3 + +# Shared configuration +config: + mysqlDatabase: bankappdb + ollamaUrl: "" + +# Secrets +secrets: + mysqlRootPassword: Test@123 + mysqlUser: root + mysqlPassword: Test@123 + +# Storage +storageClass: + create: true + name: gp3 + provisioner: ebs.csi.aws.com + +# Gateway +gateway: + enabled: false + hostname: "" + tls: + enabled: false diff --git a/2026/day-80/AI-BankApp-DevOps/.github/workflows/gitops-ci.yml b/2026/day-80/AI-BankApp-DevOps/.github/workflows/gitops-ci.yml new file mode 100644 index 0000000000..987549c66c --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/.github/workflows/gitops-ci.yml @@ -0,0 +1,75 @@ +name: GitOps CI - Build & Push to DockerHub + +on: + push: + branches: [feat/gitops] + paths: + - 'src/**' + - 'pom.xml' + - 'Dockerfile' + workflow_dispatch: + +permissions: + contents: write + +env: + DOCKERHUB_REPO: trainwithshubham/ai-bankapp-eks + +jobs: + build-and-push: + name: Build, Test & Push Docker Image + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: 'maven' + + - name: Build with Maven + run: ./mvnw clean package -DskipTests -B + + - name: Run tests + run: ./mvnw test -B + continue-on-error: true + + - name: Set image tag + id: tag + run: echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and Push Docker image + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: | + ${{ env.DOCKERHUB_REPO }}:latest + ${{ env.DOCKERHUB_REPO }}:${{ steps.tag.outputs.sha_short }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Update Kubernetes deployment manifest + run: | + sed -i "s|image: ${{ env.DOCKERHUB_REPO }}:.*|image: ${{ env.DOCKERHUB_REPO }}:${{ steps.tag.outputs.sha_short }}|" k8s/bankapp-deployment.yml + + - name: Commit updated manifest + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add k8s/bankapp-deployment.yml + git diff --staged --quiet || git commit -m "ci: update bankapp image to ${{ steps.tag.outputs.sha_short }} [skip ci]" + git push diff --git a/2026/day-80/AI-BankApp-DevOps/.gitignore b/2026/day-80/AI-BankApp-DevOps/.gitignore new file mode 100644 index 0000000000..d1b1c95d21 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/.gitignore @@ -0,0 +1,45 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Terraform ### +.terraform/ +*.tfstate +*.tfstate.backup +*.tfplan + +### Claude ### +CLAUDE.md + +### Speaker Notes (personal, not for repo) ### +SPEAKER_NOTES.md diff --git a/2026/day-80/AI-BankApp-DevOps/.mvn/wrapper/maven-wrapper.properties b/2026/day-80/AI-BankApp-DevOps/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000000..8f96f52c66 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,19 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +wrapperVersion=3.3.2 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip diff --git a/2026/day-80/AI-BankApp-DevOps/DEPLOYMENT.md b/2026/day-80/AI-BankApp-DevOps/DEPLOYMENT.md new file mode 100644 index 0000000000..f585c23a2e --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/DEPLOYMENT.md @@ -0,0 +1,276 @@ +# Deployment Playbook — AI BankApp on EKS + +Step-by-step commands to deploy the full stack. Run these in order. + +## Prerequisites + +- AWS CLI configured (`aws configure`) +- Terraform >= 1.5.7 +- kubectl +- Helm 3 (`brew install helm`) +- Docker (for local image builds) +- GitHub repo secrets: `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN` + +## Step 1: Provision Infrastructure + +```bash +cd terraform +terraform init +terraform plan +terraform apply +# Takes ~15 minutes — creates VPC, EKS cluster (3 nodes), ArgoCD +``` + +## Step 2: Configure kubectl + +```bash +aws eks update-kubeconfig --name bankapp-eks --region us-west-2 +kubectl get nodes +# Should show 3 nodes in Ready state +``` + +## Step 3: Verify ArgoCD + +```bash +# Check all pods are Running +kubectl get pods -n argocd + +# Get ArgoCD URL +kubectl get svc argocd-server -n argocd \ + -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' + +# Get admin password +kubectl get secret argocd-initial-admin-secret -n argocd \ + -o jsonpath='{.data.password}' | base64 -d; echo + +# Login: admin / +``` + +## Step 4: Install Gateway API + Envoy Gateway + +```bash +# Install Gateway API CRDs — MUST use --server-side +# Without it, Envoy Gateway helm install fails with CRD ownership conflicts +kubectl apply --server-side \ + -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml + +# Install Envoy Gateway — MUST use --skip-crds since Gateway API CRDs already exist +helm install eg oci://docker.io/envoyproxy/gateway-helm \ + --version v1.2.6 \ + -n envoy-gateway-system \ + --create-namespace \ + --skip-crds \ + --wait + +# --skip-crds also skips Envoy Gateway's own extension CRDs, install them separately +helm pull oci://docker.io/envoyproxy/gateway-helm --version v1.2.6 --untar -d /tmp/eg-chart +kubectl apply --server-side -f /tmp/eg-chart/gateway-helm/crds/generated/ +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system + +# Verify +kubectl get gatewayclass +``` + +## Step 5: Install cert-manager (TLS/HTTPS) + +```bash +helm install cert-manager oci://quay.io/jetstack/charts/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --set crds.enabled=true \ + --set config.enableGatewayAPI=true \ + --wait + +# Verify +kubectl get pods -n cert-manager +``` + +After ArgoCD syncs the ClusterIssuer and updated Gateway (with HTTPS listener), cert-manager automatically provisions a Let's Encrypt TLS certificate. + +**Prerequisite:** Create a CNAME in GoDaddy: +- `bankapp.trainwithshubham.com` → `` + +```bash +# Check certificate status +kubectl get certificate -n bankapp +kubectl get secret bankapp-tls -n bankapp +``` + +## Step 6: Install kube-prometheus-stack + +```bash +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +helm repo update prometheus-community + +helm install kube-prometheus prometheus-community/kube-prometheus-stack \ + -n monitoring \ + --create-namespace \ + --set grafana.service.type=LoadBalancer \ + --wait + +# Get Grafana URL +kubectl get svc kube-prometheus-grafana -n monitoring \ + -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' + +# Get Grafana password +kubectl get secret kube-prometheus-grafana -n monitoring \ + -o jsonpath='{.data.admin-password}' | base64 -d; echo + +# Login: admin / +``` + +## Step 7: Build & Push Docker Image + +> CI does this automatically on push to `feat/gitops`. This step is only needed for the **first deploy** (image doesn't exist on DockerHub yet). + +```bash +# IMPORTANT: EKS runs amd64 nodes. If building on Apple Silicon (M1/M2/M3), +# you MUST specify --platform linux/amd64 or pods will fail with: +# "no match for platform in manifest: not found" + +docker buildx build --platform linux/amd64 \ + -t trainwithshubham/ai-bankapp-eks:latest \ + --push . +``` + +## Step 8: Deploy via ArgoCD + +```bash +kubectl apply -f argocd/application.yml + +# Watch sync progress +kubectl get application bankapp -n argocd -w +``` + +## Step 9: Verify Everything + +```bash +# All pods should be Running +kubectl get pods -n bankapp + +# Check PVCs are Bound +kubectl get pvc -n bankapp + +# Check Gateway has an address +kubectl get gateway -n bankapp + +# Get the app URL (NLB created by Envoy Gateway) +kubectl get svc -n envoy-gateway-system \ + -l gateway.envoyproxy.io/owning-gateway-name=bankapp-gateway \ + -o jsonpath='{.items[0].status.loadBalancer.ingress[0].hostname}' + +# Test — should return 302 (Spring Security redirect to /login) +curl -s -o /dev/null -w "%{http_code}" http:/// + +# Test login page — should return 200 +curl -s -o /dev/null -w "%{http_code}" -L http:///login +``` + +## Step 10: Pull Ollama Model + +```bash +# Ollama starts empty — pull the AI model +kubectl exec -n bankapp deploy/ollama -- ollama pull tinyllama +``` + +## Cleanup + +> **ORDER MATTERS.** Helm-installed resources (Envoy Gateway, Grafana) create AWS Load Balancers +> and Security Groups outside of Terraform. If you run `terraform destroy` first, the EKS cluster +> is gone but those orphaned resources block VPC deletion. Always clean up in this order: + +```bash +# 1. Delete ArgoCD app (removes Gateway → deletes Envoy Gateway NLB) +kubectl delete -f argocd/application.yml + +# 2. Uninstall all Helm releases that created LoadBalancers/resources +helm uninstall cert-manager -n cert-manager +helm uninstall kube-prometheus -n monitoring +helm uninstall eg -n envoy-gateway-system + +# 3. Delete Gateway API CRDs +kubectl delete -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml + +# 4. Wait for Load Balancers to fully terminate (~30-60 seconds) +echo "Waiting for LBs to terminate..." +sleep 60 + +# 5. Verify no Load Balancers remain in the VPC +aws elb describe-load-balancers --region us-west-2 \ + --query 'LoadBalancerDescriptions[*].LoadBalancerName' --output text +# Should be empty. If not, delete manually: +# aws elb delete-load-balancer --load-balancer-name --region us-west-2 + +# 6. Destroy infrastructure +cd terraform +terraform destroy +``` + +**If `terraform destroy` gets stuck on VPC deletion**, orphaned resources remain: +```bash +# Find and delete orphaned security groups +aws ec2 describe-security-groups --region us-west-2 \ + --filters Name=vpc-id,Values= \ + --query 'SecurityGroups[?GroupName!=`default`].[GroupId,GroupName]' --output table +aws ec2 delete-security-group --group-id --region us-west-2 + +# Then delete the VPC manually +aws ec2 delete-vpc --vpc-id --region us-west-2 + +# Re-run terraform destroy to clean the state +terraform destroy +``` + +--- + +## Gotchas We Hit + +### 1. Gateway API CRDs + Envoy Gateway Conflict +**Symptom:** `helm install` for Envoy Gateway fails with `conflict with "kubectl-client-side-apply"`. +**Cause:** `kubectl apply` uses client-side apply for CRDs. When Envoy Gateway tries to install the same CRDs via server-side apply, ownership conflicts. +**Fix:** Use `kubectl apply --server-side` for CRDs, then `--skip-crds` on helm install. + +### 2. Docker Image Platform Mismatch +**Symptom:** Pods show `ErrImagePull` with error `no match for platform in manifest: not found`. +**Cause:** Building on Apple Silicon (arm64) produces arm64 images. EKS nodes are amd64. +**Fix:** Use `docker buildx build --platform linux/amd64 --push`. This does NOT apply to GitHub Actions CI (runs on ubuntu/amd64). + +### 3. Image Doesn't Exist on First Deploy +**Symptom:** Pods show `ImagePullBackOff` with `repository does not exist or may require authorization`. +**Cause:** ArgoCD deploys immediately but CI hasn't pushed the first image to DockerHub yet. +**Fix:** Build and push the image manually (Step 6) before or right after applying the ArgoCD application. + +### 4. Envoy Gateway Extension CRDs Missing After `--skip-crds` +**Symptom:** `BackendTrafficPolicy` resource fails with `no matches for kind`. +**Cause:** `--skip-crds` skips ALL CRDs, including Envoy Gateway's own extension CRDs (BackendTrafficPolicy, SecurityPolicy, etc.) — not just the Gateway API CRDs. +**Fix:** +```bash +helm pull oci://docker.io/envoyproxy/gateway-helm --version v1.2.6 --untar -d /tmp/eg-chart +kubectl apply --server-side -f /tmp/eg-chart/gateway-helm/crds/generated/ +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system +``` + +### 5. Login Redirect Loop with Multiple Replicas Behind Envoy Gateway +**Symptom:** Login page keeps redirecting back to itself. No error message shown. +**Cause:** Envoy Gateway bypasses K8s Service `sessionAffinity: ClientIP` and load-balances directly to pod endpoints. With in-memory sessions, GET /login hits pod A (creates CSRF token), POST /login hits pod B (CSRF mismatch → silent redirect to /login). +**Fix:** Add `BackendTrafficPolicy` with cookie-based consistent hashing (in `k8s/gateway.yml`) and `SERVER_FORWARD_HEADERS_STRATEGY=native` in configmap. + +### 6. `terraform destroy` Stuck on VPC Deletion +**Symptom:** `terraform destroy` hangs on VPC delete with `DependencyViolation`. +**Cause:** Helm-installed resources (Envoy Gateway, Grafana LB) created AWS Load Balancers and Security Groups outside Terraform. When EKS is destroyed first, these orphan and block VPC deletion. +**Fix:** Always uninstall Helm releases and delete the ArgoCD app BEFORE running `terraform destroy`. If already stuck, delete orphaned ELBs and SGs via AWS CLI (see Cleanup section above). + +### 7. ArgoCD Shows OutOfSync After Manual Rollout Restart +**Symptom:** ArgoCD status shows `OutOfSync` even though app is healthy. +**Cause:** `kubectl rollout restart` adds a restartedAt annotation that doesn't match the Git manifest. +**Fix:** Not a problem — next CI push updates the manifest and ArgoCD syncs to it, resolving the drift. + +--- + +## Access Summary + +| Service | URL Command | Credentials | +|---------|------------|-------------| +| **BankApp** | `kubectl get svc -n envoy-gateway-system -l gateway.envoyproxy.io/owning-gateway-name=bankapp-gateway -o jsonpath='{.items[0].status.loadBalancer.ingress[0].hostname}'` | App login | +| **ArgoCD** | `kubectl get svc argocd-server -n argocd -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'` | `admin` / see Step 3 | +| **Grafana** | `kubectl get svc kube-prometheus-grafana -n monitoring -o jsonpath='{.status.loadBalancer.ingress[0].hostname}'` | `admin` / see Step 5 | diff --git a/2026/day-80/AI-BankApp-DevOps/Dockerfile b/2026/day-80/AI-BankApp-DevOps/Dockerfile new file mode 100644 index 0000000000..92c47d42b1 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/Dockerfile @@ -0,0 +1,22 @@ +# Build stage +FROM eclipse-temurin:21-jdk-alpine AS build +WORKDIR /app +COPY . . +RUN chmod +x mvnw && ./mvnw clean package -DskipTests -B + +# Run stage - alpine has significantly fewer CVEs than ubuntu/jammy +FROM eclipse-temurin:21-jre-alpine +WORKDIR /app + +# Pull latest security patches for OS libraries +RUN apk update && apk upgrade --no-cache + +# Create a non-root user for security (Alpine uses addgroup/adduser instead of groupadd/useradd) +RUN addgroup -S devsecops && adduser -S -G devsecops devsecops +USER devsecops + +# Copy only the built artifact +COPY --from=build /app/target/*.jar app.jar + +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/2026/day-80/AI-BankApp-DevOps/LICENSE b/2026/day-80/AI-BankApp-DevOps/LICENSE new file mode 100644 index 0000000000..58006f3ebf --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 TrainWithShubham + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/2026/day-80/AI-BankApp-DevOps/README.md b/2026/day-80/AI-BankApp-DevOps/README.md new file mode 100644 index 0000000000..094689956f --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/README.md @@ -0,0 +1,160 @@ +
+ +# AI BankApp + +### End-to-End GitOps on Amazon EKS + +A modern banking application with an integrated AI chatbot, deployed on AWS EKS using Terraform, ArgoCD, Gateway API, and Prometheus monitoring. + +[![Java](https://img.shields.io/badge/Java-21-ED8B00?logo=openjdk&logoColor=white)](https://www.oracle.com/java/technologies/javase/jdk21-archive-downloads.html) +[![Spring Boot](https://img.shields.io/badge/Spring%20Boot-3.4.1-6DB33F?logo=springboot&logoColor=white)](https://spring.io/projects/spring-boot) +[![Kubernetes](https://img.shields.io/badge/EKS-1.35-326CE5?logo=kubernetes&logoColor=white)](https://aws.amazon.com/eks/) +[![ArgoCD](https://img.shields.io/badge/ArgoCD-GitOps-EF7B4D?logo=argo&logoColor=white)](https://argo-cd.readthedocs.io/) +[![Terraform](https://img.shields.io/badge/Terraform-IaC-844FBA?logo=terraform&logoColor=white)](https://www.terraform.io/) +[![Docker](https://img.shields.io/badge/Docker-Hub-2496ED?logo=docker&logoColor=white)](https://hub.docker.com/) + +--- + +![Dashboard](screenshots/dashboard.png) + +![AI Chatbot](screenshots/ai-chatbot.png) + +
+ +--- + +## Features + +- **Banking Operations** — Deposit, withdraw, transfer funds between accounts +- **AI Chatbot** — Context-aware financial assistant powered by Ollama (TinyLlama), self-hosted on Kubernetes +- **Dark/Light Mode** — Glassmorphism UI with theme toggle and localStorage persistence +- **Spring Security** — BCrypt password hashing, CSRF protection, form-based authentication +- **Prometheus Metrics** — Built-in `/actuator/prometheus` endpoint for monitoring + +--- + +## Architecture + +
+ +![GitOps Pipeline](screenshots/gitops-pipeline.gif) + +
+ +| Layer | Tool | +|-------|------| +| **Infrastructure** | Terraform (VPC + EKS + ArgoCD) | +| **CI Pipeline** | GitHub Actions → DockerHub | +| **GitOps / CD** | ArgoCD (auto-sync from `k8s/` manifests) | +| **Ingress** | Gateway API + Envoy Gateway (AWS NLB) | +| **TLS** | cert-manager + Let's Encrypt (auto-provisioned) | +| **Monitoring** | kube-prometheus-stack (Prometheus + Grafana) | +| **AI Chatbot** | Ollama (TinyLlama) on EKS | +| **Storage** | EBS CSI Driver (gp3 dynamic provisioning) | + +--- + +## What Gets Deployed + +| Resource | Details | +|----------|---------| +| **EKS Cluster** | Kubernetes 1.35, 3x `t3.medium` across 3 AZs | +| **BankApp** | 2 replicas with HPA (scales to 4), rolling updates | +| **MySQL 8.0** | Persistent EBS volume (gp3) | +| **Ollama AI** | TinyLlama model with persistent storage | +| **Gateway** | HTTPS with Let's Encrypt TLS, session persistence | +| **Monitoring** | Prometheus + Grafana dashboards | +| **ArgoCD** | Auto-sync, self-heal, prune | + +--- + +## Quick Start + +> Full step-by-step commands with troubleshooting: [`DEPLOYMENT.md`](DEPLOYMENT.md) + +```bash +# 1. Provision infrastructure (~15 min) +cd terraform && terraform init && terraform apply + +# 2. Configure kubectl +aws eks update-kubeconfig --name bankapp-eks --region us-west-2 + +# 3. Install Envoy Gateway + cert-manager + Prometheus +# (see DEPLOYMENT.md Steps 4-6) + +# 4. Deploy via ArgoCD +kubectl apply -f argocd/application.yml + +# 5. Pull AI model +kubectl exec -n bankapp deploy/ollama -- ollama pull tinyllama +``` + +--- + +## CI/CD — GitOps Flow + +``` +Code Push → GitHub Actions → Build & Push to DockerHub → Update k8s manifest → ArgoCD auto-sync → EKS +``` + +1. Push code changes to `feat/gitops` +2. **GitHub Actions** builds the app, pushes Docker image to DockerHub with commit SHA tag +3. Workflow updates `k8s/bankapp-deployment.yml` with new image tag and commits back +4. **ArgoCD** detects the manifest change and auto-syncs to EKS +5. **EKS** performs a rolling update with zero downtime + +**GitHub Secrets Required:** `DOCKERHUB_USERNAME`, `DOCKERHUB_TOKEN` + +--- + +## Project Structure + +``` +. +├── terraform/ # Infrastructure as Code (VPC + EKS + ArgoCD) +├── k8s/ # Kubernetes manifests (ArgoCD watches this) +│ ├── bankapp-deployment.yml +│ ├── mysql-deployment.yml +│ ├── ollama-deployment.yml +│ ├── service.yml +│ ├── gateway.yml # Gateway API + HTTPS + session persistence +│ ├── cert-manager.yml # Let's Encrypt ClusterIssuer +│ ├── hpa.yml # Horizontal Pod Autoscaler +│ └── ... # namespace, configmap, secrets, pv, pvc +├── argocd/ +│ └── application.yml # ArgoCD Application +├── .github/workflows/ +│ └── gitops-ci.yml # CI → DockerHub → manifest update +└── DEPLOYMENT.md # Step-by-step deployment playbook +``` + +--- + +## Documentation + +| Document | Purpose | +|----------|---------| +| [`DEPLOYMENT.md`](DEPLOYMENT.md) | Step-by-step deployment commands + gotchas | +| [`terraform/README.md`](terraform/README.md) | Detailed infrastructure setup + troubleshooting | + +--- + +## Tech Stack + +**Backend:** Java 21, Spring Boot 3.4.1, Spring Security, Thymeleaf, Actuator + +**Frontend:** Bootstrap 5, glassmorphism dark/light UI, CSS custom properties + +**AI:** Ollama with TinyLlama — self-hosted, zero cost, runs as a Kubernetes pod + +**Database:** MySQL 8.0 with EBS gp3 persistent volumes + +**DevOps:** Terraform, GitHub Actions, ArgoCD, Envoy Gateway, cert-manager, kube-prometheus-stack + +--- + +
+ +**TrainWithShubham** — Happy Learning + +
diff --git a/2026/day-80/AI-BankApp-DevOps/argocd/application.yml b/2026/day-80/AI-BankApp-DevOps/argocd/application.yml new file mode 100644 index 0000000000..6b6b741b04 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/argocd/application.yml @@ -0,0 +1,21 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: bankapp + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/TrainWithShubham/AI-BankApp-DevOps.git + targetRevision: feat/gitops + path: k8s + destination: + server: https://kubernetes.default.svc + namespace: bankapp + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/2026/day-80/AI-BankApp-DevOps/docker-compose.yml b/2026/day-80/AI-BankApp-DevOps/docker-compose.yml new file mode 100644 index 0000000000..2220669840 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/docker-compose.yml @@ -0,0 +1,77 @@ +services: + mysql: + image: mysql:8.0 + container_name: bankapp-mysql + environment: + MYSQL_ROOT_PASSWORD: Test@123 + MYSQL_DATABASE: bankappdb + ports: + - "3306:3306" + volumes: + - mysql-data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - bankapp-net + + ollama: + image: ollama/ollama + container_name: bankapp-ollama + ports: + - "11434:11434" + volumes: + - ollama-data:/root/.ollama + healthcheck: + test: ["CMD", "ollama", "list"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - bankapp-net + + ollama-pull-model: + image: ollama/ollama + container_name: ollama-pull-model + environment: + - OLLAMA_HOST=ollama + volumes: + - ollama-data:/root/.ollama + depends_on: + ollama: + condition: service_healthy + entrypoint: /bin/sh + command: -c "ollama pull tinyllama" + networks: + - bankapp-net + + bankapp: + build: . + container_name: bankapp + ports: + - "8080:8080" + environment: + MYSQL_HOST: mysql + MYSQL_PORT: 3306 + MYSQL_DATABASE: bankappdb + MYSQL_USER: root + MYSQL_PASSWORD: Test@123 + OLLAMA_URL: http://ollama:11434 + depends_on: + mysql: + condition: service_healthy + ollama-pull-model: + condition: service_completed_successfully + networks: + - bankapp-net + +volumes: + mysql-data: + ollama-data: + +networks: + bankapp-net: + driver: bridge diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/.helmignore b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/Chart.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/Chart.yaml new file mode 100644 index 0000000000..044c6b7bbf --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/Chart.yaml @@ -0,0 +1,15 @@ +apiVersion: v2 +name: bankapp +description: AI-BankApp -- Spring Boot banking application with MySQL and Ollama AI chatbot +type: application +version: 0.1.0 +appVersion: "1.0.0" +maintainers: + - name: TrainWithShubham + url: https://github.com/TrainWithShubham +keywords: + - bankapp + - spring-boot + - mysql + - ollama + - ai diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/NOTES.txt b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/NOTES.txt new file mode 100644 index 0000000000..354012b3f2 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/NOTES.txt @@ -0,0 +1,9 @@ +AI-BankApp Helm Release + +Release Name: {{ .Release.Name }} +Namespace: {{ .Release.Namespace }} + +BankApp Service: +{{ include "bankapp.fullname" . }}-service + +Thank you for installing AI-BankApp. diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/_helpers.tpl b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/_helpers.tpl new file mode 100644 index 0000000000..375881a9b4 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "bankapp.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "bankapp.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "bankapp.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "bankapp.labels" -}} +helm.sh/chart: {{ include "bankapp.chart" . }} +{{ include "bankapp.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "bankapp.selectorLabels" -}} +app.kubernetes.io/name: {{ include "bankapp.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "bankapp.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "bankapp.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/bankapp-deployment.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/bankapp-deployment.yaml new file mode 100644 index 0000000000..1fd777c8bc --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/bankapp-deployment.yaml @@ -0,0 +1,62 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "bankapp.fullname" . }} + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + {{- if not .Values.bankapp.autoscaling.enabled }} + replicas: {{ .Values.bankapp.replicaCount }} + {{- end }} + selector: + matchLabels: + app: {{ include "bankapp.fullname" . }} + template: + metadata: + labels: + app: {{ include "bankapp.fullname" . }} + spec: + initContainers: + - name: wait-for-mysql + image: busybox:1.36 + command: ["/bin/sh", "-c", "until nc -z {{ include "bankapp.fullname" . }}-mysql 3306; do sleep 2; done"] + resources: + requests: { memory: "32Mi", cpu: "50m" } + limits: { memory: "64Mi", cpu: "100m" } + {{- if .Values.ollama.enabled }} + - name: wait-for-ollama + image: busybox:1.36 + command: ["/bin/sh", "-c", "until nc -z {{ include "bankapp.fullname" . }}-ollama 11434; do sleep 2; done"] + resources: + requests: { memory: "32Mi", cpu: "50m" } + limits: { memory: "64Mi", cpu: "100m" } + {{- end }} + containers: + - name: bankapp + image: "{{ .Values.bankapp.image.repository }}:{{ .Values.bankapp.image.tag }}" + imagePullPolicy: {{ .Values.bankapp.image.pullPolicy }} + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: {{ include "bankapp.fullname" . }}-config + - secretRef: + name: {{ include "bankapp.fullname" . }}-secret + {{- with .Values.bankapp.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + readinessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 30 + failureThreshold: 15 + livenessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 5 diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/configmap.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/configmap.yaml new file mode 100644 index 0000000000..27ea8f3934 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/configmap.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "bankapp.fullname" . }}-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +data: + MYSQL_HOST: {{ include "bankapp.fullname" . }}-mysql + MYSQL_PORT: "3306" + MYSQL_DATABASE: {{ .Values.config.mysqlDatabase | quote }} + OLLAMA_URL: {{ default (printf "http://%s-ollama:11434" (include "bankapp.fullname" .)) .Values.config.ollamaUrl | quote }} + SERVER_FORWARD_HEADERS_STRATEGY: "native" diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/hpa.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/hpa.yaml new file mode 100644 index 0000000000..c3e149e5d9 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/hpa.yaml @@ -0,0 +1,36 @@ +{{- if .Values.bankapp.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "bankapp.fullname" . }}-hpa + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "bankapp.fullname" . }} + minReplicas: {{ .Values.bankapp.autoscaling.minReplicas }} + maxReplicas: {{ .Values.bankapp.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.bankapp.autoscaling.targetCPUUtilization }} + behavior: + scaleUp: + stabilizationWindowSeconds: 30 + policies: + - type: Pods + value: 2 + periodSeconds: 60 + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Pods + value: 1 + periodSeconds: 60 +{{- end }} diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/mysql-deployment.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/mysql-deployment.yaml new file mode 100644 index 0000000000..54e7702659 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/mysql-deployment.yaml @@ -0,0 +1,58 @@ +{{- if .Values.mysql.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "bankapp.fullname" . }}-mysql + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + app: {{ include "bankapp.fullname" . }}-mysql + strategy: + type: Recreate + template: + metadata: + labels: + app: {{ include "bankapp.fullname" . }}-mysql + spec: + containers: + - name: mysql + image: "{{ .Values.mysql.image.repository }}:{{ .Values.mysql.image.tag }}" + ports: + - containerPort: 3306 + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "bankapp.fullname" . }}-secret + key: MYSQL_ROOT_PASSWORD + - name: MYSQL_DATABASE + valueFrom: + configMapKeyRef: + name: {{ include "bankapp.fullname" . }}-config + key: MYSQL_DATABASE + {{- with .Values.mysql.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: mysql-storage + mountPath: /var/lib/mysql + readinessProbe: + exec: + command: ["mysqladmin", "ping", "-h", "localhost"] + initialDelaySeconds: 15 + failureThreshold: 10 + livenessProbe: + exec: + command: ["mysqladmin", "ping", "-h", "localhost"] + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 5 + volumes: + - name: mysql-storage + persistentVolumeClaim: + claimName: {{ include "bankapp.fullname" . }}-mysql-pvc +{{- end }} diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/ollama-deployment.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/ollama-deployment.yaml new file mode 100644 index 0000000000..1b0b65bf12 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/ollama-deployment.yaml @@ -0,0 +1,57 @@ +{{- if .Values.ollama.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "bankapp.fullname" . }}-ollama + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + app: {{ include "bankapp.fullname" . }}-ollama + strategy: + type: Recreate + template: + metadata: + labels: + app: {{ include "bankapp.fullname" . }}-ollama + spec: + containers: + - name: ollama + image: "{{ .Values.ollama.image.repository }}:{{ .Values.ollama.image.tag }}" + ports: + - containerPort: 11434 + {{- with .Values.ollama.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + volumeMounts: + - name: ollama-storage + mountPath: /root/.ollama + lifecycle: + postStart: + exec: + command: + - /bin/sh + - -c + - | + until ollama list > /dev/null 2>&1; do sleep 2; done + ollama pull {{ .Values.ollama.model }} + readinessProbe: + exec: + command: ["/bin/sh", "-c", "ollama list | grep -q {{ .Values.ollama.model }}"] + initialDelaySeconds: 30 + failureThreshold: 30 + livenessProbe: + httpGet: + path: / + port: 11434 + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 5 + volumes: + - name: ollama-storage + persistentVolumeClaim: + claimName: {{ include "bankapp.fullname" . }}-ollama-pvc +{{- end }} diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/pre-install-job.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/pre-install-job.yaml new file mode 100644 index 0000000000..228f8255d7 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/pre-install-job.yaml @@ -0,0 +1,32 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "bankapp.fullname" . }}-db-ready + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": before-hook-creation +spec: + template: + spec: + containers: + - name: db-check + image: busybox:1.36 + command: + - /bin/sh + - -c + - | + echo "Waiting for MySQL to be ready..." + until nc -z {{ include "bankapp.fullname" . }}-mysql 3306; do + echo "MySQL not ready, retrying in 3s..." + sleep 3 + done + echo "MySQL is ready!" + resources: + requests: { memory: "32Mi", cpu: "50m" } + limits: { memory: "64Mi", cpu: "100m" } + restartPolicy: Never + backoffLimit: 10 diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/secret.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/secret.yaml new file mode 100644 index 0000000000..d03135ad23 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "bankapp.fullname" . }}-secret + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +type: Opaque +data: + MYSQL_ROOT_PASSWORD: {{ .Values.secrets.mysqlRootPassword | b64enc | quote }} + MYSQL_USER: {{ .Values.secrets.mysqlUser | b64enc | quote }} + MYSQL_PASSWORD: {{ .Values.secrets.mysqlPassword | b64enc | quote }} diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/services.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/services.yaml new file mode 100644 index 0000000000..6c88f468f4 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/services.yaml @@ -0,0 +1,40 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "bankapp.fullname" . }}-mysql + namespace: {{ .Release.Namespace }} +spec: + selector: + app: {{ include "bankapp.fullname" . }}-mysql + ports: + - port: 3306 +--- +{{- if .Values.ollama.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "bankapp.fullname" . }}-ollama + namespace: {{ .Release.Namespace }} +spec: + selector: + app: {{ include "bankapp.fullname" . }}-ollama + ports: + - port: 11434 +{{- end }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "bankapp.fullname" . }}-service + namespace: {{ .Release.Namespace }} +spec: + type: {{ .Values.bankapp.service.type }} + sessionAffinity: ClientIP + sessionAffinityConfig: + clientIP: + timeoutSeconds: 3600 + selector: + app: {{ include "bankapp.fullname" . }} + ports: + - port: {{ .Values.bankapp.service.port }} + targetPort: 8080 diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/storage.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/storage.yaml new file mode 100644 index 0000000000..f34725a46c --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/storage.yaml @@ -0,0 +1,47 @@ +{{- if .Values.storageClass.create }} +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: {{ .Values.storageClass.name }} +provisioner: {{ .Values.storageClass.provisioner }} +parameters: + type: gp3 + fsType: ext4 +reclaimPolicy: Delete +volumeBindingMode: WaitForFirstConsumer +allowVolumeExpansion: true +{{- end }} +--- +{{- if .Values.mysql.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "bankapp.fullname" . }}-mysql-pvc + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + storageClassName: {{ .Values.mysql.persistence.storageClass }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.mysql.persistence.size }} +{{- end }} +--- +{{- if .Values.ollama.enabled }} +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "bankapp.fullname" . }}-ollama-pvc + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} +spec: + storageClassName: {{ .Values.ollama.persistence.storageClass }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.ollama.persistence.size }} +{{- end }} diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/tests/test-connection.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..3d45ac8930 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "bankapp.fullname" . }}-test + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: test + image: busybox:1.36 + command: ['sh', '-c', 'wget -qO- http://{{ include "bankapp.fullname" . }}-service:8080/actuator/health'] + restartPolicy: Never diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/values-dev.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/values-dev.yaml new file mode 100644 index 0000000000..3a31675f06 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/values-dev.yaml @@ -0,0 +1,45 @@ +bankapp: + replicaCount: 1 + image: + repository: trainwithshubham/ai-bankapp-eks + tag: "latest" + pullPolicy: Always + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "250m" + autoscaling: + enabled: false + +mysql: + enabled: true + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "250m" + persistence: + size: 2Gi + storageClass: standard + +ollama: + enabled: true + model: tinyllama + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "1.5Gi" + cpu: "1000m" + persistence: + size: 5Gi + storageClass: standard + +storageClass: + create: false diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/values-prod.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/values-prod.yaml new file mode 100644 index 0000000000..40370545b9 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/values-prod.yaml @@ -0,0 +1,56 @@ +bankapp: + replicaCount: 4 + image: + repository: trainwithshubham/ai-bankapp-eks + tag: "v1.2.0" + pullPolicy: IfNotPresent + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 4 + targetCPUUtilization: 70 + +mysql: + enabled: true + resources: + requests: + memory: "512Mi" + cpu: "500m" + limits: + memory: "1Gi" + cpu: "1000m" + persistence: + size: 20Gi + storageClass: gp3 + +ollama: + enabled: true + model: tinyllama + resources: + requests: + memory: "2Gi" + cpu: "900m" + limits: + memory: "2.5Gi" + cpu: "1500m" + persistence: + size: 10Gi + storageClass: gp3 + +secrets: + mysqlRootPassword: ProdSecure@789 + mysqlUser: root + mysqlPassword: ProdSecure@789 + +storageClass: + create: true + +gateway: + enabled: true diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/values-staging.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/values-staging.yaml new file mode 100644 index 0000000000..ef27c1892d --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/values-staging.yaml @@ -0,0 +1,46 @@ +bankapp: + replicaCount: 2 + image: + repository: trainwithshubham/ai-bankapp-eks + tag: "v1.2.0" + pullPolicy: IfNotPresent + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 3 + targetCPUUtilization: 75 + +mysql: + enabled: true + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + persistence: + size: 5Gi + storageClass: gp3 + +ollama: + enabled: true + model: tinyllama + persistence: + size: 10Gi + storageClass: gp3 + +secrets: + mysqlRootPassword: StagingPass@456 + mysqlUser: root + mysqlPassword: StagingPass@456 + +storageClass: + create: true diff --git a/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/values.yaml b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/values.yaml new file mode 100644 index 0000000000..5dd2fbcd3f --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/helm-chart/bankapp/values.yaml @@ -0,0 +1,81 @@ +# BankApp configuration +bankapp: + replicaCount: 4 + image: + repository: trainwithshubham/ai-bankapp-eks + tag: "latest" + pullPolicy: Always + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + service: + type: ClusterIP + port: 8080 + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 4 + targetCPUUtilization: 70 + +# MySQL configuration +mysql: + enabled: true + image: + repository: mysql + tag: "8.0" + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + persistence: + size: 5Gi + storageClass: gp3 + +# Ollama AI configuration +ollama: + enabled: true + image: + repository: ollama/ollama + tag: "latest" + model: tinyllama + resources: + requests: + memory: "2Gi" + cpu: "900m" + limits: + memory: "2.5Gi" + cpu: "1500m" + persistence: + size: 10Gi + storageClass: gp3 + +# Shared configuration +config: + mysqlDatabase: bankappdb + ollamaUrl: "" + +# Secrets +secrets: + mysqlRootPassword: Test@123 + mysqlUser: root + mysqlPassword: Test@123 + +# Storage +storageClass: + create: true + name: gp3 + provisioner: ebs.csi.aws.com + +# Gateway +gateway: + enabled: false + hostname: "" + tls: + enabled: false diff --git a/2026/day-80/AI-BankApp-DevOps/k8s/bankapp-deployment.yml b/2026/day-80/AI-BankApp-DevOps/k8s/bankapp-deployment.yml new file mode 100644 index 0000000000..11b0ee6e1b --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/k8s/bankapp-deployment.yml @@ -0,0 +1,67 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bankapp + namespace: bankapp +spec: + replicas: 4 + selector: + matchLabels: + app: bankapp + template: + metadata: + labels: + app: bankapp + spec: + initContainers: + - name: wait-for-mysql + image: busybox:1.36 + command: ["/bin/sh", "-c", "until nc -z mysql-service 3306; do sleep 2; done"] + resources: + requests: + memory: "32Mi" + cpu: "50m" + limits: + memory: "64Mi" + cpu: "100m" + - name: wait-for-ollama + image: busybox:1.36 + command: ["/bin/sh", "-c", "until nc -z ollama-service 11434; do sleep 2; done"] + resources: + requests: + memory: "32Mi" + cpu: "50m" + limits: + memory: "64Mi" + cpu: "100m" + containers: + - name: bankapp + image: trainwithshubham/ai-bankapp-eks:1c7cb0e + imagePullPolicy: Always + ports: + - containerPort: 8080 + envFrom: + - configMapRef: + name: bankapp-config + - secretRef: + name: bankapp-secret + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + readinessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 30 + failureThreshold: 15 + livenessProbe: + httpGet: + path: /actuator/health + port: 8080 + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 5 diff --git a/2026/day-80/AI-BankApp-DevOps/k8s/cert-manager.yml b/2026/day-80/AI-BankApp-DevOps/k8s/cert-manager.yml new file mode 100644 index 0000000000..64aa7f0f2e --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/k8s/cert-manager.yml @@ -0,0 +1,22 @@ +# cert-manager ClusterIssuer for Let's Encrypt TLS certificates. +# Uses HTTP-01 validation via Gateway API — cert-manager creates a temporary +# HTTPRoute to serve the ACME challenge on port 80. +# Prerequisites: cert-manager installed with --set config.enableGatewayAPI=true +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: mentor@trainwithshubham.com + privateKeySecretRef: + name: letsencrypt-account-key + solvers: + - http01: + gatewayHTTPRoute: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: bankapp-gateway + namespace: bankapp diff --git a/2026/day-80/AI-BankApp-DevOps/k8s/configmap.yml b/2026/day-80/AI-BankApp-DevOps/k8s/configmap.yml new file mode 100644 index 0000000000..28f7a16020 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/k8s/configmap.yml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: bankapp-config + namespace: bankapp +data: + MYSQL_HOST: mysql-service + MYSQL_PORT: "3306" + MYSQL_DATABASE: bankappdb + OLLAMA_URL: http://ollama-service:11434 + SERVER_FORWARD_HEADERS_STRATEGY: native diff --git a/2026/day-80/AI-BankApp-DevOps/k8s/gateway.yml b/2026/day-80/AI-BankApp-DevOps/k8s/gateway.yml new file mode 100644 index 0000000000..ed74cf25e7 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/k8s/gateway.yml @@ -0,0 +1,94 @@ +# Requires: Gateway API CRDs + Envoy Gateway installed on the cluster +# On EKS, Envoy Gateway creates an AWS NLB for external access automatically. +# Install steps documented in terraform/README.md +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: envoy-gateway +spec: + controllerName: gateway.envoyproxy.io/gatewayclass-controller +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: bankapp-gateway + namespace: bankapp + annotations: + cert-manager.io/cluster-issuer: letsencrypt-prod +spec: + gatewayClassName: envoy-gateway + listeners: + # HTTP — needed for cert-manager ACME HTTP-01 challenge validation. + # cert-manager creates a temporary HTTPRoute in its own namespace to serve + # the challenge, so allowedRoutes must permit all namespaces. + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All + # HTTPS — TLS terminated at Envoy Gateway using cert-manager certificate + - name: https + protocol: HTTPS + port: 443 + hostname: 52.43.73.130.nip.io + tls: + mode: Terminate + certificateRefs: + - group: "" + kind: Secret + name: bankapp-tls + allowedRoutes: + namespaces: + from: Same +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: bankapp-route + namespace: bankapp +spec: + hostnames: + - 52.43.73.130.nip.io + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: bankapp-gateway + sectionName: https + - group: gateway.networking.k8s.io + kind: Gateway + name: bankapp-gateway + sectionName: http + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - group: "" + kind: Service + name: bankapp-service + port: 8080 + weight: 1 +--- +# Session persistence: Envoy Gateway bypasses K8s Service sessionAffinity and +# load-balances directly to pod endpoints. Without this policy, requests from +# the same browser can hit different pods, breaking in-memory sessions (CSRF +# tokens, login state). Cookie-based consistent hashing pins a browser to one pod. +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: BackendTrafficPolicy +metadata: + name: bankapp-session + namespace: bankapp +spec: + targetRefs: + - group: gateway.networking.k8s.io + kind: HTTPRoute + name: bankapp-route + loadBalancer: + type: ConsistentHash + consistentHash: + type: Cookie + cookie: + name: BANKAPP_AFFINITY + ttl: 3600s diff --git a/2026/day-80/AI-BankApp-DevOps/k8s/hpa.yml b/2026/day-80/AI-BankApp-DevOps/k8s/hpa.yml new file mode 100644 index 0000000000..a9e0b0eb50 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/k8s/hpa.yml @@ -0,0 +1,37 @@ +# ─── Horizontal Pod Autoscaler — BankApp ───────────────────────── +# Requires metrics-server to be installed in the cluster. +# Sized for t3.medium (2 vCPU / 4 GiB): max 4 replicas keeps +# total workload requests under node capacity. +# ───────────────────────────────────────────────────────────────── +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: bankapp-hpa + namespace: bankapp +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: bankapp + minReplicas: 2 + maxReplicas: 4 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 + behavior: + scaleUp: + stabilizationWindowSeconds: 30 + policies: + - type: Pods + value: 2 + periodSeconds: 60 + scaleDown: + stabilizationWindowSeconds: 300 + policies: + - type: Pods + value: 1 + periodSeconds: 60 diff --git a/2026/day-80/AI-BankApp-DevOps/k8s/mysql-deployment.yml b/2026/day-80/AI-BankApp-DevOps/k8s/mysql-deployment.yml new file mode 100644 index 0000000000..f5de98f330 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/k8s/mysql-deployment.yml @@ -0,0 +1,57 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mysql + namespace: bankapp +spec: + selector: + matchLabels: + app: mysql + strategy: + type: Recreate + template: + metadata: + labels: + app: mysql + spec: + containers: + - name: mysql + image: mysql:8.0 + ports: + - containerPort: 3306 + env: + - name: MYSQL_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: bankapp-secret + key: MYSQL_ROOT_PASSWORD + - name: MYSQL_DATABASE + valueFrom: + configMapKeyRef: + name: bankapp-config + key: MYSQL_DATABASE + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + volumeMounts: + - name: mysql-storage + mountPath: /var/lib/mysql + readinessProbe: + exec: + command: ["mysqladmin", "ping", "-h", "localhost"] + initialDelaySeconds: 15 + failureThreshold: 10 + livenessProbe: + exec: + command: ["mysqladmin", "ping", "-h", "localhost"] + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 5 + volumes: + - name: mysql-storage + persistentVolumeClaim: + claimName: mysql-pvc diff --git a/2026/day-80/AI-BankApp-DevOps/k8s/namespace.yml b/2026/day-80/AI-BankApp-DevOps/k8s/namespace.yml new file mode 100644 index 0000000000..0ea3fd2d9e --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/k8s/namespace.yml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: bankapp diff --git a/2026/day-80/AI-BankApp-DevOps/k8s/ollama-deployment.yml b/2026/day-80/AI-BankApp-DevOps/k8s/ollama-deployment.yml new file mode 100644 index 0000000000..b64e286cfe --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/k8s/ollama-deployment.yml @@ -0,0 +1,56 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ollama + namespace: bankapp +spec: + selector: + matchLabels: + app: ollama + strategy: + type: Recreate + template: + metadata: + labels: + app: ollama + spec: + containers: + - name: ollama + image: ollama/ollama:latest + ports: + - containerPort: 11434 + resources: + requests: + memory: "2Gi" + cpu: "900m" + limits: + memory: "2.5Gi" + cpu: "1500m" + volumeMounts: + - name: ollama-storage + mountPath: /root/.ollama + lifecycle: + postStart: + exec: + command: + - /bin/sh + - -c + - | + until ollama list > /dev/null 2>&1; do sleep 2; done + ollama pull tinyllama + readinessProbe: + exec: + command: ["/bin/sh", "-c", "ollama list | grep -q tinyllama"] + initialDelaySeconds: 30 + failureThreshold: 30 + livenessProbe: + httpGet: + path: / + port: 11434 + initialDelaySeconds: 60 + periodSeconds: 10 + failureThreshold: 5 + volumes: + - name: ollama-storage + persistentVolumeClaim: + claimName: ollama-pvc diff --git a/2026/day-80/AI-BankApp-DevOps/k8s/pv.yml b/2026/day-80/AI-BankApp-DevOps/k8s/pv.yml new file mode 100644 index 0000000000..095b06bf8c --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/k8s/pv.yml @@ -0,0 +1,16 @@ +# EKS uses dynamic provisioning via EBS CSI Driver. +# No manual PersistentVolumes needed — PVCs reference the StorageClass directly. +# This file defines the gp3 StorageClass for EKS. +# NOTE: Not set as default to avoid conflict with EKS built-in gp2 default. +# PVCs explicitly reference storageClassName: gp3 +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: gp3 +provisioner: ebs.csi.aws.com +parameters: + type: gp3 + fsType: ext4 +reclaimPolicy: Delete +volumeBindingMode: WaitForFirstConsumer +allowVolumeExpansion: true diff --git a/2026/day-80/AI-BankApp-DevOps/k8s/pvc.yml b/2026/day-80/AI-BankApp-DevOps/k8s/pvc.yml new file mode 100644 index 0000000000..fbcaa1e425 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/k8s/pvc.yml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mysql-pvc + namespace: bankapp +spec: + storageClassName: gp3 + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 5Gi +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: ollama-pvc + namespace: bankapp +spec: + storageClassName: gp3 + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi diff --git a/2026/day-80/AI-BankApp-DevOps/k8s/secrets.yml b/2026/day-80/AI-BankApp-DevOps/k8s/secrets.yml new file mode 100644 index 0000000000..4edf117533 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/k8s/secrets.yml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: bankapp-secret + namespace: bankapp +type: Opaque +data: + MYSQL_ROOT_PASSWORD: VGVzdEAxMjM= # Test@123 + MYSQL_USER: cm9vdA== # root + MYSQL_PASSWORD: VGVzdEAxMjM= # Test@123 diff --git a/2026/day-80/AI-BankApp-DevOps/k8s/service.yml b/2026/day-80/AI-BankApp-DevOps/k8s/service.yml new file mode 100644 index 0000000000..36d04255c3 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/k8s/service.yml @@ -0,0 +1,38 @@ +apiVersion: v1 +kind: Service +metadata: + name: mysql-service + namespace: bankapp +spec: + selector: + app: mysql + ports: + - port: 3306 +--- +apiVersion: v1 +kind: Service +metadata: + name: ollama-service + namespace: bankapp +spec: + selector: + app: ollama + ports: + - port: 11434 +--- +apiVersion: v1 +kind: Service +metadata: + name: bankapp-service + namespace: bankapp +spec: + type: ClusterIP + sessionAffinity: ClientIP + sessionAffinityConfig: + clientIP: + timeoutSeconds: 3600 + selector: + app: bankapp + ports: + - port: 8080 + targetPort: 8080 diff --git a/2026/day-80/AI-BankApp-DevOps/mvnw b/2026/day-80/AI-BankApp-DevOps/mvnw new file mode 100644 index 0000000000..d7c358e5a2 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/mvnw @@ -0,0 +1,259 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.3.2 +# +# Optional ENV vars +# ----------------- +# JAVA_HOME - location of a JDK home dir, required when download maven via java source +# MVNW_REPOURL - repo url base for downloading maven distribution +# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output +# ---------------------------------------------------------------------------- + +set -euf +[ "${MVNW_VERBOSE-}" != debug ] || set -x + +# OS specific support. +native_path() { printf %s\\n "$1"; } +case "$(uname)" in +CYGWIN* | MINGW*) + [ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")" + native_path() { cygpath --path --windows "$1"; } + ;; +esac + +# set JAVACMD and JAVACCMD +set_java_home() { + # For Cygwin and MinGW, ensure paths are in Unix format before anything is touched + if [ -n "${JAVA_HOME-}" ]; then + if [ -x "$JAVA_HOME/jre/sh/java" ]; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACCMD="$JAVA_HOME/jre/sh/javac" + else + JAVACMD="$JAVA_HOME/bin/java" + JAVACCMD="$JAVA_HOME/bin/javac" + + if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then + echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2 + echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2 + return 1 + fi + fi + else + JAVACMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v java + )" || : + JAVACCMD="$( + 'set' +e + 'unset' -f command 2>/dev/null + 'command' -v javac + )" || : + + if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then + echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2 + return 1 + fi + fi +} + +# hash string like Java String::hashCode +hash_string() { + str="${1:-}" h=0 + while [ -n "$str" ]; do + char="${str%"${str#?}"}" + h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296)) + str="${str#?}" + done + printf %x\\n $h +} + +verbose() { :; } +[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; } + +die() { + printf %s\\n "$1" >&2 + exit 1 +} + +trim() { + # MWRAPPER-139: + # Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds. + # Needed for removing poorly interpreted newline sequences when running in more + # exotic environments such as mingw bash on Windows. + printf "%s" "${1}" | tr -d '[:space:]' +} + +# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties +while IFS="=" read -r key value; do + case "${key-}" in + distributionUrl) distributionUrl=$(trim "${value-}") ;; + distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;; + esac +done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties" + +case "${distributionUrl##*/}" in +maven-mvnd-*bin.*) + MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ + case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in + *AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;; + :Darwin*x86_64) distributionPlatform=darwin-amd64 ;; + :Darwin*arm64) distributionPlatform=darwin-aarch64 ;; + :Linux*x86_64*) distributionPlatform=linux-amd64 ;; + *) + echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2 + distributionPlatform=linux-amd64 + ;; + esac + distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip" + ;; +maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;; +*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/2026/day-80/AI-BankApp-DevOps/mvnw.cmd b/2026/day-80/AI-BankApp-DevOps/mvnw.cmd new file mode 100644 index 0000000000..6f779cff20 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/mvnw.cmd @@ -0,0 +1,149 @@ +<# : batch portion +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Apache Maven Wrapper startup batch script, version 3.3.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } + $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" +} +$distributionUrlName = $distributionUrl -replace '^.*/','' +$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' +$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' +$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" + +if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { + Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" + Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" + exit $? +} + +if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { + Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" +} + +# prepare tmp dir +$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile +$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" +$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null +trap { + if ($TMP_DOWNLOAD_DIR.Exists) { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } + } +} + +New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null + +# Download and Install Apache Maven +Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +Write-Verbose "Downloading from: $distributionUrl" +Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +$webclient = New-Object System.Net.WebClient +if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { + $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) +} +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum +if ($distributionSha256Sum) { + if ($USE_MVND) { + Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." + } + Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash + if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { + Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." + } +} + +# unzip and move +Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/2026/day-80/AI-BankApp-DevOps/mysql-values.yaml b/2026/day-80/AI-BankApp-DevOps/mysql-values.yaml new file mode 100644 index 0000000000..542e574bcd --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/mysql-values.yaml @@ -0,0 +1,20 @@ +auth: + rootPassword: Test@123 + database: bankappdb + +primary: + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 250m + memory: 256Mi + persistence: + size: 5Gi + storageClass: "" + +metrics: + enabled: true + serviceMonitor: + enabled: false diff --git a/2026/day-80/AI-BankApp-DevOps/pom.xml b/2026/day-80/AI-BankApp-DevOps/pom.xml new file mode 100644 index 0000000000..ebe89b78d8 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/pom.xml @@ -0,0 +1,96 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.13 + + + com.example + bankapp + 0.0.1-SNAPSHOT + bankapp + Banking Web Application + + + + + + + + + + + + + + + 21 + + 10.1.52 + + 2.18.6 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-validation + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity6 + + + io.micrometer + micrometer-registry-prometheus + + + + com.mysql + mysql-connector-j + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/2026/day-80/AI-BankApp-DevOps/screenshots/ai-chatbot.png b/2026/day-80/AI-BankApp-DevOps/screenshots/ai-chatbot.png new file mode 100644 index 0000000000..9a472bd6fe Binary files /dev/null and b/2026/day-80/AI-BankApp-DevOps/screenshots/ai-chatbot.png differ diff --git a/2026/day-80/AI-BankApp-DevOps/screenshots/dashboard.png b/2026/day-80/AI-BankApp-DevOps/screenshots/dashboard.png new file mode 100644 index 0000000000..b0c823a7ac Binary files /dev/null and b/2026/day-80/AI-BankApp-DevOps/screenshots/dashboard.png differ diff --git a/2026/day-80/AI-BankApp-DevOps/screenshots/gitops-pipeline.gif b/2026/day-80/AI-BankApp-DevOps/screenshots/gitops-pipeline.gif new file mode 100644 index 0000000000..ffc9cb4695 Binary files /dev/null and b/2026/day-80/AI-BankApp-DevOps/screenshots/gitops-pipeline.gif differ diff --git a/2026/day-80/AI-BankApp-DevOps/setup-k8s/README.md b/2026/day-80/AI-BankApp-DevOps/setup-k8s/README.md new file mode 100644 index 0000000000..a5f29a2b43 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/setup-k8s/README.md @@ -0,0 +1,154 @@ +# Kind Cluster Setup + +## Prerequisites + +Install these three tools if you don't have them already. + +**Docker** — Kind runs clusters inside Docker containers. + +```bash +# macOS +brew install --cask docker + +# Ubuntu/Debian +sudo apt-get update && sudo apt-get install -y docker.io +sudo usermod -aG docker $USER && newgrp docker +``` + +**kubectl** — CLI to talk to your cluster. + +```bash +# macOS +brew install kubectl + +# Linux +curl -LO "https://dl.k8s.io/release/$(curl -sL https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" +chmod +x kubectl && sudo mv kubectl /usr/local/bin/ +``` + +**Kind** — runs K8s clusters locally using Docker. + +```bash +# macOS +brew install kind + +# Linux +curl -Lo ./kind https://kind.sigs.k8s.io/dl/latest/kind-linux-amd64 +chmod +x kind && sudo mv kind /usr/local/bin/ +``` + +## 1. Build the BankApp image + +```bash +docker build -t trainwithshubham/bankapp:k8s . +``` + +## 2. Create the cluster + +```bash +kind create cluster --config setup-k8s/kind-config.yml +``` + +## 3. Load the image into Kind + +Kind can't pull from your local Docker daemon — you need to explicitly load images into the cluster. + +```bash +kind load docker-image trainwithshubham/bankapp:k8s --name tws-cluster +``` + +## 4. Install metrics-server + +HPA needs real CPU/memory numbers. Without this, `kubectl top` and autoscaling won't work. + +```bash +kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/latest/download/components.yaml +``` + +Kind uses self-signed certs, so metrics-server can't verify kubelet TLS — patch it: + +```bash +kubectl patch deployment metrics-server -n kube-system \ + --type='json' \ + -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]' +``` + +Wait for it to be ready: + +```bash +kubectl rollout status deployment/metrics-server -n kube-system --timeout=120s +``` + +## 5. Install Kubernetes Dashboard + +```bash +kubectl apply -f https://raw.githubusercontent.com/kubernetes/dashboard/v2.7.0/aio/deploy/recommended.yaml +kubectl apply -f k8s/dashboard-admin.yml +``` + +Generate a login token: + +```bash +kubectl -n kubernetes-dashboard create token dashboard-admin +``` + +Start the proxy and open the dashboard: + +```bash +kubectl proxy +``` + +``` +http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ +``` + +Select **Token**, paste the token, and sign in. + +## 6. Apply manifests + +Order matters — namespace and config first, then storage, then workloads. + +```bash +kubectl apply -f k8s/namespace.yml +kubectl apply -f k8s/configmap.yml +kubectl apply -f k8s/secrets.yml +kubectl apply -f k8s/pv.yml +kubectl apply -f k8s/pvc.yml +kubectl apply -f k8s/mysql-deployment.yml +kubectl apply -f k8s/ollama-deployment.yml +kubectl apply -f k8s/bankapp-deployment.yml +kubectl apply -f k8s/service.yml +kubectl apply -f k8s/hpa.yml +``` + +## 7. Verify + +```bash +kubectl get all -n bankapp +kubectl get hpa -n bankapp # wait ~60s for metrics to populate +kubectl top pods -n bankapp +``` + +App will be at **http://localhost:8080** once pods are ready. + +## 8. Test HPA (autoscaling) + +Generate CPU load on bankapp to trigger scale-up: + +```bash +kubectl run load-test --image=busybox:1.36 -n bankapp -- sh -c "while true; do wget -q -O- http://bankapp-service:8080/actuator/health >/dev/null 2>&1; done" +``` + +Watch replicas scale from 2 → 4 (takes ~1-2 min for metrics to kick in): + +```bash +kubectl get hpa -n bankapp -w +``` + +Clean up when done: + +```bash +kubectl delete pod load-test -n bankapp +``` + +Scale-down happens automatically after 5 minutes of low CPU. diff --git a/2026/day-80/AI-BankApp-DevOps/setup-k8s/kind-config.yml b/2026/day-80/AI-BankApp-DevOps/setup-k8s/kind-config.yml new file mode 100644 index 0000000000..eeadbdd7cc --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/setup-k8s/kind-config.yml @@ -0,0 +1,17 @@ +kind: Cluster +apiVersion: kind.x-k8s.io/v1alpha4 +name: tws-cluster + +nodes: +- role: control-plane + image: kindest/node:v1.35.0 + extraPortMappings: + - containerPort: 30080 + hostPort: 8080 + protocol: TCP + +- role: worker + image: kindest/node:v1.35.0 + +- role: worker + image: kindest/node:v1.35.0 \ No newline at end of file diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/BankappApplication.java b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/BankappApplication.java new file mode 100644 index 0000000000..8453398612 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/BankappApplication.java @@ -0,0 +1,13 @@ +package com.example.bankapp; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BankappApplication { + + public static void main(String[] args) { + SpringApplication.run(BankappApplication.class, args); + } + +} diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/config/SecurityConfig.java b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/config/SecurityConfig.java new file mode 100644 index 0000000000..f7c0a8cf4b --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/config/SecurityConfig.java @@ -0,0 +1,40 @@ +package com.example.bankapp.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests(auth -> auth + .requestMatchers("/register", "/login", "/css/**", "/js/**", "/actuator/**").permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .loginPage("/login") + .defaultSuccessUrl("/dashboard", true) + .permitAll() + ) + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/controller/BankController.java b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/controller/BankController.java new file mode 100644 index 0000000000..94ee89230b --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/controller/BankController.java @@ -0,0 +1,86 @@ +package com.example.bankapp.controller; + +import com.example.bankapp.model.Account; +import com.example.bankapp.service.AccountService; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import java.math.BigDecimal; + +@Controller +public class BankController { + + private final AccountService accountService; + + public BankController(AccountService accountService) { + this.accountService = accountService; + } + + @GetMapping("/login") + public String loginPage() { + return "login"; + } + + @GetMapping("/register") + public String registerPage() { + return "register"; + } + + @PostMapping("/register") + public String register(@RequestParam String username, + @RequestParam String password, + Model model) { + if (accountService.registerAccount(username, password)) { + return "redirect:/login?registered"; + } + model.addAttribute("error", true); + return "register"; + } + + @GetMapping("/dashboard") + public String dashboard(@AuthenticationPrincipal Account account, Model model) { + model.addAttribute("account", account); + return "dashboard"; + } + + @PostMapping("/deposit") + public String deposit(@AuthenticationPrincipal Account account, + @RequestParam BigDecimal amount, + RedirectAttributes redirectAttributes) { + accountService.deposit(account, amount); + return "redirect:/dashboard"; + } + + @PostMapping("/withdraw") + public String withdraw(@AuthenticationPrincipal Account account, + @RequestParam BigDecimal amount, + RedirectAttributes redirectAttributes) { + if (!accountService.withdraw(account, amount)) { + redirectAttributes.addFlashAttribute("error", "Insufficient funds."); + } + return "redirect:/dashboard"; + } + + @PostMapping("/transfer") + public String transfer(@AuthenticationPrincipal Account account, + @RequestParam String toUsername, + @RequestParam BigDecimal amount, + RedirectAttributes redirectAttributes) { + String error = accountService.transferAmount(account, toUsername, amount); + if (error != null) { + redirectAttributes.addFlashAttribute("error", error); + } + return "redirect:/dashboard"; + } + + @GetMapping("/transactions") + public String transactions(@AuthenticationPrincipal Account account, Model model) { + model.addAttribute("transactions", accountService.getTransactionHistory(account)); + return "transactions"; + } +} diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/controller/ChatController.java b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/controller/ChatController.java new file mode 100644 index 0000000000..df8ba2c43f --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/controller/ChatController.java @@ -0,0 +1,27 @@ +package com.example.bankapp.controller; + +import com.example.bankapp.model.Account; +import com.example.bankapp.service.ChatService; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api") +public class ChatController { + + private final ChatService chatService; + + public ChatController(ChatService chatService) { + this.chatService = chatService; + } + + @PostMapping("/chat") + public Map chat(@AuthenticationPrincipal Account account, + @RequestBody Map request) { + String message = request.getOrDefault("message", ""); + String reply = chatService.chat(account, message); + return Map.of("reply", reply); + } +} diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/model/Account.java b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/model/Account.java new file mode 100644 index 0000000000..a85b5c5c42 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/model/Account.java @@ -0,0 +1,112 @@ +package com.example.bankapp.model; + +import jakarta.persistence.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +@Entity +@Table(name = "accounts") +public class Account implements UserDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String username; + + @Column(nullable = false) + private String password; + + @Column(nullable = false, precision = 19, scale = 2) + private BigDecimal balance = BigDecimal.ZERO; + + @OneToMany(mappedBy = "account", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List transactions = new ArrayList<>(); + + public Account() { + } + + public Account(String username, String password) { + this.username = username; + this.password = password; + this.balance = BigDecimal.ZERO; + } + + // Getters and setters + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public BigDecimal getBalance() { + return balance; + } + + public void setBalance(BigDecimal balance) { + this.balance = balance; + } + + public List getTransactions() { + return transactions; + } + + public void setTransactions(List transactions) { + this.transactions = transactions; + } + + // UserDetails implementation + + @Override + public Collection getAuthorities() { + return List.of(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/model/Transaction.java b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/model/Transaction.java new file mode 100644 index 0000000000..799de0602e --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/model/Transaction.java @@ -0,0 +1,78 @@ +package com.example.bankapp.model; + +import jakarta.persistence.*; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Entity +@Table(name = "transactions") +public class Transaction { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false, precision = 19, scale = 2) + private BigDecimal amount; + + @Column(nullable = false) + private String type; + + @Column(nullable = false) + private LocalDateTime timestamp; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "account_id") + private Account account; + + public Transaction() { + } + + public Transaction(BigDecimal amount, String type, LocalDateTime timestamp, Account account) { + this.amount = amount; + this.type = type; + this.timestamp = timestamp; + this.account = account; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public BigDecimal getAmount() { + return amount; + } + + public void setAmount(BigDecimal amount) { + this.amount = amount; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public Account getAccount() { + return account; + } + + public void setAccount(Account account) { + this.account = account; + } +} diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/repository/AccountRepository.java b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/repository/AccountRepository.java new file mode 100644 index 0000000000..72553370e9 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/repository/AccountRepository.java @@ -0,0 +1,10 @@ +package com.example.bankapp.repository; + +import com.example.bankapp.model.Account; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface AccountRepository extends JpaRepository { + Optional findByUsername(String username); +} diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/repository/TransactionRepository.java b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/repository/TransactionRepository.java new file mode 100644 index 0000000000..8940815105 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/repository/TransactionRepository.java @@ -0,0 +1,10 @@ +package com.example.bankapp.repository; + +import com.example.bankapp.model.Transaction; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TransactionRepository extends JpaRepository { + List findByAccountIdOrderByTimestampDesc(Long accountId); +} diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/service/AccountService.java b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/service/AccountService.java new file mode 100644 index 0000000000..5fbffbf965 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/service/AccountService.java @@ -0,0 +1,98 @@ +package com.example.bankapp.service; + +import com.example.bankapp.model.Account; +import com.example.bankapp.model.Transaction; +import com.example.bankapp.repository.AccountRepository; +import com.example.bankapp.repository.TransactionRepository; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +@Service +public class AccountService implements UserDetailsService { + + private final AccountRepository accountRepository; + private final TransactionRepository transactionRepository; + private final PasswordEncoder passwordEncoder; + + public AccountService(AccountRepository accountRepository, + TransactionRepository transactionRepository, + PasswordEncoder passwordEncoder) { + this.accountRepository = accountRepository; + this.transactionRepository = transactionRepository; + this.passwordEncoder = passwordEncoder; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + return accountRepository.findByUsername(username) + .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); + } + + public boolean registerAccount(String username, String password) { + if (accountRepository.findByUsername(username).isPresent()) { + return false; + } + Account account = new Account(username, passwordEncoder.encode(password)); + accountRepository.save(account); + return true; + } + + @Transactional + public void deposit(Account account, BigDecimal amount) { + account.setBalance(account.getBalance().add(amount)); + accountRepository.save(account); + + Transaction transaction = new Transaction(amount, "Deposit", LocalDateTime.now(), account); + transactionRepository.save(transaction); + } + + @Transactional + public boolean withdraw(Account account, BigDecimal amount) { + if (account.getBalance().compareTo(amount) < 0) { + return false; + } + account.setBalance(account.getBalance().subtract(amount)); + accountRepository.save(account); + + Transaction transaction = new Transaction(amount, "Withdrawal", LocalDateTime.now(), account); + transactionRepository.save(transaction); + return true; + } + + @Transactional + public String transferAmount(Account from, String toUsername, BigDecimal amount) { + if (from.getUsername().equals(toUsername)) { + return "Cannot transfer to yourself."; + } + if (from.getBalance().compareTo(amount) < 0) { + return "Insufficient funds."; + } + Account to = accountRepository.findByUsername(toUsername).orElse(null); + if (to == null) { + return "Recipient not found."; + } + + from.setBalance(from.getBalance().subtract(amount)); + to.setBalance(to.getBalance().add(amount)); + accountRepository.save(from); + accountRepository.save(to); + + LocalDateTime now = LocalDateTime.now(); + transactionRepository.save(new Transaction(amount, "Transfer Out", now, from)); + transactionRepository.save(new Transaction(amount, "Transfer In", now, to)); + + return null; // null means success + } + + public List getTransactionHistory(Account account) { + return transactionRepository.findByAccountIdOrderByTimestampDesc(account.getId()); + } +} diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/service/ChatService.java b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/service/ChatService.java new file mode 100644 index 0000000000..143b00a1d2 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/java/com/example/bankapp/service/ChatService.java @@ -0,0 +1,79 @@ +package com.example.bankapp.service; + +import com.example.bankapp.model.Account; +import com.example.bankapp.model.Transaction; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.List; +import java.util.Map; + +@Service +public class ChatService { + + @Value("${ollama.url}") + private String ollamaUrl; + + @Value("${ollama.model}") + private String model; + + private final RestTemplate restTemplate = new RestTemplate(); + private final AccountService accountService; + + public ChatService(AccountService accountService) { + this.accountService = accountService; + } + + public String chat(Account account, String userMessage) { + List recent = accountService.getTransactionHistory(account); + String context = buildContext(account, recent); + + Map request = Map.of( + "model", model, + "messages", List.of( + Map.of("role", "system", "content", context), + Map.of("role", "user", "content", userMessage) + ), + "stream", false + ); + + try { + Map response = restTemplate.postForObject( + ollamaUrl + "/api/chat", request, Map.class + ); + if (response != null && response.containsKey("message")) { + Map message = (Map) response.get("message"); + return message.get("content"); + } + return "Sorry, I couldn't process that."; + } catch (Exception e) { + return "AI assistant is unavailable. Please make sure Ollama is running."; + } + } + + private String buildContext(Account account, List transactions) { + StringBuilder sb = new StringBuilder(); + sb.append("You are a helpful banking assistant for BankApp. "); + sb.append("Keep answers short and friendly (2-3 sentences max). "); + sb.append("\n\nCustomer details:"); + sb.append("\n- Username: ").append(account.getUsername()); + sb.append("\n- Balance: $").append(account.getBalance()); + sb.append("\n- Account ID: ").append(account.getId()); + + if (!transactions.isEmpty()) { + sb.append("\n\nRecent transactions:"); + int limit = Math.min(transactions.size(), 5); + for (int i = 0; i < limit; i++) { + Transaction t = transactions.get(i); + sb.append("\n- ").append(t.getType()) + .append(": $").append(t.getAmount()) + .append(" on ").append(t.getTimestamp().toLocalDate()); + } + } else { + sb.append("\n\nNo transactions yet."); + } + + return sb.toString(); + } +} diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/resources/application.properties b/2026/day-80/AI-BankApp-DevOps/src/main/resources/application.properties new file mode 100644 index 0000000000..10318ff9e7 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/resources/application.properties @@ -0,0 +1,26 @@ +spring.application.name=bankapp + +# MySQL Database configuration +spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:bankappdb}?useSSL=false&allowPublicKeyRetrieval=true +spring.datasource.username=${MYSQL_USER:root} +spring.datasource.password=${MYSQL_PASSWORD:Test@123} +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver + +# JPA & Hibernate configuration +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect + +# Virtual threads (Java 21) +spring.threads.virtual.enabled=true + +# Actuator - expose health + prometheus for kube-prometheus-stack scraping +management.endpoints.web.exposure.include=health,prometheus +management.endpoint.health.show-details=when-authorized + +# Ollama AI +ollama.url=${OLLAMA_URL:http://localhost:11434} +ollama.model=tinyllama + +# Structured logging +logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/resources/static/css/bankapp.css b/2026/day-80/AI-BankApp-DevOps/src/main/resources/static/css/bankapp.css new file mode 100644 index 0000000000..fd4886e8af --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/resources/static/css/bankapp.css @@ -0,0 +1,639 @@ +/* ===== CSS Custom Properties ===== */ +:root { + --bg-primary: #0a0a0a; + --bg-surface: rgba(255, 255, 255, 0.03); + --text-primary: #f1f5f9; + --text-muted: #94a3b8; + --accent: #3b82f6; + --accent-hover: #2563eb; + --accent-glow: rgba(59, 130, 246, 0.4); + --success: #22c55e; + --danger: #ef4444; + --border-color: rgba(255, 255, 255, 0.08); + --border-radius: 16px; + --border-radius-sm: 10px; + --transition-speed: 0.3s; + --glass-bg: rgba(255, 255, 255, 0.05); + --glass-border: rgba(255, 255, 255, 0.1); + --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + --gradient-bg: linear-gradient(135deg, #0a0a0a 0%, #1a1a2e 50%, #16213e 100%); + --font-stack: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; +} + +[data-theme="light"] { + --bg-primary: #f8fafc; + --bg-surface: rgba(0, 0, 0, 0.02); + --text-primary: #0f172a; + --text-muted: #64748b; + --border-color: rgba(0, 0, 0, 0.08); + --glass-bg: rgba(255, 255, 255, 0.7); + --glass-border: rgba(0, 0, 0, 0.08); + --glass-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); + --gradient-bg: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 50%, #dbeafe 100%); +} + +/* ===== Reset & Base ===== */ +*, *::before, *::after { + box-sizing: border-box; +} + +body { + font-family: var(--font-stack); + background: var(--gradient-bg); + color: var(--text-primary); + margin: 0; + padding: 0; + min-height: 100vh; + transition: background var(--transition-speed), color var(--transition-speed); +} + +a { + color: var(--accent); + text-decoration: none; + transition: color var(--transition-speed); +} + +a:hover { + color: var(--accent-hover); +} + +/* ===== Navbar ===== */ +.navbar-app { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-bottom: 1px solid var(--glass-border); + padding: 0.75rem 1.5rem; + position: sticky; + top: 0; + z-index: 1000; +} + +.navbar-app .navbar-brand { + font-weight: 700; + font-size: 1.25rem; + color: var(--text-primary) !important; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.navbar-app .nav-link { + color: var(--text-muted) !important; + font-weight: 500; + padding: 0.5rem 1rem !important; + border-radius: var(--border-radius-sm); + transition: all var(--transition-speed); +} + +.navbar-app .nav-link:hover, +.navbar-app .nav-link.active { + color: var(--text-primary) !important; + background: var(--bg-surface); +} + +.theme-toggle { + background: var(--glass-bg); + border: 1px solid var(--glass-border); + color: var(--text-primary); + border-radius: 50%; + width: 40px; + height: 40px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all var(--transition-speed); + font-size: 1.1rem; +} + +.theme-toggle:hover { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +/* ===== Glass Card ===== */ +.glass-card { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + border-radius: var(--border-radius); + box-shadow: var(--glass-shadow); + padding: 2rem; + transition: transform var(--transition-speed), box-shadow var(--transition-speed); +} + +.glass-card:hover { + transform: translateY(-2px); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5); +} + +[data-theme="light"] .glass-card:hover { + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15); +} + +/* ===== Auth Pages (Login/Register) ===== */ +.auth-wrapper { + min-height: 100vh; + display: flex; + flex-direction: column; +} + +.auth-container { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + padding: 2rem 1rem; +} + +.auth-card { + width: 100%; + max-width: 440px; + animation: fadeInUp 0.5s ease-out; +} + +.auth-card h2 { + font-weight: 700; + margin-bottom: 0.5rem; +} + +.auth-card .text-muted { + color: var(--text-muted) !important; +} + +/* ===== Form Styles ===== */ +.form-floating > .form-control { + background: var(--bg-surface); + border: 1px solid var(--border-color); + color: var(--text-primary); + border-radius: var(--border-radius-sm); + transition: border-color var(--transition-speed), box-shadow var(--transition-speed); +} + +.form-floating > .form-control:focus { + background: var(--bg-surface); + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); + color: var(--text-primary); +} + +.form-floating > label { + color: var(--text-muted); +} + +.form-floating > .form-control:focus ~ label, +.form-floating > .form-control:not(:placeholder-shown) ~ label { + color: var(--accent); +} + +/* ===== Buttons ===== */ +.btn-accent { + background: var(--accent); + color: #fff; + border: none; + border-radius: var(--border-radius-sm); + padding: 0.75rem 1.5rem; + font-weight: 600; + transition: all var(--transition-speed); +} + +.btn-accent:hover { + background: var(--accent-hover); + color: #fff; + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--accent-glow); +} + +.btn-glass { + background: var(--glass-bg); + color: var(--text-primary); + border: 1px solid var(--glass-border); + border-radius: var(--border-radius-sm); + padding: 0.75rem 1.5rem; + font-weight: 500; + transition: all var(--transition-speed); +} + +.btn-glass:hover { + background: var(--accent); + color: #fff; + border-color: var(--accent); +} + +/* ===== Password Toggle ===== */ +.input-group-password { + position: relative; +} + +.input-group-password .form-control { + padding-right: 3rem; +} + +.password-toggle { + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--text-muted); + cursor: pointer; + z-index: 5; + padding: 0.25rem; +} + +.password-toggle:hover { + color: var(--accent); +} + +/* ===== Dashboard ===== */ +.dashboard-wrapper { + padding: 2rem 1rem; + max-width: 1200px; + margin: 0 auto; +} + +.balance-card { + text-align: center; + padding: 2.5rem 2rem; + margin-bottom: 2rem; +} + +.balance-label { + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-muted); + margin-bottom: 0.5rem; +} + +.balance-amount { + font-size: 3rem; + font-weight: 800; + color: var(--text-primary); + line-height: 1; +} + +.balance-amount .currency { + color: var(--accent); +} + +.account-info { + display: flex; + justify-content: center; + gap: 2rem; + margin-top: 1rem; + color: var(--text-muted); + font-size: 0.875rem; +} + +.action-cards { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.action-card { + padding: 1.5rem; +} + +.action-card h5 { + font-weight: 600; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.action-card .form-control { + background: var(--bg-surface); + border: 1px solid var(--border-color); + color: var(--text-primary); + border-radius: var(--border-radius-sm); + padding: 0.75rem 1rem; +} + +.action-card .form-control:focus { + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-glow); +} + +.action-card .form-label { + color: var(--text-muted); + font-size: 0.875rem; + font-weight: 500; +} + +/* ===== Transactions Table ===== */ +.transactions-wrapper { + padding: 2rem 1rem; + max-width: 900px; + margin: 0 auto; +} + +.table-glass { + width: 100%; + border-collapse: separate; + border-spacing: 0; +} + +.table-glass thead th { + background: var(--bg-surface); + color: var(--text-muted); + font-weight: 600; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + padding: 1rem; + border-bottom: 1px solid var(--border-color); +} + +.table-glass tbody td { + padding: 1rem; + border-bottom: 1px solid var(--border-color); + color: var(--text-primary); + vertical-align: middle; +} + +.table-glass tbody tr { + transition: background var(--transition-speed); +} + +.table-glass tbody tr:hover { + background: var(--bg-surface); +} + +.amount-positive { + color: var(--success); + font-weight: 600; +} + +.amount-negative { + color: var(--danger); + font-weight: 600; +} + +.badge-type { + display: inline-block; + padding: 0.25rem 0.75rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; +} + +.badge-deposit { + background: rgba(34, 197, 94, 0.15); + color: var(--success); +} + +.badge-withdrawal { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); +} + +.badge-transfer-out { + background: rgba(239, 68, 68, 0.15); + color: var(--danger); +} + +.badge-transfer-in { + background: rgba(34, 197, 94, 0.15); + color: var(--success); +} + +.empty-state { + text-align: center; + padding: 4rem 2rem; + color: var(--text-muted); +} + +.empty-state i { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.5; +} + +/* ===== Alert Styles ===== */ +.alert-glass { + background: rgba(239, 68, 68, 0.1); + border: 1px solid rgba(239, 68, 68, 0.3); + color: var(--danger); + border-radius: var(--border-radius-sm); + padding: 0.75rem 1rem; +} + +.alert-glass-success { + background: rgba(34, 197, 94, 0.1); + border: 1px solid rgba(34, 197, 94, 0.3); + color: var(--success); +} + +/* ===== Footer ===== */ +.footer-app { + background: var(--glass-bg); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border-top: 1px solid var(--glass-border); + padding: 1.25rem; + text-align: center; + color: var(--text-muted); + font-size: 0.85rem; + margin-top: auto; +} + +.footer-app a { + color: var(--accent); + font-weight: 500; +} + +/* ===== Animations ===== */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.fade-in { + animation: fadeInUp 0.5s ease-out; +} + +.fade-in-delay-1 { animation-delay: 0.1s; animation-fill-mode: both; } +.fade-in-delay-2 { animation-delay: 0.2s; animation-fill-mode: both; } +.fade-in-delay-3 { animation-delay: 0.3s; animation-fill-mode: both; } + +/* ===== Chat Widget ===== */ +.chat-fab { + position: fixed; + bottom: 1.5rem; + right: 1.5rem; + width: 56px; + height: 56px; + border-radius: 50%; + background: var(--accent); + color: #fff; + border: none; + font-size: 1.5rem; + cursor: pointer; + box-shadow: 0 4px 16px var(--accent-glow); + transition: all var(--transition-speed); + z-index: 1100; + display: flex; + align-items: center; + justify-content: center; +} + +.chat-fab:hover { + background: var(--accent-hover); + transform: scale(1.1); +} + +.chat-panel { + position: fixed; + bottom: 5.5rem; + right: 1.5rem; + width: 360px; + max-height: 480px; + border-radius: var(--border-radius); + background: var(--glass-bg); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid var(--glass-border); + box-shadow: var(--glass-shadow); + display: none; + flex-direction: column; + z-index: 1100; + animation: fadeInUp 0.3s ease-out; +} + +.chat-panel.open { + display: flex; +} + +.chat-header { + padding: 1rem 1.25rem; + border-bottom: 1px solid var(--border-color); + font-weight: 600; + display: flex; + align-items: center; + gap: 0.5rem; +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.75rem; + min-height: 200px; + max-height: 320px; +} + +.chat-msg { + padding: 0.625rem 0.875rem; + border-radius: 12px; + font-size: 0.875rem; + line-height: 1.4; + max-width: 85%; + word-wrap: break-word; +} + +.chat-msg.user { + background: var(--accent); + color: #fff; + align-self: flex-end; + border-bottom-right-radius: 4px; +} + +.chat-msg.bot { + background: var(--bg-surface); + border: 1px solid var(--border-color); + align-self: flex-start; + border-bottom-left-radius: 4px; +} + +.chat-msg.bot.loading { + color: var(--text-muted); + font-style: italic; +} + +.chat-input-area { + padding: 0.75rem; + border-top: 1px solid var(--border-color); + display: flex; + gap: 0.5rem; +} + +.chat-input-area input { + flex: 1; + background: var(--bg-surface); + border: 1px solid var(--border-color); + color: var(--text-primary); + border-radius: var(--border-radius-sm); + padding: 0.5rem 0.75rem; + font-size: 0.875rem; + outline: none; +} + +.chat-input-area input:focus { + border-color: var(--accent); +} + +.chat-input-area button { + background: var(--accent); + color: #fff; + border: none; + border-radius: var(--border-radius-sm); + padding: 0.5rem 0.75rem; + cursor: pointer; + transition: background var(--transition-speed); +} + +.chat-input-area button:hover { + background: var(--accent-hover); +} + +/* ===== Responsive ===== */ +@media (max-width: 768px) { + .balance-amount { + font-size: 2.25rem; + } + + .account-info { + flex-direction: column; + gap: 0.5rem; + } + + .action-cards { + grid-template-columns: 1fr; + } + + .glass-card { + padding: 1.5rem; + } + + .table-glass thead th, + .table-glass tbody td { + padding: 0.75rem 0.5rem; + font-size: 0.875rem; + } +} + +@media (max-width: 480px) { + .balance-amount { + font-size: 1.75rem; + } + + .auth-card { + padding: 1.5rem; + } +} diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/resources/static/js/theme.js b/2026/day-80/AI-BankApp-DevOps/src/main/resources/static/js/theme.js new file mode 100644 index 0000000000..e27c1f749e --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/resources/static/js/theme.js @@ -0,0 +1,33 @@ +(function () { + const STORAGE_KEY = 'bankapp-theme'; + + function getPreferred() { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) return stored; + return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'; + } + + function apply(theme) { + document.documentElement.setAttribute('data-theme', theme); + localStorage.setItem(STORAGE_KEY, theme); + const icon = document.getElementById('themeIcon'); + if (icon) { + icon.className = theme === 'dark' ? 'bi bi-sun-fill' : 'bi bi-moon-fill'; + } + } + + // Apply immediately to prevent flash + apply(getPreferred()); + + document.addEventListener('DOMContentLoaded', function () { + const btn = document.getElementById('themeToggle'); + if (btn) { + btn.addEventListener('click', function () { + const current = document.documentElement.getAttribute('data-theme') || 'dark'; + apply(current === 'dark' ? 'light' : 'dark'); + }); + } + // Re-apply to update icon after DOM is ready + apply(getPreferred()); + }); +})(); diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/resources/static/mysql/SQLScript.txt b/2026/day-80/AI-BankApp-DevOps/src/main/resources/static/mysql/SQLScript.txt new file mode 100644 index 0000000000..69a8e5c7e8 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/resources/static/mysql/SQLScript.txt @@ -0,0 +1 @@ +CREATE DATABASE bankappdb; \ No newline at end of file diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/dashboard.html b/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/dashboard.html new file mode 100644 index 0000000000..9ee685e66d --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/dashboard.html @@ -0,0 +1,162 @@ + + + + +
+ + +
+ +
+
Total Balance
+
+ $0.00 +
+ +
+ + +
+ +
+ + +
+ +
+
Deposit
+
+
+ + +
+ +
+
+ + +
+
Withdraw
+
+
+ + +
+ +
+
+ + +
+
Transfer
+
+
+ + +
+
+ + +
+ +
+
+
+
+ +
+
+ + + + +
+
+ AI Assistant +
+
+
Hi! I'm your banking assistant. Ask me about your balance, transactions, or anything else.
+
+
+ + +
+
+ + + + + diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/fragments/layout.html b/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/fragments/layout.html new file mode 100644 index 0000000000..3cafa18c59 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/fragments/layout.html @@ -0,0 +1,66 @@ + + + + + + + + + + BankApp + + + + + + + + + + + + + +
+

© 2026 BankApp. Built with Spring Boot.

+
+ + diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/login.html b/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/login.html new file mode 100644 index 0000000000..362983c1c5 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/login.html @@ -0,0 +1,72 @@ + + + + +
+ + +
+
+
+

Welcome back Josh Batch 10

+

Sign in to your account

+
+ +
+ Invalid username or password. +
+ +
+ You have been logged out. +
+ +
+ Registration successful. Please sign in. +
+ +
+
+ + +
+ +
+ + + +
+ + +
+ +

+ Don't have an account? Create one +

+
+
+ +
+
+ + + + + diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/register.html b/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/register.html new file mode 100644 index 0000000000..a54ca3a3ad --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/register.html @@ -0,0 +1,95 @@ + + + + +
+ + +
+
+
+

Create Account

+

Join BankApp today

+
+ +
+ Username already taken. Please choose another. +
+ +
+
+ + +
+ +
+ + + +
+ +
+
+
+
+ +
+ + +
+ +

+ Already have an account? Sign in +

+
+
+ +
+
+ + + + + diff --git a/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/transactions.html b/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/transactions.html new file mode 100644 index 0000000000..e0e48bbe62 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/main/resources/templates/transactions.html @@ -0,0 +1,64 @@ + + + + +
+ + +
+
+

Transaction History

+ + Dashboard + +
+ +
+ +
+
+ + + + + + + + + + + + + + + + + +
IDTypeAmountDate
+ + +
+
+
+ + +
+ +
No transactions yet
+

Your transaction history will appear here once you make your first deposit.

+ + Make a Deposit + +
+
+
+ +
+
+ + + + diff --git a/2026/day-80/AI-BankApp-DevOps/src/test/java/com/example/bankapp/BankappApplicationTests.java b/2026/day-80/AI-BankApp-DevOps/src/test/java/com/example/bankapp/BankappApplicationTests.java new file mode 100644 index 0000000000..63c64e9d7c --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/src/test/java/com/example/bankapp/BankappApplicationTests.java @@ -0,0 +1,13 @@ +package com.example.bankapp; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class BankappApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/2026/day-80/AI-BankApp-DevOps/terraform/.terraform.lock.hcl b/2026/day-80/AI-BankApp-DevOps/terraform/.terraform.lock.hcl new file mode 100644 index 0000000000..fff0fb1ed0 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/terraform/.terraform.lock.hcl @@ -0,0 +1,125 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.40.0" + constraints = ">= 4.0.0, >= 6.0.0, ~> 6.0, >= 6.28.0" + hashes = [ + "h1:Li1WiMXyFcGfuph9iGIdTScJFNjhkSR/MkuvpapGYYg=", + "zh:05171de71e4236b41740c6a4d4145cc38399b344535e7768e5380d0fdcb95609", + "zh:17910a059ac677c21b61ed5da94cffba40f7cb13ddd78c818458d122cf3ef9a9", + "zh:2f072f335d3f422bbe0f3fd46eba70a52caf43e09ee97599c26f713dbac48fed", + "zh:316da833674982910718aae754f4db0fcc89f0466ca2ca66219fe7fa435950be", + "zh:3fcdde0076bebfaeb0c61960b3e03cf3e2c5a978f7a4ebb0aa42e6b36209044a", + "zh:48cecf9748a3354ccd08275da8b34a42b3367247fc2de398e03ae4e8463299aa", + "zh:4ae0f7e76c61a4820405bd3f340b156f42f7f4480715865b008636c61fbbfa30", + "zh:5add497a7ff7063425c65460c6bc40701cf0e59e09b379e7f535fa37c8ebd80a", + "zh:7a26b4507c99382cefba2b189d4c5ebed939de8fb663a193b65d37934603804d", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:bad959d58660f6a0b0f950b038c76f378cfe045ba8560a0c3bbf8886f62d81be", + "zh:cf7af6468288a36b02d009045dd4a67c24ad99bbcad406bab0f9c12776d5d99c", + "zh:cfb683320d6d8bc1a5640538af62a5f9da285e28d0daca9e91d667d6e25585d9", + "zh:de7d26ac15362942f590175e425160a2fbf0fe0960afec40b4af78076d4fb9fb", + "zh:fe2fd136b68ea1941da60d8991d3857dd721b180d49502121f3ea8688a812704", + ] +} + +provider "registry.terraform.io/hashicorp/cloudinit" { + version = "2.3.7" + constraints = ">= 2.0.0" + hashes = [ + "h1:M9TpQxKAE/hyOwytdX9MUNZw30HoD/OXqYIug5fkqH8=", + "zh:06f1c54e919425c3139f8aeb8fcf9bceca7e560d48c9f0c1e3bb0a8ad9d9da1e", + "zh:0e1e4cf6fd98b019e764c28586a386dc136129fef50af8c7165a067e7e4a31d5", + "zh:1871f4337c7c57287d4d67396f633d224b8938708b772abfc664d1f80bd67edd", + "zh:2b9269d91b742a71b2248439d5e9824f0447e6d261bfb86a8a88528609b136d1", + "zh:3d8ae039af21426072c66d6a59a467d51f2d9189b8198616888c1b7fc42addc7", + "zh:3ef4e2db5bcf3e2d915921adced43929214e0946a6fb11793085d9a48995ae01", + "zh:42ae54381147437c83cbb8790cc68935d71b6357728a154109d3220b1beb4dc9", + "zh:4496b362605ae4cbc9ef7995d102351e2fe311897586ffc7a4a262ccca0c782a", + "zh:652a2401257a12706d32842f66dac05a735693abcb3e6517d6b5e2573729ba13", + "zh:7406c30806f5979eaed5f50c548eced2ea18ea121e01801d2f0d4d87a04f6a14", + "zh:7848429fd5a5bcf35f6fee8487df0fb64b09ec071330f3ff240c0343fe2a5224", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + ] +} + +provider "registry.terraform.io/hashicorp/helm" { + version = "2.17.0" + constraints = "~> 2.17" + hashes = [ + "h1:kQMkcPVvHOguOqnxoEU2sm1ND9vCHiT8TvZ2x6v/Rsw=", + "zh:06fb4e9932f0afc1904d2279e6e99353c2ddac0d765305ce90519af410706bd4", + "zh:104eccfc781fc868da3c7fec4385ad14ed183eb985c96331a1a937ac79c2d1a7", + "zh:129345c82359837bb3f0070ce4891ec232697052f7d5ccf61d43d818912cf5f3", + "zh:3956187ec239f4045975b35e8c30741f701aa494c386aaa04ebabffe7749f81c", + "zh:66a9686d92a6b3ec43de3ca3fde60ef3d89fb76259ed3313ca4eb9bb8c13b7dd", + "zh:88644260090aa621e7e8083585c468c8dd5e09a3c01a432fb05da5c4623af940", + "zh:a248f650d174a883b32c5b94f9e725f4057e623b00f171936dcdcc840fad0b3e", + "zh:aa498c1f1ab93be5c8fbf6d48af51dc6ef0f10b2ea88d67bcb9f02d1d80d3930", + "zh:bf01e0f2ec2468c53596e027d376532a2d30feb72b0b5b810334d043109ae32f", + "zh:c46fa84cc8388e5ca87eb575a534ebcf68819c5a5724142998b487cb11246654", + "zh:d0c0f15ffc115c0965cbfe5c81f18c2e114113e7a1e6829f6bfd879ce5744fbb", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.4" + constraints = ">= 3.0.0" + hashes = [ + "h1:L5V05xwp/Gto1leRryuesxjMfgZwjb7oool4WS1UEFQ=", + "zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43", + "zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a", + "zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991", + "zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f", + "zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e", + "zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615", + "zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442", + "zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5", + "zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f", + "zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.13.1" + constraints = ">= 0.9.0" + hashes = [ + "h1:ZT5ppCNIModqk3iOkVt5my8b8yBHmDpl663JtXAIRqM=", + "zh:02cb9aab1002f0f2a94a4f85acec8893297dc75915f7404c165983f720a54b74", + "zh:04429b2b31a492d19e5ecf999b116d396dac0b24bba0d0fb19ecaefe193fdb8f", + "zh:26f8e51bb7c275c404ba6028c1b530312066009194db721a8427a7bc5cdbc83a", + "zh:772ff8dbdbef968651ab3ae76d04afd355c32f8a868d03244db3f8496e462690", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:898db5d2b6bd6ca5457dccb52eedbc7c5b1a71e4a4658381bcbb38cedbbda328", + "zh:8de913bf09a3fa7bedc29fec18c47c571d0c7a3d0644322c46f3aa648cf30cd8", + "zh:9402102c86a87bdfe7e501ffbb9c685c32bbcefcfcf897fd7d53df414c36877b", + "zh:b18b9bb1726bb8cfbefc0a29cf3657c82578001f514bcf4c079839b6776c47f0", + "zh:b9d31fdc4faecb909d7c5ce41d2479dd0536862a963df434be4b16e8e4edc94d", + "zh:c951e9f39cca3446c060bd63933ebb89cedde9523904813973fbc3d11863ba75", + "zh:e5b773c0d07e962291be0e9b413c7a22c044b8c7b58c76e8aa91d1659990dfb5", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.2.1" + constraints = ">= 4.0.0" + hashes = [ + "h1:akFNuHwvrtnYMBofieoeXhPJDhYZzJVu/Q/BgZK2fgg=", + "zh:0d1e7d07ac973b97fa228f46596c800de830820506ee145626f079dd6bbf8d8a", + "zh:5c7e3d4348cb4861ab812973ef493814a4b224bdd3e9d534a7c8a7c992382b86", + "zh:7c6d4a86cd7a4e9c1025c6b3a3a6a45dea202af85d870cddbab455fb1bd568ad", + "zh:7d0864755ba093664c4b2c07c045d3f5e3d7c799dda1a3ef33d17ed1ac563191", + "zh:83734f57950ab67c0d6a87babdb3f13c908cbe0a48949333f489698532e1391b", + "zh:951e3c285218ebca0cf20eaa4265020b4ef042fea9c6ade115ad1558cfe459e5", + "zh:b9543955b4297e1d93b85900854891c0e645d936d8285a190030475379c5c635", + "zh:bb1bd9e86c003d08c30c1b00d44118ed5bbbf6b1d2d6f7eaac4fa5c6ebea5933", + "zh:c9477bfe00653629cd77ddac3968475f7ad93ac3ca8bc45b56d1d9efb25e4a6e", + "zh:d4cfda8687f736d0cba664c22ec49dae1188289e214ef57f5afe6a7217854fed", + "zh:dc77ee066cf96532a48f0578c35b1eaf6dc4d8ddd0e3ae8e029a3b10676dd5d3", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} diff --git a/2026/day-80/AI-BankApp-DevOps/terraform/README.md b/2026/day-80/AI-BankApp-DevOps/terraform/README.md new file mode 100644 index 0000000000..8913721447 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/terraform/README.md @@ -0,0 +1,340 @@ +# AI BankApp - End-to-End GitOps on EKS + +Complete guide to deploy AI BankApp on AWS EKS using Terraform, ArgoCD, Gateway API, and Prometheus monitoring. + +## Architecture + +``` +Developer Push → GitHub Actions CI → DockerHub → ArgoCD → EKS Cluster + ↑ + watches k8s/ manifests +``` + +``` + ┌─────────────────────────────────────────────────┐ + │ VPC (10.0.0.0/16) │ + │ │ + │ ┌──────────────┐ ┌──────────────┐ ┌────────┐ │ + │ │ Public Sub │ │ Public Sub │ │Pub Sub │ │ + │ │ 10.0.1.0/24 │ │ 10.0.2.0/24 │ │.3.0/24 │ │ + │ │ (us-west-2a) │ │ (us-west-2b) │ │ (2c) │ │ + │ └──────┬───────┘ └──────┬───────┘ └───┬────┘ │ + │ │ NAT GW │ │ + │ ┌──────┴───────┐ ┌──────────────┐ ┌───┴────┐ │ + │ │ Private Sub │ │ Private Sub │ │Priv Sub│ │ + │ │ 10.0.4.0/24 │ │ 10.0.5.0/24 │ │.6.0/24 │ │ + │ │ (Worker Nodes)│ │ (Worker Nodes)│ │(Nodes) │ │ + │ └──────────────┘ └──────────────┘ └────────┘ │ + │ ┌──────────────┐ ┌──────────────┐ ┌────────┐ │ + │ │ Intra Sub │ │ Intra Sub │ │Intra │ │ + │ │ 10.0.7.0/24 │ │ 10.0.8.0/24 │ │.9.0/24│ │ + │ │ (Ctrl Plane) │ │ (Ctrl Plane) │ │(Ctrl) │ │ + │ └──────────────┘ └──────────────┘ └────────┘ │ + └─────────────────────────────────────────────────┘ +``` + +| Component | Tool | +|-----------|------| +| Infrastructure | Terraform (VPC + EKS + ArgoCD) | +| CI Pipeline | GitHub Actions → DockerHub | +| GitOps / CD | ArgoCD (auto-sync from `k8s/` folder) | +| Ingress | Gateway API + Envoy Gateway (AWS NLB) | +| Monitoring | kube-prometheus-stack (Prometheus + Grafana) | +| AI Chatbot | Ollama (tinyllama) on EKS | +| Storage | EBS CSI Driver (gp3 dynamic provisioning) | + +## What Gets Created + +| Resource | Details | +|---|---| +| **VPC** | `10.0.0.0/16` across 3 AZs with public, private, and intra subnets | +| **NAT Gateway** | Single NAT for private subnet internet access | +| **EKS Cluster** | `bankapp-eks` running Kubernetes **1.35** | +| **Node Group** | `bankapp-ng` — 3x `t3.medium` instances on AL2023 | +| **Add-ons** | CoreDNS, kube-proxy, VPC-CNI, Pod Identity Agent, EBS CSI Driver | +| **ArgoCD** | Installed via Helm, exposed as LoadBalancer | + +## Prerequisites + +- [Terraform](https://developer.hashicorp.com/terraform/install) >= 1.5.7 +- [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) v2 (configured with `aws configure`) +- [kubectl](https://kubernetes.io/docs/tasks/tools/) +- [Helm 3](https://helm.sh/docs/intro/install/) +- GitHub repo secrets: + - `DOCKERHUB_USERNAME` — DockerHub username + - `DOCKERHUB_TOKEN` — DockerHub access token + +## 1. Provision EKS Cluster + +```bash +cd terraform +terraform init +terraform plan +terraform apply # ~15 minutes +``` + +This creates the VPC, EKS cluster, node group, add-ons, and ArgoCD. + +## 2. Configure kubectl + +```bash +aws eks update-kubeconfig --name bankapp-eks --region us-west-2 +kubectl get nodes +``` + +## 3. Access ArgoCD + +```bash +# Get the ArgoCD server URL (AWS NLB) +kubectl get svc argocd-server -n argocd -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' + +# Get the initial admin password +kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath='{.data.password}' | base64 -d +``` + +Login: username = `admin`, password = output from above. + +## 4. Install Gateway API + Envoy Gateway + +```bash +# Install Gateway API CRDs (must use --server-side to avoid ownership conflicts with Envoy Gateway) +kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.2.1/standard-install.yaml + +# Install Envoy Gateway via Helm (--skip-crds since Gateway API CRDs are already installed above) +helm install eg oci://docker.io/envoyproxy/gateway-helm \ + --version v1.2.6 \ + -n envoy-gateway-system \ + --create-namespace \ + --skip-crds \ + --wait + +# --skip-crds also skips Envoy Gateway extension CRDs (BackendTrafficPolicy etc.) +# Install them separately, then restart the controller to pick them up +helm pull oci://docker.io/envoyproxy/gateway-helm --version v1.2.6 --untar -d /tmp/eg-chart +kubectl apply --server-side -f /tmp/eg-chart/gateway-helm/crds/generated/ +kubectl rollout restart deployment envoy-gateway -n envoy-gateway-system + +# Verify GatewayClass is registered +kubectl get gatewayclass +``` + +On EKS, Envoy Gateway automatically provisions an AWS NLB when a Gateway resource is created. + +## 5. Install cert-manager (HTTPS/TLS) + +```bash +helm install cert-manager oci://quay.io/jetstack/charts/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --set crds.enabled=true \ + --set config.enableGatewayAPI=true \ + --wait +``` + +cert-manager integrates with Gateway API to auto-provision Let's Encrypt TLS certificates. +The `ClusterIssuer` and HTTPS listener are defined in `k8s/cert-manager.yml` and `k8s/gateway.yml` — ArgoCD syncs them automatically. + +**DNS setup (GoDaddy):** Create a CNAME record: +- `bankapp.trainwithshubham.com` → `` + +```bash +# Verify cert-manager +kubectl get pods -n cert-manager + +# Check certificate (after ArgoCD syncs and DNS propagates) +kubectl get certificate -n bankapp +kubectl describe certificate -n bankapp +``` + +## 6. Install kube-prometheus-stack + +```bash +# Add Helm repo +helm repo add prometheus-community https://prometheus-community.github.io/helm-charts +helm repo update + +# Install kube-prometheus-stack +helm install kube-prometheus prometheus-community/kube-prometheus-stack \ + -n monitoring \ + --create-namespace \ + --set grafana.service.type=LoadBalancer \ + --wait +``` + +Access Grafana: + +```bash +# Get Grafana URL +kubectl get svc kube-prometheus-grafana -n monitoring -o jsonpath='{.status.loadBalancer.ingress[0].hostname}' + +# Get admin password +kubectl get secret kube-prometheus-grafana -n monitoring -o jsonpath='{.data.admin-password}' | base64 -d; echo +# Login: admin / +``` + +Access Prometheus (port-forward): + +```bash +kubectl port-forward svc/kube-prometheus-kube-prome-prometheus 9090:9090 -n monitoring +# Open http://localhost:9090 +``` + +## 7. Deploy Application via ArgoCD + +### Option A: CLI + +```bash +kubectl apply -f argocd/application.yml +``` + +### Option B: ArgoCD UI + +1. Open ArgoCD UI (URL from Step 3) +2. Click **New App** → fill in: + - **Name:** `bankapp` + - **Project:** `default` + - **Repo URL:** `https://github.com/TrainWithShubham/AI-BankApp-DevOps.git` + - **Revision:** `feat/gitops` + - **Path:** `k8s` + - **Cluster:** `https://kubernetes.default.svc` + - **Namespace:** `bankapp` +3. Enable **Auto-Sync** + **Self-Heal** + **Prune** +4. Click **Create** + +ArgoCD deploys everything from `k8s/`: + +| Resource | What it does | +|----------|-------------| +| `namespace.yml` | Creates `bankapp` namespace | +| `pv.yml` | gp3 StorageClass (EBS CSI dynamic provisioning) | +| `pvc.yml` | PVCs for MySQL (5Gi) and Ollama (10Gi) | +| `configmap.yml` | DB host, port, Ollama URL | +| `secrets.yml` | DB credentials | +| `mysql-deployment.yml` | MySQL 8.0 with EBS volume | +| `ollama-deployment.yml` | Ollama AI with tinyllama model | +| `bankapp-deployment.yml` | BankApp (2 replicas, HPA enabled) | +| `service.yml` | ClusterIP services for all components | +| `gateway.yml` | Gateway API → external access via NLB | +| `hpa.yml` | Auto-scale BankApp 2-4 replicas | + +## 8. Verify Deployment + +```bash +# Pods +kubectl get pods -n bankapp + +# Services +kubectl get svc -n bankapp + +# Gateway + NLB +kubectl get gateway -n bankapp + +# Get the app URL +export APP_URL=$(kubectl get svc -n envoy-gateway-system \ + -l gateway.envoyproxy.io/owning-gateway-name=bankapp-gateway \ + -o jsonpath='{.items[0].status.loadBalancer.ingress[0].hostname}') +echo "BankApp: http://$APP_URL" +``` + +## 9. CI/CD Pipeline (GitOps Flow) + +``` +Code Push → GitHub Actions → Build & Push to DockerHub → Update k8s manifest → ArgoCD auto-sync → EKS +``` + +1. **Developer** pushes code changes to `feat/gitops` +2. **GitHub Actions** (`.github/workflows/gitops-ci.yml`): + - Builds the Spring Boot app with Maven + - Builds Docker image + - Pushes `trainwithshubham/ai-bankapp-eks:` to DockerHub + - Updates `k8s/bankapp-deployment.yml` with new image tag + - Commits with `[skip ci]` +3. **ArgoCD** detects the manifest change → auto-syncs to EKS +4. **EKS** performs rolling update with zero downtime + +### GitHub Secrets Required + +Set in repo → Settings → Secrets → Actions: + +| Secret | Value | +|--------|-------| +| `DOCKERHUB_USERNAME` | DockerHub username | +| `DOCKERHUB_TOKEN` | DockerHub access token | + +## 10. ArgoCD Add-ons + +| Add-on | Purpose | Install | +|--------|---------|---------| +| **Image Updater** | Auto-update image tags from registries | `helm install argocd-image-updater argo/argocd-image-updater -n argocd` | +| **Notifications** | Slack/Teams alerts on sync events | Built into argo-cd chart, configure via ConfigMap | +| **Rollouts** | Blue/Green and Canary deployments | `kubectl apply -n argo-rollouts -f https://github.com/argoproj/argo-rollouts/releases/latest/download/install.yaml` | +| **ApplicationSets** | Template-driven multi-cluster apps | Built into ArgoCD 2.x | + +## 11. Cleanup + +> **Order matters** — Helm releases create AWS Load Balancers outside Terraform. +> Delete them first or `terraform destroy` will hang on VPC deletion. + +```bash +# 1. Delete ArgoCD app (removes Gateway → NLB) +kubectl delete -f argocd/application.yml + +# 2. Uninstall Helm releases +helm uninstall cert-manager -n cert-manager +helm uninstall kube-prometheus -n monitoring +helm uninstall eg -n envoy-gateway-system + +# 3. Wait for LBs to terminate +sleep 60 + +# 4. Destroy infrastructure +cd terraform +terraform destroy +``` + +See [`DEPLOYMENT.md`](../DEPLOYMENT.md#cleanup) for detailed cleanup steps and troubleshooting if VPC deletion gets stuck. + +## Project Structure + +``` +. +├── terraform/ +│ ├── provider.tf # Terraform + AWS + Helm providers +│ ├── variables.tf # Configurable inputs (region, cluster, nodes) +│ ├── vpc.tf # VPC module — public, private, intra subnets +│ ├── eks.tf # EKS module — cluster, node group, add-ons +│ ├── argocd.tf # ArgoCD Helm release +│ ├── outputs.tf # Cluster info, kubectl command, ArgoCD password +│ └── terraform.tfvars # Default variable values +├── k8s/ # Application manifests (ArgoCD watches this) +├── argocd/ +│ └── application.yml # ArgoCD Application pointing to k8s/ +└── .github/workflows/ + └── gitops-ci.yml # CI → DockerHub → manifest update +``` + +## Module Versions + +| Module | Version | Registry | +|---|---|---| +| terraform-aws-modules/eks/aws | ~> 21.0 | [Terraform Registry](https://registry.terraform.io/modules/terraform-aws-modules/eks/aws/latest) | +| terraform-aws-modules/vpc/aws | ~> 6.0 | [Terraform Registry](https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest) | +| hashicorp/aws provider | ~> 6.0 | [Terraform Registry](https://registry.terraform.io/providers/hashicorp/aws/latest) | +| argoproj/argo-cd (Helm) | latest | [Artifact Hub](https://artifacthub.io/packages/helm/argo/argo-cd) | + +## Troubleshooting + +```bash +# ArgoCD app status +kubectl get applications -n argocd +kubectl describe application bankapp -n argocd + +# Pod logs +kubectl logs -l app=bankapp -n bankapp --tail=50 + +# EBS PVC status (should be Bound) +kubectl get pvc -n bankapp + +# Gateway status +kubectl get gateway,httproute -n bankapp +``` diff --git a/2026/day-80/AI-BankApp-DevOps/terraform/argocd.tf b/2026/day-80/AI-BankApp-DevOps/terraform/argocd.tf new file mode 100644 index 0000000000..7cecabbe15 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/terraform/argocd.tf @@ -0,0 +1,27 @@ +# ArgoCD — Installed via Helm into the EKS cluster +# Exposed as LoadBalancer (AWS NLB) for easy access + +resource "helm_release" "argocd" { + name = "argocd" + repository = "https://argoproj.github.io/argo-helm" + chart = "argo-cd" + namespace = "argocd" + create_namespace = true + + values = [ + yamlencode({ + server = { + service = { + type = "LoadBalancer" + } + } + configs = { + params = { + "server.insecure" = true + } + } + }) + ] + + depends_on = [module.eks] +} diff --git a/2026/day-80/AI-BankApp-DevOps/terraform/eks.tf b/2026/day-80/AI-BankApp-DevOps/terraform/eks.tf new file mode 100644 index 0000000000..6d35f9dc8c --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/terraform/eks.tf @@ -0,0 +1,81 @@ +# EKS Cluster — Module v21.x with Kubernetes 1.35 +# Uses EKS Pod Identity, access_entries, AL2023 AMI, and AWS provider v6.0+ + +module "eks" { + source = "terraform-aws-modules/eks/aws" + version = "~> 21.0" + + # v21 parameter names (renamed from v20) + name = local.name + kubernetes_version = var.cluster_version + + endpoint_public_access = true + endpoint_private_access = true + + # Cluster creator gets admin access via access_entries + enable_cluster_creator_admin_permissions = true + + # EKS Add-ons (latest versions auto-resolved) + addons = { + coredns = { + most_recent = true + } + kube-proxy = { + most_recent = true + } + vpc-cni = { + most_recent = true + before_compute = true + } + eks-pod-identity-agent = { + most_recent = true + before_compute = true + } + aws-ebs-csi-driver = { + most_recent = true + service_account_role_arn = module.ebs_csi_irsa.iam_role_arn + } + metrics-server = { + most_recent = true + } + } + + # Networking + vpc_id = module.vpc.vpc_id + subnet_ids = module.vpc.private_subnets + control_plane_subnet_ids = module.vpc.intra_subnets + + # Managed Node Group + eks_managed_node_groups = { + bankapp-ng = { + instance_types = [var.node_instance_type] + desired_size = var.node_desired_count + min_size = var.node_desired_count + max_size = var.node_max_count + + tags = { + NodeGroup = "bankapp" + } + } + } + + tags = local.tags +} + +# IRSA for EBS CSI Driver (needed to create/attach EBS volumes) +module "ebs_csi_irsa" { + source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" + version = "~> 5.0" + + role_name = "${local.name}-ebs-csi" + attach_ebs_csi_policy = true + + oidc_providers = { + main = { + provider_arn = module.eks.oidc_provider_arn + namespace_service_accounts = ["kube-system:ebs-csi-controller-sa"] + } + } + + tags = local.tags +} diff --git a/2026/day-80/AI-BankApp-DevOps/terraform/outputs.tf b/2026/day-80/AI-BankApp-DevOps/terraform/outputs.tf new file mode 100644 index 0000000000..aa9dc8a30c --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/terraform/outputs.tf @@ -0,0 +1,53 @@ +# Cluster +output "cluster_name" { + description = "EKS cluster name" + value = module.eks.cluster_name +} + +output "cluster_endpoint" { + description = "EKS cluster API endpoint" + value = module.eks.cluster_endpoint +} + +output "cluster_version" { + description = "Kubernetes version running on the cluster" + value = module.eks.cluster_version +} + +output "cluster_certificate_authority_data" { + description = "Base64 encoded certificate data for cluster authentication" + value = module.eks.cluster_certificate_authority_data + sensitive = true +} + +output "oidc_provider_arn" { + description = "OIDC provider ARN (for IRSA if needed)" + value = module.eks.oidc_provider_arn +} + +# Networking +output "vpc_id" { + description = "VPC ID" + value = module.vpc.vpc_id +} + +output "private_subnets" { + description = "Private subnet IDs (worker nodes)" + value = module.vpc.private_subnets +} + +output "public_subnets" { + description = "Public subnet IDs (load balancers)" + value = module.vpc.public_subnets +} + +# Quick commands +output "configure_kubectl" { + description = "Command to configure kubectl" + value = "aws eks update-kubeconfig --name ${module.eks.cluster_name} --region ${local.region}" +} + +output "argocd_initial_password" { + description = "Command to get ArgoCD initial admin password" + value = "kubectl get secret argocd-initial-admin-secret -n argocd -o jsonpath='{.data.password}' | base64 -d" +} diff --git a/2026/day-80/AI-BankApp-DevOps/terraform/provider.tf b/2026/day-80/AI-BankApp-DevOps/terraform/provider.tf new file mode 100644 index 0000000000..9ba9d855be --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/terraform/provider.tf @@ -0,0 +1,54 @@ +terraform { + required_version = ">= 1.5.7" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 6.0" + } + helm = { + source = "hashicorp/helm" + version = "~> 2.17" + } + } +} + +locals { + region = var.aws_region + name = var.cluster_name + vpc_cidr = "10.0.0.0/16" + azs = slice(data.aws_availability_zones.available.names, 0, 3) + public_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"] + private_subnets = ["10.0.4.0/24", "10.0.5.0/24", "10.0.6.0/24"] + intra_subnets = ["10.0.7.0/24", "10.0.8.0/24", "10.0.9.0/24"] + + tags = { + Project = "AI-BankApp" + ManagedBy = "terraform" + Environment = "production" + } +} + +data "aws_availability_zones" "available" { + filter { + name = "opt-in-status" + values = ["opt-in-not-required"] + } +} + +provider "aws" { + region = local.region +} + +provider "helm" { + kubernetes { + host = module.eks.cluster_endpoint + cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data) + + exec { + api_version = "client.authentication.k8s.io/v1beta1" + command = "aws" + args = ["eks", "get-token", "--cluster-name", module.eks.cluster_name] + } + } +} diff --git a/2026/day-80/AI-BankApp-DevOps/terraform/terraform.tfvars b/2026/day-80/AI-BankApp-DevOps/terraform/terraform.tfvars new file mode 100644 index 0000000000..4ee8cf47ea --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/terraform/terraform.tfvars @@ -0,0 +1,6 @@ +aws_region = "us-west-2" +cluster_name = "bankapp-eks" +cluster_version = "1.35" +node_instance_type = "t3.medium" +node_desired_count = 3 +node_max_count = 5 diff --git a/2026/day-80/AI-BankApp-DevOps/terraform/variables.tf b/2026/day-80/AI-BankApp-DevOps/terraform/variables.tf new file mode 100644 index 0000000000..5874833f56 --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/terraform/variables.tf @@ -0,0 +1,35 @@ +variable "aws_region" { + description = "AWS region for EKS cluster" + type = string + default = "us-west-2" +} + +variable "cluster_name" { + description = "Name of the EKS cluster" + type = string + default = "bankapp-eks" +} + +variable "cluster_version" { + description = "Kubernetes version for EKS" + type = string + default = "1.35" +} + +variable "node_instance_type" { + description = "EC2 instance type for EKS worker nodes" + type = string + default = "t3.medium" +} + +variable "node_desired_count" { + description = "Desired number of worker nodes" + type = number + default = 3 +} + +variable "node_max_count" { + description = "Maximum number of worker nodes" + type = number + default = 5 +} diff --git a/2026/day-80/AI-BankApp-DevOps/terraform/vpc.tf b/2026/day-80/AI-BankApp-DevOps/terraform/vpc.tf new file mode 100644 index 0000000000..f3f1d3768f --- /dev/null +++ b/2026/day-80/AI-BankApp-DevOps/terraform/vpc.tf @@ -0,0 +1,23 @@ +module "vpc" { + source = "terraform-aws-modules/vpc/aws" + version = "~> 6.0" + + name = local.name + cidr = local.vpc_cidr + azs = local.azs + public_subnets = local.public_subnets + private_subnets = local.private_subnets + intra_subnets = local.intra_subnets + + enable_nat_gateway = true + + public_subnet_tags = { + "kubernetes.io/role/elb" = 1 + } + + private_subnet_tags = { + "kubernetes.io/role/internal-elb" = 1 + } + + tags = local.tags +} diff --git a/2026/day-80/Day 80 Helm DevOps Deployment Workflow.png b/2026/day-80/Day 80 Helm DevOps Deployment Workflow.png new file mode 100644 index 0000000000..e5ae01012f Binary files /dev/null and b/2026/day-80/Day 80 Helm DevOps Deployment Workflow.png differ diff --git a/2026/day-80/Screenshot 2026-10-05 124950.png b/2026/day-80/Screenshot 2026-10-05 124950.png new file mode 100644 index 0000000000..7bc909210b Binary files /dev/null and b/2026/day-80/Screenshot 2026-10-05 124950.png differ diff --git a/2026/day-80/Screenshot 2026-10-05 125038.png b/2026/day-80/Screenshot 2026-10-05 125038.png new file mode 100644 index 0000000000..9128449df7 Binary files /dev/null and b/2026/day-80/Screenshot 2026-10-05 125038.png differ diff --git a/2026/day-80/Screenshot 2026-10-05 125059.png b/2026/day-80/Screenshot 2026-10-05 125059.png new file mode 100644 index 0000000000..ad6169dfda Binary files /dev/null and b/2026/day-80/Screenshot 2026-10-05 125059.png differ diff --git a/2026/day-80/Screenshot 2026-10-05 130323.png b/2026/day-80/Screenshot 2026-10-05 130323.png new file mode 100644 index 0000000000..6ddbf05b14 Binary files /dev/null and b/2026/day-80/Screenshot 2026-10-05 130323.png differ diff --git a/2026/day-80/Screenshot 2026-10-05 130410.png b/2026/day-80/Screenshot 2026-10-05 130410.png new file mode 100644 index 0000000000..bcf417c1dd Binary files /dev/null and b/2026/day-80/Screenshot 2026-10-05 130410.png differ diff --git a/2026/day-80/Screenshot 2026-10-05 130513.png b/2026/day-80/Screenshot 2026-10-05 130513.png new file mode 100644 index 0000000000..e21aad46eb Binary files /dev/null and b/2026/day-80/Screenshot 2026-10-05 130513.png differ diff --git a/2026/day-80/Screenshot 2026-10-05 130628.png b/2026/day-80/Screenshot 2026-10-05 130628.png new file mode 100644 index 0000000000..1afad00a48 Binary files /dev/null and b/2026/day-80/Screenshot 2026-10-05 130628.png differ diff --git a/2026/day-80/day-80-helm-project.md b/2026/day-80/day-80-helm-project.md new file mode 100644 index 0000000000..ee460d85ea --- /dev/null +++ b/2026/day-80/day-80-helm-project.md @@ -0,0 +1,796 @@ +# Day 80 – Helm Project: Multi-Environment Deployment and CI/CD + +## Objective + +Day 80 brings the three-day Helm journey together for the AI-BankApp. + +The goal was to: + +- Use one Helm chart for **dev, staging, and production** +- Create environment-specific values files +- Add a Helm database-readiness hook +- Add a Helm application health test +- Package the chart as a `.tgz` file +- Understand how Helm fits into a GitOps CI/CD pipeline +- Deploy and verify the dev environment on the local Kind cluster + +--- + +## Project Architecture + +The Helm chart deploys three main application components: + +```text + AI-BankApp Helm Chart + | + +--------------+--------------+ + | | | + BankApp MySQL Ollama + Spring Boot Database AI Model + :8080 :3306 :11434 + | | | + | 2Gi PVC 5Gi PVC + | + Helm Test + /actuator/health +``` + +The same chart is reused for all environments. Only the values files change. + +--- + +## Task 1 – Environment-Specific Values + +Three values files were created: + +```text +bankapp/ +├── values.yaml +├── values-dev.yaml +├── values-staging.yaml +└── values-prod.yaml +``` + +### Development – `values-dev.yaml` + +```yaml +bankapp: + replicaCount: 1 + image: + repository: trainwithshubham/ai-bankapp-eks + tag: "latest" + pullPolicy: Always + resources: + requests: + memory: "256Mi" + cpu: "100m" + limits: + memory: "512Mi" + cpu: "250m" + autoscaling: + enabled: false + +mysql: + enabled: true + resources: + requests: + memory: "128Mi" + cpu: "100m" + limits: + memory: "256Mi" + cpu: "250m" + persistence: + size: 2Gi + storageClass: standard + +ollama: + enabled: true + model: tinyllama + resources: + requests: + memory: "1Gi" + cpu: "500m" + limits: + memory: "1.5Gi" + cpu: "1000m" + persistence: + size: 5Gi + storageClass: standard + +storageClass: + create: false +``` + +### Staging – `values-staging.yaml` + +```yaml +bankapp: + replicaCount: 2 + image: + repository: trainwithshubham/ai-bankapp-eks + tag: "v1.2.0" + pullPolicy: IfNotPresent + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 3 + targetCPUUtilization: 75 + +mysql: + enabled: true + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + persistence: + size: 5Gi + storageClass: gp3 + +ollama: + enabled: true + model: tinyllama + persistence: + size: 10Gi + storageClass: gp3 + +secrets: + mysqlRootPassword: StagingPass@456 + mysqlUser: root + mysqlPassword: StagingPass@456 + +storageClass: + create: true +``` + +### Production – `values-prod.yaml` + +```yaml +bankapp: + replicaCount: 4 + image: + repository: trainwithshubham/ai-bankapp-eks + tag: "v1.2.0" + pullPolicy: IfNotPresent + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "512Mi" + cpu: "500m" + autoscaling: + enabled: true + minReplicas: 2 + maxReplicas: 4 + targetCPUUtilization: 70 + +mysql: + enabled: true + resources: + requests: + memory: "512Mi" + cpu: "500m" + limits: + memory: "1Gi" + cpu: "1000m" + persistence: + size: 20Gi + storageClass: gp3 + +ollama: + enabled: true + model: tinyllama + resources: + requests: + memory: "2Gi" + cpu: "900m" + limits: + memory: "2.5Gi" + cpu: "1500m" + persistence: + size: 10Gi + storageClass: gp3 + +secrets: + mysqlRootPassword: ProdSecure@789 + mysqlUser: root + mysqlPassword: ProdSecure@789 + +storageClass: + create: true + +gateway: + enabled: true +``` + +### Environment Comparison + +| Setting | Dev | Staging | Production | +|---|---|---|---| +| BankApp replicas | 1 fixed | HPA 2–3 | HPA 2–4 | +| Image tag | latest | v1.2.0 | v1.2.0 | +| MySQL storage | 2Gi | 5Gi | 20Gi | +| MySQL request | 128Mi / 100m | 256Mi / 250m | 512Mi / 500m | +| Ollama storage | 5Gi | 10Gi | 10Gi | +| Gateway | Disabled | Disabled | Enabled | + +This demonstrates one of Helm's biggest benefits: **one chart can deploy the same application differently in multiple environments without duplicating Kubernetes manifests.** + +--- + +## Validate the Environments + +All three environment configurations passed Helm lint: + +```bash +helm lint bankapp/ -f bankapp/values-dev.yaml +helm lint bankapp/ -f bankapp/values-staging.yaml +helm lint bankapp/ -f bankapp/values-prod.yaml +``` + +Result: + +```text +1 chart(s) linted, 0 chart(s) failed +``` + +The environments were also rendered with `helm template`. + +```bash +helm template bankapp-dev bankapp/ -f bankapp/values-dev.yaml +helm template bankapp-staging bankapp/ -f bankapp/values-staging.yaml +helm template bankapp-prod bankapp/ -f bankapp/values-prod.yaml +``` + +The rendered values confirmed: + +```text +DEV +MySQL: 2Gi +Ollama: 5Gi +BankApp: 1 replica + +STAGING +MySQL: 5Gi +Ollama: 10Gi +HPA: min 2 / max 3 + +PRODUCTION +MySQL: 20Gi +Ollama: 10Gi +HPA: min 2 / max 4 +``` + +--- + +## Task 2 – Helm Database Readiness Hook + +A Helm hook was added at: + +```text +bankapp/templates/pre-install-job.yaml +``` + +```yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "bankapp.fullname" . }}-db-ready + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-weight": "0" + "helm.sh/hook-delete-policy": before-hook-creation +spec: + template: + spec: + containers: + - name: db-check + image: busybox:1.36 + command: + - /bin/sh + - -c + - | + echo "Waiting for MySQL to be ready..." + until nc -z {{ include "bankapp.fullname" . }}-mysql 3306; do + echo "MySQL not ready, retrying in 3s..." + sleep 3 + done + echo "MySQL is ready!" + resources: + requests: { memory: "32Mi", cpu: "50m" } + limits: { memory: "64Mi", cpu: "100m" } + restartPolicy: Never + backoffLimit: 10 +``` + +### Hook annotations + +`pre-install,pre-upgrade` + +Runs the hook before an installation or upgrade. + +`hook-weight: "0"` + +Controls hook execution order when multiple hooks exist. + +`before-hook-creation` + +Removes the previous hook resource before creating another copy. + +### Local Kind observation + +For the local verification deployment, the hook was temporarily moved outside `templates/`. + +A fresh `pre-install` hook that waits for a normal MySQL Service has a lifecycle problem: normal chart resources have not necessarily been created yet when the pre-install hook runs. The chart already has BankApp init containers that wait for MySQL and Ollama, so those init containers provided dependency readiness during the local test. + +The hook was restored afterward as part of the Day 80 exercise. + +--- + +## Helm Test + +A Helm test was created: + +```text +bankapp/templates/tests/test-connection.yaml +``` + +```yaml +apiVersion: v1 +kind: Pod +metadata: + name: {{ include "bankapp.fullname" . }}-test + namespace: {{ .Release.Namespace }} + labels: + {{- include "bankapp.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: test + image: busybox:1.36 + command: ['sh', '-c', 'wget -qO- http://{{ include "bankapp.fullname" . }}-service:8080/actuator/health'] + restartPolicy: Never +``` + +The test calls the Spring Boot health endpoint: + +```text +/actuator/health +``` + +Command: + +```bash +helm test bankapp-dev -n dev +``` + +Actual result: + +```text +TEST SUITE: bankapp-dev-test +Phase: Succeeded +``` + +This confirmed that the BankApp Service could successfully serve the application's health endpoint. + +--- + +## Task 3 – Package the Helm Chart + +The chart was validated and packaged: + +```bash +helm lint bankapp/ +helm package bankapp/ +``` + +Result: + +```text +bankapp-0.1.0.tgz +``` + +The package is a portable Helm chart that can be distributed or stored in a chart repository. + +Conceptually: + +```text +bankapp/ + | + | helm package + v +bankapp-0.1.0.tgz +``` + +--- + +## Dev Deployment on Kind + +The dev environment was deployed into the `dev` namespace. + +Because the exercise's 256Mi MySQL memory limit caused MySQL to be OOMKilled in the local Kind environment, the local deployment used a 512Mi limit override: + +```bash +helm install bankapp-dev bankapp/ \ + -f bankapp/values-dev.yaml \ + --set mysql.resources.requests.memory=256Mi \ + --set mysql.resources.limits.memory=512Mi \ + -n dev \ + --timeout 5m +``` + +This keeps the exercise's environment values file unchanged while adapting the local runtime resources. + +### Final pod status + +```text +bankapp-dev-5755945795-btxqw 1/1 Running +bankapp-dev-mysql-579fd5cb9d-j8bdh 1/1 Running +bankapp-dev-ollama-6f997d754b-8d9m6 1/1 Running +``` + +### Persistent storage + +```text +bankapp-dev-mysql-pvc Bound 2Gi RWO standard +bankapp-dev-ollama-pvc Bound 5Gi RWO standard +``` + +### Application verification + +The BankApp logs showed: + +- Spring Boot started +- Tomcat initialized on port 8080 +- Spring Data JPA repositories initialized +- HikariCP started +- MySQL connection was successfully created + +The final Helm test succeeded. + +--- + +## Troubleshooting Performed + +### 1. MySQL OOMKilled + +Initial result: + +```text +bankapp-dev-mysql OOMKilled +``` + +Cause: + +The dev configuration limited MySQL to 256Mi memory, which was not enough in the local Kind environment. + +Local solution: + +```bash +--set mysql.resources.requests.memory=256Mi +--set mysql.resources.limits.memory=512Mi +``` + +After reinstalling with the override: + +```text +bankapp-dev-mysql 1/1 Running +``` + +### 2. Helm upgrade timed out + +An attempted upgrade returned: + +```text +etcdserver: request timed out +``` + +The Helm release then showed: + +```text +STATUS: pending-upgrade +``` + +A later operation returned: + +```text +another operation (install/upgrade/rollback) is in progress +``` + +The Kind nodes themselves remained `Ready`. + +For this local lab, the fastest clean recovery was to uninstall the stuck dev release and reinstall it with the corrected MySQL memory override. + +### 3. BankApp readiness initially failed + +The readiness probe temporarily returned: + +```text +connect: connection refused +``` + +The application was still starting. + +After Spring Boot and Tomcat finished initialization: + +```text +BankApp 1/1 Running +``` + +No probe change was required. + +--- + +## Task 4 – Helm in the GitOps CI/CD Pipeline + +### Current raw-manifest approach + +```text +Developer pushes code + | + v +GitHub Actions + | + v +Build Docker image + | + v +Tag image with Git commit SHA + | + v +Update k8s/bankapp-deployment.yml + | + v +Commit change to Git + | + v +ArgoCD detects change + | + v +Sync to EKS +``` + +### Helm-based GitOps approach + +```text +Developer pushes code + | + v +GitHub Actions + | + v +Build Docker image + | + v +Tag image with Git commit SHA + | + v +Update bankapp.image.tag +in values-prod.yaml + | + v +Commit values change + | + v +ArgoCD detects Git change + | + v +ArgoCD renders Helm chart + | + v +Kubernetes/EKS receives resources +``` + +A CI workflow can update only the image value instead of editing an entire Kubernetes Deployment manifest. + +Example: + +```yaml +- name: Update Helm values with new image tag + run: | + TAG=${{ steps.tag.outputs.sha_short }} + yq -i '.bankapp.image.tag = "'$TAG'"' helm-chart/bankapp/values-prod.yaml + +- name: Commit updated Helm values + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add helm-chart/bankapp/values-prod.yaml + git diff --staged --quiet || git commit -m "ci: update bankapp image to $TAG [skip ci]" + git push +``` + +### ArgoCD Helm configuration + +Instead of pointing ArgoCD to raw manifests: + +```yaml +source: + path: k8s +``` + +ArgoCD can point directly to the Helm chart: + +```yaml +source: + path: helm-chart/bankapp + helm: + valueFiles: + - values-prod.yaml +``` + +ArgoCD can render Helm templates and compare the desired Git state with the Kubernetes cluster. + +### Advantages of ArgoCD + Helm + +- One reusable chart +- Environment-specific values +- Less YAML duplication +- Git remains the source of truth +- Easier image-tag updates +- ArgoCD detects drift +- Helm provides templating and reusable configuration + +--- + +## Production Helm Practices + +A useful production deployment pattern is: + +```bash +helm upgrade --install bankapp bankapp/ \ + -f bankapp/values-prod.yaml \ + --set bankapp.image.tag=$GIT_SHA \ + -n bankapp \ + --create-namespace \ + --wait \ + --timeout 300s \ + --atomic +``` + +Important flags: + +- `--install` – install the release if it does not exist +- `--wait` – wait for Kubernetes resources to become ready +- `--timeout` – define how long Helm should wait +- `--atomic` – automatically roll back when an upgrade fails +- `--set bankapp.image.tag=$GIT_SHA` – deploy an exact application version + +### Helm diff + +Before an upgrade, `helm diff` can show what will change: + +```bash +helm plugin install https://github.com/databus23/helm-diff +helm diff upgrade bankapp bankapp/ -f bankapp/values-prod.yaml +``` + +--- + +## Production Secrets + +Real production passwords should **not** be committed in `values.yaml` or `values-prod.yaml`. + +Production options include: + +- External Secrets Operator with AWS Secrets Manager +- Sealed Secrets +- HashiCorp Vault +- CI/CD secret injection + +The passwords used in this lab are demonstration values only. + +--- + +## Helm vs Raw Manifests vs Kustomize + +| Approach | Best For | AI-BankApp Example | +|---|---|---| +| Raw manifests | Simple, single-environment deployments | Original `k8s/` directory | +| Helm | Complex applications and multiple environments | BankApp + MySQL + Ollama + HPA + hooks | +| Kustomize | Patching existing manifests without Helm-style templates | Environment overlays on the original `k8s/` manifests | + +For this AI-BankApp project, Helm is useful because the same application needs different configuration for dev, staging, and production. + +--- + +## Three-Day Helm Journey + +| Day | Concept | AI-BankApp Work | +|---|---|---| +| Day 78 | Helm repositories, install, values, upgrade and rollback | Deployed MySQL using a community Helm chart | +| Day 79 | Custom charts and Go templates | Converted the AI-BankApp Kubernetes resources into a reusable custom chart | +| Day 80 | Multi-environment values, hooks, tests, packaging and GitOps | Created dev/staging/prod configuration and connected the chart concept to CI/CD | + +--- + +## Commands Used + +```bash +# Validate +helm lint bankapp/ -f bankapp/values-dev.yaml +helm lint bankapp/ -f bankapp/values-staging.yaml +helm lint bankapp/ -f bankapp/values-prod.yaml + +# Render +helm template bankapp-dev bankapp/ -f bankapp/values-dev.yaml +helm template bankapp-staging bankapp/ -f bankapp/values-staging.yaml +helm template bankapp-prod bankapp/ -f bankapp/values-prod.yaml + +# Package +helm package bankapp/ + +# Deploy dev locally +helm install bankapp-dev bankapp/ \ + -f bankapp/values-dev.yaml \ + --set mysql.resources.requests.memory=256Mi \ + --set mysql.resources.limits.memory=512Mi \ + -n dev \ + --timeout 5m + +# Verify +helm list -A +kubectl get pods -n dev +kubectl get pvc -n dev + +# Test +helm test bankapp-dev -n dev + +# History / rollback troubleshooting +helm history bankapp-dev -n dev +helm status bankapp-dev -n dev +``` + +--- + +## Final Result + +Day 80 successfully demonstrated: + +```text +One Helm Chart + | + +---- Dev + | + +---- Staging + | + +---- Production + | + +---- Hooks + | + +---- Helm Test + | + +---- Packaged .tgz + | + +---- GitOps / CI-CD integration +``` + +Final local verification: + +```text +Helm release DEPLOYED +BankApp 1/1 Running +MySQL 1/1 Running +Ollama 1/1 Running +MySQL PVC Bound +Ollama PVC Bound +Helm test Succeeded +``` + +## Key Learning + +**Helm lets us maintain one reusable Kubernetes application package and change only the values needed for each environment. CI/CD can update the image tag in Git, and ArgoCD can detect that change, render the Helm chart, and synchronize Kubernetes with the desired state.** diff --git a/2026/day-81/AI-BankApp-DevOps b/2026/day-81/AI-BankApp-DevOps new file mode 160000 index 0000000000..d37fe11f2e --- /dev/null +++ b/2026/day-81/AI-BankApp-DevOps @@ -0,0 +1 @@ +Subproject commit d37fe11f2e754fee6d5857cd19d183ba9fdf0c7d diff --git a/2026/day-81/Screenshot 2026-10-06 125934.png b/2026/day-81/Screenshot 2026-10-06 125934.png new file mode 100644 index 0000000000..16c0009385 Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 125934.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 130129.png b/2026/day-81/Screenshot 2026-10-06 130129.png new file mode 100644 index 0000000000..a51875b3b1 Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 130129.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 132838.png b/2026/day-81/Screenshot 2026-10-06 132838.png new file mode 100644 index 0000000000..54e7302af2 Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 132838.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 133002.png b/2026/day-81/Screenshot 2026-10-06 133002.png new file mode 100644 index 0000000000..d819c3d28f Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 133002.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 133927.png b/2026/day-81/Screenshot 2026-10-06 133927.png new file mode 100644 index 0000000000..554abc7ad4 Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 133927.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 134737.png b/2026/day-81/Screenshot 2026-10-06 134737.png new file mode 100644 index 0000000000..a5cb96aa0c Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 134737.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 135244.png b/2026/day-81/Screenshot 2026-10-06 135244.png new file mode 100644 index 0000000000..2b872aaeea Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 135244.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 135337.png b/2026/day-81/Screenshot 2026-10-06 135337.png new file mode 100644 index 0000000000..b5d1478324 Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 135337.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 135426.png b/2026/day-81/Screenshot 2026-10-06 135426.png new file mode 100644 index 0000000000..ec1cebfbab Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 135426.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 140452.png b/2026/day-81/Screenshot 2026-10-06 140452.png new file mode 100644 index 0000000000..556aa83a91 Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 140452.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 140948.png b/2026/day-81/Screenshot 2026-10-06 140948.png new file mode 100644 index 0000000000..0e133408c2 Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 140948.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 141952.png b/2026/day-81/Screenshot 2026-10-06 141952.png new file mode 100644 index 0000000000..08af643e01 Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 141952.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 142800.png b/2026/day-81/Screenshot 2026-10-06 142800.png new file mode 100644 index 0000000000..0b78ccf730 Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 142800.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 142900.png b/2026/day-81/Screenshot 2026-10-06 142900.png new file mode 100644 index 0000000000..0aedd0845e Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 142900.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 143002.png b/2026/day-81/Screenshot 2026-10-06 143002.png new file mode 100644 index 0000000000..34a2ba9404 Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 143002.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 143306.png b/2026/day-81/Screenshot 2026-10-06 143306.png new file mode 100644 index 0000000000..53155c8ef6 Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 143306.png differ diff --git a/2026/day-81/Screenshot 2026-10-06 143453.png b/2026/day-81/Screenshot 2026-10-06 143453.png new file mode 100644 index 0000000000..c235d7a126 Binary files /dev/null and b/2026/day-81/Screenshot 2026-10-06 143453.png differ diff --git a/2026/day-81/Three-Day Kubernetes Study Notes.png b/2026/day-81/Three-Day Kubernetes Study Notes.png new file mode 100644 index 0000000000..49c5f4cecd Binary files /dev/null and b/2026/day-81/Three-Day Kubernetes Study Notes.png differ diff --git a/2026/day-81/day-81-eks-terraform.md b/2026/day-81/day-81-eks-terraform.md new file mode 100644 index 0000000000..14aebc9115 --- /dev/null +++ b/2026/day-81/day-81-eks-terraform.md @@ -0,0 +1,106 @@ +# Day 81 -- Amazon EKS with Terraform + +## Goal + +Build a production-style Amazon EKS environment using Terraform and +deploy the AI BankApp application. + +## Architecture + +`Terraform → AWS VPC → EKS Cluster → Worker Nodes → Kubernetes Workloads` + +## Infrastructure Created + +- Amazon EKS cluster `bankapp-eks`, Kubernetes 1.35, region + `us-west-2` +- Managed node group using `t3.medium`, with 3 worker nodes +- VPC across 3 Availability Zones with public, private, and intra + subnets +- NAT Gateway, IAM roles, security groups, and KMS encryption +- Argo CD installed through Terraform/Helm + +## EKS Add-ons + +- **VPC CNI** -- Pod networking with VPC IP addresses +- **CoreDNS** -- Kubernetes DNS +- **kube-proxy** -- Service networking +- **EBS CSI Driver** -- dynamic Amazon EBS storage +- **Metrics Server** -- CPU/memory metrics for `kubectl top` and HPA +- **EKS Pod Identity Agent** -- AWS permissions support for workloads + +## Application Deployment + +The Kubernetes application contained Spring Boot BankApp, MySQL, Ollama, +ConfigMap, Secret, Services, persistent storage, and HPA. + +The ConfigMap supplied non-sensitive settings such as MySQL host/port, +database name, and Ollama URL. Kubernetes Secret supplied sensitive +application configuration. Base64 is encoding, not encryption; +production environments should use a stronger secret-management approach +such as AWS Secrets Manager/External Secrets. + +## Persistent Storage + +A `gp3` StorageClass used the EBS CSI driver: + +`PVC → gp3 StorageClass → EBS CSI Driver → AWS EBS → PV` + +- MySQL: 5 GiB +- Ollama: 10 GiB + +Both PVCs became `Bound`. + +## MySQL and Ollama + +MySQL 8.0 used persistent storage at `/var/lib/mysql` and +readiness/liveness checks. + +Ollama exposed port `11434`, stored data on a 10 GiB volume, and pulled +the `tinyllama` model. + +## BankApp + +The Spring Boot deployment used init containers to wait for MySQL and +Ollama before starting. It exposed port `8080` and used +`/actuator/health` for health checks. + +## Horizontal Pod Autoscaler + +- Minimum Pods: 2 +- Maximum Pods: 4 +- CPU target: 70% + +The application initially ran 4 replicas and HPA later reduced it to 2 +because CPU usage was low. HPA scales Pods; node autoscaling requires a +separate solution such as Karpenter or Cluster Autoscaler. + +## Validation + +``` bash +kubectl get nodes +kubectl get pods -A +kubectl get pvc -n bankapp +kubectl top pods -n bankapp +kubectl port-forward -n bankapp svc/bankapp-service 8080:8080 +``` + +All three worker nodes became Ready, system components were healthy, and +the BankApp ran successfully. The login page opened and a test +registration succeeded, confirming application-to-database +communication. + +## Interview Explanation + +> I provisioned an Amazon EKS cluster using Terraform in a multi-AZ VPC. +> I used an EKS managed node group and deployed a Spring Boot +> application with MySQL and Ollama. I configured dynamic EBS storage, +> health checks, Services, ConfigMaps, Secrets, Metrics Server, HPA, and +> Argo CD. + +## Result + +- EKS and 3 worker nodes working +- BankApp, MySQL, and Ollama deployed +- Dynamic EBS storage working +- Metrics Server and HPA working +- Argo CD installed diff --git a/2026/day-82/AI-BankApp-DevOps b/2026/day-82/AI-BankApp-DevOps new file mode 160000 index 0000000000..d37fe11f2e --- /dev/null +++ b/2026/day-82/AI-BankApp-DevOps @@ -0,0 +1 @@ +Subproject commit d37fe11f2e754fee6d5857cd19d183ba9fdf0c7d diff --git a/2026/day-82/Screenshot 2026-10-06 125934.png b/2026/day-82/Screenshot 2026-10-06 125934.png new file mode 100644 index 0000000000..16c0009385 Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 125934.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 130129.png b/2026/day-82/Screenshot 2026-10-06 130129.png new file mode 100644 index 0000000000..a51875b3b1 Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 130129.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 132838.png b/2026/day-82/Screenshot 2026-10-06 132838.png new file mode 100644 index 0000000000..54e7302af2 Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 132838.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 133002.png b/2026/day-82/Screenshot 2026-10-06 133002.png new file mode 100644 index 0000000000..d819c3d28f Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 133002.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 133927.png b/2026/day-82/Screenshot 2026-10-06 133927.png new file mode 100644 index 0000000000..554abc7ad4 Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 133927.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 134737.png b/2026/day-82/Screenshot 2026-10-06 134737.png new file mode 100644 index 0000000000..a5cb96aa0c Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 134737.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 135244.png b/2026/day-82/Screenshot 2026-10-06 135244.png new file mode 100644 index 0000000000..2b872aaeea Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 135244.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 135337.png b/2026/day-82/Screenshot 2026-10-06 135337.png new file mode 100644 index 0000000000..b5d1478324 Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 135337.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 135426.png b/2026/day-82/Screenshot 2026-10-06 135426.png new file mode 100644 index 0000000000..ec1cebfbab Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 135426.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 140452.png b/2026/day-82/Screenshot 2026-10-06 140452.png new file mode 100644 index 0000000000..556aa83a91 Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 140452.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 140948.png b/2026/day-82/Screenshot 2026-10-06 140948.png new file mode 100644 index 0000000000..0e133408c2 Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 140948.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 141952.png b/2026/day-82/Screenshot 2026-10-06 141952.png new file mode 100644 index 0000000000..08af643e01 Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 141952.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 142800.png b/2026/day-82/Screenshot 2026-10-06 142800.png new file mode 100644 index 0000000000..0b78ccf730 Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 142800.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 142900.png b/2026/day-82/Screenshot 2026-10-06 142900.png new file mode 100644 index 0000000000..0aedd0845e Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 142900.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 143002.png b/2026/day-82/Screenshot 2026-10-06 143002.png new file mode 100644 index 0000000000..34a2ba9404 Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 143002.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 143306.png b/2026/day-82/Screenshot 2026-10-06 143306.png new file mode 100644 index 0000000000..53155c8ef6 Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 143306.png differ diff --git a/2026/day-82/Screenshot 2026-10-06 143453.png b/2026/day-82/Screenshot 2026-10-06 143453.png new file mode 100644 index 0000000000..c235d7a126 Binary files /dev/null and b/2026/day-82/Screenshot 2026-10-06 143453.png differ diff --git a/2026/day-82/Three-Day Kubernetes Study Notes.png b/2026/day-82/Three-Day Kubernetes Study Notes.png new file mode 100644 index 0000000000..49c5f4cecd Binary files /dev/null and b/2026/day-82/Three-Day Kubernetes Study Notes.png differ diff --git a/2026/day-82/day-82-eks-gateway-https.md b/2026/day-82/day-82-eks-gateway-https.md new file mode 100644 index 0000000000..6b520705d2 --- /dev/null +++ b/2026/day-82/day-82-eks-gateway-https.md @@ -0,0 +1,92 @@ +# Day 82 -- Envoy Gateway, Gateway API and HTTPS + +## Goal + +Expose the AI BankApp securely to the internet using Kubernetes Gateway +API, Envoy Gateway, an AWS Load Balancer, cert-manager, and Let's +Encrypt. + +## Traffic Flow + +`Internet → AWS Load Balancer → Envoy Gateway → HTTPRoute → BankApp Service → BankApp Pods` + +## Gateway API + +The lab used: - `GatewayClass` - `Gateway` - `HTTPRoute` - +`BackendTrafficPolicy` + +Envoy Gateway was installed using Helm. The `envoy-gateway` GatewayClass +was accepted successfully. + +## AWS Load Balancer + +Creating the Gateway caused Envoy to create a Kubernetes `LoadBalancer` +Service, which provisioned an external AWS load balancer. + +## HTTPRoute + +The HTTPRoute sent requests to `bankapp-service:8080`. + +It reported: - `Accepted=True` - `ResolvedRefs=True` + +An HTTP test returned `302 Found` and redirected to `/login`, proving +the complete external routing path worked. + +## cert-manager and Let's Encrypt + +cert-manager was installed with Helm with Gateway API support enabled. A +production Let's Encrypt `ClusterIssuer` used the ACME HTTP-01 challenge +and became `READY=True`. + +For the lab, `nip.io` supplied a hostname mapped to the load balancer +IP, allowing certificate validation without configuring a separate DNS +domain. + +The TLS certificate became Ready after the ACME challenge completed. + +## HTTPS Validation + +The HTTPS endpoint returned `HTTP/2 302` with a redirect to `/login`. + +This proved: - HTTPS/TLS worked - the certificate was valid - Envoy +routing worked - BankApp was reachable + +## Session Affinity + +An Envoy `BackendTrafficPolicy` provided cookie-based session affinity. +The HTTPS response contained the BankApp affinity cookie, confirming the +policy worked. + +## Storage Check + +- MySQL PVC: Bound, 5 GiB, gp3 +- Ollama PVC: Bound, 10 GiB, gp3 + +## Useful Commands + +``` bash +kubectl get gatewayclass +kubectl get gateway -n bankapp +kubectl get httproute -n bankapp +kubectl get certificate -n bankapp +kubectl get clusterissuer +kubectl get pvc -n bankapp +``` + +## Interview Explanation + +> I exposed my EKS application using Kubernetes Gateway API and Envoy +> Gateway. Envoy created an AWS external load balancer, and HTTPRoute +> sent traffic to the Spring Boot Service. I installed cert-manager and +> configured Let's Encrypt HTTPS, then added cookie-based session +> affinity with an Envoy BackendTrafficPolicy. + +## Result + +- Gateway API and Envoy Gateway working +- AWS external Load Balancer created +- HTTPRoute validated +- cert-manager and Let's Encrypt working +- HTTPS working +- Cookie session affinity working +- Persistent storage verified diff --git a/2026/day-83/AI-BankApp-DevOps b/2026/day-83/AI-BankApp-DevOps new file mode 160000 index 0000000000..d37fe11f2e --- /dev/null +++ b/2026/day-83/AI-BankApp-DevOps @@ -0,0 +1 @@ +Subproject commit d37fe11f2e754fee6d5857cd19d183ba9fdf0c7d diff --git a/2026/day-83/Screenshot 2026-10-06 125934.png b/2026/day-83/Screenshot 2026-10-06 125934.png new file mode 100644 index 0000000000..16c0009385 Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 125934.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 130129.png b/2026/day-83/Screenshot 2026-10-06 130129.png new file mode 100644 index 0000000000..a51875b3b1 Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 130129.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 132838.png b/2026/day-83/Screenshot 2026-10-06 132838.png new file mode 100644 index 0000000000..54e7302af2 Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 132838.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 133002.png b/2026/day-83/Screenshot 2026-10-06 133002.png new file mode 100644 index 0000000000..d819c3d28f Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 133002.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 133927.png b/2026/day-83/Screenshot 2026-10-06 133927.png new file mode 100644 index 0000000000..554abc7ad4 Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 133927.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 134737.png b/2026/day-83/Screenshot 2026-10-06 134737.png new file mode 100644 index 0000000000..a5cb96aa0c Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 134737.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 135244.png b/2026/day-83/Screenshot 2026-10-06 135244.png new file mode 100644 index 0000000000..2b872aaeea Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 135244.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 135337.png b/2026/day-83/Screenshot 2026-10-06 135337.png new file mode 100644 index 0000000000..b5d1478324 Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 135337.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 135426.png b/2026/day-83/Screenshot 2026-10-06 135426.png new file mode 100644 index 0000000000..ec1cebfbab Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 135426.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 140452.png b/2026/day-83/Screenshot 2026-10-06 140452.png new file mode 100644 index 0000000000..556aa83a91 Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 140452.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 140948.png b/2026/day-83/Screenshot 2026-10-06 140948.png new file mode 100644 index 0000000000..0e133408c2 Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 140948.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 141952.png b/2026/day-83/Screenshot 2026-10-06 141952.png new file mode 100644 index 0000000000..08af643e01 Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 141952.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 142800.png b/2026/day-83/Screenshot 2026-10-06 142800.png new file mode 100644 index 0000000000..0b78ccf730 Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 142800.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 142900.png b/2026/day-83/Screenshot 2026-10-06 142900.png new file mode 100644 index 0000000000..0aedd0845e Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 142900.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 143002.png b/2026/day-83/Screenshot 2026-10-06 143002.png new file mode 100644 index 0000000000..34a2ba9404 Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 143002.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 143306.png b/2026/day-83/Screenshot 2026-10-06 143306.png new file mode 100644 index 0000000000..53155c8ef6 Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 143306.png differ diff --git a/2026/day-83/Screenshot 2026-10-06 143453.png b/2026/day-83/Screenshot 2026-10-06 143453.png new file mode 100644 index 0000000000..c235d7a126 Binary files /dev/null and b/2026/day-83/Screenshot 2026-10-06 143453.png differ diff --git a/2026/day-83/Three-Day Kubernetes Study Notes.png b/2026/day-83/Three-Day Kubernetes Study Notes.png new file mode 100644 index 0000000000..49c5f4cecd Binary files /dev/null and b/2026/day-83/Three-Day Kubernetes Study Notes.png differ diff --git a/2026/day-83/day-83-eks-monitoring.md b/2026/day-83/day-83-eks-monitoring.md new file mode 100644 index 0000000000..9f89d09b2f --- /dev/null +++ b/2026/day-83/day-83-eks-monitoring.md @@ -0,0 +1,121 @@ +# Day 83 -- Prometheus and Grafana Monitoring on EKS + +## Goal + +Monitor the AI BankApp EKS environment using Prometheus, Grafana, Spring +Boot Actuator, Micrometer, and ServiceMonitor. + +## Metrics Flow + +`Spring Boot → Actuator/Micrometer → /actuator/prometheus → ServiceMonitor → Prometheus → Grafana` + +## Spring Boot Metrics + +The application contained: - `spring-boot-starter-actuator` - +`micrometer-registry-prometheus` + +Metrics were exposed at `/actuator/prometheus`. + +## kube-prometheus-stack + +The Prometheus Community `kube-prometheus-stack` Helm chart was +installed in the `monitoring` namespace. + +It provided Prometheus, Grafana, Alertmanager, Prometheus Operator, +kube-state-metrics, and node-exporter. + +## Verify Application Metrics + +``` bash +kubectl port-forward -n bankapp svc/bankapp-service 8080:8080 +curl -s http://localhost:8080/actuator/prometheus +``` + +The endpoint returned application startup/ready time, disk, and HikariCP +database connection metrics. + +## ServiceMonitor + +The BankApp Service port was named `http`. A `ServiceMonitor` named +`bankapp-monitor` was created in the `monitoring` namespace and +configured to scrape `/actuator/prometheus` every 30 seconds from the +BankApp Service. + +## Prometheus Validation + +``` bash +kubectl port-forward -n monitoring svc/monitoring-kube-prometheus-prometheus 9090:9090 +``` + +Prometheus discovered the BankApp targets. + +PromQL: + +``` promql +application_ready_time_seconds +``` + +The query returned two series corresponding to the two BankApp Pods +running after HPA scaling. This proved: + +`BankApp → ServiceMonitor → Prometheus` + +## Grafana + +Grafana's Kubernetes dashboard displayed cluster CPU, memory, resource +requests/limits, and namespace usage. This confirmed successful +visualization of EKS/Kubernetes metrics. + +> Before sharing screenshots publicly, crop or blur passwords, AWS +> account information, credentials, and other sensitive information. + +## Useful Commands + +``` bash +kubectl get pods -n monitoring +kubectl get servicemonitor -n monitoring +kubectl get endpoints bankapp-service -n bankapp +kubectl top pods -n bankapp +``` + +## Cleanup + +To avoid AWS cost, Kubernetes-created cloud resources were removed +before Terraform infrastructure. + +The BankApp namespace was deleted, which removed the Gateway and PVCs. +The Envoy LoadBalancer Service disappeared and `kubectl get pv` returned +no resources. Manually installed monitoring, cert-manager, and Envoy +Helm releases were then uninstalled. + +Finally: + +``` bash +terraform plan -destroy +terraform destroy +``` + +The destroy plan showed 84 resources and Terraform completed +successfully with all 84 destroyed. The AWS EKS console then showed zero +clusters, and the EC2 console showed no running worker instances in +`us-west-2`. + +## Interview Explanation + +> I implemented EKS monitoring using kube-prometheus-stack. Spring Boot +> exposed metrics through Actuator and Micrometer. I created a +> ServiceMonitor so Prometheus could scrape the application, verified +> the metrics using PromQL, and used Grafana Kubernetes dashboards to +> monitor CPU, memory, and workloads. + +## Result + +- Prometheus and Grafana working +- Actuator/Micrometer metrics exposed +- ServiceMonitor working +- BankApp targets discovered +- PromQL verified application metrics +- Grafana Kubernetes dashboard verified +- AWS Load Balancer and EBS-backed PVs cleaned up +- Terraform destroyed 84 infrastructure resources +- EKS deletion and no running EC2 workers verified diff --git a/2026/day-84/AI-BankApp-DevOps b/2026/day-84/AI-BankApp-DevOps new file mode 160000 index 0000000000..81c9186ca8 --- /dev/null +++ b/2026/day-84/AI-BankApp-DevOps @@ -0,0 +1 @@ +Subproject commit 81c9186ca86a72fa3e712f9bf890d095419e2ec8 diff --git a/2026/day-84/DAY-84-ArgoCD Self-Healing Guide.png b/2026/day-84/DAY-84-ArgoCD Self-Healing Guide.png new file mode 100644 index 0000000000..ffbd37d9f5 Binary files /dev/null and b/2026/day-84/DAY-84-ArgoCD Self-Healing Guide.png differ diff --git a/2026/day-84/Screenshot 2026-10-07 131141.png b/2026/day-84/Screenshot 2026-10-07 131141.png new file mode 100644 index 0000000000..9ee67b9f98 Binary files /dev/null and b/2026/day-84/Screenshot 2026-10-07 131141.png differ diff --git a/2026/day-84/Screenshot 2026-10-07 132057.png b/2026/day-84/Screenshot 2026-10-07 132057.png new file mode 100644 index 0000000000..11af88ea4f Binary files /dev/null and b/2026/day-84/Screenshot 2026-10-07 132057.png differ diff --git a/2026/day-84/Screenshot 2026-10-07 132322.png b/2026/day-84/Screenshot 2026-10-07 132322.png new file mode 100644 index 0000000000..68ae86cf4d Binary files /dev/null and b/2026/day-84/Screenshot 2026-10-07 132322.png differ diff --git a/2026/day-84/Screenshot 2026-10-07 134141.png b/2026/day-84/Screenshot 2026-10-07 134141.png new file mode 100644 index 0000000000..55942cabc9 Binary files /dev/null and b/2026/day-84/Screenshot 2026-10-07 134141.png differ diff --git a/2026/day-84/day-84-gitops-argocd.md b/2026/day-84/day-84-gitops-argocd.md new file mode 100644 index 0000000000..02eb02a071 --- /dev/null +++ b/2026/day-84/day-84-gitops-argocd.md @@ -0,0 +1,155 @@ +# Day 84 -- GitOps with ArgoCD + +## Objective + +Implement GitOps for the **Asma AI BankApp** on Amazon EKS using ArgoCD. +Git is the source of truth, and ArgoCD synchronizes the desired +Kubernetes state. + +## Architecture + +``` text +Developer → GitHub → ArgoCD → Amazon EKS + ├─ BankApp + ├─ MySQL + ├─ Ollama + ├─ HPA + └─ Gateway / HTTPS +``` + +## ArgoCD Application + +The Application watches the `feat/gitops` branch and the `k8s` +directory. + +``` yaml +syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true +``` + +- `prune: true` removes managed resources that are removed from Git. +- `selfHeal: true` corrects manual cluster drift. +- `CreateNamespace=true` allows creation of the destination namespace. +- `ServerSideApply=true` uses Kubernetes server-side apply. + +## HPA Replica Drift Problem + +ArgoCD became `OutOfSync` while the application itself was healthy. The +HPA was dynamically changing the BankApp Deployment replica count while +ArgoCD compared that value with Git. + +``` text +ArgoCD → Deployment configuration +HPA → dynamic replica count +``` + +### Fix + +ArgoCD was told to ignore the HPA-managed replica field: + +``` yaml +ignoreDifferences: + - group: apps + kind: Deployment + name: bankapp + namespace: bankapp + jsonPointers: + - /spec/replicas +``` + +After applying the fix, the Application returned to: + +``` text +SYNC STATUS HEALTH STATUS +Synced Healthy +``` + +The configuration change was committed and pushed to the GitOps branch. + +## Self-Healing Test + +The BankApp Service was intentionally deleted from the live cluster: + +``` bash +kubectl delete service bankapp-service -n bankapp +``` + +The Service still existed in Git, so ArgoCD detected drift and +automatically recreated it. + +``` text +Git says Service exists + ↓ +Service manually deleted from EKS + ↓ +ArgoCD detects drift + ↓ +selfHeal: true + ↓ +Service recreated automatically +``` + +No manual recreation was performed. + +Final verification: + +``` bash +kubectl get applications -n argocd +``` + +Result: + +``` text +SYNC STATUS HEALTH STATUS +Synced Healthy +``` + +## GitOps Flow + +``` text +Change → Commit → Push to GitHub + ↓ + ArgoCD + ↓ + Compare Git with EKS + ↓ + Synchronize + ↓ + Desired state +``` + +## Important Lessons + +1. **Git is the source of truth.** +2. **Synced** means the live configuration matches the desired Git + state. +3. **Healthy** means the deployed resources are operating normally. +4. HPA and ArgoCD need clear ownership; HPA owns replica scaling. +5. ArgoCD self-healing can automatically correct manual Kubernetes + drift. + +## Interview Answer + +**What is GitOps?** + +GitOps is a way of managing Kubernetes where Git is the source of truth. +ArgoCD continuously compares the configuration in Git with the live +cluster and automatically synchronizes changes or corrects configuration +drift. + +## Day 84 Result + +- ArgoCD configured for Asma AI BankApp +- Git used as source of truth +- Automated sync and self-healing enabled +- HPA replica drift identified and fixed +- Self-healing successfully tested +- Deleted Service automatically restored +- Final ArgoCD status: **Synced + Healthy** + +**Day 84 -- GitOps with ArgoCD: COMPLETE** diff --git a/2026/day-85/ArgoCD GitOps Deep Dive Notes.png b/2026/day-85/ArgoCD GitOps Deep Dive Notes.png new file mode 100644 index 0000000000..4bc6ac9ebb Binary files /dev/null and b/2026/day-85/ArgoCD GitOps Deep Dive Notes.png differ diff --git a/2026/day-85/Screenshot 2026-10-07 134839.png b/2026/day-85/Screenshot 2026-10-07 134839.png new file mode 100644 index 0000000000..49c0acf912 Binary files /dev/null and b/2026/day-85/Screenshot 2026-10-07 134839.png differ diff --git a/2026/day-85/Screenshot 2026-10-07 135720.png b/2026/day-85/Screenshot 2026-10-07 135720.png new file mode 100644 index 0000000000..a193f7f3cc Binary files /dev/null and b/2026/day-85/Screenshot 2026-10-07 135720.png differ diff --git a/2026/day-85/Screenshot 2026-10-07 141623.png b/2026/day-85/Screenshot 2026-10-07 141623.png new file mode 100644 index 0000000000..b413ea9f0c Binary files /dev/null and b/2026/day-85/Screenshot 2026-10-07 141623.png differ diff --git a/2026/day-85/Screenshot 2026-10-07 141829.png b/2026/day-85/Screenshot 2026-10-07 141829.png new file mode 100644 index 0000000000..1dabb7ddb7 Binary files /dev/null and b/2026/day-85/Screenshot 2026-10-07 141829.png differ diff --git a/2026/day-85/Screenshot 2026-10-07 141938.png b/2026/day-85/Screenshot 2026-10-07 141938.png new file mode 100644 index 0000000000..c4d0f1068c Binary files /dev/null and b/2026/day-85/Screenshot 2026-10-07 141938.png differ diff --git a/2026/day-85/Screenshot 2026-10-07 142040.png b/2026/day-85/Screenshot 2026-10-07 142040.png new file mode 100644 index 0000000000..26526bfd12 Binary files /dev/null and b/2026/day-85/Screenshot 2026-10-07 142040.png differ diff --git a/2026/day-85/Screenshot 2026-10-07 142148.png b/2026/day-85/Screenshot 2026-10-07 142148.png new file mode 100644 index 0000000000..332f90227f Binary files /dev/null and b/2026/day-85/Screenshot 2026-10-07 142148.png differ diff --git a/2026/day-85/Screenshot 2026-10-07 142504.png b/2026/day-85/Screenshot 2026-10-07 142504.png new file mode 100644 index 0000000000..b5beeec40c Binary files /dev/null and b/2026/day-85/Screenshot 2026-10-07 142504.png differ diff --git a/2026/day-85/Screenshot 2026-10-07 142548.png b/2026/day-85/Screenshot 2026-10-07 142548.png new file mode 100644 index 0000000000..0ddd8fd395 Binary files /dev/null and b/2026/day-85/Screenshot 2026-10-07 142548.png differ diff --git a/2026/day-85/Screenshot 2026-10-07 143125.png b/2026/day-85/Screenshot 2026-10-07 143125.png new file mode 100644 index 0000000000..4dbefd7e39 Binary files /dev/null and b/2026/day-85/Screenshot 2026-10-07 143125.png differ diff --git a/2026/day-85/Screenshot 2026-10-07 143300.png b/2026/day-85/Screenshot 2026-10-07 143300.png new file mode 100644 index 0000000000..3a45589e64 Binary files /dev/null and b/2026/day-85/Screenshot 2026-10-07 143300.png differ diff --git a/2026/day-85/day-85-argocd-deep-dive.md b/2026/day-85/day-85-argocd-deep-dive.md new file mode 100644 index 0000000000..3c49e4efca --- /dev/null +++ b/2026/day-85/day-85-argocd-deep-dive.md @@ -0,0 +1,502 @@ +# Day 85 - ArgoCD Deep Dive: GitOps Operations + +## Objective + +Day 85 focused on deeper ArgoCD operations for the **Asma AI BankApp** running on Amazon EKS. + +The goal was to practice: + +- Automated vs manual synchronization +- ArgoCD sync waves +- Rollback and recovery +- App of Apps pattern +- ArgoCD Notifications +- ArgoCD Projects and RBAC concepts + +--- + +## Architecture + +```text +GitHub Repository + | + v + ArgoCD + | + v + Amazon EKS + | + v +Asma AI BankApp +``` + +Git is the source of truth. ArgoCD compares the desired configuration in Git with the live Kubernetes cluster. + +--- + +# Task 1 - Automated vs Manual Sync + +The BankApp ArgoCD Application normally uses automated synchronization with: + +```yaml +syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true +``` + +### Manual Sync Test + +Automatic synchronization was temporarily disabled. + +A harmless ConfigMap change was committed to Git. + +ArgoCD detected the difference and showed: + +```text +OutOfSync +Healthy +``` + +Because auto-sync was disabled, ArgoCD did not automatically apply the Git change. + +The application was then manually synchronized. + +After synchronization: + +```text +Synced +Healthy +``` + +Finally, automatic synchronization was restored. + +### Key Learning + +- **Manual Sync:** ArgoCD detects Git changes but waits for a person to synchronize. +- **Auto Sync:** ArgoCD automatically applies Git changes. +- **Prune:** Removes ArgoCD-managed Kubernetes resources that were removed from Git. +- **Self Heal:** Corrects configuration drift when the live cluster is changed manually. + +--- + +# Task 2 - ArgoCD Sync Waves + +Sync waves control the order in which Kubernetes resources are applied. + +The annotation used is: + +```yaml +metadata: + annotations: + argocd.argoproj.io/sync-wave: "0" +``` + +The following order was configured: + +| Wave | Resources | +|---|---| +| `-2` | Namespace, StorageClass | +| `-1` | PersistentVolumeClaims, ConfigMap, Secret | +| `0` | MySQL Deployment, Ollama Deployment, Services | +| `1` | BankApp Deployment | +| `2` | HorizontalPodAutoscaler | + +### Deployment Flow + +```text +Wave -2 +Namespace + StorageClass + | + v +Wave -1 +PVC + ConfigMap + Secret + | + v +Wave 0 +MySQL + Ollama + Services + | + v +Wave 1 +BankApp + | + v +Wave 2 +HPA +``` + +This ensures dependencies are created before the resources that need them. + +The sync-wave configuration was committed to Git, and ArgoCD synchronized the application successfully. + +Final status: + +```text +Synced / Healthy +``` + +### Key Learning + +ArgoCD processes lower-numbered waves before higher-numbered waves. Resources in the same wave can be processed together. + +--- + +# Task 3 - ArgoCD Rollback + +ArgoCD deployment history was reviewed. + +The current sync-wave deployment and the previous application revision were identified. + +Before rollback, automatic synchronization was disabled so ArgoCD would not immediately restore the newest Git revision. + +The application was rolled back from the current deployment to the previous revision using the ArgoCD UI. + +After rollback: + +```text +OutOfSync / Healthy +``` + +This was expected. + +### Why OutOfSync? + +Git still contained the newest configuration, but the live Kubernetes cluster had been rolled back to an older revision. + +```text +Git = New version +Cluster = Previous version + +Git != Cluster + +Result: OutOfSync +``` + +The application remained healthy because the older version was still a working deployment. + +The latest Git version was then manually synchronized again. + +Final result: + +```text +Synced / Healthy +``` + +Automatic synchronization was restored with: + +```text +prune = true +selfHeal = true +``` + +### Rollback vs Git Revert + +**ArgoCD Rollback** + +```text +Changes the live cluster to an older deployed revision. +Git itself does not change. +``` + +**Git Revert** + +```text +Creates a new Git commit that reverses an earlier change. +ArgoCD then synchronizes the cluster to that Git state. +``` + +For GitOps, Git remains the source of truth. + +### Interview Explanation + +> An ArgoCD rollback can temporarily restore an older cluster revision, but it does not change Git. If Git still contains the newer configuration, the application becomes OutOfSync. In a GitOps workflow, a Git revert is the preferred way to permanently roll back desired state. + +--- + +# Task 4 - App of Apps Pattern + +The App of Apps pattern was implemented. + +Two ArgoCD Applications were used: + +```text +Git + | + v +asma-app-of-apps +Parent Application + | + v +asma-ai-bankapp +Child Application + | + v +Kubernetes Resources +``` + +The parent application watches the `argocd-apps` directory. + +The child application manages the BankApp resources from the `k8s` directory. + +Both applications were verified: + +```text +asma-ai-bankapp Synced Healthy +asma-app-of-apps Synced Healthy +``` + +### Why App of Apps? + +Instead of manually creating many ArgoCD Applications, one parent Application can manage multiple child Applications. + +Example: + +```text +Root Application + | + +-- BankApp + | + +-- Monitoring + | + +-- Gateway + | + +-- Other Applications +``` + +### Important Lab Decision + +Existing cert-manager and Envoy Gateway installations were left under their existing Helm management instead of forcing ArgoCD to take ownership during this lab. + +This avoided unnecessary ownership conflicts with working cluster components. + +--- + +# Task 5 - ArgoCD Notifications + +The ArgoCD Notifications Controller was verified as running. + +A notification template was added: + +```yaml +template.app-sync-status: | + message: | + Application {{.app.metadata.name}} sync status is {{.app.status.sync.status}}. +``` + +A trigger was also configured: + +```yaml +trigger.on-sync-status: | + - description: Application sync status changed + send: + - app-sync-status + when: app.status.sync.status == 'Synced' +``` + +### Notification Flow + +```text +Application becomes Synced + | + v +ArgoCD trigger matches + | + v +Notification template + | + v +Configured notification service +``` + +No real Slack, email, or webhook recipient was connected during this lab, so no external notification delivery was claimed. + +### Key Learning + +ArgoCD Notifications consists of: + +- **Trigger** - decides when a notification should happen. +- **Template** - defines the message. +- **Service/Recipient** - defines where the message is delivered. + +--- + +# Task 6 - ArgoCD Projects and RBAC + +An ArgoCD AppProject was created for the Day 85 security demonstration. + +Project: + +```text +asma-devops-project +``` + +The project was restricted to the BankApp Git repository and BankApp namespace. + +Conceptually: + +```text +AppProject + | + +-- Which Git repositories are allowed? + | + +-- Which Kubernetes destinations are allowed? +``` + +### Why AppProject? + +AppProjects create security boundaries between ArgoCD applications. + +They can restrict: + +- Source Git repositories +- Destination clusters +- Destination namespaces +- Kubernetes resource types +- Application access + +### RBAC Concept + +RBAC means **Role-Based Access Control**. + +It controls what a user or team is allowed to do. + +Example: + +```text +Developer + -> View applications + -> Sync approved applications + +Admin + -> Create applications + -> Delete applications + -> Manage projects + -> Manage ArgoCD configuration +``` + +For safety, the working BankApp was not moved into the restrictive demo project during this lab because the application also uses cluster-scoped resources. + +--- + +# HPA and ArgoCD Drift Handling + +The BankApp uses a HorizontalPodAutoscaler. + +HPA can change: + +```yaml +spec: + replicas: +``` + +If ArgoCD continuously compares this field with Git, it may fight with the HPA. + +The Application therefore ignores the BankApp Deployment replica difference: + +```yaml +ignoreDifferences: + - group: apps + kind: Deployment + name: bankapp + namespace: bankapp + jsonPointers: + - /spec/replicas +``` + +### Key Learning + +```text +Git defines application configuration +HPA controls runtime replica count +ArgoCD ignores only the replica difference +``` + +This allows GitOps and Kubernetes autoscaling to work together. + +--- + +# Day 85 Final Verification + +At the end of the lab: + +```text +BankApp: Synced / Healthy +App of Apps: Synced / Healthy +Auto Sync: Enabled +Prune: Enabled +Self Heal: Enabled +Notifications Controller: Running +AppProject: Created +``` + +The application remained operational after the rollback test and was restored to the latest Git state. + +--- + +# Important Commands Used + +Check application: + +```bash +kubectl get application asma-ai-bankapp -n argocd +``` + +Check ArgoCD applications: + +```bash +kubectl get applications -n argocd +``` + +View deployment history: + +```bash +kubectl get application asma-ai-bankapp -n argocd \ + -o jsonpath='{range .status.history[*]}ID={.id}{" Revision="}{.revision}{" Deployed="}{.deployedAt}{"\n"}{end}' +``` + +Check notifications controller: + +```bash +kubectl get pods -n argocd | grep notifications +``` + +Check Helm releases: + +```bash +helm list -A +``` + +Check AppProject: + +```bash +kubectl get appproject asma-devops-project -n argocd +``` + +--- + +# Interview Summary + +> In Day 85, I worked with advanced ArgoCD GitOps features on Amazon EKS. I tested manual and automated synchronization, configured sync waves to control Kubernetes deployment order, performed an ArgoCD rollback and restored the cluster from Git, implemented the App of Apps pattern, configured a notification trigger and template, and created an ArgoCD AppProject to understand security boundaries and RBAC. I also handled the interaction between ArgoCD and HPA by ignoring the Deployment replica field so autoscaling would not create continuous configuration drift. + +--- + +# Day 85 Completed + +**Topics completed:** + +```text +Automated Sync - Complete +Manual Sync - Complete +Sync Waves - Complete +Rollback - Complete +GitOps Recovery - Complete +App of Apps - Complete +Notifications - Complete +Projects / RBAC - Complete +HPA Drift Handling - Complete +``` + +**Final Status: Day 85 Complete** diff --git a/2026/day-86/Handwritten GitOps CI_CD Pipeline Notes.png b/2026/day-86/Handwritten GitOps CI_CD Pipeline Notes.png new file mode 100644 index 0000000000..4d03cb6c56 Binary files /dev/null and b/2026/day-86/Handwritten GitOps CI_CD Pipeline Notes.png differ diff --git a/2026/day-86/Screenshot 2026-10-07 151225.png b/2026/day-86/Screenshot 2026-10-07 151225.png new file mode 100644 index 0000000000..a669147795 Binary files /dev/null and b/2026/day-86/Screenshot 2026-10-07 151225.png differ diff --git a/2026/day-86/Screenshot 2026-10-07 151800.png b/2026/day-86/Screenshot 2026-10-07 151800.png new file mode 100644 index 0000000000..5a66eb9acd Binary files /dev/null and b/2026/day-86/Screenshot 2026-10-07 151800.png differ diff --git a/2026/day-86/Screenshot 2026-10-07 152002.png b/2026/day-86/Screenshot 2026-10-07 152002.png new file mode 100644 index 0000000000..259f333824 Binary files /dev/null and b/2026/day-86/Screenshot 2026-10-07 152002.png differ diff --git a/2026/day-86/Screenshot 2026-10-07 152424.png b/2026/day-86/Screenshot 2026-10-07 152424.png new file mode 100644 index 0000000000..207d635c0d Binary files /dev/null and b/2026/day-86/Screenshot 2026-10-07 152424.png differ diff --git a/2026/day-86/Screenshot 2026-10-07 153002.png b/2026/day-86/Screenshot 2026-10-07 153002.png new file mode 100644 index 0000000000..36be953dc1 Binary files /dev/null and b/2026/day-86/Screenshot 2026-10-07 153002.png differ diff --git a/2026/day-86/Screenshot 2026-10-07 153225.png b/2026/day-86/Screenshot 2026-10-07 153225.png new file mode 100644 index 0000000000..0ebb3658ff Binary files /dev/null and b/2026/day-86/Screenshot 2026-10-07 153225.png differ diff --git a/2026/day-86/Screenshot 2026-10-07 153254.png b/2026/day-86/Screenshot 2026-10-07 153254.png new file mode 100644 index 0000000000..5bf5e89828 Binary files /dev/null and b/2026/day-86/Screenshot 2026-10-07 153254.png differ diff --git a/2026/day-86/Screenshot 2026-10-07 153334.png b/2026/day-86/Screenshot 2026-10-07 153334.png new file mode 100644 index 0000000000..dccb9ff858 Binary files /dev/null and b/2026/day-86/Screenshot 2026-10-07 153334.png differ diff --git a/2026/day-86/Screenshot 2026-10-07 153412.png b/2026/day-86/Screenshot 2026-10-07 153412.png new file mode 100644 index 0000000000..9e941932b2 Binary files /dev/null and b/2026/day-86/Screenshot 2026-10-07 153412.png differ diff --git a/2026/day-86/day-86-end-to-end-gitops.md b/2026/day-86/day-86-end-to-end-gitops.md new file mode 100644 index 0000000000..342f69616c --- /dev/null +++ b/2026/day-86/day-86-end-to-end-gitops.md @@ -0,0 +1,365 @@ +# Day 86 -- End-to-End GitOps CI/CD Pipeline with ArgoCD + +## Goal + +Deploy the updated **Asma AI BankApp** through: + +**Code → GitHub → GitHub Actions → DockerHub → Git → ArgoCD → Amazon +EKS** + +Then verify, troubleshoot, and clean up AWS resources. + +## 1. Project Overview + +- Customized the frontend as **Asma AI BankApp**. +- GitHub Actions handled the CI pipeline. +- Maven built the Java application. +- Docker built and pushed the application image. +- The pipeline updated the Kubernetes image tag in Git. +- ArgoCD reconciled Git with Amazon EKS. +- Final application state reached **Synced + Healthy**. +- Troubleshot a real `ImagePullBackOff` rollout issue. + +## 2. Architecture + +``` text +Developer Code Change + ↓ + GitHub + ↓ + GitHub Actions + ↓ +Build Java Application + ↓ +Build Docker Image + ↓ +Push to DockerHub + ↓ +Update Image Tag in Git + ↓ + ArgoCD + ↓ + Amazon EKS + ↓ + Asma AI BankApp +``` + +**Main idea:** GitHub Actions builds and publishes the application. +ArgoCD deploys the desired state stored in Git. + +## 3. GitHub Actions CI Pipeline + +Workflow: + +``` text +.github/workflows/gitops-ci.yml +``` + +Main steps: 1. Checkout code. 2. Configure Java 21. 3. Build with Maven. +4. Run tests. 5. Generate a short Git SHA. 6. Authenticate to DockerHub +using GitHub Secrets. 7. Build and push the Docker image. 8. Update the +Kubernetes Deployment image tag. 9. Commit the manifest change back to +Git using `[skip ci]`. + +### Why use a Git SHA image tag? + +A SHA tag identifies the exact application version that produced the +image. It makes deployments traceable and avoids depending only on +`latest`. + +### Why `[skip ci]`? + +The pipeline commits the updated image tag back to Git. `[skip ci]` +prevents that bot commit from unnecessarily triggering the workflow +again. + +## 4. DockerHub Security + +GitHub Secrets used: + +``` text +DOCKERHUB_USERNAME +DOCKERHUB_TOKEN +``` + +Never commit the actual token or credentials to Git. + +The initial DockerHub authentication failed. After correcting the secret +values, the GitHub Actions pipeline completed successfully. + +## 5. GitOps with ArgoCD + +``` text +Git = Desired State + ↓ + ArgoCD + ↓ +Compare Git with live Kubernetes + ↓ + Amazon EKS +``` + +Important statuses: - **Synced** -- Git matches the live cluster. - +**OutOfSync** -- Git and the cluster differ. - **Healthy** -- +application resources are working. - **Progressing** -- Kubernetes is +still completing a rollout. - **Self-Heal** -- ArgoCD can correct +configuration drift. + +The project also demonstrated the **App of Apps** pattern, where a root +ArgoCD Application manages child Applications. + +## 6. HPA and ArgoCD + +The BankApp uses a Horizontal Pod Autoscaler. Because HPA changes +Deployment replicas dynamically, ArgoCD was configured to ignore the +BankApp replica field: + +``` yaml +ignoreDifferences: + - group: apps + kind: Deployment + name: bankapp + namespace: bankapp + jsonPointers: + - /spec/replicas +``` + +This prevents ArgoCD and HPA from fighting over replica count. + +## 7. Troubleshooting Scenario + +### Problem + +The BankApp rollout failed with: + +``` text +deployment "bankapp" exceeded its progress deadline +``` + +Pod status showed: + +``` text +ImagePullBackOff +``` + +Pod events revealed Kubernetes was attempting to pull an old image tag +that was not available in the new DockerHub repository. + +### Investigation + +Compared the desired image in Git with the live Deployment: + +``` bash +grep "image:" k8s/bankapp-deployment.yml + +kubectl get deployment bankapp -n bankapp \ + -o jsonpath='{.spec.template.spec.containers[?(@.name=="bankapp")].image}{"\n"}' +``` + +Result: + +``` text +Git desired image != Live Kubernetes image +``` + +ArgoCD showed: + +``` text +OutOfSync +Progressing +``` + +Its operation message showed it was waiting for the BankApp Deployment +to become healthy. + +### Fix + +Applied the exact committed Deployment manifest: + +``` bash +kubectl apply -f k8s/bankapp-deployment.yml +``` + +Then verified: + +``` bash +kubectl rollout status deployment/bankapp -n bankapp --timeout=120s +``` + +Final result: + +``` text +deployment "bankapp" successfully rolled out +``` + +ArgoCD returned to: + +``` text +Synced +Healthy +``` + +### Troubleshooting Flow + +``` text +Rollout problem + ↓ +Check Pods + ↓ +ImagePullBackOff + ↓ +Check Pod Events + ↓ +Old/missing image identified + ↓ +Compare Git vs Live Deployment + ↓ +Apply correct committed manifest + ↓ +Successful rollout + ↓ +ArgoCD Synced + Healthy +``` + +## 8. Useful Verification Commands + +Check rollout: + +``` bash +kubectl rollout status deployment/bankapp -n bankapp --timeout=120s +``` + +Check live image: + +``` bash +kubectl get deployment bankapp -n bankapp \ + -o jsonpath='{.spec.template.spec.containers[?(@.name=="bankapp")].image}{"\n"}' +``` + +Check ArgoCD: + +``` bash +kubectl get application asma-ai-bankapp -n argocd \ + -o custom-columns='NAME:.metadata.name,SYNC:.status.sync.status,HEALTH:.status.health.status' +``` + +Expected final status: + +``` text +SYNC HEALTH +Synced Healthy +``` + +## 9. AWS Verification + +Before cleanup: - Amazon EKS cluster was Active. - Three EC2 worker +nodes were running. - Worker node health checks passed. - BankApp +successfully ran on EKS. + +**Security:** Blur/crop AWS account numbers, ARNs, endpoints, instance +IDs, node names, IPs, and infrastructure identifiers before publishing +screenshots. + +## 10. Cleanup + +Cleanup is important because EKS, EC2, NAT Gateway, load balancers, and +related AWS resources can continue generating charges. + +Completed: + +``` bash +kubectl delete application asma-app-of-apps -n argocd +kubectl delete application asma-ai-bankapp -n argocd +helm uninstall eg -n envoy-gateway-system +helm uninstall cert-manager -n cert-manager +``` + +Cert-manager kept some CRDs because of Helm resource policy. Since the +whole EKS cluster will be destroyed, they do not need separate cleanup +for this lab. + +Terraform cleanup: + +``` bash +cd terraform +terraform init -upgrade +terraform destroy +``` + +Review the destroy plan carefully before entering `yes`. + +After destroy, verify the lab-created EKS cluster, EC2 workers, load +balancer, NAT Gateway, and related storage/resources are gone. + +## 11. Tool Responsibilities + +``` text +Terraform + ↓ +Creates AWS infrastructure + +GitHub Actions + ↓ +Builds/tests application +Builds and pushes Docker image +Updates image tag in Git + +ArgoCD + ↓ +Watches Git +Reconciles Kubernetes + +Amazon EKS + ↓ +Runs the application +``` + +## 12. Interview Answers + +### Explain your Day 86 project + +I built an end-to-end GitOps CI/CD pipeline for a Java BankApp. GitHub +Actions builds the application, creates and pushes a Docker image, and +updates the Kubernetes image tag in Git. ArgoCD watches Git and deploys +the desired state to Amazon EKS. + +### What problem did you troubleshoot? + +The rollout became stuck with `ImagePullBackOff` because the live +Deployment referenced an unavailable old image tag. I checked pod +events, compared Git with the live Deployment, corrected the deployment +state, verified the rollout, and confirmed ArgoCD returned to Synced and +Healthy. + +### Why doesn't CI deploy directly using kubectl? + +In this GitOps design, CI updates Git and ArgoCD performs the +deployment. Git remains the source of truth. + +### Terraform vs ArgoCD? + +Terraform creates AWS infrastructure. ArgoCD manages Kubernetes +application state from Git. + +## 13. Final Day 86 Status + +``` text +Frontend customization DONE +GitHub Actions CI DONE +Java/Maven build DONE +Docker image build DONE +DockerHub push DONE +SHA image tagging DONE +Git manifest update DONE +ArgoCD GitOps deployment DONE +EKS rollout DONE +Troubleshooting DONE +ArgoCD Synced + Healthy DONE +App of Apps demonstration DONE +AWS cleanup IN PROGRESS +``` + +## Key Lesson + +> **CI builds and publishes the application. Git stores the desired +> deployment state. ArgoCD reconciles Amazon EKS with Git.** diff --git a/2026/day-87/Day 87_ Agentic AI for DevOps.png b/2026/day-87/Day 87_ Agentic AI for DevOps.png new file mode 100644 index 0000000000..cc34dd6ed0 Binary files /dev/null and b/2026/day-87/Day 87_ Agentic AI for DevOps.png differ diff --git a/2026/day-87/Screenshot 2026-10-08 120205.png b/2026/day-87/Screenshot 2026-10-08 120205.png new file mode 100644 index 0000000000..70b4c9eb66 Binary files /dev/null and b/2026/day-87/Screenshot 2026-10-08 120205.png differ diff --git a/2026/day-87/Screenshot 2026-10-08 120323.png b/2026/day-87/Screenshot 2026-10-08 120323.png new file mode 100644 index 0000000000..b1216bb542 Binary files /dev/null and b/2026/day-87/Screenshot 2026-10-08 120323.png differ diff --git a/2026/day-87/agentic-ai-for-devops b/2026/day-87/agentic-ai-for-devops new file mode 160000 index 0000000000..5f328ff303 --- /dev/null +++ b/2026/day-87/agentic-ai-for-devops @@ -0,0 +1 @@ +Subproject commit 5f328ff30333709ee3e2cb7aaae847a34108faa8 diff --git a/2026/day-87/day-87-agentic-ai-intro.md b/2026/day-87/day-87-agentic-ai-intro.md new file mode 100644 index 0000000000..64521a12a6 --- /dev/null +++ b/2026/day-87/day-87-agentic-ai-intro.md @@ -0,0 +1,70 @@ +# Day 87 – Introduction to Agentic AI for DevOps + +## Objective +Learn Agentic AI basics for DevOps and test a local AI model with DevOps troubleshooting. + +## Environment +- WSL Ubuntu +- Python virtual environment +- Docker, kubectl, Kind +- Ollama +- LangChain +- Local LLM + +## Agentic AI +A normal LLM mainly answers questions. An AI agent combines an LLM with tools so it can inspect real systems. + +```text +User Question -> LLM -> Agent -> DevOps Tools -> Result -> Explanation +``` + +The LLM is the **brain** and tools are the **hands**. + +## Hands-on Work + +### Environment Verification +The setup verification checked Python, Docker, kubectl, Kind, Ollama, and the local model. The preflight verification completed successfully. + +### Module 1 – DevOps Explainer +The local model was tested with a Docker troubleshooting example. It successfully explained a duplicate container-name error in simple language. + +```text +DevOps Error -> Local LLM -> Explanation +``` + +### Module 2 – Tool-Using Agent +The next module introduced tools that could inspect Docker resources. + +```text +Question + | + v +LLM chooses a tool + | + v +Tool reads system information + | + v +LLM explains the result +``` + +During local testing, the tool-agent interaction had model compatibility/performance limitations. This was documented as a local model/tool limitation rather than recorded as a successful tool call. + +## Key Lessons +- Ollama runs the LLM locally. +- LangChain connects the LLM with agent tools. +- An agent can reason about a question and select a tool. +- A model that answers text questions may not necessarily support the tool-calling behavior required by an agent framework. +- Verify the model, framework, and tools separately when troubleshooting. + +## Interview Explanation +> Agentic AI combines an LLM with tools. The LLM provides reasoning and the tools allow the agent to inspect systems such as Docker or Kubernetes. In this lab I used Ollama for a local model and LangChain for the agent workflow. I also learned that model compatibility is important for tool calling. + +## Day 87 Result +- Local Agentic AI environment prepared +- Setup verification completed +- Local LLM tested +- Docker error explanation completed successfully +- Tool-calling agent architecture studied +- Model/tool compatibility troubleshooting performed +- Ready for multi-tool agents and MCP diff --git a/2026/day-87/setUp-env-for87.png b/2026/day-87/setUp-env-for87.png new file mode 100644 index 0000000000..98dda369ef Binary files /dev/null and b/2026/day-87/setUp-env-for87.png differ diff --git a/2026/day-88/Day 88_ Multi-Tool Agents with MCP.png b/2026/day-88/Day 88_ Multi-Tool Agents with MCP.png new file mode 100644 index 0000000000..f7bca7e7e6 Binary files /dev/null and b/2026/day-88/Day 88_ Multi-Tool Agents with MCP.png differ diff --git a/2026/day-88/Screenshot 2026-10-08 123835.png b/2026/day-88/Screenshot 2026-10-08 123835.png new file mode 100644 index 0000000000..96bb491598 Binary files /dev/null and b/2026/day-88/Screenshot 2026-10-08 123835.png differ diff --git a/2026/day-88/Screenshot 2026-10-08 123842.png b/2026/day-88/Screenshot 2026-10-08 123842.png new file mode 100644 index 0000000000..54d46dda4d Binary files /dev/null and b/2026/day-88/Screenshot 2026-10-08 123842.png differ diff --git a/2026/day-88/Screenshot 2026-10-08 124255.png b/2026/day-88/Screenshot 2026-10-08 124255.png new file mode 100644 index 0000000000..5a9655eff1 Binary files /dev/null and b/2026/day-88/Screenshot 2026-10-08 124255.png differ diff --git a/2026/day-88/Screenshot 2026-10-08 124542.png b/2026/day-88/Screenshot 2026-10-08 124542.png new file mode 100644 index 0000000000..f577802671 Binary files /dev/null and b/2026/day-88/Screenshot 2026-10-08 124542.png differ diff --git a/2026/day-88/Screenshot 2026-10-08 125447.png b/2026/day-88/Screenshot 2026-10-08 125447.png new file mode 100644 index 0000000000..4bf90db670 Binary files /dev/null and b/2026/day-88/Screenshot 2026-10-08 125447.png differ diff --git a/2026/day-88/Screenshot 2026-10-08 125737.png b/2026/day-88/Screenshot 2026-10-08 125737.png new file mode 100644 index 0000000000..937cc6fb7d Binary files /dev/null and b/2026/day-88/Screenshot 2026-10-08 125737.png differ diff --git a/2026/day-88/day-88-multi-tool-mcp.md b/2026/day-88/day-88-multi-tool-mcp.md new file mode 100644 index 0000000000..726cfc7dd1 --- /dev/null +++ b/2026/day-88/day-88-multi-tool-mcp.md @@ -0,0 +1,147 @@ +# Day 88 – Multi-Tool Agents, MCP, and CI/CD Analysis + +## Objective +Work with multiple DevOps tools, learn Model Context Protocol (MCP), troubleshoot Kubernetes through MCP, inspect GitHub Actions failures, and build a custom tool. + +## Environment +- WSL Ubuntu +- Python virtual environment +- Docker, kubectl, Kind +- Ollama with `gemma4` +- LangChain +- FastMCP +- GitHub CLI (`gh`) + +## Task 1 – Multi-Tool Agent +The multi-tool lab introduced Docker and Kubernetes tools that an AI agent can choose from. A deliberately broken Docker container and Kubernetes pod were created for troubleshooting practice. + +The Kubernetes test pod used an invalid image and entered `ImagePullBackOff`. + +```text +User -> LLM -> ReAct Agent -> Docker/Kubernetes Tools -> Result -> Answer +``` + +The earlier local multi-tool agent test experienced a model/tool execution delay. The resources were verified independently, and the later MCP implementation successfully diagnosed the Kubernetes problem. + +## Task 2 – MCP +MCP stands for **Model Context Protocol**. It provides a standard way for AI clients to connect to external tools and data. + +```text +AI Client / Agent + | + MCP + | + v + MCP Server + | + +-- Kubernetes tools + +-- Pod details + +-- Events +``` + +The lab used an MCP server, MCP client, reusable tools, and `stdio` transport. + +## Task 3 – Kubernetes MCP Server and Client +The lab used: + +```text +module-3/mcp_server.py +module-3/agent_with_mcp.py +``` + +The exact lab model, `gemma4`, was installed in Ollama. FastMCP started successfully with the **Kubernetes Tools** server using `stdio`. + +The agent listed pods in the default namespace and detected the deliberately broken pod in `ImagePullBackOff`. + +When asked why the pod was not running, the MCP agent inspected Kubernetes information and correctly determined that the configured container image did not exist. It explained that the image name needed to point to a valid published image. + +```text +Question -> gemma4 -> MCP Client -> Kubernetes MCP Server + -> Kubernetes information -> LLM explanation +``` + +This successfully demonstrated MCP tool discovery and Kubernetes troubleshooting. + +## Task 4 – CI/CD Failure Analyzer +The CI/CD analyzer was run inside an existing Git repository with GitHub Actions history. + +It used GitHub CLI to inspect workflow runs and successfully located a previous failed workflow. When it attempted to retrieve the detailed logs, GitHub returned: + +```text +HTTP 410: Server Error +``` + +The detailed historical logs were therefore unavailable, so the agent correctly stated that it could not provide a detailed root-cause analysis. + +This is documented as an external log-availability limitation, not as a successful failure-log diagnosis. + +```text +User -> AI Agent -> GitHub CLI -> Workflow Runs / Logs -> Analysis +``` + +## Task 5 – Custom Log Searcher +A custom LangChain tool named `search_logs` was created in: + +```text +log_search_tool.py +``` + +It accepts a log file and search pattern, scans the file, and returns matching lines. + +Testing with the pattern `ERROR` successfully returned: + +```text +Line 2: ERROR Database connection failed +Line 4: ERROR Connection timeout +``` + +This demonstrated how a custom DevOps operation can be wrapped as an AI-agent tool. + +## Task 6 – Cleanup +The temporary broken resources were cleaned up: +- Broken Docker container removed +- Broken Kubernetes pod deleted +- Docker verified clean for that test container +- No resources remained in the default Kubernetes namespace + +The existing local Kind cluster was retained for later local labs. No AWS infrastructure was created for Day 88. + +## Key Lessons + +### ReAct +```text +Think -> Choose Tool -> Run Tool -> Read Result -> Answer +``` + +### MCP +MCP separates the AI client from tool implementations and provides a standard interface for discovering and using tools. + +### CI/CD Agent +An AI agent can use GitHub CLI to inspect workflow history and logs, but it is limited by the data GitHub still makes available. + +### Custom Tools +Existing scripts and command-line operations can be wrapped as tools and made available to an AI agent. + +## Interview Explanation +> On Day 88 I worked with multi-tool AI agents and MCP. I ran a FastMCP Kubernetes server over stdio and connected an AI client using the local gemma4 model. The agent discovered Kubernetes tools through MCP and diagnosed an ImagePullBackOff issue caused by an invalid image. I also tested a GitHub Actions failure analyzer and built a custom log-search tool. + +## Screenshot Evidence +Keep screenshots showing: +- FastMCP Kubernetes server using `stdio` +- MCP agent detecting and diagnosing the broken pod +- CI/CD analyzer finding a failed workflow +- Custom Log Searcher returning error lines + +Before publishing screenshots, crop or blur credentials, tokens, account information, and infrastructure identifiers. + +## Day 88 Result +- Multi-tool agent architecture studied +- MCP concepts understood +- FastMCP Kubernetes server started successfully +- MCP client successfully used Kubernetes tools +- Broken Kubernetes pod diagnosed through MCP +- CI/CD analyzer inspected workflow history +- Historical logs were unavailable due to HTTP 410 +- Custom Log Searcher built and tested +- Temporary resources cleaned up +- Day 88 hands-on lab completed diff --git a/2026/day-88/log_search_tool.py b/2026/day-88/log_search_tool.py new file mode 100644 index 0000000000..f860fb2063 --- /dev/null +++ b/2026/day-88/log_search_tool.py @@ -0,0 +1,24 @@ +from langchain_core.tools import tool +from pathlib import Path + +@tool +def search_logs(file_path: str, pattern: str) -> str: + """Search a log file for lines containing a pattern.""" + path = Path(file_path) + + if not path.exists(): + return f"File not found: {file_path}" + + matches = [] + with open(path, "r") as f: + for line_number, line in enumerate(f, 1): + if pattern.lower() in line.lower(): + matches.append(f"Line {line_number}: {line.strip()}") + + return "\n".join(matches) if matches else "No matches found." + +if __name__ == "__main__": + print(search_logs.invoke({ + "file_path": "sample.log", + "pattern": "ERROR" + })) diff --git a/2026/day-89/KubeHealer AIOps Study Notes.png b/2026/day-89/KubeHealer AIOps Study Notes.png new file mode 100644 index 0000000000..6df5874f3b Binary files /dev/null and b/2026/day-89/KubeHealer AIOps Study Notes.png differ diff --git a/2026/day-89/Screenshot 2026-10-08 141347.png b/2026/day-89/Screenshot 2026-10-08 141347.png new file mode 100644 index 0000000000..08b592b7a6 Binary files /dev/null and b/2026/day-89/Screenshot 2026-10-08 141347.png differ diff --git a/2026/day-89/Screenshot 2026-10-08 141420.png b/2026/day-89/Screenshot 2026-10-08 141420.png new file mode 100644 index 0000000000..65d03b6dc2 Binary files /dev/null and b/2026/day-89/Screenshot 2026-10-08 141420.png differ diff --git a/2026/day-89/Screenshot 2026-10-08 141445.png b/2026/day-89/Screenshot 2026-10-08 141445.png new file mode 100644 index 0000000000..bf673b2f2a Binary files /dev/null and b/2026/day-89/Screenshot 2026-10-08 141445.png differ diff --git a/2026/day-89/Screenshot 2026-10-08 141458.png b/2026/day-89/Screenshot 2026-10-08 141458.png new file mode 100644 index 0000000000..c5e75604f5 Binary files /dev/null and b/2026/day-89/Screenshot 2026-10-08 141458.png differ diff --git a/2026/day-89/Screenshot 2026-10-08 141654.png b/2026/day-89/Screenshot 2026-10-08 141654.png new file mode 100644 index 0000000000..abbf45012f Binary files /dev/null and b/2026/day-89/Screenshot 2026-10-08 141654.png differ diff --git a/2026/day-89/Screenshot 2026-10-08 141752.png b/2026/day-89/Screenshot 2026-10-08 141752.png new file mode 100644 index 0000000000..9d87549ad7 Binary files /dev/null and b/2026/day-89/Screenshot 2026-10-08 141752.png differ diff --git a/2026/day-89/day-89-kubehealer-aiops.md b/2026/day-89/day-89-kubehealer-aiops.md new file mode 100644 index 0000000000..745334ad96 --- /dev/null +++ b/2026/day-89/day-89-kubehealer-aiops.md @@ -0,0 +1,318 @@ +# Day 89 -- KubeHealer: AI-Powered Kubernetes Self-Healing + +## Objective + +Day 89 focused on AIOps using KubeHealer with Kubernetes, Temporal, and +Claude. The goal was to diagnose unhealthy Kubernetes workloads, propose +safe remediation, keep human control and guardrails, execute supported +fixes, escalate unsafe cases, and track workflow history with Temporal. + +## 1. What is AIOps? + +AIOps means using AI to help operate and troubleshoot IT systems. +Traditional monitoring can report that something is broken. AIOps can +also help explain why it is broken and propose a possible fix. + +AI should not have unlimited control over production systems. Safe +automation needs guardrails. + +## 2. Six Guardrails + +1. **Human Approval** -- important changes should require human + approval. +2. **Scope Limits** -- AI should only operate on approved resources and + actions. +3. **Audit Trail** -- diagnoses, proposed fixes, approvals, and results + should be recorded. +4. **Rollback** -- automation should have a recovery path if a change + causes problems. +5. **Timeout and Retry Limits** -- workflows should not retry forever. +6. **Escalation** -- if the system cannot safely fix a problem, it + should stop and ask a human. + +## 3. Architecture + +``` text +Broken Kubernetes Workload + | + v + KubeHealer + / \ + v v +Kubernetes Claude AI + API Diagnosis + \ / + v v + Guardrails / Human Control + | + v + Temporal + | + v + Execute / Track Fix + | + v + Kubernetes Workload +``` + +**Kubernetes** provided the workloads being diagnosed and repaired. + +**Claude** analyzed Kubernetes information and helped generate diagnoses +and proposed remediation. + +**Temporal** managed durable workflow execution and stored workflow +history. + +**KubeHealer** connected Kubernetes, AI, safety controls, and workflow +execution. + +## 4. Lab Environment + +The lab used a local Kind Kubernetes cluster, a Python virtual +environment, KubeHealer, Temporal development server and Web UI, +Anthropic API integration, and the Kubernetes Python client. + +No AWS infrastructure was required for Day 89. + +## 5. Three Broken Applications + +### Broken Image + +The web workload used an incorrect container image. + +**Symptom:** `ImagePullBackOff / ErrImagePull` + +**Diagnosis:** Kubernetes could not pull the invalid image. + +**Remediation:** Use a valid NGINX image. + +### Memory Problem + +The memory workload had an extremely small memory limit. + +**Symptom:** `CrashLoopBackOff / Error` + +**Diagnosis:** The container did not have enough memory to operate +correctly. + +**Remediation:** Increase the memory resource configuration. + +### Missing ConfigMap + +The configuration workload referenced a ConfigMap that did not exist. + +**Symptom:** `CreateContainerConfigError` + +**Diagnosis:** Required application configuration was missing. + +**Decision:** Escalate instead of blindly creating configuration that +could not safely be inferred. + +This demonstrated an important AIOps rule: sometimes the safest +automated action is to make no change and ask a human. + +## 6. KubeHealer Workflow + +``` text +Detect + ↓ +Collect Kubernetes Information + ↓ +AI Diagnosis + ↓ +Proposed Remediation + ↓ +Safety / Human Control + ↓ +Execute Supported Action + ↓ +Verify Result + ↓ +Record Workflow History +``` + +The worker connected successfully to the required services and processed +workflow tasks. + +## 7. Compatibility Issues Found + +### Claude Model Compatibility + +The repository referenced a Claude model identifier that was unavailable +through the configured API. The available model list was checked and the +configuration was updated to an available Claude Sonnet model so the lab +could continue. + +### Kubernetes Resource Compatibility + +A second issue involved assumptions in the remediation implementation. +Part of the remediation code expected Deployment-managed workloads, +while testing also exposed standalone-Pod behavior. + +Kubernetes correctly rejected changing immutable resource fields on an +already-created standalone Pod. + +### Lesson + +A production healing system should first identify whether a Pod belongs +to a Deployment, ReplicaSet, StatefulSet, DaemonSet, Job, or is a +standalone Pod before applying remediation. + +## 8. Final Kubernetes Result + +The final evidence showed the two repairable workloads healthy while the +missing-configuration case remained unresolved for escalation. + +``` text +Configuration workload -> CreateContainerConfigError +Memory workload -> Running +Web workload -> Running +``` + +The final principle was: + +``` text +Safe to repair automatically? -> Repair +Not enough information? -> Escalate +``` + +## 9. Temporal Workflow Evidence + +The Temporal Web UI displayed KubeHealer workflow history. During +troubleshooting, both running and failed workflow executions were +visible. + +The failed executions are part of the real lab evidence because they +recorded compatibility problems encountered during testing. + +Temporal also demonstrated the idea of **durable execution**: workflow +history remains available even when workers or individual execution +attempts encounter problems. + +## 10. AI Healing vs Traditional Automation + + ----------------------------------------------------------------------- + Traditional Automation AI-Assisted AIOps + ----------------------------------- ----------------------------------- + Uses predefined rules Can analyze operational context + + Requires known conditions Can assist with unfamiliar symptoms + + Executes predetermined actions Can propose remediation from + evidence + + Limited reasoning Adds an AI reasoning layer + + Depends heavily on fixed Can assist diagnosis, but still + assumptions needs guardrails + ----------------------------------------------------------------------- + +AI does not replace Kubernetes knowledge. Engineers still need to +understand controllers, permissions, resources, observability, rollback, +failure modes, and security. + +## 11. Connection to Previous DevOps Topics + +``` text +Docker + ↓ +Kubernetes + ↓ +Terraform + ↓ +GitHub Actions + ↓ +Ansible + ↓ +Helm + ↓ +EKS + ↓ +GitOps / Argo CD + ↓ +Agentic AI + ↓ +MCP / AI Tools + ↓ +AIOps / KubeHealer +``` + +Day 89 added a new question to the DevOps journey: can AI help +understand an operational failure and safely participate in remediation? + +## 12. Cleanup + +After collecting evidence: - The Day 89 Kind cluster was deleted. - The +Temporal development server was stopped. - The Python virtual +environment was deactivated. - No AWS resources were created for this +lab. - An unrelated local Kind cluster from earlier work was +intentionally left untouched. + +## 13. Screenshot Evidence + +Add these screenshots to the repository/documentation: - KubeHealer +worker, diagnosis, proposed remediation, or workflow execution. - Final +Kubernetes state showing the repaired workloads and escalation case. - +Temporal Web UI showing KubeHealer workflow history. + +Before publishing, crop or blur credentials, API keys, tokens, internal +identifiers, and other sensitive information. + +## 14. Problems and Lessons + +The lab did not work perfectly on the first attempt. Issues included an +unavailable Claude model identifier, workload/remediation compatibility +assumptions, Kubernetes immutable Pod fields, and workflow failures +during testing. + +These failures demonstrated a real DevOps responsibility: automation +must handle actual platform behavior rather than only the expected happy +path. + +## 15. Interview Explanation + +**Question: What did you do in your KubeHealer AIOps lab?** + +I built a local Kubernetes AIOps lab using KubeHealer, Temporal, and +Claude. I tested image, memory, and configuration failures. The +AI-assisted workflow helped diagnose problems and propose remediation +while using safety guardrails. I also investigated compatibility issues +between Kubernetes workload types and remediation code. The repairable +workloads were recovered, while the unsafe configuration case was +escalated. Temporal provided durable workflow history and showed how AI +can safely support DevOps operations. + +## 16. Main Takeaway + +> **AI should assist operations, not receive uncontrolled access to +> operations.** + +``` text +AI Reasoning + + +Kubernetes Knowledge + + +Human Control + + +Guardrails + + +Audit History + + +Safe Remediation + = +Responsible AIOps +``` + +## Day 89 Status + +**Completed** + +- AIOps concepts and six guardrails covered +- Local Kubernetes environment tested +- KubeHealer and Claude integration tested +- Three failure scenarios investigated +- Supported remediation demonstrated +- Escalation behavior demonstrated +- Temporal workflow history captured +- Final Kubernetes evidence captured +- Environment cleaned up diff --git a/2026/day-89/kubehealer b/2026/day-89/kubehealer new file mode 160000 index 0000000000..2a39844b61 --- /dev/null +++ b/2026/day-89/kubehealer @@ -0,0 +1 @@ +Subproject commit 2a39844b61bbf527ad63751e35fef17ea3fb31f6 diff --git a/2026/day-90/day-90-graduation.md b/2026/day-90/day-90-graduation.md new file mode 100644 index 0000000000..7e2e4961cd --- /dev/null +++ b/2026/day-90/day-90-graduation.md @@ -0,0 +1,667 @@ +# Day 90 -- Grand Finale: My 90 Days of DevOps Journey + +## I Made It to Day 90 🎓 + +When I started this challenge, my goal was simple: I wanted to +understand DevOps by actually doing it. + +Over these 90 days, I did much more than read about tools. I created +environments, broke things, fixed errors, deployed applications, worked +with cloud infrastructure, built CI/CD pipelines, monitored systems, +used GitOps, and finally explored AI-powered DevOps. + +Some days were easy. Some days took much longer than expected. There +were many errors along the way, but troubleshooting those errors became +one of the most valuable parts of this journey. + +Day 90 is not about learning another tool. It is about connecting +everything I learned and seeing the complete picture. + +------------------------------------------------------------------------ + +## My 90-Day DevOps Journey + +### Days 1--13: Linux Fundamentals + +I started with the foundation of DevOps: Linux. + +I practiced: + +- Linux commands +- Files and directories +- Users and permissions +- Processes +- Storage and LVM +- Basic system administration + +Linux became the environment underneath almost every tool I used later. + +### Days 14--15: Networking + +I learned the basics of how systems communicate: + +- IP addresses +- DNS +- Ports +- Subnets +- Network communication + +This became especially important when I later worked with Docker, +Kubernetes, AWS, and EKS. + +### Days 16--21: Shell Scripting + +I learned how Bash can automate repetitive work. + +Instead of typing the same commands again and again, I learned to put +steps into scripts and make tasks repeatable. + +### Days 22--28: Git and GitHub + +Git became a major part of the rest of the challenge. + +I practiced: + +- Repositories +- Commits +- Branches +- Push and pull +- Merge concepts +- GitHub CLI +- Troubleshooting Git problems + +Later, Git became much more than source control because GitOps used Git +as the source of truth for deployment. + +### Days 29--37: Docker + +Docker was one of the areas where everything started becoming more +practical. + +I worked with: + +- Docker images +- Containers +- Dockerfiles +- Volumes +- Networks +- Docker Compose +- Multi-stage builds +- Container troubleshooting + +I learned why containers make applications easier to package and run +consistently. + +### Days 38--49: CI/CD and GitHub Actions + +Next, I connected source code with automation. + +I learned: + +- GitHub Actions workflows +- YAML +- Triggers +- Jobs and runners +- Secrets +- Build and deployment automation +- DevSecOps concepts + +This showed me how a code push can automatically start a complete +pipeline. + +### Days 50--58: Kubernetes + +Kubernetes was one of the biggest parts of my journey. + +I practiced: + +- Pods +- Deployments +- Services +- Namespaces +- ConfigMaps +- Secrets +- RBAC +- Scaling +- Troubleshooting + +I also spent a lot of time understanding why workloads fail instead of +only memorizing commands. + +### Days 59--67: Terraform + +Terraform helped me understand Infrastructure as Code. + +I worked with: + +- Providers +- Resources +- Variables +- Outputs +- Locals +- State +- Remote state +- Lifecycle +- Import +- Networking +- Modules +- AWS infrastructure + +One of the biggest lessons was that infrastructure should be +reproducible instead of manually created every time. + +### Days 68--72: Ansible + +With Ansible, I learned configuration management. + +I practiced: + +- Inventory +- Managed nodes +- Ad-hoc commands +- Playbooks +- Variables +- Templates +- Roles +- Vault concepts + +Terraform helped create infrastructure, while Ansible helped configure +systems after they existed. + +### Days 73--77: Observability + +I learned that deploying an application is not enough. We also need to +understand what is happening after deployment. + +I worked with: + +- Prometheus +- Grafana +- Loki +- Promtail +- OpenTelemetry concepts +- Metrics +- Logs +- Traces +- Alerting + +Observability taught me to ask not only, "Is the application running?" +but also, "Why is it behaving this way?" + +### Days 78--80: Helm + +Helm made Kubernetes deployments easier to manage. + +I practiced: + +- Helm charts +- Templates +- Values files +- Subcharts +- Environment-specific configuration +- Dev, staging, and production values + +I learned how one reusable chart can support multiple environments. + +### Days 81--83: Amazon EKS + +This block connected Kubernetes with AWS. + +I worked with a real project and practiced: + +- EKS +- Terraform-based infrastructure +- Kubernetes deployment on AWS +- Gateway API +- Storage +- Scaling +- Monitoring + +This was where many earlier skills started working together as one +system. + +### Days 84--86: ArgoCD and GitOps + +GitOps changed the way I looked at deployment. + +Instead of manually changing the cluster, Git became the desired state. + +I practiced: + +- ArgoCD +- GitOps principles +- Sync +- Application definitions +- Sync strategies +- App of Apps +- RBAC +- CI/CD and GitOps integration + +I also connected the CI pipeline so application changes could flow +through Git and into the Kubernetes environment. + +### Days 87--89: Agentic AI for DevOps + +The final technical block introduced AI into DevOps. + +I explored: + +- LLM agents +- ReAct concepts +- MCP +- AI tools +- KubeHealer +- Temporal +- AI-assisted Kubernetes diagnosis +- Human approval and safety guardrails + +Day 89 was especially useful because the lab did not work perfectly. + +I encountered compatibility problems and Kubernetes restrictions. +Instead of hiding those failures, I learned from them. + +That felt like real DevOps work. + +------------------------------------------------------------------------ + +# How Everything Connects + +One of my biggest lessons is that DevOps tools should not be learned as +completely separate technologies. + +A real pipeline can look like this: + +``` text +Developer writes code + ↓ +Git / GitHub + ↓ +GitHub Actions + ↓ +Build Docker image + ↓ +Push image to registry + ↓ +Update Kubernetes manifest in Git + ↓ +ArgoCD detects Git change + ↓ +ArgoCD syncs Kubernetes + ↓ +Application runs on EKS + ↓ +Helm manages Kubernetes configuration + ↓ +Prometheus / Grafana / Loki observe the system + ↓ +AI-assisted tools help diagnose problems + ↓ +Safe fix goes through the controlled workflow + ↓ +Git remains the source of truth +``` + +This is the part of the challenge that made everything click for me. + +------------------------------------------------------------------------ + +# My AI-BankApp Project + +The AI-BankApp became an important project during the later part of the +challenge because I used it across several technologies. + +I worked with the application through: + +- Helm +- Kubernetes +- Amazon EKS +- Terraform +- Gateway and storage concepts +- Monitoring +- ArgoCD +- GitOps +- GitHub Actions + +Instead of learning every tool with a completely unrelated example, I +could see how the same application moved through different stages of a +DevOps lifecycle. + +That made the project feel much closer to real-world work. + +------------------------------------------------------------------------ + +# My Top 5 "Aha!" Moments + +## 1. DevOps is a pipeline, not a collection of tools + +At first, Docker, Kubernetes, Terraform, Ansible, Helm, and ArgoCD can +look like separate technologies. + +Later I understood that each one solves a different part of the same +delivery process. + +## 2. Git can control more than source code + +With GitOps, Git became the desired state of the environment. + +That helped me understand why teams want infrastructure and deployment +changes to be reviewable and traceable. + +## 3. Terraform state is extremely important + +Terraform is not only about writing `.tf` files. + +State tells Terraform what it manages. Learning remote state, lifecycle +behavior, imports, and cleanup helped me understand Infrastructure as +Code much better. + +## 4. Troubleshooting teaches more than a perfect lab + +Some of my strongest learning happened when something failed. + +I had to read errors, inspect resources, understand what the tool +expected, make a change, and test again. + +That process improved my confidence much more than simply copying a +working command. + +## 5. AI needs guardrails in DevOps + +AI can help diagnose infrastructure problems, but it should not blindly +change production systems. + +Human approval, scope limits, audit history, rollback, retry limits, and +escalation are important. + +The safest answer from an AI system is sometimes: "I do not have enough +information. A human should review this." + +------------------------------------------------------------------------ + +# The Hardest Part of My Journey + +There was not just one difficult command or one difficult tool. + +The hardest part was continuing when a lab did not behave the way I +expected. + +Kubernetes, EKS, GitOps, and the AI/KubeHealer labs all gave me +situations where I had to troubleshoot instead of simply following +instructions. + +On Day 89, for example, I encountered an unavailable AI model +configuration, differences between Kubernetes workload types, and +restrictions around updating Pod resources. + +I kept investigating until I understood what was happening and reached a +safe final result. + +That experience reminded me of something important: + +> DevOps is not about never seeing errors. It is about learning how to +> investigate them without giving up. + +------------------------------------------------------------------------ + +# My Skills Inventory + +These ratings represent my confidence after completing this challenge. A +5/5 does not mean I know everything about a technology. It means I feel +very confident using it based on the hands-on work I completed. + + Skill Days Confidence + -------------------------------------------- -------- ------------ + Linux command line 1--13 4/5 + Shell scripting 16--21 3/5 + Git & GitHub 22--28 4/5 + Docker 29--37 **5/5** + CI/CD -- GitHub Actions 38--49 4/5 + Kubernetes 50--58 **5/5** + Terraform 59--67 **5/5** + Ansible 68--72 4/5 + Observability -- Prometheus, Grafana, Loki 73--77 4/5 + Helm 78--80 **5/5** + Amazon EKS 81--83 4/5 + ArgoCD / GitOps 84--86 **5/5** + Agentic AI for DevOps 87--89 4/5 + +I also know which areas I want to practice more. This challenge gave me +a foundation, not an endpoint. + +------------------------------------------------------------------------ + +# What I Want to Learn Next + +My next goal is not to collect tools just for the sake of adding names +to my resume. + +I want to deepen the skills I already learned and build more complete +projects. + +Areas I want to continue exploring include: + +- Advanced Kubernetes +- Multi-cluster Kubernetes +- Advanced Terraform and drift detection +- Kubernetes security +- Secrets management +- AWS Secrets Manager and HashiCorp Vault +- Service mesh +- Cloud cost optimization / FinOps +- Chaos engineering +- Database operations +- Production observability +- More advanced GitOps +- AI-assisted DevOps and AIOps + +I also want to continue building portfolio projects that connect these +skills from beginning to end. + +------------------------------------------------------------------------ + +# Certifications + +Certifications I can consider as I continue learning include: + +- AWS certifications +- Certified Kubernetes Administrator (CKA) +- Certified Kubernetes Application Developer (CKAD) +- HashiCorp Terraform Associate +- GitHub Actions certification + +The certification is useful, but my main goal is to keep building +hands-on experience. + +------------------------------------------------------------------------ + +# Screenshot Collage + +For my final Day 90 documentation, I can include screenshots from +different stages of the challenge: + +1. Linux / terminal work +2. Docker containers +3. Kubernetes workloads +4. Terraform infrastructure +5. Ansible automation +6. Grafana dashboards +7. Helm deployments +8. EKS project +9. ArgoCD UI +10. AI-BankApp +11. GitHub Actions pipeline +12. KubeHealer / Temporal AIOps lab + +Before publishing screenshots, I will remove or blur credentials, +tokens, account information, internal identifiers, and other sensitive +information. + +------------------------------------------------------------------------ + +# Advice to Someone Starting Day 1 + +If someone starts this challenge tomorrow, this is what I would tell +them: + +**Do not rush only to finish the day number. Try to understand why you +are running each command.** + +You will get errors. That is normal. + +Read the error before searching for another command. + +Keep notes. + +Destroy cloud resources when you finish a lab. + +Use Git regularly. + +Do not expose credentials in screenshots or repositories. + +Most importantly, keep going when a lab becomes difficult. + +Sometimes the day that takes the longest teaches you the most. + +------------------------------------------------------------------------ + +# How This Journey Changed Me + +When I started this journey, I already had development experience, but I +felt stuck. Somewhere during my work as a developer, I realized that a +**DevOps mindset had been hiding inside me all along**. + +Whenever a deployment failed, I was always interested in finding out +why. I liked analyzing failures, debugging quickly, and understanding +what was happening behind the application. That made me think: **why +shouldn't I seriously start my journey into DevOps?** + +Because I already understood development, I decided to go deeper into +the operational side. + +At first, starting with Linux felt completely different. But soon I +understood why Linux comes first. **If you are strong in Linux, you +debug faster.** You understand processes, files, permissions, logs, +resources, and what is actually happening inside the system. + +Networking gave me another level of confidence. When you understand DNS, +ports, IPs, subnets, and how systems communicate, your brain starts +connecting the problem much faster. Instead of randomly trying commands, +you start asking: *Where is the communication stopping? What is blocking +this service? Why can't this workload connect?* + +Then came **Docker and Kubernetes --- and I loved them.** + +I really enjoyed working with containers. As I became more comfortable +with Kubernetes, `kubectl` stopped feeling like just another +command-line tool. Sometimes it genuinely felt like I was **having a +conversation with the cluster**: + +*What is wrong? Show me the pods. Show me the events. Show me the logs. +Why did this deployment fail?* + +And slowly, the cluster would give me the information I needed. + +**Terraform** changed the way I looked at infrastructure. After building +infrastructure through code, going manually through the AWS Console for +everything no longer felt like the best approach. With Terraform, I +could define what I wanted, review it, create it, change it, and destroy +it in a repeatable way. + +Then **Ansible made me say "wow."** Installation, setup, and +configuration across systems could be automated instead of repeated +manually. + +**Observability** showed me a smarter way to troubleshoot. Metrics, +logs, dashboards, and alerts are not just nice graphs. They help you +diagnose what is happening and find problems faster. + +Then came **Helm**. After working with long Kubernetes YAML files, I +understood why Helm is so useful. Templates and values make Kubernetes +configuration much easier to reuse and manage. At the same time, I know +Helm is an area where continued hands-on practice will make me even +stronger. + +When I reached **Amazon EKS**, many things that had seemed complicated +earlier started connecting naturally. I could understand the +relationship between the managed control plane, worker nodes, +networking, storage, Kubernetes workloads, and the application running +on top. + +**ArgoCD and GitOps** took that understanding another step further. +Instead of manually changing production, Git could describe the desired +state and ArgoCD could continuously work to keep the cluster aligned +with it. + +Finally, **Agentic AI for DevOps** was one of the most interesting parts +of the journey. + +I learned that AI in DevOps is not simply asking a chatbot a question. A +DevOps engineer needs to understand how an AI agent investigates an +issue, calls tools, reads infrastructure information, diagnoses +failures, proposes fixes, handles model or tool errors, and works safely +with guardrails and human approval. + +Even working across multiple terminals with Kubernetes, Temporal, AI +models, workers, and debugging failures taught me something important: +**AI becomes powerful in DevOps when the engineer already understands +the system underneath it.** + +Looking back, I don't feel that I suddenly became interested in DevOps +during these 90 days. + +I feel like **I discovered the DevOps engineer that was already hidden +inside the developer in me.** + +Development taught me how to build an application. + +This journey taught me to think about **how to build it, deploy it, +automate it, observe it, troubleshoot it, recover it, and keep it +running.** + +And that is the direction I want to continue. + +------------------------------------------------------------------------ + +# My Day 90 Takeaway + +My biggest takeaway from these 90 days is simple: + +> **DevOps is not about memorizing commands. It is about building a +> reliable path from code to production and knowing how to troubleshoot +> that path when something goes wrong.** + +The tools will continue to change. + +The important ideas remain: + +- Automation +- Infrastructure as Code +- Containerization +- Orchestration +- CI/CD +- Observability +- GitOps +- Security +- Reliability +- Continuous learning + +------------------------------------------------------------------------ + +# 90 Days Completed 🎓 + +From Linux commands to Kubernetes. + +From Docker containers to EKS. + +From Terraform to Ansible. + +From GitHub Actions to ArgoCD. + +From monitoring dashboards to AI-assisted Kubernetes troubleshooting. + +I completed the journey by building, breaking, troubleshooting, fixing, +and learning. + +This is not the end of my DevOps journey. + +It is the foundation for what I build next. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..0cb84f23e0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . + +EXPOSE 5000 + +CMD ["python", "app.py"] diff --git a/app.py b/app.py new file mode 100644 index 0000000000..a3c4f45ade --- /dev/null +++ b/app.py @@ -0,0 +1,15 @@ +from flask import Flask + +app = Flask(__name__) + +@app.route("/") +def home(): + return "Hello from DevOps CI/CD Lab!" + +@app.route("/health") +def health(): + return {"status": "healthy"} + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000) + \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..9324d5a748 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask +pytest +requests \ No newline at end of file diff --git a/scripts/check_day_44.py b/scripts/check_day_44.py new file mode 100644 index 0000000000..ccb6fda1f0 --- /dev/null +++ b/scripts/check_day_44.py @@ -0,0 +1,6 @@ +def add(a, b): + return a + b + +assert add(2, 3) == 5 + +print("Tests passed successfully") diff --git a/scripts/test_app.py b/scripts/test_app.py new file mode 100644 index 0000000000..e93f43b1ad --- /dev/null +++ b/scripts/test_app.py @@ -0,0 +1,13 @@ +from app import app + +def test_home(): + client = app.test_client() + response = client.get("/") + assert response.status_code == 200 + assert b"Hello from DevOps CI/CD Lab!" in response.data + +def test_health(): + client = app.test_client() + response = client.get("/health") + assert response.status_code == 200 + assert response.json["status"] == "healthy" \ No newline at end of file