docker-development-youtube-series Documentation

Go repository by @marcel-dempers. Model Context Protocol (MCP) server & tools with 5.8k stars.

5,823 stars Go Go 99 files · ~125,823 tokens
RAW Doc

Docker & Self-Hosted Deployment Verified OCI

Deploy docker-development-youtube-series on any server, homelab, or cloud container platform.

1-Click Terminal Command
bash (docker run)
docker run -d --name docker-development-youtube-series -p 5000:5000 --restart always aimvector/csharp:1.0.0
Configured Services & Ports
⚙️ csharp
image: aimvector/csharp:1.0.0
🔌 Ports: 5000:5000
⚙️ golang
image: aimvector/golang:1.0.0
🔌 Ports: 5001:5000, 2345:2345
⚙️ python
image: aimvector/python:1.0.0
🔌 Ports: 5003:5000, 5678:5678
⚙️ nodejs
image: aimvector/nodejs:1.0.0
🔌 Ports: 5002:5000, 9229:9229
⚙️ spring-java
image: aimvector/spring-java:1.0.0
🔌 Ports: 9999:8080
docker-compose.yml
yaml (docker-compose.yml)
services:
  csharp: #docker run --rm -it -v ${PWD}:/work -w /work -p 5000:5000 aimvector/csharp:1.0.0 /bin/sh
    container_name: csharp
    image: aimvector/csharp:1.0.0
    build:
      context: ./c#
    volumes:
    - ./c#/src/:/work/
    ports:
      - 5000:5000
  golang: #docker run --rm -it -v ${PWD}:/go/src/work -v ${PWD}/golang/configs/:/configs -v ${PWD}/golang/secrets/:/secrets  -p 5001:5000 -p 2345:2345 aimvector/golang:1.0.0
    container_name: golang
    image: aimvector/golang:1.0.0
    build: 
      context: ./golang
    volumes:
    - ./golang/configs:/configs/
    - ./golang/secrets:/secrets/
    - ./golang/src/:/go/src/work/
    ports:
      - 5001:5000
      - 2345:2345
  python: #docker run --rm -it -v ${PWD}:/work -w /work -p 5003:5000 aimvector/python:1.0.0 /bin/sh
    container_name: python
    image: aimvector/python:1.0.0
    build:
      context: ./python
      #target: debug
    #working_dir: /work      #comment out for build.target:prod
    #entrypoint: /bin/sh     #comment out for build.target:prod
    #stdin_open: true        #comment out for build.target:prod
    #tty: true               #comment out for build.target:prod
    volumes:
    - ./python/src/:/work
    ports:
      - 5003:5000
      - 5678:5678
  nodejs: #docker run --rm -it -v ${PWD}:/work -w /work -p 5002:5000 aimvector/nodejs:1.0.0 /bin/sh
    container_name: nodejs
    image: aimvector/nodejs:1.0.0
    build:
      context: ./nodejs
      target: debug
    #working_dir: /work      #comment out for build.target:prod
    #entrypoint: /bin/sh     #comment out for build.target:prod
    #stdin_open: true        #comment out for build.target:prod
    #tty: true               #comment out for build.target:prod
    volumes:
    - ./nodejs/src/:/work/src/
    ports:
      - 5002:5000
      - 9229:9229            #debug port
  spring-java: #docker run --rm -it -v ${PWD}:/work -w /work -p 5002:5000 aimvector/nodejs:1.0.0 /bin/sh
    container_name: spring-java
    image: aimvector/spring-java:1.0.0
    build:
      context: ./springboot/java/
    working_dir: /work/src/      #comment out for build.target:prod
    entrypoint: /bin/sh     #comment out for build.target:prod
    stdin_open: true        #comment out for build.target:prod
    tty: true               #comment out for build.target:prod
    volumes:
    - ./springboot/java/:/work/src/
    ports:
      - 9999:8080

Technical Documentation: marcel-dempers/docker-development-youtube-series

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (`README.md`)

The Ultimate Engineer Toolbox :hammer::wrench:

A Collection of tools, hands-on walkthroughs with source code.
The Ultimate Swiss Army knife for DevOps, Developers and Platform Engineers

Steps Playlist :tv: Source
Kubernetes Lightning Course In Video descriptions
Learn Kubernetes (my old series) source
Learn about CI/CD tools :whale:
Deploy Kubernetes to the cloud :partly_sunny: source
Monitoring Kubernetes :mag: source
Guide to Logging :page_with_curl: source
Guide to ServiceMesh :globe_with_meridians: source

Docker Development Basics

Step :heavy_check_mark: Video :movie_camera: Source Code :octocat:
Working with Dockerfiles (.NET, Golang, Python, NodeJS) source
Working with code (.NET, Golang, Python, NodeJS) source
Docker Multistage explained source
Debugging Go in Docker source
Debugging .NET in Docker source
Debugging Python in Docker source
Debugging NodeJS in Docker source

Engineering Toolbox :hammer::wrench:

Checkout the toolbox website


Overview (`course/README.md`)

🎓 The Ultimate DevOps Course (early access 🚧)

This course is my ultimate guide to DevOps,SRE & platform engineering for beginners

🚧 This course is currently under early access and undergoing constant review and updates

🚀 Setting the stage

In my many years working in the field, I have noticed a range of challenges and problems that people face when trying to start a DevOps, SRE, platform or cloud engineering career.

🔴The current challenges of careers in Tech - *where to start ?*

DevOps has its challenges:

  • There is no simple starting point to start learning about DevOps
  • DevOps requires a very broad range of skills that can be very overwhelming for new people to learn
  • DevOps is often treated as "Dev" and "Ops" which means you need understanding of two very broad fields
  • DevOps technologies are littered with buzz words, such as cloud, AI, machine learning, CI/CD, scalability, immutable infrastructure, automation, security and it never ends
  • Qualifications have its challenges:

  • Universities or Colleges offer IT degrees or courses, which often only cover traditional & "legacy" IT knowledge.
  • They do not cover real world challenges faced when working with the cloud, or working with popular open source technologies or modern challenges that businesses face these days
  • These technologies are often evolving at a rapid pace and traditional Universities cannot keep up with these.
  • Cloud certifications have its challenges:

  • Cloud certifications often only focus on a specific cloud provider which can build bias
  • Often only solve really specific problems, and miss real world problems
  • Develop very narrow focussed problem solving skill
  • Miss out on powerful open source skills
  • Vendor locking: Often stuck only specialising in one vendor's product
  • 🎯 Course Goals & Expectations

    This course provides a practical hands-on roadmap into the world of DevOps, SRE and platform engineering.
    It will set you up with all the fundamentals required for successful future learning.
    During this course you will apply a wide range of DevOps principles and use modern technologies in a real world hands-on manner to create yourself a personal website.
    While building your website, we will cover topics such as:

  • Source Control
  • Developer Tools
  • Operating Systems
  • Servers & Virtualization
  • Web technology
  • Monitoring, Logging, Metrics
  • Observability
  • Command line and automation scripting
  • CI / CD Pipelines
  • Infrastructure as Code
  • Basics of Cloud
  • Cloud deployments
  • The personal website created throughout this course will become your marketing tool for your career to demonstrate all your newly obtained above-mentioned skills and abilities.
    You will build the ultimate foundation and be equipped to make more informed decisions on what you want to focus on next in your career.

    🛠 System & Other Requirements

  • **a Github account:** To follow along, we will be creating GitHub accounts
  • **Operating System:** Windows (Pro and Enterprise ), Linux or MacOS
  • * *note: You'll need Pro or Enterprise Windows to support Virtualization*
  • You will need at least 2GB EXTRA RAM and 25GB of hard disk space to complete modules related to virtualization.
  • * *These numbers are early access guide only and may be reviewed and adjusted based on course feedback*

    📖 Table of Contents

    📖 Where to find and explore the full course

    You can explore the course on my website


    Overview (`deno/README.md`)

    Introduction to Deno with Docker


    Overview (`drone-ci/README.md`)

    Introduction to Drone CI


    Overview (`jenkins/readme.md`)

    Jenkins on Amazon Kubernetes

    For running Jenkins on AMAZON, start here

    Jenkins on Local (Docker Windows \ Minikube \ etc)

    For running Jenkins on Local Docker for Windows or Minikube
    Watch the video

    Setting up Jenkins Agent

    After installing kubernetes-plugin for Jenkins

    • Go to Manage Jenkins | Bottom of Page | Cloud | Kubernetes (Add kubenretes cloud)
    • Fill out plugin values
      • Name: kubernetes
      • Kubernetes URL: https://kubernetes.default:443
      • Kubernetes Namespace: jenkins
      • Credentials | Add | Jenkins (Choose Kubernetes service account option & Global + Save)
      • Test Connection | Should be successful! If not, check RBAC permissions and fix it!
      • Jenkins URL: http://jenkins
      • Tunnel : jenkins:50000
      • Apply cap only on alive pods : yes!
      • Add Kubernetes Pod Template
        • Name: jenkins-slave
        • Namespace: jenkins
        • Service Account: jenkins
        • Labels: jenkins-slave (you will need to use this label on all jobs)
        • Containers | Add Template
          • Name: jnlp
          • Docker Image: aimvector/jenkins-slave
          • Command to run :
          • Arguments to pass to the command:
          • Allocate pseudo-TTY: yes
          • Add Volume
            • HostPath type
            • HostPath: /var/run/docker.sock
            • Mount Path: /var/run/docker.sock
        • Timeout in seconds for Jenkins connection: 300
    • Save

    Test a build

    To run docker commands inside a jenkins agent you will need a custom jenkins agent with docker-in-docker working.
    Take a look and build the docker file in ./dockerfiles/jenkins-agent
    Push it to a registry and use it instead of above configured * Docker Image: jenkins/jnlp-slave
    If you do not use the custom image, the below pipeline will not work because default * Docker Image: jenkins/jnlp-slave public image does not have docker ability.

    • Add a Jenkins Pipeline
    text
    node('jenkins-slave') {
        
         stage('unit-tests') {
            sh(script: """
                docker run --rm alpine /bin/sh -c "echo hello world"
            """)
        }
    }

    Overview (`kubernetes/readme.md`)

    Learn Kubernetes :hammer::wrench:

    Full playlist

    Getting Started with Kubernetes on Windows for beginners

    Kubectl

    Checkout kubectl for detailed steps

    Deployments

    Checkout deployments for detailed steps

    Configuration Management

    Secret Management

    Services

    Ingress


    StatefulSets

    Persistent Volumes

    Manage YAML


    Overview (`tracing/README.md`)

    Introduction to Distributed Tracing

    In this episode we take a look at distributed tracing.
    We'll take a look at the concept, what distributed tracing is, what problems it solves, how to emit traces and the platform architecture to collect traces.

    Example microservice architecture

    First of all, we need an example application.
    In this demo, I have a few microservices that work together to form a video catalog.

    A simple Web UI: videos-web


    Consider videos-web
    It's an HTML application that lists a bunch of playlists with videos in them.

    text
    +------------+
    | videos-web |
    |            |
    +------------+

    A simple API: playlists-api


    For videos-web to get any content, it needs to make a call to playlists-api

    text
    +------------+     +---------------+
    | videos-web +---->+ playlists-api |
    |            |     |               |
    +------------+     +---------------+

    Playlists consist of data like title, description etc, and a list of videos.
    Playlists are stored in a database.
    playlists-api stores its data in a database

    text
    +------------+     +---------------+    +--------------+
    | videos-web +---->+ playlists-api +--->+ playlists-db |
    |            |     |               |    |              |
    +------------+     +---------------+    +--------------+

    A little complexity


    Each playlist item contains only a list of video id's.
    A playlist does not have the full metadata of each video.

    Example playlist:

    text
    {
      "id" : "playlist-01",
      "title": "Cool playlist",
      "videos" : [ "video-1", "video-x" , "video-b"]
    }

    Take not above videos: [] is a list of video id's

    Videos have their own title and description and other metadata.

    To get this data, we need a videos-api
    This videos-api has its own database too

    text
    +------------+       +-----------+
    | videos-api +------>+ videos-db |
    |            |       |           |
    +------------+       +-----------+

    For the playlists-api to load all the video data, it needs to call videos-api for each video ID it has.

    Traffic flow


    A single GET request to the playlists-api will get all the playlists
    from its database with a single DB call

    For every playlist and every video in each list, a separate GET call will be made to the videos-api which will
    retrieve the video metadata from its database.

    This will result in many network fanouts between playlists-api and videos-api and many call to its database.
    This is intentional to demonstrate a busy network.

    Full application architecture


    text
    +------------+     +---------------+    +--------------+
    | videos-web +---->+ playlists-api +--->+ playlists-db |
    |            |     |               |    |    [redis]   |
    +------------+     +-----+---------+    +--------------+
                             |
                             v
                       +-----+------+       +-----------+
                       | videos-api +------>+ videos-db |
                       |            |       |  [redis]  |
                       +------------+       +-----------+

    Run the apps: Docker


    There is a docker-compose.yaml in this directory.
    Change your terminal to this folder and run:

    text
    cd tracing
    
    docker-compose build
    
    docker-compose up

    You can access the app on http://localhost.
    You should now see the complete architecture in the browser

    Traces


    To see our traces, we can access the Jaeger UI on http://localhost:16686


    Overview (`ai/agents/README.md`)

    Introduction to Agents

    In this video guide we discuss what an AI agent is from a DevOps, SRE & Platform Engineering perspective.

    Engineers will play a key role in:

    • Hosting Agents in cloud, Clusters, Local, etc
    • Understand traffic routing to agents
    • Securing Agentic systems

    In this guide we learn about:

    • What an agent is
    • Different forms of AI Agents
    • Context engineering
    • Agents.md & System Instructions
    • Subagents
    • Agent Skills

    Agents.md

    AI Agents usually follow their own System Instructions depending on the Agent type. For example Gemini looks for GEMINI.md at the root of the repo. Claude looks for CLAUDE.md

    There is a open standard for AGENTS.md.

    Our example AGENTS.md can be copied to the root of this repo to demonstrate a system instruction for an agent using a tool like OpenCode.

    For a deeper dive in setting up agents for each AI (Gemini,Claude,Copilot,etc), checkout the playlist here

    Subagents

    Agent usually have one contenxt window.
    Subagents provide capability to have dedicated agents for each specilized task with their own context windows. For example, you can have a dedicated agent for troubleshooting or debugging, vs Code Review. This means you can keep the context window of the main agent free.

    Our example subagent can be copied into the subagent directory for OpenCode which is .opencode/agents/k8s_expert/k8s_expert.md and our OpenCode terminal will pick it up on restart.

    For a deeper dive in setting up subagents for each AI (Gemini,Claude,Copilot,etc), checkout the playlist here

    Agent Skills

    If you look at Agents, you will notice the markdown file may become quite large.
    We have a lot of commands and scripts inside that file.

    Agent Skills allow us to take this to the next level. Instead of having all the "brains" in the Agents.md file, we can break up functionality and place them in skills.

    In this video, we'll discuss why skills are so important and how to use them

    There is an open standard written for Agent Skills as well.

    Our example skills folder can be copied to the dedicated .agents/skills/<name-of-skills> folder where our OpenCode terminal will pick it up.

    The main thing you will notice once we use skills, is that the Agents instruction file will become a lot leaner and we can essentially move all the "how to"'s out and into scripts.

    The instructions then just clearly point to skills that the agent can execute.

    We can summarise it as:

    markdown
    ---
    name: k8s_expert
    description: A specialized kubernetes agent specialised in local cluster development and troubleshooting.
    ---
    
    You help engineers provision, inspect, and debug Kubernetes clusters running on their local machine.
    
    ## Persona & Behaviour
    
    - Speak like a senior platform engineer: direct, precise, no hand-holding
    - Always confirm tool availability before running commands
    - Prefer `kind` for local clusters unless the user specifies otherwise
    - When creating clusters, always confirm the cluster name and Kubernetes version first
    - When troubleshooting, always check events before logs
    
    ## Available Skills
    
    * Provision test k8s infrastructure for testing technical guides using provided available skill

    We can proceed to use the skill by either referring to it in a prompt or depending on the AI app we are using, for example we can use /skills in OpenCode to execute a skill.


    Overview (`ai/mcp/README.md`)

    Introduction to MCP

    Running an MCP

    Start our simple portable sandbox in a linux container.
    We'll expose a port so we can showcase HTTP calls as well.

    shell
    docker run -it --rm \
      -p 8080:8080 \
      ubuntu:latest bash
    
    # install some tools we'll use:
    apt-get update && apt-get install -y curl tree git jq tar
    
    #install nodejs and python as a requirement for many MCP servers
    curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.4/install.sh | bash
    # in lieu of restarting the shell
    \. "$HOME/.nvm/nvm.sh"
    nvm install 24
    node -v
    
    apt-get install -y python3 python3-pip python3-venv# Create the environment
    python3 -m venv /opt/venv
    source /opt/venv/bin/activate

    Example 1: STDIO (File & GIT MCP Demo)

    Firstly, let's look at a git MCP server.
    To do this we need a basic git repo, lets create one:

    shell
    mkdir -p /workspace && cd /workspace
    echo "# Hello MCP" > README.md
    echo "console.log('hello')" > app.js
    mkdir src && echo "export default {}" > src/index.js
    tree
    
    git init
    git config user.email "[email protected]"
    git config user.name "MCP Demo"
    git add .
    git commit -m "initial commit"
    
    # leave an uncommitted change for the diff demo
    echo "## Updated" >> README.md

    File MCP Example:

    Use the MCP in three steps:

    shell
    (
      echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'
      echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"list_directory","arguments":{"path":"/workspace"}},"id":2}'
      echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"read_text_file","arguments":{"path":"/workspace/README.md"}},"id":3}'
      sleep 2
    ) | npx -y @modelcontextprotocol/server-filesystem /workspace 2>/dev/null | jq -c .

    GIT MCP Example:

    Setup git MCP:

    shell
    pip3 install mcp-server-git

    Use the MCP in three steps:

    shell
    (
      echo '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'
      echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"git_log","arguments":{"repo_path":"/workspace","max_count":3}},"id":2}'
      echo '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"git_diff_unstaged","arguments":{"repo_path":"/workspace"}},"id":3}'
      sleep 2
    ) | python3 -u -m mcp_server_git --repository /workspace 2>/dev/null | jq -c .

    Example 2: HTTP (GitHub & K8s MCP Demo)

    In this example, we move from stdio to HTTP which is basically just MCP hosted over a Web Endpoint.

    Github MCP Example:

    There are official MCP servers available online. The Official GitHub MCP server is what we will be using

    Let's install it:

    shell
    curl -fsSL https://github.com/github/github-mcp-server/releases/download/v1.0.5/github-mcp-server_Linux_x86_64.tar.gz \
      | tar -xz -C /usr/local/bin github-mcp-server
    github-mcp-server --help

    Start it:

    shell
    github-mcp-server http --port 8080

    We need to provide authentication with Github:

    shell
    export GITHUB_PERSONAL_ACCESS_TOKEN=''

    Call it in three steps:

    shell
    # step-1: initialise
    curl -s -i http://localhost:8080/mcp \
           -H "Content-Type: application/json" \
           -H "Accept: application/json, text/event-stream" \
           -H "Authorization: Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}" \
           -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'
    
    MCP_SESSION_ID=''
    
    # step-2: list tools
    curl -s http://localhost:8080/mcp \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -H "Authorization: Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}" \
      -H "Mcp-Session-Id: ${MCP_SESSION_ID}" \
      -d '{"jsonrpc":"2.0","method":"tools/list","id":2}'
    
    # step-3: tool call
    curl -s http://localhost:8080/mcp \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -H "Authorization: Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}" \
      -H "Mcp-Session-Id: ${MCP_SESSION_ID}" \
      -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"search_repositories","arguments":{"query":"kubernetes gateway-api","sort":"stars","perPage":5}},"id":3}'

    Kubernetes MCP Example:

    Lets create a Kubernetes cluster to play with using kind

    shell
    kind create cluster --name mcp --image kindest/node:v1.34.0

    In this example we will use a Kubernetes MCP and setup a read-only ServiceAccount so it can read our cluster.

    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/kubernetes/02-mcp.yaml
    
    # check mcp server
    kubectl get pods
    kubectl get sa
    kubectl get service
    
    # forward ports
    kubectl port-forward svc/mcp-k8s 8080

    Call it in three steps:

    shell
    # step-1: initialise
    curl -s -i http://localhost:8080/mcp \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'
    
    MCP_SESSION_ID=''
    
    # step-2: list tools
    curl -s http://localhost:8080/mcp \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -H "Mcp-Session-Id: ${MCP_SESSION_ID}" \
      -d '{"jsonrpc":"2.0","method":"tools/list","id":2}'
    
    # step-3: tool call
    curl -s http://localhost:8080/mcp \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -H "Mcp-Session-Id: ${MCP_SESSION_ID}" \
      -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"namespaces_list","arguments":{}},"id":3}'

    Overview (`apache/kafka/README.md`)

    Introduction to Kafka

    This guide is under the messaging section alongside other message brokers like RabbitMQ etc.
    Checkout the guide under the messaging/kafka folder


    Overview (`argo/argo-cd/README.md`)

    Introduction to Argo CD


    Overview (`golang/introduction/readme.md`)

    Introduction to Learning Go

    Go can be downloaded from golang.org

    Test your go installation:

    text
    go version

    Run Go in Docker

    We can also run go in a small docker container:

    text
    cd golang\introduction
    
    docker build --target dev . -t go
    docker run -it -v ${PWD}:/work go sh
    go version

    Code Structure

    https://golang.org/doc/code.html

    • Package:

      • Source files in same directory that are compiled together
      • Have visibility on all source files in the same package
    • Modules:

      • Collection of packages that are released together

    Our repository can contain one or more go modules, but usually 1.

    • At the root of the repo

    go.mod Declares module path + import path for packages. (Where to download them)

    • When we write our own program, we can define a module path
    • This allows us to publish our code (if we want), so others can download it
    • The module path could be something like github.com/google/go-cmp
    • Makes it easy for other programs to consume our module

    Our first Program

    • Create a folder containing our application
    text
    mkdir app
    • Define a module path (github.com/docker-development-youtube-series/golang/introdouction/app)
    text
    # change directory to your application source code
    
    cd app
    
    # create a go module file
    
    go mod init github.com/docker-development-youtube-series/golang/introdouction/app
    • Create basic source code

    In the app folder, create a program called app.go
    Paste the following content into it.

    text
    package main
    
    import "fmt"
    
    func main() {
    	fmt.Println("Hello, world.")
    }
    • Run your application code

    You can run your application

    text
    go run app.go

    Building our Program

    Build your application into a static binary:

    text
    go build

    This will produce a compiled program called app
    You can run this program easily:

    text
    ./app

    Install your application (optional)

    "This command builds the app command, producing an executable binary.
    It then installs that binary as $HOME/go/bin/app (or, under Windows, %USERPROFILE%\go\bin\app.exe)"

    text
    go install github.com/docker-development-youtube-series/golang/introdouction/app

    The Code

    Functions

    In the video we cover writing functions.
    It allows us to execute a block of code
    You want to give your function a single purpose
    Functions can have an input and return an output
    Well thought out functions makes it easier to write tests

    Instead of doing a boring x + y function that adds two numbers, let's do something a little
    more realistic but still basic:

    text
    // This function returns some data 
    // The data could be coming from a database, or a file.
    // The person calling the function should not care
    // Since the function does not leak its Data provider
    
    func getData(inputs)(outputs){
    }
    
    // functions can take multiple inputs, and return multiple outputs
    
    // lets say we have 1) customers and 2) the cities they are from
    // we may want to 1) get a list of customers and 2) get a list of cities
    // therefore we have 2 types of data, 1) customers 2) cities
    // let's improve our function so its gets data based on the type
    
    func getData(customerId int) (customer string) {
    }

    Variables

    To hold data in programming languages, we use variables.
    Variables take up space in memory, so we want to keep it minimal.
    Let's declare variables in our function

    text
    func getData(customerId int) (customer string) {
      var firstName = "Marcel"
      lastName := "Dempers"
      
      fullName := firstName + " " + lastName
      return fullName
    
      //or we can return the computation instead of adding another variable!
      return firstName + " " + lastName
    
      //or we dont even need to declare variables :)
      return "Marcel Dempers"
    
    }

    Control Flows (if\else)

    You can see we're not using the customerId input in our function.
    Let's use it!

    Control flows allow us to add "rules" to our code.
    "If this is the case, then do that, else do something else".

    So let's say we have a customer ID 1 coming in, we may only want to
    return our customer if it matches the customerId

    text
    func getData(customerId int) (customer string) {
    
      if customerId == 1 {
        return "Marcel Dempers"
      } else if customerId == 2 {
        return "Bob Smith"
      } else {
        return ""
      }
      
    }

    Let's invoke our function :

    text
    //in the main() function
    
    //get our customer
    customer := getData(1)
    fmt.Println(customer)
    
    //get the wrong customer
    customer := getData(3)
    fmt.Println(customer)

    Arrays

    At the moment, we can only return 1 customer at a time on our function.
    Realistically we need the ability to return more data, not just a single customer.

    Arrays allow us to make a collection of variables of the same type.
    We can now return a list of customers!

    Let's change our function to get an array of customers!

    text
    func getData() (customers [2]string) {
      //create 1 record
      customer := "Marcel Dempers"
    
      //assign our customer to the array
      customers[0] = customer
    
      //OR we can assign it like this
      customers[1] = "Bob Smith"
    
      //send it back to the caller
      return customers
      
    }

    Now we also have to change our calling function to expect an array:

    text
    customers := getData()
    
    fmt.Println(customers)

    Slices

    Since arrays are fixed size, Slices are a dynamically-sized view into arrays.
    Let's create a slice instead of array so we can add customers dynamically!

    text
    func getData() (customers []string) {
      
      //initialise our slice of type string
      customers = []string{ "Marcel Dempers", "Bob Smith", "John Smith"}
      
      //add more legendary customers dynamically 
      customers = append(customers, "Ben Spain")
      customers = append(customers, "Aleem Janmohamed")
      customers = append(customers, "Jamie le Notre")
      customers = append(customers, "Victor Savkov")
      customers = append(customers, "P The Admin")
      customers = append(customers, "Adrian Oprea")
      customers = append(customers, "Jonathan D")
    
      //send it back to the caller
      return customers
      
    }

    Loops

    Loops are used to iterate over collections, lists, arrays etc.
    Let's say we need to loop through our customers

    In the main() function, we can grab the list of customers and loop them.
    In this demo, we'll cover a basic for loop, but there are several approaches to writing loops.

    text
    //loop forever
    for {
      //any code in here will run forever!
      
      fmt.Println("Infinite Loop 1")
    	time.Sleep(time.Second)
    
      //unless we break out the loop like this
      break
    }
    
    //loop for x number of loops
    for x := 0; x < 10; x++ {
    
      //any code in here will run 10 times! (unless we break!)
      fmt.Println(customers[x])
    
    }
    
    //loop for ALL our customer
    
    for x, customer := range customers {
    
      //we can access the "customer" variable in this approach
      customer = customers[x]
      fmt.Println(customer)
    
      //OR
      //we can use the supplied customer from the loop
      // and silence the x variable, replace it with a _ character
      fmt.Println(customer)
    }

    Structs

    So far so good, however, customer data is not useful as strings.
    Customers can have a firstname, lastname, and more properties.

    For this purpose we'd like to group some variables into a single variable.
    This is what struct allows us to do.
    Let's create a struct for our customer

    Let's create a new go file called customers.go

    text
    package main
    
    Customer struct {
      FirstName string
      LastName string
      FullName string
    }

    Let's put it all together:

    In customers.go, let's create a function to get customers

    text
    func GetCustomers()(customers []Customer) {
    
      //we can declare customers like this:
      marcel := 	Customer{ FirstName: "Marcel", LastName: "Dempers" }
    
    	customers = append(customers,
    		Customer{ FirstName: "Marcel", LastName: "Dempers" },
    		Customer{ FirstName: "Ben", LastName: "Spain" },
    		Customer{ FirstName: "Aleem", LastName: "Janmohamed" },
    		Customer{ FirstName: "Jamie", LastName: "le Notre" },
    		Customer{ FirstName: "Victor", LastName: "Savkov" },
    		Customer{ FirstName: "P", LastName: "The Admin" },
    		Customer{ FirstName: "Adrian", LastName: "Oprea" },
    		Customer{ FirstName: "Jonathan", LastName: "D" },
    	)
    
    	return customers
    
    }

    In main() we can now call our shiny new function

    text
    customers := GetCustomers()
    
    for _, customer := range customers {
      //we can access the "customer" variable in this approach
      fmt.Println(customer)
    }

    Docker

    For our dev environment, we have a simple image using go
    We also set a work directory and alias the target as dev

    This means we can use this container layer as a development environment.
    Later down the track we can add debuggers in here for example.
    Checkout my debugging video for go: https://youtu.be/kToyI16IFxs

    Development environment

    text
    FROM golang:1.15 as dev
    
    WORKDIR /work

    Building our code

    text
    FROM golang:1.15 as build
    
    WORKDIR /app
    COPY ./app/* /app/
    RUN go build -o app

    The Runtime

    text
    FROM alpine as runtime 
    COPY --from=build /app/app /
    CMD ./app

    Building the Container

    text
    docker build . -t customer-app

    Running the Container

    text
    docker run customer-app

    Overview (`hashicorp/vault-2022/readme.md`)

    Hashicorp Vault Guide

    Requirements:

    • Kubernetes 1.21
    • Kind or Minikube

    For this tutorial, I will be using Kubernetes 1.21.
    If you are watching the old guide for Kubernetes 1.17, go here

    Lets create a Kubernetes cluster to play with using kind

    text
    cd hashicorp/vault-2022
    
    kind create cluster --name vault --image kindest/node:v1.21.1 --config kind.yaml

    Next up, I will be running a small container where I will be doing all the work from:
    You can skip this part if you already have kubectl and helm on your machine.

    text
    docker run -it --rm --net host -v ${HOME}/.kube/:/root/.kube/ -v ${PWD}:/work -w /work alpine sh

    Install kubectl

    text
    apk add --no-cache curl
    curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
    chmod +x ./kubectl
    mv ./kubectl /usr/local/bin/kubectl

    Install helm

    text
    curl -LO https://get.helm.sh/helm-v3.7.2-linux-amd64.tar.gz
    tar -C /tmp/ -zxvf helm-v3.7.2-linux-amd64.tar.gz
    rm helm-v3.7.2-linux-amd64.tar.gz
    mv /tmp/linux-amd64/helm /usr/local/bin/helm
    chmod +x /usr/local/bin/helm

    Now we have helm and kubectl and can access our kind cluster:

    text
    kubectl get nodes
    NAME                  STATUS   ROLES                  AGE   VERSION
    vault-control-plane   Ready    control-plane,master   37s   v1.21.1

    Let's add the Helm repositories, so we can access the Kubernetes manifests

    text
    helm repo add hashicorp https://helm.releases.hashicorp.com

    Storage: Consul

    We will use a very basic Consul cluster for our Vault backend.
    Let's find what versions of Consul are available:

    text
    helm search repo hashicorp/consul --versions

    We can use chart 0.39.0 which is the latest at the time of this demo
    Let's create a manifests folder and grab the YAML:

    text
    mkdir manifests
    
    helm template consul hashicorp/consul \
      --namespace vault \
      --version 0.39.0 \
      -f consul-values.yaml \
      > ./manifests/consul.yaml

    Deploy the consul services:

    text
    kubectl create ns vault
    kubectl -n vault apply -f ./manifests/consul.yaml
    kubectl -n vault get pods

    TLS End to End Encryption

    See steps in ./tls/ssl_generate_self_signed.md
    You'll need to generate TLS certs (or bring your own)
    Remember not to check-in your TLS to GIT :)

    Create the TLS secret

    text
    kubectl -n vault create secret tls tls-ca \
     --cert ./tls/ca.pem  \
     --key ./tls/ca-key.pem
    
    kubectl -n vault create secret tls tls-server \
      --cert ./tls/vault.pem \
      --key ./tls/vault-key.pem

    Generate Kubernetes Manifests

    Let's find what versions of vault are available:

    text
    helm search repo hashicorp/vault --versions

    In this demo I will use the 0.19.0 chart

    Let's firstly create a values file to customize vault.
    Let's grab the manifests:

    text
    helm template vault hashicorp/vault \
      --namespace vault \
      --version 0.19.0 \
      -f vault-values.yaml \
      > ./manifests/vault.yaml

    Deployment

    text
    kubectl -n vault apply -f ./manifests/vault.yaml
    kubectl -n vault get pods

    Initialising Vault

    text
    kubectl -n vault exec -it vault-0 -- sh
    kubectl -n vault exec -it vault-1 -- sh
    kubectl -n vault exec -it vault-2 -- sh
    
    vault operator init
    vault operator unseal
    
    kubectl -n vault exec -it vault-0 -- vault status
    kubectl -n vault exec -it vault-1 -- vault status
    kubectl -n vault exec -it vault-2 -- vault status

    Web UI

    Let's checkout the web UI:

    text
    kubectl -n vault get svc
    kubectl -n vault port-forward svc/vault-ui 443:8200

    Now we can access the web UI here

    Enable Kubernetes Authentication

    For the injector to be authorised to access vault, we need to enable K8s auth

    text
    kubectl -n vault exec -it vault-0 -- sh 
    
    vault login
    vault auth enable kubernetes
    
    vault write auth/kubernetes/config \
    token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
    kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \
    kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
    issuer="https://kubernetes.default.svc.cluster.local"
    exit

    Summary

    So we have a vault, an injector, TLS end to end, stateful storage.
    The injector can now inject secrets for pods from the vault.

    Now we are ready to use the platform for different types of secrets:

    Secret Injection Guides

    Basic Secrets

    Objective:

    • Let's create a basic secret in vault manually
    • Application consumes the secret automatically

    Try it


    Overview (`hashicorp/vault/readme.md`)

    Hashicorp Vault Guide - Deprecated

    Vault

    For this tutorial, I use Kubernetes 1.17
    It's critical because we'll need certain admission controllers enabled.

    To get 1.17 for Linux\Windows, just use kind since you can create a 1.17 with admissions all setup.

    text
    #Windows
    kind create cluster --name vault --image kindest/node:v1.17.0@sha256:9512edae126da271b66b990b6fff768fbb7cd786c7d39e86bdf55906352fdf62
    
    #Linux
    kind create cluster --name vault --kubeconfig ~/.kube/kind-vault --image kindest/node:v1.17.0@sha256:9512edae126da271b66b990b6fff768fbb7cd786c7d39e86bdf55906352fdf62

    TLS End to End Encryption

    VIDEO: <Coming-Soon>
    See steps in ./tls/ssl_generate_self_signed.txt
    You'll need to generate TLS certs (or bring your own)
    Create base64 strings from the files, place it in the server-tls-secret.yaml and apply it.
    Remember not to check-in your TLS to GIT :)

    Deployment

    text
    kubectl create ns vault-example
    kubectl -n vault-example apply -f ./hashicorp/vault/server/
    kubectl -n vault-example get pods

    Storage

    text
    kubectl -n vault-example get pvc

    ensure vault-claim is bound, if not, kubectl -n vault-example describe pvc vault-claim
    ensure correct storage class is used for your cluster.
    if you need to change the storage class, delete the pvc, edit YAML and re-apply

    Initialising Vault

    text
    kubectl -n vault-example exec -it vault-example-0 vault operator init
    # unseal 3 times
    kubectl -n vault-example exec -it vault-example-0 vault operator unseal
    kubectl -n vault-example get pods

    Deploy the Injector

    VIDEO: <Coming-Soon>
    Injector allows pods to automatically get secrets from the vault.

    text
    kubectl -n vault-example apply -f ./hashicorp/vault/injector/
    kubectl -n vault-example get pods

    Injector Kubernetes Auth Policy

    For the injector to be authorised to access vault, we need to enable K8s auth

    text
    kubectl -n vault-example exec -it vault-example-0 vault login
    kubectl -n vault-example exec -it vault-example-0 vault auth enable kubernetes
    
    kubectl -n vault-example exec -it vault-example-0 sh
    vault write auth/kubernetes/config \
    token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
    kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \
    kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
    exit
    
    kubectl -n vault-example get pods

    Summary

    So we have a vault, an injector, TLS end to end, stateful storage.
    The injector can now inject secrets for pods from the vault.

    Now we are ready to use the platform for different types of secrets:

    Secret Injection Guides

    I've broken this down into basic guides to avoid this document from becoming too large.

    Basic Secrets

    Objective:

    • Let's create a basic secret in vault manually
    • Application consumes the secret automatically

    Try it

    Dynamic Secrets: Postgres

    Objective:

    • We have a Postgres Database
    • Let's delegate Vault to manage life cycles of our database credentials
    • Deploy an app, that automatically gets it's credentials from vault

    Try it


    Overview (`jenkins/amazon-eks/readme.md`)

    Jenkins on Amazon Kubernetes

    Create a cluster

    Follow my Introduction to Amazon EKS for beginners guide, to create a cluster
    Video here

    Setup our Cloud Storage

    text
    # deploy EFS storage driver
    kubectl apply -k "github.com/kubernetes-sigs/aws-efs-csi-driver/deploy/kubernetes/overlays/stable/?ref=master"
    
    # get VPC ID
    aws eks describe-cluster --name getting-started-eks --query "cluster.resourcesVpcConfig.vpcId" --output text
    # Get CIDR range
    aws ec2 describe-vpcs --vpc-ids vpc-id --query "Vpcs[].CidrBlock" --output text
    
    # security for our instances to access file storage
    aws ec2 create-security-group --description efs-test-sg --group-name efs-sg --vpc-id VPC_ID
    aws ec2 authorize-security-group-ingress --group-id sg-xxx  --protocol tcp --port 2049 --cidr VPC_CIDR
    
    # create storage
    aws efs create-file-system --creation-token eks-efs
    
    # create mount point 
    aws efs create-mount-target --file-system-id FileSystemId --subnet-id SubnetID --security-group GroupID
    
    # grab our volume handle to update our PV YAML
    aws efs describe-file-systems --query "FileSystems[*].FileSystemId" --output text

    More details about EKS storage here

    Setup a namespace

    text
    kubectl create ns jenkins

    Setup our storage for Jenkins

    text
    kubectl get storageclass
    
    # create volume
    kubectl apply -f ./jenkins/amazon-eks/jenkins.pv.yaml 
    kubectl get pv
    
    # create volume claim
    kubectl apply -n jenkins -f ./jenkins/amazon-eks/jenkins.pvc.yaml
    kubectl -n jenkins get pvc

    Deploy Jenkins

    text
    # rbac
    kubectl apply -n jenkins -f ./jenkins/jenkins.rbac.yaml 
    
    kubectl apply -n jenkins -f ./jenkins/jenkins.deployment.yaml
    
    kubectl -n jenkins get pods

    Expose a service for agents

    text
    kubectl apply -n jenkins -f ./jenkins/jenkins.service.yaml

    Jenkins Initial Setup

    text
    kubectl -n jenkins exec -it <podname> cat /var/jenkins_home/secrets/initialAdminPassword
    kubectl port-forward -n jenkins <podname> 8080
    
    # setup user and recommended basic plugins
    # let it continue while we move on!

    SSH to our node to get Docker user info

    text
    eval $(ssh-agent)
    ssh-add ~/.ssh/id_rsa
    ssh -i ~/.ssh/id_rsa [email protected]
    id -u docker
    cat /etc/group
    # Get user ID for docker
    # Get group ID for docker

    Docker Jenkins Agent

    Docker file is here

    text
    # you can build it
    
    cd ./jenkins/dockerfiles/
    docker build . -t aimvector/jenkins-slave

    Continue Jenkins setup

    Install Kubernetes Plugin
    Configure Plugin: Values I used are here

    Install Kubernetes Plugin

    Try a pipeline

    text
    /* Detailed source-code truncated for AI context efficiency. */

    Overview (`kubernetes/affinity/README.md`)

    Kubernetes Concept: Affinity \ Anti-Affinity

    Create a kubernetes cluster

    In this guide we we''ll need a Kubernetes cluster for testing. Let's create one using kind

    text
    cd kubernetes/affinity
    kind create cluster --name demo --image kindest/node:v1.28.0 --config kind.yaml

    Test the cluster:

    text
    kubectl get nodes
    NAME                 STATUS   ROLES           AGE   VERSION
    demo-control-plane   Ready    control-plane   59s   v1.28.0
    demo-worker          Ready    <none>          36s   v1.28.0
    demo-worker2         Ready    <none>          35s   v1.28.0
    demo-worker3         Ready    <none>          35s   v1.28.0

    Node Affinity

    Node Affinity is similar to nodeSelector however you can define more complex expressions. "Like my pods must run on SSD nodes or preffer SSD nodes"

    For example:

    • Node selector is a hard and fast rule meaning a pod will not be scheduled if the selection is not satisfied
    • For example, when using os selector as linux , a pod can only be scheduled if there is a node available where os label is linux

    Node Affinity allows an expression.

    text
    kubectl apply -f node-affinity.yaml

    We can see our pods are prefering SSD and are always going to us-east

    text
    kubectl get pods -owide 
    
    #introduce more pods
    kubectl scale deploy app-disk --replicas 10
    
    #observe all pods on demo-worker

    If there is some trouble with our ssd disk, kubectl taint nodes demo-worker type=ssd:NoSchedule, we can see pods going to the non-ssd disk nodes in us-east

    This is because our pods prefer SSD, however there is no SSD available, so would still go to non-SSD nodes as long as there are nodes available in us-east

    If something goes wrong in our last us-east node: kubectl taint nodes demo-worker3 type=ssd:NoSchedule and we roll out more pods kubectl scale deploy app-disk --replicas 20,
    notice that our new pods are now in Pending status because no nodes satisfy our node affinity rules

    Fix our nodes.

    text
    kubectl taint nodes demo-worker type=ssd:NoSchedule-
    kubectl taint nodes demo-worker3 type=ssd:NoSchedule-

    Scale back down to 0

    text
    kubectl scale deploy app-disk --replicas 0
    kubectl scale deploy app-disk --replicas 1
    
    # pod should go back to demo-worker , node 1
    kubectl get pods -owide

    Pod Affinity

    Now Pod Affinity is an expression to allow us to state that pods should gravitate towards other pods

    text
    kubectl apply -f pod-affinity.yaml
    
    # observe where pods get deployed
    kubectl get pods -owide
    
    kubectl scale deploy app-disk --replicas 3
    kubectl scale deploy web-disk --replicas 3

    Pod Anti-Affinity

    Let's say we observe our app-disk application disk usage is quite intense, and we would like to prevent app-disk pods from running together.
    This is where anti-affinity comes in:

    text
    podAntiAffinity:
      requiredDuringSchedulingIgnoredDuringExecution:
      - labelSelector:
          matchExpressions:
          - key: app
            operator: In
            values:
            - app-disk
        topologyKey: "kubernetes.io/hostname"

    After applying the above, we can roll it out and observe scheduling:

    text
    kubectl scale deploy app-disk --replicas 0
    kubectl scale deploy web-disk --replicas 0
    kubectl apply -f node-affinity.yaml
    kubectl get pods -owide
    
    kubectl scale deploy app-disk --replicas 2 #notice pending pods when scaling to 3
    kubectl get pods -owide
    kubectl scale deploy web-disk --replicas 2
    kubectl get pods -owide

    Overview (`kubernetes/autoscaling/readme.md`)

    Kubernetes Autoscaling Guide

    Cluster Autoscaling

    Cluster autoscaler allows us to scale cluster nodes when they become full
    I would recommend to learn about scaling your cluster nodes before scaling pods.
    Video here

    Horizontal Pod Autoscaling

    HPA allows us to scale pods when their resource utilisation goes over a threshold

    Requirements

    A Cluster

    • For both autoscaling guides, we'll need a cluster.
    • For Cluster Autoscaler You need a cloud based cluster that supports the cluster autoscaler
    • For HPA We'll use kind

    Cluster Autoscaling - Creating an AKS Cluster

    text
    # azure example
    
    NAME=aks-getting-started
    RESOURCEGROUP=aks-getting-started
    SERVICE_PRINCIPAL=
    SERVICE_PRINCIPAL_SECRET=
    
    az aks create -n $NAME \
    --resource-group $RESOURCEGROUP \
    --location australiaeast \
    --kubernetes-version 1.16.10 \
    --nodepool-name default \
    --node-count 1 \
    --node-vm-size Standard_F4s_v2  \
    --node-osdisk-size 250 \
    --service-principal $SERVICE_PRINCIPAL \
    --client-secret $SERVICE_PRINCIPAL_SECRET \
    --output none \
    --enable-cluster-autoscaler \
    --min-count 1 \
    --max-count 5

    Horizontal Pod Autocaling - Creating a Kind Cluster

    My Node has 6 CPU cores for this demo

    text
    kind create cluster --name hpa --image kindest/node:v1.18.4

    Metric Server

    • For Cluster Autoscaler - On cloud-based clusters, Metric server may already be installed.
    • For HPA - We're using kind

    Metric Server provides container resource metrics for use in autoscaling pipelines

    Because I run K8s 1.18 in kind, the Metric Server version i need is 0.3.7
    We will need to deploy Metric Server 0.3.7
    I used components.yamlfrom the release page link above.

    Important Note : For Demo clusters (like kind), you will need to disable TLS
    You can disable TLS by adding the following to the metrics-server container args

    For production, make sure you remove the following :

    text
    - --kubelet-insecure-tls
    - --kubelet-preferred-address-types="InternalIP"

    Deployment:

    text
    cd kubernetes\autoscaling
    kubectl -n kube-system apply -f .\components\metric-server\metricserver-0.3.7.yaml
    
    #test 
    kubectl -n kube-system get pods
    
    #note: wait for metrics to populate!
    kubectl top nodes

    Example Application

    For all autoscaling guides, we'll need a simple app, that generates some CPU load

    • Build the app
    • Push it to a registry
    • Ensure resource requirements are set
    • Deploy it to Kubernetes
    • Ensure metrics are visible for the app
    text
    # build
    
    cd kubernetes\autoscaling\components\application
    docker build . -t aimvector/application-cpu:v1.0.0
    
    # push
    docker push aimvector/application-cpu:v1.0.0
    
    # resource requirements
    resources:
      requests:
        memory: "50Mi"
        cpu: "500m"
      limits:
        memory: "500Mi"
        cpu: "2000m"
    
    # deploy 
    kubectl apply -f deployment.yaml
    
    # metrics
    kubectl top pods

    Cluster Autoscaler

    For cluster autoscaling, you should be able to scale the pods manually and watch the cluster scale.
    Cluster autoscaling stops here.
    For Pod Autoscaling (HPA), continue

    Generate some traffic

    Let's deploy a simple traffic generator pod

    text
    cd kubernetes\autoscaling\components\application
    kubectl apply -f .\traffic-generator.yaml
    
    # get a terminal to the traffic-generator
    kubectl exec -it traffic-generator sh
    
    # install wrk
    apk add --no-cache wrk
    
    # simulate some load
    wrk -c 5 -t 5 -d 99999 -H "Connection: Close" http://application-cpu
    
    #you can scale to pods manually and see roughly 6-7 pods will satisfy resource requests.
    kubectl scale deploy/application-cpu --replicas 2

    Deploy an autoscaler

    text
    # scale the deployment back down to 2
    kubectl scale deploy/application-cpu --replicas 2
    
    # deploy the autoscaler
    kubectl autoscale deploy/application-cpu --cpu-percent=95 --min=1 --max=10
    
    # pods should scale to roughly 6-7 to match criteria of 95% of resource requests
    
    kubectl get pods
    kubectl top pods
    kubectl get hpa/application-cpu  -owide
    
    kubectl describe hpa/application-cpu

    Vertical Pod Autoscaling

    The vertical pod autoscaler allows us to automatically set request values on our pods
    based on recommendations.
    This helps us tune the request values based on actual CPU and Memory usage.

    More here


    Overview (`kubernetes/cert-manager/README.md`)

    Introduction to cert-manager for Kubernetes

    We need a Kubernetes cluster

    Lets create a Kubernetes cluster to play with using kind

    shell
    kind create cluster --name certmanager --image kindest/node:v1.34.0

    Concepts

    It's important to understand the various concepts and new Kubernetes resources that
    cert-manager introduces.

    • Issuers docs
    • Certificate docs
    • CertificateRequests docs
    • Orders and Challenges docs

    Installation

    You can find the latest release for cert-manager on their GitHub Releases page

    For this demo, I will use K8s 1.34 and cert-manager v1.19.2

    shell
    #test cluster access:
    kubectl get nodes
    NAME                        STATUS   ROLES           AGE     VERSION
    certmanager-control-plane   Ready    control-plane   4m25s   v1.34.0
    
    # note: in the legacy video, we used kubectl to install cert-manager.
    #       this is now upgraded to use helm instead using the new OCI repo
    
    # install cert-manager 
    CHART_VERSION="v1.19.2"
    
    # checkout the values
    helm show values oci://quay.io/jetstack/charts/cert-manager > kubernetes/cert-manager/default-values.yaml
    
    helm install \
      cert-manager oci://quay.io/jetstack/charts/cert-manager \
      --version ${CHART_VERSION} \
      --namespace cert-manager \
      --create-namespace \
      --set crds.enabled=true \
      --set config.enableGatewayAPI=true

    Cert Manager Resources

    We can see our components deployed

    shell
    kubectl -n cert-manager get all
    NAME                                           READY   STATUS    RESTARTS   AGE
    pod/cert-manager-75bb65b7b9-qmjt5              1/1     Running   0          6m57s
    pod/cert-manager-cainjector-5cd89979d6-bs6m8   1/1     Running   0          6m57s
    pod/cert-manager-webhook-8fc5dcf5f-xn5pq       1/1     Running   0          6m57s
    
    NAME                              TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)            AGE
    service/cert-manager              ClusterIP   10.96.50.192    <none>        9402/TCP           6m57s
    service/cert-manager-cainjector   ClusterIP   10.96.144.242   <none>        9402/TCP           6m57s
    service/cert-manager-webhook      ClusterIP   10.96.223.22    <none>        443/TCP,9402/TCP   6m57s
    
    NAME                                      READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/cert-manager              1/1     1            1           6m57s
    deployment.apps/cert-manager-cainjector   1/1     1            1           6m57s
    deployment.apps/cert-manager-webhook      1/1     1           1           6m57s
    
    NAME                                                 DESIRED   CURRENT   READY   AGE
    replicaset.apps/cert-manager-75bb65b7b9              1         1         1       6m57s
    replicaset.apps/cert-manager-cainjector-5cd89979d6   1         1         1       6m57s
    replicaset.apps/cert-manager-webhook-8fc5dcf5f       1         1         1       6m57s

    Test Certificate Issuing

    Let's create some test certificates

    shell
    kubectl create ns cert-manager-test
    
    # create a self signed certificate
    kubectl apply -f kubernetes/cert-manager/selfsigned/issuer.yaml
    kubectl apply -f kubernetes/cert-manager/selfsigned/certificate.yaml
    
    # see progress
    kubectl describe certificate -n cert-manager-test
    kubectl get secrets -n cert-manager-test
    
    # cleanup
    kubectl delete ns cert-manager-test

    Configuration

    https://cert-manager.io/docs/configuration/

    Setup my DNS

    I can get the public IP address of my computer by running a simple command:

    text
    curl ifconfig.co

    I can log into my DNS provider and point my DNS A record to my IP.
    Also setup my router to allow 80 and 443 to come to my PC

    Note: If you are running in the cloud, your Ingress controller or Gateway API and Cloud provider will give you a
    public IP and you can point your DNS to that accordingly.

    Deploy pods that require SSL\TLS

    shell
    kubectl apply -f kubernetes/deployments/
    kubectl apply -f kubernetes/services/
    
    kubectl get pods

    Option 1: Using an Ingress Controller

    Let's deploy an Ingress controller:

    • Note: In the legacy vide, we used Kubernetes Ingress NGINX which is now deprecated. This guide is updated to use Traefik instead. You should be able to use any Ingress controller. *
    shell
    CHART_VERSION="37.3.0"
    helm repo add traefik https://helm.traefik.io/traefik
    helm repo update
    
    helm show values traefik/traefik > kubernetes/cert-manager/traefik-default-values.yaml
    
    helm install traefik traefik/traefik \
      --version $CHART_VERSION \
      --namespace traefik \
      --set service.type=NodePort \
      --set ports.web.exposedPort=30911 \
      --set ports.websecure.exposedPort=30787 \
      --create-namespace
    
    # check install 
    kubectl -n traefik get pods
    
    # get our service IP
    kubectl -n traefik get svc
    
    # deploy an ingress route that needs TLS for our example app
    kubectl apply -f kubernetes/cert-manager/ingress.yaml
    
    # curl over HTTP 
    curl http://test.marceldempers.dev

    Create an Ingress Let's Encrypt Issuer

    We create a ClusterIssuer for Ingress that allows us to issue certs in any namespace

    shell
    kubectl apply -f kubernetes/cert-manager/issuer-ingress.yaml
    
    # check the issuer
    kubectl describe clusterissuer letsencrypt-issuer

    Issue Certificate

    shell
    kubectl apply -f kubernetes/cert-manager/certificate.yaml
    
    # check the cert issue status
    kubectl describe certificate example-app
    
    # you can track and diagnose the ordering request process
    kubectl get CertificateRequest
    #note:  you can use kubectl describe on the resource too
    
    # TLS created as a secret
    kubectl get secrets
    NAME                  TYPE                                  DATA   AGE
    secret-tls       kubernetes.io/tls                     2      1m
    
    # test TLS 
    curl https://test.marceldempers.dev
    Hello World!
    
    # cleanup
    kubectl delete certificate example-app
    kubectl delete clusterissuer letsencrypt-issuer
    kubectl delete secret secret-tls

    Option 2: Using a Gateway API

    To use this option, we will need a Gateway API enabled cluster. This means you need the Gateway API CRD's installed.

    See our Introduction to Gateway API guide
    At the bottom of the guide, there is a matrix of many other Gateway APIs to choose from.

    • Note: cert-manager needs to have config.enableGatewayAPI=true enabled. *

    Create a Gateway Class

    shell
    kubectl apply -f kubernetes/gateway-api/traefik/01-gatewayclass.yaml

    Create a Gateway

    We can use our Gateway from our Traefik Gateway API Guide

    shell
    kubectl apply -f kubernetes/gateway-api/traefik/02-gateway.yaml

    Create a Gateway API Let's Encrypt Issuer

    We create a ClusterIssuer for Ingress that allows us to issue certs in any namespace

    shell
    kubectl apply -f kubernetes/cert-manager/issuer-gatewayapi.yaml
    
    # check the issuer
    kubectl describe clusterissuer letsencrypt-issuer

    Issue Certificate

    shell
    kubectl apply -f kubernetes/cert-manager/certificate.yaml
    
    # check the cert issue status
    kubectl describe certificate example-app
    
    # you can track and diagnose the ordering request process
    kubectl get CertificateRequest
    #note:  you can use kubectl describe on the resource too
    
    # TLS created as a secret
    kubectl get secrets
    NAME                  TYPE                                  DATA   AGE
    secret-tls       kubernetes.io/tls                     2      1m
    
    # test TLS 
    kubectl apply -f kubernetes/cert-manager/httproute.yaml
    curl https://test.marceldempers.dev
    Hello World!
    
    # cleanup
    kubectl delete certificate example-app
    kubectl delete clusterissuer letsencrypt-issuer
    kubectl delete secret secret-tls

    Overview (`kubernetes/cloud/readme.md`)

    Kubernetes in the Cloud

    Introduction to Kubernetes on Cloud Providers

    Microsoft Azure here
    Digital Ocean here
    Linode Cloud here
    Amazon Web Services here
    Google Cloud here


    Overview (`kubernetes/configmaps/README.md`)

    Introduction to Kubernetes: Configmaps

    Create a cluster with Kind

    text
    kind create cluster --name demo --image kindest/node:v1.33.1

    Checkout our cluster

    text
    kubectl get nodes
    NAME                 STATUS   ROLES           AGE   VERSION
    demo-control-plane   Ready    control-plane   19m   v1.33.1

    Deploy a ConfigMap

    text
    kubectl apply -f kubernetes/configmaps/configmap.yaml

    Deploy a pod that consumes the ConfigMap

    text
    kubectl apply -f kubernetes/configmaps/deployment.yaml

    Checking the deployment pods
    The kubectl describe command is essential for checking out kubernetes resources

    text
    kubectl describe deploy example-deploy

    We can checkout the config contents inside the pod's container with the kubectl exec command to get shell

    text
    kubectl exec -it <pod-name> -- bash

    When we make a change to the ConfigMap, we will notice that the configuration file changes and reflects inside the pod.
    This takes a number of seconds to a minute or so to propogate


    Overview (`kubernetes/crossplane/README.md`)

    Introduction to Crossplane

    Crossplane
    Crossplane Documentation

    We need a Kubernetes cluster

    Lets create a Kubernetes cluster to play with using kind

    text
    kind create cluster --name crossplane --image kindest/node:v1.33.0

    Installing Crossplane

    In this guide we will reference the official document steps in the links above.
    I've recorded the commands we follow in the video too

    text
    helm repo add crossplane-stable https://charts.crossplane.io/stable
    helm repo update
    
    helm search repo crossplane-stable --versions

    We'll install version 1.19.1 at the time of this guide

    text
    VERSION=1.19.1
    
    helm install crossplane \
    crossplane-stable/crossplane \
    --namespace crossplane-system \
    --version $VERSION \
    --create-namespace

    View our install:

    text
    kubectl get pods -n crossplane-system
    kubectl get deployments -n crossplane-system

    Once the pods are all running, we can see the api-versions

    text
    kubectl api-versions  | grep crossplane

    We can also see the new k8s objects that are installed with

    text
    kubectl api-resources | grep crossplane

    Providers

    Providers allow us to setup external providers that helps provision infrastructure for external services.

    For example, our crossplane cluster may have providers for deploying Azure, AWS, GCP or any other external infrastructure

    Furthermore, there is marketplace that hosts many providers, configurations and extensions for Crossplane called Upbound

    Install a Provider for a cloud provider Azure:

    text
    kubectl apply -f kubernetes/crossplane/provider-azure.yaml

    Check our provider:

    text
    kubectl get provider
    kubectl describe provider provider-family-azure

    Provider Configuration

    Once we have a provider setup, we can configure it using a ProviderConfig in Kubernetes
    An impotrant configuration is to tell the Crossplane Provider how to authenticate with its external service.

    For example, when using an Azure Provider, you need an Azure Service Principal, and for AWS you may need a service account with AWS account id and key.
    Each provider will have their own supported authentication methods.

    Create Provider credentials

    text
    SUBSCRIPTION_ID=<subscription-id>
    RESOURCE_GROUP=marcel-test
    
    az account set -s $SUBSCRIPTION_ID
    az group create -n $RESOURCE_GROUP -l australiaeast
    az ad sp create-for-rbac --sdk-auth  \
    -n marcel-test \
    --role Contributor \
    --scopes "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP" > azure-credentials.json

    Create Provider Kubernetes Secret

    text
    kubectl create secret \
    generic azure-secret \
    -n crossplane-system \
    --from-file=creds=./azure-credentials.json

    Deploy the Provider Configuration

    text
    kubectl apply -f kubernetes/crossplane/providerconfig-azure.yaml

    Create Provider Resources

    text
    kubectl apply -f kubernetes/crossplane/resources/azure/resource-vnet.yaml
    
    error: resource mapping not found for name: "marcel-test-vnet" namespace: "" from "kubernetes/crossplane/resources/azure/resource-vnet.yaml": no matches for kind "VirtualNetwork" in version "network.azure.upbound.io/v1beta1"
    ensure CRDs are installed first

    We see there is no CRD for Azure VNETs, that is because every type of resource in Azure is modularized into a separate provider, so we will need the networking provider first

    Install the Azure Network Provider:

    text
    kubectl apply -f kubernetes/crossplane/provider-azure-network.yaml

    Retry the resource creation:

    text
    kubectl apply -f kubernetes/crossplane/resources/azure/resource-vnet.yaml
    kubectl get virtualnetwork

    Deploy a Virtual Network Subnet

    text
    kubectl apply -f kubernetes/crossplane/resources/azure/resource-subnet.yaml
    kubectl get subnet

    Deploy a Virtual Network Card

    text
    kubectl apply -f kubernetes/crossplane/resources/azure/resource-networkcard.yaml
    kubectl get networkinterface

    Deploy a Virtual Machine

    Firstly need to add the compoute provider for Azure

    text
    kubectl apply -f kubernetes/crossplane/provider-azure-compute.yaml

    Deploy a Virtual Machine:

    text
    kubectl apply -f kubernetes/crossplane/resources/azure/resource-virtualmachine.yaml

    Cleanup Resources

    text
    kubectl delete linuxvirtualmachine marcel-test
    kubectl delete networkinterface marcel-test
    kubectl delete subnet marcel-test
    kubectl delete virtualnetwork marcel-test

    Overview (`kubernetes/daemonsets/README.md`)

    Kubernetes Daemonsets

    We need a Kubernetes cluster

    Lets create a Kubernetes cluster to play with using kind
    Because a Daemonset is all about running pods on every node, lets create a 3 node cluster:

    text
    cd kubernetes/daemonsets
    kind create cluster --name daemonsets --image kindest/node:v1.20.2 --config kind.yaml

    Test our cluster:

    text
    kubectl get nodes
    NAME                       STATUS     ROLES                  AGE   VERSION
    daemonsets-control-plane   Ready      control-plane,master   65s   v1.20.2
    daemonsets-worker          Ready      <none>                 31s   v1.20.2
    daemonsets-worker2         Ready      <none>                 31s   v1.20.2
    daemonsets-worker3         NotReady   <none>                 31s   v1.20.2

    Introduction

    Kubernetes provide documentation for what a Daemonset is with examples.

    Basic Daemonset

    Let's deploy a daemonset that runs a pod on each node and collects the name of the node

    text
    kubectl apply -f daemonset.yaml
    
    kubectl get pods -o wide
    NAME                      READY   STATUS    RESTARTS   AGE     IP           NODE                       NOMINATED NODE   READINESS GATES
    example-daemonset-8lcr5   1/1     Running   0          3m21s   10.244.2.4   daemonsets-worker          <none>           <none>
    example-daemonset-9jhgx   1/1     Running   0          81s     10.244.3.4   daemonsets-worker2         <none>           <none>
    example-daemonset-lvvsd   1/1     Running   0          2m41s   10.244.1.4   daemonsets-worker3         <none>           <none>
    example-daemonset-xxcv9   1/1     Running   0          119s    10.244.0.7   daemonsets-control-plane   <none>           <none>

    We can see the logs of any pod

    text
    kubectl logs <example-daemonset-xxxx>

    Cleanup:

    text
    kubectl delete ds example-daemonset

    Basic Daemonset: Exposing HTTP

    Let's deploy a daemonset that runs a pod on each node and exposes an HTTP endpoint on each node.
    In this demo we'll use a simple NGINX on port 80

    text
    kubectl apply -f daemonset-communication.yaml
    kubectl get pods

    Communicating with Daemonset Pods

    https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/#communicating-with-daemon-pods

    Let's deploy a pod that can talk to our daemonset

    text
    kubectl apply -f pod.yaml
    
    kubectl exec -it pod -- bash

    Service Type: ClusterIP or LoadBalancer

    Let's deploy a service of type ClusterIP

    text
    kubectl apply -f ./services/clusterip-service.yaml
    
    while true; do curl http://daemonset-svc-clusterip; sleep 1s; done
    Hello from daemonsets-worker2
    Hello from daemonsets-control-plane
    Hello from daemonsets-worker2
    Hello from daemonsets-worker3
    Hello from daemonsets-worker
    Hello from daemonsets-worker

    Node IP and Node Port

    We can add the nodePort field to the pods port section to expose a port on the node.

    Let's expose the node port in the pod spec:

    text
    ports:
    - containerPort: 80
      hostPort: 80
      name: "http"

    This means we can contact the daemonset pod using the Node IP and port:

    text
    # get the node ips
    kubectl get nodes -owide
    
    NAME                       STATUS   ROLES                  AGE    VERSION   INTERNAL-IP   EXTERNAL-IP       KERNEL-VERSION                CONTAINER-RUNTIME
    daemonsets-control-plane   Ready    control-plane,master   112m   v1.20.2   172.18.0.4    <none>
    daemonsets-worker          Ready    <none>                 111m   v1.20.2   172.18.0.3    <none>
    daemonsets-worker2         Ready    <none>                 111m   v1.20.2   172.18.0.2    <none>
    daemonsets-worker3         Ready    <none>                 111m   v1.20.2   172.18.0.6    <none>
    
    #example:
    
    bash-5.1# curl http://172.18.0.4:80
    Hello from daemonsets-control-plane
    bash-5.1# curl http://172.18.0.2:80
    Hello from daemonsets-worker2

    Service: Headless service

    Let's deploy a headless service where clusterIP: None

    text
    kubectl apply -f ./services/headless-service.yaml

    There are a few ways to discover our pods:

    1. Discover the DNS records via the "Headless" services
    text
    apk add --no-cache bind-tools
    dig daemonset-svc-headless.default.svc.cluster.local

    Notice it resolves to multiple DNS records for each pod:

    text
    ;; ANSWER SECTION:
    daemonset-svc-headless.default.svc.cluster.local. 30 IN A 10.244.0.5
    daemonset-svc-headless.default.svc.cluster.local. 30 IN A 10.244.2.2
    daemonset-svc-headless.default.svc.cluster.local. 30 IN A 10.244.3.2
    daemonset-svc-headless.default.svc.cluster.local. 30 IN A 10.244.1.2
    1. Discover pods endpoints by retrieving the endpoints for the headless service
    text
    kubectl describe endpoints daemonset-svc-headless

    Example:

    text
    Addresses:          10.244.0.5,10.244.1.2,10.244.2.2,10.244.3.2

    Get A records for each pod by using the following format:

    .<my-namespace>.pod.<cluster-domain.example>.

    text
    #examples:
    
    10-244-0-5.default.pod.cluster.local
    10-244-1-2.default.pod.cluster.local
    10-244-2-2.default.pod.cluster.local
    10-244-3-2.default.pod.cluster.local

    Communicate with the pods over DNS:

    text
    curl http://10-244-0-5.default.pod.cluster.local
    Hello from daemonsets-control-plane

    Real world Examples:

    Monitoring Nodes: Node-Exporter Daemonset

    We clone the official kube-prometheus repo to get monitoring manifests for Kubernetes.

    text
    git clone https://github.com/prometheus-operator/kube-prometheus.git

    Check the compatibility matrix here

    For this demo, we will use the compatible version tag 0.8

    text
    git checkout v0.8.0

    Deploy Prometheus Operator and CRDs

    text
    cd .\manifests\
    kubectl create -f .\setup\

    Deploy remaining resources including node exporter daemonset

    text
    kubectl create -f .
    
    # wait for pods to be up
    kubectl get pods -n monitoring
    
    #access prometheus in the browser
    kubectl -n monitoring port-forward svc/prometheus-k8s 9090

    See the Daemonset communications on the Prometheus targets page

    Checkout my monitoring guide for kubernetes for more in depth info

    Monitoring: Logging via Fluentd

    Take a look at my monitoring guide for Fluentd


    Overview (`kubernetes/datree/README.md`)

    Introduction to Datree

    Installation

    Best place to start is the documentation

    I like to start all my work inside a docker container.
    Let's run a small Alpine linux container

    text
    docker run -it -v ${PWD}:/work -v ${HOME}/.kube/:/root/.kube/ -w /work --net host alpine sh

    Install some dependancies

    Let's install curl and unzip because the installation script uses those.
    We will also install sudo since we are running in a container as root and install scripts have sudo commands in them.

    text
    apk add curl unzip bash sudo

    Automatic Installation

    We can install the latest version of Datree with the command advertised:

    text
    curl https://get.datree.io | /bin/bash

    Manual Installation

    Or we can grab a specific version of datree on the GitHub releases page.
    For example: 1.5.20 binary

    text
    curl -L https://github.com/datreeio/datree/releases/download/1.5.20/datree-cli_1.5.20_Linux_x86_64.zip -o /tmp/datree.zip
    
    unzip /tmp/datree.zip -d /tmp && \
    chmod +x /tmp/datree && \
    mv /tmp/datree /usr/local/bin/datree

    Now we can run the datree command:

    text
    datree
    Datree is a static code analysis tool for kubernetes files. Full code can be found at https://github.com/datreeio/datree
    
    Usage:
      datree [command]
    
    Available Commands:
      completion       Generate completion script for bash,zsh,fish,powershell
      config           Configuration management
      help             Help about any command
      kustomize        Render resources defined in a kustomization.yaml file and run a policy check against them
      publish          Publish policies configuration for given <fileName>.
      test             Execute static analysis for given <pattern>
      version          Print the version number
    
    Flags:
      -h, --help   help for datree
    
    Use "datree [command] --help" for more information about a command.

    Testing Kubernetes Manifests

    We have a number of Kubernetes manifests in this repo.
    Datree does a few things for us:

    • YAML validation ( Is this YAML well formatted ? )
    • Schema validation. ( Is this a Kubernetes YAML file ? For the right version ? )
    • Policy checks ( Checks YAML to ensure good practises are followed )

    Let's test my example manifests under our datree folder kubernetes\datree\example

    YAML validation

    If we break the YAML file format, we can detect that with the YAML validation feature

    text
    datree test ./kubernetes/datree/example/deployment.yaml

    Policy checks

    When we fix our YAML file, notice if we run datree test again, we get some policy checks failing

    text
    datree test ./kubernetes/datree/example/deployment.yaml

    Let's test some other types of Kubernetes objects

    text
    datree test ./kubernetes/services/service.yaml
    datree test ./kubernetes/configmaps/configmap.yaml
    datree test ./kubernetes/statefulsets/statefulset.yaml
    datree test ./kubernetes/ingress/ingress.yaml

    Schema validation

    Datree can also check if our YAML matches the target Kubernetes version schema.
    For example, our Ingress YAML is a newer version of Kubernetes

    text
    datree test --schema-version 1.14.0 ./kubernetes/ingress/ingress-nginx-example.yaml
    datree test --schema-version 1.19.0 ./kubernetes/ingress/ingress-nginx-example.yaml

    We can also test a directory of YAML files and include * wildcard in your scans.
    Let's test my latest Kubernetes tutorial that contains a Wordpress + MySQL + Ingress setup:

    text
    datree test kubernetes/tutorials/basics/yaml/*.y*ml

    Policies

    Now if we take a look at the CLI output of datree we notice a link in the Summary output.
    The URL is in the form of https://app.datree.io/login?t=<token>

    text
    (Summary)
    
    - Passing YAML validation: 4/4
    
    - Passing Kubernetes (1.20.0) schema validation: 4/4
    
    - Passing policy check: 2/4
    
    +-----------------------------------+------------------------------------------------------+
    | Enabled rules in policy "Default" | 21                                                   |
    | Configs tested against policy     | 5                                                    |
    | Total rules evaluated             | 84                                                   |
    | Total rules skipped               | 0                                                    |
    | Total rules failed                | 14                                                   |
    | Total rules passed                | 70                                                   |
    | See all rules in policy           | https://app.datree.io/login?t=xxxxxxxxxxxxxxxxxxxxxx |
    +-----------------------------------+------------------------------------------------------+

    We can use this URL to access the Datree UI to get a view of the policy management screens
    Checkout the link to access the UI which helps us manage our policies.

    Policy examples

    One of the key features about policies is that we can apply rule sets for specific environments.
    Perhaps you have a development environment where policies are a little loose and a staging server that has tighter restrictions to match production, or even a regulated environment that has very tight controls.

    We can use the Datree UI to create policies with different sets of rules.
    We can then tell datree about the policy we want it to test against:

    text
    datree test kubernetes/datree/example/deployment.yaml -p production

    For a new policy, we notice that 0 rules are enabled, so now we have the flexibility to set up the rules we want to protect this environment.

    Helm

    What if I don't use kubectl and use helm instead ?
    Let's install helm in our container

    text
    apk add tar git
    curl -L https://get.helm.sh/helm-v3.5.4-linux-amd64.tar.gz -o /tmp/helm.tar.gz && \
    tar -xzf /tmp/helm.tar.gz -C /tmp && \
    chmod +x /tmp/linux-amd64/helm && \
    mv /tmp/linux-amd64/helm /usr/local/bin/helm

    Let's install the helm plugin for datree

    text
    helm plugin install https://github.com/datreeio/helm-datree

    Now we can test a helm chart we have in our repo from my helm tutorial

    text
    cd kubernetes/helm
    
    helm datree test example-app \
    -- --values ./example-app/example-app-01.values.yaml

    Kustomize

    What if I don't use helm and use kustomize instead ?
    Datree has out the box built-in kustomize support
    Let's test our kustomize template from a video I did on kustomize

    text
    datree kustomize test .\kubernetes\kustomize\application\

    CI/CD examples

    We can even run datree in GitHub Actions and various CI/CD integrations.

    Admission Controller

    So far, datree helps us detect misconfigurations on our local machine as well as at our CI level.
    But what about the things that don't flow via our CI ?

    When folks deploy stuff directly to our clusters via kubectl or helm.
    Datree now allows us to not only detect but prevent misconfigurations being applied using a new admission controller feature.

    The admission controller is available here

    Create a Kubernetes cluster

    Let's start by creating a local kind cluster

    Note that we create a Kubernetes 1.23 cluster.
    So we want to use datree to validate and ensure our manifests comply with that version of Kubernetes.

    text
    kind create cluster --name datree --image kindest/node:v1.23.6

    Let's also grab kubectl:

    text
    curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.23.6/bin/linux/amd64/kubectl
    chmod +x ./kubectl
    mv ./kubectl /usr/local/bin/kubectl

    We'll need a datree token so our admission controller can read our policies

    text
    export DATREE_TOKEN=[your-token]

    Installation

    I will need some dependencies since I am running in a lightweight alpine container.
    OpenSSL is needed by the webhook install to generate certificates.

    text
    apk add openssl

    Let's grab the datree manifests

    text
    curl -L https://get.datree.io/admission-webhook -o datree.sh
    chmod +x datree.sh
    bash datree.sh

    With the admission controller now deployed, datree will validate things coming into the cluster.
    For example, if we bypass our CI/CD, datree will catch our deployment and run our policy checks

    I have a separate example deployment in our datree folder that we can play with:

    text
    kubectl apply -f kubernetes/datree/example/deployment.yaml

    Output:

    text
    /* Detailed source-code truncated for AI context efficiency. */

    Now to get this deployment fixed up, let's go ahead and comply to some of the policies
    Under the deployment.yaml I have included a livenessProbe as well as a readinessProbe
    Let's add those in.
    And finally we need to also add CPU and Memory requests and limit values.

    The last one is simple. We should avoid using the default namespace. So I will create an example namespace where I will keep all example apps.

    text
    kubectl create ns examples

    And finally we can deploy our resource, and specify a namespace:

    text
    kubectl apply -n examples -f kubernetes/datree/example/deployment.yaml
    deployment.apps/example-deploy created

    Kubectl

    But what about resources already in your cluster ?
    Datree covers this with their kubectl plugin.

    We can grab the install script right off the GitHub Release page.
    For this demo I'll grab the v0.11 version

    Installation:

    text
    curl -L https://github.com/datreeio/kubectl-datree/releases/download/v0.1.1/manual_install.sh -o /tmp/kubectl-plugin.sh
    chmod +x /tmp/kubectl-plugin.sh
    bash /tmp/kubectl-plugin.sh

    Now we have datree inside kubectl and can perform checks in our cluster.
    We can check our entire namespace now, which should be pretty clean:

    text
    kubectl datree test -- --namespace examples
    Fetching resources, this may take some time depending on the amount of resources in your cluster...
    
    (Summary)
    
    - Passing YAML validation: 1/1
    
    - Passing Kubernetes (1.24.2) schema validation: 1/1
    
    - Passing policy check: 1/1
    
    +-----------------------------------+------------------------------------------------------+
    | Enabled rules in policy "Default" | 21                                                   |
    | Configs tested against policy     | 1                                                    |
    | Total rules evaluated             | 21                                                   |
    | Total rules skipped               | 0                                                    |
    | Total rules failed                | 0                                                    |
    | Total rules passed                | 21                                                   |
    | See all rules in policy           | https://app.datree.io/login?t=xxxxxxxxxxxxxxxxxxxxxx |
    +-----------------------------------+------------------------------------------------------+
    
    The following cluster resources in namespace 'examples' were checked:
    
    deployment.apps/example-deploy

    Overview (`kubernetes/deployments/readme.md`)

    Introduction to Kubernetes: Deployments

    Build an example app:

    text
    #   Important!
    #   make sure you are at root of the repository
    #   in your terminal
    
    #   you can choose which app you want to build!
    
    
    #   aimvector/golang:1.0.0
    docker-compose build golang
    
    
    #   aimvector/csharp:1.0.0
    docker-compose build csharp
    
    #   aimvector/nodejs:1.0.0
    docker-compose build nodejs
    
    #   aimvector/python:1.0.0
    docker-compose build python

    Take a look at example deployment yaml


    Overview (`kubernetes/fluxcd/README.md`)

    Introduction to Flux CD v2

    Create a kubernetes cluster

    In this guide we we''ll need a Kubernetes cluster for testing. Let's create one using kind

    text
    kind create cluster --name fluxcd --image kindest/node:v1.26.3

    Run a container to work in

    run Alpine Linux:

    text
    docker run -it --rm -v ${HOME}:/root/ -v ${PWD}:/work -w /work --net host alpine sh

    install some tools

    text
    # install curl 
    apk add --no-cache curl
    
    # install kubectl 
    curl -sLO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
    chmod +x ./kubectl
    mv ./kubectl /usr/local/bin/kubectl

    test cluster access:

    text
    /work # kubectl get nodes
    NAME                    STATUS   ROLES    AGE   VERSION
    fluxcd-control-plane   Ready    control-plane   54s   v1.26.3

    Get the Flux CLI

    Let's download the flux command-line utility.
    We can get this utility from the GitHub Releases page

    It's also worth noting that you want to ensure you get a compatible version of flux which supports your version of Kubernetes. Checkout the prerequisites page.

    text
    curl -o /tmp/flux.tar.gz -sLO https://github.com/fluxcd/flux2/releases/download/v2.1.1/flux_2.1.1_linux_amd64.tar.gz
    tar -C /tmp/ -zxvf /tmp/flux.tar.gz
    mv /tmp/flux /usr/local/bin/flux
    chmod +x /usr/local/bin/flux

    Now we can run flux --help to see its installed

    Check our cluster

    text
    flux check --pre

    Documentation

    As with every guide, we start with the documentation
    The Core Concepts is a good place to start.

    We begin by following the steps under the bootstrap section for GitHub

    We'll need to generate a personal access token (PAT) that can create repositories by checking all permissions under repo.

    Once we have a token, we can set it:

    text
    export GITHUB_TOKEN=<your-token>

    Then we can bootstrap it using the GitHub bootstrap method

    text
    flux bootstrap github \
      --token-auth \
      --owner=marcel-dempers \
      --repository=docker-development-youtube-series \
      --path=kubernetes/fluxcd/repositories/infra-repo/clusters/dev-cluster \
      --personal \
      --branch fluxcd-2022
    
    flux check
    
    # flux manages itself using GitOps objects:
    kubectl -n flux-system get GitRepository
    kubectl -n flux-system get Kustomization

    Check the source code that flux bootstrap created

    text
    git pull origin <branch-name>

    Understanding GitOps Repository structures

    Generally, in GitOps you have a dedicated repo for infrastructure templates.
    Your infrastructure will "sync" from the this repo

    text
    developer    +-----------+     +----------+           
                  |           |     | CI       |           
      ----------> | REPO(code)|---> | PIPELINE |           
                  +-----------+     +----------+           
                                             |  commit     
                                             v             
               +----------+ sync    +----------+           
               |  INFRA   |-------> |INFRA     |           
               |  (k8s)   |         |REPO(yaml)|           
               +----------+         +----------+

    Flux repository structure documentation

    • Mono Repo (all k8s YAML in same "infra repo")
    • Repo per team
    • Repo per app

    Take note in this guide the folders under kubernetes/fluxcd/repositories represent different GIT repos

    text
    - repositories
      - infra-repo
      - example-app-1
      - example-app-2

    build our example apps

    Let's say we have a microservice called example-app-1 and it has its own GitHub repo somewhere.
    For demo, it's code is under kubernetes/fluxcd/repositories/example-app-1/

    text
    # go to our "git repo"
    cd kubernetes/fluxcd/repositories/example-app-1
    # check the files
    ls
    
    cd src
    docker build . -t example-app-1:0.0.1
    
    #load the image to our test cluster so we dont need to push to a registry
    kind load docker-image example-app-1:0.0.1 --name fluxcd

    setup our gitops pipeline

    Now we will also have a "infra-repo" GitHub repo where infrastructure configuration files for GitOps live.

    text
    cd kubernetes/fluxcd
    
    # tell flux where our Git repo is and where the YAML is
    # this is once off
    # flux will monitor the example-app-1 Git repo for when any infrastructure changes, it will sync
    kubectl -n default apply -f repositories/infra-repo/apps/example-app-1/gitrepository.yaml
    kubectl -n default apply -f repositories/infra-repo/apps/example-app-1/kustomization.yaml
    
    # check our flux resources 
    kubectl -n default describe gitrepository example-app-1
    kubectl -n default describe kustomization example-app-1
    
    # check deployed resources
    kubectl get all
    
    kubectl port-forward svc/example-app-1 80:80

    Now we have setup CD, let's take a look at CI

    changes to our example apps

    Once we make changes to our app.py we can build a new image with a new tag

    text
    docker build . -t example-app-1:0.0.2
    
    #load the image to our test cluster so we dont need to push to a registry
    kind load docker-image example-app-1:0.0.2 --name fluxcd 
    
    # update our kubernetes deployment YAML image tag
    # git commit with [skip ci] as the prefix of commit message & git push to branch!

    If we wait a minute or so we can kubectl port-forward svc/example-app-1 80:80 again and see the changes

    automate deploy by updating manifest

    So all we did to update our app is to build a new image, push it to our registry and update the image tag in our kubernetes deployment YAML file and flux will sync it.
    This is generally the role of CI, where flux concern is mainly CD.

    Here is an example on how to automate that

    automate deploy by image scanning

    text
    docker push                   
                                                                                
            developer    +-----------+     +----------+       +-------------+   
                         |           |     | CI       |       |IMAGE        |   
             ----------> | REPO(code)|---> | PIPELINE | ----->|REGISTRY     |   
                         +-----------+     +----------+       +-------------+   
                                                               ^                
                                                               |sync            
                                                               |                
                                       +----------+  commit   +----------+      
                                       |INFRA     | <-------- |  INFRA   |      
                                       |REPO(yaml)|           |  (k8s)   |      
                                       +----------+           +----------+

    An alternative method is to use your CI to build and push a newly tagged image to your registry (same as first option) and use Flux image scanner to trigger the rollout instead of automating a commit to your config repo.

    We firstly need to enable image scanning as its not enabled by default.
    To do this we just need to re-bootstrap flux with an addition flag

    text
    flux bootstrap github \
      --token-auth \
      --owner=marcel-dempers \
      --repository=docker-development-youtube-series \
      --path=kubernetes/fluxcd/repositories/infra-repo/clusters/dev-cluster \
      --components-extra=image-reflector-controller,image-automation-controller \
      --personal \
      --branch fluxcd-2022

    We need to create a image registry credential where we will push our image:

    text
    kubectl -n default create secret docker-registry dockerhub-credential --docker-username '' --docker-password '' --docker-email '[email protected]'

    build and push example-app-2

    text
    cd kubernetes\fluxcd\repositories\example-app-2\
    ls
    cd src
    ls
    docker build . -t aimvector/example-app-2:0.0.1
    docker push aimvector/example-app-2:0.0.1

    We will need to tell Flux how to manage our image deployment
    Note that this time our Kubernetes YAML is in the configs repo.
    This is because our application repo triggers it's CI which will build and push a new image to our cluster
    Flux will then detect the new image tag and update our Kubernetes YAML in our configs repo.
    If Flux pushed the update to our application repo, it will cause a CI/CD loop.

    add image policy and repository

    text
    kubectl -n default apply -f kubernetes/fluxcd/repositories/infra-repo/apps/example-app-2/gitrepository.yaml
    kubectl -n default apply -f kubernetes/fluxcd/repositories/infra-repo/apps/example-app-2/kustomization.yaml
    
    # see our application 
    kubectl get deploy
    kubectl get pods
    
    # tell flux about our image update policy
    kubectl -n default apply -f kubernetes/fluxcd/repositories/infra-repo/apps/example-app-2/imagerepository.yaml
    kubectl -n default apply -f kubernetes/fluxcd/repositories/infra-repo/apps/example-app-2/imagepolicy.yaml
    kubectl -n default apply -f kubernetes/fluxcd/repositories/infra-repo/apps/example-app-2/imageupdateautomation.yaml
    
    # we will also need to provide authentication for our git repo
    flux create secret git example-app-2-github --url https://github.com/marcel-dempers/docker-development-youtube-series --username '' --password '' --namespace default

    There are a number of ways to authenticate with GitRepositories

    text
    kubectl describe imagepolicy example-app-2
    kubectl describe  imagerepository example-app-2
    kubectl describe imageupdateautomation example-app-2

    Build and push our example-app-2

    text
    #make application changes and rebuild + push
    
    docker build . -t aimvector/example-app-2:0.0.2
    docker push aimvector/example-app-2:0.0.2
    
    
    #see changes new tags
    kubectl describe imagerepository
    
    #see image being updated
    kubectl describe imagepolicy example-app-2
    
    # see flux commiting back to the repo
    kubectl describe imageupdateautomation example-app-2

    Overview (`kubernetes/fluxcd/flux-v1-readme.md`)

    Flux Getting Started Guide (old v1)

    1 - Kubernetes

    Get a Kubernetes Cluster. In this video, I use Docker for Windows.
    If you are new to Kubernetes, checkout my videos here

    2 - Flux CTL

    I used Flux 1.18 which I got from the GitHub Release Page
    Rename it to fluxctl.exe & place it in a folder that is on your $env:Path environment variable.
    Open a new terminal and try

    text
    fluxctl

    4 - Installing Flux

    Make sure you are pointing to the kubernetes cluster you want to use

    text
    kubectl config current-context
    kubectl get nodes
    text
    kubectl create ns flux
    
    $GHUSER = "marcel-dempers"
    fluxctl install `
    --git-user=${GHUSER} `
    --git-email=${GHUSER}@users.noreply.github.com `
    [email protected]:${GHUSER}/docker-development-youtube-series `
    --git-path=kubernetes/configmaps,kubernetes/secrets,kubernetes/deployments `
    --git-branch=flux-test `
    --namespace=flux | kubectl apply -f -
    
    kubectl -n flux rollout status deployment/flux
    
    $env:FLUX_FORWARD_NAMESPACE = "flux"
    fluxctl list-workloads
    fluxctl identity
    
    
    https://github.com/marcel-dempers/docker-development-youtube-series/settings/keys/new
    
    fluxctl sync
    
      annotations:
        fluxcd.io/tag.example-app: semver:~1.0
        fluxcd.io/automated: 'true'
    
    fluxctl policy -w default:deployment/example-deploy --tag "example-app=1.0.*"

    Overview (`kubernetes/gateway-api/README.md`)

    Introduction to Gateway API

    In this video, we will dive into the features of Gateway API.
    Once you finish this video, click here to deep dive each of the Gateway API controllers.

    Documentation

    We need a Kubernetes cluster

    Lets create a Kubernetes cluster to play with using kind

    text
    kind create cluster --name gatewayapi --image kindest/node:v1.34.0

    Test our cluster and makes sure kubectl is configured for it:

    text
    kubectl get nodes
    NAME                       STATUS   ROLES           AGE   VERSION
    gatewayapi-control-plane   Ready    control-plane   40s   v1.34.0

    Gateway API CRDs

    The Kubernetes Gateway API is not installed on kubernetes by default. My guess is it may at some point.
    For now, we can grab it from the Gateway API SIGS Guide

    ***Important Note: *At the time of recording this guide, we'd like to look at as many features as possible, hence the experimental install is used.

    shell
    kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/standard-install.yaml

    This will install (Stable Channel):

    • Gateway Classes: kubectl get gatewayclass
    • Gateways: kubectl get gateway
    • HTTP Routes: kubectl get httproute

    These APIs are part of the Experimental Channel:

    • TLS Routes: kubectl get tlsroute
    • TCP Routes: kubectl get tcproute
    • UDP Routes: kubectl get udproute

    **Note: Gateway API is very new, and all of the above is subject to change quite rapidly **

    Setup some example applications

    The following will deploy a deployment, service and require configMap and secret for the applications to work.

    shell
    # deploy example apps
    kubectl apply -f python/deployment.yaml 
    kubectl apply -f golang/deployment.yaml
    kubectl apply -f kubernetes/gateway-api/web-app.yaml
    
    # check example apps
    kubectl get pods
    NAME                             READY   STATUS    RESTARTS
    go-deploy-b9c69978d-529qb        1/1     Running   0
    go-deploy-b9c69978d-jfb2b        1/1     Running   0
    python-deploy-54cbfc948b-5ch7w   1/1     Running   0
    python-deploy-54cbfc948b-qh22f   1/1     Running   0
    web-app-67fbb5d844-68wq4         1/1     Running   0
    
    kubectl get service
    NAME         TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)
    go-svc       ClusterIP   10.96.173.22   <none>        5000/TCP
    python-svc   ClusterIP   10.96.113.49   <none>        5000/TCP
    web-app      ClusterIP   10.96.44.18    <none>        80/TCP

    Create test Domains

    We also need to imagine we have a domain called example-app.com , so let's set that up on our hosts file

    log
    127.0.0.1  example-app.com
    127.0.0.1  example-app-go.com
    127.0.0.1  example-app-python.com

    Install a Gateway API controller

    To use the Gateway API features in Kubernetes, you need a controller that implements the above CRDs.
    In this introduction guide I will use an existing Gateway API controller called Traefik.

    Note: Keep in mind that this introduction has no dependency on Traefik specifically, therefore any controller that supports Gateway API can be used.
    At the bottom of this guide, I will provide guides on each of the popular Gateway API implementations.

    A Basic Gateway API controller install:

    shell
    CHART_VERSION="37.3.0" # traefik version v3.6.0
    helm repo add traefik https://helm.traefik.io/traefik
    helm repo update
    helm search repo traefik --versions
    
    
    helm install traefik traefik/traefik \
      --version $CHART_VERSION \
      --values kubernetes/gateway-api/values.yaml \
      --namespace traefik \
      --create-namespace

    As we don't have a LoadBalancer service in kind, let's port-forward so we can pretend we have one

    shell
    # check the pods
    kubectl -n traefik get pods 
    
    # check the logs 
    kubectl -n traefik logs -l app.kubernetes.io/instance=traefik-traefik
    
    # port forward for access
    kubectl -n traefik port-forward svc/traefik 80

    Install a Gateway Class

    To start enabling traffic to our newly created apps, we will start with installing a Gateway Class.

    Note that we use a Traefik Class in our example.

    Documentation

    GatewayClass is a cluster-scoped resource defined by the infrastructure provider. This resource represents a class of Gateways that can be instantiated.

    shell
    kubectl apply -f kubernetes/gateway-api/traefik/01-gatewayclass.yaml
    
    # check
    kubectl get gatewayclass
    
    # describe
    kubectl describe gatewayclass

    Install a Gateway

    Next we need to install a Gateway that implements our Gateway Class
    Note that we use a Traefik Gateway in our example.

    Documentation

    This gateway lives in the same namespace as the routes and applications

    shell
    kubectl apply -f kubernetes/gateway-api/traefik/02-gateway.yaml

    Traffic Management Features : HTTP Routes

    Documentation

    The important fields on HTTP Route we will cover:

    • parentRefs
    • sectionName
    • hostnames
    • rules
    • matches
    • filters

    Feature Table:

    Feature Example
    Route by Hostname example
    Route by Path example
    Route using URL Rewrite example
    Header Modification example
    HTTPS & TLS example

    For traffic management, we can take a look at some basic HTTP routes.

    Route by Hostname

    We can route by host.
    This will route all traffic that matches the Host header with the hostnames field:

    http://example-app-python.com/ 👉🏽 http://python-svc:5000
    http://example-app-go.com/ 👉🏽 http://go-svc:5000

    shell
    kubectl apply -f kubernetes/gateway-api/03-httproute-by-hostname.yaml

    Route by Path

    We can also route by host and path with different matching strategies.

    Exact:
    http://example-app-python.com/ 👉🏽 http://python-svc:5000/
    http://example-app-go.com/ 👉🏽 http://go-svc:5000/

    PathPrefix:
    http://example-app-python.com/* 👉🏽 http://python-svc:5000/*
    http://example-app-go.com/* 👉🏽 http://go-svc:5000/*

    shell
    kubectl apply -f kubernetes/gateway-api/04-httproute-by-path-exact.yaml

    Route using URL Rewrite

    We can rewrite the hostname or URL using URL rewrite.
    This way, we can combine our services under one domain and our controller can act as a true API gateway:

    http://example-app.com/api/python 👉🏽 http://python-svc:5000/
    http://example-app.com/api/go 👉🏽 http://go-svc:5000/
    As well as:
    http://example-app.com/api/go/status 👉🏽 http://go-svc:5000/status

    shell
    kubectl apply -f kubernetes/gateway-api/05-httproute-by-path-rewrite.yaml

    Request\Response Header Manipulation

    With Gateway API, you can modify request and response headers.
    This is possible with the ResponseHeaderModifier filter

    At the time of this recording, Gateway API does not natively support CORS.
    Even with it in the Experimental channel, many controllers do not support it yet.

    Let's do a basic CORS header modification for our Go HTTPRoute

    shell
    kubectl port-forward svc/web-app 8000:80

    The Header modification:

    shell
    kubectl apply -f kubernetes/gateway-api/06-httproute-header-modify.yaml

    HTTPS and TLS

    In my video, I generate a test TLS cert using mkcert

    shell
    curl -L https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64 -o mkcert && chmod +x mkcert && mv mkcert /usr/local/bin/
    
    #linux
    export CAROOT=${PWD}/kubernetes/gateway-api/tls
    #windows
    $env:CAROOT = "${PWD}" + "\kubernetes\gateway-api\tls"
    
    mkcert -key-file kubernetes/gateway-api/tls/key.pem -cert-file kubernetes/gateway-api/tls/cert.pem example-app.com
    
    mkcert -install

    Now that we have a TLS cert, we can create a Kubernetes secret to store it:

    text
    kubectl create secret tls secret-tls -n default --cert kubernetes/gateway-api/tls/cert.pem --key kubernetes/gateway-api/tls/key.pem

    We need to

    • Adjust our Gateway, to enable the TLS Listener first!
    • Then apply the TLS listener in our HTTP Route to enable TLS, using sectionName
    shell
    kubectl apply -f kubernetes/gateway-api/07-httproute-tls.yaml

    Let's port-forward to 443 since that is where TLS is exposed:

    shell
    kubectl -n traefik port-forward svc/traefik 443

    Result:
    https://example-app.com/api/go 👉🏽 http://go-svc:5000/

    Checkout More Official Guides on the Kubernetes Gateway API SIGs page.

    Infrastructure Labels

    A useful feature is the ability to customize infrastructure under the hood for Gateways.
    For example, cloud load balancers etc.

    We can use Infrastructure Labels to do so. This will set annotations or labels on any infrastructure that gets created.

    Gateway API Controller Guides

    Name Guide Video
    Traefik Introduction to Traefik Gateway API
    Envoy Introduction to Envoy Gateway API
    Istio Introduction to Istio Gateway API
    NGINX Fabric Introduction to NGINX Fabric Gateway API
    Cilium Introduction to Cilium Gateway API
    kgateway Introduction to kgateway (a.k.a Gloo Gateway)
    HAProxy Introduction to HAProxy & Gateway API
    Linkerd Introduction to Linkerd Gateway API

    Overview (`kubernetes/helm/README.md`)

    Introduction to Helm

    We need a Kubernetes cluster

    Lets create a Kubernetes cluster to play with using kind

    text
    kind create cluster --name helm --image kindest/node:v1.31.1

    Getting Started with Helm

    Firstly, I like to do most of my work in containers so everything is reproducible
    and my machine remains clean.

    Get a container to work in

    Run a small alpine linux container where we can install and play with helm:

    text
    docker run -it --rm -v ${HOME}:/root/ -v ${PWD}:/work -w /work --net host alpine sh
    
    # install curl & kubectl
    apk add --no-cache curl nano
    curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
    chmod +x ./kubectl
    mv ./kubectl /usr/local/bin/kubectl
    export KUBE_EDITOR="nano"
    
    # test cluster access:
    /work # kubectl get nodes
    NAME                    STATUS   ROLES    AGE   VERSION
    helm-control-plane   Ready    control-plane   26m   v1.31.1

    Install Helm CLI

    text
    curl -LO https://get.helm.sh/helm-v3.17.2-linux-amd64.tar.gz
    tar -C /tmp/ -zxvf helm-v3.17.2-linux-amd64.tar.gz
    rm helm-v3.17.2-linux-amd64.tar.gz
    mv /tmp/linux-amd64/helm /usr/local/bin/helm
    chmod +x /usr/local/bin/helm

    Create our first Chart

    For reference in the rest of the guide, I have left my full templates in:
    <GitRepo>/kubernetes/helm/example-app

    text
    cd kubernetes/helm
    
    mkdir temp && cd temp
    helm create example-app

    Cleanup the template

    We can delete unwanted files:

    • delete everything under /templates, keeping only _helpers.tpl
    • delete tests folder under templates

    Add Kubernetes files to our new Chart

    Copy the following files into our example-app/templates/ folder

    • <GitRepo>/kubernetes/deployments/deployment.yaml
    • <GitRepo>/kubernetes/services/service.yaml
    • <GitRepo>/kubernetes/configmaps/configmap.yaml
    • <GitRepo>/kubernetes/secrets/secret.yaml

    Test the rendering of our template

    text
    helm template example-app example-app

    Install our app using our Chart

    text
    helm install example-app example-app
    
    # list our releases
    
    helm list
    
    # see our deployed components
    
    kubectl get all
    kubectl get cm
    kubectl get secret

    Value injections for our Chart

    For CI systems, we may want to inject an image tag as a build number

    Basic parameter injection:

    text
    # values.yaml
    
    deployment:
      image: "aimvector/python"
      tag: "1.0.4"
    
    # deployment.yaml
    
    image: {{ .Values.deployment.image }}:{{ .Values.deployment.tag }}
    
    # upgrade our release
    
    helm upgrade example-app example-app --values ./example-app/values.yaml
    
    # see revision increased
    
    helm list

    Make our Chart more generic

    Let's make our chart generic so it can be reused:
    For the following objects, replace example-deploy and example-app to inject: "{{ .Values.name }}"

    • deployment.yaml
    • services.yaml
    • secret.yaml
    • configmap.yaml

    Now that our application is generic
    We can deploy another copy of it.

    Rename values.yaml to example-app.values.yaml
    Create our second app values file example-app-02.values.yaml

    text
    helm install example-app-02 example-app --values ./example-app/example-app-02.values.yaml

    Trigger deployment change when config changes

    By default, a deployment will not rollout new pods when a configmap changes.
    Some application read configuration at start up and deployment may need to roll out
    new pods when a configmap changes. Let's do that:

    text
    # deployment.yaml
    
    kind: Deployment
    spec:
      template:
        metadata:
          annotations:
            checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
    
    # rollout the change
    
    helm upgrade example-app example-app --values ./example-app/example-app-01.values.yaml

    If\Else and Default values

    You can also set default values in case they are not supplied by the values.yaml file.
    This may help you keep the values.yaml file small

    text
    {{- if .Values.deployment.resources }}
      resources:
        {{- if .Values.deployment.resources.requests }}
        requests:
          memory: {{ .Values.deployment.resources.requests.memory | default "50Mi" | quote }}
          cpu: {{ .Values.deployment.resources.requests.cpu | default "10m" | quote }}
        {{- else}}
        requests:
          memory: "50Mi"
          cpu: "10m"
        {{- end}}
        {{- if .Values.deployment.resources.limits }}
        limits:
          memory: {{ .Values.deployment.resources.limits.memory | default "1024Mi" | quote }}
          cpu: {{ .Values.deployment.resources.limits.cpu | default "1" | quote }}
        {{- else}}  
        limits:
          memory: "1024Mi"
          cpu: "1"
        {{- end }}
      {{- else }}
      resources:
        requests:
          memory: "50Mi"
          cpu: "10m"
        limits:
          memory: "1024Mi"
          cpu: "1"
      {{- end}} 
    
    
    # rollout the change
    
    helm upgrade example-app example-app --values ./example-app/example-app-01.values.yaml

    Overview (`kubernetes/kubectl/README.md`)

    Introduction to KUBECTL

    To start off this tutorial, we will be using kind to create our test cluster.
    You can use minikube or any Kubernetes cluster.

    Kind is an amazing tool for running test clusters locally as it runs in a container which makes it lightweight and easy to run throw-away clusters for testing purposes.

    Download KUBECTL

    We can download kubectl from the Official Docs

    Create a kubernetes cluster

    In this guide we will run two clusters side by side so we can demonstrate cluster access.
    Create two clusters:

    text
    kind create cluster --name dev --image kindest/node:v1.23.5
    kind create cluster --name prod --image kindest/node:v1.23.5

    See cluster up and running:

    text
    kubectl get nodes
    NAME                  STATUS   ROLES                  AGE     VERSION
    prod-control-plane   Ready    control-plane,master   2m12s   v1.23.5

    Understanding the KUBECONFIG

    Default location of the kubeconfig file is in /.kube/config

    text
    kind: Config
    apiVersion: v1
    clusters:
     - list of clusters (addresses \ endpoints) 
    users:
     - list of users (thing that identifies us when accessing a cluster [certificate]) 
    contexts:
     - list of contexts ( which user and cluster to use when running commands)

    Commands to interact with kubeconfig are kubectl config.
    Key commands are telling kubectl which context to use

    text
    kubectl config current-context
    kubectl config get-contexts
    kubectl config use-context <name>

    You can also tell your kubectl to use different config files.
    This is useful to keep your production config separate from your development ones

    Set the $KUBECONFIG environment variable to a path:

    text
    #linux
    export KUBECONFIG=<path>
    
    #windows 
    $ENV:KUBECONFIG="C:\Users\aimve\.kube\config"

    We can export seperate configs using kind
    This is possible with cloud based clusters as well:

    text
    kind --name dev export kubeconfig --kubeconfig C:\Users\aimve\.kube\dev-config 
    
    kind --name prod export kubeconfig --kubeconfig C:\Users\aimve\.kube\prod-config 
    
    #switch to prod
    $ENV:KUBECONFIG="C:\Users\aimve\.kube\prod-config"
    kubectl get nodes

    Working with Kubernetes resources

    Now that we have cluster access, next we can read resources from the cluster
    with the kubectl get command.

    Namespaces

    Most kubernetes resources are namespace scoped:

    text
    kubectl get namespaces

    By default, kubectl commands will run against the default namespace

    List resources in a namespace

    text
    kubectl get <resource>
    
    kubectl get pods
    kubectl get deployments
    kubectl get services
    kubectl get configmaps
    kubectl get secrets
    kubectl get ingress

    Create resources in a namespace

    We can create a namespace with the kubectl create command:

    text
    kubectl create ns example-apps

    Let's create a couple of resources:

    text
    kubectl -n example-apps create deployment webserver --image=nginx --port=80
    kubectl -n example-apps get deploy
    kubectl -n example-apps get pods
    
    kubectl -n example-apps create service clusterip webserver --tcp 80:80
    kubectl -n example-apps get service
    kubectl -n example-apps port-forward svc/webserver 80
    # we can access http://localhost/
    
    kubectl -n example-apps create configmap webserver-config --from-file config.json=./kubernetes/kubectl/config.json
    kubectl -n example-apps get cm
    
    kubectl -n example-apps create secret generic webserver-secret --from-file secret.json=./kubernetes/kubectl/secret.json
    kubectl -n example-apps get secret

    Working with YAML

    As you can see we can create resources with kubectl but this is only for basic testing purposes.
    Kubernetes is a declarative platform, meaning we should provide it what to create instead
    of running imperative line-by-line commands.

    We can also get the YAML of pre-existing objects in our cluster with the -o yaml flag on the get command

    Let's output all our YAML to a yaml folder:

    text
    kubectl -n example-apps get cm webserver-config -o yaml > .\kubernetes\kubectl\yaml\config.yaml
    kubectl -n example-apps get secret webserver-secret -o yaml > .\kubernetes\kubectl\yaml\secret.yaml
    kubectl -n example-apps get deploy webserver -o yaml > .\kubernetes\kubectl\yaml\deployment.yaml
    kubectl -n example-apps get svc webserver -o yaml > .\kubernetes\kubectl\yaml\service.yaml

    Create resources from YAML files

    The most common and recommended way to create resources in Kubernetes is with the kubectl apply command.
    This command takes in declarative YAML files.

    To show you how powerful it is, instead of creating things line-by-line, we can deploy all our infrastructure
    with a single command.

    Let's deploy a Wordpress CMS site, with a back end MySQL database.
    This is a snippet taken from my How to learn Kubernetes video:

    text
    kubectl create ns wordpress-site
    kubectl -n wordpress-site apply -f ./kubernetes/tutorials/basics/yaml/

    We can checkout our site with the port-forward command:

    text
    kubectl -n wordpress-site get svc
    
    NAME        TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)    AGE
    mysql       ClusterIP   10.96.146.75   <none>        3306/TCP   17s
    wordpress   ClusterIP   10.96.157.6    <none>        80/TCP     17s
    
    kubectl -n wordpress-site port-forward svc/wordpress 80

    Clean up

    text
    kind delete cluster --name dev
    kind delete cluster --name prod

    Overview (`kubernetes/kustomize/readme.md`)

    The Basics

    text
    kubectl apply -f kubernetes/kustomize/application/namespace.yaml
    kubectl apply -f kubernetes/kustomize/application/configmap.yaml
    kubectl apply -f kubernetes/kustomize/application/deployment.yaml
    kubectl apply -f kubernetes/kustomize/application/service.yaml
    
    # OR 
    
    kubectl apply -f kubernetes/kustomize/application/
    
    kubectl delete ns example

    Kustomize

    Build

    text
    kubectl kustomize .\kubernetes\kustomize\ | kubectl apply -f -
    # OR
    kubectl apply -k .\kubernetes\kustomize\
    
    kubectl delete ns example

    Overlays

    text
    kubectl kustomize .\kubernetes\kustomize\environments\production | kubectl apply -f -
    # OR
    kubectl apply -k .\kubernetes\kustomize\environments\production
    
    kubectl delete ns example

    Overview (`kubernetes/persistentvolume/readme.md`)

    Persistent Volumes Demo

    Container Storage

    By default containers store their data on the file system like any other process.
    Container file system is temporary and not persistent during container restarts
    When container is recreated, so is the file system

    text
    # run postgres
    docker run -d --rm -e POSTGRES_DB=postgresdb -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin123 postgres:10.4
    
    # enter the container 
    docker exec -it <container-id> bash
    
    # login to postgres
    psql --username=admin postgresdb
    
    #create a table
    CREATE TABLE COMPANY(
       ID INT PRIMARY KEY     NOT NULL,
       NAME           TEXT    NOT NULL,
       AGE            INT     NOT NULL,
       ADDRESS        CHAR(50),
       SALARY         REAL
    );
    
    #show table
    \dt
    
    # quit 
    \q

    Restarting the above container and going back in you will notice \dt commands returning no tables.
    Since data is lost.

    Same can be demonstrated using Kubernetes

    text
    cd .\kubernetes\persistentvolume\
    
    kubectl create ns postgres
    kubectl apply -n postgres -f ./postgres-no-pv.yaml
    kubectl -n postgres get pods 
    kubectl -n postgres exec -it postgres-0 bash
    
    # run the same above mentioned commands to create and list the database table
    
    kubectl delete po -n postgres postgres-0
    
    # exec back in and confirm table does not exist.

    Persist data Docker

    text
    docker volume create postges
    docker run -d --rm -v postges:/var/lib/postgresql/data -e POSTGRES_DB=postgresdb -e POSTGRES_USER=admin -e POSTGRES_PASSWORD=admin123 postgres:10.4
    
    # run the same tests as above and notice

    Persist data Kubernetes

    text
    kubectl apply -f persistentvolume.yaml
    kubectl apply -n postgres -f persistentvolumeclaim.yaml
    
    kubectl apply -n postgres -f postgres-with-pv.yaml
    
    kubectl -n postgres get pods

    Overview (`kubernetes/portainer/README.md`)

    Introduction to Portainer

    Start here 👉🏽https://www.portainer.io/
    Documentation 👉🏽https://docs.portainer.io/

    Portainer installation

    In this demo, I will be running Kubernetes 1.22 using kind
    Which is compatible with portainer 2.11.1

    Let's go ahead with a local docker install:

    text
    cd kubernetes\portainer
    mkdir volume-ce 
    
    docker run -d -p 9443:9443 -p 8000:8000 --name portainer-ce `
    --restart=always `
    -v /var/run/docker.sock:/var/run/docker.sock `
    -v ${PWD}/volume-ce:/data `
    portainer/portainer-ce:2.11.1

    SSL & DOMAIN

    We can also upload SSL certificates for our portainer.
    In this demo, portainer will issue self signed certificates.
    We will need a domain for our portainer server so our clusters can contact it.
    Let's use nip.io to create a public endpoint for portainer.

    Create Kubernetes Cluster

    Let's start by creating a local kind cluster

    For local clusters, we can use the public endpoint Agent.
    We can get a public endpoint for the portainer agent by:

    • Ingress
    • LoadBalancer
    • NodePort

    So we'll deploy portainer agent with NodePort for local

    For production environments, I would recommend not to expose the portainer agent.
    In this case, for Production, we'll use the portainer edge agent.

    To get NodePort exposed in kind, we'll open a host port with a kind.yaml config

    text
    kind create cluster --name local --config kind.yaml

    Manage Kubernetes Environments

    The portainer UI gives us a one line command to deploy the portainer agent.
    Note that in the video, we pick the node port option.

    Local: Portainer Agent

    I download the YAML from here to take a closer look at what it is deploying

    Deploy the portainer agent in my kind cluster:

    text
    kubectl apply -f portainer-agent-ce211-k8s-nodeport.yaml

    See the agent:

    text
    kubectl -n portainer get pods

    See the service with the endpoint it exposes:

    text
    kubectl -n portainer get svc

    Now since we dont have a public load balancer and using nodeport, our service will be exposed on the node IP.
    Since the Kubernetes node is our local machine, we should be able to access the portainer agent on <computer-IP>:30778

    We can obtain our local IP with ipconfig
    The IP and NodePort will be used to connect our portainer server to the new agent.

    Production: Portainer Edge Agent

    For the Edge agent, we get the command in the portainer UI.
    Once deployed, we can see the egde agent in our AKS cluster:

    text
    kubectl -n portainer get pods

    Helm

    Let's showcase how to deploy helm charts.
    Most folks would have helm charts for their ingress controllers, monitoring, logging and other
    platform dependencies.

    Let's add Kubernetes NGINX Ingress repo:

    text
    https://kubernetes.github.io/ingress-nginx

    GitOps

    So from the Application menu, we can add an application from a git repository.
    Let's add this repo:

    text
    https://github.com/marcel-dempers/docker-development-youtube-series

    We also specify all our manifests path that portainer needs to deploy:

    • kubernetes/portainer/example-application/deployment.yaml
    • kubernetes/portainer/example-application/configmap.yaml
    • kubernetes/portainer/example-application/service.yaml
    • kubernetes/portainer/example-application/ingress.yaml

    Portainer will now poll our repo and deploy any updates, GitOps style!

    Oauth Setup example | Business edition

    Here are the values I used for the Oauth settings:

    Field Value
    Client ID xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
    Client secret xxxxxxxxxxxxxxxxxxxx
    Authorization URL https://login.microsoftonline.com/``/oauth2/authorize
    Access token URL https://login.microsoftonline.com/``/oauth2/token
    Resource URL https://login.microsoftonline.com/``/openid/userinfo
    Redirect URL https://localhost:9443/
    Logout URL https://login.microsoftonline.com/``/oauth2/logout
    User identifier unique_name
    Scopes openid profile

    Overview (`kubernetes/probes/README.md`)

    Introduction to Kubernetes Probes

    Create a kubernetes cluster

    In this guide we we''ll need a Kubernetes cluster for testing. Let's create one using kind

    text
    cd kubernetes/probes
    kind create cluster --name demo --image kindest/node:v1.28.0

    Test the cluster:

    text
    kubectl get nodes
    NAME                 STATUS   ROLES           AGE   VERSION
    demo-control-plane   Ready    control-plane   59s   v1.28.0

    Applications

    Client app is used to act as a client that sends web requests :

    text
    kubectl apply -f client.yaml

    The server app is the app that will receive web requests:

    text
    kubectl apply -f server.yaml

    Test making web requests constantly:

    text
    while true; do curl http://server; sleep 1s; done

    Bump the server version label up and apply to force a new deployment
    Notice the client throws an error, so traffic is interupted, not good!

    This is because our new pod during deployment is not ready to take traffic!

    Readiness Probes

    Let's add a readiness probe that tells Kubernetes when we are ready:

    text
    readinessProbe:
      httpGet:
        path: /
        port: 5000
      initialDelaySeconds: 3
      periodSeconds: 3
      failureThreshold: 3

    Automatic failover with Readiness probes

    Let's pretend our application starts hanging and not longer returns responses
    This is common with some web servers and may need to be manually restarted

    text
    kubectl exec -it podname -- sh -c "rm /data.txt"

    Now we will notice our client app starts getting errors.
    Few things to notice:

    • Our readiness probe detected an issue and removed traffic from the faulty pod.
    • We should be running more than one application so we would be highly available
    text
    kubectl scale deploy server --replicas 2
    • Notice traffic comes back as its routed to the healthy pod

    Fix our old pod: kubectl exec -it podname -- sh -c "echo 'ok' > /data.txt"

    • If we do this again with 2 pods, notice we still get an interuption but our app automaticall stabalises after some time
    • This is because readinessProbe has failureThreshold and some failure will be expected before recovery
    • Do not set this failureThreshold too low as you may remove traffic frequently. Tune accordingly!

    Readiness probes help us automatically remove traffic when there are intermittent network issues

    Liveness Probes

    Liveness probe helps us when we cannot automatically recover.
    Let's use the same mechanism to create a vaulty pod:

    text
    kubectl exec -it podname -- sh -c "rm /data.txt"

    Our readiness probe has saved us from traffic issues.
    But we want the pod to recover automatically, so let's create livenessProbe:

    text
    livenessProbe:
      httpGet:
        path: /
        port: 5000
      initialDelaySeconds: 3
      periodSeconds: 4
      failureThreshold: 8

    Scale back up: kubectl scale deploy server --replicas 2
    Create a vaulty pod: kubectl exec -it podname -- sh -c "rm /data.txt"

    If we observe we will notice the readinessProbe saves our traffic, and livenessProbe will eventually replace the bad pod

    Startup Probes

    The startup probe is for slow starting applications
    It's important to understand difference between start up and readiness probes.
    In our examples here, readiness probe acts as a startup probe too, since our app is fairly slow starting!
    This difference is explained in the video.


    Overview (`kubernetes/rancher/README.md`)

    Introduction to Rancher: On-prem Kubernetes

    This guide follows the general instructions of running a manual rancher install and running our own infrastructure on Hyper-v

    Hyper-V : Prepare our infrastructure

    In this demo, we will use Hyper-V to create our infrastructure.
    For on-premise, many companies use either Hyper-V, VMWare Vsphere and other technologies to create virtual infrastructure on bare metal.

    Few points to note here:

    • Benefit of Virtual infrastructure is that it's immutable
      a) We can add and throw away virtual machines at will.
      b) This makes maintenance easier as we can roll updated virtual machines instead of
      patching existing machines and turning them to long-living snowflakes.
      c) Reduce lifespan of machines

    • Bare Metal provides the compute.
      a) We don't want Kubernetes directly on bare metal as we want machines to be immutable.
      b) This goes back to the previous point on immutability.

    • Every virtual machine needs to be able to reach each other on the network
      a) This is a kubernetes networking requirements that all nodes can communicate with one another

    Hyper-V : Create our network

    In order for us to create virtual machines all on the same network, I am going to create a virtual switch in Hyper-v
    Open Powershell in administrator

    text
    # get our network adapter where all virtual machines will run on
    # grab the name we want to use
    Get-NetAdapter
    
    Import-Module Hyper-V
    $ethernet = Get-NetAdapter -Name "Ethernet 2"
    New-VMSwitch -Name "virtual-network" -NetAdapterName $ethernet.Name -AllowManagementOS $true -Notes "shared virtual network interface"

    Hyper-V : Create our machines

    We firstly need harddrives for every VM.
    Let's create three:

    text
    mkdir c:\temp\vms\linux-0\
    mkdir c:\temp\vms\linux-1\
    mkdir c:\temp\vms\linux-2\
    
    New-VHD -Path c:\temp\vms\linux-0\linux-0.vhdx -SizeBytes 20GB
    New-VHD -Path c:\temp\vms\linux-1\linux-1.vhdx -SizeBytes 20GB
    New-VHD -Path c:\temp\vms\linux-2\linux-2.vhdx -SizeBytes 20GB
    text
    New-VM `
    -Name "linux-0" `
    -Generation 1 `
    -MemoryStartupBytes 2048MB `
    -SwitchName "virtual-network" `
    -VHDPath "c:\temp\vms\linux-0\linux-0.vhdx" `
    -Path "c:\temp\vms\linux-0\"
    
    New-VM `
    -Name "linux-1" `
    -Generation 1 `
    -MemoryStartupBytes 2048MB `
    -SwitchName "virtual-network" `
    -VHDPath "c:\temp\vms\linux-1\linux-1.vhdx" `
    -Path "c:\temp\vms\linux-1\"
    
    New-VM `
    -Name "linux-2" `
    -Generation 1 `
    -MemoryStartupBytes 2048MB `
    -SwitchName "virtual-network" `
    -VHDPath "c:\temp\vms\linux-2\linux-2.vhdx" `
    -Path "c:\temp\vms\linux-2\"

    Setup a DVD drive that holds the iso file for Ubuntu Server

    text
    Set-VMDvdDrive -VMName "linux-0" -ControllerNumber 1 -Path "C:\temp\ubuntu-20.04.3-live-server-amd64.iso"
    Set-VMDvdDrive -VMName "linux-1" -ControllerNumber 1 -Path "C:\temp\ubuntu-20.04.3-live-server-amd64.iso"
    Set-VMDvdDrive -VMName "linux-2" -ControllerNumber 1 -Path "C:\temp\ubuntu-20.04.3-live-server-amd64.iso"

    Start our VM's

    text
    Start-VM -Name "linux-0"
    Start-VM -Name "linux-1"
    Start-VM -Name "linux-2"

    Now we can open up Hyper-v Manager and see our infrastructure.
    In this video we'll connect to each server, and run through the initial ubuntu setup.
    Once finished, select the option to reboot and once it starts, you will notice an unmount error on CD-Rom
    This is ok, just shut down the server and start it up again.

    Hyper-V : Setup SSH for our machines

    Now in this demo, because I need to copy rancher bootstrap commands to each VM, it would be easier to do so
    using SSH. So let's connect to each VM in Hyper-V and setup SSH.
    This is because copy+paste does not work without Enhanced Session mode in Ubuntu Server.

    Let's temporarily turn on SSH on each server:

    text
    sudo apt update
    sudo apt install -y nano net-tools openssh-server
    sudo systemctl enable ssh
    sudo ufw allow ssh
    sudo systemctl start ssh

    Record the IP address of each VM so we can SSH to it:

    text
    sudo ifconfig
    # record eth0
    linux-0 IP=192.168.0.16
    linux-1 IP=192.168.0.17
    linux-2 IP=192.168.0.18

    In new Powershell windows, let's SSH to our VMs

    Setup Docker

    It is required that every machine that needs to join our cluster, has docker running on it.
    Firstly, rancher will use docker to run it's agent as well as bootstrap the cluster.

    Install docker on each VM:

    text
    curl -sSL https://get.docker.com/ | sh
    sudo usermod -aG docker $(whoami)
    sudo service docker start

    Running Rancher in Docker

    So Rancher can be deployed almost anywhere.
    We can run it in Kubernetes on-prem or the cloud.

    Now because we want Rancher to manage kubernetes clusters, we dont want it running in the clusters we are managing.
    So I would like to keep my Rancher server outside and separate from my Kubernetes clusters.

    So let's setup a single server with docker

    Persist data

    We will want to persist ranchers data across reboots.
    Rancher stores its data under /var/lib/rancher

    In this repo, let's create a space to persist data:

    text
    cd kubernetes/rancher
    mkdir volume

    Run Rancher

    text
    docker run -d --name rancher-server  -v ${PWD}/volume:/var/lib/rancher --restart=unless-stopped -p 80:80 -p 443:443 --privileged rancher/rancher

    Unlock Rancher

    Once its up and running we can extract the Rancher initial boostrap password from the logs

    text
    docker logs rancher-server > rancher.log

    Get Rancher IP

    It's important that our servers can reach the Rancher server.
    As all the VMs and my machine are on the same network, we can use my machine IP as the server IP so the VM's can reach it.
    let's grab the IP:

    text
    ipconfig

    We can now access Rancher on localhost

    Deploy Sample Workloads

    To deploy some sample basic workloads, let's get the kubeconfig for our cluster

    Set kubeconfig:

    text
    $ENV:KUBECONFIG="<path-to-kubeconfig>"

    Deploy 2 pods, and a service:

    text
    kubectl create ns marcel
    kubectl -n marcel apply -f .\kubernetes\configmaps\configmap.yaml
    kubectl -n marcel apply -f .\kubernetes\secrets\secret.yaml
    kubectl -n marcel apply -f .\kubernetes\deployments\deployment.yaml
    kubectl -n marcel apply -f .\kubernetes\services\service.yaml

    One caveat is because we are not a cloud provider, Kubernetes does not support our service type=LoadBalancer.
    For that, we need something like metallb.
    However - we can port-forward

    text
    kubectl -n marcel get svc 
    NAME              TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
    example-service   LoadBalancer   10.43.235.240   <pending>     80:31310/TCP   13s
    
    kubectl -n marcel port-forward svc/example-service 81:80

    We can access our example-app on port 81


    Overview (`kubernetes/rbac/README.md`)

    Introduction to Kubernetes: RBAC

    Create Kubernetes cluster

    text
    kind create cluster --name rbac --image kindest/node:v1.20.2

    Kubernetes CA Certificate

    Kubernetes does not have a concept of users, instead it relies on certificates and would only
    trust certificates signed by its own CA.

    To get the CA certificates for our cluster, easiest way is to access the master node.
    Because we run on kind, our master node is a docker container.
    The CA certificates exists in the /etc/kubernetes/pki folder by default.
    If you are using minikube you may find it under ~/.minikube/.

    Access the master node:

    text
    docker exec -it rbac-control-plane bash
    
    ls -l /etc/kubernetes/pki
    total 60
    -rw-r--r-- 1 root root 1135 Sep 10 01:38 apiserver-etcd-client.crt
    -rw------- 1 root root 1675 Sep 10 01:38 apiserver-etcd-client.key
    -rw-r--r-- 1 root root 1143 Sep 10 01:38 apiserver-kubelet-client.crt
    -rw------- 1 root root 1679 Sep 10 01:38 apiserver-kubelet-client.key
    -rw-r--r-- 1 root root 1306 Sep 10 01:38 apiserver.crt
    -rw------- 1 root root 1675 Sep 10 01:38 apiserver.key
    -rw-r--r-- 1 root root 1066 Sep 10 01:38 ca.crt
    -rw------- 1 root root 1675 Sep 10 01:38 ca.key
    drwxr-xr-x 2 root root 4096 Sep 10 01:38 etcd
    -rw-r--r-- 1 root root 1078 Sep 10 01:38 front-proxy-ca.crt
    -rw------- 1 root root 1679 Sep 10 01:38 front-proxy-ca.key
    -rw-r--r-- 1 root root 1103 Sep 10 01:38 front-proxy-client.crt
    -rw------- 1 root root 1675 Sep 10 01:38 front-proxy-client.key
    -rw------- 1 root root 1679 Sep 10 01:38 sa.key
    -rw------- 1 root root  451 Sep 10 01:38 sa.pub
    
    exit the container

    Copy the certs out of our master node:

    text
    cd kubernetes/rbac
    docker cp rbac-control-plane:/etc/kubernetes/pki/ca.crt ca.crt
    docker cp rbac-control-plane:/etc/kubernetes/pki/ca.key ca.key

    Kubernetes Users

    As mentioned before, Kubernetes has no concept of users, it trusts certificates that is signed by its CA.
    This allows a lot of flexibility as Kubernetes lets you bring your own auth mechanisms, such as OpenID Connect or OAuth.

    This allows managed Kubernetes offerings to use their cloud logins to authenticate.

    So on Azure, I can use my Microsoft account, GKE my Google account and AWS EKS my Amazon account.

    You will need to consult your cloud provider to setup authentication.
    Example Azure AKS

    User Certificates

    First thing we need to do is create a certificate signed by our Kubernetes CA.
    We have the CA, let's make a certificate.

    Easy way to create a cert is use openssl and the easiest way to get openssl is to simply run a container:

    text
    docker run -it -v ${PWD}:/work -w /work -v ${HOME}:/root/ --net host alpine sh
    
    apk add openssl

    Let's create a certificate for Bob Smith:

    text
    #start with a private key
    openssl genrsa -out bob.key 2048

    Now we have a key, we need a certificate signing request (CSR).
    We also need to specify the groups that Bob belongs to.
    Let's pretend Bob is part of the Shopping team and will be developing
    applications for the Shopping

    text
    openssl req -new -key bob.key -out bob.csr -subj "/CN=Bob Smith/O=Shopping"

    Use the CA to generate our certificate by signing our CSR.
    We may set an expiry on our certificate as well

    text
    openssl x509 -req -in bob.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out bob.crt -days 1

    Building a kube config

    Let's install kubectl in our container to make things easier:

    text
    apk add curl nano
    curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
    chmod +x ./kubectl
    mv ./kubectl /usr/local/bin/kubectl

    We'll be trying to avoid messing with our current kubernetes config.
    So lets tell kubectl to look at a new config that does not yet exists

    text
    export KUBECONFIG=~/.kube/new-config

    Create a cluster entry which points to the cluster and contains the details of the CA certificate:

    text
    kubectl config set-cluster dev-cluster --server=https://127.0.0.1:52807 \
    --certificate-authority=ca.crt \
    --embed-certs=true
    
    #see changes 
    nano ~/.kube/new-config

    kubectl config set-credentials bob --client-certificate=bob.crt --client-key=bob.key --embed-certs=true

    kubectl config set-context dev --cluster=dev-cluster --namespace=shopping --user=bob

    kubectl config use-context dev

    kubectl get pods
    Error from server (Forbidden): pods is forbidden: User "Bob Smith" cannot list resource "pods" in API group "" in the namespace "shopping"

    Give Bob Smith Access

    text
    cd kubernetes/rbac
    kubectl create ns shopping
    
    kubectl -n shopping apply -f .\role.yaml
    kubectl -n shopping apply -f .\rolebinding.yaml

    Test Access as Bob

    kubectl get pods
    No resources found in shopping namespace.

    Kubernetes Service Accounts

    So we've covered users, but what about applications or services running in our cluster ?
    Most business apps will not need to connect to the kubernetes API unless you are building something that integrates with your cluster, like a CI/CD tool, an autoscaler or a custom webhook.

    Generally applications will use a service account to connect.
    You can read more about Kubernetes Service Accounts.

    Let's deploy a service account

    text
    kubectl -n shopping apply -f serviceaccount.yaml

    Now we can deploy a pod that uses the service account

    text
    kubectl -n shopping apply -f pod.yaml

    Now we can test the access from within that pod by trying to list pods:

    text
    kubectl -n shopping exec -it shopping-api -- bash
    
    # Point to the internal API server hostname
    APISERVER=https://kubernetes.default.svc
    
    # Path to ServiceAccount token
    SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
    
    # Read this Pod's namespace
    NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace)
    
    # Read the ServiceAccount bearer token
    TOKEN=$(cat ${SERVICEACCOUNT}/token)
    
    # Reference the internal certificate authority (CA)
    CACERT=${SERVICEACCOUNT}/ca.crt
    
    # List pods through the API
    curl --cacert ${CACERT} --header "Authorization: Bearer $TOKEN" -s ${APISERVER}/api/v1/namespaces/shopping/pods/ 
    
    # we should see an error not having access

    Now we can allow this pod to list pods in the shopping namespace

    text
    kubectl -n shopping apply -f serviceaccount-role.yaml
    kubectl -n shopping apply -f serviceaccount-rolebinding.yaml

    If we try run curl command again we can see now we are able to get a json
    response with pod information


    Overview (`kubernetes/reloader/README.md`)

    Introduction to K8s Reloader

    In this demo we will walk through a project called reloader

    Create a cluster with Kind

    text
    kind create cluster --name demo --image kindest/node:v1.33.1

    Checkout our cluster

    text
    kubectl get nodes
    NAME                 STATUS   ROLES           AGE   VERSION
    demo-control-plane   Ready    control-plane   19m   v1.33.1

    For this demo, we'll want to deploy a ConfigMap and/or a Secret and consume these using a Deployment

    Deploy a ConfigMap

    text
    kubectl apply -f kubernetes/configmaps/configmap.yaml

    Deploy a Secret

    text
    kubectl apply -f kubernetes/secrets/secret.yaml

    Install Reloader

    text
    helm repo add stakater https://stakater.github.io/stakater-charts
    helm repo update
    
    # see versions
    helm search repo stakater/reloader -l
    
    #pick a version
    CHART_VERSION="2.1.5"
    
    
    helm install reloader stakater/reloader \
    --create-namespace \
    --namespace reloader \
    --version $CHART_VERSION \
    --values kubernetes/reloader/values.yaml

    Usage

    Deploy an application that consumes the 'ConfigMap' and 'Secret' with auto reload enabled:

    text
    kubectl apply -f kubernetes/reloader/deployment.yaml

    We can now update the ConfigMap object and observe our example-app pods restarting when the ConfigMap changes.
    We can also run kubectl logs -l app=example-app in a separate terminal and observe the changing values inside pods.


    Overview (`kubernetes/secrets/README.md`)

    Introduction to Kubernetes: Secrets

    Create a cluster with Kind

    text
    kind create cluster --name secrets --image kindest/node:v1.31.1

    Our Secret

    We have a secret under kubernetes/secrets/secret.json

    text
    cat kubernetes/secrets/secret.json

    Using our secret in a container

    As a file:

    text
    docker run -it -v $PWD/kubernetes/secrets/secret.json:/secrets/secret.json ubuntu:latest bash
    
    cat /secrets/secret.json

    As environment variables:

    text
    api_key="somesecretgoeshere"
    docker run -it -e API_KEY=$api_key ubuntu:latest bash
    
    echo $API_KEY

    Kubernetes Secret

    Read more about Kubernetes secrets

    Create our secret

    There are two main ways we can create a Kubernetes secret.
    Either by creating the secret object with kubectl create secret or apply\create it declaratively using YAML with kubectl apply -f

    kubectl create secret:

    text
    kubectl create secret generic mysecret --from-file kubernetes/secrets/secret.json

    kubectl apply -f or kubectl create -f allows us to define things declaratively using YAML files:

    text
    kubectl apply -f kubernetes/secrets/secret.yaml

    Use our secret

    In order to use our secret we add a volume to our pod spec and then mount that using a volumeMount
    We can also use a secret references as env variable

    text
    kubectl apply -f kubernetes/secrets/pod.yaml

    Overview (`kubernetes/servicemesh/readme.md`)

    Introduction to Service Mesh

    To understand service mesh, we need a good use case.
    We need some service-to-service communication.
    A basic microservice architecture will do.

    Read Me : The Introduction Guide

    Video :point_down:

    Service Mesh Guides

    Introduction to Linkerd

    Getting started with Linkerd

    Read Me: readme

    Video :point_down:

    Introduction to Istio

    Getting started with Istio

    Read Me: readme

    Video :point_down:


    Overview (`kubernetes/servicemonitors/README.md`)

    Introduction to Service Monitors

    In order to understand service monitors, we will need to understand how to monitor
    kubernetes environment.
    You will need a base understanding of Kubernetes and have a basic understanding of the kube-prometheus monitoring stack.

    Checkout the video How to monitor Kubernetes in 2022:

    Create a kubernetes cluster

    text
    # create cluster
    kind create cluster --name monitoring --image kindest/node:v1.23.5
    
    # see cluster up and running
    kubectl get nodes
    NAME                  STATUS   ROLES                  AGE     VERSION
    monitoring-control-plane   Ready    control-plane,master   2m12s   v1.23.5

    Deploy kube-prometheus

    Installation:

    text
    kubectl create -f ./monitoring/prometheus/kubernetes/1.23/manifests/setup/
    kubectl create -f ./monitoring/prometheus/kubernetes/1.23/manifests/

    Check the install:

    text
    kubectl -n monitoring get pods

    After a few minutes, everything should be up and running:

    text
    kubectl -n monitoring get pods
    NAME                                   READY   STATUS    RESTARTS   AGE
    alertmanager-main-0                    2/2     Running   0          3m10s
    alertmanager-main-1                    2/2     Running   0          3m10s
    alertmanager-main-2                    2/2     Running   0          3m10s
    blackbox-exporter-6b79c4588b-t4czf     3/3     Running   0          4m7s
    grafana-7fd69887fb-zm2d2               1/1     Running   0          4m7s
    kube-state-metrics-55f67795cd-f7frb    3/3     Running   0          4m6s
    node-exporter-xjdtn                    2/2     Running   0          4m6s
    prometheus-adapter-85664b6b74-bvmnj    1/1     Running   0          4m6s
    prometheus-adapter-85664b6b74-mcgbz    1/1     Running   0          4m6s
    prometheus-k8s-0                       2/2     Running   0          3m9s
    prometheus-k8s-1                       2/2     Running   0          3m9s
    prometheus-operator-6dc9f66cb7-z98nj   2/2     Running   0          4m6s

    View dashboards

    text
    kubectl -n monitoring port-forward svc/grafana 3000

    Then access Grafana on localhost:3000

    Access Prometheus

    text
    kubectl -n monitoring port-forward svc/prometheus-operated 9090

    Then access Prometheus on localhost:9090.

    Create our own Prometheus

    text
    kubectl apply -n monitoring -f ./kubernetes/servicemonitors/prometheus.yaml

    View our prometheus prometheus-applications-0 instance:

    text
    kubectl -n monitoring get pods

    Checkout our prometheus UI

    text
    kubectl -n monitoring port-forward prometheus-applications-0 9090

    Deploy a service monitor for example app

    text
    kubectl -n default apply -f ./kubernetes/servicemonitors/servicemonitor.yaml

    After applying the service monitor, if Prometheus is correctly selecting it, we should see the item appear under the Service Discovery page in Prometheus.
    Double check with with port-forward before proceeding.
    If it does not appear, that means your Prometheus instance is not selecting the service monitor accordingly. Either a label mismatch on the namespace or the service monitor.

    Deploy our example app

    text
    kubectl -n default apply -f ./kubernetes/servicemonitors/example-app/

    Now we should see a target in the Prometheus Targets page.


    Overview (`kubernetes/services/README.md`)

    Introduction to Kubernetes: Services

    Create a Kubernetes cluster

    Firstly, we will need a Kubernetes cluster and will create one using kind

    text
    kind create cluster --name services --image kindest/node:v1.31.1

    Test the cluster

    text
    kubectl get nodes
    NAME                     STATUS   ROLES           AGE     VERSION
    services-control-plane   Ready    control-plane   5m48s   v1.31.1

    Deploy a few pods

    Let's deploy a few pods using a Kubernetes Deployment. To understand services, we need to deploy some pods that become our "upstream" or "endpoint" that we want to access.

    text
    kubectl apply -f kubernetes/deployments/deployment.yaml

    Deploy a service

    We can now deploy a Kubernetes service that targets our deployment:

    text
    kubectl apply -f kubernetes/services/service.yaml

    Overview (`kubernetes/shipa/README.md`)

    Introduction to Shipa

    We need a Kubernetes cluster

    To get the most out of Shipa, I'll be using real Cloud Provider Kubernetes as well as a local
    minikube cluster.

    Minikube

    I will start with a local minikube cluster to get Shipa running:

    text
    # start up a cluster
    
    minikube start --kubernetes-version='v1.18.2' --memory='5gb' --disk-size='20gb' --driver=hyperv
    
    # check our cluster
    
    kubectl get nodes
    NAME       STATUS   ROLES    AGE   VERSION
    minikube   Ready    master   45s   v1.18.2

    Minikube Traffic Routes

    text
    # add a route for accessing Shipa API
    route add 10.100.10.10/32 MASK 255.255.255.255  $(minikube ip)
    
    # add a route for accessing our Applications
    route add 10.100.10.20/32 MASK 255.255.255.255  $(minikube ip)

    Getting Started with Shipa

    Install Dependencies

    text
    docker run -it --rm  -v ${PWD}:/work -w /work alpine sh
    apk add --no-cache curl unzip
    
    cd kubernetes/shipa/
    mkdir installs && cd installs

    Install Kubectl

    text
    curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.19.4/bin/windows/amd64/kubectl.exe

    Install Helm CLI

    text
    curl -LO https://get.helm.sh/helm-v3.4.1-windows-amd64.zip && \
    unzip helm-v3.4.1-windows-amd64.zip && \
    mv windows-amd64/helm.exe . && \
    rm -rf windows-amd64 && \
    rm helm-v3.4.1-windows-amd64.zip

    Install Shipa CLI

    text
    curl -LO  https://storage.googleapis.com/shipa-client/v1.1/shipa_windows_amd64.exe
    mv shipa_windows_amd64.exe shipa.exe

    Add all the CLI to our $PATH

    We can now add the CLI executables to a folder somewhere on our machine
    Then we add it to our $PATH

    • installs/helm.exe
    • installs/kubectl.exe
    • installs/shipa.exe

    Download Shipa Helm Chart

    Official Docs here
    We can find all the releases of Shipa here
    In this demo, I will use version 1.1.1

    Let's download Shipa:

    text
    curl  -L -s -o shipa1.1.1.zip https://github.com/shipa-corp/helm-chart/archive/v1.1.1.zip && \
    unzip shipa1.1.1.zip && rm shipa1.1.1.zip && \
    mv helm-chart-1.1.1 shipa-helm-chart-1.1.1 && \
    
    # we can abandon this container
    exit

    Install Shipa

    Let's add Shipa to our minikube cluster:

    text
    /* Detailed source-code truncated for AI context efficiency. */

    Targets

    text
    shipa target-add dev 10.100.10.10
    shipa target-list
    shipa target-set dev
    shipa login
    
    #username: [email protected]
    #password: shipa2020

    Pools

    https://learn.shipa.io/docs/pool-management

    text
    shipa pool-add dev --public --kube-namespace dev-team --provisioner kubernetes
    shipa pool-list

    Teams

    text
    shipa team-create dev-team
    shipa pool-constraint-set theonepool team dev-team --append

    Clusters

    text
    shipa cluster-list
    +------------+-------------+---------------+-------------+---------+------------+-------+-------+
    | Name       | Provisioner | Addresses     | Custom Data | Default | Pools      | Teams | Error |
    +------------+-------------+---------------+-------------+---------+------------+-------+-------+
    | shipa-core | kubernetes  | 10.96.0.1:443 |             | false   | theonepool |       |       |
    +------------+-------------+---------------+-------------+---------+------------+-------+-------+

    Deploy Applications to Shipa (minikube)

    text
    cd kubernetes\shipa\developers\docker\golang
    
    docker build . -t aimvector/shipa-golang:v1
    docker push aimvector/shipa-golang:v1
    
    # create an app: Golang
    shipa app-create go-helloworld static -t dev-team -o theonepool
    
    # deploy the app: Golang
    shipa app-deploy -i aimvector/shipa-golang:v1 -a go-helloworld
    shipa app-list
    
    cd .\kubernetes\shipa\developers\docker\python\
    
    docker build . -t aimvector/shipa-python:v1
    docker push aimvector/shipa-python:v1
    
    # create an app: Python
    shipa app-create python-helloworld static -t dev-team -o theonepool
    shipa env set FLASK_APP=/app/server.py -a python-helloworld
    
    # deploy the app: Python
    shipa app-deploy -i aimvector/shipa-python:v1 -a python-helloworld
    shipa app-list

    Kubernetes in the Cloud

    To create a Kubernetes cluster, you can follow my guides on each Cloud provider below:

    Cloud Kubernetes Source Video
    Azure AKS Source Code
    Amazon EKS Source Code
    Google GKE Source Code
    Digital Ocean DO Source Code
    Linode LKE Source Code

    Deploy Shipa to Azure AKS

    Let's add Shipa to each one of our clusters running in the Cloud.
    In the video I will deploy Shipa to an Azure AKS cluster:

    text
    # ensure we are pointing to AKS Or the
    kubectl get nodes
    
    # deploy Shipa to AKS
    
    cd kubernetes/shipa
    
    # create username + password override
    cat > ops/values.override.yaml << EOF
    auth:
      adminUser: [email protected]
      adminPassword: shipa-prod-aks2020
    EOF
    
    kubectl create ns shipa-system
    
    kubectl apply -n shipa-system -f limits.yaml
    
    helm install shipa ./installs/shipa-helm-chart-1.1.1 --timeout=1000s --namespace=shipa-system -f ops/values.override.yaml
    
    #wait until the dashboard pod is up!
    kubectl --namespace=shipa-system get pods
    
    # get the ingress IP for the Shipa API
    kubectl --namespace=shipa-system get svc shipa-ingress-nginx  -o jsonpath="{.status.loadBalancer.ingress[0].ip}"
    
    # add the ingress IP as our Shipa target for AKS dev
    shipa target-add dev-aks 20.53.128.244 -s
    shipa target-set dev-aks
    shipa target-list
    
    # lets login, view the node, and dashboard app URL
    shipa login [email protected]
    shipa node-list
    shipa app-list
    
    #add a host file entry for the dashboard, I.E:
    20.193.20.106 dashboard.20.193.20.106.shipa.cloud
    
    # create a team for developers
    shipa team-create dev-team
    
    #allow dev-team to deploy to AKS pool
    shipa pool-constraint-set theonepool team dev-team --append

    MultiCloud - Add Shipa to Amazon EKS

    text
    # add service account to EKS cluster to allow Shipa to connect
    kubectl apply -f ops/shipa-admin-service-account.yaml 
    
    #add a pool allow people to deploy to Amazon
    shipa pool-add amazon --public --kube-namespace dev-team --provisioner kubernetes
    
    # allow dev-team to deploy to Amazon
    shipa pool-constraint-set amazon  team dev-team --append
    
    # We will need the EKS token of the service account to allow connection
    kubectl -n kube-system describe secret $(kubectl -n kube-system get secret | grep shipa-admin | awk '{print $1}')
    
    # We will also need the CA cert of Kubernetes
    kubectl get secret $(kubectl get secret | grep default-token | awk '{print $1}') -o jsonpath='{.data.ca\.crt}' | base64 -d

    On the dashboard, we add the cluster using the above information.
    We can obtain the EKS address in the Amazon console.

    Deploying Apps to MultiCloud

    text
    # create an app: Golang
    shipa app-create go-helloworld static -t dev-team -o theonepool
    
    # deploy the app: Golang
    shipa app-deploy -i aimvector/shipa-golang:v1 -a go-helloworld
    shipa app-list
    
    # create an app: Python
    shipa app-create python-helloworld static -t dev-team -o amazon
    shipa env set FLASK_APP=/app/server.py -a python-helloworld
    
    # deploy the app: Python
    shipa app-deploy -i aimvector/shipa-python:v1 -a python-helloworld
    shipa app-list
    text
    https://collabnix.com/the-rise-of-shipa-a-continuous-operation-platform/
    
    1. Configured default user:
    
    Username: [email protected]
    Password: adminadmin123
    
    2. If this is a production cluster, please configure persistent volumes.
       The default reclaimPolicy for dynamically provisioned persistent volumes is "Delete" and
       users are advised to change it for production
    
       The code snippet below can be used to set reclaimPolicy to "Retain" for all volumes:
    
    PVCs=$(kubectl --namespace=shipa-system get pvc -l release=shipa -o name)
    
    for pvc in $PVCs; do
        volumeName=$(kubectl -n shipa-system get $pvc -o template --template=\{\{.spec.volumeName\}\})
        kubectl -n shipa-system patch pv $volumeName -p '{"spec":{"persistentVolumeReclaimPolicy":"Retain"}}'
    done
    
    3. Set default target for shipa-client:
    export SHIPA_HOST=$(kubectl --namespace=shipa-system get svc shipa-ingress-nginx  -o jsonpath="{.status.loadBalancer.ingress[0].ip}")
    
    shipa target-add shipa $SHIPA_HOST -s
    
    shipa login [email protected]
    shipa node-list
    shipa app-list

    Overview (`kubernetes/velero/README.md`)

    Introduction to Velero

    We need a Kubernetes cluster

    Lets create a Kubernetes cluster to play with using kind

    text
    kind create cluster --name velero --image kindest/node:v1.19.1

    Get a container to work in

    Run a small alpine linux container where we can install and play with velero:

    text
    docker run -it --rm -v ${HOME}:/root/ -v ${PWD}:/work -w /work --net host alpine sh
    
    # install curl & kubectl
    apk add --no-cache curl nano
    curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
    chmod +x ./kubectl
    mv ./kubectl /usr/local/bin/kubectl
    export KUBE_EDITOR="nano"
    
    #test cluster access:
    /work # kubectl get nodes
    NAME                    STATUS   ROLES    AGE   VERSION
    velero-control-plane   Ready    master   26m   v1.18.4

    Velero CLI

    Lets download the velero command line tool
    I grabbed the v1.5.1 release using curl

    You can go to the releases page to get it

    text
    curl -L -o /tmp/velero.tar.gz https://github.com/vmware-tanzu/velero/releases/download/v1.5.1/velero-v1.5.1-linux-amd64.tar.gz 
    tar -C /tmp -xvf /tmp/velero.tar.gz
    mv /tmp/velero-v1.5.1-linux-amd64/velero /usr/local/bin/velero
    chmod +x /usr/local/bin/velero
    
    velero --help

    Deploy some stuff

    text
    kubectl apply -f kubernetes/configmaps/configmap.yaml
    kubectl apply -f kubernetes/secrets/secret.yaml
    kubectl apply -f kubernetes/deployments/deployment.yaml
    kubectl apply -f kubernetes/services/service.yaml
    
    kubectl get all

    Create storage in Azure and AWS

    In this example, we'll create a storage in AWS and Azure to try both scenarios.
    You can follow along in the video

    Create a storage account and secret for: Azure
    Create a storage account and secret for: AWS

    text

    Deploy Velero for Azure

    Start here

    text
    # Azure credential file
    cat << EOF  > /tmp/credentials-velero
    AZURE_STORAGE_ACCOUNT_ACCESS_KEY=${AZURE_STORAGE_ACCOUNT_ACCESS_KEY}
    AZURE_CLOUD_NAME=AzurePublicCloud
    EOF
    
    velero install \
        --provider azure \
        --plugins velero/velero-plugin-for-microsoft-azure:v1.1.0 \
        --bucket $BLOB_CONTAINER \
        --secret-file /tmp/credentials-velero \
        --backup-location-config resourceGroup=$AZURE_BACKUP_RESOURCE_GROUP,storageAccount=$AZURE_STORAGE_ACCOUNT_NAME,storageAccountKeyEnvVar=AZURE_STORAGE_ACCOUNT_ACCESS_KEY,subscriptionId=$AZURE_BACKUP_SUBSCRIPTION_ID \
        --use-volume-snapshots=false
    
    
    kubectl -n velero get pods
    kubectl logs deployment/velero -n velero

    Deploy Velero for AWS

    Start here

    text
    cat > /tmp/credentials-velero <<EOF
    [default]
    aws_access_key_id=$AWS_ACCESS_ID
    aws_secret_access_key=$AWS_ACCESS_KEY
    EOF
    
    velero install \
        --provider aws \
        --plugins velero/velero-plugin-for-aws:v1.1.0 \
        --bucket $BUCKET \
        --backup-location-config region=$REGION \
        --snapshot-location-config region=$REGION \
        --secret-file /tmp/credentials-velero
    
    kubectl -n velero get pods
    kubectl logs deployment/velero -n velero

    Create a Backup

    text
    velero backup create default-namespace-backup --include-namespaces default
    
    # describe
    velero backup describe default-namespace-backup
    
    # logs
    velero backup logs default-namespace-backup

    Do a Restore

    text
    # delete all resources
    
    kubectl delete -f kubernetes/configmaps/configmap.yaml
    kubectl delete -f kubernetes/secrets/secret.yaml
    kubectl delete -f kubernetes/deployments/deployment.yaml
    kubectl delete -f kubernetes/services/service.yaml
    
    velero restore create default-namespace-backup --from-backup default-namespace-backup
    
    # describe
    velero restore describe default-namespace-backup
    
    #logs 
    velero restore logs default-namespace-backup
    
    # see items restored
    
    kubectl get all

    Overview (`messaging/kafka/README.md`)

    Introduction to Kafka

    Official Docs

    Building a Docker file

    As always, we start with a dockerfile
    We can build our dockerfile

    text
    cd .\messaging\kafka\
    docker build . -t aimvector/kafka:2.7.0

    Exploring the Kafka Install

    We can then run it to explore the contents:

    text
    docker run --rm --name kafka -it aimvector/kafka:2.7.0 bash
    
    ls -l /kafka/bin/
    cat /kafka/config/server.properties

    We can use the docker cp command to copy the file out of our container:

    text
    docker cp kafka:/kafka/config/server.properties ./server.properties
    docker cp kafka:/kafka/config/zookeeper.properties ./zookeeper.properties

    Note: We'll need the Kafka configuration to tune our server and Kafka also requires
    at least one Zookeeper instance in order to function. To achieve high availability, we'll run
    multiple kafka as well as multiple zookeeper instances in the future

    Zookeeper

    Let's build a Zookeeper image. The Apache folks have made it easy to start a Zookeeper instance the same way as the Kafka instance by simply running the start-zookeeper.sh script.

    text
    cd ./zookeeper
    docker build . -t aimvector/zookeeper:2.7.0
    cd ..

    Let's create a kafka network and run 1 zookeeper instance

    text
    docker network create kafka
    docker run -d `
    --rm `
    --name zookeeper-1 `
    --net kafka `
    -v ${PWD}/config/zookeeper-1/zookeeper.properties:/kafka/config/zookeeper.properties `
    aimvector/zookeeper:2.7.0
    
    docker logs zookeeper-1

    Kafka - 1

    text
    docker run -d `
    --rm `
    --name kafka-1 `
    --net kafka `
    -v ${PWD}/config/kafka-1/server.properties:/kafka/config/server.properties `
    aimvector/kafka:2.7.0
    
    docker logs kafka-1

    Kafka - 2

    text
    docker run -d `
    --rm `
    --name kafka-2 `
    --net kafka `
    -v ${PWD}/config/kafka-2/server.properties:/kafka/config/server.properties `
    aimvector/kafka:2.7.0

    Kafka - 3

    text
    docker run -d `
    --rm `
    --name kafka-3 `
    --net kafka `
    -v ${PWD}/config/kafka-3/server.properties:/kafka/config/server.properties `
    aimvector/kafka:2.7.0

    Topic

    Let's create a Topic that allows us to store Order information.
    To create a topic, Kafka and Zookeeper have scripts with the installer that allows us to do so.

    Access the container:

    text
    docker exec -it zookeeper-1 bash

    Create the Topic:

    text
    /kafka/bin/kafka-topics.sh \
    --create \
    --zookeeper zookeeper-1:2181 \
    --replication-factor 1 \
    --partitions 3 \
    --topic Orders

    Describe our Topic:

    text
    /kafka/bin/kafka-topics.sh \
    --describe \
    --topic Orders \
    --zookeeper zookeeper-1:2181

    Simple Producer & Consumer

    The Kafka installation also ships with a script that allows us to produce
    and consume messages to our Kafka network:

    We can then run the consumer that will receive that message on that Orders topic:

    text
    docker exec -it zookeeper-1 bash
    
    /kafka/bin/kafka-console-consumer.sh \
    --bootstrap-server kafka-1:9092,kafka-2:9092,kafka-3:9092 \
    --topic Orders --from-beginning

    With a consumer in place, we can start producing messages

    text
    docker exec -it zookeeper-1 bash
    
    echo "New Order: 1" | \
    /kafka/bin/kafka-console-producer.sh \
    --broker-list kafka-1:9092,kafka-2:9092,kafka-3:9092 \
    --topic Orders > /dev/null

    Once we have a message in Kafka, we can explore where it got stored in which partition:

    text
    docker exec -it kafka-1 bash
    
    apt install -y tree
    tree /tmp/kafka-logs/
    
    
    ls -lh /tmp/kafka-logs/Orders-*
    
    /tmp/kafka-logs/Orders-0:
    total 4.0K
    -rw-r--r-- 1 root root 10M May  4 06:54 00000000000000000000.index    
    -rw-r--r-- 1 root root   0 May  4 06:54 00000000000000000000.log      
    -rw-r--r-- 1 root root 10M May  4 06:54 00000000000000000000.timeindex
    -rw-r--r-- 1 root root   8 May  4 06:54 leader-epoch-checkpoint       
    
    /tmp/kafka-logs/Orders-1:
    total 4.0K
    -rw-r--r-- 1 root root 10M May  4 06:54 00000000000000000000.index    
    -rw-r--r-- 1 root root   0 May  4 06:54 00000000000000000000.log      
    -rw-r--r-- 1 root root 10M May  4 06:54 00000000000000000000.timeindex
    -rw-r--r-- 1 root root   8 May  4 06:54 leader-epoch-checkpoint       
    
    /tmp/kafka-logs/Orders-2:
    total 8.0K
    -rw-r--r-- 1 root root 10M May  4 06:54 00000000000000000000.index    
    -rw-r--r-- 1 root root  80 May  4 06:57 00000000000000000000.log      
    -rw-r--r-- 1 root root 10M May  4 06:54 00000000000000000000.timeindex
    -rw-r--r-- 1 root root   8 May  4 06:54 leader-epoch-checkpoint

    By seeing 0 bytes in partition 0 and 1, we know the message is sitting in partition 2 as it has 80 bytes.
    We can check the message with :

    text
    cat /tmp/kafka-logs/Orders-2/*.log

    Docker Compose with Kafka

    So far we've taken a look at staring up Kafka and Zookeeper instances with
    docker commands.
    We've explored the kafka configuration and how to produce and consume messages.
    Let's put it all together in a docker compose file.

    With compose we'd like to be able to build our containers, pointing to a
    dockerfile folder with build.context.
    We'll also use volumes to mount config files.

    Important note that producers and consumers are running kafka images
    because kafka installation comes prepacked with example consumers and producers as scripts.
    We override the kafka entrypoint with bash and stdin so it starts in a
    paused state so that we have run scripts on these instances.

    Let's start with an empty docker-compose.yaml file :

    text
    version: "3.8"
    services:

    Zookeeper

    text
    zookeeper-1:
      container_name: zookeeper-1
      image: aimvector/zookeeper:2.7.0
      build:
        context: ./zookeeper
      volumes:
      - ./config/zookeeper-1/zookeeper.properties:/kafka/config/zookeeper.properties

    Kafka-1 to 3

    We run 3 kafka instances.
    Changing the service name, container name and config mount folder:

    text
    kafka-1:
      container_name: kafka-1
      image: aimvector/kafka:2.7.0
      build: 
        context: .
      volumes:
      - ./config/kafka-1/server.properties:/kafka/config/server.properties
      - ./data/kafka-1/:/tmp/kafka-logs/

    Producer

    text
    kafka-producer:
        container_name: kafka-producer
        image: aimvector/kafka:2.7.0
        build: 
          context: .
        working_dir: /kafka
        entrypoint: /bin/bash
        stdin_open: true
        tty: true

    Consumer

    text
    kafka-consumer:
        container_name: kafka-consumer
        image: aimvector/kafka:2.7.0
        build: 
          context: .
        working_dir: /kafka
        entrypoint: /bin/bash
        stdin_open: true
        tty: true

    Start the containers

    text
    cd messaging\kafka
    
    docker compose build
    docker compose up

    Overview (`messaging/rabbitmq/readme.md`)

    RabbitMQ

    Docker image over here

    text
    # run a standalone instance
    docker network create rabbits
    docker run -d --rm --net rabbits --hostname rabbit-1 --name rabbit-1 rabbitmq:3.8 
    
    # how to grab existing erlang cookie
    docker exec -it rabbit-1 cat /var/lib/rabbitmq/.erlang.cookie
    
    # clean up
    docker rm -f rabbit-1

    Management

    text
    docker run -d --rm --net rabbits -p 8080:15672 -e RABBITMQ_ERLANG_COOKIE=DSHEVCXBBETJJVJWTOWT --hostname rabbit-manager --name rabbit-manager rabbitmq:3.8-management
    
    #join the manager
    
    docker exec -it rabbit-manager rabbitmqctl stop_app
    docker exec -it rabbit-manager rabbitmqctl reset
    docker exec -it rabbit-manager rabbitmqctl join_cluster rabbit@rabbit-1
    docker exec -it rabbit-manager rabbitmqctl start_app
    docker exec -it rabbit-manager rabbitmqctl cluster_status

    Enable Statistics

    text
    docker exec -it rabbit-1 rabbitmq-plugins enable rabbitmq_management
    docker exec -it rabbit-2 rabbitmq-plugins enable rabbitmq_management
    docker exec -it rabbit-3 rabbitmq-plugins enable rabbitmq_management

    Message Publisher

    text
    cd messaging\rabbitmq\applications\publisher
    docker build . -t aimvector/rabbitmq-publisher:v1.0.0
    
    docker run -it --rm --net rabbits -e RABBIT_HOST=rabbit-1 -e RABBIT_PORT=5672 -e RABBIT_USERNAME=guest -e RABBIT_PASSWORD=guest -p 80:80 aimvector/rabbitmq-publisher:v1.0.0

    Message Consumer

    text
    docker build . -t aimvector/rabbitmq-consumer:v1.0.0
    docker run -it --rm --net rabbits -e RABBIT_HOST=rabbit-1 -e RABBIT_PORT=5672 -e RABBIT_USERNAME=guest -e RABBIT_PASSWORD=guest aimvector/rabbitmq-consumer:v1.0.0

    Clustering

    https://www.rabbitmq.com/cluster-formation.html

    Note

    Remember we will need the Erlang Cookie to allow instances to authenticate with each other.

    Manual Clustering

    text
    docker exec -it rabbit-1 rabbitmqctl cluster_status
    
    #join node 2
    
    docker exec -it rabbit-2 rabbitmqctl stop_app
    docker exec -it rabbit-2 rabbitmqctl reset
    docker exec -it rabbit-2 rabbitmqctl join_cluster rabbit@rabbit-1
    docker exec -it rabbit-2 rabbitmqctl start_app
    docker exec -it rabbit-2 rabbitmqctl cluster_status
    
    #join node 3
    docker exec -it rabbit-3 rabbitmqctl stop_app
    docker exec -it rabbit-3 rabbitmqctl reset
    docker exec -it rabbit-3 rabbitmqctl join_cluster rabbit@rabbit-1
    docker exec -it rabbit-3 rabbitmqctl start_app
    docker exec -it rabbit-3 rabbitmqctl cluster_status

    Automated Clustering

    text
    docker run -d --rm --net rabbits `
    -v ${PWD}/config/rabbit-1/:/config/ `
    -e RABBITMQ_CONFIG_FILE=/config/rabbitmq `
    -e RABBITMQ_ERLANG_COOKIE=WIWVHCDTCIUAWANLMQAW `
    --hostname rabbit-1 `
    --name rabbit-1 `
    -p 8081:15672 `
    rabbitmq:3.8-management
    
    docker run -d --rm --net rabbits `
    -v ${PWD}/config/rabbit-2/:/config/ `
    -e RABBITMQ_CONFIG_FILE=/config/rabbitmq `
    -e RABBITMQ_ERLANG_COOKIE=WIWVHCDTCIUAWANLMQAW `
    --hostname rabbit-2 `
    --name rabbit-2 `
    -p 8082:15672 `
    rabbitmq:3.8-management
    
    docker run -d --rm --net rabbits `
    -v ${PWD}/config/rabbit-3/:/config/ `
    -e RABBITMQ_CONFIG_FILE=/config/rabbitmq `
    -e RABBITMQ_ERLANG_COOKIE=WIWVHCDTCIUAWANLMQAW `
    --hostname rabbit-3 `
    --name rabbit-3 `
    -p 8083:15672 `
    rabbitmq:3.8-management
    
    #NODE 1 : MANAGEMENT http://localhost:8081
    #NODE 2 : MANAGEMENT http://localhost:8082
    #NODE 3 : MANAGEMENT http://localhost:8083
    
    # enable federation plugin
    docker exec -it rabbit-1 rabbitmq-plugins enable rabbitmq_federation 
    docker exec -it rabbit-2 rabbitmq-plugins enable rabbitmq_federation
    docker exec -it rabbit-3 rabbitmq-plugins enable rabbitmq_federation

    Basic Queue Mirroring

    text
    docker exec -it rabbit-1 bash
    
    # https://www.rabbitmq.com/ha.html#mirroring-arguments
    
    rabbitmqctl set_policy ha-fed \
        ".*" '{"federation-upstream-set":"all", "ha-mode":"nodes", "ha-params":["rabbit@rabbit-1","rabbit@rabbit-2","rabbit@rabbit-3"]}' \
        --priority 1 \
        --apply-to queues

    Automatic Synchronization

    https://www.rabbitmq.com/ha.html#unsynchronised-mirrors

    text
    rabbitmqctl set_policy ha-fed \
        ".*" '{"federation-upstream-set":"all", "ha-sync-mode":"automatic", "ha-mode":"nodes", "ha-params":["rabbit@rabbit-1","rabbit@rabbit-2","rabbit@rabbit-3"]}' \
        --priority 1 \
        --apply-to queues

    Further Reading

    https://www.rabbitmq.com/ha.html

    Clean up

    text
    docker rm -f rabbit-1
    docker rm -f rabbit-2
    docker rm -f rabbit-3

    RabbitMQ on Kubernetes

    Checkout the Kubernetes walkthrough here


    Overview (`monitoring/fluentbit/README.md`)

    Introduction to FluentBit

    We always start with the official documentation

    Running fluentbit

    text
    # starts fluentbit with default config
    docker run -it cr.fluentbit.io/fluent/fluent-bit:4.0
    
    # see HELP text 
    docker run -it cr.fluentbit.io/fluent/fluent-bit:4.0 -h

    Configure fluentbit

    We've created a new fluent-bit.yaml with the example content from the documentation.

    We can update our docker command to mount the config and tell fluent-bit to use the new configuration file we created

    shell
    docker run -it -p 2020:2020 \
    -v ./monitoring/fluentbit/fluent-bit.yaml:/fluent-bit/etc/fluent-bit.yaml \
    cr.fluentbit.io/fluent/fluent-bit:4.0 --config=/fluent-bit/etc/fluent-bit.yaml

    Inputs

    The example config file has a random input.
    Let's create two inputs, one reading log files and another accepting HTTP inputs

    yaml
    pipeline:
      inputs:
      - name: tail
        path: /var/lib/docker/containers/*/*.log
        multiline.parser: docker
    
      - name: http
        listen: 0.0.0.0
        port: 8888

    We'll need to add a volume mount so our fluent-bit container has access to the container logs, so let's mount /var/lib/docker/

    We also need port 8888 to match our input plugin port

    shell
    docker run -it \
    -p 2020:2020 \
    -p 8888:8888 \
    -v ./monitoring/fluentbit/fluent-bit.yaml:/fluent-bit/etc/fluent-bit.yaml \
    -v /var/lib/docker:/var/lib/docker \
    cr.fluentbit.io/fluent/fluent-bit:4.0 --config=/fluent-bit/etc/fluent-bit.yaml

    Test our HTTP input plugin using curl

    shell
    curl -d '{"namespace":"product","message":"hello from product"}' -X POST -H "content-type: application/json" http://localhost:8888/product.log

    Outputs

    In the example config file we had a stdout output plugin that writes all data from the input plugin to the terminal.
    Let's create another output plugin that writes data to file so we can inspect it:

    yaml
    - name: file
      match: '*'
      template: '{log}'
      path: /fluent-bit/.data

    Let's add a volume mount so we can see the output data.
    Let's mount a local .data folder to /fluent-bit/.data

    shell
    docker run -it \
    -p 2020:2020 \
    -p 8888:8888 \
    -v ./monitoring/fluentbit/fluent-bit.yaml:/fluent-bit/etc/fluent-bit.yaml \
    -v /var/lib/docker:/var/lib/docker \
    -v ./monitoring/fluentbit/.data:/fluent-bit/.data \
    cr.fluentbit.io/fluent/fluent-bit:4.0 --config=/fluent-bit/etc/fluent-bit.yaml

    Concepts

    There are key concepts to learn about fluent-bit, now that we have events being ingested.

    Filter plugin

    This plugin only includes requests from "product" namespace

    yaml
    filters:
    - name: grep
      match: '*'
      regex: namespace product

    This plugin excludes all requests from "search" namespace

    yaml
    filters:
    - name: grep
      match: '*'
      exclude: namespace search

    Test our HTTP input plugin using curl to send different events to our fluent-bit service:

    shell
    #request allowed
    curl -d '{"namespace":"product","message":"hello from product"}' -X POST -H "content-type: application/json" http://localhost:8888/product.log
    
    # request dropped\filtered
    curl -d '{"namespace":"search","message":"hello from search"}' -X POST -H "content-type: application/json" http://localhost:8888/search.log

    Overview (`monitoring/logging/readme.md`)

    Logging

    Logging Basics

    • Standardised Logging
    • Centralised Logging

    Check it out

    Introduction to Fluentd

    • What is fluentd
    • Configuration
    • Plugins
    • Demos

    Check if out

    Introduction to OpenTelemetry

    • What is OpenTelemetry
    • Configuration
    • Demos

    Overview (`monitoring/opentelemetry/README.md`)

    OpenTelemetry

    Logs

    Tracing

    Guide here

    Metrics

    Guide here


    Overview (`monitoring/prometheus/readme.md`)

    Prometheus Application Monitoring

    a Video reference guide

    To run any of the commands, please ensure you open a terminal and navigate to the path where this readme is located.

    Start Prometheus, Grafana & Dashboards

    text
    docker compose up -d prometheus
    docker compose up -d grafana
    docker compose up -d grafana-dashboards

    Start the example app you prefer

    text
    docker compose up -d --build go-application
    docker compose up -d --build python-application
    docker compose up -d --build dotnet-application
    docker compose up -d --build nodejs-application

    Generate some requests by opening the application in the browser

    text
    http://localhost:80 #Golang
    http://localhost:81 #Python
    http://localhost:82 #Dotnet
    http://localhost:83 #NodeJS

    Check Dashboards

    text
    http://localhost:3000

    Prometheus Queries

    Golang Examples

    Requests per Second over 2minutes

    text
    irate(go_request_operations_total[2m])

    Request duration

    text
    rate(go_request_duration_seconds_sum[2m]) / rate(go_request_duration_seconds_total[2m])

    Prometheus Guide on Kubernetes

    Checkout the prometheus guide here


    Overview (`python/introduction/README.md`)

    Introduction to Learning Python

    Best place to start is the documentation www.python.org

    Download Python 3

    Docker images for Python on Docker Hub

    Run Python in Docker

    We can also run python in a docker container:

    text
    cd python\introduction
    
    docker build --target dev . -t python
    docker run -it -v ${PWD}:/work python sh
    
    /work # python --version
    Python 3.9.

    Our first Program

    • Create a folder containing our application source code
    text
    mkdir src
    • Create basic source code

    In the src folder, create a program called app.py
    Paste the following content into it.

    text
    def main():
    	print("Hello, world.")
    
    main()
    • Run your application code

    You can run your application

    text
    /work # python src/app.py
    Hello, world.

    The Code

    In the video we cover writing functions.
    It allows us to execute a block of code
    You want to give your function a single purpose
    Functions can have an input and return an output
    Well thought out functions makes it easier to write tests

    Instead of doing a boring x + y function that adds two numbers, let's do something a little
    more realistic but still basic:

    text
    # This function returns some data 
    # The data could be coming from a database, or a file.
    # The person calling the function should not care
    # how the data is retrieved.
    # Since the function does not leak its Data provider
    
    def getCustomer():
      return "Marcel Dempers"
    
    print(getCustomer())

    Function inputs

    As we saw, functions can return outputs, and also take inputs.
    Let's accept a customer ID and return a customer record with that ID:

    text
    def getCustomer(customerID):
      return "CustomerID: " + customerID + ", Name: Marcel Dempers"
    
    print(getCustomer("abc"))

    Variables

    To hold data in programming languages, we use variables.
    Variables take up space in memory, so we want to keep it minimal.
    Let's declare variables in our function:

    text
    def getCustomer(customerID):
      firstName = "Marcel"
      lastName = "Dempers"
      fullName = firstName + " " + lastName
    
      return fullName
    
      #or we can return the computation instead of adding another variable!
      return firstName + " " + lastName
    
      #or we don't even need to declare variables :)
      return "Marcel Dempers"
    
    customer = getCustomer("abc")
    print(customer)

    Control Flows (if\else)

    You can see we're not using the customerId input in our function.
    Let's use it and showcase control flows to create logic!

    Control flows allow us to add "rules" to our code.
    "If this is the case, then do that, else do something else".

    So let's say we have a customer ID 1 coming in, we may only want to
    return our customer if it matches the customerID

    text
    def getCustomer(customerID):
      if customerID == "abc":
        return "Marcel Dempers"
      elif customerID == "def":
        return "Bob Smith"
      else:
        return ""
    
    customer = getCustomer("abc")
    print(customer)

    Let's invoke our function :

    text
    marcel = getCustomer("abc")
    print(marcel)
    
    bob = getCustomer("def")
    print(bob)

    Arrays

    Our function can only return one customer at a time.
    What if we wanted to return more than one customer.
    In real world systems, we may return database records.
    This is where we start looking at arrays, lists, dictionaries etc.

    Let's add another function to get an array of customers!

    text
    def getCustomers():
      customers = [ "Marcel Dempers", "Bob Smith" ]
      return customers
    
    customers = getCustomers()
    print(customers)

    You can get array values by index starting at 0:

    text
    customers = getCustomers()
    
    marcel = customers[0]
    bob = customers[1]
    
    print(marcel + "\n" + bob)

    We can set a value in our Array.
    Arrays are fixed in size, notice we cannot just access a
    record :

    text
    customers[2] = "John Smith"
    IndexError: list assignment index out of range

    Instead we can use the append() function of the array to add items.

    text
    def getCustomers():
      customers = [ "Marcel Dempers", "Bob Smith" ]
      customers.append("James Baker")
      customers.append("Jonathan D")
      customers.append("Aleem Janmohamed")
      customers.append("Ivo Galic")
      customers.append("Joel Griffiths")
      customers.append("Michael Spinks")
      customers.append("Victor Savkov")
      return customers

    We can remove items with the remove() function.

    text
    customers.remove("Bob Smith")

    Dictionaries

    Dictionaries have an advantage whereby items can be found using a key.
    With arrays, we have to know the index number where the item is located or else we cannot find items unless we write a loop.
    With Dictionaries, we store key value pairs. So the key, may be our customer ID.

    Let's replace our customer array with a dictionary

    text
    customers = {
        "a": "James Baker",
        "b": "Jonathan D",
        "c": "Aleem Janmohamed",
        "d": "Ivo Galic",
        "e": "Joel Griffiths",
        "f": "Michael Spinks",
        "g": "Victor Savkov"
      }

    We can also set and access an item by key

    text
    #update or create item
    customers["h"] = "Marcel Dempers"
    
    #get an item
    marcel = customers["h"]
    print(marcel)

    Let's update our getCustomer() function to get customer from our dictionary

    text
    def getCustomer(customerID):
      customer = getCustomers()
      return customer[customerID]
    
    def getCustomers():
      customers = {
        "a": "James Baker",
        "b": "Jonathan D",
        "c": "Aleem Janmohamed",
        "d": "Ivo Galic",
        "e": "Joel Griffiths",
        "f": "Michael Spinks",
        "g": "Victor Savkov",
        "h" : "Marcel Dempers"
      }
      
      return customers
    
    customers = getCustomers()
    customer = getCustomer("h")
    
    print(customer)
    print(customers)

    Loops

    Loops are used to iterate over collections, lists, arrays etc.
    There are two primitive loops in Python
    Let's say we need to loop through our customers

    While loop

    While loops are simple to write by condition:

    text
    i = 0
    while i < 7:
      print(customers[i])
      i += 1

    We can also use this condition to loop arrays:

    text
    customers = [ "Marcel Dempers", "Bob Smith" ]
    customers.append("James Baker")
    customers.append("Jonathan D")
    customers.append("Aleem Janmohamed")
    customers.append("Ivo Galic")
    customers.append("Joel Griffiths")
    customers.append("Michael Spinks")
    customers.append("Victor Savkov")
    i = 0
    while i <= 8:
      print(customers[i])
      i += 1

    For loops

    For looping a dictionary of list, for loops are a little easier:

    text
    customers = getCustomers()
    
    for customerID in customers:
      print(customers[customerID])

    Classes and Objects

    So far so good, however, customer data is not useful as strings.
    Customers can have a firstname, lastname, and more properties.

    For this purpose we'd like to group some variables into a single variable.
    This is what classes allows us to do.
    Let's create a class for our customer

    text
    class Customer:
      customerID = ""
      firstName  = ""
      lastName   = ""
      def fullName(self):
        return self.firstName + " " + self.lastName
    
    marcel = Customer()
    marcel.firstName = "Marcel"
    marcel.lastName = "Dempers"
    marcel.customerID = "h"
    
    print(marcel.fullName())

    Now how can we create an object with our values without having to set every
    value individually ?
    Constructors allow us to initialise our object with some default values.

    text
    class Customer:
      def __init__(self, c="", f="", l=""):
        self.customerID = c
        self.firstName  = f
        self.lastName   = l
      def fullName(self):
        return self.firstName + " " + self.lastName
    
    marcel = Customer("h", "Marcel", "Dempers")
    
    print(marcel.fullName())

    Let's plug our class into our current application and put it all together:
    Firstly, we convert all our customers from strings to objects:

    text
    def getCustomers():
      customers = {
        "a": Customer("a","James", "Baker"),
        "b": Customer("b", "Jonathan", "D"),
        "c": Customer("c", "Aleem", "Janmohamed"),
        "d": Customer("d", "Ivo", "Galic"),
        "e": Customer("e", "Joel", "Griffiths"),
        "f": Customer("f", "Michael", "Spinks"),
        "g": Customer("g", "Victor", "Savkov"),
        "h" : Customer("h", "Marcel", "Dempers")
      }
      
      return customers

    Then we can proceed to get our customer and access their fields:

    text
    customers = getCustomers()
    for customerID in customers:
      print(customers[customerID].fullName())

    Docker

    So far we have used our container as a development environment.
    Docker allows us to run python in an isolated environment.
    Docker also has a concept of stages, I.E Docker Multistage.

    Therefore we may have a stage for development where we simply mount source code to access a python environment. In addition to this, we may add another layer for debugging where we can install debugger and extra development tools which we
    do not want in production.
    Finally we can create a smaller stage which contains only python + our source which we can run as a runtime image in production.

    Example:

    text
    FROM python:3.9.6-alpine3.13 as dev
    
    WORKDIR /work
    
    FROM python:3.9.6-alpine3.13 as debugging
    
    # add a debugger
    
    FROM dev as runtime
    COPY ./src/ /app 
    
    ENTRYPOINT [ "python", "/app/app.py" ]

    Building the Container

    text
    docker build . -t customer-app

    Running the Container

    text
    docker run customer-app

    Overview (`storage/redis/readme.md`)

    Redis

    Docker

    Docker image over here

    Running redis

    text
    docker network create redis
    docker run -it --rm --name redis --net redis -p 6379:6379 redis:6.0-alpine

    Configuration

    Redis configuration documentation here

    Starting Redis with a custom config

    text
    cd .\storage\redis\
    docker run -it --rm --name redis --net redis -v ${PWD}/config:/etc/redis/ redis:6.0-alpine redis-server /etc/redis/redis.conf

    Security

    Redis should not be exposed to public.
    Always use a strong password in redis.conf

    text
    requirepass SuperSecretSecureStrongPass

    Persistence

    Redis Persistence Documentation here

    text
    docker volume create redis
    cd .\storage\redis\
    docker run -it --rm --name redis --net redis -v ${PWD}/config:/etc/redis/ -v redis:/data/  redis:6.0-alpine redis-server /etc/redis/redis.conf

    Redis client application

    An example application that reads a key from Redis, increments it and writes it back to Redis.

    text
    cd .\storage\redis\applications\client\
    
    # start go dev environment
    docker run -it -v ${PWD}:/go/src -w /go/src --net redis -p 80:80 golang:1.14-alpine
    
    go build client.go
    # start the app
    ./client
    
    # build the container
    docker build . -t aimvector/redis-client:v1.0.0

    Run our application

    text
    cd .\storage\redis\applications\client\
    docker build . -t aimvector/redis-client:v1.0.0
    
    docker run -it --net redis `
    -e REDIS_HOST=redis `
    -e REDIS_PORT=6379 `
    -e REDIS_PASSWORD="SuperSecretSecureStrongPass" `
    -p 80:80 `
    aimvector/redis-client:v1.0.0

    Redis Replication and High Availability

    Lets move on to the clustering secion.


    Overview (`ai/agents/openclaw/README.md`)

    Introduction to OpenClaw

    In this video, we will dive into the features of OpenClaw.

    Documentation

    Source Code

    shell
    git clone --depth 1 https://github.com/openclaw/openclaw.git /tmp/openclaw

    Docker

    View docker setup scripts:

    shell
    tree /tmp/openclaw/scripts/docker/
    /tmp/openclaw/scripts/docker/
    ├── cleanup-smoke
    │   ├── Dockerfile
    │   └── run.sh
    ├── install-sh-common
    │   ├── cli-verify.sh
    │   └── version-parse.sh
    ├── install-sh-e2e
    │   ├── Dockerfile
    │   └── run.sh
    ├── install-sh-nonroot
    │   ├── Dockerfile
    │   └── run.sh
    ├── install-sh-smoke
    │   ├── Dockerfile
    │   └── run.sh
    ├── sandbox
    │   ├── Dockerfile
    │   ├── Dockerfile.browser
    │   └── Dockerfile.common
    ├── setup.sh
    └── shared-image-artifact.sh

    After cloning the code, we can run setup according to the docs.
    Use a pre-built image to avoid having to build it ourselves:

    shell
    export OPENCLAW_IMAGE="ghcr.io/openclaw/openclaw:latest"
    ./scripts/docker/setup.sh

    Build our own custom dockerfile:

    shell
    docker build . -t aimvector/openclaw

    Least privileges

    Linux containers run with a default set of capabilities, which are fine-grained privileges that are subsets of what root can do. --cap-drop ALL removes every single one, leaving the container with zero elevated privileges.

    Capability Allows
    CAP_NET_BIND_SERVICE Bind to ports < 1024
    CAP_NET_RAW Raw sockets, packet sniffing
    CAP_SYS_ADMIN Mount filesystems and many other things
    CAP_CHOWN Change file ownership
    CAP_KILL Send signals to any process

    In our dockerfile, we also run our container as a non-root unprivileged user.

    Secrets (environment)

    We set our secrets in environment variables and inject those into the container :

    shell
    export OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)"
    export GEMINI_API_KEY="xxxxxxx"
    export DISCORD_BOT_TOKEN="xxxxxxx"
    export GITHUB_TOKEN="xxxxxxx"

    Run our container:

    shell
    mkdir ~/.openclaw
    cp ai/agents/openclaw/openclaw.json  ~/.openclaw/
    docker run -it --rm \
      --name openclaw \
      -e OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN} \
      -e GEMINI_API_KEY=${GEMINI_API_KEY} \
      -e DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN} \
      -e GITHUB_TOKEN=${GITHUB_TOKEN} \
      --cap-drop ALL \
      -v $PWD:/work \
      -v ~/.openclaw:/home/openclaw/.openclaw \
      -w /work \
      --net host \
      -v /var/run/docker.sock:/var/run/docker.sock \
      aimvector/openclaw

    Configration

    If we want to generate a new openclaw.json, we can run the onboarding process by overriding the entrypoint to bash
    We can tweak our docker run to add --entrypoint bash to get into the container and run the openclaw onboard process to get a configuration.

    We can run the onboard manually to generate an openclaw.json configuration file:

    shell
    openclaw onboard --classic

    Kubernetes

    View docker setup scripts:

    shell
    tree /tmp/openclaw/scripts/k8s/
    /tmp/openclaw/scripts/k8s/
    ├── create-kind.sh
    ├── deploy.sh
    └── manifests
        ├── configmap.yaml
        ├── deployment.yaml
        ├── kustomization.yaml
        ├── pvc.yaml
        └── service.yaml
    
    1 directories, 7 files

    Lets create a Kubernetes cluster to play with using kind

    text
    kind create cluster --name openclaw --image kindest/node:v1.36.1

    Test our cluster and makes sure kubectl is configured for it:

    text
    kubectl get nodes
    NAME                     STATUS   ROLES           AGE     VERSION
    openclaw-control-plane   Ready    control-plane   2m30s   v1.36.1

    View kubernetes deployment scripts:

    shell
    tree /tmp/openclaw/scripts/k8s/
    
    /tmp/openclaw/scripts/k8s/
    ├── create-kind.sh
    ├── deploy.sh
    └── manifests
        ├── configmap.yaml
        ├── deployment.yaml
        ├── kustomization.yaml
        ├── pvc.yaml
        └── service.yaml
    
    2 directories, 7 files

    I've taken a copy of these manifests by using cp.

    shell
    cp /tmp/openclaw/scripts/k8s/manifests/* ai/agents/openclaw/kubernetes/

    You can now use Helm, Kuztomize, ArgoCD, Flux or any mechanism of your choice to manage this deployment.

    Deploy Secret

    We can refer to the deployment.yaml and we see the secret references under secretKeyRef

    We will need a secret for our Gateway UI, a secret for our model provider as well as our channel.

    shell
    export OPENCLAW_GATEWAY_TOKEN="$(openssl rand -hex 32)"
    export GEMINI_API_KEY="xxxxxxx"
    export DISCORD_BOT_TOKEN="xxxxxxx"

    I'll be modifying the deployment.yaml to add my channel token DISCORD_BOT_TOKEN.
    Note that we can reference this ENV variable in our openclaw.json configuration.

    yaml
    - name: DISCORD_BOT_TOKEN
      valueFrom:
        secretKeyRef:
          name: openclaw-secrets
          key: DISCORD_BOT_TOKEN
          optional: false

    Create the secret:

    shell
    kubectl create ns openclaw
    kubectl create secret generic openclaw-secrets -n openclaw \
      --from-literal OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN} \
      --from-literal GEMINI_API_KEY=${GEMINI_API_KEY} \
      --from-literal DISCORD_BOT_TOKEN=${DISCORD_BOT_TOKEN}

    Configure OpenClaw

    We've configured our openclaw instance in our Configmap.yaml

    Deploy Openclaw

    Apply the manifests

    shell
    kubectl -n openclaw apply -f ai/agents/openclaw/kubernetes/

    Install Plugins

    shell
    export NPM_CONFIG_CACHE=~/.openclaw/.npm
    openclaw plugins install @openclaw/discord
    kubectl -n openclaw delete pods --all

    Overview (`ai/kubernetes/llm-kube/README.md`)

    Introduction to LLMKube

    Website: LLMKube

    What is LLMKube?

    LLMKube is a Kubernetes operator for self-hosted LLM inference.
    Source Code: defilantech/LLMKube
    Built on llama.cpp.

    We need a Kubernetes cluster

    Lets create a Kubernetes cluster to play with using kind

    text
    kind create cluster --name llmkube --image kindest/node:v1.34.3

    Test our cluster and makes sure kubectl is configured for it:

    text
    kubectl get nodes
    NAME                       STATUS   ROLES           AGE   VERSION
    llmkube-control-plane   Ready    control-plane   40s   v1.34.3

    Installing LLMKube with Helm

    shell
    helm repo add llmkube https://defilantech.github.io/LLMKube
    helm repo update
    
    helm search repo llmkube --versions
    CHART_VERSION="0.7.5"
    
    helm install llmkube llmkube/llmkube \
      --namespace llmkube-system \
      --version ${CHART_VERSION} \
      --create-namespace

    Check installation:

    shell
    kubectl get pods -n llmkube-system

    Two new CRDs Model and InferenceService are now available in the cluster:

    shell
    kubectl get crds | grep llmkube
    inferenceservices.inference.llmkube.dev
    models.inference.llmkube.dev

    The CRDs

    Model

    yaml
    # Deploy the Model
    kubectl apply -f ai/kubernetes/llm-kube/model.yaml
    
    # Check the Model
    kubectl get model
    kubectl describe model

    InferenceService

    yaml
    # Deploy the InferenceService
    kubectl apply -f ai/kubernetes/llm-kube/inference.yaml
    
    # Check the InferenceService
    kubectl get inferenceservices
    kubectl describe inferenceservices

    Testing the Inference API

    Port-forward the service to reach it from your machine:

    shell
    kubectl port-forward svc/gemma-e2b-service 8080:8080

    Send a test request to the OpenAI-compatible endpoint:

    shell
    curl -X POST http://localhost:8080/v1/chat/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "gemma-e2b",
        "messages": [
          {"role": "user", "content": "What is Kubernetes in one sentence?"}
        ],
        "max_tokens": 80
      }'

    OpenAI-Compatible Integration

    The example below connects Opencode to the local service

    json
    {
      "$schema": "https://opencode.ai/config.json",
      "provider": {
        "llmkube": {
          "npm": "@ai-sdk/openai-compatible",
          "name": "LLMKube (local)",
          "options": {
            "baseURL": "http://localhost:8080/v1"
          },
          "models": {
            "gemma-e2b": {
              "name": "Gemma 4 E2B (kind)",
              "limit": {
                "context": 32768,
                "output": 4096
              }
            }
          }
        }
      }
    }

    Overview (`ai/models/llama-cpp/README.md`)

    Introduction to llama.cpp

    In this guide, we will learn how to run large language models locally using llama.cpp

    We will cover:

    • What is llama.cpp
    • What is quantization
    • How to find and run models
    • How to host a local model server
    • Interact with models using CLI like OpenCode.

    What is llama.cpp

    llama.cpp is a lightweight, high-performance implementation designed to run large language models locally on your own machine.
    It has no external dependencies and no Python — just pure C++.

    Key highlights:

    • Runs entirely on your own hardware — no cloud, no API keys required
    • Supports CPU, NVIDIA GPU (CUDA), AMD GPU (ROCm), Intel GPU (SYCL), and Vulkan
    • Natively supports quantized models (Q2 through Q8) for memory efficiency
    • Ships with a CLI (llama-cli) and a local HTTP server (llama-server)

    Quantization

    • AI models are massive
      • a 7B parameter model at full precision (F32) needs ~28GB of RAM
      • a 31B parameter model needs a really good top of the range GPU like a 4090
    • Quantization compresses the model's numbers (e.g. 32-bit down to 4-bit), cutting memory by up to 8x with minimal quality loss
    • A 7B model can run in as little as 4GB of RAM on a laptop CPU
    • llama.cpp natively supports Q2 through Q8, letting you tune the speed, memory, and quality trade-off to your hardware

    Quantization levels:

    Quant Size Quality Use when
    Q2_K Smallest Lowest Very limited RAM
    Q3_K_M Very small Low Tight on RAM
    Q4_K_M Small Good Best balance — most popular
    Q5_K_M Medium Better Have spare RAM
    Q6_K Large Very good Plenty of RAM
    Q8_0 Largest Near lossless Maximum quality
    F16 Huge Lossless Rarely practical

    Where to Get Models

    Hugging Face is the primary hub for AI models — think of it as the Docker Hub of models.

    We can start with Gemma 4

    Source Code

    llama.cpp is fully open source and available on GitHub.
    Pre-built binary releases for all platforms are available at github.com/ggml-org/llama.cpp/releases.

    Docker Images

    The easiest way to get started is via the official Docker images.
    Official Docker Guide

    Three image types are available:

    • light: includes CLI and completion tools
    • full: includes light + tools for model conversion (safetensors → GGUF)
    • server: only the llama-server executable

    Choosing the right image

    Image Backend Vendor
    ghcr.io/ggml-org/llama.cpp:full CPU Any
    ghcr.io/ggml-org/llama.cpp:full-cuda CUDA 12 + cuBLAS NVIDIA
    ghcr.io/ggml-org/llama.cpp:full-cuda13 CUDA 13 + cuBLAS NVIDIA
    ghcr.io/ggml-org/llama.cpp:full-rocm ROCm + hipBLAS AMD
    ghcr.io/ggml-org/llama.cpp:full-intel SYCL Intel Arc/Xe
    ghcr.io/ggml-org/llama.cpp:full-vulkan Vulkan Cross-vendor
    ghcr.io/ggml-org/llama.cpp:full-openvino OpenVINO Intel CPU/iGPU/NPU

    Running llama.cpp

    Example 1: Linux (CPU or integrated graphics):
    Using --device /dev/dri for integrated Graphics

    shell
    docker run -it \
      -v ~/models:/models \
      --device /dev/dri \
      -p 8080:8080 \
      --entrypoint bash ghcr.io/ggml-org/llama.cpp:full-intel

    Example 2:

    WSL with NVIDIA GPU:

    shell
    docker run -it \
      -v ~/models:/models \
      --gpus all \
      -p 8080:8080 \
      --entrypoint bash ghcr.io/ggml-org/llama.cpp:full-cuda

    Once inside the container, verify the CLI works with ./llama-cli --help

    Running a Model

    Download and run directly from Hugging Face

    The -hf flag lets you pull a model directly from Hugging Face by repo and quant:

    shell
    ./llama-cli -hf ggml-org/gemma-4-E4B-it-GGUF:Q4_K_M

    Note: This downloads and caches the model under ~/.cache/huggingface/ inside the container, not your /models mount. To persist models across container restarts, download them manually to /models first.

    Run from a local GGUF file

    shell
    ./llama-cli -m /models/gemma-4-E4B-it-Q4_K_M.gguf -ngl 99

    Use -ngl 0 to disable GPU offload and run on CPU only:

    shell
    ./llama-cli -m /models/gemma-4-E4B-it-Q4_K_M.gguf -ngl 0

    Hosting a Model

    To expose your model as an HTTP API, use llama-server on port 8080.
    Once running, you can access the built-in web UI and API at http://localhost:8080.

    Linux (CPU):

    shell
    export LLAMA_ARG_HOST=0.0.0.0
    ./llama-server -m /models/gemma-4-E4B-it-Q4_K_M.gguf \
      --port 8080 \
      -ngl 0 \
      --jinja \
      -c 8192 \
      --parallel 1 \
      --temperature 1.0 \
      --top-p 0.95 \
      --top-k 64

    WSL with NVIDIA GPU:

    shell
    export LLAMA_ARG_HOST=0.0.0.0
    
    # gemma-4-31B-it-Q4_K_M.gguf
    # gemma-4-26B-A4B-it-Q4_K_M.gguf
    # gemma-4-E2B-it-Q8_0.gguf
    # gemma-4-E4B-it-Q4_K_M.gguf
    
    ./llama-server -m /models/gemma-4-E2B-it-Q8_0.gguf \
      --port 8080 \
      -ngl 99 \
      --jinja \
      -c 131072 \
      --parallel 1 \
      --temperature 1.0 \
      --top-p  0.95 \
      --top-k 64

    Key flags:

    Flag Purpose
    -ngl 99 Offload all layers to GPU (llama.cpp uses as many as fit in VRAM)
    -c 65536 Context window size in tokens
    --parallel 1 1 concurrent request slot — correct for single-user use
    --jinja Enables Jinja2 chat templates for proper instruction-tuned model formatting
    --temperature Default sampling temperature (0.0 = deterministic, 1.0 = creative)

    Model Interaction via OpenCode

    You can interact with your locally hosted model using OpenCode.
    OpenCode connects to the llama-server endpoint over its OpenAI-compatible API.

    Configure ~/.opencode/opencode.json:

    json
    {
      "$schema": "https://opencode.ai/config.json",
      "provider": {
        "llama.cpp": {
          "npm": "@ai-sdk/openai-compatible",
          "name": "llama-server (local)",
          "options": {
            "baseURL": "http://localhost:8080/v1"
          },
          "models": {
            "gemma-4-26B-A4B-it-Q4_K_M.gguf": {
              "name": "gemma-4-26B-A4B-it-Q4_K_M (local)",
              "limit": {
                "context": 65536,
                "output": 16384
              }
            }
          }
        }
      }
    }

    Overview (`ai/openai/introduction/README.md`)

    Introduction to Open AI

    Overview

    What is Open AI ?

    • Research company on AI development
    • Builds and provides models
    • Builds and provides a standard protocol for using AI

    What is a model ?

    I see a model as a language super database.
    Instead of writing a query, that is slow to query a traditional database like SQL, you can throw a question at a model and it gives you an answer really fast

    Model examples:

    • GPT 3.5
    • GPT 4

    Getting started

    The best way to get started and to understand OpenAI, is to learn hands on

    • Create an OpenAI account here

    Chat GPT

    Here you can find the link to ChatGPT

    Open AI Playground

    Here you can find the link to the OpenAI Playground

    Build an AI powered app

    We can start with a main.py that reads a message

    text
    import sys
    
    message = sys.argv[0]

    Then we will need the code from the Open AI playground and add it to our main.py.
    Move the import statements to the top

    Once you have tidied up everything, you can get the response message from the AI:

    text
    responseMessage = response.choices[0].message.content

    Let's build our app

    text
    cd ai\openai\introduction
    docker build . -t ai-app

    Set my OpenAI API key

    text
    $ENV:OPENAI_API_KEY=""

    Run our AI App:

    text
    docker run -it -e OPENAI_API_KEY=$ENV:OPENAI_API_KEY ai-app

    When we run the app, notice it has no concept of memory.
    The playground works because it keeps track of all the user and AI messages and keeps appending new messages to it
    So it can track the conversation.

    Let's keep track of messages, by writing it to a local file
    We will also take the system message out and keep it as a constant in our code

    Full example:

    text
    import sys
    import os
    import json
    import openai
    
    openai.api_key = os.getenv("OPENAI_API_KEY")
    
    #read the incoming message
    message = sys.argv[1]
    user_message = {
      "role" : "user",
      "content" : message
    }
    
    systemMessage = {
      "role": "system",
      "content": "You are a kubernetes exper that can assist developers with troubleshooting deployments\n\nTo help the developer you will need to know the namespaces as well as the pod name. Ask for missing information\n\nGenerate a command to help the developer surface logs or information\n"
    }
    
    # read the cached user messages if there are any
    userMessages = []
    if os.path.isfile("messages.json"):
      with open('messages.json', newline='') as messagesFile:
        data = messagesFile.read()
        userMessages = json.loads(data)
    
    # add the new message to it and update the cached messages
    userMessages.append(user_message)
    with open('messages.json', 'w', newline='') as messagesFile:
      msgJSON = json.dumps(userMessages)
      messagesFile.write(msgJSON)
      print(msgJSON)
    
    messages = []
    messages.append(systemMessage)
    messages.extend(userMessages)
    
    response = openai.ChatCompletion.create(
      model="gpt-3.5-turbo",
      messages=messages,
      temperature=1,
      max_tokens=256,
      top_p=1,
      frequency_penalty=0,
      presence_penalty=0
    )
    
    responseMessage = response.choices[0].message.content
    print(responseMessage)

    Now we can mount our volume so we persist the cache of messages

    text
    docker run -it -e OPENAI_API_KEY=$ENV:OPENAI_API_KEY -v ${PWD}:/app ai-app "can you help me with my deployment?"
    Of course! I'd be happy to help with your deployment. Could you please provide me with the namespace and the name of the pod you're encountering issues with?
    
    docker run -it -e OPENAI_API_KEY=$ENV:OPENAI_API_KEY -v ${PWD}:/app ai-app "my pod is pod-123" 
    Sure, I can help you with your deployment. Can you please provide me with the namespace in which the pod is running?
    
    docker run -it -e OPENAI_API_KEY=$ENV:OPENAI_API_KEY -v ${PWD}:/app ai-app "its in the products namespace"
    Great! To surface the logs for the pod "pod-123" in the "products" namespace, you can use the following command:
    
    ```shell
    kubectl logs -n products pod-123

    This command will retrieve the logs for the specified pod in the given namespace. Make sure you have the necessary permissions to access the namespace.

    text
    ---
    
    ### Overview (`automation/cicd/octopus-deploy/README.md`)
    
    # Introduction to Octopus Deploy
    
    [](https://youtu.be/xxxxx) 
    
    Start here 👉🏽[https://octopus.com/](https://octopus.com/) 
    Documentation 👉🏽[https://octopus.com/docs](https://octopus.com/docs)
    
    In this video, I sign up with the Free version of Octopus deploy to get my license key. 
    
    ## Create Kubernetes clusters
    
    In my video I use `kind` to act as my DEV and PROD environment where I run microservices 
    Let's create that:
    ```shell
    kind create cluster --name dev --image kindest/node:v1.34.0
    kind create cluster --name prod --image kindest/node:v1.34.0

    Octopus Components & Installation

    In this video, I will explore the architecture of Octopus and the required components, so I'll be using the self-hosted option for learning.

    I highly recommend you checkout the Cloud version.

    My settings:

    text
    cd automation/cicd/octopus-deploy
    
    export MASTER_KEY=$(openssl rand -base64 32)
    export OCTOPUS_SERVER_BASE64_LICENSE=xxxxx
    export ADMIN_USERNAME="octo-admin"
    export ADMIN_PASSWORD="octo-admin123"
    export ADMIN_EMAIL="[email protected]"

    Spin up a database for Octopus to use:

    text
    docker run -d --name octopus-database \
    --net kind \
    -u root \
    -e ACCEPT_EULA="Y" \
    -e MSSQL_SA_PASSWORD="OurStrongPassword123" \
    -v ${PWD}/.data/mssql/:/var/opt/mssql/data \
    mcr.microsoft.com/mssql/server:2022-latest

    Create our first Octopus server:

    notes: ports to expose

    text
    docker run -d \
    --name octopus \
    --net kind \
    -p 8080:8080 \
    -p 8443:8443 \
    -p 10943:10943 \
    -e ACCEPT_EULA="Y" \
    -e ADMIN_USERNAME=$ADMIN_USERNAME \
    -e ADMIN_PASSWORD=$ADMIN_PASSWORD \
    -e ADMIN_EMAIL=$ADMIN_EMAIL \
    -e OCTOPUS_SERVER_BASE64_LICENSE=$OCTOPUS_SERVER_BASE64_LICENSE \
    -e MASTER_KEY=$MASTER_KEY \
    -e DB_CONNECTION_STRING="Server=octopus-database,1433;Database=OctopusDeploy;User=sa;Password=OurStrongPassword123" \
    -v ${PWD}/.data/octopus/repository/:/repository \
    -v ${PWD}/.data/octopus/artifacts/:/artifacts \
    -v ${PWD}/.data/octopus/taskLogs/:/taskLogs \
    -v ${PWD}/.data/octopus/cache/:/cache \
    -v ${PWD}/.data/octopus/import/:/import \
    -v ${PWD}/.data/octopus/eventExports/:/eventExports \
    octopusdeploy/octopusdeploy:2025.3

    Access Octopus Dashboard

    Let's get the IP of my machine so we can access Octopus:

    text
    ifconfig eth0 | grep 'inet' | awk '{print $2}' | cut -d: -f2

    We can access the dashboard on http://:8080

    Setup Environments

    The Octopus UI helps us setup and configure environments.
    In this video, we add two Kubernetes environments, k8s-dev and k8s-prod

    We'll setup targets that will allow us to deploy to each environment.

    Setup Targets

    The Environment page allows us to configure deployment targets.
    The Octopus UI guides us to install the Kubernetes Agent using Helm.
    In this video, we will add targets for each Kubernetes environment so we can target those environments for deployment.

    Script Modules

    Octopus allows us to create script modules that we can use during deployments.
    Instead of placing raw scripts into deployment steps, we can create re-usable modules

    Let's create a simple module called create_namespace that creates a kubernetes namespace:

    bash
    create_namespace() {
    NAMESPACE_NAME=$1
    
    # Check if the namespace exists.
    # The 'get namespace' command will fail if the namespace is not found.
    # The '>/dev/null 2>&1' part suppresses all output (success or failure) 
    # so the script remains silent unless a new namespace is created.
    
    kubectl get namespace $NAMESPACE_NAME >/dev/null 2>&1
    
    # $? is the exit status of the last command (0 for success, non-zero for failure)
    if [ $? -ne 0 ]; then
        echo "Namespace '$NAMESPACE_NAME' not found. Creating it now..."
        kubectl create namespace $NAMESPACE_NAME
        echo "Namespace '$NAMESPACE_NAME' created successfully."
    else
        echo "Namespace '$NAMESPACE_NAME' already exists. Skipping creation."
    fi
    }

    Now we'll be able to source that later in a deployment step.

    Setup Project

    We'll setup a project to deploy our service. A project holds the process & steps as well as variables and features that allow us to deploy our software.

    Setup Deployment Steps

    We can now use a Script step template to run our script module

    bash
    source create_namespace.sh
    create_namespace $(get_octopusvariable "NAMESPACE")

    Setup Variables

    Variables allow us to configure almost anything about our steps, and scripts. They can be config values or even secrets.
    In this video we'll add a NAMESPACE variable for our product namespace.

    Setup Kubernetes Step

    Let's add a new step to "Deploy Kubernetes YAML"
    We can refernce YAML in many ways, in this video we use Github:

    text
    https://github.com/marcel-dempers/docker-development-youtube-series.git

    We also specify all our manifests path that octopus needs to deploy:

    text
    automation/cicd/octopus-deploy/example-application/deployment.yaml
    automation/cicd/octopus-deploy/example-application/service.yaml
    automation/cicd/octopus-deploy/example-application/ingress.yaml

    Variable injection

    Octopus allows us to manipulate files, and flows and achieve different outcomes in many ways. One powerful feature is injecting values into files.

    We can use this for configuration value injection, secret injection etc.

    Let's update contents in our configmap.yaml based on our environment.
    We can inject values into our JSON block based on the environment we are deploying too.

    spec:rules:0:host

    text
    spec:
      ingressClassName: nginx
      rules:
      - host: marcel.local

    Create a step template

    In this video, we also create a new step template which allows us to write modular templates that can be re-used throughout Octopus.
    Since may microservices may use ConfigMap objects, we can create a step template for it

    Our step template name: Deploy ConfigMap to Kubernetes
    Parameters:

    • config_name of the ConfigMap to create
    • config_namespace name of the Namespace to deploy the ConfigMap to
    • config_body the contents of the ConfigMap as Multi-line text

    Inline Source Code:

    bash
    NAMESPACE=$(get_octopusvariable "config_namespace")
    CONFIGMAP_NAME=$(get_octopusvariable "config_name")
    BODY_JSON=$(get_octopusvariable "config_body")
    
    CONFIG_KEY=config.json
    # Check if arguments are provided
    if [ -z "$NAMESPACE" ] || [ -z "$CONFIGMAP_NAME" ] || [ -z "$BODY_JSON" ]; then
        echo "Error: Missing one or more required arguments."
        usage
    fi
    
    kubectl create configmap "${CONFIGMAP_NAME}" --namespace "${NAMESPACE}" \
      --from-literal="${CONFIG_KEY}=${BODY_JSON}" \
      --dry-run=client -o yaml | kubectl apply -f -

    We can then proceed to add this step to our deployment process.

    We can do the same and create a Secret deployment template:

    Our step template name: Deploy Secret to Kubernetes
    Parameters:

    • secret_name of the Secret to create
    • secret_namespace name of the Namespace to deploy the Secret to
    • secret_body the contents of the Secret as Multi-line text
    bash
    NAMESPACE=$(get_octopusvariable "secret_namespace")
    SECRET_NAME=$(get_octopusvariable "secret_name")
    BODY_JSON=$(get_octopusvariable "secret_body")
    
    SECRET_KEY="secret.json"
    
    # Check if arguments are provided
    if [ -z "$NAMESPACE" ] || [ -z "$SECRET_NAME" ] || [ -z "$BODY_JSON" ]; then
        echo "Error: Missing one or more required arguments."
        usage
    fi
    
    ENCODED_BODY=$(echo -n "$BODY_JSON" | base64)
    
    kubectl create secret generic "${SECRET_NAME}" --namespace "${NAMESPACE}" \
      --from-literal="${SECRET_KEY}=${ENCODED_BODY}" \
      --dry-run=client -o yaml | kubectl apply -f -

    Overview (`course/chapters/chapter-1-source-control-git/README.md`)

    Chapter 1 : Source Control and GIT

    Back to Course Introduction

    Introduction

    In this chapter we will cover the fundamental tools you need to know to start a DevOps, SRE or platform engineering career from the beginning.
    This starts with knowing how to work with files.
    Source control allows us to work with files, collaborate with others and track changes and histrory of source code.

    We will also take a look at popular tools that software engineers use to work with files.
    What your will learn:

  • What source control is
  • How to use GIT to manage source code
  • Learn about the basics of hosted source control systems
  • What an IDE is
  • Developer Tools
  • How to use an IDE as an engineer
  • 🎬 [Module 1: Introduction to GIT: Source Control](../../content/source-control/git/README.md)

    GIT is the foundation of DevOps, Programming & Software Engineering
    This is because it is a source code distributed version control system.
    In DevOps, you will deal with code, command line, documentation, scripts, automation, infrastructure-as-code and more.
    Generally, all of this involves writing files and storing them.
    GIT is the system you use to store these files.

    In this module we'll take a read through GIT to understand where it started and why it is so successful and widely used in software engineering.

    In this module

  • What is GIT
  • Installing GIT
  • Command line
  • Create your first GIT repository
  • Branching
  • Commits
  • Merging branches
  • GUI Tools for GIT
  • VSCode
  • 🎬 [Module 2: Introduction to IDEs: Visual Studio Code](../../content/editors/vscode/introduction/README.md)

    When working on source code using GIT, you will want to work on files and directories.
    This includes configuration files, code files, scripts and so on.
    To get a nice overview of all our files and folders, as well as have access to GIT and command line, we'll use an IDE.
    An IDE is an integrated development environment.
    It is software that helps us deal with software development and engineering.

    In this module:

  • What is VSCode
  • Directory and File management
  • Editing files
  • Command line terminals
  • Extensions
  • Themes
  • GIT features
  • 🎬 [Module 3: Introduction to Github: Hosted Source Control](../../content/source-control/github/README.md)

    Storing source code in a Git repository on a local machine carries a risk of data loss.
    If your harddrive crashes, or you lose your laptop, all your work would be lost.
    Therefore it's always recommended to use a hosted source control provider like Github, BitBucket, Gitlab and others.

    It also makes collaboration possible, when you have a team of people working on the same project

    In this module:

  • We create a Github account
  • Overview of Github source control
  • Integrating our repository with Github
  • Working with our repository

  • Overview (`course/chapters/chapter-2-operating-systems/README.md`)

    Chapter 2 : Operating Systems & Virtualization

    Back to Course Introduction

    Introduction

    In this Chapter, we will cover the fundamentals around servers and what role they play in I.T. Servers make up the entire cloud and it's what's responsible for all compute.

    We'll cover the different types of servers, technologies used to create and provision servers, as well as the skills and basic fundamental knowledge required to manage servers.

    This includes the Operating system running on the server, the role of the Kernel and the different type of operating system resources that we will need to know about.

    What your will learn:

  • What servers are
  • Server technology & Virtualization
  • Operating System functions & resources
  • Creating our first virtual server
  • Introduction to Linux
  • Introduction to Command-line
  • Introduction to scripting
  • 🎬 [Module 1: Introduction Operating Systems for DevOps](../../content/operating-systems/introduction/README.md)

    In this module

  • What is an Operating system
  • Why Operating Systems are important for DevOps
  • Operating System Key Functions
  • Operating systems resources
  • text
    <li> user management  </li>
    <li> permission management  </li>
    <li> hardware management & devices </li>
    <li> resource management </li>
      
        <li> CPU</li>
        <li> Memory</li>
        <li> Disk</li>
  • The Kernel
  • Terminal & Command Line
  • 🎬 [Module 2: Introduction to Servers & Virtualisation](../../content/operating-systems/virtualization/README.md)

    In this module

  • What are servers
  • Introduction to Virtualisation
  • Virtualisation software
  • Creating our first server
  • 🎬 [Module 3: Introduction to Linux for beginners](../../content/operating-systems/linux/introduction/README.md)

    In this module

  • What is Linux
  • Installation and setup of a linux server
  • Accessing servers
  • Command line basics
  • User accounts
  • File management
  • 🎬 [Module 4: Understanding the command line](../../content/operating-systems/commandline/introduction/README.md)

    In this module

  • Benefits of using command line
  • Why Command line is important for DevOps
  • Command line structure
  • Command line examples
  • Navigating command usage
  • Command line tools
  • 🎬 [Module 5: Bash scripting for beginners](../../content/operating-systems/scripting/bash/README.md)

    In this module

  • Benefits of writing bash scripts
  • Basics of Automation
  • Introduction to bash scripting
  • Pro's and Con's of scripting
  • Write our first bash script

  • Overview (`course/chapters/chapter-3-linux-monitoring/README.md`)

    Chapter 3 : Linux Monitoring

    Back to Course Introduction

    This the previous chapters, we have spent quite a bit of time understanding operating systems and command line and why it's so important.
    Modern cloud and IT tends to hide or abstract a lot of how servers and operating systems function as well as how they are monitored.
    This means engineers often lack the knowledge and expertise on how to dismantle a system and monitor it with bare essential command line tools.

    In this chapter we'll take a look at monitoring concepts and introduce a few more command line tools that help us monitor our server.

    🎬 [Module 1: Introduction to Linux monitoring](../../content/operating-systems/linux/monitoring/README.md)

    In this module

  • Importance of Monitoring
  • Monitoring vs Observability
  • Resources to monitor: CPU, Memory, Network, Disk
  • Monitoring Tools Overview
  • Basic Monitoring Commands
  • Logs vs Metrics vs Traces & tooling
  • 🎬 [Module 2: Linux CPU Monitoring](../../content/operating-systems//linux/monitoring/cpu/README.md)

    In this module

  • Understanding CPU Metrics
  • Tools for CPU Monitoring (e.g., top, htop, mpstat)
  • Analyzing CPU Usage
  • Practical Examples
  • 🎬 [Module 3: Linux Memory Monitoring](../../content/operating-systems//linux/monitoring/memory/README.md)

    In this module

  • Understanding Memory Metrics
  • Tools for Memory Monitoring (e.g., free, vmstat)
  • Analyzing Memory Usage
  • Practical Examples
  • 🎬 [Module 4: Linux Disk Usage Monitoring](../../content/operating-systems//linux/monitoring/disk/README.md)

    In this module

  • Understanding Disk Metrics
  • Tools for Disk Monitoring (e.g., df, du, iostat)
  • Analyzing Disk Usage
  • Practical Examples
  • 🎬 [Module 5: Linux Network Monitoring](../../content/operating-systems//linux/monitoring/network/README.md)

    In this module

  • Understanding Network Metrics
  • Tools for Network Monitoring (e.g., iftop, netstat, ss)
  • Analyzing Network Usage
  • Practical Examples
  • ⌛ Module 6: Introduction to Prometheus

    In this module

  • What is Prometheus
  • Concepts of monitoring
  • Logs vs Metrics vs Traces
  • Pull vs Push approach
  • Installation
  • Monitor our Linux server
  • Grafana Preview (next module)
  • ⌛ Module 7: Introduction to Grafana

    In this module

  • What is Grafana
  • Data sources and concepts
  • Installation & Setup
  • Basic queries
  • Installing node-exporter dashboard

  • Overview (`course/chapters/chapter-4-web-and-http/README.md`)

    Chapter 4 : The Web & HTTP

    Back to Course Introduction

    Web servers are an importand foundation of the IT ecosystem.
    The internet is fundamentally made up of web servers.
    In this chapter we'll learn about the basics of web servers and get our first web server up and running.

    🎬 [Module 1: Introduction to Web](../../content/web/README.md)

    In this module

  • What is the Web
  • What is a web server
  • Run a very basic web server
  • Basics of Web
  • text
    <li> Client & Server </li>
      <li> HTTP  </li>
      <li> DNS </li>
      <li> Status codes </li>
      <li> Request methods </li>
      <li> TCP\UDP and Ports </li>
      <li> Network testing tools </li>

    🎬 [Module 2: The Basics of HTTP](../../content/web/http/README.md)

    In this module

  • What is the HTTP protocol
  • Roles of clients and servers
  • Understanding requests and responses
  • Interpreting responses & status codes
  • 🎬 [Module 3: The Basics of HTML Websites and Web Services or APIs ](../../content/web/html/README.md)

    DevOps engineers are often tasked to troubleshoot and assist developers who may have Web applications deployed.
    Perhaps there are network failures, resources may not be correctly loading or incorrectly deployed.

    In this chapter we will learn about the basics of HTML and web assets, how they render and how to use developer tools to troubleshoot networking and other issues with Web applications. It's a high level module designed to give you a glimpse in the shoes of a UI or front-end developer

    In this module

  • HTML files, syntax and usage
  • Basics of Stylesheets & Scripts
  • Troubleshooting HTML in Browsers
  • Understanding different HTTP responses: API vs WEB
  • Monitoring and Debugging
  • 🎬 [Module 4: Creating web servers with NGINX](../../content/web/servers/README.md)

    In this module

  • Web Server overview
  • Installing NGINX Web server
  • Understand the NGINX configuration
  • Setup a basic web site with a few pages
  • Make our website public
  • Understanding SSL\TLS security for Web servers
  • 🎬[Module 5: HTTP Troubleshooting](../../content/web/http/troubleshooting/README.md)

    In this module

  • Troubleshooting client and servers
  • Understanding HTTP status codes and meaning of errors
  • Interpret & Understand web server error and access logs
  • Tools
  • 🚧🎬 [Module 6: The Basics of DNS & Domains](../../content/web/dns/README.md)

    In this module

  • What are domains
  • What are host names
  • What is DNS
  • DNS tooling
  • Types of DNS
  • Importance of TLS/SSL for secure communication
  • 🚧🎬 [Module 7: The Basics of SSL & TLS](../../content/web/tls/README.md)

    In this module

  • What is SSL & TLS
  • HTTPS connections
  • Importance of encryption on the Web
  • TLS Tools
  • Setting up TLS in Web servers for HTTPS
  • ⏳(Coming Soon)🎬 Module 7: Creating our own personal website

    During this entire course we will touch on a wide spectrum of DevOps tooling, concepts and technology.
    The outcome of this course is to have real-world hands-on experience with all these topics, all the way from setup, scripting, coding to building, CI/CD and deployment including monitoring and observability.

    In this module

  • Put together a DevOps CV personal website using HTML, CSS and Javascript
  • Understanding website deployment and changes
  • ⏳(Coming Soon)🎬 Module 8: Get free SSL certificates

    To make our website public on the web, we should introduce a domain and SSL certificate

    In this module

  • Setup domain for your website
  • Understanding SSL\TLS
  • Introduction to Let's encrypt

  • Overview (`course/chapters/chapter-4.1-web-server-monitoring/README.md`)

    Chapter 4.1 : Web server: Monitoring

    Back to Course Introduction

    In our chapter on monitoring, we've covered some basics around dashboards and metrics.
    In this chapter we will expand on that to include application logs and metrics that are crucial in understanding our web server application health

    We'll cover the key concepts of log collection and visualisation of those logs using various open source tools

    🎬 Module 1: Introduction to Fluentd

    In this module

    • What is Fluentd
    • Benefits of using a log collector
    • Benefits of sending logs to external systems
    • Installation and Setup
    • Capture using configuration
    • Plugins to send logs externally

    🎬 Module 2: Monitoring Logs with Grafana Loki

    In this module

    • What is Grafana Loki
    • Log capture from external sources
    • Get our application logs into Grafana Loki
    • Visualise our web server logs in Grafana

    Overview (`course/chapters/chapter-5-ci-cd/README.md`)

    Chapter 5 : CI/CD & Automation

    Back to Course Introduction

    In previous chapters we've setup our own web server and website.

    🎬 [Module 1: Introduction to CI/CD](../../content/automation/cicd/introduction/README.md)

    In this module

  • What is Continuous Integration (CI)
  • What is Continuous Delivery & Deployments (CD)
  • CI/CD phases from A to Z
  • Popular CI/CD Tools
  • 🎬 [Module 2: Introduction to Automation](../../content/automation/introduction/README.md)

    In this module

  • What is automation
  • Our automation journey so far
  • Key Focusses of Automation:
  • text
    <li> Scripting & Coding </li>
      <li> CI/CD: Automated build & deployments </li>
      <li> Infrastructure as Code </li>
      <li> Configuration Management </li>
      <li> GitOps </li>

    🎬 [Module 3: CI Example: Creating a CI pipeline with Github Actions](../../content/automation/cicd/ci/examples/github/README.md)

    In this module

  • Continuous integration overview
  • Networking challenges & Security concerns
  • Architecture review
  • Scripting: Automate CI system install
  • Github Actions workflow file overview
  • Building and packaging software (artifacts)
  • Create a CI pipeline using Github Actions
  • 🎬 [Module 4: CD Example: Creating a CD pipeline with Github Actions](../../content/automation/cicd/cd/examples/github/README.md)

    In this module

  • Continuous delivery\deployment overview
  • Github Actions workflow file review
  • Graceful and Rolling deployments
  • Create CD steps in our CICD pipeline using Github Actions
  • 🎬 [Module 5: Scripting Example: Automate our Infrastructure ](../../content/automation/scripting/examples/README.md)

    In this module

  • Scripting Requirements
  • Scripting our Server Infrastructure
  • Using the Command line
  • Process of building a script: iteration and testing
  • Challenges of Scripting
  • 🎬 [Module 6: Declarative Tools Example: Automate our Infrastructure (Vagrant)](../../content/automation/config-management/examples/vagrant/README.md)

    In this module

  • Declarative vs Imperative Automation
  • Desired State
  • Infrastructure as Code
  • Declarative IaC with Vagrant
  • 🎬 [Module 7: Configuration Management Example: Automate our Infrastructure (Ansible)](../../content/automation/config-management/examples/ansible/README.md)

    In this module

  • What is Configuration Management
  • Challenges with Scripting & Idempotence
  • Declarative configuration
  • Introduction to Ansible
  • Migrating scripts to declarative configuration

  • Overview (`course/chapters/chapter-6-docker/README.md`)

    Chapter 6 : Docker & Linux Containers

    Back to Course Introduction

    🎬 [Module 1: Introduction to Linux containers](../../content/containers/introduction/README.md)

    In this module

  • What are containers
  • Processes vs Containers
  • How processes work inside containers
  • What problems do they solve
  • Virtual Machines vs Containers
  • 🎬 [Module 2: Running and Configuring Containers](../../content/containers/running/README.md)

    In this module

  • The Container runtime
  • How containers & processes get inputs
  • Popular container runtimes
  • Installing a container runtime
  • Running our first container
  • 🎬 [Module 3: Building Container Images](../../content/containers/building/README.md)

    In this module

  • What are container images
  • Filesystem layers
  • Writing our first `Dockerfile`
  • Common `Dockerfile` instructions
  • Layer caching and optimizing builds
  • Tagging and pushing images
  • Running a custom built container
  • 🎬 [Module 4: CI/CD Pipelines for Containers](../../content/containers/cicd/README.md)

    In this module

  • CI/CD fundamentals overview
  • Learn about container lifecycle
  • Building containers
  • Publishing containers
  • Deploying containers
  • 🎬 [Module 5: Container Networking](../../content/containers/networking/README.md)

    In this module

  • Default container networks
  • Creating custom networks
  • Secure container communication
  • Internal DNS and service discovery
  • 🎬 [Module 6: Introduction to Container Orchestration](../../content/containers/orchestration/README.md)

    In this module

  • Limitations of standalone containers
  • Declarative vs Imperative management
  • Desired State
  • Networking and Service Discovery challenges
  • Overview of orchestration technologies

  • Overview (`course/content/web/README.md`)

    🎬 Introduction to the Web

    💡 Preface

    This module is part of a course on DevOps.
    Checkout the course introduction for more information
    This module is part of chapter 4

    This module draws from my extensive experience in the world of Web. Building and managing Web servers, building web sites, web services, monitoring , security, networking etc.

    It focuses on my personal approach to understanding the Web as well as how I troubleshoot.

    What is the Web

    The Web is just a bunch of computers and devices that are connected to a network. This is one giant network made up of public and private networks

    In our module on servers and virtualization, we created a small private network and we learned that this network is connected to the internet via our gateway (which in my case, was my home router)

    In our module on Network monitoring, we covered all of this in a diagram. I will link it here for a recap

    The internet is technically private networks that are connected by public networks, similar to our diagram below

    We'll need to ensure we understand the diagram, because the network components form the core architecture of the internet.

    Understanding the big picture

    This entire chapter will break down the following diagram and deep dive into each component. It is critically important as an engineer to understand these fundamentals as it plays a role in building , testing , troubleshooting and working with almost every aspect of DevOps
    This diagram puts together all important aspects and components that play a part in Web and HTTP

    What is HTTP

    In our previous Chapters on servers and network monitoring, we covered the first diagram showcasing TCP connections between a client process and a server process.
    We learned that TCP is the lowest level and smallest form of network connection.
    We learned that a TCP connection goes through various states and we even saw this in a hands-on practical using tools like netstat and ss to monitor network connections
    TCP connections allow clients and servers to transport network packets between each other. This is how "data exchange" happens.

    Now in the world of Web, we need a standard of how these network packets need to be structured, so that devices on the web can communicate effectively and securely

    Just like human languages have structures, rules and standards which allow us to communicate in ways we can all understand.

    When you type a website address into your browser, your browser creates a TCP packet in a "special format" that a web server can accept, understand and it can respond with requested content which your browser can then understand and display to you.

    This "special format" of a TCP packet is called "HTTP"
    So in a nutshell, an HTTP packet is just a TCP packet, but it has more information that has to conform to certain standards

    In order to create an HTTP packet, we can use scripts, tools or programming languages. But before we go there, lets understand more overall components

    Note: We will have an entire module dedicated to HTTP

    Client & Server

    What is a Client

    Zooming into our above diagram on the Web, a client is a process that initiates a TCP connection. In the world of Web, we talk about HTTP.

    An HTTP client will create an HTTP packet that conforms to the HTTP specification. This HTTP packet is referred to as an HTTP Request.

    An HTTP request has information and structure that a "Server" will understand.

    What is a Server

    Looking at our diagram once again, a server is a process that accepts a TCP connection. In the world of Web, we talk about HTTP

    An HTTP server will listen on a port and accept a connection. It understands HTTP and will generally only respond to TCP network requests that conforms to HTTP specifications

    An HTTP server (or a Web server) will generally respond with an HTTP response

    In our diagram above, we can visualize the incoming connection to a server and see the HTTP request as well as the HTTP response.

    When it comes to development of HTTP based services like microservices, it is crucial to understand HTTP requests and responses. DevOps engineers spend a lot of time analyzing HTTP traffic to understand performance, health , and security of an overall system.

    The HTTP requests and responses make up a large portion of monitoring, so understanding these will help you make sense of web server logs to understand what is happening between clients and servers in your environment.

    HTTP Requests & Responses

    HTTP Requests

    In our networking diagram, we can visualize a pure raw TCP connection flow. We can see that in order to satisfy the connection, a TCP packet would need header details such as source and destination IP addresses & ports to know where to connect to.

    In order for clients and servers to send each other "data" this data forms part of the TCP payload which is just arbitrary raw binary data.

    An HTTP request is simply extra metadata that is sent in a TCP network packet as part of the TCP payload.

    In the world of Web, we require extra fields, for example a web browser needs a URL of the website you need to access.
    It needs to know if the website supports HTTPS or HTTP. When a user needs to log into a website, the browser needs to pass this login information to the server.
    As users interact with websites, data is passed back and forth between the browser (acting as client) and a web server (acting as server).

    These all form part of the "metadata" I described, which needs to be added to a TCP network packet. Remember that under the hood of all this, there are still pure raw TCP connections.

    Let's refer back to the diagram, and I will briefly walk through these fields that are required and supported by HTTP

    HTTP Responses

    A client is responsible for creating the HTTP request. Once sent and received by a server, the server will process it and generally respond with an HTTP response.
    An HTTP response also has certain fields that it both requires and supports.

    Let's refer back to the diagram, and I will briefly walk through these fields that are required and supported by the HTTP response

    As this is just an overview module, we will deep dive this protocol further in a later module.

    mozilla.org is a great resource for exploring the world of HTTP and its specifications.

    Creating an HTTP request

    I use curl on a daily basis to test and troubleshoot web servers. It's essential for every DevOps, SRE, Platform or Cloud engineer to master.
    The curl and wget tools are available in almost all Linux distributions and may even be available on many of your production servers.
    I often use curl on production servers to test if web servers are working correctly or if they are responding with the correct HTTP responses.

    We can make a simple HTTP request with curl like so:

    text
    curl https://marceldempers.dev

    We can pass -v to indicate curl to give us more verbose data on the HTTP request and response. Doing so allows us to see request and response headers and other fields we talked about in HTTP:

    text
    curl -v https://marceldempers.dev

    We can construct a HTTP request ourselves by passing some of the fields we talked about

    text
    curl -X GET  \
    -H "Host: marceldempers.dev" \
     https://marceldempers.dev

    Troubleshooting HTTP requests and responses

    1. Command Line

    We can use curl alongside -v to troubleshoot HTTP requests responses using the command line.

    1. Browser

    It's also important to get used to doing a similar thing in the browser. Browsers support developer tools which have network tooling built-in. We can use this to inspect HTTP requests and HTTP responses for websites to analyze web traffic

    When analyzing HTTP traffic , it's important to pay attention to HTTP responses and understand HTTP status codes in depth. In my experience this is the simplest field with the largest oversight when troubleshooting HTTP traffic.
    It's important to study and understand the meaning of each HTTP status code.

    1. Logs

    We can also gather HTTP details like URLS, Status Codes, IP addresses, Latency details etc from Web server logs.
    All web servers have configuration we can enable that tells the server to output HTTP request and response logs.

    We will use steps 1 - 3 extensively when troubleshooting HTTP requests and responses.
    We'll further explore web monitoring in more detail in a future module.

    What is DNS

    Looking back at our network diagram, notice that I only have IP addresses for client to server communication.
    This is because TCP uses only source and destination IP addresses and fundamentally uses IP's to forward traffic.

    For HTTP, you may have noticed we have an HTTP header in our request that indicates Host: marceldempers.dev and there is no IP address there.
    It is up to the client to resolve the host name and work out what the IP address is, so that the client can construct a TCP packet.

    This means the client takes the URL portion which is the domain, and performs a DNS lookup to get the IP address.

    We can do this ourselves with command line tools like nslookup or dig.

    Note: dig needs to be installed with sudo apt-get install -y dnsutils

    text
    nslookup google.com

    DNS servers will resolve domains that we purchase, to IP addresses.
    Important to know that IP addresses can be dynamically issued by your Internet service provider, so your public IP address of your server could change depending on where you host it.

    All cloud providers generally provide static IP addresses which do not change. This helps us point our DNS names to these static IP addresses when running web servers.
    Hosting a web server on a public IP address that is dynamic is a little more complicated, but it is possible.

    We will further explore the world of Domains, DNS and SSL\TLS in an upcoming module

    What is a web server

    A Web server is a type of server that serves HTTP requests and produces HTTP responses.
    Web servers listen on ports for incoming TCP connections, just as illustrated in our above network and web diagrams.
    Web servers basically serve files, which can be made up of different types of content from text files, to HTML, javascript, CSS stylesheets and more. A web server could host a website or web application such as a microservice.

    Examples of Web servers

    Popular Web servers that I have used:

    • NGINX
    • Apache
    • Caddy
    • IIS (for Microsoft Windows)

    Popular programming languages also allow you to write your own or host web application code on the programming language runtime. <//br>

    In this course, we will explore the most popular web servers, but that will require its own module to cover it. For simplicity of this module, I have written a small standalone web server we can run.

    Important Note: Please note that links to these files can change without notice and may be different from what you read on screen in the video at the time of recording. You can always click the hyperlink in the document to get the latest path

    We use cd to change directory to the above web server and run it:

    text
    ./server

    Web server logs

    Web servers have the capability to write logs. Logs are basically just a sequence of events and diagnostic information that helps with troubleshooting and analysis.
    Web servers can have two types of logs.

    • Error logs: These logs are errors thrown by the web server. This could indicate problems with the web server or with its configuration
    • Access logs: These logs contain a combination of important HTTP request and response fields, such as the time the request came in, the IP of the client making the request, the URL and PATH of the request, its response status code and other important data.
      Each line in the access log represent important fields of a request and response

    I've mentioned in previous modules that in Linux, our commands write output into stdout which is a Standard Stream in Linux that writes output to a terminal that we can view.

    Let's send some HTTP requests to our web server using curl:

    text
    curl -X GET http://localhost:8080/home.html
    curl -X GET http://localhost:8080/home-notexist.html

    Now in the terminal where we started the web server, we can see our logs being output to stdout.
    Let's analyze the HTTP logs:

    text
    ./server 
    2025/04/01 14:20:50 Starting server on :8080
    2025/04/01 14:21:00 GET /home.html 127.0.0.1:37836 HTTP/1.1 200
    2025/04/01 14:21:05 GET /home-notexist.html 127.0.0.1:37840 HTTP/1.1 404

    We can also open our page in the browser and see how the browser requests additional content like the .js and .css and .ico files.
    This is because the browser will load the home.html page and try to render it.
    While rendering, if we take a look at the home/html file, we will see .css and .js script file references. The browser knows to get these and use them to apply styles and rendering to the site.
    This is why we see these additional requests:

    text
    ./server 
    2025/04/01 16:19:44 Starting server on :8080
    2025/04/01 16:19:54 GET /home.html [::1]:54736 HTTP/1.1 200
    2025/04/01 16:19:54 GET /style.css [::1]:54736 HTTP/1.1 200
    2025/04/01 16:19:54 GET /script.js [::1]:54736 HTTP/1.1 200
    2025/04/01 16:19:54 GET /favicon.ico [::1]:54736 HTTP/1.1 404

    Notice that when refreshing, we may stop seeing the .js and .css request. This is because the browser may cache these resources to prevent making repeat requests to the server

    We'll learn more about HTML, JS, and CSS in an upcoming module


    Overview (`github/actions/self-hosted-runner/README.md`)

    Introduction to GitHub Actions: Self hosted runners

    Create a kubernetes cluster

    In this guide we we''ll need a Kubernetes cluster for testing. Let's create one using kind

    text
    kind create cluster --name githubactions --image kindest/node:v1.28.0@sha256:b7a4cad12c197af3ba43202d3efe03246b3f0793f162afb40a33c923952d5b31

    Let's test our cluster:

    text
    kubectl get nodes
    NAME                          STATUS   ROLES           AGE     VERSION
    githubactions-control-plane   Ready    control-plane   2m53s   v1.28.0

    Running the Runner in Docker

    We can simply install this directly on to virtual machines , but for this demo, I'd like to run it in Kubernetes inside a container.

    Security notes

    • Running in Docker needs high priviledges.
    • Would not recommend to use these on public repositories.
    • Would recommend to always run your CI systems in seperate Kubernetes clusters.

    Creating a Dockerfile

    • Installing Docker CLI
      For this to work we need a dockerfile and follow instructions to Install Docker.
      I would grab the content and create statements for my dockerfile

    Now notice that we only install the docker CLI.
    This is because we want our running to be able to run docker commands , but the actual docker server runs elsewhere
    This gives you flexibility to tighten security by running docker on the host itself and potentially run the container runtime in a non-root environment

    • Installing Github Actions Runner

    Next up we will need to install the GitHub actions runner in our dockerfile
    Now to give you a "behind the scenes" of how I usually build my dockerfiles, I run a container to test my installs:

    text
    docker build . -t github-runner:latest 
    docker run -it github-runner bash

    Next steps:

    • Now we can see docker is installed
    • To see how a runner is installed, lets go to our repo | runner and click "New self-hosted runner"
    • Try these steps in the container
    • We will needfew dependencies
    • We download the runner

    Finally lets test the runner in docker

    text
    docker run -it -e GITHUB_PERSONAL_TOKEN="" -e GITHUB_OWNER=marcel-dempers -e GITHUB_REPOSITORY=docker-development-youtube-series github-runner

    Deploy to Kubernetes

    Load our github runner image so we dont need to push it to a registry:

    text
    kind load docker-image github-runner:latest --name githubactions

    Create a kubernetes secret with our github details

    text
    kubectl create ns github
    kubectl -n github create secret generic github-secret `
      --from-literal GITHUB_OWNER=marcel-dempers `
      --from-literal GITHUB_REPOSITORY=docker-development-youtube-series `
      --from-literal GITHUB_PERSONAL_TOKEN=""
    
    kubectl -n github apply -f kubernetes.yaml

    Overview (`golang/introduction/part-2.json/readme.md`)

    Introduction to Go: JSON

    In programming languages, you will very often deal with data structures internally.
    Sometimes, you need to pass data outside of your application or read data from another application, or even a file.

    API, for example often expose data in json, xml, grpc and all sorts of formats.
    Before we can really take a look at building APIs and even storing data in databases,
    we need some fundamental knowledge about how to convert these data structures, into structures
    that our application can understand.

    In part 1, we dealt with Variables and more importantly, we dealt with struct data type, which made us build a customer object.

    If we wanted to pass the customer to a database, or an external system, it's often required that we convert this to json format.

    Dev Environment

    The same as Part 1, we start with a dockerfile where we declare our version of go.

    text
    cd golang\introduction\part-2.json
    
    docker build --target dev . -t go
    docker run -it -v ${PWD}:/work go sh
    go version

    Create our App

    Create a new directory that holds defines our repository and holds our module

    text
    mkdir videos
    • Define a module path
    text
    # change directory to your application source code
    
    cd videos
    
    # create a go module file
    
    go mod init videos

    Create our base code

    We start out all our applications with a main.go defining our package, declaring our import dependencies
    and our entrypoint main() function

    text
    package main
    
    import "fmt"
    
    func main() {
    	fmt.Println("Hello, world.")
    }

    Create our Videos app

    Firstly, we create a seperate code file videos.go that deals with our YouTube videos

    Similar to Part 1, we define the file, we define what a video looks like using a struct
    and a function for returning a slice of videos.

    text
    package main
    
    import ()
    
    type video struct {
    	Id string  
    	Title string  
    	Description string 
    	Imageurl string 
    	Url string 
    }
    
    func getVideos()(videos []video){
    	//Get our videos here,
    	//and return them
    	return videos
    }

    Populating our Video Struct

    Similar to our customers app, we created a struct and populated it with data:
    Let's add the following to our getVideos() function:

    text
    video1 := video{
      Id : "eyvLwK5C2dw",
      Title : "Kubernetes on Azure",
      Imageurl : "https://i.ytimg.com/vi/eyvLwK5C2dw/mqdefault.jpg?sqp=CISC_PoF&rs=AOn4CLDo7kizrJozB0pxBhxL9JbyiW_EPw",
      Url : "https://youtu.be/eyvLwK5C2dw",
      Description : "",
    }
    
    video2 := video{
      Id : "QThadS3Soig",
      Title : "Kubernetes on Amazon",
      Imageurl : "https://i.ytimg.com/vi/QThadS3Soig/sddefault.jpg",
      Url : "https://youtu.be/QThadS3Soig",
      Description : "",
    }
    
    return []video{ video1, video2}

    We need to also invoke our function that will return the videos
    Don't worry about where the videos will come from.
    We always start with the building blocks and move on from there

    In our main() function:

    text
    func main() {
    	videos := getVideos()
    	fmt.Println(videos)
    }

    Files

    Now, Ideally, we do not want to "hard code" our videos like this.
    Currently, our videos are embedded into the code and we can only return 2 videos.
    If we want to introduce a new video, we have to rebuild the application.

    In the real world, videos are our data.
    Data lives outside of the application. Like in a database.

    Technically, we can use a database, but that is too big of a step for now.
    So let's start with a file instead.

    Introducing ioutil

    It's important to learn how to navigate and read go documentation.
    Let's go to https://golang.org/pkg/io/ioutil/

    We can import this library since its part of the Go standard library set.

    text
    import (
    	"io/ioutil"
    )

    Let's move our videos into a json file called videos.json :

    text
    [
      {
    		"id" : "QThadS3Soig",
    		"title" : "Kubernetes on Amazon",
    		"imageurl" : "https://i.ytimg.com/vi/QThadS3Soig/sddefault.jpg",
    		"url" : "https://youtu.be/QThadS3Soig",
    		"description" : ""
    	},
      {
    		"id" : "eyvLwK5C2dw",
    		"title" : "Kubernetes on Azure",
    		"imageurl" : "https://i.ytimg.com/vi/eyvLwK5C2dw/mqdefault.jpg?sqp=CISC_PoF&rs=AOn4CLDo7kizrJozB0pxBhxL9JbyiW_EPw",
    		"url" : "https://youtu.be/eyvLwK5C2dw",
    		"description" : ""
    	}
    ]

    Reading a file

    Let's take another look at https://golang.org/pkg/io/ioutil/
    Notice the ReadFile function
    We can read a file from disk:

    text
    fileBytes, err := ioutil.ReadFile("./videos.json")
    
    	if err != nil {
    		panic(err)
    	}
    
      fileContent := string(fileBytes)
      fmt.Println(fileContent)

    Panic

    Also notice the function panic.

    "A panic typically means something went unexpectedly wrong. Mostly we use it to fail fast on errors that shouldn’t occur during normal operation, or that we aren’t prepared to handle gracefully."

    For now, we will panic on every potential error.
    In a future video, we'll cover Error handling in more depth

    JSON

    Working with JSON is pretty straightforward in go.
    struct objects are pretty well synergized with json

    Let's take a look at the json package that is part of go.
    https://golang.org/pkg/encoding/json/

    text
    "encoding/json"

    This package allows us to marshal and unmarshal JSON.
    This allows conversion between a struct or a go type, and json

    Unmarshal

    From JSON(bytes) ==> Struct\Go type

    text
    err = json.Unmarshal(fileBytes, &videos)
    
    	if err != nil {
    		panic(err)
    	}
    
    	return videos

    Using our Data

    Now in the real world, you would use your data to do something.
    Let's say we'd like to update some common terms and conditions to the video description

    In our main function, let's loop the videos & update the description.
    Feel free to checkout Part 1 of the series on loops!

    text
    for _, video := range videos {
      }

    Now we dont want to override the description, we want to inject the terms and conditions on a new line

    text
    video.Description = video.Description + "\n Remember to Like & Subscribe!"

    Note that when we run this, it does not print our new description field
    This is because the loop range is giving us a copy of the video object.
    This means we are updating a copy, but printing out the original.
    We need to use the loop indexer and update the original object in the slice.

    text
    for i, _ := range videos {
    		videos[i].Description = videos[i].Description + "\n Remember to Like & Subscribe!"
    	}

    Run our app again and now it updates the original video!

    Marshal

    https://golang.org/pkg/encoding/json/#Marshal
    From Struct\Go type ==> JSON(bytes)

    Let's create a new function for saving our video back to file

    text
    func saveVideos(videos []video)(){
    
      videoBytes, err  := json.Marshal(videos)
      if err != nil {
    		panic(err)
    	}
    
    }

    Writing to File

    https://golang.org/pkg/io/ioutil/

    text
    err = ioutil.WriteFile("./videos-updated.json", videoBytes, 0644)
      if err != nil {
    		panic(err)
    	}

    Then in our main function we can save our videos after the update:

    text
    saveVideos(videos)

    Mapping fields

    In our example our json and our struct fields match exactly.
    Sometimes this is not possible to maintain.
    Sometimes we need to tell go, to map certain json fields to certain fields in our struct
    We can do it like so:

    text
    type video struct {
    	Id string `json:"id"`
    	Title string `json:"title"`
    	Description string `json:"description"`
    	Imageurl string `json:"imageurl"`
    	Url string `json:"url"`
    }

    Overview (`golang/introduction/part-3.http/readme.md`)

    Introduction to Go: HTTP

    HTTP is a fundamental part of Microservices and Web distributed systems

    Go has a built in HTTP web server package. The package can be found here
    We simply have to import the http package:

    text
    import (
      "net/http"
    )

    In part 1, we covered the fundamentals of writing basic Go
    In part 2, we've learn how to use basic data structures like json so we can send\receive data.

    We'll be combining both these techniques so we can serve our videos data over a web endpoint.

    As always, let's start with our dockerfile , main.go and videos.go we created in Part 2

    Dev Environment

    The same as Part 1+2, we start with a dockerfile where we declare our version of go.

    text
    cd golang\introduction\part-3.http
    
    docker build --target dev . -t go
    docker run -it -v ${PWD}:/work go sh
    go version

    Create our App

    Create a new directory that holds defines our repository and holds our module

    text
    mkdir videos
    • Define a module path
    text
    # change directory to your application source code
    
    cd videos
    
    # create a go module file
    
    go mod init videos

    Create our base code

    We start out all our applications with a main.go defining our package, declaring our import dependencies
    and our entrypoint main() function

    text
    package main
    
    import "fmt"
    
    func main() {
    	fmt.Println("Hello, world.")
    }

    Create our Videos app

    Firstly, we create a seperate code file videos.go that deals with our YouTube videos

    The videos.go file defines what a video struct looks like, a getVideos() function to retrieve
    videos list as a slice and a saveVideos() function to save videos to a file locally.

    Let's copy the following content from Part 2 and create videos.go :

    We want videos.go to be part of package main:

    text
    package main

    We import 2 packages, 1 for reading and writing files, and another for dealing with json

    text
    import (
    	"io/ioutil"
    	"encoding/json"
    )

    Then we define what a video struct looks like:

    text
    type video struct {
    	Id string  
    	Title string  
    	Description string 
    	Imageurl string 
    	Url string 
    }

    We have a function for retrieving video objects as a list of type slice :

    text
    func getVideos()(videos []video){
    	
    	fileBytes, err := ioutil.ReadFile("./videos.json")
    
    	if err != nil {
    		panic(err)
    	}
    
    	err = json.Unmarshal(fileBytes, &videos)
    
    	if err != nil {
    		panic(err)
    	}
    
    	return videos
    }

    We also need to copy our videos.json file which contains our video data.

    And finally, we have a function that accepts a list of type slice and stores the videos to a local file

    text
    func saveVideos(videos []video)(){
    
      videoBytes, err  := json.Marshal(videos)
      if err != nil {
    		panic(err)
    	}
    
    	err = ioutil.WriteFile("./videos-updated.json", videoBytes, 0644)
      if err != nil {
    		panic(err)
    	}
    
    }

    HTTP Package

    https://golang.org/pkg/net/http/

    The HTTP package allows us to implement an HTTP client and a server.
    A client is a component that makes HTTP calls.
    A server is a component that receives or serves HTTP.

    The HTTP package is capable of sending HTTP requests as well as defining a server
    for receiving HTTP requests.

    We can use this to run an HTTP server to serve files, or serve data, like an API.

    Let's define a server in main.go :

    text
    # just one line :)
    
    http.ListenAndServe(":8080", nil)
    
    # ListenAndServe starts an HTTP server with a given address and handler. 
    # The handler is usually nil, which means to use DefaultServeMux. 
    # Handle and HandleFunc add handlers to DefaultServeMux

    Now before we run this, since we're running in Docker, we want to exit the container
    and rerun it, but this time open port 8080

    text
    docker run -it -p 8080:8080 -v ${PWD}:/work go sh
    cd videos
    
    go run main.go 
    # you will notice the application pausing

    We should see our server with a 404 on http://localhost:8080/

    Handle HTTP requests

    In order to handle requests, we can tell the HTTP service that we want it to run a function
    for the request coming in.

    We can see the http package has a HandleFunc function: https://golang.org/pkg/net/http/

    To see this in action, lets create a Hello() function:

    text
    func Hello(){
    }

    And tell our http service to run it:

    text
    http.HandleFunc("/", Hello)

    We cannot run this yet. As per http documentation, our Hello function needs to take in some inputs.
    func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

    Therefore we need to add inputs to our function:

    text
    func Hello(w http.ResponseWriter, r *http.Request){
    }

    This allows us to get the request, its body, headers and a write where we can send a response.
    Run this in the browser and you will notice the 404 goes away, but we now get an empty response.

    HTTP Response

    Let's write a reponse to the incoming request.
    The response write has a Write() function that takes a bunch of bytes.
    We can convert string to bytes by casting a string to a []byte like:
    []byte("Hello!"). Let's convert it and write "Hello" to the response:

    text
    w.Write([]byte("Hello!"))

    IF we run this code, we can see "Hello!" in the response body

    HTTP Headers

    Headers play an important role in HTTP communication.
    Lets access all the headers of the incoming request!
    If we look at the Header definition here, we can see how to access it.
    Let's use the for loop we learnt in part 1

    text
    for i, value := range r.Header {
    }

    We learn't from our loop, we have in indexer and a value.
    For i, we can rename it to header since it represents the header key in the dictionary.
    And the value is the value of type []string, containing the value of the header:

    text
    for header, value := range r.Header {
      fmt.Printf("Key: %v \t Value: %v \n", header, value)
    }

    We can use fmt to print out the values and look at the headers.

    We can also set headers on our response.
    If we take a look at the http docs, we can see header is also a dictionary or strings.

    text
    w.Header().Add("TestHeader", "TestValue")

    You can now see the headers in the response value if you use curl or your browser development tools

    HTTP Methods | GET

    Web servers can serve data in a number of ways and support multiple type of HTTP methods.
    GET is used to request data from a specified resource.
    So far, our HTTP route for our Hello function is using the GET method.

    Let's make our GET method more useful by serving our video data
    Let's rename our Hello() function to HandleGetVideos().
    Our / route will return all videos:

    text
    videos := getVideos()

    In part 2 we covered JSON.
    We need to convert our video slice of struct's to JSON in order to return it to the client.

    For this we learnt about the Marshall function:

    Import the JSON package:

    text
    "encoding/json"

    Convert our videos to JSON :

    text
    videoBytes, err  := json.Marshal(videos)
    
    if err != nil {
      panic(err)
    }
    
    w.Write(videoBytes)

    If we run this code and hit our / endpoint, we can now see JSON data being returned.
    This is a core part of building an API in Go.

    HTTP Methods | POST

    A POST method is used to send data to a server to create/update a resource.
    Since we built a saveVideos function, lets use that so a client can update videos!

    We need to define a new route, we can all it /update :

    text
    http.HandleFunc("/update", HandleUpdateVideos)

    And we need to define an HandleUpdateVideos() function:

    text
    func HandleUpdateVideos(w http.ResponseWriter, r *http.Request){
    }

    Let's validate the request method to ensure its POST
    We need to also ensure we send a status code to inform the user of method not allowed.

    https://golang.org/pkg/net/http/#ResponseWriter

    text
    if r.Method == "POST" {
    		//update our videos here!
    	} else {
    	  w.WriteHeader(405)
    		fmt.Fprintf(w, "Method not Supported!")
    	}

    Now we need to accept JSON from the POST request body
    https://golang.org/pkg/net/http/#Request

    In the docs above, we can see the request Body is of type Body io.ReadCloser
    To read that, we can use the ioutil package
    https://golang.org/pkg/io/ioutil/

    text
    import "io/ioutil"

    Then we can read the body into a slice of bytes:

    text
    body, err := ioutil.ReadAll(r.Body)
    
    if err != nil {
      panic(err)
    }

    Now that we have the body in a []byte, we need to use our knowledge from part 2 where we
    convert []byte to a slice of video items.

    text
    var videos []video
    err = json.Unmarshal(body, &videos)
    if err != nil {
      panic(err)
    }

    Creating our video objects allows us to do some validation if we wanted to.
    We can ensure the request body adheres to our API contract for this videos API.
    So instead of calling panic, lets return a 400 Bad request status code if we cannot
    Unmarshal the JSON data. This might help with some basic validation.

    text
    w.WriteHeader(400)
    fmt.Fprintf(w, "Bad request")

    And Finally, let's update our videos file! :

    text
    saveVideos(videos)

    Build our Docker container

    Let's uncomment all the build lines in the dockerfile
    Full dockerfile :

    text
    FROM golang:1.15-alpine as dev
    
    WORKDIR /work
    
    FROM golang:1.15-alpine as build
    
    WORKDIR /videos
    COPY ./videos/* /videos/
    RUN go build -o videos
    
    
    FROM alpine as runtime 
    COPY --from=build /videos/videos /
    COPY ./videos/videos.json /
    CMD ./videos

    Build :

    text
    cd golang\introduction\part-3.http
    docker build . -t videos

    Run :

    text
    docker run -it -p 8080:8080 videos

    Things to know

    • SSL for secure web connection
    • Authentication
    • Good API validation
    • Support a backwards compatible contract (Inputs remain consistent)

    Overview (`golang/introduction/part-4.commandline/readme.md`)

    Introduction to Go: Command Line

    Command line apps are a fundamental part of software development

    Go has a built in Commandline parser package. The package can be found here
    We simply have to import the flag package:

    text
    import (
      "flag"
    )

    In part 1, we covered the fundamentals of writing basic Go
    In part 2, we've learn how to use basic data structures like json so we can send\receive data.
    Part 3 was about exposing data via a Web server.

    We'll be combining these techniques so we can serve our videos data over a commandline application.

    As always, let's start with our dockerfile , main.go and videos.go we created in Part 2

    Dev Environment

    The same as Part 1+2+3, we start with a dockerfile where we declare our version of go.

    text
    cd golang\introduction\part-4.commandline
    
    docker build --target dev . -t go
    docker run -it -v ${PWD}:/work go sh
    go version

    Create our App

    Create a new directory that holds defines our repository and holds our module

    text
    mkdir videos
    cd videos
    • Define a module path
    text
    # create a go module file
    
    go mod init videos

    Create our base code

    We start out all our applications with a main.go defining our package, declaring our import dependencies
    and our entrypoint main() function

    text
    package main
    
    import "fmt"
    
    func main() {
    	fmt.Println("Hello, world.")
    }

    Create our Videos app

    Firstly, we create a seperate code file videos.go that deals with our YouTube videos

    The videos.go file defines what a video struct looks like, a getVideos() function to retrieve
    videos list as a slice and a saveVideos() function to save videos to a file locally.

    Let's copy the following content from Part 2 and create videos.go :

    We want videos.go to be part of package main:

    text
    package main

    We import 2 packages, 1 for reading and writing files, and another for dealing with json

    text
    import (
    	"io/ioutil"
    	"encoding/json"
    )

    Then we define what a video struct looks like:

    text
    type video struct {
    	Id string  
    	Title string  
    	Description string 
    	Imageurl string 
    	Url string 
    }

    We have a function for retrieving video objects as a list of type slice :

    text
    func getVideos()(videos []video){
    	
    	fileBytes, err := ioutil.ReadFile("./videos.json")
    
    	if err != nil {
    		panic(err)
    	}
    
    	err = json.Unmarshal(fileBytes, &videos)
    
    	if err != nil {
    		panic(err)
    	}
    
    	return videos
    }

    We also need to copy our videos.json file which contains our video data.

    And finally, we have a function that accepts a list of type slice and stores the videos to a local file

    text
    func saveVideos(videos []video)(){
    
      videoBytes, err  := json.Marshal(videos)
      if err != nil {
    		panic(err)
    	}
    
    	err = ioutil.WriteFile("./videos.json", videoBytes, 0644)
      if err != nil {
    		panic(err)
    	}
    
    }

    Flag Package

    https://golang.org/pkg/flag/

    Package flag implements command-line flag parsing.
    So we can run our videos app and pass it inputs like any other CLI.

    Let's build a CLI tool that users our getVideos() and saveVideos functions.

    To get all videos, perhaps we'd like a command

    text
    # get all videos
    videos get --all
    
    # get video by ID
    videos get -id <video-id>
    
    # add a video to our list
    videos add -id -title -url -imageurl -desc

    To start, we import package flag

    text
    import (
    	"flag"
    	"fmt"
    )

    Let's define our subcommands in main.go :

    text
    //'videos get' subcommand
        getCmd := flag.NewFlagSet("get", flag.ExitOnError)

    videos get command will need two inputs, --all if the user wants to return all videos, or --id if the user only wants a specific video

    text
    // inputs for `videos get` command
      getAll := getCmd.Bool("all", false, "Get all videos")
      getID := getCmd.String("id", "", "YouTube video ID")

    videos add command will take a bit more inputs to be able to add a video to our storage.

    text
    addCmd := flag.NewFlagSet("add", flag.ExitOnError)
    
      addID := addCmd.String("id", "", "YouTube video ID")
      addTitle := addCmd.String("title", "", "YouTube video Title")
      addUrl := addCmd.String("url", "", "YouTube video URL")
      addImageUrl := addCmd.String("imageurl", "", "YouTube video Image URL")
      addDesc := addCmd.String("desc", "", "YouTube video description")

    When a user runs our videos CLI tool, we may need to validate that
    our application receives the right subcommands. So lets ensure a simple validation to check if the user has passed a subcommand

    To check the arguments passed to our CLI, we use the "os" package. Check the Args variable, it holds usefull information passed to our application including its name.
    var Args []string

    We can do a simple check by ensuring the length of os.Args is atleast 2.

    We firstly need to add os to our import section
    Followed by our check:

    text
    if len(os.Args) < 2 {
            fmt.Println("expected 'get' or 'add' subcommands")
            os.Exit(1)
      }

    Handling our subcommands

    So to handle each sub command like get and add, we add a simple
    switch statement that can branch into different pathways of execution,
    based on a variables content.

    Let's take a look at this simple switch statement:

    text
    //look at the 2nd argument's value
    switch os.Args[1] {
      case "get": // if its the 'get' command
        //hande get here
      case "add": // if its the 'add' command
        //hande add here
      default: // if we don't understand the input
    }

    Let's create seperate handler functions for each sub command to keep our code tidy:

    text
    func HandleGet(getCmd *flag.FlagSet, all *bool, id *string){
    }
    
    func HandleAdd(addCmd *flag.FlagSet,id *string, title *string, url *string, imageUrl *string, description *string ){
    }

    Now that we have seperate functions for each subcommand, we can take appropriate actions in each. Let's firstly parse the command flags for each subcommand:

    This allows us to parse everything after the videos <subcommand> arguments:

    text
    getCmd.Parse(os.Args[2:])

    Input Validation

    For our HandleGet function, let's validate input to ensure its correct.

    text
    if *all == false && *id == "" {
        fmt.Print("id is required or specify --all for all videos")
        getCmd.PrintDefaults()
        os.Exit(1)
      }

    Let's handle the scenario if user passing --all flag:

    text
    if *all {
    		//return all videos
    		videos := getVideos()
    		
    		fmt.Printf("ID \t Title \t URL \t ImageURL \t Description \n")
    		for _, video := range videos {
    			fmt.Printf("%v \t %v \t %v \t %v \t %v \n",video.Id, video.Title, video.Url, video.Imageurl,video.Description)
    		}
    
    		return
    }

    Let's handle when user is searching for a video by ID

    text
    if *id != "" {
    		videos := getVideos()
    		id := *id
    		for _, video := range videos {
    			if id == video.Id {
    				fmt.Printf("ID \t Title \t URL \t ImageURL \t Description \n")
    				fmt.Printf("%v \t %v \t %v \t %v \t %v \n",video.Id, video.Title, video.Url, video.Imageurl,video.Description)
    
    			}
    		}
    }

    Parsing multiple fields

    For our HandleAdd function, we need to validate multiple inputs, create a video struct, append it to the existing video list and save it back to file

    Let's create a ValidateVideo() function with similar inputs to our HandleAdd():

    text
    func ValidateVideo(addCmd *flag.FlagSet,id *string, title *string, url *string, imageUrl *string, description *string ){
    }

    Let's simply validate all fields since they are all required:

    text
    if *id == "" || *title == "" || *url == "" || *imageUrl == "" || *description == "" {
    		fmt.Print("all fields are required for adding a video")
    		addCmd.PrintDefaults()
    		os.Exit(1)
    	}

    And we can now call this function in our add function:

    text
    ValidateVideo(addCmd, id,title,url, imageUrl, description)

    Adding our video

    Now that we have some basic validation, not perfect, but good enough to get started, let's add our video to the existing file.

    Define a video struct with the CLI input:

    text
    video := video{
    		Id: *id,
    		Title: *title,
    		Description: *description,
    		Imageurl: *imageUrl, 
    		Url: *url, 
    	}

    Get the existing videos:

    text
    videos := getVideos()

    Append our video to the list:

    text
    videos = append(videos,video)

    Save the new updated video list:

    text
    saveVideos(videos)

    Build our Docker container

    Let's uncomment all the build lines in the dockerfile
    Full dockerfile :

    text
    FROM golang:1.15-alpine as dev
    
    WORKDIR /work
    
    FROM golang:1.15-alpine as build
    
    WORKDIR /videos
    COPY ./videos/* /videos/
    RUN go build -o videos
    
    
    FROM alpine as runtime 
    COPY --from=build /videos/videos /usr/local/bin/videos
    COPY ./videos/videos.json /
    COPY run.sh /
    RUN chmod +x /run.sh
    ENTRYPOINT [ "./run.sh" ]

    For our entrypoint, we need to create a shell script to accept all the arguments:

    Let's create a script called run.sh

    text
    #!/bin/sh
    
    videos $@

    Build :

    text
    cd golang\introduction\part-4.commandline
    docker build . -t videos

    Run :

    text
    docker run -it videos get --help

    Overview (`golang/introduction/part-5.database.redis/readme.md`)

    Introduction to Go: Storing data in Redis Database

    Up until now, we've learned the fundamentals of Go and built a small web microservice that handles our video data.
    Our service has a / GET endpoint for returning all videos, as well as a simple /update endpoint for updating our list of videos.

    We've learnt how to read and write from files and learn how to work with json data.
    This is important for learning Go, however there are a few challenges for using a file as storage.

    • It can be problematic if we have more than one instance of our service writing to the same file
    • It's important to keep state out of our application, so if our application crashes, we don't lose our data

    In part 1, we covered the fundamentals of writing basic Go
    In part 2, we've learnt how to use basic data structures like json so we can send\receive data.
    In part 3, we've learnt how to write a HTTP service to expose our videos data

    Start up a Redis Cluster

    Follow my Redis clustering Tutorial

    Code is over here

    Go Dev Environment

    The same as Part 1+2+3+4, we start with a dockerfile where we declare our version of go.

    The dockerfile:

    text
    FROM golang:1.15-alpine as dev
    
    WORKDIR /work

    Let's build and start our container:

    text
    cd golang\introduction\part-5.database.redis
    
    docker build --target dev . -t go
    docker run -it -p 80:80 -v ${PWD}:/work go sh
    go version

    Our application

    Create a new directory that holds defines our repository and holds our module

    text
    mkdir videos
    • Define a module path
    text
    # change directory to your application source code
    
    cd videos
    
    # create a go module file
    
    go mod init videos

    Create our base code

    We start out all our applications with a main.go defining our package, declaring our import dependencies
    and our entrypoint main() function

    text
    package main
    
    import (
    	"net/http"
    	"encoding/json"
    	"io/ioutil"
    	"fmt"
    )
    
    func main() {
    	
    	http.HandleFunc("/", HandleGetVideos)
    	http.HandleFunc("/update", HandleUpdateVideos)
    
    	http.ListenAndServe(":80", nil)
    }

    Now before we write these handler functions, we need our videos application

    Create our Videos app

    Firstly, we create a separate code file videos.go that deals with our YouTube videos

    The videos.go file defines what a video struct looks like, a getVideos() function to retrieve
    videos list as a slice and a saveVideos() function to save videos to a file locally.

    We start with our dependencies.
    We import 2 packages, 1 for reading and writing files, and another for dealing with json

    text
    package main
    
    import (
    	"io/ioutil"
    	"encoding/json"
    )

    Then we define what a video struct looks like:

    text
    type video struct {
    	Id string  
    	Title string  
    	Description string 
    	Imageurl string 
    	Url string 
    }

    We have a function for retrieving video objects as a list of type slice :

    text
    func getVideos()(videos []video){
    	
    	fileBytes, err := ioutil.ReadFile("./videos.json")
    
    	if err != nil {
    		panic(err)
    	}
    
    	err = json.Unmarshal(fileBytes, &videos)
    
    	if err != nil {
    		panic(err)
    	}
    
    	return videos
    }

    We also need to copy our videos.json file which contains our video data.

    And finally, we have a function that accepts a list of type slice and stores the videos to a local file

    text
    func saveVideos(videos []video)(){
    
      videoBytes, err  := json.Marshal(videos)
      if err != nil {
    		panic(err)
    	}
    
    	err = ioutil.WriteFile("./videos.json", videoBytes, 0644)
      if err != nil {
    		panic(err)
    	}
    
    }

    HTTP Handlers

    Now we have to define our handler functions HandleGetVideos and HandleUpdateVideos,

    text
    func HandleGetVideos(w http.ResponseWriter, r *http.Request){
    	
    	videos := getVideos()
    	videoBytes, err  := json.Marshal(videos)
    
    	if err != nil {
      	panic(err)
    	}
    
    	w.Write(videoBytes)
    }
    
    func HandleUpdateVideos(w http.ResponseWriter, r *http.Request){
    
    	if r.Method == "POST" {
    
    			body, err := ioutil.ReadAll(r.Body)
    			if err != nil {
    				panic(err)
    			}
    
    			var videos []video
    			err = json.Unmarshal(body, &videos)
    			if err != nil {
    				w.WriteHeader(400)
    				fmt.Fprintf(w, "Bad request")
    			}
    
    			saveVideos(videos)
    
    		} else {
    			w.WriteHeader(405)
    			fmt.Fprintf(w, "Method not Supported!")
    		}
    }

    Now so far, we have a web HTTP application that can list and update our youtube videos.
    Let's build and run it

    text
    go build
    
    ./videos

    If we head over to http://localhost in the browser we can see our application returns our 2 videos. .
    We can use tools like PostMan to generate a POST request to update our videos too.

    Redis Go Package

    Now instead of reading and writing our records to a json file, we are going to read and write records to Redis.

    https://github.com/go-redis/redis/

    text
    go get github.com/go-redis/redis/v8

    And to use the library, we have to import it

    text
    import (
      "github.com/go-redis/redis/v8"
    )

    Now to connect to any database, you're going to need a bit of information:

    • Database Address
    • Database Port
    • Database Username\Password

    We can define these as environment variables and read those in our code

    text
    import (
      "os"
    )
    //main.go (global variables)
    
    var redis_sentinels = os.Getenv("REDIS_SENTINELS")
    var redis_master = os.Getenv("REDIS_MASTER_NAME")
    var redis_password = os.Getenv("REDIS_PASSWORD")

    We define an empty context and Redis client
    Context can be used to control timeouts and deadlines for our application. We can set up an empty context for now.

    text
    import (
      "context"
    )
    
    var ctx = context.Background() 
    var redisClient *redis.Client

    Redis Sentinel Client

    https://redis.uptrace.dev/guide/sentinel.html#redis-server-client

    text
    import (
      "strings"
    )
    
    sentinelAddrs := strings.Split(redis_sentinels, ",")
    rdb := redis.NewFailoverClient(&redis.FailoverOptions{
      MasterName:     redis_master,
      SentinelAddrs:  sentinelAddrs,
      Password: redis_password,
      DB: 0,
    })
    
    redisClient = rdb
    
    rdb.Ping(ctx)

    We can also add the Ping() to our handler functions to ensure it can connect using the global redis client variable

    Networking

    Now we need to remember our go container may not be able to talk to the redis containers because they are running on different networks.

    If you took note, we started our Redis containers on a redis network by passing --net redis as a flag to our docker run commands.

    Let's restart our Go container on the same network

    We also need to set our ENV variables to point our container to the redis sentinels.

    If we look at our sentinel configuration, our master alias is set to mymaster

    text
    docker run -it -p 80:80 `
      --net redis `
      -e REDIS_SENTINELS="sentinel-0:5000,sentinel-1:5000,sentinel-2:5000" `
      -e REDIS_MASTER_NAME="mymaster" `
      -e REDIS_PASSWORD="a-very-complex-password-here" `
      -v ${PWD}:/work go sh

    We can now observe our container is connected to Redis.
    Our application: http://localhost

    Store our Data

    So now we can store our video data in Redis instead of a local json file.
    We'll write the json document of a video (struct) to Redis.

    Let's create a saveVideo() function that stores a single record.

    text
    func saveVideo(video video)(){
    
      videoBytes, err  := json.Marshal(video)
      if err != nil {
    		panic(err)
    	}
    
    	err = redisClient.Set(ctx, video.Id, videoBytes, 0).Err()
    	if err != nil {
    		panic(err)
    	}
    }

    Now since we have one endpoint that saves all videos, we need to update it to save each video it has.

    text
    func saveVideos(videos []video)(){
    	for _, video := range videos {
    		saveVideo(video)
    	}
    }

    To get the videos, let's create a function to retrieve a single record:

    text
    func getVideo(id string)(video video) {
    	
    	value, err := redisClient.Get(ctx, id).Result()
    
    	if err != nil {
    		panic(err)
    	}
    
    	if err != redis.Nil {
    		err = json.Unmarshal([]byte(value), &video)
    	}
    	
    	return video
    }

    And finally we need to update our GetVideos() function to loop all the video keys and return all videos

    text
    func getVideos()(videos []video){
    	
    	keys, err := redisClient.Keys(ctx,"*").Result()
    
    	if err != nil {
    		panic(err)
    	}
    
    	for _, key := range keys {
    		video := getVideo(key)
    		videos = append(videos, video)
    	}
    
    	return videos
    }

    And we need all our imports

    text
    import (
    	"encoding/json"
    	"github.com/go-redis/redis/v8"
    )

    Now if we rebuild our all and access it, we get null as there are no videos in Redis. Let's add two!

    Let's POST the following using PostMan to url http://localhost/update

    text
    [
    	{
    		"id" : "QThadS3Soig",
    		"title" : "Kubernetes on Amazon",
    		"imageurl" : "https://i.ytimg.com/vi/QThadS3Soig/sddefault.jpg",
    		"url" : "https://youtu.be/QThadS3Soig",
    		"description" : "TEST"
    	},
    	{
    		"id" : "eyvLwK5C2dw",
    		"title" : "Kubernetes on Azure",
    		"imageurl" : "https://i.ytimg.com/vi/eyvLwK5C2dw/mqdefault.jpg?sqp=CISC_PoF&rs=AOn4CLDo7kizrJozB0pxBhxL9JbyiW_EPw",
    		"url" : "https://youtu.be/eyvLwK5C2dw",
    		"description" : "TEST"
    	}
    ]

    If we refresh our page, we can now see two records!

    Improvements

    Now that you have the fundamental knowledge of HTTP and Redis,
    you can update the code to retrieve 1 video by ID, or delete a video by ID.
    You can add search functionality and more!

    Let's update our / GET handler to be able to return a single video

    text
    func HandleGetVideos(w http.ResponseWriter, r *http.Request){
    	
    	id, ok := r.URL.Query()["id"]
      
    	if ok {
    
        videoID := id[0]
        video := getVideo(videoID)
        
    		if video.Id == "" { //video not found, or empty ID
    			w.WriteHeader(http.StatusNotFound)
    			w.Write([]byte("{}"))
    			return 
    		}
    
    		videoBytes, err  := json.Marshal(video)
    		if err != nil {
      		panic(err)
    		}
    
    		w.Write(videoBytes)
        return
    		
    	}
    
    	videos := getVideos()
    	videoBytes, err  := json.Marshal(videos)
    
    	if err != nil {
      	panic(err)
    	}
    
    	w.Write(videoBytes)
    }

    We can also update our /update POST endpoint to be able to update a single video

    text
    func HandleUpdateVideos(w http.ResponseWriter, r *http.Request){
    
    	if r.Method == "POST" {
    
    			body, err := ioutil.ReadAll(r.Body)
    			if err != nil {
    				panic(err)
    			}
    
    			_, ok := r.URL.Query()["id"]
    			if ok {
    				
    				var video video
    				err = json.Unmarshal(body, &video)
    				if err != nil {
    					w.WriteHeader(400)
    					fmt.Fprintf(w, "Bad request")
    				}
    
    				saveVideo(video)
    				return
    
    			}
    			
    			var videos []video
    			err = json.Unmarshal(body, &videos)
    			if err != nil {
    				w.WriteHeader(400)
    				fmt.Fprintf(w, "Bad request")
    			}
    
    			saveVideos(videos)
    			return
    
    		} else {
    			w.WriteHeader(405)
    			fmt.Fprintf(w, "Method not Supported!")
    		}
    }

    Build our Docker container

    Full dockerfile :

    text
    FROM golang:1.15-alpine as dev
    
    WORKDIR /work
    
    FROM golang:1.15-alpine as build
    
    WORKDIR /videos
    COPY ./videos/* /videos/
    RUN go build -o videos
    
    FROM alpine as runtime 
    COPY --from=build /videos/videos /
    CMD ./videos

    Build :

    text
    cd golang\introduction\part-5.database.redis
    docker build . -t videos

    Run :

    text
    docker run -it -p 80:80 `
      --net redis `
      -e REDIS_SENTINELS="sentinel-0:5000,sentinel-1:5000,sentinel-2:5000" `
      -e REDIS_MASTER_NAME="mymaster" `
      -e REDIS_PASSWORD="a-very-complex-password-here" `
      videos

    Overview (`kubernetes/admissioncontrollers/introduction/README.md`)

    Introduction to Admission controllers

    Admission Webhook


    Installation (local)


    Create a kind cluster:

    text
    kind create cluster --name webhook --image kindest/node:v1.20.2

    TLS certificate notes for Webhook


    In order for our webhook to be invoked by Kubernetes, we need a TLS certificate.
    In this demo I'll be using a self signed cert.
    It's ok for development, but for production I would recommend using a real certificate instead.

    We'll use a very handy CloudFlare SSL tool in a docker container to get this done.

    Follow Use CFSSL to generate certificates

    After the above, we should have:

    • a Webhook YAML file
    • CA Bundle for signing new TLS certificates
    • a TLS certificate (Kubernetes secret)

    Local Development


    We always start with a dockerfile since we need a Go dev environment.

    text
    FROM golang:1.15-alpine as dev-env
    
    WORKDIR /app

    Build and run the controller

    text
    # get dev environment: webhook
    
    cd sourcecode
    docker build . -t webhook
    docker run -it --rm -p 80:80 -v ${PWD}:/app webhook sh

    We always start with Hello world!
    Let's define our basic main module and a web server

    text
    go mod init example-webhook

    New file : main.go

    text
    package main
    
    import (
      "net/http"
    	"log"
    )
    
    func main() {
      http.HandleFunc("/", HandleRoot)
    	http.HandleFunc("/mutate", HandleMutate)
      log.Fatal(http.ListenAndServe(":80", nil))
    }
    
    func HandleRoot(w http.ResponseWriter, r *http.Request){
    	w.Write([]byte("HandleRoot!"))
    }
    
    func HandleMutate(w http.ResponseWriter, r *http.Request){
    	w.Write([]byte("HandleMutate!"))
    }

    Build our code and run it

    text
    export CGO_ENABLED=0
    go build -o webhook
    ./webhook

    We'll be able to hit the http://localhost/mutate endpoint in the browser

    NOTE: In Windows, container networking is not fully supported. Our container exposes port 80, but to access our Kubernetes cluster which runs in another container, we need to enable --net host flag. This means exposing port 80 will stop working from here on

    Let's exit the container and start with --net host so our container can access our kubernetes kind cluster

    text
    docker run -it --rm --net host -v ${HOME}/.kube/:/root/.kube/ -v ${PWD}:/app webhook sh

    We can also test our access to our kubernetes cluster with the config that is mounted in:

    text
    apk add --no-cache curl
    curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
    chmod +x ./kubectl
    mv ./kubectl /usr/local/bin/kubectl

    Kubernetes

    How do we interact with Kubernetes ?
    Kubernetes provides many libraries and we'll interact with some of these today

    Since we'll receive webhook events from Kubernetes, we'll need to translate these
    requests into objects or structs that we understand.

    For this, the serializer is important:

    text
    "k8s.io/apimachinery/pkg/runtime"
    "k8s.io/apimachinery/pkg/runtime/serializer"
    
    var (
    	universalDeserializer = serializer.NewCodecFactory(runtime.NewScheme()).UniversalDeserializer()
    )

    To access Kubernetes, we need to define a config and a client using our config.
    We can authenticate with K8s in a number of ways.

    First way is good for local development and thats using a kubeconfig file.
    For production, we'll use a Kubernetes service account with RBAC permissions.
    We'll do both methods today.

    text
    # define our config and client
    var config *rest.Config
    var clientSet *kubernetes.Clientset
    
    # in main()
    
    useKubeConfig := os.Getenv("USE_KUBECONFIG")
    kubeConfigFilePath := os.Getenv("KUBECONFIG")
    
    if len(useKubeConfig) == 0 {
    		// default to service account in cluster token
    		c, err := rest.InClusterConfig()
    		if err != nil {
    			panic(err.Error())
    		}
    		config = c
    	} else {
    		//load from a kube config
    		var kubeconfig string
    
    		if kubeConfigFilePath == "" {
    			if home := homedir.HomeDir(); home != "" {
    				kubeconfig = filepath.Join(home, ".kube", "config")
    			} 
    		} else {
    			kubeconfig = kubeConfigFilePath
    		}
    
        fmt.Println("kubeconfig: " + kubeconfig)
    
    		c, err := clientcmd.BuildConfigFromFlags("", kubeconfig)
    		if err != nil {
    			panic(err.Error())
    		}
    		config = c
    	}

    Once we built our kubeconfig, we can instantiate a client to use in our app:

    text
    cs, err := kubernetes.NewForConfig(config)
      if err != nil {
        panic(err.Error())
      }
      clientSet = cs

    And we'll need to import the dependencies for this:

    text
    "os"
    	"fmt"
    	"path/filepath"
    	"k8s.io/client-go/kubernetes"
    	rest "k8s.io/client-go/rest"
    	"k8s.io/client-go/tools/clientcmd"
    	"k8s.io/client-go/util/homedir"

    Since we're also using the client-go library, we need to install the same version as
    the other libraries as we can see in the go.mod file, we're using v0.21.0

    text
    go get k8s.io/[email protected]

    Rebuild to ensure no errors:

    text
    go build -o webhook

    Test with a kubeconfig

    text
    export USE_KUBECONFIG=true
    ./webhook

    To test our access, let's create a test.go and return pods from the kube-system namespace

    text
    #test.go
    package main 
    
    import ()
    
    func test(){
    }

    Use our global clientset defined in main() and get all pods

    text
    pods, err := clientSet.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
    
    	if err != nil {
    		panic(err.Error())
    	}
    
    	fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))

    Define dependencies:

    text
    "context"
    	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    	"fmt"

    And finally invoking it in main() calling test()

    Run and test our Kubernetes access:

    text
    bash-5.0# ./webhook
    kubeconfig: /root/.kube/config
    There are 11 pods in the cluster

    Mutating Webhook

    Now that we have a working app that can talk to Kubernetes, lets implement our webhook endpoint and deploy it to kubernetes to see what type of message the API server sends us when events happen

    Firstly, we need to enable a TLS endpoint
    Let's take some parameters where we can set the path to the TLS certificate and port number to run on.

    Import flag dependency:

    text
    "flag"
    "strconv"

    Define our parameters for cert configuration

    text
    type ServerParameters struct {
    	port           int    // webhook server port
    	certFile       string // path to the x509 certificate for https
    	keyFile        string // path to the x509 private key matching `CertFile`
    }
    
    var parameters ServerParameters
    
    # in main()
    
      flag.IntVar(&parameters.port, "port", 8443, "Webhook server port.")
      flag.StringVar(&parameters.certFile, "tlsCertFile", "/etc/webhook/certs/tls.crt", "File containing the x509 Certificate for HTTPS.")
      flag.StringVar(&parameters.keyFile, "tlsKeyFile", "/etc/webhook/certs/tls.key", "File containing the x509 private key to --tlsCertFile.")
      flag.Parse()
    
    # start our web server exposing TLS endpoint 
    
    	log.Fatal(http.ListenAndServeTLS(":" + strconv.Itoa(parameters.port), parameters.certFile, parameters.keyFile, nil))

    Let's capture the request coming from Kubernetes and write it to local file for analysis

    text
    # dependencies
    "io/ioutil"
    
    # HandleMutate
    body, err := ioutil.ReadAll(r.Body)
    err = ioutil.WriteFile("/tmp/request", body, 0644)
    if err != nil {
      panic(err.Error())
    }

    Deployment


    Let's built what we have and deploy it to our kubernetes cluster
    We will firstly need to add a build step to our dockerfile to build the code
    And we'll also need to create a smaller runtime layer in our dockerfile

    Full dockerfile :

    text
    FROM golang:1.15-alpine as dev-env
    
    WORKDIR /app
    
    FROM dev-env as build-env
    COPY go.mod /go.sum /app/
    RUN go mod download
    
    COPY . /app/
    
    RUN CGO_ENABLED=0 go build -o /webhook
    
    FROM alpine:3.10 as runtime
    
    COPY --from=build-env /webhook /usr/local/bin/webhook
    RUN chmod +x /usr/local/bin/webhook
    
    ENTRYPOINT ["webhook"]

    Let's build the container and push it to a registry:

    text
    docker build . -t aimvector/example-webhook:v1
    docker push aimvector/example-webhook:v1
    text
    # apply generated secret
    kubectl -n default apply -f ./tls/example-webhook-tls.yaml
    
    
    kubectl -n default apply -f rbac.yaml
    kubectl -n default apply -f deployment.yaml
    kubectl -n default get pods
    
    # ensure above pods are running first
    
    kubectl -n default apply -f webhook.yaml

    Deploy a demo that needs mutation

    text
    kubectl -n default  apply -f ./demo-pod.yaml

    We should now be able to see an example request from Kubernetes sitting in our tmp/request location. This request is called an "AdmissionReview"

    Kubernetes sends us an AdmissionReview and expects an AdmissionResponse back.

    We can copy this review locally and use it for development so we dont need to deploy to
    kubernetes constantly. For example:

    text
    kubectl cp example-webhook-756bcb566b-9kxjp:/tmp/request ./mock-request.json

    So lets grab the info from the admission request, so we can do something with it

    text
    //dependencies 
      "k8s.io/api/admission/v1beta1"
      "errors"
    
      //HandleMutate()
    
      var admissionReviewReq v1beta1.AdmissionReview
    
    	if _, _, err := universalDeserializer.Decode(body, nil, &admissionReviewReq); err != nil {
    		w.WriteHeader(http.StatusBadRequest)
    		fmt.Errorf("could not deserialize request: %v", err)
    	} else if admissionReviewReq.Request == nil {
    		w.WriteHeader(http.StatusBadRequest)
    		errors.New("malformed admission review: request is nil")
    	}
    
    	fmt.Printf("Type: %v \t Event: %v \t Name: %v \n",
    		admissionReviewReq.Request.Kind,
    		admissionReviewReq.Request.Operation,
    		admissionReviewReq.Request.Name,
    	)

    Mutation

    Firstly we need to grab the Pod object from the admission request

    text
    //dependencies 
    apiv1 "k8s.io/api/core/v1"
    
    var pod apiv1.Pod
    
    err = json.Unmarshal(admissionReviewReq.Request.Object.Raw, &pod)
    
    if err != nil {
      fmt.Errorf("could not unmarshal pod on admission request: %v", err)
    }

    To perform a simple mutation on the object before the Kubernetes API sees the object, we can apply a patch to the operation.

    text
    //global
    
    type patchOperation struct {
    	Op    string      `json:"op"`
    	Path  string      `json:"path"`
    	Value interface{} `json:"value,omitempty"`
    }
    
    //HandleMutate()
    var patches []patchOperation

    Add a label that we can inject on the pod
    We have to craft the kubernetes object we want to patch.
    For example, a label is part of the Metadata API on the Pod spec

    https://pkg.go.dev/k8s.io/api/core/v1#Pod

    text
    // Get existing Metadata labels
    
    labels := pod.ObjectMeta.Labels
    labels["example-webhook"] = "it-worked"
    
    patches = append(patches, patchOperation{
    				Op:    "add",
    				Path:  "/metadata/labels",
    				Value: labels,
    		})

    Once you have completed all your patching, convert the patches to byte slice:

    text
    patchBytes, err := json.Marshal(patches)
    	if err != nil {
    		fmt.Errorf("could not marshal JSON patch: %v", err)
    	}

    Add it to the admission response

    text
    admissionReviewResponse := v1beta1.AdmissionReview{
          Response: &v1beta1.AdmissionResponse{
            UID: admissionReviewReq.Request.UID,
            Allowed: true,
          },
        }
    
      admissionReviewResponse.Response.Patch = patchBytes
    
      bytes, err := json.Marshal(&admissionReviewResponse)
        if err != nil {
          fmt.Errorf("marshaling response: %v", err)
        }
      
      w.Write(bytes)
    
      //dependencies 
      "encoding/json"

    Build and push the updates

    text
    docker build . -t aimvector/example-webhook:v1
    docker push aimvector/example-webhook:v1

    Delete all pods to get latest image

    text
    kubectl delete pods --all

    Redeploy our demo pod and see the mutations

    text
    kubectl -n default  apply -f ./demo-pod.yaml

    See the injected label

    text
    kubectl get pods --show-labels

    Overview (`kubernetes/autoscaling/vertical-pod-autoscaling/readme.md`)

    Vertical Pod Autoscaling

    We need a Kubernetes cluster

    Lets create a Kubernetes cluster to play with using kind

    text
    kind create cluster --name vpa --image kindest/node:v1.30.4

    Metric Server

    • For Cluster Autoscaler - On cloud-based clusters, Metric server may already be installed.
    • For HPA - We're using kind

    Metric Server provides container resource metrics for use in autoscaling pipelines

    Because I run K8s 1.30 in kind, the Metric Server version i need is 0.7.2
    We will need to deploy Metric Server 0.7.2
    I used components.yamlfrom the release page link above.

    Important Note : For Demo clusters (like kind), you will need to disable TLS
    You can disable TLS by adding the following to the metrics-server container args

    For production, make sure you remove the following :

    text
    - --kubelet-insecure-tls

    Deployment:

    text
    cd kubernetes\autoscaling
    kubectl -n kube-system apply -f .\components\metric-server\components.yaml
    
    #test 
    kubectl -n kube-system get pods
    
    #note: wait for metrics to populate!
    kubectl top nodes

    VPA

    VPA docs here
    Let's install the VPA from a container that can access our cluster

    text
    cd kubernetes/autoscaling/vertical-pod-autoscaling
    docker run -it --rm -v ${HOME}:/root/ -v ${PWD}:/work -w /work --net host debian:bookworm bash
    
    # install git
    apt-get update && apt-get install -y git curl nano
    
    # install kubectl 
    curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
    chmod +x ./kubectl
    mv ./kubectl /usr/local/bin/kubectl
    
    
    cd /tmp
    git clone https://github.com/kubernetes/autoscaler.git
    cd autoscaler/vertical-pod-autoscaler/
    
    # you may need to generate VPA certificates 
    bash ./pkg/admission-controller/gencerts.sh
    
    # deploy the VPA
    ./hack/vpa-up.sh
    
    # after few seconds, we can see the VPA components in:
    
    kubectl -n kube-system get pods

    Build and deploy example app

    text
    # build
    
    cd kubernetes\autoscaling\components\application
    docker build . -t aimvector/application-cpu:v1.0.0
    
    # push
    docker push aimvector/application-cpu:v1.0.0
    
    # deploy 
    kubectl apply -f deployment.yaml
    
    # metrics
    kubectl top pods

    Generate some traffic

    Let's deploy a simple traffic generator pod

    text
    cd kubernetes\autoscaling\components\application
    kubectl apply -f .\traffic-generator.yaml
    
    # get a terminal to the traffic-generator
    kubectl exec -it traffic-generator -- sh
    
    # install wrk
    apk add --no-cache wrk
    
    # simulate some load
    wrk -c 5 -t 5 -d 99999 -H "Connection: Close" http://application-cpu

    Deploy an example VPA

    text
    kubectl apply -f .\vertical-pod-autoscaling\vpa.yaml
    
    kubectl describe vpa application-cpu

    Deploy Goldilocks

    text
    cd /tmp
    git clone https://github.com/FairwindsOps/goldilocks.git
    cd goldilocks/hack/manifests/
    
    kubectl create namespace goldilocks
    kubectl -n goldilocks apply -f ./controller
    kubectl -n goldilocks apply -f ./dashboard
    
    
    kubectl label ns default goldilocks.fairwinds.com/enabled=true
    kubectl label ns default goldilocks.fairwinds.com/vpa-update-mode="off"
    
    kubectl -n goldilocks port-forward svc/goldilocks-dashboard 80

    Overview (`kubernetes/gateway-api/agentgateway/README.md`)

    Introduction to Agent Gateway

    Official Site | GitHub | Documentation

    Prerequisites

    To get started, you will need to follow the Introduction to Gateway API first.
    You'll need an understanding of the Gateway API.

    In the introduction guide, you will:

    • Create a local Kubernetes cluster
    • Install the Gateway API CRDs
    • Example models, either in cluster or external (OpenAI, Anthropic, Gemini)

    This will allow us access to the Gateway API so we can go ahead and deploy a Gateway API controller to use.

    To get the version you want, you can use the agent gateway Github Release page.
    For this guide, we will use 1.1.0.

    You can list helm chart versions too:

    shell
    helm show chart oci://cr.agentgateway.dev/charts/agentgateway-crds
    helm show chart oci://cr.agentgateway.dev/charts/agentgateway
    shell
    CHART_VERSION="v1.1.0"
    
    # Install CRDs
    
    helm upgrade -i agentgateway-crds oci://cr.agentgateway.dev/charts/agentgateway-crds \
      --create-namespace \
      --namespace agentgateway-system \
      --version $CHART_VERSION
    
    # Install ControlPlane
    helm upgrade -i agentgateway oci://cr.agentgateway.dev/charts/agentgateway \
      --namespace agentgateway-system \
      --version $CHART_VERSION \
      --set controller.image.pullPolicy=Always \
      --wait

    Check our installation

    shell
    # check pods are running
    kubectl -n agentgateway-system get pods
    
    # check logs
    kubectl -n agentgateway-system logs -l app.kubernetes.io/name=agentgateway

    Check the AgentGateway Class

    Verify the GatewayClass was registered:

    shell
    kubectl get gatewayclass

    Install an AgentGateway Gateway

    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/01-gateway.yaml
    
    # we can see our gateway dataplane pods
    kubectl get pods
    kubectl get svc
    
    # check gateway 
    kubectl get gateway
    
    # port forward for access
    kubectl -n default port-forward svc/agentgateway 8080:80

    Features

    Feature: LLM Routing

    AgentGateway introduces CRD AgentgatewayBackend.
    AgentgatewayBackend is an LLM provider as a routable backend.

    Create an LLM Backend: Gemini

    If your LLM Provider needs an API key, we need a Kubernetes secret for it:

    shell
    export GEMINI_API_KEY=""
    
    kubectl apply -f- <<EOF
    apiVersion: v1
    kind: Secret
    metadata:
      name: gemini-secret
    type: Opaque
    stringData:
      Authorization: $GEMINI_API_KEY
    EOF

    Create the LLM backend:

    Instead of an HTTPRoute routing to an upstream backend service, it uses an AgentgatewayBackend

    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/llm/gemini/02-backend-gemini.yaml

    Route traffic from the Gateway to the backend using a standard HTTPRoute:

    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/llm/03-httproute.yaml

    Test our HTTPRoute:

    shell
    curl "localhost:8080/v1/chat/completions" \
      -H "Content-Type: application/json" \
      -d '{
          "messages": [{"role": "user", 
          "content": "What is Kubernetes in one sentence?"}]
      }' | jq

    We can route by path as well. This means one domain and route to provider by path

    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/llm/04-httproute-gemini.yaml
    shell
    curl "localhost:8080/ai/gemini/v1/chat/completions" \
      -H "Content-Type: application/json" \
      -d '{
          "messages": [{"role": "user", 
          "content": "What is Kubernetes in one sentence?"}]
      }' | jq

    Create an LLM Backend: Anthropic

    The same pattern works for any supported provider.

    shell
    export ANTHROPIC_API_KEY=""
    
    kubectl apply -f- <<EOF
    apiVersion: v1
    kind: Secret
    metadata:
      name: anthropic-secret
    type: Opaque
    stringData:
      Authorization: $ANTHROPIC_API_KEY
    EOF
    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/llm/anthropic/05-backend-anthropic.yaml
    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/llm/06-httproute-anthropic.yaml

    Route to Anthropic Claude:

    shell
    curl "localhost:8080/ai/claude/v1/chat/completions" \
      -H "Content-Type: application/json" \
      -d '{
          "messages": [{"role": "user", 
          "content": "What is Kubernetes in one sentence?"}]
      }' | jq

    Now we have implemented LLM routing by path using HTTPRoute:

    localhost:8080/ai/gemini --> gemini flash
    localhost:8080/ai/claude --> claude sonnet

    Feature: MCP Routing

    To route to MCP servers, we will need some example MCP servers setup.
    In this example we will use a Kubernetes MCP and setup a read-only ServiceAccount so it can read our cluster.

    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/kubernetes/02-mcp.yaml
    
    # check mcp server
    kubectl get pods
    kubectl get sa
    kubectl get service

    This MCP server will serve traffic on /mcp
    Let's create a backend and an HTTPRoute for that.

    Create an MCP Backend: Kubernetes

    Instead of an HTTPRoute routing to an upstream backend service, it uses an AgentgatewayBackend
    Create the MCP backend:

    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/kubernetes/03-mcp-backend.yaml
    
    # check the backend
    kubectl get agentgatewaybackend
    kubectl describe agentgatewaybackend

    Route traffic from the Gateway to the backend using a standard HTTPRoute:

    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/04-httproute.yaml

    Test our MCP Backend: Kubernetes

    Initialize session to get a mcp-session-id from the response:

    shell
    curl -s -i http://localhost:8080/ai/mcps/kubernetes/mcp \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'

    MCP Tool call to list namespaces in our cluster:

    shell
    MCP_SESSION_ID=''
    
    curl -s http://localhost:8080/ai/mcps/kubernetes/mcp \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -H "Mcp-Session-Id: ${MCP_SESSION_ID}" \
      -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"namespaces_list","arguments":{}},"id":3}'

    Create an MCP Backend: Github

    To showcase routing to and federating different MCPs under different or the same endpoints, we will create another MCP server. We have use Github MCP

    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/github/05-mcp.yaml
    
    # check mcp server
    kubectl get pods
    kubectl get sa
    kubectl get service

    Then we create a backend to route to:

    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/github/06-mcp-backend.yaml
    
    # check the backend
    kubectl get agentgatewaybackend
    kubectl describe agentgatewaybackend

    Route traffic from the Gateway to the backend using a standard HTTPRoute:

    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/07-httproute.yaml

    Test our MCP Backend: Github

    Initialize session to get a mcp-session-id from the response:

    shell
    curl -s -i http://localhost:8080/ai/mcps/github/mcp \
           -H "Content-Type: application/json" \
           -H "Accept: application/json, text/event-stream" \
           -H "Authorization: Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}" \
           -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'

    MCP Tool call to search repositories:

    shell
    MCP_SESSION_ID=''
    
    ```shell
    curl -s http://localhost:8080/ai/mcps/github/mcp \
      -H "Content-Type: application/json" \
      -H "Accept: application/json, text/event-stream" \
      -H "Authorization: Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}" \
      -H "Mcp-Session-Id: ${MCP_SESSION_ID}" \
      -d '{"jsonrpc":"2.0","method":"tools/call","params":{"name":"search_repositories","arguments":{"query":"kubernetes gateway-api","sort":"stars","perPage":5}},"id":3}'

    Now we have implemented MCP routing by path using HTTPRoute:

    localhost:8080/ai/mcps/kubernetes --> kubernetes MCP service

    localhost:8080/ai/mcps/github --> Github MCP service

    Federate our MCPs under one endpoint

    To federate endpoints we have to define an AgentGatewayBackend that combines our MCPs

    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/08-federatedbackend.yaml

    Deploy an HTTPRoute to our single federated endpoint:

    shell
    kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/09-httproute.yaml

    Get session ID:

    shell
    curl -s -i http://localhost:8080/ai/mcps/mcp \
           -H "Content-Type: application/json" \
           -H "Accept: application/json, text/event-stream" \
           -H "Authorization: Bearer ${GITHUB_PERSONAL_ACCESS_TOKEN}" \
           -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'

    Now we can proceed to call tools under the single endpoint for both Kubernetes and Github MCP.


    Overview (`kubernetes/gateway-api/cilium/README.md`)

    Introduction to Cilium: Gateway API

    cilium

    Prerequisites

    This guide will assume you have a basic understanding of the Gateway API.

    If it's your first time using Gateway API, please see the Introduction to Gateway API guide first

    This guide has a slight twist from the other Gateway APIs.

    It's because Cilium is a network CNI for Kubernetes.

    This means we need to disable the default kubernetes CNI networking in our cluster, so we will be creating it slightly differently.

    We also need to install the Gateway API CRDs followed by Cilium, before we deploy all our example applications.

    What is Cilium

    Cilium leverages eBPF technology to provide high-performance networking, security and observability for containerised applications, especially in Kubernetes.

    eBPF (Extended Berkeley Packet Filter) is a technology that allows us to run custom programs safely in the Linux Kernel without the need for modules, changing source code.

    These programs allow programs like Cilium to add security, performance and observability to the network.

    Create a kubernetes cluster

    shell
    kind create cluster --name gatewayapi --image kindest/node:v1.34.0 --config kubernetes/gateway-api/cilium/kind.yaml

    Test our cluster and makes sure kubectl is configured for it:

    shell
    kubectl get nodes
    NAME                       STATUS     ROLES           AGE   VERSION
    gatewayapi-control-plane   NotReady   control-plane   93s   v1.34.0
    gatewayapi-worker          NotReady   <none>          83s   v1.34.0

    The nodes will remain in a NotReady state until we unstall our network plugin, which we will do with Cilium.

    Install Gateway API CRDs

    Now we can deploy the Gateway API CRDs following our Introduction to Gateway API guide.

    The CRDs need to be in place when we install Cilium.

    Configuration

    It's always good to get a grip on the default helm values to see what we can do with the chart.

    In this demo we'll only focus on the gatewayAPI section of the helm values file.

    We created our own values.yaml which allows us to enable Gateway API, and use Cilium as the kube proxy replacement so it takes over our networking.

    Installation

    Install:

    shell
    CHART_VERSION="1.19.4"
    helm repo add cilium https://helm.cilium.io
    helm repo update
    helm search repo cilium --versions
    
    # default configuration
    helm show values cilium/cilium > kubernetes/gateway-api/cilium/default-values.yaml
    
    helm upgrade --install cilium cilium/cilium \
      --version $CHART_VERSION \
      --namespace kube-system \
      --values kubernetes/gateway-api/cilium/values.yaml

    Check our installation

    shell
    # check the pods
    kubectl -n kube-system get pods
    
    # nodes should be Ready once above is Running
    kubectl get nodes
    NAME                       STATUS   ROLES           AGE   VERSION
    gatewayapi-control-plane   Ready    control-plane   18m   v1.34.0
    gatewayapi-worker          Ready    <none>          18m   v1.34.0

    We can also check the Cilium Operator logs to ensure we are smooth sailing:

    shell
    kubectl -n kube-system logs -l app.kubernetes.io/name=cilium-operator
    kubectl -n kube-system logs -l app.kubernetes.io/name=cilium-envoy
    kubectl -n kube-system logs -l app.kubernetes.io/name=cilium-agent

    In the introduction guide, you will:

    • Deploy example apps to our cluster
    • Have Domains for our traffic
    • Have TLS certificates

    This will allow us access to the Gateway API so we can go ahead and deploy a Gateway API controller to use. </br

    Install a Cilium Gateway Class

    shell
    kubectl apply -f kubernetes/gateway-api/cilium/01-gatewayclass.yaml

    The CiliumGatewayClassConfig CRD for Cilium allows us to customize the Gateway class.

    This allows us to change the deployment and services for gateways that use this class.
    In our case, we can set the service.type = NodePort

    shell
    kubectl apply -f kubernetes/gateway-api/cilium/01.1-gatewayclass-config.yaml

    Install a Cilium Gateway

    shell
    kubectl apply -f kubernetes/gateway-api/cilium/02-gateway.yaml

    Demo notes on NodePort service

    Patch the NodePort service

    In kind, we don't have a load balancer service, so we'll use a NodePort service for demo purpose.

    Cilium creates a NodePort service for the gateway automatically.
    In Kubernetes this NodePort service will have automatically generated ports which won't match our configuration.

    Therefore, we need to pin the NodePorts to match the extraPortMappings in kind.yaml (30080 for HTTP, 30443 for HTTPS):

    shell
    SVC=$(kubectl get svc -n default -l gateway.networking.k8s.io/gateway-name=gateway-api -o name)
    kubectl patch -n default $SVC --type='json' -p='[
      {"op":"replace","path":"/spec/ports/0/nodePort","value":30080},
      {"op":"replace","path":"/spec/ports/1/nodePort","value":30443}
    ]'

    Install the Cilium CLI

    text
    CILIUM_CLI_VERSION=v0.19.0
    CLI_ARCH=amd64
    curl -L --fail --remote-name-all https://github.com/cilium/cilium-cli/releases/download/${CILIUM_CLI_VERSION}/cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}
    sha256sum --check cilium-linux-${CLI_ARCH}.tar.gz.sha256sum
    tar xzvf cilium-linux-${CLI_ARCH}.tar.gz -C /usr/local/bin
    rm cilium-linux-${CLI_ARCH}.tar.gz{,.sha256sum}

    HTTP Traffic management

    Feel free to quickly run through the basic traffic management table for using HTTPRoute routing for traffic.

    Note: HTTPRoute features are not specific to this controller and should be available to any other gateway API controller that you choose.


    Overview (`kubernetes/gateway-api/envoy/README.md`)

    Introduction to Envoy: Gateway API

    envoy

    Prerequisites

    To get started, you will need to follow the Introduction to Gateway API first.

    You'll need an understanding of the Gateway API.

    In the introduction guide, you will:

    • Create a local Kubernetes cluster
    • Install the Gateway API CRDs
    • Deploy example apps to our cluster
    • Have Domains for our traffic
    • Have TLS certificates

    This will allow us access to the Gateway API so we can go ahead and deploy a Gateway API controller to use. </br

    What is Envoy

    It's important to take a step back and fully understand and appreciate what Envoy is.

    Envoy is not only a Gateway API, that's just one of its features.

    Let's take a look at the Official Site and jump to the documentation.

    Envoy has a separate web page for the Gateway API feature.

    Envoy: Gateway API controller

    Envoy has a helm chart specifically for the gateway product.

    The helm chart relies on the Gateway API CRD's we installed in our cluster already as well as Envoy Proxy.

    Installation

    We can install the helm chart with helm install or upgrade it with helm upgrade:

    shell
    CHART_VERSION="v1.6.0"
    helm show chart oci://docker.io/envoyproxy/gateway-helm
    helm show values oci://docker.io/envoyproxy/gateway-helm > kubernetes/gateway-api/envoy/default-values.yaml
    
    helm install envoy-gateway oci://docker.io/envoyproxy/gateway-helm \
      --version $CHART_VERSION \
      --values kubernetes/gateway-api/envoy/values.yaml \
      --namespace envoy-gateway-system \
      --create-namespace

    Upgrade:

    shell
    helm upgrade envoy-gateway oci://docker.io/envoyproxy/gateway-helm \
      --values kubernetes/gateway-api/envoy/values.yaml \
      --version $CHART_VERSION \
      --namespace envoy-gateway-system

    Configuration

    Most of the Gateway API controllers are installed using helm.

    Before we install it, let's take a look at the default-values.yaml

    Check Installation

    Now we should have the Envoy gateway API controller up and running.

    This is not the gateway itself, but the controller that will manage the CRDs we get access to and implement some gateway API CRDs.

    shell
    # check the controller pods
    kubectl -n envoy-gateway-system get pods
    
    # check the controller pod logs 
    kubectl -n envoy-gateway-system logs -l app.kubernetes.io/instance=envoy-gateway

    Install an Envoy Gateway Class

    shell
    kubectl apply -f kubernetes/gateway-api/envoy/01-gatewayclass.yaml

    Install an Envoy Gateway

    shell
    kubectl apply -f kubernetes/gateway-api/envoy/02-gateway.yaml

    When we apply the gateway, we get a new gateway api pod.

    shell
    # check the new gateway-api pod
    kubectl -n envoy-gateway-system get pods
    
    # we also have a new service
    kubectl -n envoy-gateway-system get svc

    Gateway Configuration

    EnvoyProxy is the CRD that allows us to configure each gateway API instance.

    For example, we can change the deployment or service of the instance like so:

    shell
    kubectl apply -f kubernetes/gateway-api/envoy/02.1-gateway-config.yaml
    
    # we should see 2 replicas 
    kubectl -n envoy-gateway-system get deploy
    kubectl -n envoy-gateway-system get pods
    
    # port forward for access (get the correct service)
    kubectl -n envoy-gateway-system get svc
    kubectl -n envoy-gateway-system port-forward svc/envoy-gateway-default 80

    HTTP Traffic management

    Feel free to quickly run through the basic traffic management table for using HTTPRoute routing for traffic.

    Note: HTTPRoute features are not specific to this controller and should be available to any other gateway API controller that you choose.

    Gateway API Extensions

    Envoy Gateway has its own extensions on top of the native Gateway API features we've seen so far, using custom CRD's.

    Client Traffic Policies

    Connection Limit

    Envoy allows us to limit connections to gateways & listeners.

    We can use tools like hey to demonstrate connection limits.

    shell
    hey -c 10 -q 1 -z 10s -host "example-app.com" http://localhost:80/api/go/status
    
    kubectl apply -f kubernetes/gateway-api/envoy/08-clientpolicy-connectionlimit.yaml
    
    hey -c 10 -q 1 -z 10s -host "example-app.com" http://localhost:80/api/go/status

    Mutual TLS

    Currently we have a listener for TLS (HTTPs). For normal TLS, the client does not have to pass a private key.

    Envoy supports mutual TLS

    shell
    # tell Envoy about the CA cert 
    kubectl create secret generic secret-tls-ca --from-file=rootCA.pem=kubernetes/gateway-api/tls/rootCA.pem
    
    # generate client certs for curl
    mkcert -client client-user.com
    
    kubectl apply -f kubernetes/gateway-api/envoy/09-clientpolicy-mtls.yaml
    
    # browser TLS should now be broken "Access to example-app.com was denied"
    # This is because a client needs to pass certificates to trust 
    
    curl -v -H "Host: example-app.com" \
    --cert kubernetes/gateway-api/tls/client-user.com-client.pem \
    --key kubernetes/gateway-api/tls/client-user.com-client-key.pem \
    --cacert kubernetes/gateway-api/tls/rootCA.pem \
    https://example-app.com/api/go/status

    Backend Traffic Policies

    Local and Global Rate limits

    We can rate limit each HTTP route using Local Rate Limit:

    shell
    kubectl apply -f kubernetes/gateway-api/envoy/10-backendpolicy-ratelimit.yaml
    
    for i in {1..4}; do
    curl -I --header "x-user-id: one" http://example-app.com/api/go/status ; sleep 1;
    done

    Security Policies

    CORS

    shell
    # port-forward to set pre-cors
    kubectl port-forward svc/web-app 8000:80
    
    kubectl apply -f kubernetes/gateway-api/envoy/11-securitypolicy-cors.yaml

    Basic Auth

    shell
    #cleanup client policies to avoid conflict
    kubectl delete clienttrafficpolicies mtls-policy
    kubectl delete SecurityPolicy go-route-cors
    
    # port-forward to 443 
    kubectl -n envoy-gateway-system port-forward svc/envoy-gateway-default 443
    
    #install htpasswd (alpine)
    apk add apache2-utils
    htpasswd -cbs .htpasswd bob password123
    
    kubectl create secret generic basic-auth --from-file=.htpasswd
    
    kubectl apply -f kubernetes/gateway-api/envoy/12-securitypolicy-basicauth.yaml

    API Auth

    shell
    #cleanup previous basic auth policy
    kubectl delete SecurityPolicy go-route-basicauth
    
    kubectl apply -f kubernetes/gateway-api/envoy/13-securitypolicy-apiauth.yaml
    
    # port-forward
    kubectl -n envoy-gateway-system port-forward svc/envoy-gateway-default 443

    You can see Client authentication failed. in the browser.
    We need to pass the header

    text
    curl -v -H "Host: example-app.com" \
    -H 'x-api-key: secret123' \
    --cert kubernetes/gateway-api/tls/client-user.com-client.pem \
    --key kubernetes/gateway-api/tls/client-user.com-client-key.pem \
    --cacert kubernetes/gateway-api/tls/rootCA.pem \
    https://example-app.com/api/go/status

    Logs:

    shell
    kubectl -n envoy-gateway-system logs -l app.kubernetes.io/managed-by=envoy-gateway

    Overview (`kubernetes/gateway-api/haproxy/README.md`)

    Introduction to HAProxy: Gateway API

    haproxy

    Prerequisites

    To get started, you will need to follow the Introduction to Gateway API first.

    You'll need an understanding of the Gateway API.

    Important Note: According to testing and HAProxy documentation, it currently only supports version v0.5.1
    Tried GatewayClass with gateway.networking.k8s.io/v1 and does not work

    In the introduction guide, you will:

    • Create a local Kubernetes cluster
    • Install the Gateway API CRDs
    shell
    kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v0.5.1/experimental-install.yaml
    • Deploy example apps to our cluster
    • Have Domains for our traffic
    • Have TLS certificates

    This will allow us access to the Gateway API so we can go ahead and deploy a Gateway API controller to use. </br

    Introduction

    Let's take a look at the Official Documentation.

    At the time of this guide the HAProxy Gateway API solution is built alongside the HAProxy Kubernetes Ingress controller. So its one solution that can achieve both Ingress and Gateway functionality.

    The HAProxy Unified Gateway is still under development at the time of this guide.

    Configuration

    Most of the Gateway API controllers are installed using helm.

    Before we install it, let's take a look at the default-values.yaml

    Installation

    We can install the helm chart with helm install or with kubectl as stated in the documentation. Important to know that the below Helm chart installs the ingress controller. In order to enable Gateway API functionality, we need to turn it on using our values file.

    shell
    CHART_VERSION="1.48.0"
    helm repo add haproxytech https://haproxytech.github.io/helm-charts
    helm repo update
    helm search repo haproxytech --versions
    
    # see the default configuration
    helm show values haproxytech/kubernetes-ingress \
    > kubernetes/gateway-api/haproxy/default-values.yaml
    
    #apply the cluster roles and bindings (RBAC) to give HAProxy access to Gateway resources
    kubectl apply -f https://raw.githubusercontent.com/haproxytech/kubernetes-ingress/master/deploy/tests/config/experimental/gwapi-rbac.yaml
    
    helm install haproxy-kubernetes-ingress haproxytech/kubernetes-ingress \
      --version $CHART_VERSION \
      --create-namespace \
      --namespace haproxy-controller \
      --values kubernetes/gateway-api/haproxy/values.yaml

    Check Installation

    Now we should have the HAProxy controller up and running.

    This is not the gateway itself, but the controller that will manage the CRDs we get access to and implement some gateway API CRDs.

    shell
    # check the controller pods
    kubectl -n haproxy-controller get pods
    
    # check the controller pod logs 
    kubectl -n haproxy-controller logs -l app.kubernetes.io/name=kubernetes-ingress

    Install an HAProxy Gateway Class

    Please note apiVersion: gateway.networking.k8s.io/v1alpha2 is used and v1 does not work. :

    shell
    kubectl apply -f kubernetes/gateway-api/haproxy/01-gatewayclass.yaml

    Install an HAProxy Gateway

    shell
    kubectl apply -f kubernetes/gateway-api/haproxy/02-gateway.yaml

    Traffic Routing

    At the time of this guide, HAProxy only supports TCPRoute, and not HTTPRoute like the other controllers we covered thus far.

    shell
    kubectl apply -f kubernetes/gateway-api/haproxy/03-tcproute.yaml
    shell
    kubectl -n haproxy-controller port-forward svc/haproxy-kubernetes-ingress 80

    Overview (`kubernetes/gateway-api/istio/README.md`)

    Introduction to Istio: Gateway API

    istio

    Prerequisites

    To get started, you will need to follow the the Introduction to Gateway API first.

    You'll need an understanding of the Gateway API.

    In the introduction guide, you will:

    • Create a local Kubernetes cluster
    • Install the Gateway API CRDs
    • Deploy example apps to our cluster
    • Have Domains for our traffic
    • Have TLS certificates

    This will allow us access to the Gateway API so we can go ahead and deploy a Gateway API controller to use. </br

    What is Istio

    Before diving into installing Istio, its very important to understand what Istio is.

    Istio is not only a Gateway API, its a service mesh.

    In a distributed system with service-to-service communication, a service mesh places a proxy next to each service.

    This proxy intercepts all inbound and outbound communication for it's neighboring service.

    Doing so, a service mesh provides many features

    • observability: monitor network calls (metrics \ traces)
    • traffic management: traffic routing features
    • resillience: automatic retry, circuit breaker, rate limiting etc
    • security: mutual TLS, authentication, policies etc

    Let's take a look at the Official Site and jump to the documentation.

    Istio: Gateway API controller

    With Istio, we have a few more steps to install the gateway API. We fistly need to install the base helm chart which includes Istio specific CRD components required.

    Installation

    We can install the helm chart with helm install or upgrade it with helm upgrade:

    shell
    BASE_CHART_VERSION="1.28.1"
    ISTIOD_CHART_VERSION="1.28.1"
    
    helm repo add istio https://istio-release.storage.googleapis.com/charts
    helm repo update
    
    # get versions for the charts
    helm search repo istio/base --versions | head
    helm search repo istio/istiod --versions | head
    
    helm install istio-base istio/base \
      --version $BASE_CHART_VERSION \
      --namespace istio-system \
      --create-namespace \
      --set defaultRevision=default 
    
    helm install istiod istio/istiod \
      --version $ISTIOD_CHART_VERSION \
      --namespace istio-system \
      --wait

    Configuration

    text
    helm show values istio/istiod \
      --version $ISTIOD_CHART_VERSION > kubernetes/gateway-api/istio/default-values.yaml

    Check Installation

    Now we should have the Istio control plane / controller up and running.

    This is not the gateway itself, but the controller that will manage the CRDs we get access to and implements some gateway API CRD's.

    shell
    # check deployed status
    helm ls -n istio-system
    
    # check the controller pods
    kubectl get pods -n istio-system --output wide
    
    # check the controller pod logs 
    kubectl -n istio-system logs -l app.kubernetes.io/instance=istiod

    Install an Istio Gateway Class

    shell
    kubectl apply -f kubernetes/gateway-api/istio/01-gatewayclass.yaml

    Install an Istio Gateway

    shell
    kubectl apply -f kubernetes/gateway-api/istio/02-gateway.yaml

    When we apply the gateway, we get a new gateway api pod.

    Unlike Traefik and Envoy, in Istio, the gateway pod is created in the same namespace as the gateway

    shell
    # check the new gateway-api pod - this time its in the same namespace as the gateway object!
    kubectl -n default get pods
    
    # we also have a new service
    kubectl -n default get svc

    Gateway Configuration

    For global Gateway class defaults, we can use a ConfigMap with a special label in the istio-system namespace.

    This allows us to set global defaults for all our gatways.

    shell
    kubectl apply -f kubernetes/gateway-api/istio/01.1-gatewayclass-config.yaml

    For Gateways, we can use the parametersRef field under infrastructure and use a ConfigMap to configure it further.

    For example, we can change the deployment or service of the instance like so:

    shell
    kubectl apply -f kubernetes/gateway-api/istio/02.1-gateway-config.yaml
    
    # we should see 2 replicas 
    kubectl -n default get pods
    
    # port forward for access (get the correct service)
    kubectl -n default port-forward svc/gateway-api-istio 80

    HTTP Traffic management

    Feel free to quickly run through the basic traffic management table for using HTTPRoute routing for traffic.

    Note: HTTPRoute features are not specific to this controller and should be available to any other gateway API controller that you choose.

    Istio Gateway VS Istio Gateway API

    Istio has been around for a lot longer than Gateway API has. Therefore Istio has solved much of the traffic management challenges using it's own CRDs, such as VirtualService.

    There is a very important difference between Istio and the other Gateway APIs we've covered so far.

    Istio has made a strict separation between it's own Gateway (networking.istio.io/v1) and the Gateway of Gateway API (gateway.networking.k8s.io/v1)

    This means, their CRDs such as VirtualService does not attach to the new Kubernetes Gateway API like Gateway and HTTPRoute.

    Therefore, to use Gateway API features, one should use Gateway API and not Istio Gateways.

    Installing Classic Istio Gateway

    To install the Istio gateway, we also use helm

    shell
    ISTIOGATEWAY_CHART_VERSION="1.28.1"
    
    helm repo add istio https://istio-release.storage.googleapis.com/charts
    helm repo update
    
    # get versions for the charts
    helm search repo istio/gateway --versions | head
    
    helm install istio-ingress istio/gateway \
      --version $ISTIOGATEWAY_CHART_VERSION \
      --namespace istio-system
    
    kubectl -n istio-system port-forward svc/istio-ingress 80

    Enable Application Service Mesh

    To use virtual services we need to enable the mesh in our cluster.

    I will be using the classic side-car mode, so we'll label our default namespace with the correct labels so that the Istio control plane will automatically inject sidecar containers into all pods in the namespace

    Virtual Services are used throughout Istio to manage traffic.

    shell
    kubectl label namespace default istio-injection=enabled
    
    # delete all pods to get sidecars added
    kubectl delete pods --all

    We will also need the Gateway controller that will be managing our Istio Gateways.

    Istio Gateway Ingress

    Let's deploy the Istio Gateway

    shell
    kubectl apply -f kubernetes/gateway-api/istio/08-istiogateway.yaml

    Traffic management using Istio Gateways

    Technically, as this is a Gateway API guide, this following bit would be out of scope.

    However I wanted to show you the difference by running your thorugh a quick demo of VirtualService using Isio Gateway.

    As an example, VirtualService can be used to deal Traffic in the same way as Gateway API and Ingress

    Let's deploy a Virtual service to perform basic traffic routing:

    shell
    kubectl apply -f kubernetes/gateway-api/istio/09-virtualservice.yaml

    Overview (`kubernetes/gateway-api/kgateway/README.md`)

    Introduction to kgateway: Gateway API

    kgateway

    Prerequisites

    To get started, you will need to follow the Introduction to Gateway API first.

    You'll need an understanding of the Gateway API.

    In the introduction guide, you will:

    • Create a local Kubernetes cluster
    • Install the Gateway API CRDs
    • Deploy example apps to our cluster
    • Have Domains for our traffic
    • Have TLS certificates

    This will allow us access to the Gateway API so we can go ahead and deploy a Gateway API controller to use. </br

    Let's start with the Official Documentation

    kgateway: Gateway API controller

    kgateway is feature rich when it comes to L7 HTTP routing.

    In this guide we will turn on the Gateway API feature in kgateway and use that specifically.

    Installation

    To get the version you want, you can use the kgateway Github Release page.

    For this guide, we will use 2.1.2.

    Install:

    shell
    CHART_VERSION="v2.3.3"
    
    helm show chart oci://cr.kgateway.dev/kgateway-dev/charts/kgateway --version $CHART_VERSION
    helm show chart oci://cr.kgateway.dev/kgateway-dev/charts/kgateway-crds --version $CHART_VERSION
    
    helm show values oci://cr.kgateway.dev/kgateway-dev/charts/kgateway --version $CHART_VERSION > kubernetes/gateway-api/kgateway/default-values.yaml
    
    # deploy CRDs
    helm upgrade kgateway-crds oci://cr.kgateway.dev/kgateway-dev/charts/kgateway-crds \
      --version ${CHART_VERSION} \
      --namespace kgateway-system \
      --create-namespace \
      --install
    
    # Deploy kgateway control plane
    helm upgrade kgateway oci://cr.kgateway.dev/kgateway-dev/charts/kgateway \
      --version $CHART_VERSION \
      --values kubernetes/gateway-api/kgateway/values.yaml \
      --namespace kgateway-system \
      --install \
      --create-namespace

    Check our installation

    shell
    # check the pods
    kubectl -n kgateway-system get pods
    
    # check the logs 
    kubectl -n kgateway-system logs -l app.kubernetes.io/name=kgateway

    Install a kgateway Gateway Class

    The helm chart will create a GatewayClass for us as per docs.

    shell
    kubectl get gc kgateway -o yaml

    Install a kgateway Gateway

    shell
    kubectl apply -f kubernetes/gateway-api/kgateway/01-gateway.yaml
    
    # we can see our gateway dataplane pods
    kubectl get pods
    kubectl get svc
    
    # port forward for access
    kubectl -n default port-forward svc/gateway-api 80

    Configuration

    Most of the Gateway API controllers are installed using helm.

    Before we install it, let's take a look at the values.yaml

    kgateway has a section on Gateway Customization where we can adjust gateway parameters.

    Gateway API allows us to inject parameters using the infrastructure.parametersRef field

    We can customize our gateway:

    shell
    kubectl apply -f kubernetes/gateway-api/kgateway/02-gateway-config.yaml

    HTTP Traffic management

    Feel free to quickly run through the basic traffic management table for using HTTPRoute routing for traffic.

    Note: HTTPRoute features are not specific to this controller and should be available to any other gateway API controller that you choose.

    Policies: TrafficPolicy

    kgateway uses TrafficPolicy as their custom CRD to apply traffic policies to certain HTTPRoutes or all HTTPRoutes that a Gateway serves.

    There are many TrafficPolicy Features available.
    The cool thing here is that these TrafficPolicies are now pluggable into Gateway API HTTPRoutes.

    In HTTPRoute objects, under filters, we can add extensions using something like:

    yaml
    filters:
    - type: ExtensionRef
      extensionRef:
        group: gateway.kgateway.dev
        kind: TrafficPolicy
        name: cors-feature

    TrafficPolicy Example

    Example of a Traffic Policy supporting CORS

    shell
    kubectl apply -f kubernetes/gateway-api/kgateway/03-trafficpolicy-cors.yaml

    Policies: HTTPListenerPolicy

    This is a special CRD for attaching policies to listeners on gateways.

    We can use this to set custom access log formats and various features of the gateway listener.

    The following changes our log format to JSON

    shell
    kubectl apply -f kubernetes/gateway-api/kgateway/04-gateway-accesslogs.yaml
    
    # see log format
    kubectl logs -l app.kubernetes.io/name=gateway-api

    Policies: BackendConfigPolicy

    BackendConfigPolicy allows us to configure connection settings for a backend like a Kubernetes Service

    AgentGateway

    One thing that sets kgateway apart from the other gateway API's is its integration with Agent Gateway


    Overview (`kubernetes/gateway-api/linkerd/README.md`)

    Introduction to Linkerd: Gateway API

    linkerd

    Prerequisites

    To get started, you will need to follow the Introduction to Gateway API first.

    You'll need an understanding of the Gateway API.

    In the introduction guide, you will:

    • Create a local Kubernetes cluster
    • Install the Gateway API CRDs
    • Deploy example apps to our cluster (below)

    In this guide, we will deploy our videos application which has both north\south and east\west traffic:

    shell
    kubectl apply -f monitoring/opentelemetry/applications/playlists-api/
    kubectl apply -f monitoring/opentelemetry/applications/playlists-db/
    kubectl apply -f monitoring/opentelemetry/applications/videos-web/
    kubectl apply -f monitoring/opentelemetry/applications/videos-api/
    kubectl apply -f monitoring/opentelemetry/applications/videos-db/
    • Have Domains for our traffic

    This will allow us access to the Gateway API so we can go ahead and deploy a Gateway API controller to use. </br

    What is Linkerd

    Linkerd is a service mesh technology for Kubernetes that has gateway api support

    Linkerd is the first service mesh to achieve CNCF graduation status and is highly matured.

    In a distributed system with service-to-service communication, a service mesh uses various methods to intercept all inbound and outbound communication to and from a given service.

    So when a service makes an outbound call or has an inbound network call, the mesh is aware of it.

    Doing so, a service mesh provides many features:

    • observability: monitor network calls (metrics \ traces)
    • traffic management: traffic routing features
    • resillience: automatic retry, circuit breaker, rate limiting etc
    • security: mutual TLS, authentication, policies etc

    Each service mesh is different in terms of how it achieves this. Some meshes use side-cars to intercept and proxy traffic that go in and out of pods. Others use node level proxies and even eBPF to proxy at kernel level.

    Linkerd uses the sidecar approach with their proxy written in Rust.

    There are a few things that make Linkerd very different to other Gateway APIs we looked at.

    Linkerd is a big player in the The GAMMA Initiative for Gateway API.

    Linkerd does not focus on north/south traffic and therefore we will use an alternative gateway API implementation for that.

    Linkerd's focus is more about mesh traffic, which is east\west.

    Linkerd: Gateway API

    Linkerd provides Gateway API support included within its single installation.

    Unlike some other gateways, that provide Gateway API support through a separate helm chart, Linkerd is all one single simple install.

    The helm chart relies on the Gateway API CRD's we installed in our cluster already

    We can checkout the official install via helm for more instructions.

    Installation

    We firstly need to create mTLS certificates that Linkerd will use for secure internal communication in its service mesh

    shell
    apk add step-cli #alpine linux
    
    #use the linkerd CLI to check the install
    export LINKERD2_VERSION=edge-25.12.3
    curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install-edge | sh
    export PATH=$PATH:/root/.linkerd2/bin
    
    linkerd check --pre
    
    #generate CA
    step certificate create root.linkerd.cluster.local /tmp/ca.crt /tmp/ca.key \
    --profile root-ca --no-password --insecure
    
    #generate intermediate cert for csr's
    step certificate create identity.linkerd.cluster.local /tmp/issuer.crt /tmp/issuer.key \
    --profile intermediate-ca --not-after 8760h --no-password --insecure \
    --ca /tmp/ca.crt --ca-key /tmp/ca.key

    We can install the helm chart with helm install or upgrade it with helm upgrade
    Please note that I will be using the linkerd edge release in this demo to ensure I have the latest functionality. For production, you may want to use the stable release instead.

    shell
    CRD_CHART_VERSION="2025.12.3"
    CHART_VERSION="2025.12.3"
    
    helm repo add linkerd-edge https://helm.linkerd.io/edge
    helm repo update
    helm search repo linkerd-edge --versions | grep linkerd-edge/linkerd-crds | head
    helm search repo linkerd-edge --versions | grep linkerd-edge/linkerd-control-plane | head
    
    # get values files
    helm show values linkerd-edge/linkerd-control-plane > kubernetes/gateway-api/linkerd/default-values.yaml
    
    helm install linkerd-crds linkerd-edge/linkerd-crds \
      --version $CRD_CHART_VERSION \
      --namespace linkerd \
      --create-namespace
    
    helm install linkerd-control-plane \
      --version $CHART_VERSION \
      --namespace linkerd \
      --create-namespace \
      --set-file identityTrustAnchorsPEM=/tmp/ca.crt \
      --set-file identity.issuer.tls.crtPEM=/tmp/issuer.crt \
      --set-file identity.issuer.tls.keyPEM=/tmp/issuer.key \
      --values kubernetes/gateway-api/linkerd/values.yaml \
      linkerd-edge/linkerd-control-plane

    Check Installation

    Now we should have the Envoy gateway API controller up and running.

    This is not the gateway itself, but the controller that will manage the CRDs we get access to and implement some gateway API CRDs.

    shell
    # check the control plane pods
    kubectl -n linkerd get pods
    
    linkerd check

    Configuration

    Most of the Gateway API controllers are installed using helm.

    Before we install it, let's take a look at the default-values.yaml

    Linkerd also provide a High Availability YAML file for HA deployments which I found here on Github.

    So in this guide I will base my demo off that with the controllerReplicas set to 1 for demo purpose as I am running a single node cluster.

    Install a Gateway Class & Gateway

    Since Linderd does not have a gateway, we will be using Envoy from our Envoy Gateway Guide

    shell
    # deploy gateway class
    kubectl apply -f kubernetes/gateway-api/envoy/02.1-gateway-config.yaml
    
    # deploy a gateway
    kubectl apply -f kubernetes/gateway-api/envoy/02-gateway.yaml

    When we apply the gateway, we get a new gateway api pod.

    shell
    # check the new gateway-api pod
    kubectl -n envoy-gateway-system get pods
    
    # we also have a new service
    kubectl -n envoy-gateway-system get svc
    
    # port forward for access to the new gateway
    kubectl -n envoy-gateway-system port-forward svc/envoy-gateway-default 80

    Mesh our Services

    To add our services to the Linkerd mesh, we need to annotate each deployment

    shell
    kubectl get deploy playlists-api -o yaml | linkerd inject - | kubectl apply -f -
    kubectl get deploy playlists-db -o yaml | linkerd inject - | kubectl apply -f -
    kubectl get deploy videos-api -o yaml | linkerd inject - | kubectl apply -f -
    kubectl get deploy videos-db -o yaml | linkerd inject - | kubectl apply -f -
    kubectl get deploy videos-web -o yaml | linkerd inject - | kubectl apply -f -

    Linkerd Dashboard

    To access the linkerd dashboard, we need to install the viz extension

    text
    linkerd viz install | kubectl apply -f -
    kubectl -n linkerd-viz port-forward svc/web 8084

    HTTPRoutes: North \ South Traffic

    For north\south traffic, we will use Gateway API and setup a HTTP route

    shell
    kubectl apply -f kubernetes/gateway-api/linkerd/03-httproute.yaml

    HTTPRoutes - East \ West Traffic

    For east\west traffic features:

    Let's deploy a V2 of our videos-api that only returns a video about the Gateway API.

    shell
    # new V2
    kubectl apply -f kubernetes/gateway-api/linkerd/04-east-west-app.yaml
    
    # mesh the new app
    kubectl get deploy videos-api-v2 -o yaml | linkerd inject - | kubectl apply -f -
    
    # HTTPRoute that routes traffic east \ west
    kubectl apply -f kubernetes/gateway-api/linkerd/04-httproute-east-west.yaml

    You could use this for a number of reasons:

    • Inject latency
    • Inject faults to test applications
    • Canary deployments
    • Dynamic routing

    Overview (`kubernetes/gateway-api/nginx-fabric/README.md`)

    Introduction to NGINX Gateway Fabric: Gateway API

    nginx-fabric

    Prerequisites

    To get started, you will need to follow the Introduction to Gateway API first.

    You'll need an understanding of the Gateway API.

    In the introduction guide, you will:

    • Create a local Kubernetes cluster
    • Install the Gateway API CRDs
    • Deploy example apps to our cluster
    • Have Domains for our traffic
    • Have TLS certificates

    This will allow us access to the Gateway API so we can go ahead and deploy a Gateway API controller to use. </br

    What is NGINX Gateway Fabric

    NGINX Gateway Fabric is an open-source project that provides an implementation of the Gateway API using NGINX as the data plane.

    Let's checkout the Official Documentation

    Install a NGINX Gateway Class

    In our other guides, we deployed and managed a Gateway Class outside of the controller installation.

    NGINX Fabric is a little different as their GatewayClass is managed by helm and there are extra configurations managed by the chart that gets injected into the GatewayClass under the parametersRef field.

    If you've been following this series you will know the parametersRef field is used to extend the class. NGINX uses a custom CRD called NginxProxy to customize the class, but these settings come from their helm chart.

    For example:

    yaml
    apiVersion: gateway.networking.k8s.io/v1
    kind: GatewayClass
    metadata:
      name: nginx-gateway
      annotations:
        meta.helm.sh/release-name: ngf
        meta.helm.sh/release-namespace: nginx-gateway
      labels:
        app.kubernetes.io/managed-by: Helm
    spec:
      controllerName: gateway.nginx.org/nginx-gateway-controller
      parametersRef:
        group: gateway.nginx.org
        kind: NginxProxy
        name: ngf-proxy-config
        namespace: nginx-gateway

    NGINX Fabric: Gateway API controller

    NGINX Fabric covers most of the core Gateway API features.

    It also supports custom features using policies

    Configuration

    Most of the Gateway API controllers are installed using helm.

    Before we install it, let's take a look at the values.yaml

    The Helm chart allows us to configure many options. Some I find quite important:

    • Control plane deployment & Service
    • Data plane deployment & Service modification
    • Ports to use for incoming traffic
    • Control Gateway API
      • Default CRDs
      • Default GatewayClass
      • Default Gateways

    There are helm examples on Github maintained by the NGINX community.

    Here is the helm chart documentation

    It's always good to get a grip on the default helm values to see what we can do with the chart.

    The values file is used to customise NGINX control & data plane, underlying pods, deployments and services as well as turning features on and off.

    We can go ahead and install the controller to start with and use helm upgrade to make changes as we need to.

    Installation

    Install:

    shell
    CHART_VERSION="2.3.0"
    
    helm show values oci://ghcr.io/nginx/charts/nginx-gateway-fabric > kubernetes/gateway-api/nginx-fabric/default-values.yaml
    
    helm show chart oci://ghcr.io/nginx/charts/nginx-gateway-fabric
    
    helm install ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric \
      --values kubernetes/gateway-api/nginx-fabric/values.yaml \
      --version $CHART_VERSION \
      --namespace nginx-gateway \
      --create-namespace

    Upgrade:

    shell
    helm upgrade ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric \
      --values kubernetes/gateway-api/nginx-fabric/values.yaml \
      --version $CHART_VERSION \
      --namespace nginx-gateway

    Check our installation

    shell
    # check the pods
    kubectl -n nginx-gateway get pods
    
    # check the logs 
    kubectl -n nginx-gateway logs -l app.kubernetes.io/instance=ngf

    Install a NGINX Gateway

    So we have the GatewayClass managed by the helm chart, therefore we skip the 01-gatewayclass.yaml and go straight to the gateway.

    This will deploy NGINX dataplane pods to our target default namespace:

    shell
    kubectl apply -f kubernetes/gateway-api/nginx-fabric/02-gateway.yaml
    
    # check gateway pods
    kubectl get pods 
    
    #check logs
    kubectl logs -l app.kubernetes.io/instance=ngf
    
    # port forward for access
    kubectl port-forward svc/gateway-api-nginx-gateway 80

    Check & Configure the NGINX Dataplane

    We can configure our NGINX Gateway, using the spec.infrastructure.parametersRef and point a Gateway to a NginxProxy resource. We can attach this config to a Gateway or GatewayClass

    This allows us to control specific gateways or all gateways globally

    Let's create a configuration to set the number of replicas on our Gateway deployment and set our service to ClusterIP since we don't have a load balancer.

    Apply changes:

    shell
    kubectl apply -f kubernetes/gateway-api/nginx-fabric/02.1-gateway-config.yaml

    In addition -

    When we define HTTPRoute's for our traffic, The NGINX control plane detects those and works with the data plane pods to get an nginx.conf for the desired traffic rules.

    If you are familiar with NGINX you will know all traffic rules are in the nginx.conf. So this solution transforms K8s CRDs to NGINX rules under the hood

    shell
    # get a dataplane pod name
    kubectl get pods 
    pod=gateway-api-nginx-gateway-5899b6cd6f-dpkbh
    
    # check the generated nginx configuration file
    kubectl exec -it -n default $pod -- nginx -T

    HTTP Traffic management

    Feel free to quickly run through the basic traffic management table for using HTTPRoute routing for traffic.

    Note: HTTPRoute features are not specific to this controller and should be available to any other gateway API controller that you choose.

    Policy APIs

    Kubernetes Gateway API provides policy attachment that allows us to augment or add configuration to existing resources

    Client Settings Policies

    ClientSettingsPolicy is an NGINX Fabric CRD that allows us to customize how our Gateway behaves with client connections.

    We can set things like Keep Alive , Timeout settings and HTTP Body sizes.

    This CRD uses the above-mentioned policy attachment API for Gateway API.

    Ensure the policy is accepted

    shell
    kubectl apply -f kubernetes/gateway-api/nginx-fabric/08-clientsettings-keepalive.yaml
    
    # check 
    kubectl describe clientsettingspolicies gateway-client-settings

    Make more than 5 requests and notice every one re-using the connection except for the last:

    shell
    # make 6 requests
    curl -v http://example-app.com/api/go \
      http://example-app.com/api/go \
      http://example-app.com/api/go \
      http://example-app.com/api/go \
      http://example-app.com/api/go \
      http://example-app.com/api/go 
    
    # note connection re-use 
    * Re-using existing connection with host example-app.com

    SnippetsFilter API

    If you have used the NGINX Ingress controller, you may recall "custom snippets" , "server snippets" or "location snippets"

    SnippetsFilter is an API that allows NGINX Gateway Fabric to expose some of NGINX raw configuration for tweaking.

    Similar to NGINX Ingress, the NGINX proxy in Gateway Fabric also generates NGINX configuration files as demonstrated earlier. SnippetsFilter allows us to insert our own NGINX configuration into these files.

    This feature is enabled in our values.yaml file.

    shell
    kubectl apply -f kubernetes/gateway-api/nginx-fabric/09-snippetsfilter.yaml
    
    # check 
    kubectl describe SnippetsFilter custom-snippet

    Overview (`kubernetes/gateway-api/nginx-ingress/README.md`)

    Ingress-NGINX Migration to Gateway API

    nginx-ingress-migration

    Ingress Class

    shell
    kubectl get ingress -A -o custom-columns="NAMESPACE:.metadata.namespace,NAME:.metadata.name,CLASS:.spec.ingressClassName"

    Ingress to Gateway Tool

    shell
    wget -q "https://github.com/kubernetes-sigs/ingress2gateway/releases/download/v0.4.0/ingress2gateway_Linux_x86_64.tar.gz" -O /tmp/igtogw.tar.gz && \
      tar -C /tmp -xzf /tmp/igtogw.tar.gz && \
      mv /tmp/ingress2gateway /usr/local/bin/ingress2gateway && \
      chmod +x /usr/local/bin/ingress2gateway
    
    ./ingress2gateway print --providers=ingress-nginx

    Annotations Audit

    To get a list of all NGINX Ingress Annotations we are using:

    shell
    kubectl get ingress -A -o json  | \
      jq -r '.items[].metadata.annotations | keys[]' | \
      grep "nginx.ingress" | \
      sort -u

    Now we can generate our audit list by selecting the above annotated output:

    shell
    apk add util-linux
    audit_fields=(
      "NAMESPACE:.metadata.namespace"
      "NAME:.metadata.name"
      "HOSTS:.spec.rules[*].host"
      "MIGRATED:.metadata.labels.migrated"
      "REWRITE:.metadata.annotations.nginx\.ingress\.kubernetes\.io/rewrite-target"
      "SNIPPET:.metadata.annotations.nginx\.ingress\.kubernetes\.io/configuration-snippet"
      "AUTH:.metadata.annotations.nginx\.ingress\.kubernetes\.io/auth-type"
      "PathType:.spec.rules[*].http.paths[*].pathType"
      "TLS:.spec.tls[*].secretName"
    )
    
    IFS=$','
    column_args="${audit_fields[*]}"
    
    kubectl get ingress -A -o custom-columns="$column_args"

    Gateway API migration

    Feature Ingress Nginx Annotation Native Gateway API Resource / Filter Example
    Basic Routing <none> HTTPRoute example: Route by Domain
    Path Rewrite rewrite-target HTTPRoute → URLRewrite filter example: URL Rewrite
    TLS termination tls.secretName Gateway → Listeners → tls.certificateRefs example: TLS termination
    Redirects app-root, ssl-redirect HTTPRoute → RequestRedirect filter example: HTTP redirects
    Header Mod add-header HTTPRoute → RequestHeaderModifier filter
    Basic Auth auth-type: basic Policy Attachment (Targeting HTTPRoute) custom example: BasicAuth
    Canary/Weight canary-weight HTTPRoute → backendRefs.weight
    Mirroring mirror-target HTTPRoute → RequestMirror filter
    Backend TLS backend-protocol: HTTPS BackendTLSPolicy (Standard Object)
    Cross-Namespace (Standard Ingress) ReferenceGrant (Standard Object)
    Request Timeout proxy-read-timeout HTTPRoute → spec.rules.timeouts

    Examples

    In these examples, we will run port-forward to test the old and new endpoints using curl. You want to ensure you get similar or same expected outcomes to ensure compatibility on your new endpoint.

    shell
    # port-forward to ingress
    kubectl -n ingress-nginx port-forward svc/ingress-nginx-controller 80
    # port-forward to gateway
    kubectl -n traefik port-forward svc/traefik 80

    Route by Domain

    Before:

    yaml
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: python-route
    spec:
      ingressClassName: nginx
      rules:
      - host: example-app-python.com
        http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: python-svc
                port:
                  number: 5000
    ---
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: go-route
    spec:
      ingressClassName: nginx
      rules:
      - host: example-app-go.com
        http:
          paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: go-svc
                port:
                  number: 5000

    This is what the HTTPRoute:Route by Domain looks like.

    shell
    kubectl apply -f kubernetes/gateway-api/03-httproute-by-hostname.yaml

    Test:

    shell
    curl example-app-python.com
    Hello from Python!
    
    curl example-app-go.com
    Hello from Go

    Mark as done

    text
    kubectl label ingress go-route python-route migrated=true

    Path Rewrite

    Before:

    yaml
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      annotations:
        nginx.ingress.kubernetes.io/rewrite-target: /$2
      name: python-route-bypath
      namespace: default
    spec:
      ingressClassName: nginx
      rules:
      - host: example-app.com
        http:
          paths:
          - backend:
              service:
                name: python-svc
                port:
                  number: 5000
            path: /api/python(/|$)(.*)
            pathType: ImplementationSpecific
    ---
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      annotations:
        nginx.ingress.kubernetes.io/rewrite-target: /$2
      name: go-route-bypath
      namespace: default
    spec:
      ingressClassName: nginx
      rules:
      - host: example-app.com
        http:
          paths:
          - backend:
              service:
                name: go-svc
                port:
                  number: 5000
            path: /api/go(/|$)(.*)
            pathType: ImplementationSpecific

    After:

    yaml
    apiVersion: gateway.networking.k8s.io/v1
    kind: HTTPRoute
    metadata:
      name: python-route-bypath
      namespace: default
    
    spec:
      parentRefs:
      - name: gateway-api
        sectionName: http
        kind: Gateway
    
      hostnames:
      - example-app.com
    
      rules:
      - matches:
        - path: 
            type: PathPrefix
            value: /api/python
        filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
        backendRefs:
        - name: python-svc
          port: 5000
          weight: 1
    ---
    
    apiVersion: gateway.networking.k8s.io/v1
    kind: HTTPRoute
    metadata:
      name: go-route-bypath
      namespace: default
    
    spec:
      parentRefs:
      - name: gateway-api
        sectionName: http
        kind: Gateway
    
      hostnames:
      - example-app.com
    
      rules:
      - matches:
        - path:
            type: PathPrefix
            value: /api/go 
        filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
        backendRefs:
        - name: go-svc
          port: 5000
          weight: 1

    Test:

    shell
    curl example-app.com/api/python
    Hello from Python!
    
    curl example-app.com/api/go
    Hello from Go

    Mark as done

    text
    kubectl label ingress go-route-bypath python-route-bypath  migrated=true

    TLS Termination

    Before:

    yaml
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: go-route-tls
      namespace: default
      annotations:
        nginx.ingress.kubernetes.io/rewrite-target: /$2
    spec:
      ingressClassName: nginx
      tls:
      - hosts:
        - example-app.com
        secretName: secret-tls
      rules:
      - host: example-app.com
        http:
          paths:
          - path: /api/go/tls(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: go-svc
                port:
                  number: 5000

    After:

    yaml
    apiVersion: gateway.networking.k8s.io/v1
    kind: HTTPRoute
    metadata:
      name: go-route-tls
      namespace: default
    
    spec:
      parentRefs:
      - name: gateway-api
        sectionName: http
        kind: Gateway
      - name: gateway-api
        sectionName: https
        kind: Gateway
    
      hostnames:
      - example-app.com
      rules:
      - matches:
        - path:
            type: PathPrefix
            value: /api/go/tls
        filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
        backendRefs:
        - name: go-svc
          port: 5000
          weight: 1

    We can use our port-forward on port 443 to test the TLS in the browser

    Mark as done

    text
    kubectl label ingress go-route-tls python-route migrated=true

    HTTP Redirects

    Before:

    yaml
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: python-route-tls
      namespace: default
      annotations:
        nginx.ingress.kubernetes.io/ssl-redirect: "true"
        nginx.ingress.kubernetes.io/rewrite-target: /$2
    spec:
      ingressClassName: nginx
      tls:
      - hosts:
        - example-app.com
        secretName: secret-tls
      rules:
      - host: example-app.com
        http:
          paths:
          - path: /api/python/tls(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: python-svc
                port:
                  number: 5000

    After:

    yaml
    apiVersion: gateway.networking.k8s.io/v1
    kind: HTTPRoute
    metadata:
      name: python-route-tls-redirect
      namespace: default
    spec:
      parentRefs:
      - name: gateway-api
        sectionName: http
      hostnames:
      - example-app.com
      rules:
      - matches:
        - path:
            type: PathPrefix
            value: /api/python/tls
        filters:
        - type: RequestRedirect
          requestRedirect:
            scheme: https
            statusCode: 301
    ---
    apiVersion: gateway.networking.k8s.io/v1
    kind: HTTPRoute
    metadata:
      name: python-route-tls
      namespace: default
    
    spec:
      parentRefs:
      - name: gateway-api
        sectionName: https
        kind: Gateway
    
      hostnames:
      - example-app.com
      rules:
      - matches:
        - path:
            type: PathPrefix
            value: /api/python/tls
        filters:
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
        backendRefs:
        - name: python-svc
          port: 5000
          weight: 1

    Test:

    shell
    # before
    curl http://example-app.com/api/python/tls/
    <html>
    <head><title>308 Permanent Redirect</title></head>
    
    <h1>308 Permanent Redirect</h1>
    <hr>nginx
    
    </html>
    
    # after 
    curl http://example-app.com/api/python/tls/ -v
    < HTTP/1.1 301 Moved Permanently
    < Location: https://example-app.com/api/python/tls/

    Mark as done

    text
    kubectl label ingress python-route-tls migrated=true

    Basic Auth (Custom Feature)

    Create a Basic Auth secret:

    shell
    apk add apache2-utils
    htpasswd -c auth python
    kubectl create secret generic python-route-auth-secret --from-file=auth

    Before:

    yaml
    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: python-route-auth
      annotations:
        nginx.ingress.kubernetes.io/auth-type: basic
        nginx.ingress.kubernetes.io/auth-secret: python-route-auth-secret
        nginx.ingress.kubernetes.io/auth-secret-type: auth-file
        nginx.ingress.kubernetes.io/rewrite-target: /$2
    spec:
      ingressClassName: nginx
      rules:
      - host: example-app.com
        http:
          paths:
          - path: /api/python/login(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: python-svc
                port:
                  number: 5000

    After:

    yaml
    apiVersion: gateway.networking.k8s.io/v1
    kind: HTTPRoute
    metadata:
      name: python-route-auth
      namespace: default
    spec:
      parentRefs:
      - name: gateway-api
        sectionName: http
        kind: Gateway
      hostnames:
      - "example-app.com"
      rules:
      - matches:
        - path:
            type: PathPrefix
            value: /api/python/login
        filters:
        - type: ExtensionRef
          extensionRef:
            group: traefik.io
            kind: Middleware
            name: basic-auth
        - type: URLRewrite
          urlRewrite:
            path:
              type: ReplacePrefixMatch
              replacePrefixMatch: /
        backendRefs:
        - name: python-svc
          port: 5000
    ---
    apiVersion: traefik.io/v1alpha1
    kind: Middleware
    metadata:
      name: basic-auth
    spec:
      basicAuth:
        secret: python-route-auth-secret

    Test:

    shell
    #Before:
    curl http://example-app.com/api/python/login
    <html>
    <head><title>401 Authorization Required</title></head>
    
    <h1>401 Authorization Required</h1>
    <hr>nginx
    
    </html>
    
    #After:
    curl http://public.my-services.com/path-a/login
    401 Unauthorized

    Mark as done

    text
    kubectl label ingress python-route-auth migrated=true

    Overview (`kubernetes/gateway-api/traefik/README.md`)

    Introduction to Traefik: Gateway API

    traefik

    Prerequisites

    To get started, you will need to follow the Introduction to Gateway API first.

    You'll need an understanding of the Gateway API.

    In the introduction guide, you will:

    • Create a local Kubernetes cluster
    • Install the Gateway API CRDs
    • Deploy example apps to our cluster
    • Have Domains for our traffic
    • Have TLS certificates

    This will allow us access to the Gateway API so we can go ahead and deploy a Gateway API controller to use. </br

    Install a Traefik Gateway Class

    shell
    kubectl apply -f kubernetes/gateway-api/traefik/01-gatewayclass.yaml

    Install a Traefik Gateway

    shell
    kubectl apply -f kubernetes/gateway-api/traefik/02-gateway.yaml

    Let's start with the Official Documentation

    Traefik: Gateway API controller

    Traefik has a lot of features, including Ingress controller functionality.

    In this guide we will turn on the Gateway API feature in Traefik and use that specifically.

    Configuration

    Most of the Gateway API controllers are installed using helm.

    Before we install it, let's take a look at the values.yaml

    Traefik allows us to configure many options. Some I find quite important:

    • Access logs - fields and format
    • Ports to use for incoming traffic
    • Infrastructure annotations \ labels
    • Control Gateway API
      • Default CRDs
      • Default GatewayClass
      • Default Gateways

    It's always good to get a grip on the default helm values

    The values file is used to customise Traefik, underlying pods, deployments and services as well as turning features on and off.

    We can go ahead and install the controller to start with and use helm upgrade to make changes as we need to.

    Installation

    Install:

    shell
    CHART_VERSION="37.3.0" # traefik version v3.6.0
    helm repo add traefik https://helm.traefik.io/traefik
    helm repo update
    helm search repo traefik --versions
    
    
    helm install traefik traefik/traefik \
      --version $CHART_VERSION \
      --values kubernetes/gateway-api/traefik/values.yaml \
      --namespace traefik \
      --create-namespace

    Upgrade:

    shell
    helm upgrade traefik traefik/traefik \
      --version $CHART_VERSION \
      --values kubernetes/gateway-api/traefik/values.yaml \
      --namespace traefik

    Check our installation

    shell
    # check the pods
    kubectl -n traefik get pods
    
    # check the logs 
    kubectl -n traefik logs -l app.kubernetes.io/instance=traefik-traefik
    
    # port forward for access
    kubectl -n traefik port-forward svc/traefik 80

    HTTP Traffic management

    Feel free to quickly run through the basic traffic management table for using HTTPRoute routing for traffic.

    Note: HTTPRoute features are not specific to this controller and should be available to any other gateway API controller that you choose.

    Middlewares

    One of the key strengths of using Traefik is that they have a catalogue of middlewares that are plugins to perform popular common actions on traffic.

    There are many Middlewares available.

    The cool thing here is that these Middlewares are now pluggable into Gateway API HTTPRoutes.

    In HTTPRoute objects, under filters, we can add extensions using something like:

    yaml
    filters:
    - type: ExtensionRef
      extensionRef:
        group: traefik.io
        kind: Middleware
        name: add-prefix

    Prefix

    Note: To use ExtensionRef in Traefik you need to enable providers.kubernetesCRD.enabled = true. We have that enabled in our values.yaml file.

    Expectation:

    http://example-app.com/anything 👉🏽 http://go-svc:5000/prefix/anything

    shell
    kubectl apply -f kubernetes/gateway-api/traefik/08-middleware-addprefix.yaml

    We can follow the log of our upstream to see the HTTP request:

    shell
    kubectl logs -f -l app=go-app

    Basic Auth

    We can also add HTTP basic authentication to allow users to access secured resources protected by username and passwords.

    The Basic Auth middleware points to a secret which supports Kubernetes basic auth secret type.

    shell
    kubectl apply -f kubernetes/gateway-api/traefik/09-middleware-basicauth.yaml

    Headers

    Traefik allows us to manipulate headers too using Middleware component called headers

    We can use this to handle scenarios like CORS (Cross-Origin Resource Sharing)

    Let's access our web app directly over localhost which makes a call to example-app.com and should be blocked by CORS.

    shell
    kubectl port-forward svc/web-app 8000:80

    Once we apply the Middleware update to our HTTPRoute, we can perform cross origin API calls:

    shell
    kubectl apply -f kubernetes/gateway-api/traefik/10-middleware-headers.yaml

    Overview (`kubernetes/secrets/sealed-secrets/README.md`)

    Introduction to Sealed Secrets

    k8s-sealedsecrets

    Checkout the Sealed Secrets GitHub Repo

    There are a number of use-cases where this is a really great concept.

    1. GitOps - Storing your YAML manifests in Git and using GitOps tools to sync the manifests to your clusters (For example Flux and ArgoCD!)

    2. Giving a team access to secrets without revealing the secret material.

    developer: "I want to confirm my deployed secret value is X in the cluster"

    developer can compare sealedSecret YAML in Git, with the sealedSecret in the cluster and confirm the value is the same.

    Create a kubernetes cluster

    In this guide we we'll need a Kubernetes cluster for testing. Let's create one using kind

    text
    kind create cluster --name sealedsecrets --image kindest/node:v1.23.5

    See cluster up and running:

    text
    kubectl get nodes
    NAME                  STATUS   ROLES                  AGE     VERSION
    sealedsecrets-control-plane   Ready    control-plane,master   2m12s   v1.23.5

    Run a container to work in

    run Alpine Linux:

    text
    docker run -it --rm -v ${HOME}:/root/ -v ${PWD}:/work -w /work --net host alpine sh

    install kubectl

    text
    apk add --no-cache curl
    curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
    chmod +x ./kubectl
    mv ./kubectl /usr/local/bin/kubectl

    install helm

    text
    curl -o /tmp/helm.tar.gz -LO https://get.helm.sh/helm-v3.10.1-linux-amd64.tar.gz
    tar -C /tmp/ -zxvf /tmp/helm.tar.gz
    mv /tmp/linux-amd64/helm /usr/local/bin/helm
    chmod +x /usr/local/bin/helm

    test cluster access:

    text
    /work # kubectl get nodes
    NAME                    STATUS   ROLES    AGE   VERSION
    sealedsecrets-control-plane   Ready    control-plane,master   3m26s   v1.23.5

    Install Sealed Secret Controller

    download the YAML

    In this demo we'll use version 0.19.1 of the sealed secrets controller downloaded from the
    Github releases page

    text
    curl -L -o ./kubernetes/secrets/sealed-secrets/controller-v0.19.1.yaml https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.19.1/controller.yaml

    install using Helm

    You can also install the controller using helm

    text
    helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
    helm search repo sealed-secrets --versions
    helm template sealed-secrets --version 2.7.0 -n kube-system sealed-secrets/sealed-secrets \
    > ./kubernetes/secrets/sealed-secrets/controller-helm-v0.19.1.yaml

    With helm template we can explore the YAML and then replace the helm template with helm install
    to install the chart

    install using YAML manifest

    text
    kubectl apply -f kubernetes/secrets/sealed-secrets/controller-v0.19.1.yaml

    Check the installation

    The controller deploys to the kube-system namespace by default.

    text
    kubectl -n kube-system get pods

    Check the logs of the sealed secret controller

    text
    kubectl -n kube-system logs -l name=sealed-secrets-controller --tail -1

    From the logs we can see that it writes the encryption key its going to use as a kubernetes secret

    Example log:

    text
    2022/11/05 21:38:20 New key written to kube-system/sealed-secrets-keymwzn9

    Encryption keys

    text
    kubectl -n kube-system get secrets
    kubectl -n kube-system get secret sealed-secrets-keygxlvg -o yaml

    Download KubeSeal

    The same way we downloaded the sealed secrets controller from the GitHub releases page,
    we'll want to download kubeseal from the assets section

    text
    curl -L -o /tmp/kubeseal.tar.gz \
    https://github.com/bitnami-labs/sealed-secrets/releases/download/v0.19.1/kubeseal-0.19.1-linux-amd64.tar.gz
    tar -xzf /tmp/kubeseal.tar.gz -C /tmp/
    chmod +x /tmp/kubeseal
    mv /tmp/kubeseal /usr/local/bin/

    We can now run kubeseal --help

    Sealing a basic Kubernetes Secret

    Looks at our existing Kubernetes secret YAML

    text
    cat kubernetes/secrets/secret.yaml

    If you run kubeseal you will see it pause and expect input from stdin.

    You can paste your secret YAML and press CTRL+D to terminate stdin.

    You will notice it writes a sealedSecret to stdout.

    We can then automate this using | characters.

    Create a sealed secret using stdin :

    text
    cat kubernetes/secrets/secret.yaml | kubeseal -o yaml  > kubernetes/secrets/sealed-secrets/sealed-secret.yaml

    Create a sealed secret using a YAML file:

    text
    kubeseal -f kubernetes/secrets/secret.yaml -o yaml > kubernetes/secrets/sealed-secrets/sealed-secret.yaml

    Deploy the sealed secret

    text
    kubectl apply -f kubernetes/secrets/sealed-secrets/sealed-secret.yaml

    Now few seconds later, see the secret

    text
    kubectl -n default get secret
    NAME                  TYPE                                  DATA   AGE
    mysecret              Opaque                                1      25s

    How the encryption key is managed

    By default the controller generates a key as we saw earlier and stores it in a Kubernetes secret.

    By default, the controller will generate a new active key every 30 days.
    It keeps old keys so it can decrypt previous encrypted sealed secrets and will use the active key with new encryption.

    It's important to keep these keys secured.

    When the controller starts it consumes all the secrets and will start using them

    This means we can backup these keys in a Vault and use them to migrate our clusters if we wanted to.

    We can also override the renewal period to increase or decrease the value. 0 turns it off

    To showcase this I can set --key-renew-period=<value> to 5min to watch how it works.

    text
    apk add nano
    export KUBE_EDITOR=nano

    Set the flag on the command like so to add a new key every 5 min for testing:

    text
    spec:
      containers:
      - command:
        - controller
        - --key-renew-period=5m
    
    kubectl edit deployment/sealed-secrets-controller --namespace=kube-system

    You should see a new key created under secrets in the kube-system namespace

    text
    kubectl -n kube-system get secrets

    Backup your encryption keys

    To get your keys out for backup purpose, it's as simple as grabbing a secret by label using kubectl :

    text
    kubectl get secret -n kube-system \
      -l sealedsecrets.bitnami.com/sealed-secrets-key \
      -o yaml \
      >  kubernetes/secrets/sealed-secrets/sealed-secret-keys.key

    This can be used when migrating from one cluster to another, or simply for keeping backups.

    Migrate your encryption keys to a new cluster

    To test this, lets delete our cluster and recreate it.

    text
    kind delete cluster --name sealedsecrets
    kind create cluster --name sealedsecrets --image kindest/node:v1.23.5
    
    # check the cluster
    kubectl get nodes
    
    # redeploy sealed-secrets controller
    kubectl apply -f kubernetes/secrets/sealed-secrets/controller-v0.19.1.yaml
    
    kubectl -n kube-system get pods

    restore our encryption keys

    text
    kubectl apply -f kubernetes/secrets/sealed-secrets/sealed-secret-keys.key

    apply our old sealed secret

    text
    kubectl apply -f kubernetes/secrets/sealed-secrets/sealed-secret.yaml

    see sealed secret status

    To troubleshoot the secret, you can use the popular kubectl describe command.

    Note that we're unable to decrypt the secret.

    Why is that ?

    We'll this is because the encryption key secrets are read when the controller starts.

    So we will need to restart the controller to that it can read ingest the encryption keys:

    text
    kubectl delete pod -n kube-system -l name=sealed-secrets-controller

    Re-encrypting secrets with the latest key

    We can also use kubeseal --re-encrypt to encrypt a secret again.

    Let's say we want to encrypt with the latest key.

    This will re-encrypt the sealed secret without having to pull the actual secret to the client

    text
    cat ./kubernetes/secrets/sealed-secrets/sealed-secret.yaml \
    | kubeseal --re-encrypt -o yaml

    I can then save this to override the original old local sealed secret file:

    text
    cat ./kubernetes/secrets/sealed-secrets/sealed-secret.yaml \
    | kubeseal --re-encrypt -o yaml \
    > tmp.yaml && mv tmp.yaml ./kubernetes/secrets/sealed-secrets/sealed-secret.yaml

    Overview (`kubernetes/servicemesh/istio/README.md`)

    Introduction to Istio

    We need a Kubernetes cluster

    Lets create a Kubernetes cluster to play with using kind

    text
    kind create cluster --name istio --image kindest/node:v1.19.1

    Deploy our microservices (Video catalog)

    text
    # ingress controller
    kubectl create ns ingress-nginx
    kubectl apply -f monitoring/opentelemetry/applications/ingress-nginx/
    
    # applications
    kubectl apply -f monitoring/opentelemetry/applications/playlists-api/
    kubectl apply -f monitoring/opentelemetry/applications/playlists-db/
    kubectl apply -f monitoring/opentelemetry/applications/videos-web/
    kubectl apply -f monitoring/opentelemetry/applications/videos-api/
    kubectl apply -f monitoring/opentelemetry/applications/videos-db/

    Make sure our applications are running

    text
    kubectl get pods
    NAME                            READY   STATUS    RESTARTS   AGE  
    playlists-api-d7f64c9c6-rfhdg   1/1     Running   0          2m19s
    playlists-db-67d75dc7f4-p8wk5   1/1     Running   0          2m19s
    videos-api-7769dfc56b-fsqsr     1/1     Running   0          2m18s
    videos-db-74576d7c7d-5ljdh      1/1     Running   0          2m18s
    videos-web-598c76f8f-chhgm      1/1     Running   0          100s

    Make sure our ingress controller is running

    text
    kubectl -n ingress-nginx get pods
    NAME                                        READY   STATUS    RESTARTS   AGE  
    nginx-ingress-controller-6fbb446cff-8fwxz   1/1     Running   0          2m38s
    nginx-ingress-controller-6fbb446cff-zbw7x   1/1     Running   0          2m38s

    We'll need a fake DNS name servicemesh.demo

    Let's fake one by adding the following entry in our hosts (C:\Windows\System32\drivers\etc\hosts) file:

    text
    127.0.0.1  servicemesh.demo

    Let's access our applications via Ingress

    text
    kubectl -n ingress-nginx port-forward deploy/nginx-ingress-controller 80

    Access our application in the browser

    We should be able to access our site under http://servicemesh.demo/home/



    Getting Started with Istio

    Firstly, I like to do most of my work in containers so everything is reproducable

    and my machine remains clean.

    Get a container to work in


    Run a small `alpine linux` container where we can install and play with `istio`:
    text
    docker run -it --rm -v ${HOME}:/root/ -v ${PWD}:/work -w /work --net host alpine sh
    
    # install curl & kubectl
    apk add --no-cache curl nano
    curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
    chmod +x ./kubectl
    mv ./kubectl /usr/local/bin/kubectl
    export KUBE_EDITOR="nano"
    
    #test cluster access:
    /work # kubectl get nodes
    NAME                    STATUS   ROLES    AGE   VERSION
    istio-control-plane   Ready    master   26m   v1.18.4

    Install Istio CLI

    text
    curl -L https://istio.io/downloadIstio | ISTIO_VERSION=1.6.12 TARGET_ARCH=x86_64 sh -
    
    mv istio-1.6.12/bin/istioctl /usr/local/bin/
    chmod +x /usr/local/bin/istioctl
    mv istio-1.6.12 /tmp/

    Pre flight checks

    Istio has a great capability to check compatibility with the target cluster

    text
    istioctl x precheck

    Istio Profiles

    https://istio.io/latest/docs/setup/additional-setup/config-profiles/

    text
    istioctl profile list
    
    istioctl install --set profile=default
    
    kubectl -n istio-system get pods
    
    istioctl proxy-status

    Mesh our video catalog services

    There are 2 ways to mesh:

    1. Automated Injection:

    You can set the istio-injection=enabled label on a namespace to have the istio side car automatically injected into any pod that gets created in the labelled namespace

    This is a more permanent solution:
    Pods will need to be recreated for injection to occur

    text
    kubectl label namespace/default istio-injection=enabled
    
    # restart all pods to get sidecar injected
    kubectl delete pods --all
    1. Manual Injection:
      This may only be temporary as your CI/CD system may roll out the previous YAML.
      You may want to add this command to your CI/CD to keep only certain deployments part of the mesh.
    text
    kubectl get deploy
    NAME            READY   UP-TO-DATE   AVAILABLE   AGE
    playlists-api   1/1     1            1           8h 
    playlists-db    1/1     1            1           8h 
    videos-api      1/1     1            1           8h 
    videos-db       1/1     1            1           8h 
    videos-web      1/1     1            1           8h 
    
    # Lets manually inject istio sidecar into our Ingress Controller:
    
    kubectl -n ingress-nginx get deploy nginx-ingress-controller  -o yaml | istioctl kube-inject -f - | kubectl apply -f -
    
    # You can manually inject istio sidecar to every deployment like this:
    
    kubectl get deploy playlists-api -o yaml | istioctl kube-inject -f - | kubectl apply -f -
    kubectl get deploy playlists-db -o yaml | istioctl kube-inject -f - | kubectl apply -f -
    kubectl get deploy videos-api -o yaml | istioctl kube-inject -f - | kubectl apply -f -
    kubectl get deploy videos-db -o yaml | istioctl kube-inject -f - | kubectl apply -f -
    kubectl get deploy videos-web -o yaml | istioctl kube-inject -f - | kubectl apply -f -

    TCP \ HTTP traffic

    Let's run a curl loop to generate some traffic to our site

    We'll make a call to /home/ and to simulate the browser making a call to get the playlists,

    we'll make a follow up call to /api/playlists

    text
    While ($true) { curl -UseBasicParsing http://servicemesh.demo/home/;curl -UseBasicParsing http://servicemesh.demo/api/playlists; Start-Sleep -Seconds 1;}

    Observability

    Grafana

    text
    kubectl apply -n istio-system -f /tmp/istio-1.6.12/samples/addons/grafana.yaml

    We can see the components in the istio-system namespace:

    text
    kubectl -n istio-system get pods

    Access grafana dashboards :

    text
    kubectl -n istio-system port-forward svc/grafana 3000

    Kiali

    NOTE: this may fail because CRDs need to generate, if so, just rerun the command:

    text
    kubectl apply -f /tmp/istio-1.6.12/samples/addons/kiali.yaml
    
    kubectl -n istio-system get pods
    kubectl -n istio-system port-forward svc/kiali 20001

    Virtual Services

    Auto Retry

    Let's add a fault in the videos-api by setting env variable FLAKY=true

    text
    kubectl edit deploy videos-api
    text
    kubectl apply -f kubernetes/servicemesh/istio/retries/videos-api.yaml

    We can describe pods using istioctl

    text
    # istioctl x describe pod <videos-api-POD-NAME>
    
    istioctl x describe pod videos-api-584768f497-jjrqd
    Pod: videos-api-584768f497-jjrqd
       Pod Ports: 10010 (videos-api), 15090 (istio-proxy)
    Suggestion: add 'version' label to pod for Istio telemetry.
    --------------------
    Service: videos-api
       Port: http 10010/HTTP targets pod port 10010
    VirtualService: videos-api
       1 HTTP route(s)

    Analyse our namespace:

    text
    istioctl analyze --namespace default

    Traffic Splits

    Let's deploy V2 of our application which has a header that's under development

    text
    kubectl apply -f kubernetes/servicemesh/istio/traffic-splits/videos-web-v2.yaml
    
    # we can see v2 pods
    kubectl get pods

    Let's send 50% of traffic to V1 and 50% to V2 by using a VirtualService

    text
    kubectl apply -f kubernetes/servicemesh/istio/traffic-splits/videos-web.yaml

    Canary Deployments

    Traffic splits has its uses, but sometimes we may want to route traffic to other

    parts of the system using feature toggles, for example, setting a cookie



    Let's send all users that have the cookie value version=v2 to V2 of our videos-web.

    text
    kubectl apply -f kubernetes/servicemesh/istio/canary/videos-web.yaml

    We can confirm this works, by setting the cookie value version=v2 followed by accessing https://servicemesh.demo/home/ on a browser page


    Overview (`kubernetes/servicemesh/linkerd/readme.md`)

    Introduction to Linkerd

    We need a Kubernetes cluster

    Lets create a Kubernetes cluster to play with using kind

    text
    kind create cluster --name linkerd --image kindest/node:v1.30.4

    Deploy our microservices (Video catalog)

    text
    # ingress controller
    kubectl create ns ingress-nginx
    kubectl apply -f monitoring/opentelemetry/ingress-nginx/
    
    # applications
    kubectl apply -f monitoring/opentelemetry/applications/playlists-api/
    kubectl apply -f monitoring/opentelemetry/applications/playlists-db/
    kubectl apply -f monitoring/opentelemetry/applications/videos-web/
    kubectl apply -f monitoring/opentelemetry/applications/videos-api/
    kubectl apply -f monitoring/opentelemetry/applications/videos-db/

    Make sure our applications are running

    text
    kubectl get pods
    NAME                            READY   STATUS    RESTARTS   AGE  
    playlists-api-d7f64c9c6-rfhdg   1/1     Running   0          2m19s
    playlists-db-67d75dc7f4-p8wk5   1/1     Running   0          2m19s
    videos-api-7769dfc56b-fsqsr     1/1     Running   0          2m18s
    videos-db-74576d7c7d-5ljdh      1/1     Running   0          2m18s
    videos-web-598c76f8f-chhgm      1/1     Running   0          100s

    Make sure our ingress controller is running

    text
    kubectl -n ingress-nginx get pods
    NAME                                       READY   STATUS      RESTARTS   AGE
    ingress-nginx-admission-create-fxzx8       0/1     Completed   0          9m2s
    ingress-nginx-admission-patch-fwc2k        0/1     Completed   2          9m2s
    ingress-nginx-controller-d49697d5f-6qggd   1/1     Running     0          9m2s

    We'll need a fake DNS name servicemesh.demo

    Let's fake one by adding the following entry in our hosts (C:\Windows\System32\drivers\etc\hosts) file:

    text
    127.0.0.1  servicemesh.demo

    Let's access our applications via Ingress

    text
    kubectl -n ingress-nginx port-forward svc/ingress-nginx-controller 80

    Access our application in the browser

    We should be able to access our site under http://servicemesh.demo/home/



    Getting Started with Linkerd

    Firstly, I like to do most of my work in containers so everything is reproducible

    and my machine remains clean.

    Get a container to work in


    Run a small `alpine linux` container where we can install and play with `linkerd`:
    text
    docker run -it --rm -v ${HOME}:/root/ -v ${PWD}:/work -w /work --net host alpine sh
    
    # install curl & kubectl
    apk add --no-cache curl nano
    curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/linux/amd64/kubectl
    chmod +x ./kubectl
    mv ./kubectl /usr/local/bin/kubectl
    export KUBE_EDITOR="nano"
    
    #test cluster access:
    /work # kubectl get nodes
    NAME                    STATUS   ROLES           AGE   VERSION
    linkerd-control-plane   Ready    control-plane   40m   v1.30.4

    Linkerd CLI

    Lets download the linkerd command line tool

    I grabbed the edge-24.11.3 release using curl

    You can go to the releases page to get it

    text
    curl -L -o linkerd https://github.com/linkerd/linkerd2/releases/download/edge-24.11.3/linkerd2-cli-edge-24.11.3-linux-amd64 
    chmod +x linkerd && mv ./linkerd /usr/local/bin/
    
    linkerd --help

    Pre flight checks

    Linkerd has a great capability to check compatibility with the target cluster

    text
    linkerd check --pre

    Get the YAML

    text
    # install CRDs first
    linkerd install --crds > ./kubernetes/servicemesh/linkerd/manifest/linkerd-edge-24.11.3-crds.yaml
    linkerd install  > ./kubernetes/servicemesh/linkerd/manifest/linkerd-edge-24.11.3.yaml

    Install Linkerd

    text
    kubectl apply -f ./kubernetes/servicemesh/linkerd/manifest/linkerd-edge-24.11.3-crds.yaml
    kubectl apply -f ./kubernetes/servicemesh/linkerd/manifest/linkerd-edge-24.11.3.yaml

    Let's wait until all components are running

    text
    watch kubectl -n linkerd get pods
    kubectl -n linkerd get svc

    Do a final check

    text
    linkerd check

    The dashboard

    To access the linkerd dashboard, we need to install the viz extension

    text
    linkerd viz install | kubectl apply -f -
    kubectl -n linkerd-viz port-forward svc/web 8084

    Mesh our video catalog services

    There are 2 ways to mesh:

    1. We can add an annotation to your deployment to persist the mesh if our YAML is part of a GitOps flow:
      This is a more permanent solution:
    text
    template:
        metadata:
          annotations:
            linkerd.io/inject: enabled
    1. Or inject linkerd on the fly:
      This may only be temporary as your CI/CD system may roll out the previous YAML
    text
    kubectl get deploy
    NAME            READY   UP-TO-DATE   AVAILABLE   AGE
    playlists-api   1/1     1            1           8h 
    playlists-db    1/1     1            1           8h 
    videos-api      1/1     1            1           8h 
    videos-db       1/1     1            1           8h 
    videos-web      1/1     1            1           8h 
    
    kubectl get deploy playlists-api -o yaml | linkerd inject - | kubectl apply -f -
    kubectl get deploy playlists-db -o yaml | linkerd inject - | kubectl apply -f -
    kubectl get deploy videos-api -o yaml | linkerd inject - | kubectl apply -f -
    kubectl get deploy videos-db -o yaml | linkerd inject - | kubectl apply -f -
    kubectl get deploy videos-web -o yaml | linkerd inject - | kubectl apply -f -
    kubectl -n ingress-nginx get deploy ingress-nginx-controller  -o yaml | linkerd inject - | kubectl apply -f -

    Generate some traffic

    Let's run a curl loop to generate some traffic to our site

    We'll make a call to /home/ and to simulate the browser making a call to get the playlists,

    we'll make a follow up call to /api/playlists

    text
    While ($true) { curl -UseBasicParsing http://servicemesh.demo/home/;curl -UseBasicParsing http://servicemesh.demo/api/playlists; Start-Sleep -Seconds 1;}
    
    linkerd -n default check --proxy
    
    linkerd -n default stat deploy

    Add Faulty behaviour in videos API

    text
    kubectl edit deploy videos-api
    
    #set environment FLAKY=true

    Service Profile

    text
    linkerd profile -n default videos-api --tap deploy/videos-api --tap-duration 10s

    After crafting the serviceprofile, we can apply it using kubectl

    text
    kubectl apply -f kubernetes/servicemesh/linkerd/serviceprofiles/videos-api.yaml

    We can see that service profile helps us add retry policies in place:

    text
    linkerd routes -n default deploy/playlists-api --to svc/videos-api -o wide
    linkerd top deploy/videos-api

    Mutual TLS

    We can validate if mTLS is working

    text
    /work # linkerd -n default edges deployment
    SRC                  DST             SRC_NS    DST_NS    SECURED       
    playlists-api        videos-api      default   default   √
    linkerd-prometheus   playlists-api   linkerd   default   √
    linkerd-prometheus   playlists-db    linkerd   default   √
    linkerd-prometheus   videos-api      linkerd   default   √
    linkerd-prometheus   videos-db       linkerd   default   √
    linkerd-prometheus   videos-web      linkerd   default   √
    linkerd-tap          playlists-api   linkerd   default   √
    linkerd-tap          playlists-db    linkerd   default   √
    linkerd-tap          videos-api      linkerd   default   √
    linkerd-tap          videos-db       linkerd   default   √
    linkerd-tap          videos-web      linkerd   default   √
    
    linkerd -n default tap deploy

    Overview (`kubernetes/tutorials/basics/README.md`)

    Kubernetes Tutorial: The Basics

    How to learn Kubernetes in 2022

    This guide is aimed to fast-track your Kubernetes learning by focusing on a practical hands-on overview guide.

    When learning Kubernetes, you usually have an idea of some existing system you own and manage, or a website that you are building.

    The challenge is understanding which Kubernetes building blocks you need in order to run your workloads on Kubernetes


    The problem: "I want to adopt Kubernetes"
    The problem: "I have some common existing infrastructure"

    Our focus: Solving the problem by learning each building block
    in order to port our infrastructure to Kubernetes.

    Understanding Containers

    Before even looking at Kubernetes, you need to have a general understanding of containers like docker.
    Your workloads need to fit in containers in order to be shipped on Kubernetes.

    Containers also have a bunch of assumptions that you need to meet.

    • Defining the container - Dockerfile
    • Serving traffic - Exposing ports
    • Configuration - mount config files & secrets or env variables
    • Data persistence - When a container is terminated, everything inside the container is gone
    • Container entrypoint - The main process that runs in the container. Your app

    Docker installation

    Create Network

    text
    docker network create wordpress

    Build & Test container images

    Wordpress example

    text
    cd kubernetes\tutorials\basics\
    
    docker build -f dockerfiles/wordpress.dockerfile . -t aimvector/wordpress-example
    • Run our Wordpress container
    text
    docker run -it --rm -p 80:80 --net wordpress aimvector/wordpress-example

    The wordpress container will be visible on port 80 on http://localhost/

    MySQL example

    text
    docker build -f dockerfiles/mysql.dockerfile . -t aimvector/mysql-example
    • How do we run our MySQL ?

    We need to understand that databases require storage and state
    Just like installing software on a server, it will store its files in some
    directory. Mysql stores its files under /var/lib/mysql

    • We need a volume mount

    Let's see how to run this in docker

    text
    mkdir data
    
    docker run --rm -d `
    --name mysql `
    --net wordpress `
    -e MYSQL_DATABASE=exampledb `
    -e MYSQL_USER=exampleuser `
    -e MYSQL_PASSWORD=examplepassword `
    -e MYSQL_RANDOM_ROOT_PASSWORD=1 `
    -v ${PWD}/data:/var/lib/mysql `
    aimvector/mysql-example
    
    # we can see the container with
    docker ps
    CONTAINER ID   IMAGE                     COMMAND                  CREATED         STATUS         PORTS                 NAMES
    92cde663a3f5   aimvector/mysql-example   "docker-entrypoint.s…"   5 seconds ago   Up 3 seconds   3306/tcp, 33060/tcp   mysql
    • Run Wordpress and connect it to MySQL
    text
    docker run -d `
    --rm `
    -p 80:80 `
    --name wordpress `
    --net wordpress `
    -e WORDPRESS_DB_HOST=mysql `
    -e WORDPRESS_DB_USER=exampleuser `
    -e WORDPRESS_DB_PASSWORD=examplepassword `
    -e WORDPRESS_DB_NAME=exampledb `
    aimvector/wordpress-example

    Clean up

    text
    docker rm -f wordpress
    docker rm -f mysql
    docker network rm wordpress
    rm data

    Kubernetes Tools: kubectl

    To manage and work with Kubernetes, you need kubectl

    Let's grab that from here

    Run Kubernetes Locally

    • Install kubectl to work with kubernetes

    We'll head over to the kubernetes site to download kubectl

    • Install the kind binary

    You will want to head over to the kind site

    • Create a cluster
    text
    kind create cluster --image kindest/node:v1.23.5

    Namespaces

    text
    kubectl create namespace cms

    Configmaps

    Environment Variables for pods

    How to use configmaps

    text
    kubectl -n cms create configmap mysql `
    --from-literal MYSQL_RANDOM_ROOT_PASSWORD=1
    
    kubectl -n cms get configmaps

    Secrets

    How to use secrets in pods

    text
    kubectl -n cms create secret generic wordpress `
    --from-literal WORDPRESS_DB_HOST=mysql `
    --from-literal WORDPRESS_DB_USER=exampleuser `
    --from-literal WORDPRESS_DB_PASSWORD=examplepassword `
    --from-literal WORDPRESS_DB_NAME=exampledb
    
    kubectl -n cms create secret generic mysql `
    --from-literal MYSQL_USER=exampleuser `
    --from-literal MYSQL_PASSWORD=examplepassword `
    --from-literal MYSQL_DATABASE=exampledb
    
    
    kubectl -n cms get secret

    Deployments

    cd kubernetes\tutorials\basics

    text
    kubectl -n cms apply -f yaml/deploy.yaml
    kubectl -n cms get pods

    Services

    Services documentation

    text
    kubectl -n cms apply -f .\yaml\service.yaml
    kubectl -n cms get svc

    Storage Class

    StorageClass documentation

    text
    kubectl get storageclass

    Statefulset

    Statefulset documentation

    Let's deploy our mysql using what we learnt above:

    text
    kubectl -n cms apply -f yaml/statefulset.yaml
    
    kubectl -n cms get pods

    Persistent Volumes

    Documentation

    Port Forwarding

    We can access private service endpoints or pods using port-forward :

    text
    kubectl -n cms get pods
    kubectl -n cms port-forward  80

    Public Traffic

    In order to make our site public, its common practise to expose web servers via

    a proxy or API gateway.

    In Kubernetes, an Ingress is used.

    Ingress

    To use an ingress, we need an ingress controller

    text
    kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.1.3/deploy/static/provider/cloud/deploy.yaml
    
    kubectl -n ingress-nginx get pods
    
    kubectl -n ingress-nginx --address 0.0.0.0 port-forward svc/ingress-nginx-controller 80

    Create an Ingress

    text
    kubectl -n cms apply -f yaml/ingress.yaml

    Overview (`linux/containers/podman/README.md`)

    Introduction to Podman

    In this guide, we will dive into Linux containers with Podman and explore the full container lifecycle.

    We will cover what Podman is, how it differs from Docker, how to install it, and how to build and run containers with it.

    What is Podman

    Podman is a daemonless, open source, Linux native tool for working with Linux containers

    • Free and open source
    • Fast, lightweight, and daemonless
    • Can run Kubernetes pods directly
    • Provides a Docker-compatible CLI (alternative to docker)

    Note: Podman does not require a background daemon. It uses a fork-exec model directly with the Linux kernel (via runc), which makes it easier to run rootless and more secure by default.

    Podman also supports "Pods", groups of containers similar to Kubernetes pods, and can generate Kubernetes YAML directly from running containers.

    Documentation

    Installing Podman

    Podman runs natively on Linux, and also works well inside WSL if you're on Windows.

    Running it on native Linux (or a Linux VM/WSL) gives the best portability and closest match to how it runs in production.

    Note: Podman Desktop is a separate GUI tool for managing Podman on Windows/macOS. We'll cover it in a dedicated video.

    shell
    # Debian/Ubuntu
    sudo apt-get update
    sudo apt-get install -y podman

    Basic Commands

    Podman's CLI mirrors Docker's, so most commands feel familiar right away:

    shell
    podman --help

    Building a Container Image

    Podman builds standard OCI images, so any existing Dockerfile works without changes.

    We'll quickly build images across a few different languages, C#, Node.js, Python, and Go, to show that the same Dockerfile format applies regardless of stack.

    shell
    cd python
    podman build -t aimvector/python:1.0.0
    
    # note that podman is case sensitive and looking for Dockerfile instread of dockerfile .
    podman build -f dockerfile -t aimvector/python:1.0.0
    
    cd nodejs
    podman build -f dockerfile -t aimvector/nodejs:1.0.0

    Notice how podman prefixes our tag with localhost for local images:
    Docker uses default docker.io as prefix.

    Remember Kubernetes used to default to docker.io too, and no longer does.

    It's good practice to get used to referencing full image names (e.g. docker.io/library/nginx instead of just nginx)

    Running Containers

    Run a container the same way you would with Docker:

    shell
    #interactive with -it or background with -d
    podman run -d -p 80:5000 localhost/aimvector/python:1.0.0
    
    # view containers
    podman ps 
    
    # check container logs
    podman logs
    
    # stop containers
    podman stop
    
    # view stopped containers
    podman ps -a 
    
    # remove containers
    podman rm

    Compose

    Podman also supports Compose files through podman compose:
    Note in my demo its not found, as podman "wraps" docker compose under the hood if it exists.

    We can install it with sudo apt-get install -y podman-compose

    shell
    podman compose build

    Overview (`messaging/rabbitmq/kubernetes/readme.md`)

    RabbitMQ on Kubernetes

    rabbitmq-k8s

    Create a cluster with kind

    text
    kind create cluster --name rabbit --image kindest/node:v1.18.4

    Namespace

    text
    kubectl create ns rabbits

    Storage Class

    text
    kubectl get storageclass
    NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
    standard (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  84s

    Deployment

    text
    kubectl apply -n rabbits -f .\kubernetes\rabbit-rbac.yaml
    kubectl apply -n rabbits -f .\kubernetes\rabbit-configmap.yaml
    kubectl apply -n rabbits -f .\kubernetes\rabbit-secret.yaml
    kubectl apply -n rabbits -f .\kubernetes\rabbit-statefulset.yaml

    Access the UI

    text
    kubectl -n rabbits port-forward rabbitmq-0 8080:15672

    Go to htttp://localhost:8080

    Username: guest

    Password: guest

    Message Publisher

    text
    cd messaging\rabbitmq\applications\publisher
    docker build . -t aimvector/rabbitmq-publisher:v1.0.0
    
    kubectl apply -n rabbits -f deployment.yaml

    Automatic Synchronization

    https://www.rabbitmq.com/ha.html#unsynchronised-mirrors

    text
    rabbitmqctl set_policy ha-fed \
        ".*" '{"federation-upstream-set":"all", "ha-sync-mode":"automatic", "ha-mode":"nodes", "ha-params":["[email protected]","[email protected]","[email protected]"]}' \
        --priority 1 \
        --apply-to queues

    Overview (`monitoring/logging/opentelemetry/README.md`)

    Logging with OpenTelemetry

    opentel-intro


    Overview (`monitoring/opentelemetry/kubernetes/README.md`)

    Introduction to OpenTelemetry Operator

    OpenTelemetry Operator documentation

    We need a Kubernetes cluster

    Lets create a Kubernetes cluster to play with using kind

    text
    kind create cluster --name otel --image kindest/node:v1.34.0

    Test our cluster:

    text
    kubectl get nodes
    NAME                 STATUS   ROLES           AGE   VERSION
    otel-control-plane   Ready    control-plane   40s   v1.34.0

    Helm charts

    OpenTelemetry Operator chart

    text
    helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
    helm repo update
    
    helm search repo open-telemetry --versions

    We'll install version 0.93.1 at the time of this guide.
    I would suggest to make sure the version you pick is compatible with the Kubernetes version you are running.

    text
    OTEL_VERSION=0.93.1

    Cert-manager chart

    We will need cert-manager deployed which Otel uses for local TLS certificate management

    text
    helm repo add jetstack https://charts.jetstack.io
    
    helm search repo jetstack --versions

    We'll install version v1.18.2 of cert-manager at the time of this guide which is compatible with our Kubernetes version.

    text
    CERTMANAGER_VERSION=v1.18.2

    Install helm charts

    Install cert-manager:

    text
    helm install \
    cert-manager jetstack/cert-manager \
    --namespace cert-manager \
    --create-namespace \
    --version $CERTMANAGER_VERSION \
    --set crds.enabled=true \
    --set startupapicheck.timeout="5m"

    Install OpenTelemetry Operator:

    text
    helm install opentelemetry-operator open-telemetry/opentelemetry-operator \
    --namespace opentelemetry-operator-system \
    --create-namespace \
    --version $OTEL_VERSION \
    --values=monitoring/opentelemetry/kubernetes/values.yaml

    View our install:

    text
    kubectl get pods -n cert-manager
    kubectl get pods -n opentelemetry-operator-system

    Create a Collector

    In this guide I copied a starter collector from the official documentation and changed some of the values.

    To split out the guides, we'll have separate collectors for logs, metrics and traces.

    Let's deploy our collectors in a central monitoring namespace:

    text
    kubectl create namespace monitoring
    
    kubectl apply -n monitoring -f monitoring/opentelemetry/kubernetes/collector-tracing.yaml

    View our collector:

    text
    kubectl -n monitoring get pods
    kubectl -n monitoring get svc

    Tracing

    Instrumentation

    To start receiving traces, we'll be using the OpenTelemetry auto instrumentation to inject the instrumentation into our Kubernetes pods.

    To make use of the auto injection for Opentelemetry, we use the Instrumentation resources.

    Note that I am creating this in the default namespace next to where my applications are.

    text
    kubectl apply -f monitoring/opentelemetry/kubernetes/instrumentation.yaml

    Deploy Microservices

    Build applications:

    text
    docker compose --file monitoring/opentelemetry/docker-compose.yaml build

    Load images into kind:

    text
    kind load docker-image aimvector/service-mesh:videos-web-1.0.0 --name otel
    kind load docker-image aimvector/service-mesh:playlists-api-1.0.0 --name otel
    kind load docker-image aimvector/service-mesh:videos-api-1.0.0 --name otel
    text
    kubectl apply -f monitoring/opentelemetry/applications/playlists-api/
    kubectl apply -f monitoring/opentelemetry/applications/playlists-db/
    kubectl apply -f monitoring/opentelemetry/applications/videos-web/
    kubectl apply -f monitoring/opentelemetry/applications/videos-db/
    kubectl apply -f monitoring/opentelemetry/applications/videos-api/

    Generate some traffic with port-forward

    text
    kubectl port-forward svc/videos-web 80:80
    kubectl port-forward svc/playlists-api 81:80

    Tracing Data Store

    In this guide, we'll use a Tempo database for tracing data

    text
    helm repo add grafana https://grafana.github.io/helm-charts
    helm repo update
    
    helm search repo  grafana/tempo
    
    TEMPO_VERSION=1.23.3
    
    helm install tempo grafana/tempo \
        --create-namespace \
        --namespace grafana \
        --version $TEMPO_VERSION \
        --values monitoring/opentelemetry/kubernetes/tempo.yaml

    Metrics Data Store

    In this guide, we'll use a Prometheus for our metrics data

    text
    helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
    helm search repo prometheus-community --versions
    
    PROMETHEUS_STACK_VERSION=77.5.0
    
    helm install kube-prometheus-stack prometheus-community/kube-prometheus-stack \
      --version ${PROMETHEUS_STACK_VERSION} \
      --namespace prometheus-operator-system \
      --create-namespace \
      --set prometheusOperator.enabled=true \
      --set prometheusOperator.nodeSelector."kubernetes\.io/os"=linux \
      --set prometheusOperator.fullnameOverride="prometheus-operator" \
      --set prometheusOperator.manageCrds=true \
      --set alertmanager.enabled=false \
      --set grafana.enabled=false \
      --set prometheus-node-exporter.enabled=false \
      --set nodeExporter.enabled=false \
      --set kubeStateMetrics.enabled=false \
      --set prometheus.enabled=false
    
    kubectl -n prometheus-operator-system get pods 
    
    # Deploy our dedicated Prometheus for metrics storage
    
    kubectl apply -n monitoring -f monitoring/opentelemetry/kubernetes/prometheus.yaml

    Dashboards

    In this guide I use a simple Grafana for dashboards

    text
    helm search repo grafana/grafana
    
    GRAFANA_VERSION=9.4.4
    
    helm install grafana grafana/grafana \
      --namespace grafana \
      --version $GRAFANA_VERSION \
      --values monitoring/opentelemetry/kubernetes/grafana.yaml

    Access Grafana

    We can port-forward to Grafana

    text
    kubectl -n grafana port-forward svc/grafana 3000:80

    Overview (`monitoring/opentelemetry/metrics/README.md`)

    OpenTelemetry Metrics

    opentel-intro

    Build the applications

    To build the applications navigate your terminal to the location of the compose file

    text
    cd monitoring/opentelemetry/

    Build:

    text
    docker compose build

    Docker

    Start up the stack using local docker compose, by running:

    text
    docker compose up

    Kubernetes

    For Kubernetes, we will use the OpenTelemetry Operator.

    See that guide over here

    Generate from Traffic

    You can generate some traffic by visiting the application on http://localhost

    Note: You will need port 80 and 81 open for the applications to function

    Access Grafana

    You can access Grafana on http://localhost:3000 to test the Tempo datasource.


    Overview (`monitoring/opentelemetry/tracing/README.md`)

    Tracing with OpenTelemetry

    opentel-intro

    opentel-intro

    Build the applications

    To build the applications navigate your terminal to the location of the compose file

    text
    cd monitoring/opentelemetry/

    Build:

    text
    docker compose build

    Docker

    Start up the stack using local docker compose, by running:

    text
    docker compose up

    Kubernetes

    For Kubernetes, we will use the OpenTelemetry Operator.

    See that guide over here

    Generate from Traffic

    You can generate some traffic by visiting the application on http://localhost

    Note: You will need port 80 and 81 open for the applications to function

    Access Grafana

    You can access Grafana on http://localhost:3000 to test the Tempo datasource.


    Overview (`monitoring/prometheus/kubernetes/readme.md`)

    Kubernetes monitoring with Prometheus

    Kubernetes 1.14.8

    Kubernetes 1.15-1.17

    Kubernetes 1.18.4

    Kubernetes 1.23

    Prometheus Overview

    Prometheus Overview and Architecture - Video : here

    Source: here

    Application Monitoring

    Monitoring Python code with Prometheus - Video : here

    Monitoring Nodejs code with Prometheus - Video : here

    Monitoring C# code with Prometheus - Video : here

    Source: here

    Prometheus Operator guide

    Video : here

    Source Code tested on K8s version 1.14.8

    Source: here

    Node Exporter guide

    Video : here

    Source Code tested on K8s version 1.14.8

    Source: here


    Overview (`monitoring/prometheus/sharding/README.md`)

    Prometheus Basic Sharding

    a Video reference guide

    To run any of the commands, please ensure you open a terminal and navigate to the path where this readme is located.

    Start example applications

    To start our example applications, we'll need to navigate to the folder where we have our example services in our docker-compose file

    text
    cd monitoring\prometheus\
    
    docker compose up -d --build go-application
    docker compose up -d --build python-application
    docker compose up -d --build dotnet-application
    docker compose up -d --build nodejs-application

    Start Prometheus

    text
    cd sharding
    docker compose up -d prometheus-00
    docker compose up -d prometheus-01

    Overview (`monitoring/prometheus/thanos/README.md`)

    Prometheus Thanos Basics

    This guide relies on our Prometheus Application Monitoring Guide.

    Start example applications

    To start our example applications, we'll need to navigate to the folder where we have our example services in our docker-compose file

    text
    cd monitoring\prometheus\
    
    docker compose up -d --build go-application
    docker compose up -d --build python-application
    docker compose up -d --build dotnet-application
    docker compose up -d --build nodejs-application

    Start Sharded Prometheus Instances

    The following will start two shards of Prometheus, using the hasmod sharding technique.

    Basically two random services will be selected and scraped by each Prometheus shard. </br

    text
    cd sharding
    docker compose up -d prometheus-00
    docker compose up -d prometheus-01

    Start the Thanos Components

    The following will start two Thanos side cars, one for each Prometheus instance.
    It will also start a Minio S3 service

    text
    cd ../thanos
    docker compose up

    Overview (`python/introduction/part-2.files/README.md`)

    Introduction to Python: FILES

    In Python, dealing with files is very common and is a very important part of
    programming for a number of reasons:

    • Applications may need to read configuration files
    • In Data science, data is often sourced from files (CSV, XML, JSON, etc)
    • Data is often analysed in Python when its written in different stages of analysis
    • DevOps engineers often stores the state of infrastructure or data as files for automation purposes.

    Files are not the endgame for storage.

    Remember there are things like Caches and Databases.

    But before learning those things, file handling is the best place to start.

    Python Dev Environment

    The same as Part 1, we start with a dockerfile where we declare our version of python.

    text
    cd python\introduction\part-2.files
    
    docker build --target dev . -t python
    docker run -it -v ${PWD}:/work python sh
    
    /work # python --version
    Python 3.9.6

    Our application

    Firstly we have a class to define what a customer looks like:

    text
    class Customer:
      def __init__(self, c="",f="",l=""):
        self.customerID = c
        self.firstName  = f
        self.lastName   = l
      def fullName(self):
        return self.firstName + " " + self.lastName

    Then we need a function which returns our customers:

    text
    def getCustomers():
      customers = {
        "a": Customer("a","James", "Baker"),
        "b": Customer("b", "Jonathan", "D"),
        "c": Customer("c", "Aleem", "Janmohamed"),
        "d": Customer("d", "Ivo", "Galic"),
        "e": Customer("e", "Joel", "Griffiths"),
        "f": Customer("f", "Michael", "Spinks"),
        "g": Customer("g", "Victor", "Savkov"),
        "h" : Customer("h", "Marcel", "Dempers")
      }
      return customers

    Here is a function to return a specific customer:

    text
    def getCustomer(customerID):
      customer = getCustomers()
      return customer[customerID]

    Opening Files

    Python provides an open function to open files.

    open() takes a file path\name and access mode

    text
    "r" - Read - Default value. Opens a file for reading, error if the file does not exist
    "a" - Append - Opens a file for appending, creates the file if it does not exist
    "w" - Write - Opens a file for writing, creates the file if it does not exist
    "x" - Create - Creates the specified file, returns an error if the file exists

    Try open a file that holds our customer data:

    text
    open("customers.log")

    We can see the file does not exist:

    text
    /work # python src/app.py
    Traceback (most recent call last):
      File "/work/src/app.py", line 26, in <module>
        open("customers.log")
    FileNotFoundError: [Errno 2] No such file or directory: 'customers.log'

    Let's use what we learned (if statements), to check if the file exists!
    We'll need a built in library for handling files

    text
    import os.path

    Then we can use the os.path.isfile("customers.log") command to check if the file exists

    text
    os.path.isfile("customers.log")

    Using if logic we can check if the file is there:

    text
    if os.path.isfile("customers.log"):
      print("file exists")
    else:
      print("file does not exists")

    Now we know the file does not exist, but if it did, we can now read it with open

    text
    f = open("customers.log")

    Let's also loop each customer in the file and print it

    text
    for customer in f:
      print(customer)
    f.close()

    Now we know the file does not exist, let's create it!

    text
    customers = getCustomers()
    for customerID in customers:
      c = customers[customerID]
      f.write(c.customerID + "," + c.firstName + "," + c.lastName)

    Now if we run our code the first time, it will create and populate the file as it does not exist,
    and will read the file and display the content on the second run.

    Instead of looping each line in the file, we can read the entire file with the file's read() function:

    text
    print(f.read())

    Comma-Separated Values : CSV

    As we can see, our customers.log file is in CSV format with every field separated by commas.

    So far, we've demonstrated using primitives to read and write to files to store our data.
    When looping data structures like dictionaries and writing each line one by one to a file
    will use a lot of CPU if the data is large.

    CSV: Reading our file

    To work with CSV's, we need to import a library
    We also need to add headers to our file so it makes setting fields easier:

    text
    customerID, firstName, lastName
    text
    import csv
    with open('customers.log', newline='') as customerFile:
      reader = csv.DictReader(customerFile)
      for row in reader:
        #print(row)
        print("customer id:" + row['customerID'] + " fullName : " + row['firstName'] + " " + row['lastName'])

    CSV: Writing our file

    Create an array with our field headers

    text
    fields = ['customerID', 'firstName', 'lastName']
    with open('customers.log', 'w', newline='') as customerFile:
      writer = csv.writer(customerFile)
      writer.writerow(fields)
      customers = getCustomers()
      for customerID in customers:
        customer = customers[customerID]
        writer.writerow([customer.customerID, customer.firstName, customer.lastName])

    Putting it all together

    Now that we have code that reads and writes to a file, let's update our getCustomers function to return
    customers from our file.

    We read the file if it exists, read it into a list and convert the list to a dictionary:

    text
    def getCustomers():
      if os.path.isfile("customers.log"):
        with open('customers.log', newline='') as customerFile:
          reader = csv.DictReader(customerFile)
          l = list(reader)
          customers = {c["customerID"]: c for c in l}
          return customers
      else: 
        return {}

    We can test our function to see it working:

    text
    customers = getCustomers()
    for customerID in customers:
      print(customers[customerID])

    Let's also create a function to update customers

    text
    def updateCustomers(customers):
      fields = ['customerID', 'firstName', 'lastName']
      with open('customers.log', 'w', newline='') as customerFile:
        writer = csv.writer(customerFile)
        writer.writerow(fields)
        for customerID in customers:
          customer = customers[customerID]
          writer.writerow([customer.customerID, customer.firstName, customer.lastName])

    Let's test our two functions by deleting our file and recreate it using our functions:

    text
    customers = {
        "a": Customer("a","James", "Baker"),
        "b": Customer("b", "Jonathan", "D"),
        "c": Customer("c", "Aleem", "Janmohamed"),
        "d": Customer("d", "Ivo", "Galic"),
        "e": Customer("e", "Joel", "Griffiths"),
        "f": Customer("f", "Michael", "Spinks"),
        "g": Customer("g", "Victor", "Savkov"),
        "h" : Customer("h", "Marcel", "Dempers")
    }
    
    #save it
    updateCustomers(customers)
    
    #add another test customer
    test = Customer("t", "Test", "Customer")
    customers["t"] = test
    
    #save it
    updateCustomers(customers)
    
    #see the changes
    customers = getCustomers()
    for customer in customers:
      print(customers[customer])

    Docker

    Let's build our container image and run it while mounting our customer file

    Our final dockerfile

    text
    FROM python:3.9.6-alpine3.13 as dev
    
    WORKDIR /work
    
    FROM dev as runtime
    COPY ./src/ /app 
    
    ENTRYPOINT [ "python", "/app/app.py" ]

    Build and run our container.
    Notice the customers.log file get created if it does not exists.

    text
    cd python\introduction\part-2.files
    
    docker build . -t customer-app
    
    docker run -v ${PWD}:/work -w /work customer-app

    Overview (`python/introduction/part-3.json/README.md`)

    Introduction to Python: JSON

    JSON is a very important format for storing data in Software engineering.

    JSON is popular for the following scenarios :

    • Popular standard for data structures in general.
    • Applications may talk to each other by passing JSON data back and forth (HTTP Web APIs).
    • Applications often store configuration in JSON format as a configuration file.
    • Data may be stored in databases or caches in JSON format.
    • DevOps engineers may store infrastructure configuration in JSON format.

    Python Dev Environment

    The same as Part 1, we start with a dockerfile where we declare our version of python.

    text
    FROM python:3.9.6-alpine3.13 as dev
    
    WORKDIR /work

    Let's build and start our container:

    text
    cd python\introduction\part-3.json
    
    docker build --target dev . -t python
    docker run -it -v ${PWD}:/work python sh
    
    /work # python --version
    Python 3.9.6

    Our application

    Firstly we have a class to define what a customer looks like:

    text
    class Customer:
      def __init__(self, c="",f="",l=""):
        self.customerID = c
        self.firstName  = f
        self.lastName   = l
      def fullName(self):
        return self.firstName + " " + self.lastName

    Then we need a function which returns our customers:

    text
    def getCustomers():
      customers = {
        "a": Customer("a","James", "Baker"),
        "b": Customer("b", "Jonathan", "D"),
        "c": Customer("c", "Aleem", "Janmohamed"),
        "d": Customer("d", "Ivo", "Galic"),
        "e": Customer("e", "Joel", "Griffiths"),
        "f": Customer("f", "Michael", "Spinks"),
        "g": Customer("g", "Victor", "Savkov"),
        "h" : Customer("h", "Marcel", "Dempers")
      }
      return customers

    Here is a function to return a specific customer:

    text
    def getCustomer(customerID):
      customer = getCustomers()
      return customer[customerID]

    Test our functions:

    text
    customers = getCustomers()
    customer = customers["h"]
    
    print(customers)
    print(customer.fullName)

    Files

    In part-2 we learnt about reading and writing to files.

    We changed our functions to get customers from file and update customers by writing the data back to file.

    Let's do that quick:

    Get Customers

    text
    import os.path
    import csv
    
    def getCustomers():
      if os.path.isfile("customers.log"):
        with open('customers.log', newline='') as customerFile:
          reader = csv.DictReader(customerFile)
          l = list(reader)
          customers = {c["customerID"]: c for c in l}
          return customers
      else: 
        return {}

    Update Customers

    Let's create a function to update our customers:

    text
    def updateCustomers(customers):
      fields = ['customerID', 'firstName', 'lastName']
      with open('customers.log', 'w', newline='') as customerFile:
        writer = csv.writer(customerFile)
        writer.writerow(fields)
        for customerID in customers:
          customer = customers[customerID]
          writer.writerow([customer.customerID, customer.firstName, customer.lastName])

    Test

    Let's test our two functions to ensure they work

    text
    customers = {
        "a": Customer("a","James", "Baker"),
        "b": Customer("b", "Jonathan", "D"),
        "c": Customer("c", "Aleem", "Janmohamed"),
        "d": Customer("d", "Ivo", "Galic"),
        "e": Customer("e", "Joel", "Griffiths"),
        "f": Customer("f", "Michael", "Spinks"),
        "g": Customer("g", "Victor", "Savkov"),
        "h" : Customer("h", "Marcel", "Dempers")
    }
    
    #save it
    updateCustomers(customers)
    
    #see the changes
    customers = getCustomers()
    for customer in customers:
      print(customers[customer])

    JSON

    JSON is a very simple data structure for building objects.
    you can define any type of object and data using JSON.

    In JSON, objects are defined by open and close brackets, like {}

    Arrays or lists are defined as square brackets, like []

    We can therefore define a customer like :
    Notice keys and values have quotes, and separated by colon

    text
    {
      "customerID" : "a",
      "firstName: "Bob",
      "lastName": "Smith"
    }

    We can define an array of customers in JSON by declaring our array with square brackets and having customer objects inside, separated by commas:

    text
    # Example Array:
    [{customer-1},{customer-2},{customer-3},{customer-4} ]

    JSON Library

    Python provides a library for dealing with JSON.

    Let's take our customer dictionary, and convert it to json structure and
    finally write it to file using our update function.

    text
    import json

    Now first things first, we need to convert our customers dictionary to a JSON structured string so we can store it to file.

    The json library allows us to convert dictionaries to files using
    the dumps function:

    text
    jsonData = json.dumps(customers)
    print(jsonData)

    But if we run this we get an error:
    TypeError: Object of type Customer is not JSON serializable

    That is because the library can only deal with full dictionaries.
    Taking a closer look at our dictionary, our customer is a class.

    text
    # our key is a string
    # our value is a class
    "h" : Customer("h", "Marcel", "Dempers")

    json.dumps() only allows full dictionaries so we need our customer object to be in dictionary form like so:

    text
    "h": { "customerID": "h", "firstName" : "Marcel", "lastName" : "Dempers"}

    To fix our dictionary, we can iterate it and build a new one

    text
    # start with an empty one
    customerDict = {}
    
    #populate our new dictionary
    for id in customers:
      customerDict[id] = customers[id].__dict__
    
    #print it
    print(customerDict)

    Now we can convert our customer dictionary to json

    text
    customerJSON = json.dumps(customerDict)
    print(customerJSON)

    Now we've converted our data into JSON, we can view this in a new tab window and change the language mode to JSON to visualise the data in our new format.

    Now essentially our application will use dictionaries when working with the data
    but store and transport it as JSON.

    Therefore our update function should take a dictionary and convert it to JSON
    and store it.

    text
    def updateCustomers(customer):
      with open('customers.json', 'w', newline='') as customerFile:
        customerJSON = json.dumps(customer) 
        customerFile.write(customerJSON)

    We also need to change our getCustomers function to read the JSON file
    and convert that data correctly back to a dictionary for processing.

    text
    def getCustomers():
      if os.path.isfile("customers.json"):
        with open('customers.json', newline='') as customerFile:
          data = customerFile.read()
          customers = json.loads(data) 
          return customers
      else: 
        return {}

    Finally let's test our functions :

    text
    customers = getCustomers()
    print(customers)

    It's also very easy to add a customer to our JSON data using our class and using pythons internal __dict__ property to convert the object to a dictionary and add it to our customers dictionary

    text
    customers["i"] = Customer("i", "Bob", "Smith").__dict__
    updateCustomers(customers)

    Docker

    Let's build our container image and run it while mounting our customer file

    Our final dockerfile

    text
    FROM python:3.9.6-alpine3.13 as dev
    
    WORKDIR /work
    
    FROM dev as runtime
    COPY ./src/ /app 
    
    ENTRYPOINT [ "python", "/app/app.py" ]

    Build and run our container.
    Notice the customers.json file gets created if it does not exist.

    text
    cd python\introduction\part-3.json
    
    docker build . -t customer-app
    
    docker run -v ${PWD}:/work -w /work customer-app

    Overview (`python/introduction/part-4.http/README.md`)

    Introduction to Python: HTTP (Flask)

    So far, we've learnt Python fundamentals, worked with data, files and
    more importantly, basic data structures like csv and json.

    JSON is a very popular format for now only storing, but transporting data in web HTTP applications. Files are important because we may
    need to read configuration files for our application.

    One application could make a web request to ask for customer data from our application. By sending back JSON, the receiving application can easily process the data it gets.

    Be sure to checkout Part 2: Files

    As well as Part 3: JSON

    Python Dev Environment

    The same as Part 1, we start with a dockerfile where we declare our version of python.

    text
    FROM python:3.9.6-alpine3.13 as dev
    
    WORKDIR /work

    Let's build and start our container:

    text
    cd python\introduction\part-4.http
    
    docker build --target dev . -t python
    docker run -it -v ${PWD}:/work python sh
    
    /work # python --version
    Python 3.9.6

    Our application

    We're going to use what we've learnt in part 1,2 & 3 and create
    our customer app that stores customer data

    Firstly we have to import our dependencies:

    text
    import os.path
    import csv
    import json

    Then we have a class to define what a customer looks like:

    text
    class Customer:
      def __init__(self, c="",f="",l=""):
        self.customerID = c
        self.firstName  = f
        self.lastName   = l
      def fullName(self):
        return self.firstName + " " + self.lastName

    Then we need a function which returns our customers:

    text
    def getCustomers():
      if os.path.isfile("customers.json"):
        with open('customers.json', newline='') as customerFile:
          data = customerFile.read()
          customers = json.loads(data)
          return customers
      else: 
        return {}

    Here is a function to return a specific customer:

    text
    def getCustomer(customerID):
      customer = getCustomers()
      return customer[customerID]

    And finally a function for updating our customers:

    text
    def updateCustomers(customers):
      with open('customers.json', 'w', newline='') as customerFile:
        customerJSON = json.dumps(customers)
        customerFile.write(customerJSON)

    In the previous episode, we've created a json file to hold all our customers.

    We've learnt how to read and write to file and temporarily use the file for our storage.

    text
    {
      "a": {
        "customerID": "a",
        "firstName": "James",
        "lastName": "Baker"
      },
      "b": {
        "customerID": "b",
        "firstName": "Jonathan",
        "lastName": "D"
      },
      "c": {
        "customerID": "c",
        "firstName": "Aleem",
        "lastName": "Janmohamed"
      },
      "d": {
        "customerID": "d",
        "firstName": "Ivo",
        "lastName": "Galic"
      },
      "e": {
        "customerID": "e",
        "firstName": "Joel",
        "lastName": "Griffiths"
      },
      "f": {
        "customerID": "f",
        "firstName": "Michael",
        "lastName": "Spinks"
      },
      "g": {
        "customerID": "g",
        "firstName": "Victor",
        "lastName": "Savkov"
      }
    }

    Now that we have our customer data, we can read and update the data with our two simple functions:

    text
    customers = getCustomers()
    print(customers)

    Let's add a customer and write it back to file:

    text
    customers["h"] = Customer("h", "Marcel", "Dempers").__dict__
    updateCustomers(customers)

    Now customers.json has the new entry if we rerun our code:

    text
    python src/app.py

    Flask

    Checkout official Flask documentation here

    A minimal flask application looks like this:

    text
    from flask import Flask
    
    app = Flask(__name__)
    
    @app.route("/")
    def hello_world():
        return "Hello, World!"

    To get this to work, we need to look at Pythons Library system, or pip

    External Packages

    If we look at the from flask import Flask statement, it assumes we have a flask dependency. Our previous import statements worked, because they are built-in libraries in python.

    Flask is an external package.

    External packages are managed by pip

    We may use pip to install Flask. If we need to connect to databases, we may install packages to do so. Like a mysql or a redis package.

    With pip, we define our dependencies in a requirements.txt file.
    We can check for the latest version of Flask here

    We can also use pip install commands to install dependencies.

    However, this means we need a new pip install command for every dependency which will grow as
    our application and needs grow. Best is to use a requirements.txt file.

    Let's create a requirements.txt file:

    text
    Flask == 2.0.2

    We can install our dependencies using:

    text
    pip install -r src/requirements.txt

    Let's implement the minimal example we posted above.

    To run our application, note that the app.py runs as per usual.

    But to start Flask, we can follow the instructions as per the official document:

    text
    export FLASK_APP=src/app
    flask run -h 0.0.0 -p 5000

    Note that we cannot access http://localhost:5000 because our app is running in docker.

    To access it, we need to restart our container and this time expose our port.

    text
    exit
    docker run -it -p 5000:5000 -v ${PWD}:/work python sh
    
    #get our dependencies and start our application
    pip install -r requirements.txt
    export FLASK_APP=src/app
    flask run -h 0.0.0 -p 5000

    Now we can access our app on http://localhost:5000/

    Routes

    Web servers allow us to define various URLs or routes.

    Currently we can see logs showing GET requests for route /

    Notice the HTTP status code 200 indicating success.

    text
    * Running on http://0.0.0:5000/ (Press CTRL+C to quit)
    172.17.0.1 - - [22/Sep/2021 23:10:14] "GET / HTTP/1.1" 200 -
    172.17.0.1 - - [22/Sep/2021 23:10:14] "GET /favicon.ico HTTP/1.1" 404 -
    172.17.0.1 - - [22/Sep/2021 23:12:20] "GET / HTTP/1.1" 200 -
    172.17.0.1 - - [22/Sep/2021 23:12:21] "GET / HTTP/1.1" 200 -
    172.17.0.1 - - [22/Sep/2021 23:12:21] "GET / HTTP/1.1" 200 -

    Routes allow us to have many endpoints, so we could have endpoints such as:

    text
    /all            --> which returns all customers
    /get/<customerID>    --> which returns one customer by CustomerID
    /add         --> which adds or updates a customer

    Defining these routes are pretty straight forward, see docs

    Let's define our routes:

    text
    @app.route("/")
    def get_customers():
        return "Hello, get_customers!"
    
    @app.route("/get/<string:customerID>", methods=['GET'])
    def get_customer(customerID):
        return "Hello, get!"
    
    @app.route("/add", methods=['POST'])
    def add_customer(customer):
        return "Hello, add!"

    Now that we've built our URLs using routes, next we need to understand HTTP methods

    HTTP Methods

    There are a number of HTTP methods for web services, popular ones being POST and GET

    GET method is for general retrieval of data

    POST method is for passing data to our service

    Let's setup each of our routes with dedicated HTTP Methods

    text
    @app.route("/", methods=['GET'])
    def get_customers():
        return "Hello, get_customers!"
    
    @app.route("/get/<string:customerID>", methods=['GET'])
    def get_customer(customerID):
        return "Hello, get!"
    
    @app.route("/add", methods=['POST'])
    def add_customer():
        return "Hello, add!"

    Now we can see if we access http://localhost:5000/add
    We get: Method Not Allowed , because it only accepts POST and browser by default, does GET

    Let's fill out our routes

    Get Customers

    text
    @app.route("/", methods=['GET'])
    def get_customers():
        customers = getCustomers()
        return json.dumps(customers)

    Get Customer

    text
    @app.route("/get/<string:customerID>", methods=['GET'])
    def get_customer(customerID):
         customer = getCustomer(customerID)
         return customer

    HTTP Status Codes

    You can access your browser's developer tools and see the network tab

    Status codes are important for troubleshooting production traffic.

    200 = Success.

    We can then access a customer via the /get/<customerID> route.
    Our Customer ID's are a to h and notice we get an Internal Server Error if
    we pass an incorrect ID.

    500 = Internal Server Error

    This means something went wrong in our code. See the logs indicating a KeyError, because we're trying to access a key in our customer dictionary that does not exist.

    Let's handle that error

    text
    #update our getCustomer function to check if the key exists
    
    def getCustomer(customerID):
      customers = getCustomers()
      if customerID in customers:
        return customers[customerID]
      else: 
        return {}

    Note when we rerun this, we get a blank customer back.

    Also note we're getting a 200 status code.

    This may not be super appropriate, as our customer we requested is technically not found. There is a status code for that

    404 = Not Found

    Let's handle the status code.

    text
    @app.route("/get/<string:customerID>", methods=['GET'])
    def get_customer(customerID):
        customer = getCustomer(customerID)
        if customer == {}:
          return {}, 404
        else:
          return customer

    Add Customer

    In order to add a customer, we need to be able to read request data.

    More about Requests here

    Import requests library

    text
    from flask import request

    Read the request body as JSON:

    text
    @app.route("/add", methods=['POST'])
    def add_customer():
        print(request.json)
        return "success", 200

    Let's test that!

    text
    curl -X POST http://localhost:5000/add -d '{ "data" : "hello" }'

    Notice our print message is None, this is because we need to specify content type in the request header. Let's send the content type as JSON.

    text
    curl -X POST \
    -H 'Content-Type: application/json' \
    http://localhost:5000/add -d '{ "data" : "hello" }'

    Now we see that we read the data as {'data': 'hello'}

    This allows us to read customer data as JSON, validate it, and write it to storage.

    Let's POST a customer:

    text
    curl -X POST -v \
    -H 'Content-Type: application/json' \
    http://localhost:5000/add -d '
    {
      "customerID": "i",
      "firstName": "Bob",
      "lastName": "Smith"
    }'

    Note if we break the json format, we get a 400 status code

    400 = Bad Request

    400 indicates a bad request, letting the client know they have to send the right formatted data.

    Docker

    Let's build our container image and run it while mounting our customer file

    Our final dockerfile

    text
    FROM python:3.9.6-alpine3.13 as dev
    
    WORKDIR /work
    
    FROM dev as runtime
    WORKDIR /app
    COPY ./src/requirements.txt /app/
    RUN pip install -r /app/requirements.txt
    
    COPY ./src/app.py /app/app.py
    ENV FLASK_APP=app.py
    
    CMD flask run -h 0.0.0 -p 5000

    Build our container.

    text
    cd python\introduction\part-4.http
    
    docker build . -t customer-app

    Notice that we need to mount our customers.json file into the container.

    But also notice the containers working directory is /app and in the app.py
    we load customers.json from the working directory which is the same directory as the app.

    When mounting files to a container, docker effectively formats that path, so to mount our json file, we would lose our app.py because of the format.

    Therefore it's always important to mount configs, secrets or data to seperate folders.

    Let's change that to /data

    text
    #set a global variable 
    dataPath = "/data/customers.json"

    Now we can change all references to customers.json to our variable.
    And then mount that location:

    text
    docker build . -t customer-app
    docker run -it -p 5000:5000 -v ${PWD}:/data customer-app

    Overview (`python/introduction/part-5.database.redis/README.md`)

    Introduction to Python: Storing data in Redis Database

    So far, we've learnt Python fundamentals, worked with data, files , HTTP and more importantly, basic data structures like csv and json.

    Be sure to checkout:

    Part 1: Intro to Python

    Part 2: Files

    Part 3: JSON

    Start up a Redis Cluster

    Follow my Redis clustering Tutorial

    Redis Guide

    Code is over here

    Python Dev Environment

    The same as Part 1+2+3+4, we start with a dockerfile where we declare our version of python.

    text
    FROM python:3.9.6-alpine3.13 as dev
    
    WORKDIR /work

    Let's build and start our container:

    text
    cd python\introduction\part-5.database.redis
    
    docker build --target dev . -t python
    docker run -it -v ${PWD}:/work -p 5000:5000 --net redis python sh
    
    /work # python --version
    Python 3.9.6

    Our application

    We're going to use what we've learnt in part 1,2 & 3 and create
    our customer app that handles customer data

    Firstly we have to import our dependencies:

    text
    import os.path
    import csv
    import json
    from flask import Flask
    from flask import request

    Then we have a class to define what a customer looks like:

    text
    class Customer:
      def __init__(self, c="",f="",l=""):
        self.customerID = c
        self.firstName  = f
        self.lastName   = l
      def fullName(self):
        return self.firstName + " " + self.lastName

    And also set a global variable for the location of our videos json file:

    text
    dataPath = "./customers.json"

    Then we need a function which returns our customers:

    text
    def getCustomers():
      if os.path.isfile(dataPath):
        with open(dataPath, newline='') as customerFile:
          data = customerFile.read()
          customers = json.loads(data)
          return customers
      else: 
        return {}

    Here is a function to return a specific customer:

    text
    def getCustomer(customerID):
      customers = getCustomers()
    
      if customerID in customers:
        return customers[customerID]
      else:
        return {}

    And finally a function for updating our customers:

    text
    def updateCustomers(customers):
      with open(dataPath, 'w', newline='') as customerFile:
        customerJSON = json.dumps(customers)
        customerFile.write(customerJSON)

    In the previous episode, we've created a json file to hold all our customers.

    We've learnt how to read and write to file and temporarily use the file for our storage.

    Let's create a file called customers.json :

    text
    {
      "a": {
        "customerID": "a",
        "firstName": "James",
        "lastName": "Baker"
      },
      "b": {
        "customerID": "b",
        "firstName": "Jonathan",
        "lastName": "D"
      },
      "c": {
        "customerID": "c",
        "firstName": "Aleem",
        "lastName": "Janmohamed"
      },
      "d": {
        "customerID": "d",
        "firstName": "Ivo",
        "lastName": "Galic"
      },
      "e": {
        "customerID": "e",
        "firstName": "Joel",
        "lastName": "Griffiths"
      },
      "f": {
        "customerID": "f",
        "firstName": "Michael",
        "lastName": "Spinks"
      },
      "g": {
        "customerID": "g",
        "firstName": "Victor",
        "lastName": "Savkov"
      }
    }

    Now that we have our customer data and functions to read and update our customer data, let's define our Flask application:

    text
    app = Flask(__name__)

    We create our route to get all customers:

    text
    @app.route("/", methods=['GET'])
    def get_customers():
        customers = getCustomers()
        return json.dumps(customers)

    A route to get one customer by ID:

    text
    @app.route("/get/<string:customerID>", methods=['GET'])
    def get_customer(customerID):
        customer = getCustomer(customerID)
    
        if customer == {}:
          return {}, 404
        else:
          return customer

    And finally a route to update or add customers called /set :

    text
    @app.route("/set", methods=['POST'])
    def add_customer():
        jsonData = request.json
    
        if "customerID" not in jsonData:
          return "customerID required", 400
        if "firstName" not in jsonData:
          return "firstName required", 400
        if "lastName" not in jsonData:
          return "lastName required", 400
        
        customers = getCustomers()
        customers[jsonData["customerID"]] = Customer( jsonData["customerID"], jsonData["firstName"], jsonData["lastName"]).__dict__
        updateCustomers(customers)
        return "success", 200

    Before we can be done, we need to import our Flask dependency we covered in our Python HTTP video.

    Let's create a requirements.txt file:

    text
    Flask == 2.0.2

    We can install our dependencies using:

    text
    pip install -r requirements.txt

    This gives us a web application that handles customer data and using a file as it's storage

    To test it, we can start up Flask:

    text
    export FLASK_APP=src/app
    flask run -h 0.0.0 -p 5000

    Now we can confirm it's working by accessing our application in the browser on http://localhost:5000

    Redis

    To connect to Redis, we'll use a popular library called redis-py which we can grab from here

    The pip install is over here

    Let's add that to our requirements.txt dependency files.

    text
    redis == 3.5.3

    We can proceed to install it using pip install

    text
    pip install -r requirements.txt

    Now to connect to Redis in a highly available manner, we need to take a look at the
    Sentinel support section of the guide

    Let's test the library. The beauty of Python is that it's a scripting language, so we don't have to compile and keep restarting our application, we can test each line of code.

    text
    python
    from redis.sentinel import Sentinel
    
    sentinel = Sentinel([('sentinel-0', 5000),('sentinel-1', 5000),('sentinel-2', 5000)], socket_timeout=0.1)
    
    sentinel.discover_master('mymaster')
    
    sentinel.discover_slaves('mymaster')
    
    master = sentinel.master_for('mymaster',password = "a-very-complex-password-here", socket_timeout=0.1)
    
    slave = sentinel.slave_for('mymaster',password = "a-very-complex-password-here", socket_timeout=0.1)
    
    master.set('foo', 'bar')
    slave.get('foo')

    We can demonstrate reading and writing a key value pair.

    We can also demonstrate failure, when we stop the current master, we'll get a connection error. It's important to implement retry logic.

    If we wait a moment and execute commands again, we will see that it starts to work.

    text
    # stop current master
    docker rm -f redis-0
    
    master.set('foo', 'bar2')
    
    redis.exceptions.ConnectionError: Connection closed by server.
    
    # retry moments later...
    
    master.set('foo', 'bar2')
    slave.get('foo')
    
    sentinel.discover_master('mymaster')
    sentinel.discover_slaves('mymaster')

    We can find the current master by running docker inspect to see who owns that IP address.

    Start up redis-0 again, to simulate a recovery from failure.

    Connecting our App to Redis

    To connect to redis, we'll want to read the connection info from environment variables. Let's set some global variables.

    text
    import os
    
    redis_sentinels = os.environ.get('REDIS_SENTINELS')
    redis_master_name = os.environ.get('REDIS_MASTER_NAME')
    redis_password = os.environ.get('REDIS_PASSWORD')

    We will need to restart our container so we can inject these environment variables. Let's go ahead and do that:

    text
    docker run -it -p 5000:5000 `
      --net redis `
      -v ${PWD}:/work `
      -e REDIS_SENTINELS="sentinel-0:5000,sentinel-1:5000,sentinel-2:5000" `
      -e REDIS_MASTER_NAME="mymaster" `
      -e REDIS_PASSWORD="a-very-complex-password-here" `
      python sh
    
    # re-install our dependencies
    pip install -r requirements.txt

    Now we can setup a client:

    text
    from redis.sentinel import Sentinel
    
    sentinels = []
    
    for s in redis_sentinels.split(","):
      sentinels.append((s.split(":")[0], s.split(":")[1]))
    
    redis_sentinel = Sentinel(sentinels, socket_timeout=5)
    redis_master = redis_sentinel.master_for(redis_master_name,password = redis_password, socket_timeout=5)

    Retry logic

    Now we noticed that if we have a master that fails, the sentinels will choose and assign a new master. We can see this by simply retrying our redis command.

    When talking to redis we need to have some retry capability to be able to recover from this scenario.

    Let's build a retry function at the top of our application, that runs a redis command:

    text
    def redis_command(command, *args):
      max_retries = 3
      count = 0
      backoffSeconds = 5
      while True:
        try:
          return command(*args)
        except (redis.exceptions.ConnectionError, redis.exceptions.TimeoutError):
          count += 1
          if count > max_retries:
            raise
        print('Retrying in {} seconds'.format(backoffSeconds))
        time.sleep(backoffSeconds)

    We can test out our redis_command by calling it and printing the result
    to the screen

    text
    print(redis_command(redis_master.set, 'foo', 'bar'))
    print(redis_command(redis_master.get, 'foo'))

    We can simulate failure again, by finding and stopping the current master.

    Once we're done with our tests, we can exec into the current master and run FLUSHALL to remove our test records from redis.

    Saving our data to Redis

    Now let's change our customer functions to point to Redis instead of file

    Starting with getCustomer to retrieve a single customer

    text
    def getCustomer(customerID):
      customer = redis_command(redis_master.get, customerID)
      
      if customer == None:
        return {}
      else:
        c = str(customer, 'utf-8')
        return json.loads(c)

    Now we can use that to return all our customers by updating the getCustomers function:

    text
    def getCustomers():
      customers = {}
      customerIDs = redis_command(redis_master.scan_iter, "*")
      for customerID in customerIDs:
        customer = getCustomer(customerID)
        customers[customer["customerID"]] = customer
      
      return customers

    Let's improve our functions by adding a new function to update a single customer:

    text
    def updateCustomer(customer):
      redis_command(redis_master.set, customer.customerID, json.dumps(customer.__dict__))

    And finally we can use that function to update all customers by tweaking our updateCustomers function:

    text
    def updateCustomers(customers):
      for customer in customers:
        updateCustomer(customer)

    Now our simple functions are done, let's hook them up to our endpoints

    text
    # firstly delete these test lines
    print(redis_command(redis_master.set, 'foo', 'bar'))
    print(redis_command(redis_master.get, 'foo'))

    Our simple Get all

    text
    @app.route("/", methods=['GET'])
    def get_customers():
      customers = getCustomers()
      return json.dumps(customers)

    Our Get by ID

    text
    @app.route("/get/<string:customerID>", methods=['GET'])
    def get_customer(customerID):
        customer = getCustomer(customerID)
    
        if customer == {}:
          return {}, 404
        else:
          return customer

    And our update endpoint to update a customer

    text
    @app.route("/set", methods=['POST'])
    def add_customer():
        jsonData = request.json
    
        if "customerID" not in jsonData:
          return "customerID required", 400
        if "firstName" not in jsonData:
          return "firstName required", 400
        if "lastName" not in jsonData:
          return "lastName required", 400
        
        customer = Customer( jsonData["customerID"], jsonData["firstName"], jsonData["lastName"])
        updateCustomer(customer)
        return "success", 200

    Docker

    Let's build our container image and run it while mounting our customer file

    Our final dockerfile

    text
    FROM python:3.9.6-alpine3.13 as dev
    
    WORKDIR /work
    
    FROM dev as runtime
    WORKDIR /app
    
    COPY ./requirements.txt /app/
    RUN pip install -r /app/requirements.txt
    
    COPY ./src/app.py /app/app.py
    ENV FLASK_APP=app.py
    
    CMD flask run -h 0.0.0 -p 5000

    Build our container.

    text
    cd python\introduction\part-5.database.redis
    
    docker build . -t customer-app

    Now we can run our production container:

    text
    docker build . -t customer-app
    
    docker run -it -p 5000:5000 `
      --net redis `
      -e REDIS_SENTINELS="sentinel-0:5000,sentinel-1:5000,sentinel-2:5000" `
      -e REDIS_MASTER_NAME="mymaster" `
      -e REDIS_PASSWORD="a-very-complex-password-here" `
      customer-app

    Overview (`security/letsencrypt/introduction/readme.md`)

    Let's encrypt

    Introduction

    To start off, I run an NGINX web server.

    This could be running anywhere in the cloud.

    text
    docker run -it -p 80:80 nginx bash
    
    # get my public IP for this server 
    curl ifconfig.co
    
    # lets get out of the container
    exit

    Now that we have the public IP for our server, lets start it up again

    This time, without bash

    We should be able to access it in the browser

    text
    docker run -it -p 80:80 nginx

    In the video, we create a DNS record and point it to the IP of our server

    Certbot

    The docs

    To build certbot, i simply change directory and build my certbot container

    text
    cd .\security\letsencrypt\introduction\
    
    docker build . -t certbot
    
    docker run -it --rm --name certbot `
    -v ${PWD}:/letsencrypt `
    -v ${PWD}/certs:/etc/letsencrypt `
    certbot bash

    NGINX

    We've customised our nginx.conf as shown in the video

    Run this NGINX, we mount the shared folder that certbot will use:

    text
    cd .\security\letsencrypt\introduction\
    
    docker run -it --rm --name nginx `
    -v ${PWD}/nginx.conf:/etc/nginx/nginx.conf `
    -v ${PWD}:/letsencrypt `
    -v ${PWD}/certs:/etc/letsencrypt `
    -p 80:80 `
    -p 443:443 `
    nginx

    Issue certificate

    In certbot, generate our cert:

    text
    certbot certonly --webroot
    
    # webroot is the folder we mounted: /letsencrypt
    
    # certificate outputs under etc/letsencrypt/live/**
    # since we share this volume with our webserver, we dont need to copy
    # certificates across.
    
    IMPORTANT NOTES:
     - Congratulations! Your certificate and chain have been saved at:
       /etc/letsencrypt/live/marcel.guru/fullchain.pem
       Your key file has been saved at:
       /etc/letsencrypt/live/marcel.guru/privkey.pem
       Your cert will expire on 2020-12-03. To obtain a new or tweaked
       version of this certificate in the future, simply run certbot
       again. To non-interactively renew *all* of your certificates, run
       "certbot renew"
     - Your account credentials have been saved in your Certbot
       configuration directory at /etc/letsencrypt. You should make a
       secure backup of this folder now. This configuration directory will
       also contain certificates and private keys obtained by Certbot so
       making regular backups of this folder is ideal.
     - If you like Certbot, please consider supporting our work by:
    
       Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
       Donating to EFF:                    https://eff.org/donate-le

    Renewal

    To do a dry run of cert renewal:

    text
    certbot renew --dry-run

    Reload our NGINX web server if the certs change:

    text
    docker exec -it nginx sh -c "nginx -s reload"

    Checkout the Certbot docs for more details


    Overview (`storage/redis/clustering/readme.md`)

    Replication

    Documentation here

    Confi