Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Create Versatile Microservices in Golang — Part 8 (Kubernetes and Container Engine)

DZone's Guide to

Create Versatile Microservices in Golang — Part 8 (Kubernetes and Container Engine)

Today we'll learn about using Container Engine and Kubernetes to deploy containers to the container engine cluster we built with Terraform.

· Microservices Zone ·
Free Resource

Learn the Benefits and Principles of Microservices Architecture for the Enterprise

In the previous post, we looked at creating a container engine cluster with Terraform. In this post, we'll look at deploying containers into our cluster using Container Engine and Kubernetes.

Kubernetes

Firstly, what is Kubernetes? Kubernetes is an open-source, container management framework. It is platform agnostic, meaning you can run this on your local machine, on AWS, on Google Cloud, or anywhere else. It gives you control over groups of containers and their network rules, using declerative configuration.

You simply write yaml/json files which describe which containers should be running, and where. You define your networking rules, such as any port forwarding. It also handles service discovery for you.

Kubernetes is one of the most important additions to the cloud scene and is quickly becoming the defacto choice for cloud container management. So this is a good one to understand.

So let's get started!

First, ensure you have the kubectl CLI installed locally:

$ gcloud components install kubectl 

Now let's ensure you're connected to your cluster and correctly authenticated. First, we'll log in and ensure we're authenticated. Second, we'll set the project settings to ensure we're using the correct project ID and availability zone.

$ echo "This command will open a web browser, and will ask you to login
$ gcloud auth application-default login 

$ gcloud config set project shippy-freight
$ gcloud config set compute/zone eu-west2-a

$ echo "Now generate a security token and access to your KB cluster"
$ gcloud container clusters get-credentials shippy-freight-cluster

In the above commands, you may need to replace the compute/zone with whichever region you've chosen, your project id and cluster names may also differ to mine.

Here's an outline...

$ echo "This command will open a web browser, and will ask you to login
$ gcloud auth application-default login 

$ gcloud config set project <project-id>
$ gcloud config set compute/zone <availability-zone>

$ echo "Now generate a security token and access to your KB cluster"
$ gcloud container clusters get-credentials <cluster-name>

Your project ID can be found by clicking here...

Now look for your project ID...

Your clusters region/zone and cluster name can be found by clicking the 'Compute Engine' in the left side menu, then click "VM Instances." You'll see your Kubernetes VM, click into that for more details, and you should be able to find every piece of information regarding your cluster here.

So now, if you run...

$ kubectl get pods 

You should see... No resources found.. That's fine, we haven't deployed anything yet. So let's start thinking about what it is we actually need to deploy. We need a MongoDB instance. Typically, you'd deploy a MongoDB instance, or database instance along with every service, for complete separation. But for this example, we're going to cheat and just stick with one centralized instance. This is a single point of failure, however, so in a real-world scenario, you might want to consider splitting out your database instances more in line with your services. However, our way is also okay.

Then we need to deploy our services, vessel-service, user-service, consignment-service, and email-service. Okay, that's easy enough!

Let's start with our MongoDB instance. As this doesn't belong to a single service, and this is part of the platform as a whole, we'll keep these deployments in our shippy-infrastructure repo (which I've omitted from GitHub as it contains too much sensitive data), but I'll give you my full deployment files.

First, we need a config to create an ssd, for long-term storage. This is so that when our containers restart, they don't lose all of their data.

// shippy-infrastructure/deployments/mongodb-ssd.yml
kind: StorageClass
apiVersion: storage.k8s.io/v1beta1
metadata:
  name: fast
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-ssd

