This tutorial is one of four that put into practice concepts from Microservices March 2022: Kubernetes Networking:
Want detailed guidance on using NGINX for even more Kubernetes networking use cases? Download our free eBook, Managing Kubernetes Traffic with NGINX: A Practical Guide.
Your organization just launched its first app and API in Kubernetes. You’ve been told to expect high traffic volumes (and already implemented autoscaling to ensure NGINX Ingress Controller can quickly route the traffic), but there are concerns that the API may be targeted by a malicious attack. If the API receives a high volume of HTTP requests – a possibility with brute‑force password guessing or DDoS attacks – then both the API and app might be overwhelmed or even crash.
But you’re in luck! The traffic‑control technique called rate limiting is an API gateway use case that limits the incoming request rate to a value typical for real users. You configure NGINX Ingress Controller to implement a rate‑limiting policy, which prevents the app and API from getting overwhelmed by too many requests. Nice work!
This blog accompanies the lab for Unit 2 of Microservices March 2022 – Exposing APIs in Kubernetes, demonstrating how to combine multiple NGINX Ingress Controllers with rate limiting to prevent apps and APIs from getting overwhelmed.
To run the tutorial, you need a machine with:
To get the most out of the lab and tutorial, we recommend that before beginning you:
Review the background blogs, webinar, and video
Watch the 18‑minute video summary of the lab:
This tutorial uses these technologies:
The instructions for each challenge include the complete text of the YAML files used to configure the apps. You can also copy the text from our GitHub repo. A link to GitHub is provided along with the text of each YAML file.
This tutorial includes three challenges:
In this challenge, you deploy a minikube cluster and install Podinfo as a sample app and API. You then deploy NGINX Ingress Controller, configure traffic routing, and test the Ingress configuration.
Create a minikube cluster. After a few seconds, a message confirms the deployment was successful.
$ minikube start 🏄 Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default
Podinfo is a “web application made with Go that showcases best practices of running microservices in Kubernetes”. We’re using it as a sample app and API because of its small footprint.
Using the text editor of your choice, create a YAML file called 1-apps.yaml with the following contents (or copy from GitHub). It defines a Deployment that includes:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
selector:
matchLabels:
app: api
template:
metadata:
labels:
app: api
spec:
containers:
- name: api
image: stefanprodan/podinfo
ports:
- containerPort: 9898
---
apiVersion: v1
kind: Service
metadata:
name: api
spec:
ports:
- port: 80
targetPort: 9898
nodePort: 30001
selector:
app: api
type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frontend
spec:
selector:
matchLabels:
app: frontend
template:
metadata:
labels:
app: frontend
spec:
containers:
- name: frontend
image: stefanprodan/podinfo
ports:
- containerPort: 9898
---
apiVersion: v1
kind: Service
metadata:
name: frontend
spec:
ports:
- port: 80
targetPort: 9898
nodePort: 30002
selector:
app: frontend
type: LoadBalancer
Deploy the app and API:
$ kubectl apply -f 1-apps.yamldeployment.apps/api created
service/api created
deployment.apps/frontend created
service/frontend created
Confirm that the pods for Podinfo API and Podinfo Frontend deployed successfully, as indicated by the value Running
in the STATUS
column.
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
api-7574cf7568-c6tr6 1/1 Running 0 87s
frontend-6688d86fc6-78qn7 1/1 Running 0 87s
The fastest way to install NGINX Ingress Controller is with Helm.
Install NGINX Ingress Controller in a separate namespace (nginx) using Helm.
Create the namespace:
$ kubectl create namespace nginx
Add the NGINX repository to Helm:
$ helm repo add nginx-stable https://helm.nginx.com/stable
Download and install NGINX Ingress Controller in your cluster:
$ helm install main nginx-stable/nginx-ingress \
--set controller.watchIngressWithoutClass=true \
--set controller.ingressClass=nginx \
--set controller.service.type=NodePort \
--set controller.service.httpPort.nodePort=30010 \
--set controller.enablePreviewPolicies=true \
--namespace nginx
Confirm that the NGINX Ingress Controller pod deployed, as indicated by the value Running
in the STATUS
column (for legibility, the output is spread across two lines).
$ kubectl get pods -namespace nginx NAME READY STATUS ...
main-nginx-ingress-779b74bb8b-d4qtc 1/1 Running ...
... RESTARTS AGE
... 0 92s
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: first
spec:
ingressClassName: nginx
rules:
- host: "example.com"
http:
paths:
- backend:
service:
name: frontend
port:
number: 80
path: /
pathType: Prefix
- host: "api.example.com"
http:
paths:
- backend:
service:
name: api
port:
number: 80
path: /
pathType: Prefix
$ kubectl apply -f 2-ingress.yaml ingress.networking.k8s.io/first created
To ensure your Ingress configuration is performing as expected, test it using a temporary pod. Launch a disposable BusyBox pod in the cluster:
$ kubectl run -ti --rm=true busybox --image=busybox If you don't see a command prompt, try pressing enter.
/ #
Test Podinfo API by issuing a request to the NGINX Ingress Controller pod with the hostname api.example.com. The output shown indicates that the API is receiving traffic.
/ # wget --header="Host: api.example.com" -qO- main-nginx-ingress.nginx {
"hostname": "api-687fd448f8-t7hqk",
"version": "6.0.3",
"revision": "",
"color": "#34577c",
"logo": "https://raw.githubusercontent.com/stefanprodan/podinfo/gh-pages/cuddle_clap.gif",
"message": "greetings from podinfo v6.0.3",
"goos": "linux",
"goarch": "arm64",
"runtime": "go1.16.9",
"num_goroutine": "6",
"num_cpu": "4"
}
/ # wget --header="Host: example.com" --header="User-Agent: Mozilla" -qO- main-nginx-ingress.nginx <!DOCTYPE html>
<html>
<head>
<title>frontend-596d5c9ff4-xkbdc</title>
# ...
In another terminal, open Podinfo in a browser. The greetings from podinfo page indicates Podinfo is running.
$ minikube service podinfo
Congratulations! NGINX Ingress Controller is receiving requests and forwarding them to the app and API.
In the original terminal, end the BusyBox session:
/ # exit
$
In this challenge, you install Locust, an open source load‑generation tool, and use it to simulate a traffic surge that overwhelms the API and causes the app to crash.
Using the text editor of your choice, create a YAML file called 3-locust.yaml with the following contents (or copy from GitHub).
The ConfigMap
object defines a script called locustfile.py which generates requests to be sent to the pod, complete with the correct headers. The traffic is not distributed evenly between the app and API – requests are skewed to Podinfo API, with only 1 of 5 requests going to Podinfo Frontend.
The Deployment
and Service
objects define the Locust pod.
apiVersion: v1
kind: ConfigMap
metadata:
name: locust-script
data:
locustfile.py: |-
from locust import HttpUser, task, between
class QuickstartUser(HttpUser):
wait_time = between(0.7, 1.3)
@task(1)
def visit_website(self):
with self.client.get("/", headers={"Host": "example.com", "User-Agent": "Mozilla"}, timeout=0.2, catch_response=True) as response:
if response.request_meta["response_time"] > 200:
response.failure("Frontend failed")
else:
response.success()
@task(5)
def visit_api(self):
with self.client.get("/", headers={"Host": "api.example.com"}, timeout=0.2) as response:
if response.request_meta["response_time"] > 200:
response.failure("API failed")
else:
response.success()
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: locust
spec:
selector:
matchLabels:
app: locust
template:
metadata:
labels:
app: locust
spec:
containers:
- name: locust
image: locustio/locust
ports:
- containerPort: 8089
volumeMounts:
- mountPath: /home/locust
name: locust-script
volumes:
- name: locust-script
configMap:
name: locust-script
---
apiVersion: v1
kind: Service
metadata:
name: locust
spec:
ports:
- port: 8089
targetPort: 8089
nodePort: 30015
selector:
app: locust
type: LoadBalancer
Deploy Locust:
$ kubectl apply -f 3-locust.yaml configmap/locust-script created
deployment.apps/locust created
service/locust created
kubectl
apply
command and so the installation is still in progress, as indicated by the value ContainerCreating
for the Locust pod in the STATUS
field. Wait until the value is Running
before continuing to the next section. (The output is spread across two lines for legibility.)
$ kubectl get pods
NAME READY STATUS ... api-7574cf7568-c6tr6 1/1 Running ...
frontend-6688d86fc6-78qn7 1/1 Running ... locust-77c699c94d-hc76t 0/1 ContainerCreating ...
... RESTARTS AGE
... 0 33m
... 0 33m
... 0 4s
Open Locust in a browser.
$ minikube service locust
Enter the following values in the fields:
Click the Start swarming button to send traffic to Podinfo API and Podinfo Frontend. Observe the traffic patterns on the Locust Charts and Failures tabs:
This is problematic because a single bad actor using the API can take down not only the API, but all apps served by NGINX Ingress Controller!
In the final challenge, you deploy two NGINX Ingress Controllers to eliminate the limitations of the previous deployment, creating a separate namespace for each one, installing separate NGINX Ingress Controller instances for Podinfo Frontend and Podinfo API, reconfigure Locust to direct traffic for the app and API to their respective NGINX Ingress Controllers, and verify that rate limiting is effective. First, let’s look at how to address the architectural problem. In the previous challenge, you overwhelmed NGINX Ingress Controller with API requests, which also impacted the app. This happened because a single Ingress controller was responsible for routing traffic to both the web app (Podinfo Frontend) and the API (Podinfo API).
Running a separate NGINX Ingress Controller pod for each of your services prevents your app from being impacted by too many API requests. This isn’t necessarily required for every use case, but in our simulation it’s easy to see the benefits of running multiple NGINX Ingress Controllers.
The second part of the solution, which prevents Podinfo API from getting overwhelmed, is to implement rate limiting by using NGINX Ingress Controller as an API gateway.
Rate limiting restricts the number of requests a user can make in a given time period. To mitigate a DDoS attack, for example, you can use rate limiting to limit the incoming request rate to a value typical for real users. When rate limiting is implemented with NGINX, clients that submit too many requests are redirected to an error page so they cannot negatively impact the API. Learn how this works in the NGINX Ingress Controller documentation.
An API gateway routes API requests from clients to the appropriate services. A big misinterpretation of this simple definition is that an API gateway is a unique piece of technology. It’s not. Rather, “API gateway” describes a set of use cases that can be implemented via different types of proxies – most commonly an ADC or load balancer and reverse proxy, and increasingly an Ingress controller or service mesh. Rate limiting is a common use case for deploying an API gateway. Learn more about API gateway use cases in Kubernetes in How Do I Choose? API Gateway vs. Ingress Controller vs. Service Mesh on our blog.
Before you can implement the new architecture and rate limiting, you must delete the previous NGINX Ingress Controller configuration.
$ kubectl delete -f 2-ingress.yaml
ingress.networking.k8s.io "first" deleted
Create a namespace called nginx‑web for Podinfo Frontend:
$ kubectl create namespace nginx-web
namespace/nginx-web created
Create a namespace called nginx‑api for Podinfo API:
$ kubectl create namespace nginx-api
namespace/nginx-api created
Install NGINX Ingress Controller:
$ helm install web nginx-stable/nginx-ingress
--set controller.ingressClass=nginx-web \
--set controller.service.type=NodePort \
--set controller.service.httpPort.nodePort=30020 \
--namespace nginx-web
Create an Ingress manifest called 4-ingress-web.yaml for Podinfo Frontend (or copy from GitHub).
apiVersion: k8s.nginx.org/v1 kind: Policy metadata: name: rate-limit-policy spec: rateLimit: rate: 10r/s key: ${binary_remote_addr} zoneSize: 10M --- apiVersion: k8s.nginx.org/v1 kind: VirtualServer metadata: name: api-vs spec: ingressClassName: nginx-api host: api.example.com policies: - name: rate-limit-policy upstreams: - name: api service: api port: 80 routes: - path: / action: pass: api
Deploy the new manifest:
$ kubectl apply -f 4-ingress-web.yaml
ingress.networking.k8s.io/frontend created
Now, reconfigure Locust and verify that:
Perform these steps:
Change the Locust script so that:
Because Locust supports just a single URL in the dashboard, hardcode the value in the Python script using the YAML file 6-locust.yaml with the following contents (or copy from GitHub). Take note of the URLs in each task
.
apiVersion: v1
kind: ConfigMap
metadata:
name: locust-script
data:
locustfile.py: |-
from locust import HttpUser, task, between
class QuickstartUser(HttpUser):
wait_time = between(0.7, 1.3)
@task(1)
def visit_website(self):
with self.client.get("http://web-nginx-ingress.nginx-web/", headers={"Host": "example.com", "User-Agent": "Mozilla"}, timeout=0.2, catch_response=True) as response:
if response.request_meta["response_time"] > 200:
response.failure("Frontend failed")
else:
response.success()
@task(5)
def visit_api(self):
with self.client.get("http://api-nginx-ingress.nginx-api/", headers={"Host": "api.example.com"}, timeout=0.2) as response:
if response.request_meta["response_time"] > 200:
response.failure("API failed")
else:
response.success()
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: locust
spec:
selector:
matchLabels:
app: locust
template:
metadata:
labels:
app: locust
spec:
containers:
- name: locust
image: locustio/locust
ports:
- containerPort: 8089
volumeMounts:
- mountPath: /home/locust
name: locust-script
volumes:
- name: locust-script
configMap:
name: locust-script
---
apiVersion: v1
kind: Service
metadata:
name: locust
spec:
ports:
- port: 8089
targetPort: 8089
nodePort: 30015
selector:
app: locust
type: LoadBalancer
Deploy the new Locust configuration. The output confirms that the script changed but the other elements remain unchanged.
$ kubectl apply -f 6-locust.yaml
configmap/locust-script configured
deployment.apps/locust unchanged
service/locust unchanged
Delete the Locust pod to force a reload of the new ConfigMap. To identify the pod to remove, the argument to the kubectl
delete
pod
command is expressed as piped commands that select the Locust pod from the list of all pods.
$ kubectl delete pod `kubectl get pods | grep locust | awk {'print $1'}`
Verify Locust has been reloaded (the value for the Locust pod in the AGE
column is only a few seconds).
$ kubectl get pods
NAME READY STATUS ... api-7574cf7568-jrlvd 1/1 Running ...
frontend-6688d86fc6-vd856 1/1 Running ... locust-77c699c94d-6chsg 0/1 Running ...
... RESTARTS AGE
... 0 9m57s
... 0 9m57s
... 0 6s
Return to Locust and change the parameters in these fields:
Click the Start swarming button to send traffic to Podinfo API and Podinfo Frontend.
In the Locust title bar at top left, observe how as the number of users climbs in the STATUS column, so does the value in FAILURES column. However, the errors are no longer coming from Podinfo Frontend but rather from Podinfo API because the rate limit set for the API means excessive requests are being rejected. In the trace at lower right you can see NGINX is returning the message503
Service
Temporarily
Unavailable
, which is part of the rate‑limiting feature and can be customized. The API is rate limited, and the web application is always available. Well done!
In the real world, rate limiting alone isn't enough to protect your apps and APIs from bad actors. You need to implement at least one or two of the following methods for protecting Kubernetes apps, APIs, and infrastructure:
We cover these topics and more in Unit 3 of Microservices March 2022 – Microservices Security Pattern in Kubernetes. To try NGINX Ingress Controller for Kubernetes with NGINX Plus and NGINX App Protect, start your free 30-day trial today or contact us to discuss your use cases. To try NGINX Ingress Controller with NGINX Open Source, you can obtain the release source code, or download a prebuilt container from DockerHub.
"This blog post may reference products that are no longer available and/or no longer supported. For the most current information about available F5 NGINX products and solutions, explore our NGINX product family. NGINX is now part of F5. All previous NGINX.com links will redirect to similar NGINX content on F5.com."