Deploy an AI Security API to Kubernetes
Containerize a threat-detection API and deploy it to a local Kubernetes cluster.
Introduction
30 Second Summary
Every time you interact with an AI tool, your request hits a service running inside a container on a cluster of servers. When that service needs to scale up or update without going offline, container orchestration makes it happen.
In this project, you will build a threat-detection API, package it into a Docker container, and deploy it to a local Kubernetes cluster. You will scale it to multiple replicas and push a zero-downtime rolling update.
What You'll Build
A Python Flask API that classifies text as "threat" or "safe", containerized with Docker and orchestrated on a local Kubernetes cluster.
By the end of this project, you'll have:
- A containerized Python API running inside a local Kubernetes cluster with multiple replicas.
- A working rolling update pipeline that swaps API versions with zero downtime.
- Kubernetes YAML manifests defining your Deployment and Service resources.
- Secret Mission: Secure the API with Kubernetes Secrets and authentication headers.
Are there any prerequisites?
None. This guide walks you through installing every tool you need, including VS Code, Docker Desktop, and Python. Basic familiarity with running commands in a terminal is helpful but not required.
Install Your Tools
Before you can build and deploy your threat-detection API, you need three core tools on your machine.
VS Code is your code editor. Docker Desktop gives you both Docker (for building container images) and Kubernetes (for orchestrating those containers in a cluster). In this step, you will install all three and verify they work.
In this step, get ready to:
- Install VS Code for editing your project files.
- Install Docker Desktop with the WSL 2 backend.
- Enable Kubernetes inside Docker Desktop and verify the cluster is running.
Install VS Code
VS Code is a free, lightweight code editor that you will use to write your Python API, Dockerfile, and Kubernetes manifests.
- Open your browser and navigate to https://code.visualstudio.com/download.
- Under the Windows section, click the User Installer x64 download button.
- Once the download finishes, double-click VSCodeUserSetup-{version}.exe to run the installer.
- Follow the prompts in the installer wizard, accepting the defaults.
- When the installation completes, click Finish.
What does this installer do?
The User Setup installer places VS Code under your local user folder and automatically adds the code command to your system PATH. This means you can launch VS Code from any terminal by typing code.
Now verify that VS Code installed correctly. You need to open a fresh PowerShell window so it picks up the updated PATH.
- Press the Windows key to open your search bar.
- Type PowerShell and press Enter to open it.
- Verify VS Code is installed by running this command:
code --version
✔️ I see a version number
VS Code is installed and on your PATH. You are ready to move on.
ⓧ Command not found
The installer may not have added VS Code to your PATH, or the terminal session is stale.
- Close all PowerShell windows and open a brand new one.
- Run code --version again.
- If it still fails, uninstall VS Code and reinstall using the same User Setup installer. Make sure you do not skip any steps in the wizard.
Install Docker Desktop
Docker Desktop is a single application that bundles the Docker engine, the kubectl CLI, and a built-in Kubernetes cluster. Installing it gives you everything you need to build container images and run them in an orchestrator.
- Open your browser and navigate to https://docs.docker.com/desktop/setup/install/windows-install/.
- Click the download button to get Docker Desktop Installer.exe.
- Double-click Docker Desktop Installer.exe to run the installer.
- When the installer asks which installation mode you prefer, choose Per-user installation. This does not require administrator privileges.
- On the Configuration page, ensure Use WSL 2 instead of Hyper-V is selected.
- Follow the remaining prompts in the installation wizard.
- When the installation is successful, click Close.
Why WSL 2?
WSL 2 (Windows Subsystem for Linux 2) provides a lightweight Linux kernel that Docker uses to run containers. It is faster and more compatible than the older Hyper-V backend for per-user installations.
Docker Desktop does not start automatically after installation. You need to launch it manually.
- Press the Windows key to open your search bar.
- Type Docker Desktop and press Enter to open it.
- On first launch, Docker Desktop shows the Docker Subscription Service Agreement. Click Accept to continue.
Is Docker Desktop free?
Docker Desktop is free for personal use, education, and small businesses (fewer than 250 employees AND less than $10 million in annual revenue). Since you are using it for learning, you qualify for free use.
Wait for Docker Desktop to finish initializing. Once the Dashboard appears, Docker is ready.
- Switch back to your PowerShell window and verify Docker is installed by running this command:
docker --version
✔️ I see a version number
Docker is installed and running. You are ready to enable Kubernetes.
ⓧ Command not found
Docker Desktop may still be starting, or the PATH was not updated.
- Make sure Docker Desktop is fully running (look for the whale icon in your system tray).
- Close your PowerShell window and open a new one so it picks up the updated PATH.
- Run docker --version again.
- If it still fails, restart your computer to ensure Docker Desktop services are running.
Enable Kubernetes in Docker Desktop
Docker Desktop includes a built-in single-node Kubernetes cluster. You just need to create it from the Dashboard. This gives you a real cluster running locally that uses the same kubectl commands you would use in production.
- In the Docker Desktop Dashboard, select the Kubernetes view in the left sidebar.
- Click Create cluster.
- For cluster type, choose kubeadm. This is a single-node cluster and the simplest option for learning.
- Click Create and wait approximately one minute for Kubernetes to initialize.
Why kubeadm?
Docker Desktop offers kubeadm (single-node) and kind (multi-node) cluster types. For this project, a single node is all you need. It runs the same Kubernetes API as production clusters, so every command you learn here transfers directly to real-world deployments.
Once the cluster finishes creating, verify it is running from PowerShell.
- Switch back to your PowerShell window and run this command to check the Kubernetes version:
kubectl version
- Next, confirm the cluster node is ready by running:
kubectl get nodes
✔️ I see docker-desktop with STATUS Ready
Your local Kubernetes cluster is up and running. You are all set to start building.
ⓧ Command not found or connection refused
The kubectl binary may not be on your PATH, or the cluster is still starting.
- Confirm Docker Desktop is running and the Kubernetes indicator in the bottom-left of the Dashboard shows a green dot.
- Close your PowerShell window and open a new one.
- Run kubectl get nodes again.
- If the node shows NotReady, wait another minute and try again. The cluster sometimes takes extra time on first creation.
Still seeing errors?
Make sure virtualization is enabled in your BIOS settings. Docker Desktop requires hardware virtualization to run. You can check this by opening Task Manager, going to the Performance tab, and confirming Virtualization: Enabled is shown.
Help me troubleshoot Docker Desktop and Kubernetes not starting on Windows.
Your environment is ready. You have VS Code for editing, Docker for building containers, and a local Kubernetes cluster for deployment. Next up, you will build the threat-detection API that you will eventually deploy to this cluster.
Build the Threat Detection API
Your tools are installed and your local Kubernetes cluster is running. Now it is time to build the actual application you will deploy.
In this step, you will create a Python Flask API that simulates an AI threat-detection service. The API accepts text input and classifies it as "threat" or "safe" based on keyword matching.
In this step, get ready to:
- Set up a Python project with Flask installed.
- Write the threat detection API with health check and analysis endpoints.
- Test the API locally to confirm it classifies threats correctly.
Create the project and install Flask
Before you can write any API code, you need Python installed and a project folder ready. You will also create a virtual environment to keep your project dependencies isolated.
- Check if Python is already installed by running this command in your PowerShell terminal:
python --version
✔️ I see version 3.13 or higher
Python is ready to go. Move on to creating your project folder below.
ⓧ I see an older version
You have Python installed but need version 3.13 for this project. The older version may cause dependency issues with the packages you will install.
- Open your browser and go to https://www.python.org/downloads/.
- Download the Python 3.13 installer for Windows (64-bit).
- Run the installer. On the very first screen, check the box labelled Add Python to PATH before clicking anything else.
- Click Install Now and wait for the installation to complete.
- Close and reopen your PowerShell terminal, then run python --version again to confirm it shows 3.13.
Still seeing the old version?
Windows may resolve the old Python first from PATH. Re-run the 3.13 installer, choose Modify, and ensure the PATH checkbox is enabled. Then restart your terminal.
help me fix my Python version on Windows
ⓧ Command not found
Python is not installed yet. Follow these steps to install it.
- Open your browser and go to https://www.python.org/downloads/.
- Download the Python 3.13 installer for Windows (64-bit).
- Run the installer. On the very first screen, check the box labelled Add Python to PATH before clicking anything else.
- Click Install Now and wait for the installation to complete.
- Close and reopen your PowerShell terminal, then run python --version to confirm it shows 3.13.
Python command still not recognized?
The Add Python to PATH checkbox is unchecked by default and easy to miss. Re-run the installer, select Modify, and ensure PATH is enabled. Then restart your terminal completely.
help me get Python working on Windows
- Create a project folder on your Desktop and move into it by running these commands:
cd ~/Desktop
mkdir threat-api
cd threat-api
- Open this folder in VS Code by running:
code .
VS Code opens with the threat-api folder as your workspace.
- Open the integrated terminal in VS Code by pressing Ctrl+` (backtick key, usually above Tab).
- Create a virtual environment by running:
python -m venv venv
What is a virtual environment?
A virtual environment is an isolated Python installation inside your project folder. It keeps your project's packages separate from other Python projects on your machine. This prevents version conflicts between different projects.
- Activate the virtual environment by running:
venv\Scripts\activate
You should see (venv) appear at the start of your terminal prompt. This confirms the virtual environment is active.
- Install Flask by running:
pip install Flask==3.1.3
You should see output ending with "Successfully installed" and a list of packages including Flask.
Seeing a permission error or pip not found?
Make sure your virtual environment is active. You should see (venv) in your prompt. If not, run venv\Scripts\activate again.
help me fix pip install issues in my virtual environment
Write the API
Your project is ready for code. The API needs two endpoints: one for health checks (so Kubernetes can monitor the container) and one for analyzing text input.
- Create a new file called app.py in the threat-api folder. In VS Code, click the New File icon in the Explorer sidebar and name it app.py.
- Add the application setup and health endpoint by pasting this code into app.py:
import os
from flask import Flask, request, jsonify
# Create the Flask application
app = Flask(__name__)
# Version tag for tracking deployments
VERSION = "v1"
# Keywords that indicate potential security threats
THREAT_KEYWORDS = [
"malware", "phishing", "exploit", "ransomware",
"injection", "overflow", "trojan", "backdoor"
]
# Health check endpoint for Kubernetes readiness probes
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "ok", "version": VERSION})
What does this code do?
Flask is the web framework that handles HTTP requests. VERSION is a tag you will use later to confirm which version of your app is running after a Kubernetes update.
THREAT_KEYWORDS is the list of suspicious terms the API checks for. The /health endpoint returns a simple status response so Kubernetes can verify your container is alive.
- Add the analysis endpoint and the main entry point below the health function in app.py:
# Main analysis endpoint that classifies text as threat or safe
@app.route("/analyze", methods=["POST"])
def analyze():
data = request.get_json()
# Validate that the request contains a text field
if not data or "text" not in data:
return jsonify({"error": "Missing 'text' field"}), 400
text = data["text"].lower()
# Check for threat keywords in the input text
threats_found = [kw for kw in THREAT_KEYWORDS if kw in text]
# Calculate classification and confidence score
if threats_found:
classification = "threat"
confidence = min(0.5 + len(threats_found) * 0.15, 0.99)
else:
classification = "safe"
confidence = 0.92
return jsonify({
"classification": classification,
"confidence": round(confidence, 2),
"threats_detected": threats_found,
"version": VERSION
})
# Run the app on all interfaces so Docker can access it
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
What does this code do?
The /analyze endpoint accepts a POST request with JSON containing a text field. It converts the text to lowercase and checks for any matching threat keywords.
If keywords are found, the confidence score increases with more matches (capped at 0.99). The host="0.0.0.0" setting makes the app listen on all network interfaces. This is required for Docker containers, because 127.0.0.1 would be unreachable from outside the container.
- Save app.py by pressing Ctrl+S.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
Here is the complete app.py file. Make sure yours matches this exactly:
import os
from flask import Flask, request, jsonify
# Create the Flask application
app = Flask(__name__)
# Version tag for tracking deployments
VERSION = "v1"
# Keywords that indicate potential security threats
THREAT_KEYWORDS = [
"malware", "phishing", "exploit", "ransomware",
"injection", "overflow", "trojan", "backdoor"
]
# Health check endpoint for Kubernetes readiness probes
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "ok", "version": VERSION})
# Main analysis endpoint that classifies text as threat or safe
@app.route("/analyze", methods=["POST"])
def analyze():
data = request.get_json()
# Validate that the request contains a text field
if not data or "text" not in data:
return jsonify({"error": "Missing 'text' field"}), 400
text = data["text"].lower()
# Check for threat keywords in the input text
threats_found = [kw for kw in THREAT_KEYWORDS if kw in text]
# Calculate classification and confidence score
if threats_found:
classification = "threat"
confidence = min(0.5 + len(threats_found) * 0.15, 0.99)
else:
classification = "safe"
confidence = 0.92
return jsonify({
"classification": classification,
"confidence": round(confidence, 2),
"threats_detected": threats_found,
"version": VERSION
})
# Run the app on all interfaces so Docker can access it
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Test the API locally
Before containerizing anything, you should confirm the API works on its own. This saves debugging time later when Docker or Kubernetes is involved.
- Start the Flask development server by running:
python app.py
You should see output indicating Flask is running on http://0.0.0.0:5000. The terminal will stay occupied by the running server.
- Open a second terminal in VS Code by clicking the + icon in the terminal panel (top-right of the terminal area).
- Test the health endpoint by running:
Invoke-RestMethod http://localhost:5000/health
You should see a response showing status: ok and version: v1.
- Test the analysis endpoint with a threat sample by running:
Invoke-RestMethod -Method Post -Uri http://localhost:5000/analyze -ContentType "application/json" -Body '{"text": "detected malware in the system"}'
You should see a response with classification: threat and malware listed in the threats detected.
- Test with a safe input by running:
Invoke-RestMethod -Method Post -Uri http://localhost:5000/analyze -ContentType "application/json" -Body '{"text": "the weather is nice today"}'
This time you should see classification: safe with a confidence of 0.92.
- Switch back to the first terminal (the one running Flask) and press Ctrl+C to stop the server.
Seeing a connection refused error?
Make sure you ran python app.py in the first terminal and that it shows Flask is running. The server must be active before you can send requests to it.
Also confirm your virtual environment is active ((venv) in the prompt). If Flask is not installed in the active environment, the server will fail to start.
help me debug my Flask API not responding
Your threat detection API is working locally. Next up, you will package it into a Docker container so it can run anywhere.
Containerize with Docker
Your threat detection API works on your machine. But what happens when you need to ship it to a server, a teammate, or a cloud cluster?
Docker solves this by packaging your code, dependencies, and runtime into a single portable unit called a container. In this step, you'll write a Dockerfile, build an image, and run your API inside a container.
In this step, get ready to:
- Write a Dockerfile that defines your container image.
- Create a requirements file for your Python dependencies.
- Build and run the container to test your API.
Write the Dockerfile
A Dockerfile is a text file with instructions that tell Docker how to build your image layer by layer. Each instruction adds a new layer to the image.
- In VS Code's Explorer sidebar, right-click inside the threat-api folder.
- Select New File and name it Dockerfile (no file extension).
- Add the following code to Dockerfile:
FROM python:3.13-slim
RUN useradd -m appuser
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
USER appuser
EXPOSE 5000
CMD ["python", "app.py"]
What does this Dockerfile do?
- FROM python:3.13-slim starts from an official Python image that is small (~150MB) but has everything you need.
- RUN useradd -m appuser creates a non-root user. Running containers as root is a security risk because a compromised container could access the host system.
- WORKDIR /app sets the working directory inside the container.
- COPY requirements.txt . and RUN pip install install dependencies first. This layer gets cached, so rebuilds are faster when only your code changes.
- USER appuser switches to the non-root user before running the app.
- CMD ["python", "app.py"] defines the command that runs when the container starts.
Why use a non-root user?
By default, containers run as root. If an attacker exploits a vulnerability in your app, they gain root access inside the container. With a non-root user, the blast radius is limited.
This is a common interview question for Security AI Engineer roles. Interviewers want to see that you understand container security best practices.
- Save Dockerfile.
You won't see a visible change yet. The next chunk creates the requirements.txt file that this Dockerfile references.
Create the requirements file
The Dockerfile copies requirements.txt into the container and installs from it. This file pins exact dependency versions so your container builds are reproducible.
- In VS Code's Explorer sidebar, right-click inside the threat-api folder.
- Select New File and name it requirements.txt.
- Add this single line to requirements.txt:
Flask==3.1.3
Why pin the version?
Pinning Flask==3.1.3 ensures every build uses the same version. Without a pin, a future pip install Flask could pull a newer version that breaks your app.
- Save requirements.txt.
- Confirm both files exist by running this in your VS Code terminal:
ls
You should see app.py, Dockerfile, requirements.txt, and the venv folder listed.
✔️ Awesome, I've got everything!
Great. Double check you've saved both files before moving on to the build.
ⓧ I'd like to double check the full code
Here is the complete Dockerfile:
FROM python:3.13-slim
RUN useradd -m appuser
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
USER appuser
EXPOSE 5000
CMD ["python", "app.py"]
And here is the complete requirements.txt:
Flask==3.1.3
Build and run the container
With your Dockerfile and requirements ready, you can now build a Docker image and run it as a container. The image is a snapshot of your app and its environment.
- Make sure your terminal is in the threat-api folder (the one containing your Dockerfile).
- Build your Docker image by running this command:
docker build -t threat-api:v1 .
What does this command do?
- docker build reads the Dockerfile and executes each instruction to create an image.
- -t threat-api:v1 tags the image with the name threat-api and version v1.
- . tells Docker to use the current directory as the build context (where it looks for your Dockerfile and source files).
You should see output showing each build step. The first line includes the word FINISHED when the build completes successfully.
Build failed?
- Make sure Docker Desktop is running. Check the system tray for the Docker whale icon.
- Confirm you are in the threat-api folder. Run ls and verify you see Dockerfile listed.
- Check that your Dockerfile has no file extension. Windows sometimes adds .txt silently. Rename it to just Dockerfile if needed.
Still stuck? Help me debug my Docker build error
- Start a container from your newly built image by running:
docker run -d -p 5000:5000 threat-api:v1
What do these flags mean?
- -d runs the container in detached mode (in the background). Docker prints the container ID and returns you to the terminal.
- -p 5000:5000 maps port 5000 on your machine to port 5000 inside the container, so you can reach the API from your browser.
Docker prints a long hexadecimal string. This is your container ID. Copy or note the first few characters. You will need them to stop the container shortly.
- Open your browser and go to http://localhost:5000/health.
You should see a JSON response showing {"status":"ok","version":"v1"}.
Don't see the health response?
- Check that the container is running by executing docker ps. You should see threat-api:v1 listed with STATUS Up.
- If no container is listed, it crashed on start. Run docker logs followed by the container ID to see what went wrong.
- Make sure port 5000 is not already in use. If you left python app.py running from the previous step, stop it first with Ctrl+C in that terminal.
Still stuck? Help me figure out why my container is not responding
- Stop the running container by running this command (replace the container ID with the one Docker printed earlier):
docker stop [[CONTAINER_ID="your container ID from the docker run output"]]
Your API is now proven to work inside a container. The image is tagged threat-api:v1 and ready for deployment.
Next up, you'll deploy this container to a local Kubernetes cluster and learn how orchestration keeps your API running at scale.
Deploy to Kubernetes
Your containerized API is ready to go. Right now it runs as a single Docker container on your machine. That works for testing, but in production, you need multiple copies running behind a single endpoint so the service stays up even if one copy crashes.
This is exactly what Kubernetes does. In this step, you will write YAML manifests that tell Kubernetes how many copies of your API to run, and how to expose them to the network.
In this step, get ready to:
- Write a Kubernetes Deployment manifest to run 2 replicas of your API.
- Write a Kubernetes Service manifest to expose the API on port 30080.
- Deploy both manifests and verify the pods are running.
Write the Deployment manifest
A Deployment is a Kubernetes resource that manages a set of identical Pods. You declare how many replicas you want, which container image to use, and Kubernetes handles scheduling them across the cluster.
- Create a folder called k8s inside your threat-api project folder by running this command in your VS Code terminal:
mkdir k8s
- Create a new file called deployment.yaml inside the k8s folder by running:
New-Item k8s/deployment.yaml
- Open k8s/deployment.yaml in VS Code and paste the following manifest:
apiVersion: apps/v1
kind: Deployment
metadata:
name: threat-api
labels:
app: threat-api
spec:
replicas: 2
selector:
matchLabels:
app: threat-api
template:
metadata:
labels:
app: threat-api
spec:
containers:
- name: threat-api
image: threat-api:v1
ports:
- containerPort: 5000
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 5
periodSeconds: 10
What does this manifest do?
This YAML file declares the desired state of your application to Kubernetes.
- replicas: 2 tells Kubernetes to always keep 2 copies of your API running.
- image: threat-api:v1 points to the Docker image you built in the previous step.
- readinessProbe hits the /health endpoint every 10 seconds. Kubernetes only sends traffic to a Pod once this probe succeeds.
- selector and labels connect the Deployment to its Pods using the app: threat-api label.
- Save k8s/deployment.yaml.
File not saving?
Make sure you created the k8s folder first. If VS Code shows the file path in red, the folder does not exist yet.
Help me create the k8s folder and deployment.yaml file.
✔️ Awesome, I've got everything!
Great. Make sure you saved k8s/deployment.yaml before moving on.
ⓧ I'd like to double check the full code
apiVersion: apps/v1
kind: Deployment
metadata:
name: threat-api
labels:
app: threat-api
spec:
replicas: 2
selector:
matchLabels:
app: threat-api
template:
metadata:
labels:
app: threat-api
spec:
containers:
- name: threat-api
image: threat-api:v1
ports:
- containerPort: 5000
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 5
periodSeconds: 10
Write the Service manifest
A Service is the networking layer that exposes your Pods to the outside world. Without it, your Pods are only reachable inside the cluster.
- Create a new file called service.yaml inside the k8s folder by running:
New-Item k8s/service.yaml
- Open k8s/service.yaml in VS Code and paste the following manifest:
apiVersion: v1
kind: Service
metadata:
name: threat-api-service
spec:
type: NodePort
selector:
app: threat-api
ports:
- protocol: TCP
port: 5000
targetPort: 5000
nodePort: 30080
What does this Service do?
This creates a NodePort Service. NodePort opens a specific port on every node in your cluster and forwards traffic to your Pods.
- nodePort: 30080 is the port you will access from your browser at http://localhost:30080.
- targetPort: 5000 is the port your Flask app listens on inside the container.
- selector: app: threat-api matches the labels on your Deployment's Pods, so the Service knows where to route traffic.
- Save k8s/service.yaml.
Unsure about NodePort range?
Kubernetes only allows NodePort values between 30000 and 32767. If you use a port outside this range, the apply will fail.
Why is my NodePort value being rejected by Kubernetes?
✔️ Awesome, I've got everything!
Great. Make sure you saved k8s/service.yaml before moving on.
ⓧ I'd like to double check the full code
apiVersion: v1
kind: Service
metadata:
name: threat-api-service
spec:
type: NodePort
selector:
app: threat-api
ports:
- protocol: TCP
port: 5000
targetPort: 5000
nodePort: 30080
Deploy and verify
Both manifests are ready. Now you will apply them to your local Kubernetes cluster and confirm everything is running.
- Apply both manifests to your cluster by running this command:
kubectl apply -f k8s/
What does this command do?
The kubectl apply -f k8s/ command reads every YAML file in the k8s/ folder and creates (or updates) the corresponding resources in your cluster. It processes both deployment.yaml and service.yaml in one go.
- Check that your pods are running by running:
kubectl get pods
You should see 2 pods with a STATUS of Running. The output looks similar to this:
NAME READY STATUS RESTARTS AGE
threat-api-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
threat-api-xxxxxxxxxx-xxxxx 1/1 Running 0 30s
- Confirm the Service is active by running:
kubectl get services
You should see threat-api-service listed with type NodePort and port 30080 in the output.
- Test the API through Kubernetes by sending a POST request. Run this in your terminal:
curl.exe -X POST http://localhost:30080/analyze -H "Content-Type: application/json" -d '{\"text\": \"possible malware detected in system\"}'
Why curl.exe instead of curl?
In PowerShell, curl is an alias for Invoke-WebRequest. Using curl.exe calls the real curl binary that ships with Windows 11. The \\\" escaping is needed because PowerShell strips quotes when passing arguments to external programs.
You should see a JSON response with "classification": "threat" and "version": "v1".
- View the logs from one of your pods by running:
kubectl logs [[POD_NAME="your pod name from kubectl get pods"]]
You should see Flask request logs showing the POST request you just made.
Pods stuck in Pending or CrashLoopBackOff?
If your pods show Pending status, Kubernetes cannot schedule them. Make sure Docker Desktop is running and the Kubernetes cluster is active (the Docker Desktop icon should show a green dot).
If pods show CrashLoopBackOff, the container is crashing on startup. Run kubectl logs on the failing pod to see the error. Common causes are a typo in app.py or a missing dependency in requirements.txt.
If curl.exe returns "connection refused", the pods may not be ready yet. Wait 10-15 seconds for the readiness probe to pass, then try again.
Help me debug my Kubernetes pods that are not running.
Your API is now running on Kubernetes with 2 replicas behind a single endpoint. Next up, you will scale the deployment and push a zero-downtime update from v1 to v2.
Scale and Rolling Update
In the previous step, you deployed your threat-detection API to Kubernetes and verified it handles real requests through its NodePort Service.
Running two replicas is a good start, but production AI services need to handle traffic spikes and ship updates without interrupting users. In this step, you will scale your deployment to four replicas and push a v2 update with zero downtime using Kubernetes rolling updates.
In this step, get ready to:
- Scale the deployment to 4 replicas for higher availability.
- Push a rolling update from v1 to v2 of the API.
- Verify the zero-downtime update and learn how to rollback.
Scale the deployment to 4 replicas
Your deployment currently runs 2 replicas. For AI inference workloads, more replicas mean better availability and load distribution across pods.
- Scale the threat-api deployment to 4 replicas by running this command:
kubectl scale deployment threat-api --replicas=4
What does this command do?
The kubectl scale command imperatively changes the desired replica count for your deployment. Kubernetes immediately begins spinning up new pods to match the target of 4.
- Watch the new pods spin up in real time by running:
kubectl get pods -w
- Wait until all 4 pods show Running in the STATUS column, then press Ctrl+C to stop watching.
Why do multiple replicas matter for AI inference?
AI inference endpoints can be computationally expensive. If a single pod becomes overloaded or crashes, the other replicas continue serving requests. This is how production systems maintain high availability.
Kubernetes automatically distributes incoming traffic across all healthy replicas through the Service you created earlier.
Pods stuck in Pending?
If pods stay in Pending status, your Docker Desktop cluster may not have enough resources allocated. Open Docker Desktop, go to Settings then Resources, and increase the memory allocation to at least 4GB.
Help me fix pods stuck in Pending
Push a rolling update to v2
Now you will update the API to v2 by changing the version indicator in your code, rebuilding the Docker image, and telling Kubernetes to roll out the new version.
- In app.py, find this line near the top of the file:
VERSION = "v1"
- Change "v1" to "v2", so the line now looks like this:
VERSION = "v2"
- Save app.py.
✔️ Awesome, I've got everything!
Great. Double check you have saved your file before moving on.
ⓧ I'd like to double check the full code
import os
from flask import Flask, request, jsonify
app = Flask(__name__)
VERSION = "v2"
THREAT_KEYWORDS = [
"malware", "phishing", "exploit", "ransomware",
"injection", "overflow", "trojan", "backdoor"
]
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "ok", "version": VERSION})
@app.route("/analyze", methods=["POST"])
def analyze():
data = request.get_json()
if not data or "text" not in data:
return jsonify({"error": "Missing 'text' field"}), 400
text = data["text"].lower()
threats_found = [kw for kw in THREAT_KEYWORDS if kw in text]
if threats_found:
classification = "threat"
confidence = min(0.5 + len(threats_found) * 0.15, 0.99)
else:
classification = "safe"
confidence = 0.92
return jsonify({
"classification": classification,
"confidence": round(confidence, 2),
"threats_detected": threats_found,
"version": VERSION
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
- Rebuild the Docker image with the v2 tag by running this command:
docker build -t threat-api:v2 .
What does this command do?
This builds a new container image from your updated app.py and tags it as threat-api:v2. The Dockerfile itself has not changed. Only the application code inside the image is different.
- You should see a success message at the end of the build output confirming the image was tagged.
Build failed?
Make sure Docker Desktop is still running. Also confirm you are in the threat-api folder where your Dockerfile lives.
Help me debug my Docker build
- Tell Kubernetes to update the running deployment to the new image by running:
kubectl set image deployment/threat-api threat-api=threat-api:v2
What does this command do?
The kubectl set image command updates the container image for a running deployment without editing the YAML file. The format is deployment/NAME CONTAINER_NAME=NEW_IMAGE.
Kubernetes sees the image change and begins a rolling update. It gradually spins up pods with the new image while terminating old pods, ensuring the service stays available throughout.
- Watch the rollout progress by running:
kubectl rollout status deployment/threat-api
- Wait until you see a message confirming the rollout is complete.
Now update your deployment manifest to keep it in sync with the live cluster state.
- In k8s/deployment.yaml, find this line:
image: threat-api:v1
- Change threat-api:v1 to threat-api:v2, so the line now looks like this:
image: threat-api:v2
- Save k8s/deployment.yaml.
✔️ Awesome, I've got everything!
Great. Your manifest now matches the live cluster state.
ⓧ I'd like to double check the full code
apiVersion: apps/v1
kind: Deployment
metadata:
name: threat-api
labels:
app: threat-api
spec:
replicas: 2
selector:
matchLabels:
app: threat-api
template:
metadata:
labels:
app: threat-api
spec:
containers:
- name: threat-api
image: threat-api:v2
ports:
- containerPort: 5000
readinessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 5
periodSeconds: 10
Why update the manifest separately?
The kubectl set image command updates the live cluster but does not change your YAML file on disk. Keeping the manifest in sync is important so that the next kubectl apply does not accidentally revert your image back to v1.
Verify the zero-downtime update
Your rolling update is complete. Time to confirm that all pods are running the new version and that the API responds correctly.
- Confirm the deployment now references v2 by running:
kubectl describe deployment threat-api
- Look for Image: threat-api:v2 in the output under the Containers section.
- Test the updated API by sending a POST request to the /analyze endpoint:
curl.exe -X POST http://localhost:30080/analyze -H "Content-Type: application/json" -d "{\"text\": \"possible malware detected\"}"
What should I see?
The response should include "version": "v2" and "classification": "threat" since the text contains the keyword malware. This confirms your pods are running the updated code.
- You can also verify the version directly by hitting the health endpoint:
curl.exe http://localhost:30080/health
- You should see {"status":"ok","version":"v2"} in the output.
Know how to rollback
If a new version introduces a bug, you can instantly revert to the previous version by running:
kubectl rollout undo deployment/threat-api
This tells Kubernetes to redeploy the previous image. You do not need to run this now. Just know it exists as your safety net for production deployments.
Still seeing v1 in the response?
The rolling update may still be in progress. Run kubectl get pods and check that all pods show a recent AGE value. Old pods with longer uptimes are still serving v1.
Also verify that your app.py has VERSION = "v2" and that you rebuilt the image with docker build -t threat-api:v2 . before running kubectl set image.
Help me debug why I still see v1
Your API is now running v2 across four replicas with zero-downtime deployment skills in your toolkit. Next up, you will take your security posture even further in the Secret Mission by protecting the API with Kubernetes Secrets.
Secret mission
Secure API Keys with Kubernetes Secrets
Your API is running and scaling, but anyone who knows the URL can hit it. In production, AI inference endpoints need authentication to prevent abuse and unauthorized access. Can you use Kubernetes Secrets to inject an API key and lock down the /analyze endpoint?
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally using Docker Desktop, so there are no ongoing cloud costs. However, the Kubernetes cluster consumes system RAM while Docker Desktop is running.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- Docker Desktop Kubernetes cluster (kubeadm single-node).
- Kubernetes Deployment (threat-api), Service (threat-api-service), and Secret (api-credentials).
- Docker images (threat-api:v1, threat-api:v2, threat-api:v3).
- Local project folder (threat-api) on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with the project.
- Your Kubernetes cluster stays up and the pods continue running.
- Docker Desktop consumes approximately 2GB of system RAM while running. You can check resource usage in Docker Desktop's Dashboard.
- Your local threat-api folder and all Docker images remain on disk.
Pause - I'll come back to this later
Shut down Docker Desktop to free up memory. Everything persists and restarts next time you open it.
- Right-click the Docker icon in your system tray.
- Select Quit Docker Desktop.
- Your Kubernetes cluster, pods, services, secrets, and Docker images all persist on disk.
- Next time you open Docker Desktop, your cluster and deployments resume automatically.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below in order.
- Delete Kubernetes resources (Deployment, Service, and Secret) by running these commands in PowerShell:
kubectl delete -f k8s/
kubectl delete secret api-credentials
- Remove all unused Docker images by running:
docker image prune -a
Docker will prompt you to confirm. Type y and press Enter.
- Stop the Kubernetes cluster in Docker Desktop.
- Open Docker Desktop Dashboard and select the Kubernetes view in the left sidebar.
- Select Stop to disable the cluster. This stops and removes Kubernetes containers.
- Delete your local project folder by running:
cd ~/Desktop
Remove-Item -Recurse -Force threat-api
What about Docker Desktop itself?
You do not need to uninstall Docker Desktop. It is a general-purpose development tool you can reuse for future projects.
If you do want to uninstall it:
- Open Windows Settings from the Start menu.
- Select Apps, then Apps & features.
- Find Docker Desktop in the list and select Uninstall.
Nice Work!
Nice Work!
You just built, containerized, deployed, scaled, and updated a threat-detection API on a local Kubernetes cluster. That is the full container-to-orchestration pipeline that teams use to ship AI services to production.
You've learned how to:
- Build and containerize a Python Flask API using Docker with security best practices like non-root users and slim base images.
- Deploy a containerized application to a local Kubernetes cluster with multiple replicas for high availability.
- Perform a zero-downtime rolling update from v1 to v2, proving your deployment pipeline handles version upgrades gracefully.
- Secret Mission: Secured the API with Kubernetes Secrets and authentication headers, ensuring only authorized clients can access the endpoint.
Ready to quiz yourself?