Now our deployment file (we'll go into these in a little more detail throughout the post)...

// shippy-infrastructure/deployments/mongodb-deployment.yml
apiVersion: apps/v1beta1
kind: StatefulSet
metadata:
  name: mongo
spec:
  serviceName: "mongo"
  replicas: 3
  selector:
    matchLabels:
      app: mongo
  template:
    metadata:
      labels:
        app: mongo
        role: mongo
    spec:
      terminationGracePeriodSeconds: 10
      containers:
        - name: mongo
          image: mongo
          command:
            - mongod
            - "--replSet"
            - rs0
            - "--smallfiles"
            - "--noprealloc"
            - "--bind_ip"
            - "0.0.0.0"
          ports:
            - containerPort: 27017
          volumeMounts:
            - name: mongo-persistent-storage
              mountPath: /data/db
        - name: mongo-sidecar
          image: cvallance/mongo-k8s-sidecar
          env:
            - name: MONGO_SIDECAR_POD_LABELS
              value: "role=mongo,environment=test"
  volumeClaimTemplates:
  - metadata:
      name: mongo-persistent-storage
      annotations:
        volume.beta.kubernetes.io/storage-class: "fast"
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
          storage: 10Gi

And a service file...

apiVersion: v1
kind: Service
metadata:
  name: mongo
  labels:
    name: mongo
spec:
  ports:
  - port: 27017
    targetPort: 27017
  clusterIP: None
  selector:
    role: mongo

A lot of that, at this point, might be meaningless to you. So let's try and clear some of the key concepts up in Kubernetes.

Nodes

Read the docs here. Nodes are your VMs or physical servers, your containers are clustered across your nodes, and services are used to expose access between the groups of containers running on each node/pod.

Pods

Read the docs here. Pods are groups of related containers. For instance, a pod could contain your auth-service container, your user database container, your login signup user interface etc. These containers are all clearly related. Pods allow you to group them together so that they always have access to one another, are running in the same immediate network space, and allow you to scale them as a group. This is really cool! Pods are a very misunderstood feature in Kubernetes.

Deployments

Read the docs here. Deployments are state controllers, a deployment is a description of what the final outcome should be, and what it should be kept at. A deployment is an instruction to Kubernetes, saying, for example, I would like three of these containers, running on these ports, with these environment variables. Kubernetes will ensure that that state is maintained. If one of the containers crashes and goes down to two containers, it will bring up another to meet the demand for three containers.

StatefulSet

Read the docs here. A stateful set is very similar to a deployment, except it will retain, using some kind of storage, some state regarding the containers. This is ideal for sharded datstores for example.

Under the hood, MongoDB writes data to a binary datastore format, most databases do something of this nature. In creating a database instance within something disposable, such as a Docker container. Your data will be lost if the container restarts. Traditionally you would use volumes to mount the data/files on container start.

You can do this with deployments in Kubernetes, but StatefulSets has some additional automation around this with regards to clustering nodes, so it's a more natural fit for our MongoDB containers.

Services

Read the docs here. A service is a grouping of network rules, such as port forwarding and DNS rules; which connects your pods together at a network level, and controls who can talk to who, or what can be accessed from the outside world.

There are two main types of service that you'll likely come across, a load balancer and a node port.

A load balancer is a round-robin load balancer, which creates an IP address to proxy against your selected node. You would use this to expose a service out to the public.

A node port exposes pods to the top-level network space, so that they can be accessed by other services, internally to other pods/instances. These are useful for exposing nodes out to other pods. This is the one you'd use to allow services to communicate with one another. This is your service discovery in essence. Or at least a part of it.

This has been a very whistle-stop tour of Kubernetes, there's a lot more to it, keep digging and reading. It's worth noting also that if you're a Docker user on your local computer, if you're using edge version of Docker for Mac/Windows, for example, you can spin up a Kubernetes cluster locally on your machine. This is really useful for testing.

So we've created three files, one for storage, one for our stateful set, and one for our service. The outcome of this will be a replicated set of MongoDB containers, with stateful storage and a service exposing the datastore across our other pods. Let's go ahead and create these, be sure to do this in the correct order, as some depend on each other.

echo "shippy-infrastructure"
$ kubectl create -f ./deployments/mongodb-ssd.yml
$ kubectl create -f ./deployments/mongodb-deployment.yml
$ kubectl create -f ./deployments/mongodb-service.yml

Give these a few moments, but you can check the status of your MongoDB containers, by running:

$ kubectl get pods 

You might notice one of your pods is "pending." If you run $ kubectl describe node you might see an error regarding insufficient CPU. Unfortunately, some of the cluster management and Kubernetes tooling can be quite CPU intensive on its own. So one node might not be enough for that as well as a clustered mongo instance.

So we're going to turn auto-scaling on for our cluster, which defaults to one pool. In order to do this, head over to your Google Cloud Console, select Kubernetes engine, edit your instance, turn on auto-scaling and set the min to one and the max to two, and hit save.

After a few minutes, your nodes will scale to two and you'll see "ContainerCreating" when you run $ kubectl get pods until all of your containers are running as expected.

Now that we have our database cluster and an auto-scaling Kubernetes engine, let's start deploying some services!

Vessel Service

The vessel service is very lightweight, it doesn't do too much, or depend on anything else, so that seems like a good place to start.

First of all, we need to make a few small code changes to our vessel-service code.

// shippy-vessel-service/main.go
import (

    ...
    k8s "github.com/micro/kubernetes/go/micro"   
)

func main() {
    ... 
    // Replace existing service var with... 
    srv := k8s.NewService(
micro.Name("shippy.vessel"),
micro.Version("latest"),
)
}

All we've done here is replace the existing micro.NewService() with this new library we imported, k8s.NewService(). So what's this new library?

Micro on Kubernetes

One of the things I love about micro is that it was built with a great understanding of the cloud and has adapted along the way to new technologies. Micro has really taken Kubernetes seriously, and so, have created a Kubernetes library for micro.

Under the hood, all this library really is, is micro, configured with a sensible set of defaults for Kubernetes, and a service selector which integrates directly on-top of Kubernetes services. Which means it offloads service discovery entirely to Kubernetes. It also the default to use gRPC as the default transport. Of course, you can override these behaviors using environment variables and plugins, too.

I can also say that there's some really exciting work in this space coming up from Micro, that I'm super excited about. Make sure you get involved on the Slack channel!

Now, let's create a deployment file for our service, we'll go into a little more detail this time about what each section does.

// shippy-vessel-service/deployments/deployment.yml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: vessel
spec:
  replicas: 1
  selector:
    matchLabels:
      app: vessel
  template:
    metadata:
      labels:
        app: vessel
    spec:
        containers:
        - name: vessel-service
          image: eu.gcr.io/<project-name>/vessel-service:latest
          imagePullPolicy: Always
          command: [
            "./shippy-vessel-service",
            "--selector=static",
            "--server_address=:8080",
          ]
          env:
          - name: DB_HOST
            value: "mongo:27017"
          - name: UPDATED_AT
            value: "Mon 19 Mar 2018 12:05:58 GMT"
          ports:
          - containerPort: 8080
            name: vessel-port

There's a lot going on here, but I'll try and break some of it down a little more. First, you'll see kind: Deployment, there are many different "things" in Kubernetes, of which can almost be viewed as "cloud primitives." In programming languages, you have strings, integers, structs, methods, etc. These are primatives. Well, Kubernetes views the cloud in the same way. So view these as your primatives. In this case, a deployment, which is a form of controller primitive. Controllers ensure that your desired state is maintained correctly. A deployment is a form of stateless controller, they are ephemeral, things aren't lost of disrupted as they restart, or exit. Stateful sets are like deployments, except they maintain some static data and state as explained earlier. But our services shouldn't contain any state, microservices should be stateless. Thus, we're using a deployment here.

Next you have a spec block, this starts with some metadata about the deployment, its name, how many of these pods (replicas), should it maintain (if one of these die, assuming you're using more than one, it's the controllers job to check the desired number of pods are running, and starts another to maintain the desired state if not). Selectors and templates expose some metadata about this pod, which allow it to be found and connected to by services.

Then you have another spec block (this is kinda confusing, but hey!). This time for our individual containers, or volumes, shared metadata etc. In this service, we're just spinning up a single container. The containers field is an array, this is because we can start several containers as part of a pod. The point is to group related containers.

The container metadata here is pretty self-explanatory, we're starting a Docker container, from an image, we're setting some environment variables, passing in some commands at runtime, and exposing a port (which our service will look for).

You'll notice I passed in a new command: --selector=static, this tells the Kubernetes micro set-up to use Kubernetes for its service discovery and load-balancing. This is really cool because now your micro code is interacting directly with Kubernete's powerful DNS, networking, load-balancing, and service discovery.

You can omit this option and continue to use micro as previous. But we might as well get the benefits of Kubernetes here.

You'll also notice we're pulling our Docker image from a private repository. When you use Google's container tools, you get a private container registry, which you can use by building your Docker image, and pushing it, like so...

$ docker build -t eu.gcr.io/<your-project-name>/vessel-service:latest .
$ gcloud docker -- push eu.gcr.io/<your-project-name>/vessel-service:latest

Now for our service...

// shippy-vessel-service/deployments/service.yml
apiVersion: v1
kind: Service
metadata:
  name: vessel
  labels:
    app: vessel
spec:
  ports:
  - port: 8080
    protocol: TCP
  selector:
    app: vessel

Here, as mentioned above we have a kind, which in this case is a service primitive (a group of network level DNS and firewall rules essentially). Then we give the service a name and a label. The spec allows us to define a port for the service, and you can also define a targetPort here to look for a specific container. But we're doing this automatically thanks to the Kubernetes/micro implementation. Then finally, the selector, this is very important, this must match the name of the pod you want it to target; otherwise, the service won't be able to find anything to proxy, and it won't work.

Now, let's deploy these changes to our cluster.

$ kubectl create -f ./deployments/deployment.yml
$ kubectl create -f ./deployments/service.yml

Give it a few seconds, then run...

$ kubectl get pods
$ kubectl get services

And you should be able to see your new pod and your new service. Ensure these are running as expected.

If you do see an error, you can run $ kubectl proxy, then open http://localhost:8001/ui in your browser, to see the Kubernetes ui, this will give you the ability to explore, in-depth, the state of your containers etc.

One thing worth mentioning here is that deployments are atomistic and immutable, meaning they have to be changed in some way to be updated. They are turned into a unique hash, and if that hash isn't changed, then the deployment won't be updated.

If you run $ kubectl replace -f ./deployments/deployment.yml, nothing will happen. This is because Kubernetes has detected that nothing's changed.

There are many ways of getting around this, but it's worth noting that for the most part, it will be your container that will have changed, so instead of using the label latest, you should give each container a unique label, such as a build number, for example: vessel-service:<build-no>. This will be picked up as a change and the deployment will be replaced.

But in this tutorial, we're going to do something a little naughty, but be wary that this is lazy, and not particularly best practice. I've created a new file deployments/deployment.tmpl, which will act as the base deployment template. Then I've set an environment variable called UPDATED_AT, with a value of {{ UPDATED_AT }}. I've updated the Makefile to open this template file, sed the current date/time over the value of that environment variable, and output it to a final deployment.yml file. This is a bit of a hack, but it will do for now. I've seen various ways of doing this, do whatever you feel is appropriate for you.

// shippy-vessel-service/Makefile
deploy:
sed "s/{{ UPDATED_AT }}/$(shell date)/g" ./deployments/deployment.tmpl > ./deployments/deployment.yml
kubectl replace -f ./deployments/deployment.yml

So there we have it, that's one service deployed and running as expected!

I'm now going to do the same for our other services. I'll update the repo's for each of those for brevity, please take a look here...

Postgres deployment for our user service...

apiVersion: apps/v1beta2
kind: StatefulSet
metadata:
  name: postgres
spec:
  serviceName: postgres
  selector:
    matchLabels:
      app: postgres
  replicas: 3
  template:
    metadata:
      labels:
        app: postgres
        role: postgres
    spec:
      terminationGracePeriodSeconds: 10
      containers:
        - name: postgres
          image: postgres
          ports:
            - name: postgres
              containerPort: 5432
          volumeMounts:
            - name: postgres-persistent-storage
              mountPath: /var/lib/postgresql/data
  volumeClaimTemplates:
  - metadata:
      name: postgres-persistent-storage
      annotations:
        volume.beta.kubernetes.io/storage-class: "fast"
    spec:
      accessModes: [ "ReadWriteOnce" ]
      resources:
        requests:
         storage: 10Gi

Postgres service...

apiVersion: v1
kind: Service
metadata:
  name: postgres
  labels:
    app: postgres
spec:
  ports:
  - name: postgres
    port: 5432
    targetPort: 5432
  clusterIP: None
  selector:
    role: postgres

Postgres storage...

kind: StorageClass
apiVersion: storage.k8s.io/v1beta1
metadata:
  name: fast
provisioner: kubernetes.io/gce-pd
parameters:
  type: pd-ssd

Deploying Micro

// shippy-infrastructure/deployments/micro-deployment.yml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: micro
spec:
  replicas: 3
  selector:
    matchLabels:
      app: micro
  template:
    metadata:
      labels:
        app: micro
    spec:
        containers:
        - name: micro
          image: microhq/micro:kubernetes
          args:
            - "api"
            - "--handler=rpc"
            - "--namespace=shippy"
          env:
          - name: MICRO_API_ADDRESS
            value: ":80"
          ports:
          - containerPort: 80
            name: port

And now a service...

// shippy-infrastructure/deployments/micro-service.yml
apiVersion: v1
kind: Service
metadata:
  name: micro
spec:
  type: LoadBalancer
  ports:
  - name: api-http
    port: 80
    targetPort: "port"
    protocol: TCP
  selector:
    app: micro

In our service here, we're using a LoadBalancer type, which exposes an external load balancer with an IP address out to the public. If you run $ kubectl get services, after a minute or two (you'll see "pending" for a while), you'll get an IP address. This is public, and you can assign to a domain name, etc.

