loading...
Microsoft Azure

Learn how to access your Kubernetes applications using Services

abhirockzz profile image Abhishek Gupta ・Updated on ・13 min read

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 Pods 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 Deployments control several Pods and treat them as stateless entities - how do clients access these groups of Pods? 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 Pods. It decouples the client application from the specifics of a Deployment (or a set of Pods 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 a Service 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 Pods.

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:

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 Pods 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 per spec.ports.port attribute in the Service 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 the Service 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 and Pod 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 Pods. 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 any selector - Kubernetes will NOT create an Endpoints object
  • manually create the Endpoints resource corresponding to your Service (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 the Service
  • we've used the subsets attribute to specify the ip and the port (we found the ip backing demo.nats.io by simply using ping 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 Pods using LoadBalancer, NodePort and ClusterIP Service types. A Headless Service allows access to individual Pods - 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 Pods - 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 Services front end a bunch of Pods, an Ingress can be configured to work with several backing Services 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!

Discussion

pic
Editor guide