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:
Qualifications have its challenges:
Cloud certifications have its challenges:
🎯 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:
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
📖 Table of Contents
- Chapter 1: Source Control & GIT
- Chapter 2: Operating Systems
- Chapter 3: Linux Monitoring
- Chapter 4: The Web & HTTP
- Chapter 5: CI/CD & Automation
- 🚧Chapter 6: Containers & Docker
- 🚧Chapter 7: Docker Compose
- 🚧Chapter 8: Infrastructure as Code
- 🚧Chapter 9: Introduction to Cloud
📖 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
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.
+------------+
| videos-web |
| |
+------------+A simple API: playlists-api
For videos-web to get any content, it needs to make a call to playlists-api
+------------+ +---------------+
| 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
+------------+ +---------------+ +--------------+
| 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:
{
"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
+------------+ +-----------+
| 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
+------------+ +---------------+ +--------------+
| 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:
cd tracing
docker-compose build
docker-compose upYou 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:
---
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 skillWe 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.
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/activateExample 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:
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.mdFile MCP Example:
Use the MCP in three steps:
(
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:
pip3 install mcp-server-gitUse the MCP in three steps:
(
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:
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 --helpStart it:
github-mcp-server http --port 8080We need to provide authentication with Github:
export GITHUB_PERSONAL_ACCESS_TOKEN=''Call it in three steps:
# 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
kind create cluster --name mcp --image kindest/node:v1.34.0In this example we will use a Kubernetes MCP and setup a read-only ServiceAccount so it can read our cluster.
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 8080Call it in three steps:
# 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:
go versionRun Go in Docker
We can also run go in a small docker container:
cd golang\introduction
docker build --target dev . -t go
docker run -it -v ${PWD}:/work go sh
go versionCode 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
mkdir app- Define a module path (github.com/docker-development-youtube-series/golang/introdouction/app)
# 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.
package main
import "fmt"
func main() {
fmt.Println("Hello, world.")
}- Run your application code
You can run your application
go run app.goBuilding our Program
Build your application into a static binary:
go buildThis will produce a compiled program called app
You can run this program easily:
./appInstall 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)"
go install github.com/docker-development-youtube-series/golang/introdouction/appThe 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:
// 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
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
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 :
//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!
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:
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!
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.
//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
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
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
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
FROM golang:1.15 as dev
WORKDIR /workBuilding our code
FROM golang:1.15 as build
WORKDIR /app
COPY ./app/* /app/
RUN go build -o appThe Runtime
FROM alpine as runtime
COPY --from=build /app/app /
CMD ./appBuilding the Container
docker build . -t customer-appRunning the Container
docker run customer-appOverview (`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
cd hashicorp/vault-2022
kind create cluster --name vault --image kindest/node:v1.21.1 --config kind.yamlNext 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.
docker run -it --rm --net host -v ${HOME}/.kube/:/root/.kube/ -v ${PWD}:/work -w /work alpine shInstall kubectl
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/kubectlInstall helm
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/helmNow we have helm and kubectl and can access our kind cluster:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
vault-control-plane Ready control-plane,master 37s v1.21.1Let's add the Helm repositories, so we can access the Kubernetes manifests
helm repo add hashicorp https://helm.releases.hashicorp.comStorage: Consul
We will use a very basic Consul cluster for our Vault backend.
Let's find what versions of Consul are available:
helm search repo hashicorp/consul --versionsWe 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:
mkdir manifests
helm template consul hashicorp/consul \
--namespace vault \
--version 0.39.0 \
-f consul-values.yaml \
> ./manifests/consul.yamlDeploy the consul services:
kubectl create ns vault
kubectl -n vault apply -f ./manifests/consul.yaml
kubectl -n vault get podsTLS 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
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.pemGenerate Kubernetes Manifests
Let's find what versions of vault are available:
helm search repo hashicorp/vault --versionsIn 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:
helm template vault hashicorp/vault \
--namespace vault \
--version 0.19.0 \
-f vault-values.yaml \
> ./manifests/vault.yamlDeployment
kubectl -n vault apply -f ./manifests/vault.yaml
kubectl -n vault get podsInitialising Vault
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 statusWeb UI
Let's checkout the web UI:
kubectl -n vault get svc
kubectl -n vault port-forward svc/vault-ui 443:8200Now 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
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"
exitSummary
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
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.
#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:9512edae126da271b66b990b6fff768fbb7cd786c7d39e86bdf55906352fdf62TLS 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
kubectl create ns vault-example
kubectl -n vault-example apply -f ./hashicorp/vault/server/
kubectl -n vault-example get podsStorage
kubectl -n vault-example get pvcensure 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
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 podsDeploy the Injector
VIDEO: <Coming-Soon>
Injector allows pods to automatically get secrets from the vault.
kubectl -n vault-example apply -f ./hashicorp/vault/injector/
kubectl -n vault-example get podsInjector Kubernetes Auth Policy
For the injector to be authorised to access vault, we need to enable K8s auth
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 podsSummary
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
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
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
# 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 textMore details about EKS storage here
Setup a namespace
kubectl create ns jenkinsSetup our storage for Jenkins
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 pvcDeploy Jenkins
# rbac
kubectl apply -n jenkins -f ./jenkins/jenkins.rbac.yaml
kubectl apply -n jenkins -f ./jenkins/jenkins.deployment.yaml
kubectl -n jenkins get podsExpose a service for agents
kubectl apply -n jenkins -f ./jenkins/jenkins.service.yamlJenkins Initial Setup
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
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 dockerDocker Jenkins Agent
Docker file is here
# you can build it
cd ./jenkins/dockerfiles/
docker build . -t aimvector/jenkins-slaveContinue Jenkins setup
Install Kubernetes Plugin
Configure Plugin: Values I used are here
Install Kubernetes Plugin
Try a pipeline
/* 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
cd kubernetes/affinity
kind create cluster --name demo --image kindest/node:v1.28.0 --config kind.yamlTest the cluster:
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.0Node 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
osselector aslinux, a pod can only be scheduled if there is a node available whereoslabel islinux
Node Affinity allows an expression.
kubectl apply -f node-affinity.yamlWe can see our pods are prefering SSD and are always going to us-east
kubectl get pods -owide
#introduce more pods
kubectl scale deploy app-disk --replicas 10
#observe all pods on demo-workerIf 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.
kubectl taint nodes demo-worker type=ssd:NoSchedule-
kubectl taint nodes demo-worker3 type=ssd:NoSchedule-Scale back down to 0
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 -owidePod Affinity
Now Pod Affinity is an expression to allow us to state that pods should gravitate towards other pods
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 3Pod 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:
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:
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 -owideOverview (`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 AutoscalerYou need a cloud based cluster that supports the cluster autoscaler - For
HPAWe'll use kind
Cluster Autoscaling - Creating an AKS Cluster
# 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 5Horizontal Pod Autocaling - Creating a Kind Cluster
My Node has 6 CPU cores for this demo
kind create cluster --name hpa --image kindest/node:v1.18.4Metric 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 :
- --kubelet-insecure-tls
- --kubelet-preferred-address-types="InternalIP"Deployment:
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 nodesExample 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
# 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 podsCluster 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
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 2Deploy an autoscaler
# 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-cpuVertical 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
kind create cluster --name certmanager --image kindest/node:v1.34.0Concepts
It's important to understand the various concepts and new Kubernetes resources thatcert-manager introduces.
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
#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=trueCert Manager Resources
We can see our components deployed
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 6m57sTest Certificate Issuing
Let's create some test certificates
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-testConfiguration
https://cert-manager.io/docs/configuration/
Setup my DNS
I can get the public IP address of my computer by running a simple command:
curl ifconfig.coI 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
kubectl apply -f kubernetes/deployments/
kubectl apply -f kubernetes/services/
kubectl get podsOption 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. *
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.devCreate an Ingress Let's Encrypt Issuer
We create a ClusterIssuer for Ingress that allows us to issue certs in any namespace
kubectl apply -f kubernetes/cert-manager/issuer-ingress.yaml
# check the issuer
kubectl describe clusterissuer letsencrypt-issuerIssue Certificate
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-tlsOption 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-managerneeds to haveconfig.enableGatewayAPI=trueenabled. *
Create a Gateway Class
kubectl apply -f kubernetes/gateway-api/traefik/01-gatewayclass.yamlCreate a Gateway
We can use our Gateway from our Traefik Gateway API Guide
kubectl apply -f kubernetes/gateway-api/traefik/02-gateway.yamlCreate a Gateway API Let's Encrypt Issuer
We create a ClusterIssuer for Ingress that allows us to issue certs in any namespace
kubectl apply -f kubernetes/cert-manager/issuer-gatewayapi.yaml
# check the issuer
kubectl describe clusterissuer letsencrypt-issuerIssue Certificate
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-tlsOverview (`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
kind create cluster --name demo --image kindest/node:v1.33.1Checkout our cluster
kubectl get nodes
NAME STATUS ROLES AGE VERSION
demo-control-plane Ready control-plane 19m v1.33.1Deploy a ConfigMap
kubectl apply -f kubernetes/configmaps/configmap.yamlDeploy a pod that consumes the ConfigMap
kubectl apply -f kubernetes/configmaps/deployment.yamlChecking the deployment pods
The kubectl describe command is essential for checking out kubernetes resources
kubectl describe deploy example-deployWe can checkout the config contents inside the pod's container with the kubectl exec command to get shell
kubectl exec -it <pod-name> -- bashWhen 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
kind create cluster --name crossplane --image kindest/node:v1.33.0Installing 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
helm repo add crossplane-stable https://charts.crossplane.io/stable
helm repo update
helm search repo crossplane-stable --versionsWe'll install version 1.19.1 at the time of this guide
VERSION=1.19.1
helm install crossplane \
crossplane-stable/crossplane \
--namespace crossplane-system \
--version $VERSION \
--create-namespaceView our install:
kubectl get pods -n crossplane-system
kubectl get deployments -n crossplane-systemOnce the pods are all running, we can see the api-versions
kubectl api-versions | grep crossplaneWe can also see the new k8s objects that are installed with
kubectl api-resources | grep crossplaneProviders
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:
kubectl apply -f kubernetes/crossplane/provider-azure.yamlCheck our provider:
kubectl get provider
kubectl describe provider provider-family-azureProvider 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
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.jsonCreate Provider Kubernetes Secret
kubectl create secret \
generic azure-secret \
-n crossplane-system \
--from-file=creds=./azure-credentials.jsonDeploy the Provider Configuration
kubectl apply -f kubernetes/crossplane/providerconfig-azure.yamlCreate Provider Resources
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 firstWe 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:
kubectl apply -f kubernetes/crossplane/provider-azure-network.yamlRetry the resource creation:
kubectl apply -f kubernetes/crossplane/resources/azure/resource-vnet.yaml
kubectl get virtualnetworkDeploy a Virtual Network Subnet
kubectl apply -f kubernetes/crossplane/resources/azure/resource-subnet.yaml
kubectl get subnetDeploy a Virtual Network Card
kubectl apply -f kubernetes/crossplane/resources/azure/resource-networkcard.yaml
kubectl get networkinterfaceDeploy a Virtual Machine
Firstly need to add the compoute provider for Azure
kubectl apply -f kubernetes/crossplane/provider-azure-compute.yamlDeploy a Virtual Machine:
kubectl apply -f kubernetes/crossplane/resources/azure/resource-virtualmachine.yamlCleanup Resources
kubectl delete linuxvirtualmachine marcel-test
kubectl delete networkinterface marcel-test
kubectl delete subnet marcel-test
kubectl delete virtualnetwork marcel-testOverview (`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:
cd kubernetes/daemonsets
kind create cluster --name daemonsets --image kindest/node:v1.20.2 --config kind.yamlTest our cluster:
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.2Introduction
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
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
kubectl logs <example-daemonset-xxxx>Cleanup:
kubectl delete ds example-daemonsetBasic 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
kubectl apply -f daemonset-communication.yaml
kubectl get podsCommunicating 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
kubectl apply -f pod.yaml
kubectl exec -it pod -- bashService Type: ClusterIP or LoadBalancer
Let's deploy a service of type ClusterIP
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-workerNode 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:
ports:
- containerPort: 80
hostPort: 80
name: "http"This means we can contact the daemonset pod using the Node IP and port:
# 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-worker2Service: Headless service
Let's deploy a headless service where clusterIP: None
kubectl apply -f ./services/headless-service.yamlThere are a few ways to discover our pods:
- Discover the DNS records via the "Headless" services
apk add --no-cache bind-tools
dig daemonset-svc-headless.default.svc.cluster.localNotice it resolves to multiple DNS records for each pod:
;; 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- Discover pods endpoints by retrieving the endpoints for the headless service
kubectl describe endpoints daemonset-svc-headlessExample:
Addresses: 10.244.0.5,10.244.1.2,10.244.2.2,10.244.3.2Get A records for each pod by using the following format:
.<my-namespace>.pod.<cluster-domain.example>.
#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.localCommunicate with the pods over DNS:
curl http://10-244-0-5.default.pod.cluster.local
Hello from daemonsets-control-planeReal world Examples:
Monitoring Nodes: Node-Exporter Daemonset
We clone the official kube-prometheus repo to get monitoring manifests for Kubernetes.
git clone https://github.com/prometheus-operator/kube-prometheus.gitCheck the compatibility matrix here
For this demo, we will use the compatible version tag 0.8
git checkout v0.8.0Deploy Prometheus Operator and CRDs
cd .\manifests\
kubectl create -f .\setup\Deploy remaining resources including node exporter daemonset
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 9090See 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
docker run -it -v ${PWD}:/work -v ${HOME}/.kube/:/root/.kube/ -w /work --net host alpine shInstall 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.
apk add curl unzip bash sudoAutomatic Installation
We can install the latest version of Datree with the command advertised:
curl https://get.datree.io | /bin/bashManual Installation
Or we can grab a specific version of datree on the GitHub releases page.
For example: 1.5.20 binary
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/datreeNow we can run the datree command:
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
datree test ./kubernetes/datree/example/deployment.yamlPolicy checks
When we fix our YAML file, notice if we run datree test again, we get some policy checks failing
datree test ./kubernetes/datree/example/deployment.yamlLet's test some other types of Kubernetes objects
datree test ./kubernetes/services/service.yaml
datree test ./kubernetes/configmaps/configmap.yaml
datree test ./kubernetes/statefulsets/statefulset.yaml
datree test ./kubernetes/ingress/ingress.yamlSchema 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
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.yamlWe 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:
datree test kubernetes/tutorials/basics/yaml/*.y*mlPolicies
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>
(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:
datree test kubernetes/datree/example/deployment.yaml -p productionFor 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
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/helmLet's install the helm plugin for datree
helm plugin install https://github.com/datreeio/helm-datreeNow we can test a helm chart we have in our repo from my helm tutorial
cd kubernetes/helm
helm datree test example-app \
-- --values ./example-app/example-app-01.values.yamlKustomize
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
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.
kind create cluster --name datree --image kindest/node:v1.23.6Let's also grab kubectl:
curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.23.6/bin/linux/amd64/kubectl
chmod +x ./kubectl
mv ./kubectl /usr/local/bin/kubectlWe'll need a datree token so our admission controller can read our policies
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.
apk add opensslLet's grab the datree manifests
curl -L https://get.datree.io/admission-webhook -o datree.sh
chmod +x datree.sh
bash datree.shWith 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:
kubectl apply -f kubernetes/datree/example/deployment.yamlOutput:
/* 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.
kubectl create ns examplesAnd finally we can deploy our resource, and specify a namespace:
kubectl apply -n examples -f kubernetes/datree/example/deployment.yaml
deployment.apps/example-deploy createdKubectl
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:
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.shNow we have datree inside kubectl and can perform checks in our cluster.
We can check our entire namespace now, which should be pretty clean:
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-deployOverview (`kubernetes/deployments/readme.md`)
Introduction to Kubernetes: Deployments
Build an example app:
# 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 pythonTake 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
kind create cluster --name fluxcd --image kindest/node:v1.26.3Run a container to work in
run Alpine Linux:
docker run -it --rm -v ${HOME}:/root/ -v ${PWD}:/work -w /work --net host alpine shinstall some tools
# 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/kubectltest cluster access:
/work # kubectl get nodes
NAME STATUS ROLES AGE VERSION
fluxcd-control-plane Ready control-plane 54s v1.26.3Get 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.
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/fluxNow we can run flux --help to see its installed
Check our cluster
flux check --preDocumentation
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:
export GITHUB_TOKEN=<your-token>Then we can bootstrap it using the GitHub bootstrap method
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 KustomizationCheck the source code that flux bootstrap created
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
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
- repositories
- infra-repo
- example-app-1
- example-app-2build 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/
# 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 fluxcdsetup our gitops pipeline
Now we will also have a "infra-repo" GitHub repo where infrastructure configuration files for GitOps live.
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:80Now 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
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
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
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-2022We need to create a image registry credential where we will push our image:
kubectl -n default create secret docker-registry dockerhub-credential --docker-username '' --docker-password '' --docker-email '[email protected]'build and push example-app-2
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.1We 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
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 defaultThere are a number of ways to authenticate with GitRepositories
kubectl describe imagepolicy example-app-2
kubectl describe imagerepository example-app-2
kubectl describe imageupdateautomation example-app-2Build and push our example-app-2
#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-2Overview (`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
fluxctl4 - Installing Flux
Make sure you are pointing to the kubernetes cluster you want to use
kubectl config current-context
kubectl get nodeskubectl 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.
We need a Kubernetes cluster
Lets create a Kubernetes cluster to play with using kind
kind create cluster --name gatewayapi --image kindest/node:v1.34.0Test our cluster and makes sure kubectl is configured for it:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
gatewayapi-control-plane Ready control-plane 40s v1.34.0Gateway 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.
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.1/standard-install.yamlThis 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.
# 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/TCPCreate 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
127.0.0.1 example-app.com
127.0.0.1 example-app-go.com
127.0.0.1 example-app-python.comInstall 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:
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-namespaceAs we don't have a LoadBalancer service in kind, let's port-forward so we can pretend we have one
# 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 80Install 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.
GatewayClass is a cluster-scoped resource defined by the infrastructure provider. This resource represents a class of Gateways that can be instantiated.
kubectl apply -f kubernetes/gateway-api/traefik/01-gatewayclass.yaml
# check
kubectl get gatewayclass
# describe
kubectl describe gatewayclassInstall a Gateway
Next we need to install a Gateway that implements our Gateway Class
Note that we use a Traefik Gateway in our example.
This gateway lives in the same namespace as the routes and applications
kubectl apply -f kubernetes/gateway-api/traefik/02-gateway.yamlTraffic Management Features : HTTP Routes
The important fields on HTTP Route we will cover:
parentRefssectionNamehostnamesrulesmatchesfilters
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
kubectl apply -f kubernetes/gateway-api/03-httproute-by-hostname.yamlRoute 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/*
kubectl apply -f kubernetes/gateway-api/04-httproute-by-path-exact.yamlRoute 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
kubectl apply -f kubernetes/gateway-api/05-httproute-by-path-rewrite.yamlRequest\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
kubectl port-forward svc/web-app 8000:80The Header modification:
kubectl apply -f kubernetes/gateway-api/06-httproute-header-modify.yamlHTTPS and TLS
In my video, I generate a test TLS cert using mkcert
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 -installNow that we have a TLS cert, we can create a Kubernetes secret to store it:
kubectl create secret tls secret-tls -n default --cert kubernetes/gateway-api/tls/cert.pem --key kubernetes/gateway-api/tls/key.pemWe 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
kubectl apply -f kubernetes/gateway-api/07-httproute-tls.yamlLet's port-forward to 443 since that is where TLS is exposed:
kubectl -n traefik port-forward svc/traefik 443Result:
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
kind create cluster --name helm --image kindest/node:v1.31.1Getting 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:
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.1Install Helm CLI
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/helmCreate our first Chart
For reference in the rest of the guide, I have left my full templates in:<GitRepo>/kubernetes/helm/example-app
cd kubernetes/helm
mkdir temp && cd temp
helm create example-appCleanup the template
We can delete unwanted files:
- delete everything under /templates, keeping only
_helpers.tpl - delete
testsfolder undertemplates
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
helm template example-app example-appInstall our app using our Chart
helm install example-app example-app
# list our releases
helm list
# see our deployed components
kubectl get all
kubectl get cm
kubectl get secretValue injections for our Chart
For CI systems, we may want to inject an image tag as a build number
Basic parameter injection:
# 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 listMake 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
helm install example-app-02 example-app --values ./example-app/example-app-02.values.yamlTrigger 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:
# 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.yamlIf\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
{{- 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.yamlOverview (`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:
kind create cluster --name dev --image kindest/node:v1.23.5
kind create cluster --name prod --image kindest/node:v1.23.5See cluster up and running:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
prod-control-plane Ready control-plane,master 2m12s v1.23.5Understanding the KUBECONFIG
Default location of the kubeconfig file is in /.kube/config
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
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:
#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:
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 nodesWorking 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:
kubectl get namespacesBy default, kubectl commands will run against the default namespace
List resources in a namespace
kubectl get <resource>
kubectl get pods
kubectl get deployments
kubectl get services
kubectl get configmaps
kubectl get secrets
kubectl get ingressCreate resources in a namespace
We can create a namespace with the kubectl create command:
kubectl create ns example-appsLet's create a couple of resources:
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 secretWorking 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:
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.yamlCreate 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:
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:
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 80Clean up
kind delete cluster --name dev
kind delete cluster --name prodOverview (`kubernetes/kustomize/readme.md`)
The Basics
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 exampleKustomize
Build
kubectl kustomize .\kubernetes\kustomize\ | kubectl apply -f -
# OR
kubectl apply -k .\kubernetes\kustomize\
kubectl delete ns exampleOverlays
kubectl kustomize .\kubernetes\kustomize\environments\production | kubectl apply -f -
# OR
kubectl apply -k .\kubernetes\kustomize\environments\production
kubectl delete ns exampleOverview (`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
# 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
\qRestarting 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
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
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 noticePersist data Kubernetes
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 podsOverview (`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:
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.1SSL & 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
kind create cluster --name local --config kind.yamlManage 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:
kubectl apply -f portainer-agent-ce211-k8s-nodeport.yamlSee the agent:
kubectl -n portainer get podsSee the service with the endpoint it exposes:
kubectl -n portainer get svcNow 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:
kubectl -n portainer get podsHelm
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:
https://kubernetes.github.io/ingress-nginxGitOps
So from the Application menu, we can add an application from a git repository.
Let's add this repo:
https://github.com/marcel-dempers/docker-development-youtube-seriesWe 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/` |
| Access token URL | https://login.microsoftonline.com/` |
| Resource URL | https://login.microsoftonline.com/` |
| Redirect URL | https://localhost:9443/ |
| Logout URL | https://login.microsoftonline.com/` |
| 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
cd kubernetes/probes
kind create cluster --name demo --image kindest/node:v1.28.0Test the cluster:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
demo-control-plane Ready control-plane 59s v1.28.0Applications
Client app is used to act as a client that sends web requests :
kubectl apply -f client.yamlThe server app is the app that will receive web requests:
kubectl apply -f server.yamlTest making web requests constantly:
while true; do curl http://server; sleep 1s; doneBump 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:
readinessProbe:
httpGet:
path: /
port: 5000
initialDelaySeconds: 3
periodSeconds: 3
failureThreshold: 3Automatic 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
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
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
failureThresholdand some failure will be expected before recovery - Do not set this
failureThresholdtoo 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:
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:
livenessProbe:
httpGet:
path: /
port: 5000
initialDelaySeconds: 3
periodSeconds: 4
failureThreshold: 8Scale 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 machinesBare 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
# 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:
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 20GBNew-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
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
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:
sudo apt update
sudo apt install -y nano net-tools openssh-server
sudo systemctl enable ssh
sudo ufw allow ssh
sudo systemctl start sshRecord the IP address of each VM so we can SSH to it:
sudo ifconfig
# record eth0
linux-0 IP=192.168.0.16
linux-1 IP=192.168.0.17
linux-2 IP=192.168.0.18In new Powershell windows, let's SSH to our VMs
ssh [email protected]
ssh [email protected]
ssh [email protected]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:
curl -sSL https://get.docker.com/ | sh
sudo usermod -aG docker $(whoami)
sudo service docker startRunning 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:
cd kubernetes/rancher
mkdir volumeRun Rancher
docker run -d --name rancher-server -v ${PWD}/volume:/var/lib/rancher --restart=unless-stopped -p 80:80 -p 443:443 --privileged rancher/rancherUnlock Rancher
Once its up and running we can extract the Rancher initial boostrap password from the logs
docker logs rancher-server > rancher.logGet 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:
ipconfigWe 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:
$ENV:KUBECONFIG="<path-to-kubeconfig>"Deploy 2 pods, and a service:
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.yamlOne 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
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:80We can access our example-app on port 81
Overview (`kubernetes/rbac/README.md`)
Introduction to Kubernetes: RBAC
Create Kubernetes cluster
kind create cluster --name rbac --image kindest/node:v1.20.2Kubernetes 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:
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 containerCopy the certs out of our master node:
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.keyKubernetes 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:
docker run -it -v ${PWD}:/work -w /work -v ${HOME}:/root/ --net host alpine sh
apk add opensslLet's create a certificate for Bob Smith:
#start with a private key
openssl genrsa -out bob.key 2048Now 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
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
openssl x509 -req -in bob.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out bob.crt -days 1Building a kube config
Let's install kubectl in our container to make things easier:
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/kubectlWe'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
export KUBECONFIG=~/.kube/new-configCreate a cluster entry which points to the cluster and contains the details of the CA certificate:
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-configkubectl 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
cd kubernetes/rbac
kubectl create ns shopping
kubectl -n shopping apply -f .\role.yaml
kubectl -n shopping apply -f .\rolebinding.yamlTest 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
kubectl -n shopping apply -f serviceaccount.yamlNow we can deploy a pod that uses the service account
kubectl -n shopping apply -f pod.yamlNow we can test the access from within that pod by trying to list pods:
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 accessNow we can allow this pod to list pods in the shopping namespace
kubectl -n shopping apply -f serviceaccount-role.yaml
kubectl -n shopping apply -f serviceaccount-rolebinding.yamlIf 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
kind create cluster --name demo --image kindest/node:v1.33.1Checkout our cluster
kubectl get nodes
NAME STATUS ROLES AGE VERSION
demo-control-plane Ready control-plane 19m v1.33.1For this demo, we'll want to deploy a ConfigMap and/or a Secret and consume these using a Deployment
Deploy a ConfigMap
kubectl apply -f kubernetes/configmaps/configmap.yamlDeploy a Secret
kubectl apply -f kubernetes/secrets/secret.yamlInstall Reloader
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.yamlUsage
Deploy an application that consumes the 'ConfigMap' and 'Secret' with auto reload enabled:
kubectl apply -f kubernetes/reloader/deployment.yamlWe 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
kind create cluster --name secrets --image kindest/node:v1.31.1Our Secret
We have a secret under kubernetes/secrets/secret.json
cat kubernetes/secrets/secret.jsonUsing our secret in a container
As a file:
docker run -it -v $PWD/kubernetes/secrets/secret.json:/secrets/secret.json ubuntu:latest bash
cat /secrets/secret.jsonAs environment variables:
api_key="somesecretgoeshere"
docker run -it -e API_KEY=$api_key ubuntu:latest bash
echo $API_KEYKubernetes 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:
kubectl create secret generic mysecret --from-file kubernetes/secrets/secret.jsonkubectl apply -f or kubectl create -f allows us to define things declaratively using YAML files:
kubectl apply -f kubernetes/secrets/secret.yamlUse 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
kubectl apply -f kubernetes/secrets/pod.yamlOverview (`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
# 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.5Deploy kube-prometheus
Installation:
kubectl create -f ./monitoring/prometheus/kubernetes/1.23/manifests/setup/
kubectl create -f ./monitoring/prometheus/kubernetes/1.23/manifests/Check the install:
kubectl -n monitoring get podsAfter a few minutes, everything should be up and running:
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 4m6sView dashboards
kubectl -n monitoring port-forward svc/grafana 3000Then access Grafana on localhost:3000
Access Prometheus
kubectl -n monitoring port-forward svc/prometheus-operated 9090Then access Prometheus on localhost:9090.
Create our own Prometheus
kubectl apply -n monitoring -f ./kubernetes/servicemonitors/prometheus.yamlView our prometheus prometheus-applications-0 instance:
kubectl -n monitoring get podsCheckout our prometheus UI
kubectl -n monitoring port-forward prometheus-applications-0 9090Deploy a service monitor for example app
kubectl -n default apply -f ./kubernetes/servicemonitors/servicemonitor.yamlAfter 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
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
kind create cluster --name services --image kindest/node:v1.31.1Test the cluster
kubectl get nodes
NAME STATUS ROLES AGE VERSION
services-control-plane Ready control-plane 5m48s v1.31.1Deploy 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.
kubectl apply -f kubernetes/deployments/deployment.yamlDeploy a service
We can now deploy a Kubernetes service that targets our deployment:
kubectl apply -f kubernetes/services/service.yamlOverview (`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 localminikube cluster.
Minikube
I will start with a local minikube cluster to get Shipa running:
# 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.2Minikube Traffic Routes
# 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
docker run -it --rm -v ${PWD}:/work -w /work alpine sh
apk add --no-cache curl unzip
cd kubernetes/shipa/
mkdir installs && cd installsInstall Kubectl
curl -LO https://storage.googleapis.com/kubernetes-release/release/v1.19.4/bin/windows/amd64/kubectl.exeInstall Helm CLI
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.zipInstall Shipa CLI
curl -LO https://storage.googleapis.com/shipa-client/v1.1/shipa_windows_amd64.exe
mv shipa_windows_amd64.exe shipa.exeAdd 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:
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
exitInstall Shipa
Let's add Shipa to our minikube cluster:
/* Detailed source-code truncated for AI context efficiency. */Targets
shipa target-add dev 10.100.10.10
shipa target-list
shipa target-set dev
shipa login
#username: [email protected]
#password: shipa2020Pools
https://learn.shipa.io/docs/pool-management
shipa pool-add dev --public --kube-namespace dev-team --provisioner kubernetes
shipa pool-listTeams
shipa team-create dev-team
shipa pool-constraint-set theonepool team dev-team --appendClusters
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)
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-listKubernetes 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 | ||
| 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:
# 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 --appendMultiCloud - Add Shipa to Amazon EKS
# 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 -dOn the dashboard, we add the cluster using the above information.
We can obtain the EKS address in the Amazon console.
Deploying Apps to MultiCloud
# 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-listhttps://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-listOverview (`kubernetes/velero/README.md`)
Introduction to Velero
We need a Kubernetes cluster
Lets create a Kubernetes cluster to play with using kind
kind create cluster --name velero --image kindest/node:v1.19.1Get a container to work in
Run a small alpine linux container where we can install and play with velero:
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.4Velero 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
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 --helpDeploy some stuff
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 allCreate 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
Deploy Velero for Azure
Start here
# 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 veleroDeploy Velero for AWS
Start here
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 veleroCreate a Backup
velero backup create default-namespace-backup --include-namespaces default
# describe
velero backup describe default-namespace-backup
# logs
velero backup logs default-namespace-backupDo a Restore
# 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 allOverview (`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
cd .\messaging\kafka\
docker build . -t aimvector/kafka:2.7.0Exploring the Kafka Install
We can then run it to explore the contents:
docker run --rm --name kafka -it aimvector/kafka:2.7.0 bash
ls -l /kafka/bin/
cat /kafka/config/server.propertiesWe can use the docker cp command to copy the file out of our container:
docker cp kafka:/kafka/config/server.properties ./server.properties
docker cp kafka:/kafka/config/zookeeper.properties ./zookeeper.propertiesNote: 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.
cd ./zookeeper
docker build . -t aimvector/zookeeper:2.7.0
cd ..Let's create a kafka network and run 1 zookeeper instance
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-1Kafka - 1
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-1Kafka - 2
docker run -d `
--rm `
--name kafka-2 `
--net kafka `
-v ${PWD}/config/kafka-2/server.properties:/kafka/config/server.properties `
aimvector/kafka:2.7.0Kafka - 3
docker run -d `
--rm `
--name kafka-3 `
--net kafka `
-v ${PWD}/config/kafka-3/server.properties:/kafka/config/server.properties `
aimvector/kafka:2.7.0Topic
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:
docker exec -it zookeeper-1 bashCreate the Topic:
/kafka/bin/kafka-topics.sh \
--create \
--zookeeper zookeeper-1:2181 \
--replication-factor 1 \
--partitions 3 \
--topic OrdersDescribe our Topic:
/kafka/bin/kafka-topics.sh \
--describe \
--topic Orders \
--zookeeper zookeeper-1:2181Simple 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:
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-beginningWith a consumer in place, we can start producing messages
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/nullOnce we have a message in Kafka, we can explore where it got stored in which partition:
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-checkpointBy 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 :
cat /tmp/kafka-logs/Orders-2/*.logDocker 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 :
version: "3.8"
services:Zookeeper
zookeeper-1:
container_name: zookeeper-1
image: aimvector/zookeeper:2.7.0
build:
context: ./zookeeper
volumes:
- ./config/zookeeper-1/zookeeper.properties:/kafka/config/zookeeper.propertiesKafka-1 to 3
We run 3 kafka instances.
Changing the service name, container name and config mount folder:
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
kafka-producer:
container_name: kafka-producer
image: aimvector/kafka:2.7.0
build:
context: .
working_dir: /kafka
entrypoint: /bin/bash
stdin_open: true
tty: trueConsumer
kafka-consumer:
container_name: kafka-consumer
image: aimvector/kafka:2.7.0
build:
context: .
working_dir: /kafka
entrypoint: /bin/bash
stdin_open: true
tty: trueStart the containers
cd messaging\kafka
docker compose build
docker compose upOverview (`messaging/rabbitmq/readme.md`)
RabbitMQ
Docker image over here
# 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-1Management
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_statusEnable Statistics
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_managementMessage Publisher
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.0Message Consumer
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.0Clustering
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
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_statusAutomated Clustering
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_federationBasic Queue Mirroring
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 queuesAutomatic Synchronization
https://www.rabbitmq.com/ha.html#unsynchronised-mirrors
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 queuesFurther Reading
https://www.rabbitmq.com/ha.html
Clean up
docker rm -f rabbit-1
docker rm -f rabbit-2
docker rm -f rabbit-3RabbitMQ on Kubernetes
Checkout the Kubernetes walkthrough here
Overview (`monitoring/fluentbit/README.md`)
Introduction to FluentBit
We always start with the official documentation
Running fluentbit
# 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 -hConfigure 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
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.yamlInputs
The example config file has a random input.
Let's create two inputs, one reading log files and another accepting HTTP inputs
pipeline:
inputs:
- name: tail
path: /var/lib/docker/containers/*/*.log
multiline.parser: docker
- name: http
listen: 0.0.0.0
port: 8888We'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
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.yamlTest our HTTP input plugin using curl
curl -d '{"namespace":"product","message":"hello from product"}' -X POST -H "content-type: application/json" http://localhost:8888/product.logOutputs
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:
- name: file
match: '*'
template: '{log}'
path: /fluent-bit/.dataLet's add a volume mount so we can see the output data.
Let's mount a local .data folder to /fluent-bit/.data
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.yamlConcepts
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
filters:
- name: grep
match: '*'
regex: namespace productThis plugin excludes all requests from "search" namespace
filters:
- name: grep
match: '*'
exclude: namespace searchTest our HTTP input plugin using curl to send different events to our fluent-bit service:
#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.logOverview (`monitoring/logging/readme.md`)
Logging
Logging Basics
- Standardised Logging
- Centralised Logging
Introduction to Fluentd
- What is fluentd
- Configuration
- Plugins
- Demos
Introduction to OpenTelemetry
- What is OpenTelemetry
- Configuration
- Demos
Overview (`monitoring/opentelemetry/README.md`)
OpenTelemetry
Logs
Tracing
Metrics
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
docker compose up -d prometheus
docker compose up -d grafana
docker compose up -d grafana-dashboardsStart the example app you prefer
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-applicationGenerate some requests by opening the application in the browser
http://localhost:80 #Golang
http://localhost:81 #Python
http://localhost:82 #Dotnet
http://localhost:83 #NodeJSCheck Dashboards
http://localhost:3000Prometheus Queries
Golang Examples
Requests per Second over 2minutes
irate(go_request_operations_total[2m])Request duration
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:
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
mkdir src- Create basic source code
In the src folder, create a program called app.py
Paste the following content into it.
def main():
print("Hello, world.")
main()- Run your application code
You can run your application
/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:
# 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:
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:
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
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 :
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!
def getCustomers():
customers = [ "Marcel Dempers", "Bob Smith" ]
return customers
customers = getCustomers()
print(customers)You can get array values by index starting at 0:
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 :
customers[2] = "John Smith"
IndexError: list assignment index out of rangeInstead we can use the append() function of the array to add items.
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 customersWe can remove items with the remove() function.
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
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
#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
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:
i = 0
while i < 7:
print(customers[i])
i += 1We can also use this condition to loop arrays:
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 += 1For loops
For looping a dictionary of list, for loops are a little easier:
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
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.
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:
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 customersThen we can proceed to get our customer and access their fields:
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:
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
docker build . -t customer-appRunning the Container
docker run customer-appOverview (`storage/redis/readme.md`)
Redis
Docker
Docker image over here
Running redis
docker network create redis
docker run -it --rm --name redis --net redis -p 6379:6379 redis:6.0-alpineConfiguration
Redis configuration documentation here
Starting Redis with a custom config
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.confSecurity
Redis should not be exposed to public.
Always use a strong password in redis.conf
requirepass SuperSecretSecureStrongPassPersistence
Redis Persistence Documentation here
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.confRedis client application
An example application that reads a key from Redis, increments it and writes it back to Redis.
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.0Run our application
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.0Redis 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.
Source Code
git clone --depth 1 https://github.com/openclaw/openclaw.git /tmp/openclawDocker
View docker setup scripts:
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.shAfter cloning the code, we can run setup according to the docs.
Use a pre-built image to avoid having to build it ourselves:
export OPENCLAW_IMAGE="ghcr.io/openclaw/openclaw:latest"
./scripts/docker/setup.shBuild our own custom dockerfile:
docker build . -t aimvector/openclawLeast 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 :
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:
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/openclawConfigration
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:
openclaw onboard --classicKubernetes
View docker setup scripts:
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 filesLets create a Kubernetes cluster to play with using kind
kind create cluster --name openclaw --image kindest/node:v1.36.1Test our cluster and makes sure kubectl is configured for it:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
openclaw-control-plane Ready control-plane 2m30s v1.36.1View kubernetes deployment scripts:
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 filesI've taken a copy of these manifests by using cp.
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.
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.
- name: DISCORD_BOT_TOKEN
valueFrom:
secretKeyRef:
name: openclaw-secrets
key: DISCORD_BOT_TOKEN
optional: falseCreate the secret:
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
kubectl -n openclaw apply -f ai/agents/openclaw/kubernetes/Install Plugins
export NPM_CONFIG_CACHE=~/.openclaw/.npm
openclaw plugins install @openclaw/discord
kubectl -n openclaw delete pods --allOverview (`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
kind create cluster --name llmkube --image kindest/node:v1.34.3Test our cluster and makes sure kubectl is configured for it:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
llmkube-control-plane Ready control-plane 40s v1.34.3Installing LLMKube with Helm
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-namespaceCheck installation:
kubectl get pods -n llmkube-systemTwo new CRDs Model and InferenceService are now available in the cluster:
kubectl get crds | grep llmkube
inferenceservices.inference.llmkube.dev
models.inference.llmkube.devThe CRDs
Model
# Deploy the Model
kubectl apply -f ai/kubernetes/llm-kube/model.yaml
# Check the Model
kubectl get model
kubectl describe modelInferenceService
# Deploy the InferenceService
kubectl apply -f ai/kubernetes/llm-kube/inference.yaml
# Check the InferenceService
kubectl get inferenceservices
kubectl describe inferenceservicesTesting the Inference API
Port-forward the service to reach it from your machine:
kubectl port-forward svc/gemma-e2b-service 8080:8080Send a test request to the OpenAI-compatible endpoint:
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
{
"$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 toolsfull: includes light + tools for model conversion (safetensors → GGUF)server: only thellama-serverexecutable
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
docker run -it \
-v ~/models:/models \
--device /dev/dri \
-p 8080:8080 \
--entrypoint bash ghcr.io/ggml-org/llama.cpp:full-intelExample 2:
WSL with NVIDIA GPU:
docker run -it \
-v ~/models:/models \
--gpus all \
-p 8080:8080 \
--entrypoint bash ghcr.io/ggml-org/llama.cpp:full-cudaOnce 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:
./llama-cli -hf ggml-org/gemma-4-E4B-it-GGUF:Q4_K_MNote: 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
./llama-cli -m /models/gemma-4-E4B-it-Q4_K_M.gguf -ngl 99Use -ngl 0 to disable GPU offload and run on CPU only:
./llama-cli -m /models/gemma-4-E4B-it-Q4_K_M.gguf -ngl 0Hosting 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):
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 64WSL with NVIDIA GPU:
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 64Key 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:
{
"$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
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:
responseMessage = response.choices[0].message.contentLet's build our app
cd ai\openai\introduction
docker build . -t ai-appSet my OpenAI API key
$ENV:OPENAI_API_KEY=""Run our AI App:
docker run -it -e OPENAI_API_KEY=$ENV:OPENAI_API_KEY ai-appWhen 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:
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
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-123This command will retrieve the logs for the specified pod in the given namespace. Make sure you have the necessary permissions to access the namespace.
---
### 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.0Octopus 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:
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:
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-latestCreate our first Octopus server:
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.3Access Octopus Dashboard
Let's get the IP of my machine so we can access Octopus:
ifconfig eth0 | grep 'inet' | awk '{print $2}' | cut -d: -f2We 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:
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
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:
https://github.com/marcel-dempers/docker-development-youtube-series.gitWe also specify all our manifests path that octopus needs to deploy:
automation/cicd/octopus-deploy/example-application/deployment.yaml
automation/cicd/octopus-deploy/example-application/service.yaml
automation/cicd/octopus-deploy/example-application/ingress.yamlVariable 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
spec:
ingressClassName: nginx
rules:
- host: marcel.localCreate 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_nameof the ConfigMap to createconfig_namespacename of the Namespace to deploy the ConfigMap toconfig_bodythe contents of the ConfigMap as Multi-line text
Inline Source Code:
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_nameof the Secret to createsecret_namespacename of the Namespace to deploy the Secret tosecret_bodythe contents of the Secret as Multi-line text
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:
🎬 [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
🎬 [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:
🎬 [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:
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:
🎬 [Module 1: Introduction Operating Systems for DevOps](../../content/operating-systems/introduction/README.md)
In this module
<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>🎬 [Module 2: Introduction to Servers & Virtualisation](../../content/operating-systems/virtualization/README.md)
In this module
🎬 [Module 3: Introduction to Linux for beginners](../../content/operating-systems/linux/introduction/README.md)
In this module
🎬 [Module 4: Understanding the command line](../../content/operating-systems/commandline/introduction/README.md)
In this module
🎬 [Module 5: Bash scripting for beginners](../../content/operating-systems/scripting/bash/README.md)
In this module
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
🎬 [Module 2: Linux CPU Monitoring](../../content/operating-systems//linux/monitoring/cpu/README.md)
In this module
🎬 [Module 3: Linux Memory Monitoring](../../content/operating-systems//linux/monitoring/memory/README.md)
In this module
🎬 [Module 4: Linux Disk Usage Monitoring](../../content/operating-systems//linux/monitoring/disk/README.md)
In this module
🎬 [Module 5: Linux Network Monitoring](../../content/operating-systems//linux/monitoring/network/README.md)
In this module
⌛ Module 6: Introduction to Prometheus
In this module
⌛ Module 7: Introduction to Grafana
In this module
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
<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
🎬 [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
🎬 [Module 4: Creating web servers with NGINX](../../content/web/servers/README.md)
In this module
🎬[Module 5: HTTP Troubleshooting](../../content/web/http/troubleshooting/README.md)
In this module
🚧🎬 [Module 6: The Basics of DNS & Domains](../../content/web/dns/README.md)
In this module
🚧🎬 [Module 7: The Basics of SSL & TLS](../../content/web/tls/README.md)
In this module
⏳(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
⏳(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
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
🎬 [Module 2: Introduction to Automation](../../content/automation/introduction/README.md)
In this module
<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
🎬 [Module 4: CD Example: Creating a CD pipeline with Github Actions](../../content/automation/cicd/cd/examples/github/README.md)
In this module
🎬 [Module 5: Scripting Example: Automate our Infrastructure ](../../content/automation/scripting/examples/README.md)
In this module
🎬 [Module 6: Declarative Tools Example: Automate our Infrastructure (Vagrant)](../../content/automation/config-management/examples/vagrant/README.md)
In this module
🎬 [Module 7: Configuration Management Example: Automate our Infrastructure (Ansible)](../../content/automation/config-management/examples/ansible/README.md)
In this module
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
🎬 [Module 2: Running and Configuring Containers](../../content/containers/running/README.md)
In this module
🎬 [Module 3: Building Container Images](../../content/containers/building/README.md)
In this module
🎬 [Module 4: CI/CD Pipelines for Containers](../../content/containers/cicd/README.md)
In this module
🎬 [Module 5: Container Networking](../../content/containers/networking/README.md)
In this module
🎬 [Module 6: Introduction to Container Orchestration](../../content/containers/orchestration/README.md)
In this module
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:
curl https://marceldempers.devWe 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:
curl -v https://marceldempers.devWe can construct a HTTP request ourselves by passing some of the fields we talked about
curl -X GET \
-H "Host: marceldempers.dev" \
https://marceldempers.devTroubleshooting HTTP requests and responses
- Command Line
We can use curl alongside -v to troubleshoot HTTP requests responses using the command line.
- 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.
- 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
nslookup google.comDNS 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:
./serverWeb 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:
curl -X GET http://localhost:8080/home.html
curl -X GET http://localhost:8080/home-notexist.htmlNow in the terminal where we started the web server, we can see our logs being output to stdout.
Let's analyze the HTTP logs:
./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 404We 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:
./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 404Notice 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
kind create cluster --name githubactions --image kindest/node:v1.28.0@sha256:b7a4cad12c197af3ba43202d3efe03246b3f0793f162afb40a33c923952d5b31Let's test our cluster:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
githubactions-control-plane Ready control-plane 2m53s v1.28.0Running 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 adockerfileand follow instructions to Install Docker.
I would grab the content and create statements for mydockerfile
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:
docker build . -t github-runner:latest
docker run -it github-runner bashNext steps:
- Now we can see
dockeris 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
docker run -it -e GITHUB_PERSONAL_TOKEN="" -e GITHUB_OWNER=marcel-dempers -e GITHUB_REPOSITORY=docker-development-youtube-series github-runnerDeploy to Kubernetes
Load our github runner image so we dont need to push it to a registry:
kind load docker-image github-runner:latest --name githubactionsCreate a kubernetes secret with our github details
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.yamlOverview (`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.
cd golang\introduction\part-2.json
docker build --target dev . -t go
docker run -it -v ${PWD}:/work go sh
go versionCreate our App
Create a new directory that holds defines our repository and holds our module
mkdir videos- Define a module path
# change directory to your application source code
cd videos
# create a go module file
go mod init videosCreate 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
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.
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:
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:
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.
import (
"io/ioutil"
)Let's move our videos into a json file called videos.json :
[
{
"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:
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/
"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
err = json.Unmarshal(fileBytes, &videos)
if err != nil {
panic(err)
}
return videosUsing 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!
for _, video := range videos {
}Now we dont want to override the description, we want to inject the terms and conditions on a new line
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.
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
func saveVideos(videos []video)(){
videoBytes, err := json.Marshal(videos)
if err != nil {
panic(err)
}
}Writing to File
https://golang.org/pkg/io/ioutil/
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:
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:
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:
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.
cd golang\introduction\part-3.http
docker build --target dev . -t go
docker run -it -v ${PWD}:/work go sh
go versionCreate our App
Create a new directory that holds defines our repository and holds our module
mkdir videos- Define a module path
# change directory to your application source code
cd videos
# create a go module file
go mod init videosCreate 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
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:
package mainWe import 2 packages, 1 for reading and writing files, and another for dealing with json
import (
"io/ioutil"
"encoding/json"
)Then we define what a video struct looks like:
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 :
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
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 :
# 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 DefaultServeMuxNow 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
docker run -it -p 8080:8080 -v ${PWD}:/work go sh
cd videos
go run main.go
# you will notice the application pausingWe 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:
func Hello(){
}And tell our http service to run it:
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:
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:
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
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:
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.
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:
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:
"encoding/json"Convert our videos to JSON :
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 :
http.HandleFunc("/update", HandleUpdateVideos)And we need to define an HandleUpdateVideos() function:
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
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/
import "io/ioutil"Then we can read the body into a slice of bytes:
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.
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.
w.WriteHeader(400)
fmt.Fprintf(w, "Bad request")And Finally, let's update our videos file! :
saveVideos(videos)Build our Docker container
Let's uncomment all the build lines in the dockerfile
Full dockerfile :
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 ./videosBuild :
cd golang\introduction\part-3.http
docker build . -t videosRun :
docker run -it -p 8080:8080 videosThings 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:
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.
cd golang\introduction\part-4.commandline
docker build --target dev . -t go
docker run -it -v ${PWD}:/work go sh
go versionCreate our App
Create a new directory that holds defines our repository and holds our module
mkdir videos
cd videos- Define a module path
# create a go module file
go mod init videosCreate 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
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:
package mainWe import 2 packages, 1 for reading and writing files, and another for dealing with json
import (
"io/ioutil"
"encoding/json"
)Then we define what a video struct looks like:
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 :
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
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
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
# 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 -descTo start, we import package flag
import (
"flag"
"fmt"
)Let's define our subcommands in main.go :
//'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
// 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.
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:
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 simpleswitch statement that can branch into different pathways of execution,
based on a variables content.
Let's take a look at this simple switch statement:
//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:
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:
getCmd.Parse(os.Args[2:])Input Validation
For our HandleGet function, let's validate input to ensure its correct.
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:
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
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():
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:
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:
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:
video := video{
Id: *id,
Title: *title,
Description: *description,
Imageurl: *imageUrl,
Url: *url,
}Get the existing videos:
videos := getVideos()Append our video to the list:
videos = append(videos,video)Save the new updated video list:
saveVideos(videos)Build our Docker container
Let's uncomment all the build lines in the dockerfile
Full dockerfile :
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
#!/bin/sh
videos $@Build :
cd golang\introduction\part-4.commandline
docker build . -t videosRun :
docker run -it videos get --helpOverview (`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:
FROM golang:1.15-alpine as dev
WORKDIR /workLet's build and start our container:
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 versionOur application
Create a new directory that holds defines our repository and holds our module
mkdir videos- Define a module path
# change directory to your application source code
cd videos
# create a go module file
go mod init videosCreate 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
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
package main
import (
"io/ioutil"
"encoding/json"
)Then we define what a video struct looks like:
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 :
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
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,
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
go build
./videosIf 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/
go get github.com/go-redis/redis/v8And to use the library, we have to import it
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
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.
import (
"context"
)
var ctx = context.Background()
var redisClient *redis.ClientRedis Sentinel Client
https://redis.uptrace.dev/guide/sentinel.html#redis-server-client
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
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 shWe 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.
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.
func saveVideos(videos []video)(){
for _, video := range videos {
saveVideo(video)
}
}To get the videos, let's create a function to retrieve a single record:
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
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
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
[
{
"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
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
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 :
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 ./videosBuild :
cd golang\introduction\part-5.database.redis
docker build . -t videosRun :
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" `
videosOverview (`kubernetes/admissioncontrollers/introduction/README.md`)
Introduction to Admission controllers
Installation (local)
Create a kind cluster:
kind create cluster --name webhook --image kindest/node:v1.20.2TLS 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.
FROM golang:1.15-alpine as dev-env
WORKDIR /appBuild and run the controller
# get dev environment: webhook
cd sourcecode
docker build . -t webhook
docker run -it --rm -p 80:80 -v ${PWD}:/app webhook shWe always start with Hello world!
Let's define our basic main module and a web server
go mod init example-webhookNew file : main.go
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
export CGO_ENABLED=0
go build -o webhook
./webhookWe'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
docker run -it --rm --net host -v ${HOME}/.kube/:/root/.kube/ -v ${PWD}:/app webhook shWe can also test our access to our kubernetes cluster with the config that is mounted in:
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/kubectlKubernetes
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:
"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.
# 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:
cs, err := kubernetes.NewForConfig(config)
if err != nil {
panic(err.Error())
}
clientSet = csAnd we'll need to import the dependencies for this:
"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
go get k8s.io/[email protected]Rebuild to ensure no errors:
go build -o webhookTest with a kubeconfig
export USE_KUBECONFIG=true
./webhookTo test our access, let's create a test.go and return pods from the kube-system namespace
#test.go
package main
import ()
func test(){
}Use our global clientset defined in main() and get all pods
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:
"context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"fmt"And finally invoking it in main() calling test()
Run and test our Kubernetes access:
bash-5.0# ./webhook
kubeconfig: /root/.kube/config
There are 11 pods in the clusterMutating 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:
"flag"
"strconv"Define our parameters for cert configuration
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(¶meters.port, "port", 8443, "Webhook server port.")
flag.StringVar(¶meters.certFile, "tlsCertFile", "/etc/webhook/certs/tls.crt", "File containing the x509 Certificate for HTTPS.")
flag.StringVar(¶meters.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
# 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 :
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:
docker build . -t aimvector/example-webhook:v1
docker push aimvector/example-webhook:v1# 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.yamlDeploy a demo that needs mutation
kubectl -n default apply -f ./demo-pod.yamlWe 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:
kubectl cp example-webhook-756bcb566b-9kxjp:/tmp/request ./mock-request.jsonSo lets grab the info from the admission request, so we can do something with it
//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
//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.
//global
type patchOperation struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value,omitempty"`
}
//HandleMutate()
var patches []patchOperationAdd 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
// 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:
patchBytes, err := json.Marshal(patches)
if err != nil {
fmt.Errorf("could not marshal JSON patch: %v", err)
}Add it to the admission response
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
docker build . -t aimvector/example-webhook:v1
docker push aimvector/example-webhook:v1Delete all pods to get latest image
kubectl delete pods --allRedeploy our demo pod and see the mutations
kubectl -n default apply -f ./demo-pod.yamlSee the injected label
kubectl get pods --show-labelsOverview (`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
kind create cluster --name vpa --image kindest/node:v1.30.4Metric 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 :
- --kubelet-insecure-tlsDeployment:
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 nodesVPA
VPA docs here
Let's install the VPA from a container that can access our cluster
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 podsBuild and deploy example app
# 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 podsGenerate some traffic
Let's deploy a simple traffic generator pod
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-cpuDeploy an example VPA
kubectl apply -f .\vertical-pod-autoscaling\vpa.yaml
kubectl describe vpa application-cpuDeploy Goldilocks
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 80Overview (`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:
helm show chart oci://cr.agentgateway.dev/charts/agentgateway-crds
helm show chart oci://cr.agentgateway.dev/charts/agentgatewayCHART_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 \
--waitCheck our installation
# check pods are running
kubectl -n agentgateway-system get pods
# check logs
kubectl -n agentgateway-system logs -l app.kubernetes.io/name=agentgatewayCheck the AgentGateway Class
Verify the GatewayClass was registered:
kubectl get gatewayclassInstall an AgentGateway Gateway
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:80Features
- Route to LLM Models
- Route to MCP Servers
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:
export GEMINI_API_KEY=""
kubectl apply -f- <<EOF
apiVersion: v1
kind: Secret
metadata:
name: gemini-secret
type: Opaque
stringData:
Authorization: $GEMINI_API_KEY
EOFCreate the LLM backend:
Instead of an HTTPRoute routing to an upstream backend service, it uses an AgentgatewayBackend
kubectl apply -f kubernetes/gateway-api/agentgateway/llm/gemini/02-backend-gemini.yamlRoute traffic from the Gateway to the backend using a standard HTTPRoute:
kubectl apply -f kubernetes/gateway-api/agentgateway/llm/03-httproute.yamlTest our HTTPRoute:
curl "localhost:8080/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role": "user",
"content": "What is Kubernetes in one sentence?"}]
}' | jqWe can route by path as well. This means one domain and route to provider by path
kubectl apply -f kubernetes/gateway-api/agentgateway/llm/04-httproute-gemini.yamlcurl "localhost:8080/ai/gemini/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role": "user",
"content": "What is Kubernetes in one sentence?"}]
}' | jqCreate an LLM Backend: Anthropic
The same pattern works for any supported provider.
export ANTHROPIC_API_KEY=""
kubectl apply -f- <<EOF
apiVersion: v1
kind: Secret
metadata:
name: anthropic-secret
type: Opaque
stringData:
Authorization: $ANTHROPIC_API_KEY
EOFkubectl apply -f kubernetes/gateway-api/agentgateway/llm/anthropic/05-backend-anthropic.yamlkubectl apply -f kubernetes/gateway-api/agentgateway/llm/06-httproute-anthropic.yamlRoute to Anthropic Claude:
curl "localhost:8080/ai/claude/v1/chat/completions" \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role": "user",
"content": "What is Kubernetes in one sentence?"}]
}' | jqNow we have implemented LLM routing by path using HTTPRoute:
localhost:8080/ai/gemini --> gemini flashlocalhost: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.
kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/kubernetes/02-mcp.yaml
# check mcp server
kubectl get pods
kubectl get sa
kubectl get serviceThis 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:
kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/kubernetes/03-mcp-backend.yaml
# check the backend
kubectl get agentgatewaybackend
kubectl describe agentgatewaybackendRoute traffic from the Gateway to the backend using a standard HTTPRoute:
kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/04-httproute.yamlTest our MCP Backend: Kubernetes
Initialize session to get a mcp-session-id from the response:
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:
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
kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/github/05-mcp.yaml
# check mcp server
kubectl get pods
kubectl get sa
kubectl get serviceThen we create a backend to route to:
kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/github/06-mcp-backend.yaml
# check the backend
kubectl get agentgatewaybackend
kubectl describe agentgatewaybackendRoute traffic from the Gateway to the backend using a standard HTTPRoute:
kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/07-httproute.yamlTest our MCP Backend: Github
Initialize session to get a mcp-session-id from the response:
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:
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
kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/08-federatedbackend.yamlDeploy an HTTPRoute to our single federated endpoint:
kubectl apply -f kubernetes/gateway-api/agentgateway/mcp/09-httproute.yamlGet session ID:
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
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
kind create cluster --name gatewayapi --image kindest/node:v1.34.0 --config kubernetes/gateway-api/cilium/kind.yamlTest our cluster and makes sure kubectl is configured for it:
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.0The 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:
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.yamlCheck our installation
# 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.0We can also check the Cilium Operator logs to ensure we are smooth sailing:
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-agentIn 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
kubectl apply -f kubernetes/gateway-api/cilium/01-gatewayclass.yamlThe 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
kubectl apply -f kubernetes/gateway-api/cilium/01.1-gatewayclass-config.yamlInstall a Cilium Gateway
kubectl apply -f kubernetes/gateway-api/cilium/02-gateway.yamlDemo 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):
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
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
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:
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-namespaceUpgrade:
helm upgrade envoy-gateway oci://docker.io/envoyproxy/gateway-helm \
--values kubernetes/gateway-api/envoy/values.yaml \
--version $CHART_VERSION \
--namespace envoy-gateway-systemConfiguration
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.
# 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-gatewayInstall an Envoy Gateway Class
kubectl apply -f kubernetes/gateway-api/envoy/01-gatewayclass.yamlInstall an Envoy Gateway
kubectl apply -f kubernetes/gateway-api/envoy/02-gateway.yamlWhen we apply the gateway, we get a new gateway api pod.
# 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 svcGateway 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:
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 80HTTP 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.
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/statusMutual 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
# 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/statusBackend Traffic Policies
Local and Global Rate limits
We can rate limit each HTTP route using Local Rate Limit:
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;
doneSecurity Policies
CORS
# port-forward to set pre-cors
kubectl port-forward svc/web-app 8000:80
kubectl apply -f kubernetes/gateway-api/envoy/11-securitypolicy-cors.yamlBasic Auth
#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.yamlAPI Auth
#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 443You can see Client authentication failed. in the browser.
We need to pass the header
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/statusLogs:
kubectl -n envoy-gateway-system logs -l app.kubernetes.io/managed-by=envoy-gatewayOverview (`kubernetes/gateway-api/haproxy/README.md`)
Introduction to HAProxy: Gateway API
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
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.
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.yamlCheck 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.
# 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-ingressInstall an HAProxy Gateway Class
Please note apiVersion: gateway.networking.k8s.io/v1alpha2 is used and v1 does not work. :
kubectl apply -f kubernetes/gateway-api/haproxy/01-gatewayclass.yamlInstall an HAProxy Gateway
kubectl apply -f kubernetes/gateway-api/haproxy/02-gateway.yamlTraffic Routing
At the time of this guide, HAProxy only supports TCPRoute, and not HTTPRoute like the other controllers we covered thus far.
kubectl apply -f kubernetes/gateway-api/haproxy/03-tcproute.yamlkubectl -n haproxy-controller port-forward svc/haproxy-kubernetes-ingress 80Overview (`kubernetes/gateway-api/istio/README.md`)
Introduction to Istio: Gateway API
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:
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 \
--waitConfiguration
helm show values istio/istiod \
--version $ISTIOD_CHART_VERSION > kubernetes/gateway-api/istio/default-values.yamlCheck 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.
# 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=istiodInstall an Istio Gateway Class
kubectl apply -f kubernetes/gateway-api/istio/01-gatewayclass.yamlInstall an Istio Gateway
kubectl apply -f kubernetes/gateway-api/istio/02-gateway.yamlWhen 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
# 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 svcGateway 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.
kubectl apply -f kubernetes/gateway-api/istio/01.1-gatewayclass-config.yamlFor 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:
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 80HTTP 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
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 80Enable 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.
kubectl label namespace default istio-injection=enabled
# delete all pods to get sidecars added
kubectl delete pods --allWe will also need the Gateway controller that will be managing our Istio Gateways.
Istio Gateway Ingress
Let's deploy the Istio Gateway
kubectl apply -f kubernetes/gateway-api/istio/08-istiogateway.yamlTraffic 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:
kubectl apply -f kubernetes/gateway-api/istio/09-virtualservice.yamlOverview (`kubernetes/gateway-api/kgateway/README.md`)
Introduction to kgateway: Gateway API
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:
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-namespaceCheck our installation
# check the pods
kubectl -n kgateway-system get pods
# check the logs
kubectl -n kgateway-system logs -l app.kubernetes.io/name=kgatewayInstall a kgateway Gateway Class
The helm chart will create a GatewayClass for us as per docs.
kubectl get gc kgateway -o yamlInstall a kgateway Gateway
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 80Configuration
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:
kubectl apply -f kubernetes/gateway-api/kgateway/02-gateway-config.yamlHTTP 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:
filters:
- type: ExtensionRef
extensionRef:
group: gateway.kgateway.dev
kind: TrafficPolicy
name: cors-featureTrafficPolicy Example
Example of a Traffic Policy supporting CORS
kubectl apply -f kubernetes/gateway-api/kgateway/03-trafficpolicy-cors.yamlPolicies: 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
kubectl apply -f kubernetes/gateway-api/kgateway/04-gateway-accesslogs.yaml
# see log format
kubectl logs -l app.kubernetes.io/name=gateway-apiPolicies: 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
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:
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
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.keyWe 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.
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-planeCheck 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.
# check the control plane pods
kubectl -n linkerd get pods
linkerd checkConfiguration
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
# 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.yamlWhen we apply the gateway, we get a new gateway api pod.
# 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 80Mesh our Services
To add our services to the Linkerd mesh, we need to annotate each deployment
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
linkerd viz install | kubectl apply -f -
kubectl -n linkerd-viz port-forward svc/web 8084HTTPRoutes: North \ South Traffic
For north\south traffic, we will use Gateway API and setup a HTTP route
kubectl apply -f kubernetes/gateway-api/linkerd/03-httproute.yamlHTTPRoutes - 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.
# 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.yamlYou 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
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:
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-gatewayNGINX 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:
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-namespaceUpgrade:
helm upgrade ngf oci://ghcr.io/nginx/charts/nginx-gateway-fabric \
--values kubernetes/gateway-api/nginx-fabric/values.yaml \
--version $CHART_VERSION \
--namespace nginx-gatewayCheck our installation
# check the pods
kubectl -n nginx-gateway get pods
# check the logs
kubectl -n nginx-gateway logs -l app.kubernetes.io/instance=ngfInstall 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:
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 80Check & 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:
kubectl apply -f kubernetes/gateway-api/nginx-fabric/02.1-gateway-config.yamlIn 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
# 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 -THTTP 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
kubectl apply -f kubernetes/gateway-api/nginx-fabric/08-clientsettings-keepalive.yaml
# check
kubectl describe clientsettingspolicies gateway-client-settingsMake more than 5 requests and notice every one re-using the connection except for the last:
# 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.comSnippetsFilter 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.
kubectl apply -f kubernetes/gateway-api/nginx-fabric/09-snippetsfilter.yaml
# check
kubectl describe SnippetsFilter custom-snippetOverview (`kubernetes/gateway-api/nginx-ingress/README.md`)
Ingress-NGINX Migration to Gateway API
Ingress Class
kubectl get ingress -A -o custom-columns="NAMESPACE:.metadata.namespace,NAME:.metadata.name,CLASS:.spec.ingressClassName"Ingress to Gateway Tool
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-nginxAnnotations Audit
To get a list of all NGINX Ingress Annotations we are using:
kubectl get ingress -A -o json | \
jq -r '.items[].metadata.annotations | keys[]' | \
grep "nginx.ingress" | \
sort -uNow we can generate our audit list by selecting the above annotated output:
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.
# 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 80Route by Domain
Before:
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: 5000This is what the HTTPRoute:Route by Domain looks like.
kubectl apply -f kubernetes/gateway-api/03-httproute-by-hostname.yamlTest:
curl example-app-python.com
Hello from Python!
curl example-app-go.com
Hello from GoMark as done
kubectl label ingress go-route python-route migrated=truePath Rewrite
Before:
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: ImplementationSpecificAfter:
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: 1Test:
curl example-app.com/api/python
Hello from Python!
curl example-app.com/api/go
Hello from GoMark as done
kubectl label ingress go-route-bypath python-route-bypath migrated=trueTLS Termination
Before:
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: 5000After:
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: 1We can use our port-forward on port 443 to test the TLS in the browser
Mark as done
kubectl label ingress go-route-tls python-route migrated=trueHTTP Redirects
Before:
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: 5000After:
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: 1Test:
# 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
kubectl label ingress python-route-tls migrated=trueBasic Auth (Custom Feature)
Create a Basic Auth secret:
apk add apache2-utils
htpasswd -c auth python
kubectl create secret generic python-route-auth-secret --from-file=authBefore:
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: 5000After:
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-secretTest:
#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 UnauthorizedMark as done
kubectl label ingress python-route-auth migrated=trueOverview (`kubernetes/gateway-api/traefik/README.md`)
Introduction to Traefik: Gateway API
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
kubectl apply -f kubernetes/gateway-api/traefik/01-gatewayclass.yamlInstall a Traefik Gateway
kubectl apply -f kubernetes/gateway-api/traefik/02-gateway.yamlLet'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:
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-namespaceUpgrade:
helm upgrade traefik traefik/traefik \
--version $CHART_VERSION \
--values kubernetes/gateway-api/traefik/values.yaml \
--namespace traefikCheck our installation
# 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 80HTTP 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:
filters:
- type: ExtensionRef
extensionRef:
group: traefik.io
kind: Middleware
name: add-prefixPrefix
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
kubectl apply -f kubernetes/gateway-api/traefik/08-middleware-addprefix.yamlWe can follow the log of our upstream to see the HTTP request:
kubectl logs -f -l app=go-appBasic 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.
kubectl apply -f kubernetes/gateway-api/traefik/09-middleware-basicauth.yamlHeaders
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.
kubectl port-forward svc/web-app 8000:80Once we apply the Middleware update to our HTTPRoute, we can perform cross origin API calls:
kubectl apply -f kubernetes/gateway-api/traefik/10-middleware-headers.yamlOverview (`kubernetes/secrets/sealed-secrets/README.md`)
Introduction to Sealed Secrets
Checkout the Sealed Secrets GitHub Repo
There are a number of use-cases where this is a really great concept.
GitOps - Storing your YAML manifests in Git and using GitOps tools to sync the manifests to your clusters (For example Flux and ArgoCD!)
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
kind create cluster --name sealedsecrets --image kindest/node:v1.23.5See cluster up and running:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
sealedsecrets-control-plane Ready control-plane,master 2m12s v1.23.5Run a container to work in
run Alpine Linux:
docker run -it --rm -v ${HOME}:/root/ -v ${PWD}:/work -w /work --net host alpine shinstall kubectl
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/kubectlinstall helm
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/helmtest cluster access:
/work # kubectl get nodes
NAME STATUS ROLES AGE VERSION
sealedsecrets-control-plane Ready control-plane,master 3m26s v1.23.5Install 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
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.yamlinstall using Helm
You can also install the controller using helm
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.yamlWith helm template we can explore the YAML and then replace the helm template with helm install
to install the chart
install using YAML manifest
kubectl apply -f kubernetes/secrets/sealed-secrets/controller-v0.19.1.yamlCheck the installation
The controller deploys to the kube-system namespace by default.
kubectl -n kube-system get podsCheck the logs of the sealed secret controller
kubectl -n kube-system logs -l name=sealed-secrets-controller --tail -1From the logs we can see that it writes the encryption key its going to use as a kubernetes secret
Example log:
2022/11/05 21:38:20 New key written to kube-system/sealed-secrets-keymwzn9Encryption keys
kubectl -n kube-system get secrets
kubectl -n kube-system get secret sealed-secrets-keygxlvg -o yamlDownload 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
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
cat kubernetes/secrets/secret.yamlIf 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 :
cat kubernetes/secrets/secret.yaml | kubeseal -o yaml > kubernetes/secrets/sealed-secrets/sealed-secret.yamlCreate a sealed secret using a YAML file:
kubeseal -f kubernetes/secrets/secret.yaml -o yaml > kubernetes/secrets/sealed-secrets/sealed-secret.yamlDeploy the sealed secret
kubectl apply -f kubernetes/secrets/sealed-secrets/sealed-secret.yamlNow few seconds later, see the secret
kubectl -n default get secret
NAME TYPE DATA AGE
mysecret Opaque 1 25sHow 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.
apk add nano
export KUBE_EDITOR=nanoSet the flag on the command like so to add a new key every 5 min for testing:
spec:
containers:
- command:
- controller
- --key-renew-period=5m
kubectl edit deployment/sealed-secrets-controller --namespace=kube-systemYou should see a new key created under secrets in the kube-system namespace
kubectl -n kube-system get secretsBackup your encryption keys
To get your keys out for backup purpose, it's as simple as grabbing a secret by label using kubectl :
kubectl get secret -n kube-system \
-l sealedsecrets.bitnami.com/sealed-secrets-key \
-o yaml \
> kubernetes/secrets/sealed-secrets/sealed-secret-keys.keyThis 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.
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 podsrestore our encryption keys
kubectl apply -f kubernetes/secrets/sealed-secrets/sealed-secret-keys.keyapply our old sealed secret
kubectl apply -f kubernetes/secrets/sealed-secrets/sealed-secret.yamlsee 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:
kubectl delete pod -n kube-system -l name=sealed-secrets-controllerRe-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
cat ./kubernetes/secrets/sealed-secrets/sealed-secret.yaml \
| kubeseal --re-encrypt -o yamlI can then save this to override the original old local sealed secret file:
cat ./kubernetes/secrets/sealed-secrets/sealed-secret.yaml \
| kubeseal --re-encrypt -o yaml \
> tmp.yaml && mv tmp.yaml ./kubernetes/secrets/sealed-secrets/sealed-secret.yamlOverview (`kubernetes/servicemesh/istio/README.md`)
Introduction to Istio
We need a Kubernetes cluster
Lets create a Kubernetes cluster to play with using kind
kind create cluster --name istio --image kindest/node:v1.19.1Deploy our microservices (Video catalog)
# 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
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 100sMake sure our ingress controller is running
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 2m38sWe'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:
127.0.0.1 servicemesh.demoLet's access our applications via Ingress
kubectl -n ingress-nginx port-forward deploy/nginx-ingress-controller 80Access 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`:
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.4Install Istio CLI
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
istioctl x precheckIstio Profiles
https://istio.io/latest/docs/setup/additional-setup/config-profiles/
istioctl profile list
istioctl install --set profile=default
kubectl -n istio-system get pods
istioctl proxy-statusMesh our video catalog services
There are 2 ways to mesh:
- 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
kubectl label namespace/default istio-injection=enabled
# restart all pods to get sidecar injected
kubectl delete pods --all- 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.
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
While ($true) { curl -UseBasicParsing http://servicemesh.demo/home/;curl -UseBasicParsing http://servicemesh.demo/api/playlists; Start-Sleep -Seconds 1;}Observability
Grafana
kubectl apply -n istio-system -f /tmp/istio-1.6.12/samples/addons/grafana.yamlWe can see the components in the istio-system namespace:
kubectl -n istio-system get podsAccess grafana dashboards :
kubectl -n istio-system port-forward svc/grafana 3000Kiali
NOTE: this may fail because CRDs need to generate, if so, just rerun the command:
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 20001Virtual Services
Auto Retry
Let's add a fault in the videos-api by setting env variable FLAKY=true
kubectl edit deploy videos-apikubectl apply -f kubernetes/servicemesh/istio/retries/videos-api.yamlWe can describe pods using istioctl
# 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:
istioctl analyze --namespace defaultTraffic Splits
Let's deploy V2 of our application which has a header that's under development
kubectl apply -f kubernetes/servicemesh/istio/traffic-splits/videos-web-v2.yaml
# we can see v2 pods
kubectl get podsLet's send 50% of traffic to V1 and 50% to V2 by using a VirtualService
kubectl apply -f kubernetes/servicemesh/istio/traffic-splits/videos-web.yamlCanary 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.
kubectl apply -f kubernetes/servicemesh/istio/canary/videos-web.yamlWe 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
kind create cluster --name linkerd --image kindest/node:v1.30.4Deploy our microservices (Video catalog)
# 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
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 100sMake sure our ingress controller is running
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 9m2sWe'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:
127.0.0.1 servicemesh.demoLet's access our applications via Ingress
kubectl -n ingress-nginx port-forward svc/ingress-nginx-controller 80Access 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`:
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.4Linkerd 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
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 --helpPre flight checks
Linkerd has a great capability to check compatibility with the target cluster
linkerd check --preGet the YAML
# 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.yamlInstall Linkerd
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.yamlLet's wait until all components are running
watch kubectl -n linkerd get pods
kubectl -n linkerd get svcDo a final check
linkerd checkThe dashboard
To access the linkerd dashboard, we need to install the viz extension
linkerd viz install | kubectl apply -f -
kubectl -n linkerd-viz port-forward svc/web 8084Mesh our video catalog services
There are 2 ways to mesh:
- 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:
template:
metadata:
annotations:
linkerd.io/inject: enabled- Or inject
linkerdon the fly:
This may only be temporary as your CI/CD system may roll out the previous YAML
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
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 deployAdd Faulty behaviour in videos API
kubectl edit deploy videos-api
#set environment FLAKY=trueService Profile
linkerd profile -n default videos-api --tap deploy/videos-api --tap-duration 10sAfter crafting the serviceprofile, we can apply it using kubectl
kubectl apply -f kubernetes/servicemesh/linkerd/serviceprofiles/videos-api.yamlWe can see that service profile helps us add retry policies in place:
linkerd routes -n default deploy/playlists-api --to svc/videos-api -o wide
linkerd top deploy/videos-apiMutual TLS
We can validate if mTLS is working
/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 deployOverview (`kubernetes/tutorials/basics/README.md`)
Kubernetes Tutorial: The Basics
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
envvariables - 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
- Install Docker here
- Let's take a look at Wordpress on Docker Hub
- Build our docker file
Create Network
docker network create wordpressBuild & Test container images
Wordpress example
cd kubernetes\tutorials\basics\
docker build -f dockerfiles/wordpress.dockerfile . -t aimvector/wordpress-example- Run our Wordpress container
docker run -it --rm -p 80:80 --net wordpress aimvector/wordpress-exampleThe wordpress container will be visible on port 80 on http://localhost/
MySQL example
- We need a database, let's take a look at MySQL on Docker Hub
- Build our MySQl container image
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
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
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-exampleClean up
docker rm -f wordpress
docker rm -f mysql
docker network rm wordpress
rm dataKubernetes Tools: kubectl
To manage and work with Kubernetes, you need kubectl
Let's grab that from here
Run Kubernetes Locally
- Install
kubectlto work with kubernetes
We'll head over to the kubernetes site to download kubectl
- Install the
kindbinary
You will want to head over to the kind site
- Create a cluster
kind create cluster --image kindest/node:v1.23.5Namespaces
kubectl create namespace cmsConfigmaps
Environment Variables for pods
How to use configmaps
kubectl -n cms create configmap mysql `
--from-literal MYSQL_RANDOM_ROOT_PASSWORD=1
kubectl -n cms get configmapsSecrets
How to use secrets in pods
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 secretDeployments
- Deployment documentation
cd kubernetes\tutorials\basics
kubectl -n cms apply -f yaml/deploy.yaml
kubectl -n cms get podsServices
Services documentation
kubectl -n cms apply -f .\yaml\service.yaml
kubectl -n cms get svcStorage Class
StorageClass documentation
kubectl get storageclassStatefulset
Statefulset documentation
Let's deploy our mysql using what we learnt above:
kubectl -n cms apply -f yaml/statefulset.yaml
kubectl -n cms get podsPersistent Volumes
Port Forwarding
We can access private service endpoints or pods using port-forward :
kubectl -n cms get pods
kubectl -n cms port-forward 80Public 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
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 80Create an Ingress
kubectl -n cms apply -f yaml/ingress.yamlOverview (`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.
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.
# Debian/Ubuntu
sudo apt-get update
sudo apt-get install -y podmanBasic Commands
Podman's CLI mirrors Docker's, so most commands feel familiar right away:
podman --helpBuilding 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.
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.0Notice 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:
#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 rmCompose
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
podman compose buildOverview (`messaging/rabbitmq/kubernetes/readme.md`)
RabbitMQ on Kubernetes
Create a cluster with kind
kind create cluster --name rabbit --image kindest/node:v1.18.4Namespace
kubectl create ns rabbitsStorage Class
kubectl get storageclass
NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE
standard (default) rancher.io/local-path Delete WaitForFirstConsumer false 84sDeployment
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.yamlAccess the UI
kubectl -n rabbits port-forward rabbitmq-0 8080:15672Go to htttp://localhost:8080
Username: guest
Password: guest
Message Publisher
cd messaging\rabbitmq\applications\publisher
docker build . -t aimvector/rabbitmq-publisher:v1.0.0
kubectl apply -n rabbits -f deployment.yamlAutomatic Synchronization
https://www.rabbitmq.com/ha.html#unsynchronised-mirrors
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 queuesOverview (`monitoring/logging/opentelemetry/README.md`)
Logging with OpenTelemetry
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
kind create cluster --name otel --image kindest/node:v1.34.0Test our cluster:
kubectl get nodes
NAME STATUS ROLES AGE VERSION
otel-control-plane Ready control-plane 40s v1.34.0Helm charts
OpenTelemetry Operator chart
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts
helm repo update
helm search repo open-telemetry --versionsWe'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.
OTEL_VERSION=0.93.1Cert-manager chart
We will need cert-manager deployed which Otel uses for local TLS certificate management
helm repo add jetstack https://charts.jetstack.io
helm search repo jetstack --versionsWe'll install version v1.18.2 of cert-manager at the time of this guide which is compatible with our Kubernetes version.
CERTMANAGER_VERSION=v1.18.2Install helm charts
Install cert-manager:
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:
helm install opentelemetry-operator open-telemetry/opentelemetry-operator \
--namespace opentelemetry-operator-system \
--create-namespace \
--version $OTEL_VERSION \
--values=monitoring/opentelemetry/kubernetes/values.yamlView our install:
kubectl get pods -n cert-manager
kubectl get pods -n opentelemetry-operator-systemCreate 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:
kubectl create namespace monitoring
kubectl apply -n monitoring -f monitoring/opentelemetry/kubernetes/collector-tracing.yamlView our collector:
kubectl -n monitoring get pods
kubectl -n monitoring get svcTracing
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.
kubectl apply -f monitoring/opentelemetry/kubernetes/instrumentation.yamlDeploy Microservices
Build applications:
docker compose --file monitoring/opentelemetry/docker-compose.yaml buildLoad images into kind:
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 otelkubectl 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
kubectl port-forward svc/videos-web 80:80
kubectl port-forward svc/playlists-api 81:80Tracing Data Store
In this guide, we'll use a Tempo database for tracing data
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.yamlMetrics Data Store
In this guide, we'll use a Prometheus for our metrics data
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.yamlDashboards
In this guide I use a simple Grafana for dashboards
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.yamlAccess Grafana
We can port-forward to Grafana
kubectl -n grafana port-forward svc/grafana 3000:80Overview (`monitoring/opentelemetry/metrics/README.md`)
OpenTelemetry Metrics
Build the applications
To build the applications navigate your terminal to the location of the compose file
cd monitoring/opentelemetry/Build:
docker compose buildDocker
Start up the stack using local docker compose, by running:
docker compose upKubernetes
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
Build the applications
To build the applications navigate your terminal to the location of the compose file
cd monitoring/opentelemetry/Build:
docker compose buildDocker
Start up the stack using local docker compose, by running:
docker compose upKubernetes
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
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-applicationStart Prometheus
cd sharding
docker compose up -d prometheus-00
docker compose up -d prometheus-01Overview (`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
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-applicationStart 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
cd sharding
docker compose up -d prometheus-00
docker compose up -d prometheus-01Start the Thanos Components
The following will start two Thanos side cars, one for each Prometheus instance.
It will also start a Minio S3 service
cd ../thanos
docker compose upOverview (`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.
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.6Our application
Firstly we have a class to define what a customer looks like:
class Customer:
def __init__(self, c="",f="",l=""):
self.customerID = c
self.firstName = f
self.lastName = l
def fullName(self):
return self.firstName + " " + self.lastNameThen we need a function which returns our customers:
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 customersHere is a function to return a specific customer:
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
"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 existsTry open a file that holds our customer data:
open("customers.log")We can see the file does not exist:
/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
import os.pathThen we can use the os.path.isfile("customers.log") command to check if the file exists
os.path.isfile("customers.log")Using if logic we can check if the file is there:
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
f = open("customers.log")Let's also loop each customer in the file and print it
for customer in f:
print(customer)
f.close()Now we know the file does not exist, let's create it!
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:
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:
customerID, firstName, lastNameimport 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
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:
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:
customers = getCustomers()
for customerID in customers:
print(customers[customerID])Let's also create a function to update customers
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:
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
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.
cd python\introduction\part-2.files
docker build . -t customer-app
docker run -v ${PWD}:/work -w /work customer-appOverview (`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.
FROM python:3.9.6-alpine3.13 as dev
WORKDIR /workLet's build and start our container:
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.6Our application
Firstly we have a class to define what a customer looks like:
class Customer:
def __init__(self, c="",f="",l=""):
self.customerID = c
self.firstName = f
self.lastName = l
def fullName(self):
return self.firstName + " " + self.lastNameThen we need a function which returns our customers:
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 customersHere is a function to return a specific customer:
def getCustomer(customerID):
customer = getCustomers()
return customer[customerID]Test our functions:
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
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:
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
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
{
"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:
# 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.
import jsonNow 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:
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.
# 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:
"h": { "customerID": "h", "firstName" : "Marcel", "lastName" : "Dempers"}To fix our dictionary, we can iterate it and build a new one
# 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
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.
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.
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 :
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
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
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.
cd python\introduction\part-3.json
docker build . -t customer-app
docker run -v ${PWD}:/work -w /work customer-appOverview (`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.
FROM python:3.9.6-alpine3.13 as dev
WORKDIR /workLet's build and start our container:
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.6Our 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:
import os.path
import csv
import jsonThen we have a class to define what a customer looks like:
class Customer:
def __init__(self, c="",f="",l=""):
self.customerID = c
self.firstName = f
self.lastName = l
def fullName(self):
return self.firstName + " " + self.lastNameThen we need a function which returns our customers:
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:
def getCustomer(customerID):
customer = getCustomers()
return customer[customerID]And finally a function for updating our customers:
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.
{
"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:
customers = getCustomers()
print(customers)Let's add a customer and write it back to file:
customers["h"] = Customer("h", "Marcel", "Dempers").__dict__
updateCustomers(customers)Now customers.json has the new entry if we rerun our code:
python src/app.pyFlask
Checkout official Flask documentation here
A minimal flask application looks like this:
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:
Flask == 2.0.2We can install our dependencies using:
pip install -r src/requirements.txtLet'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:
export FLASK_APP=src/app
flask run -h 0.0.0 -p 5000Note 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.
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 5000Now 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.
* 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:
/all --> which returns all customers
/get/<customerID> --> which returns one customer by CustomerID
/add --> which adds or updates a customerDefining these routes are pretty straight forward, see docs
Let's define our routes:
@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
@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
@app.route("/", methods=['GET'])
def get_customers():
customers = getCustomers()
return json.dumps(customers)Get Customer
@app.route("/get/<string:customerID>", methods=['GET'])
def get_customer(customerID):
customer = getCustomer(customerID)
return customerHTTP 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
#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.
@app.route("/get/<string:customerID>", methods=['GET'])
def get_customer(customerID):
customer = getCustomer(customerID)
if customer == {}:
return {}, 404
else:
return customerAdd Customer
In order to add a customer, we need to be able to read request data.
More about Requests here
Import requests library
from flask import requestRead the request body as JSON:
@app.route("/add", methods=['POST'])
def add_customer():
print(request.json)
return "success", 200Let's test that!
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.
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:
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
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 5000Build our container.
cd python\introduction\part-4.http
docker build . -t customer-appNotice 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
#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:
docker build . -t customer-app
docker run -it -p 5000:5000 -v ${PWD}:/data customer-appOverview (`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
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.
FROM python:3.9.6-alpine3.13 as dev
WORKDIR /workLet's build and start our container:
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.6Our 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:
import os.path
import csv
import json
from flask import Flask
from flask import requestThen we have a class to define what a customer looks like:
class Customer:
def __init__(self, c="",f="",l=""):
self.customerID = c
self.firstName = f
self.lastName = l
def fullName(self):
return self.firstName + " " + self.lastNameAnd also set a global variable for the location of our videos json file:
dataPath = "./customers.json"Then we need a function which returns our customers:
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:
def getCustomer(customerID):
customers = getCustomers()
if customerID in customers:
return customers[customerID]
else:
return {}And finally a function for updating our customers:
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 :
{
"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:
app = Flask(__name__)We create our route to get all customers:
@app.route("/", methods=['GET'])
def get_customers():
customers = getCustomers()
return json.dumps(customers)A route to get one customer by ID:
@app.route("/get/<string:customerID>", methods=['GET'])
def get_customer(customerID):
customer = getCustomer(customerID)
if customer == {}:
return {}, 404
else:
return customerAnd finally a route to update or add customers called /set :
@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", 200Before 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:
Flask == 2.0.2We can install our dependencies using:
pip install -r requirements.txtThis 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:
export FLASK_APP=src/app
flask run -h 0.0.0 -p 5000Now 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.
redis == 3.5.3We can proceed to install it using pip install
pip install -r requirements.txtNow to connect to Redis in a highly available manner, we need to take a look at theSentinel 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.
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.
# 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.
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:
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.txtNow we can setup a client:
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:
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
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
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:
def getCustomers():
customers = {}
customerIDs = redis_command(redis_master.scan_iter, "*")
for customerID in customerIDs:
customer = getCustomer(customerID)
customers[customer["customerID"]] = customer
return customersLet's improve our functions by adding a new function to update a single customer:
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:
def updateCustomers(customers):
for customer in customers:
updateCustomer(customer)Now our simple functions are done, let's hook them up to our endpoints
# firstly delete these test lines
print(redis_command(redis_master.set, 'foo', 'bar'))
print(redis_command(redis_master.get, 'foo'))Our simple Get all
@app.route("/", methods=['GET'])
def get_customers():
customers = getCustomers()
return json.dumps(customers)Our Get by ID
@app.route("/get/<string:customerID>", methods=['GET'])
def get_customer(customerID):
customer = getCustomer(customerID)
if customer == {}:
return {}, 404
else:
return customerAnd our update endpoint to update a customer
@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", 200Docker
Let's build our container image and run it while mounting our customer file
Our final dockerfile
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 5000Build our container.
cd python\introduction\part-5.database.redis
docker build . -t customer-appNow we can run our production container:
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-appOverview (`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.
docker run -it -p 80:80 nginx bash
# get my public IP for this server
curl ifconfig.co
# lets get out of the container
exitNow 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
docker run -it -p 80:80 nginxIn 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
cd .\security\letsencrypt\introduction\
docker build . -t certbot
docker run -it --rm --name certbot `
-v ${PWD}:/letsencrypt `
-v ${PWD}/certs:/etc/letsencrypt `
certbot bashNGINX
We've customised our nginx.conf as shown in the video
Run this NGINX, we mount the shared folder that certbot will use:
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 `
nginxIssue certificate
In certbot, generate our cert:
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-leRenewal
To do a dry run of cert renewal:
certbot renew --dry-runReload our NGINX web server if the certs change:
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