Once all that's deployed, make a service call to micro:

$ curl localhost/rpc -XPOST -d '{
    "request": { 
        "name": "test", 
        "capacity": 200, 
        "max_weight": 100000, 
        "available": true 
    },
    "method": "VesselService.Create",
    "service": "vessel"
}' -H 'Content-Type: application/json'

You should see a response, with "created: true." Pretty neat! That's your gRPC services, being proxied and converted to a web-friendly format, using a sharded, MongoDB instance. Not a huge amount of effort!

Deploying the UI

That's our services deployed and happy — let's deploy our user interface.

// shippy-ui/deployments/deployment.yml
apiVersion: apps/v1beta1
kind: Deployment
metadata:
  name: ui
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ui
  template:
    metadata:
      labels:
        app: ui
    spec:
        containers:
        - name: ui-service
          image: ewanvalentine/ui:latest
          imagePullPolicy: Always
          env:
          - name: UPDATED_AT
            value: "Tue 20 Mar 2018 08:26:39 GMT"
          ports:
          - containerPort: 80
            name: ui

Now our service...

// shippy-ui/deployments/service.yml
apiVersion: v1
kind: Service
metadata:
  name: ui
  labels:
    app: ui
spec:
  type: LoadBalancer
  ports:
  - port: 80
    protocol: TCP
    targetPort: 'ui'
  selector:
    app: ui

You'll notice this service is a load balancer on port 80, that's because this is a public user interface, this is how our users interact with our services. Fairly self-explanatory!

Wrapping Up

So there we have it, we've successfully ported and deployed our entire stack to the cloud using Docker containers and Kubernetes to manage our containers. Hopefully, you can see the value in this and didn't find it too overwhelming.

Next in the series, we'll look at hooking all of this up into a CI process in order to manage our deployments.

Sponsor me on Patreon to support more content like this.

Microservices for the Enterprise eBook: Get Your Copy Here

Topics:
microservices ,go ,golang ,tutorial ,kubernetes ,containers ,deployment

Published at DZone with permission of

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}