Welcome to another installment of the "Kubernetes in a Nutshell" blog series π In this part we will dive into Kubernetes Services. You will learn about:
- Kubernetes Services types for internal and external communication
- Techniques for service discovery within the cluster
- How to access external services etc.
As always, the code (and YAML
!) is available on GitHub
Happy to get your feedback via Twitter or just drop a comment ππ»
Kubernetes Pod
s are ephemeral i.e. they do not retain their properties across restarts or re-schedules. This applies to container storage (volume), identity (Pod
name), and even IP addresses. This poses challenges in terms of application access. Higher level abstractions like Deployment
s control several Pod
s and treat them as stateless entities - how do clients access these groups of Pod
s? Does the client need to be aware of co-ordinates of every Pod
underneath a Deployment
? Also, you cannot count on a Pod
getting the same IP after a restart - how will a client application continue to access the Pod
?
Enter Kubernetes Services!
Kubernetes Service
A Service
is a higher level component that provides access to a bunch of Pod
s. It decouples the client application from the specifics of a Deployment
(or a set of Pod
s in general) to enable predictable and stable access.
Kubernetes defines the following types of Services:
-
ClusterIP
βββfor access only within the Kubernetes cluster -
NodePort
βββaccess using IP and port of the Kubernetes Node itself -
LoadBalancer
βββan external load balancer (generally cloud provider specific) is used e.g. an Azure Load Balancer in AKS -
ExternalName
βββmaps aService
to an external DNS name
One can classify access patterns into two broad categories:
- External access
- Internal access
External Access
You can use either NodePort
or LoadBalancer
service if you want external clients to access your apps inside the Kubernetes cluster.
NodePort
NodePort
is exactly what it sounds like - makes it possible to access the app within the cluster using the IP of the Node (on which the Pod
has been scheduled) and a random port assigned by Kubernetes e.g. for a HTTP endpoint, you would use http://<node_ip>:<port>
Here is an example:
apiVersion: v1
kind: Service
metadata:
name: kin-nodeport-service
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
selector:
app: kin-service-app
Although NodePort
is conceptually quite simple, here are a few points you should note
- the random port allocation is restricted to rangeβββ
30000β32767
- the port is the same for every node in the cluster
- it is possible to specify a static port number, but the
Service
creation might fail for reasons like port allocation, invalid port, etc.
LoadBalancer
When running in a cloud provider, a LoadBalancer
service type triggers the provisioning of an external load balancer which distributes traffic amongst the backing Pod
s.
To see this in action, let's deploy an application on Azure Kubernetes Service and expose it using a LoadBalancer
service.
Using a multi-node (at least two) cloud based Kubernetes cluster makes it easy to demonstrate this concept. Feel free to use any other cloud provider (such a
GKE
to try out this scenario)
If you want to try this out using Azure, here are a few pre-requisites you should complete before going through the tutorials in this post:
- Get a free Microsoft Azure account!
- Install Azure CLI tool
-
Install
kubectl
to access your Kubernetes cluster - Setup a two-node Kubernetes cluster on Azure using the CLI
Once you've finished setting up the cluster, make sure you configure kubectl
to connect to it using the az aks get-credentials
command - this downloads credentials and configures the Kubernetes CLI to use them.
az aks get-credentials --name <AKS-cluster-name> --resource-group <AKS-resource-group>
You should be all set now. Let's start by creating the Service
along as well as the sample application.
To keep things simple, the YAML file is being referenced directly from the GitHub repo, but you can also download the file to your local machine and use it in the same way.
kubectl apply -f https://raw.githubusercontent.com/abhirockzz/kubernetes-in-a-nutshell/master/services/loadbalancer/service.yaml
kubectl apply -f https://raw.githubusercontent.com/abhirockzz/kubernetes-in-a-nutshell/master/services/app/app.yaml
the sample application is really simple Go program
To confirm that the Service
has been created
kubectl get svc/kin-lb-service
You should get back a response similar to below
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kin-lb-service LoadBalancer 10.0.149.217 <pending> 80:31789/TCP 1m
The pending
status for EXTERNAL-IP
is temporary - it's because AKS is provisioning an Azure Load Balancer behind the scenes.
After some time, you should see EXTERNAL-IP
populated with the public IP of the Load Balancer
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kin-lb-service LoadBalancer 10.0.149.217 242.42.420.42 80:31789/TCP 2m
Check that the application is deployed as well - you should see two Pod
s in Running
state
kubectl get pod -l=app=kin-service-app
NAME READY STATUS RESTARTS AGE
kin-service-app-775f989dd-gr4jk 1/1 Running 0 5m
kin-service-app-775f989dd-rmq6r 1/1 Running 0 5m
You can access the application using the load balancer IP as such:
curl http://242.42.420.42/
Note that the IP will be different in your case. Also, the load balancer port is
80
as perspec.ports.port
attribute in theService
manifest.
You should see a response similar to:
Hello from Pod IP 10.244.0.151 on Node aks-agentpool-37379363-0
This output shows:
- the IP of the
Pod
, and, - the name of the
Node
on which the Pod is present
If you try accessing the application again (curl http://242.42.420.42/
), you will most likely be load balanced to another instance Pod
which may be on a different Node
and you might see a response such as:
Hello from Pod IP 10.244.1.139 on Node aks-agentpool-37379363-1
You can scale your application (in and out) and continue to access it using the Load Balancer IP.
If you want to get the Azure Load Balancer details using the CLI, please use the following commands:
export AZURE_RESOURCE_GROUP=[enter AKS resource group]
export AKS_CLUSTER_NAME=[enter AKS cluster name]
Get the AKS cluster infra resource group name using the az aks show
command
INFRA_RG=$(az aks show --resource-group $AZURE_RESOURCE_GROUP --name $AKS_CLUSTER_NAME --query nodeResourceGroup -o tsv)
Use it to list the load balancers with the az network lb list
command (you will get back a JSON response with the load balancer information)
az network lb list -g $INFRA_RG
Internal access with ClusterIP
ClusterIP
service type and can be used communication within the cluster - just specify ClusterIP
in the spec.type
and you should be good to go!
ClusterIP
is the default service type
Even for intra-cluster communication with ClusterIP
Service
type, there has to be a way for application A
to call application B
(via the Service
). There are two ways of service discovery for apps within the cluster:
- Environment variables
DNS
Environment variables
Each Pod
is populated with a set of environment variables specific to a Service
. Let's see this in action! Create a ClusterIP
service as follows, and confirm that it has been created
kubectl apply -f https://raw.githubusercontent.com/abhirockzz/kubernetes-in-a-nutshell/master/services/clusterip/service.yaml
kubectl get svc/kin-cip-service
Remember the application we had deployed to explore LoadBalancer
Service
type? Well, delete and re-create it again (I'll explain why this was done, in a moment)
kubectl delete -f https://raw.githubusercontent.com/abhirockzz/kubernetes-in-a-nutshell/master/services/app/app.yaml
kubectl apply -f https://raw.githubusercontent.com/abhirockzz/kubernetes-in-a-nutshell/master/services/app/app.yaml
Once the app is in Running
status (check kubectl get pod -l=app=kin-service-app
), exec
(execute a command directly inside the Pod
) into the Pod
to check its environment variables
kubectl exec <enter-pod-name> -- env | grep KIN_CIP
You will see results similar to below:
KIN_CIP_SERVICE_PORT_9090_TCP_ADDR=10.0.44.29
KIN_CIP_SERVICE_SERVICE_PORT=9090
KIN_CIP_SERVICE_PORT_9090_TCP_PROTO=tcp
KIN_CIP_SERVICE_PORT_9090_TCP=tcp://10.0.44.29:9090
KIN_CIP_SERVICE_PORT=tcp://10.0.44.29:9090
KIN_CIP_SERVICE_SERVICE_HOST=10.0.44.29
KIN_CIP_SERVICE_PORT_9090_TCP_PORT=9090
Notice the format of the environment variables names? They include the name of the ClusterIP
Service
itself (i.e. kin-cip-service
) with -
replaced by _
and the rest being upper-cased. This is a pre-defined format and can be used to communicate with another application given you know the name of the Service
which backs it.
There is a caveat: a
Pod
is seeded with the environment variables only if theService
was created before it! That's the reason why we had to recreate the application to see the effect.
Let's access this application from another Pod
using the environment variables. Just run another Pod
with curl
kubectl run --rm --generator=run-pod/v1 curl --image=radial/busyboxplus:curl -i --tty
you should see a command prompt soon
Confirm that this Pod
has the environment variables as well (use env | grep KIN_CIP
) and then simply curl
the application endpoint using the environment variables
curl http://$KIN_CIP_SERVICE_SERVICE_HOST:$KIN_CIP_SERVICE_SERVICE_PORT
You should see the same response as in the case of LoadBalancer
example, i.e.
Hello from Pod IP 10.244.0.153 on Node aks-agentpool-37379363-0
Try it a few more times to confirm that the load is getting balanced among the individual Pods! So we derived the environment variables based on the Service
name i.e. kin-cip-service
was converted to KIN_CIP_SERVICE
and the rest of the parts were added - _SERVICE_HOST
and _SERVICE_PORT
for the host and port respectively.
DNS
Kubernetes has a built-in DNS server (e.g. CoreDNS
) which maintains DNS records for each Service
. Just like environment variables, DNS technique provides a consistent naming scheme based on which you can access applications given you know their Service
name (and other info like namespace
if needed).
The good thing is that this technique does not depend on the order of
Serivce
andPod
creation as was the case with environment variables
You can try it right away:
Run the curl
Pod
again
kubectl run --rm --generator=run-pod/v1 curl --image=radial/busyboxplus:curl -i --tty
To access the application:
curl http://kin-cip-service.default.svc.cluster.local:9090
Everything should work the same way! The FQDN format is <service-name>.<namespace>.<cluster-domain-suffix>
. In our case, it maps to:
-
service-name
-kin-cip-service
-
namespace
-default
-
cluster-domain-suffix
-svc.cluster.local
For applications in the same
namespace
you can actually skip most of this and just use the service name!
Mapping external services
There are cases where you are required to refer to external services from applications inside your Kubernetes cluster. You can do this in a couple of ways
- In a static manner using
Endpoints
resource - Using the
ExternalName
service type
Static Endpoints
It's a good time to reveal that Kubernetes creates an Endpoints
resource for every Service
you create (if you use a selector
which is mostly the case except for few scenarios). It's the Endpoints
object which actually captures the IPs of the backing Pod
s. You can look at existing ones using kubectl get endpoints
.
In case you want to access an external service, you need to:
- create a
Service
without anyselector
- Kubernetes will NOT create anEndpoints
object - manually create the
Endpoints
resource corresponding to yourService
(with the same name) and IP/port of the service you want to access.
This gives you the same benefits i.e. you can keep the Service
constant and update the actual entity behind the scenes if needed. As an example, we will abstract access to a public HTTP endpoint http://demo.nats.io:8222/
using this technique.
Start by creating the Service
kubectl apply -f https://raw.githubusercontent.com/abhirockzz/kubernetes-in-a-nutshell/master/services/external/static/service.yaml
Here is what the Service
looks like. Notice that we are mapping port
8080
to the actual (target) port we want to access 8222
kind: Service
apiVersion: v1
metadata:
name: demo-nats-public-service
spec:
type: ClusterIP
ports:
- port: 8080
targetPort: 8222
Let's look at the Endpoints
resource.
kind: Endpoints
apiVersion: v1
metadata:
name: demo-nats-public-service
subsets:
- addresses:
- ip: 107.170.221.32
ports:
- port: 8222
Notice the following:
- name of the resource (
demo-nats-public-service
) is the same as theService
- we've used the
subsets
attribute to specify theip
and theport
(we found theip
backingdemo.nats.io
by simply usingping demo.nats.io
)
Create it using:
kubectl apply -f https://raw.githubusercontent.com/abhirockzz/kubernetes-in-a-nutshell/master/services/external/static/endpoints.yaml
That's it! Let's see if this works. Simply run a curl
Pod
kubectl run --rm --generator=run-pod/v1 curl --image=radial/busyboxplus:curl -i --tty
Now, all we need to use is the name of our Service
i.e. demo-nats-public-service
(along with the port 8080
) and it will do the trick.
curl http://demo-nats-public-service:8080
You should see the following response (this is the same as if you had browsed to http://demo.nats.io:8222
)
<html lang="en">
<head>
<link rel="shortcut icon" href="http://nats.io/img/favicon.ico">
<style type="text/css">
body { font-family: "Century Gothic", CenturyGothic, AppleGothic, sans-serif; font-size: 22; }
a { margin-left: 32px; }
</style>
</head>
<body>
<img src="http://nats.io/img/logo.png" alt="NATS">
<br/>
<a href=/varz>varz</a><br/>
<a href=/connz>connz</a><br/>
<a href=/routez>routez</a><br/>
<a href=/gatewayz>gatewayz</a><br/>
<a href=/leafz>leafz</a><br/>
<a href=/subsz>subsz</a><br/>
<br/>
<a href=https://docs.nats.io/nats-server/configuration/monitoring.html>help</a>
</body>
</html>
ExternalName
ExternalName
is another Service
type which can be used to map a Service
to a DNS name. Note that this is a DNS name and not an IP/port combination as was the case with the above strategy (using manually created Endpoints
).
In the Serivce
manifest:
- don't include a
selector
- use
externalName
attribute to specify DNS name e.g.test.example.com
- using IP addresses is not allowed
Here is an example:
apiVersion: v1
kind: Service
metadata:
name: demo-nats-public-service2
spec:
type: ExternalName
externalName: demo.nats.io
We are creating a Service
with the name demo-nats-public-service2
which maps to DNS name demo.nats.io
using the spec.type
which is ExternalName
.
It works the same way i.e. you need to use the Service
name to access the external entity. The only difference (compared to the manual Endpoints
approach) is that you'll need to know the port as well. To try this:
Create the Service
kubectl apply -f https://raw.githubusercontent.com/abhirockzz/kubernetes-in-a-nutshell/master/services/external/external-name/service.yaml
Simply run a curl
Pod
kubectl run --rm --generator=run-pod/v1 curl --image=radial/busyboxplus:curl -i --tty
Now, all we need to use is the name of our Service
i.e. demo-nats-public-service2
(along with port 8222
)
curl http://demo-nats-public-service2:8222
You should see the same response as the previous scenario
Headless
Service
You can get load balancing among a group of Pod
s using LoadBalancer
, NodePort
and ClusterIP
Service types. A Headless
Service
allows access to individual Pod
s - there is no proxying involved in this case. This is useful in many scenarios e.g.
- consider a peer-to-peer system where individual instances (Pods) will need to access each other.
- a master-follower style service where follower instances need to be aware of the master Pod
In order to create a Headless
Service
, you need to explicitly specify None
as a value for .spec.clusterIP
. Here is an example:
apiVersion: v1
kind: Service
metadata:
name: kin-hl-service
spec:
clusterIP: None
ports:
- port: 9090
targetPort: 8080
selector:
app: kin-service-app
The way you use a Headless Service is different compared to other types. A DNS lookup against the Service (e.g. <service-name>.<namespace>.svc.cliuster.local
) returns multiple IPs corresponding to different Pods
(as compared to a single virtual IP in case of other Service types). Let's see this in action
Create the Service
kubectl apply -f https://raw.githubusercontent.com/abhirockzz/kubernetes-in-a-nutshell/master/services/headless/service.yaml
Run the curl
Pod
kubectl run --rm --generator=run-pod/v1 curl --image=radial/busyboxplus:curl -i --tty
Check the backing Pod
IPs
nslookup kin-hl-service.default.svc.cluster.local
You will get a response similar to the below
[ root@curl:/ ]$ nslookup kin-hl-service
Server: 10.0.0.10
Address 1: 10.0.0.10 kube-dns.kube-system.svc.cluster.local
Name: kin-hl-service
Address 1: 10.244.0.153 10-244-0-153.kin-hl-service.default.svc.cluster.local
Address 2: 10.244.1.141 10-244-1-141.kin-hl-service.default.svc.cluster.local
10-244-0-153.kin-hl-service.default.svc.cluster.local
and 10.244.1.141 10-244-1-141.kin-hl-service.default.svc.cluster.local
correspond to co-ordinates of the individual Pod
s - this is not possible with a traditional Service
type. You can now use this to access specific Pod
e.g.
curl http://10-244-0-153.kin-hl-service.default.svc.cluster.local:8080
//response
Hello from Pod IP 10.244.0.153 on Node aks-agentpool-37379363-0
curl http://10-244-1-141.kin-hl-service.default.svc.cluster.local:8080
//response
Hello from Pod IP 10.244.1.141 on Node aks-agentpool-37379363-1
Ingress
We did cover the basics of Kubernetes Service
, but I do want to highlight Ingress
which deserves a separate post altogether. Ingress
is not a Service
type (such as ClusterIP
etc.) - think of as an abstraction on top of a Service
. Just like Service
s front end a bunch of Pod
s, an Ingress
can be configured to work with several backing Service
s and forward the requests as per rules which you can define.
The intelligence provided by an Ingress
is actually implemented in the form of an Ingress
Controller. For example, Minikube comes with an NGINX based Ingress Controller. The controller is responsible for providing access to the appropriate backing Service
after evaluation of the Ingress
rules.
That's it for this part of the "Kubernetes in a Nutshell" series. Stay tuned for more!
Friendly reminder if you are interested in learning Kubernetes and Containers using Azure! Simply create a free account and get going! A good starting point is to use the quickstarts, tutorials and code samples in the documentation to familiarize yourself with the service. I also highly recommend checking out the 50 days Kubernetes Learning Path. Advanced users might want to refer to Kubernetes best practices or watch some of the videos for demos, top features and technical sessions.
I really hope you enjoyed and learned something from this article π Please like and follow if you did!
Top comments (0)