In one of the previous posts, we have learned how to expose arbitrary ports to the outside world using services and load balancers. However, we also found that this is not very efficient – in the worst case, the number of load balancers we need equals the number of services.
Specifically for HTTP/HTTPS traffic, there is a different option available – an ingress rule.
Ingress rules and ingress controllers
An ingress rule is a Kubernetes resource that defines a kind of routing on the HTTP(S) path level. To illustrate this, assume that you have two services running, call them svcA and svcB. Assume further that both services work with HTTP as the underlying protocol and are listening on port 80. If you expose these services naively as in the previous post, you will need two load balancers, call them LB1 and LB2. Then, to reach svcA, you would use the URL
and to access svcB, you would use
The idea of an ingress is to have only one load balancer, with one DNS entry, and to use the path name to route traffic to our services. So with an ingress, we would be able to reach svcA under the URL
and the second service under the URL
With this approach, you have several advantages. First, you only need one load balancer that clients can use for all services. Second, the path name is related to the service that you invoke, which follows best practises and makes coding against your services much easier. Finally, you can easily add new services and remove old services without a need to change DNS names.
In Kubernetes, two components are required to make this work. First, there are ingress rules. These are Kubernetes resources that contain rules which specify how to route incoming requests based on their path (or even hostname). Second, there are ingress controllers. These are controllers which are not part of the Kubernetes core software, but need to be installed on top. These controllers will work with the ingress rules to manage the actual routing. So before trying this out, we need to install an ingress controller in our cluster.
Installing an ingress controller
On EKS, there are several options for an ingress controller. One possible choice is the nginx ingress controller. This is the controller that we will use for our examples. In addition, AWS has created their own controller called AWS ALB controller that you could use as an alternative – I have not yet looked at this in detail, though.
So let us see how we can install the nginx ingress controller in our cluster. Fortunately, there is a very clear installation instruction which tells us that to run the install, we simply have to execute a number of manifest files, as you would expect for a Kubernetes application. If you have cloned my repository and brought up your cluster using the up.sh script, you are done – this script will set up the controller automatically. If not, here are the commands to do this manually.
$ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/mandatory.yaml $ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/provider/aws/service-l4.yaml $ kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/provider/aws/patch-configmap-l4.yaml
Let us go through this and see what is going on. The first file (mandatory.yaml) will set up several config maps, a service account and a new role and connect the service account with the newly created role. It then starts a deployment to bring up one instance of the nginx controller itself. You can see this pod running if you do a
kubectl get pods --namespace ingress-nginx
The first AWS specific manifest file service-l4.yaml will establish a Kubernetes service of type LoadBalancer. This will create an elastic load balancer in your AWS VPC. Traffic on the ports 80 and 443 is directed to the nginx controller.
kubectl get svc --namespace ingress-nginx
Finally, the second AWS specific file will update the config map that stores the nginx configuration and set the parameter use-proxy-protocol to True.
To verify that the installation has worked, you can use aws elb describe-load-balancers
to verify that a new load balancer has been created and curl the DNS name provided by
kubectl get svc ingress-nginx --namespace ingress-nginx
from your local machine. This will still give you an error as we have not yet defined ingress rule, but show that the ingress controller is up and running.
Setting up and testing an ingress rule
Having our ingress controller in place, we can now set up ingress rules. To have a toy example at hand, let us first apply a manifest file that will
- Add a deployment of two instances of the Apache httpd
- Install a service httpd-service accepting traffic for these two pods on port 8080
- Add a deployment of two instances of Tomcat
- Create a service tomcat-service listening on port 8080 and directing traffic to these two pods
You can either download the file here or directly use the URL with kubectl
$ kubectl apply -f https://raw.githubusercontent.com/christianb93/Kubernetes/master/network/ingress-prep.yaml deployment.apps/httpd created deployment.apps/tomcat created service/httpd-service created service/tomcat-service created
When all pods are up and running, you can again spawn a shell in one of the pods and use the DNS name entries created by Kubernetes to verify that the services are available from within the pods.
$ pod=$(kubectl get pods --output \ jsonpath="{.items[0].metadata.name}") $ kubectl exec -it $pod "/bin/bash" bash-4.4# apk add curl bash-4.4# curl tomcat-service:8080 bash-4.4# curl httpd-service:8080
Let us now define an ingress rule which directs requests to the path /httpd
to our httpd service and correspondingly requests to /tomcat
to the Tomcat service. Here is the manifest file for this rule.
apiVersion: extensions/v1beta1 kind: Ingress metadata: name: test-ingress annotations: nginx.ingress.kubernetes.io/rewrite-target : "/" spec: rules: - http: paths: - path: /tomcat backend: serviceName: tomcat-service servicePort: 8080 - path: /alpine backend: serviceName: alpine-service servicePort: 8080
The first few lines are familiar by now, specifying the API version, the type of resource that we want to create and a name. In the metadata section, we also add an annotation. The nginx ingress controller can be configured using this and similar annotations (see here for a list), and this annotation is required to make the example work.
In the specification section, we now define a set of rules. Our rule is a http rule, which, at the time of writing, is the only supported protocol. This is followed by a list of paths. Each path consists of a selector (“/httpd” and “/tomcat” in our case), followed by the specification of a backend, i.e. a combination of service name and service port, serving requests matching this path.
Next set up the ingress rule. Assuming that you have saved the manifest file above as ingress.yaml, simply run
$ kubectl apply -f ingress.yaml ingress.extensions/test-ingress created
Now let us try this. We already know that the ingress controller has created a load balancer for us which serves all ingress rules. So let us get the name of this load balancer from the service specification and then use curl to try out both paths
$ host=$(kubectl get svc ingress-nginx -n ingress-nginx --output\ jsonpath="{.status.loadBalancer.ingress[0].hostname}") $ curl -k https://$host/httpd $ curl -k https://$host/tomcat
The first curl should give you the standard output of the httpd service, the second one the standard Tomcat welcome page. So our ingress rule works.
Let us try to understand what is happening. The load balancer is listening on the HTTPS port 443 and picking up the traffic coming from there. This traffic is then redirected to a host port that you can read off from the output of aws elb describe-load-balancers
, in my case this was 31135. This node port belongs to the service ingress-nginx that our ingress controller has set up. So the traffic is forwarded to the ingress controller. The ingress controller interprets the rules, determines the target service and forwards the traffic to the target service. Ignoring the fact that the traffic goes through the node port, this gives us the following simplified picture.
In fact, this diagram is a bit simplified as (see here) the controller does not actually send the traffic to the service cluster IP, but directly to the endpoints, thus bypassing the kube-proxy mechanism, so that advanced features like session affinity can be applied.
Ingress rules have many additional options that we have not yet discussed. You can define virtual hosts, i.e. routing based on host names, define a default backend for requests that do not match any of the path selectors, use regular expressions in your paths and use TLS secrets to secure your HTTPS entry points. This is described in the Kubernetes networking documentation and the API reference.
Creating ingress rules in Python
To close this post, let us again study how to implement Ingress rules in Python. Again, it is easiest to build up the objects from the bottom to the top. So we start with our backends and the corresponding path objects.
tomcat_backend=client.V1beta1IngressBackend( service_name="tomcat-service", service_port=8080) httpd_backend=client.V1beta1IngressBackend( service_name="httpd-service", service_port=8080)
Having this, we can now define our rule and our specification section.
rule=client.V1beta1IngressRule( http=client.V1beta1HTTPIngressRuleValue( paths=[tomcat_path, httpd_path])) spec=client.V1beta1IngressSpec() spec.rules=[rule]
Finally, we assemble our ingress object. Again, this consists of the metadata (including the annotation) and the specification section.
ingress=client.V1beta1Ingress() ingress.api_version="extensions/v1beta1" ingress.kind="Ingress" metadata=client.V1ObjectMeta() metadata.name="test-ingress" metadata.annotations={"nginx.ingress.kubernetes.io/rewrite-target" : "/"} ingress.metadata=metadata ingress.spec=spec
We are now ready for our final steps. We again read the configuration, create an API endpoint and submit our creation request. You can find the full script including comments and all imports here
config.load_kube_config() apiv1beta1=client.ExtensionsV1beta1Api() apiv1beta1.create_namespaced_ingress( namespace="default", body=ingress)