Blog
December 4, 2016 Marie H.

Getting started with Microservices and K8S

Getting started with Microservices and K8S

Photo by <a href="https://www.pexels.com/@brett-sayles" target="_blank" rel="noopener">Brett Sayles</a> on <a href="https://www.pexels.com" target="_blank" rel="noopener">Pexels</a>

In this I want to showcase how you can keep things simple with microservices and highlight everything you need to know about getting started with microservices and kubernetes.

With this tutorial I am going to take my blog, wordpress php monolith, and rip apart the only components I need and make them microservices. Then, we will go through setting up a local development kubernetes with minikube.

First things first, lets think about the services we need.

Decoupling the monolith backend

It’s a blog so I think its fairly easy to decouple what we need. Here’s a simple list of the services we are going to create.

  • Posts

  • Tags

  • Categories

  • API Gateway

Services I did not mention

Yeah, that was a short list huh? Well this is by design. Those are the bare minimum pieces I need for my blog to work. The reason is when possible I try to offset things to other services. This allows for focusing on core business needs and functionality instead of on components that can easily be stood up without much work. Here is some of the components I can offset.

Functionality Service
Stats and Analytics Google Analytics
Caching CDN Provider
Comments / Discussion Disqus
Sharing / Marketing AddThis

This is a key component to microservices. Don’t reinvent the wheel like I am doing here in this tutorial haha. But in general if there is a service that is affordable or free and available you can spend more time on writing business logic.

Writing our services

For the most part these are mock services that don't do much but general separation of concerns so you can see at a high level what a microservice system looks like.

Posts

Super simple RESTful CRUD service.

app.py
"""
Post Service App Setup
"""
from flask import Flask, request
from flask_restful import Resource, Api
from flask_sqlalchemy import SQLAlchemy

# Handle Requests
class Posts(Resource):
    """
    Handles all HTTP methods for Post Service
    """

    def get(self, post_uuid=None):
        """
        Retreive all posts or a given post on uuid
        :param post_uuid: UUID
        :return: Dict
        """
        if post_uuid:
            return get_post(post_uuid)
        return get_all_posts()

    def post(self):
        """
        Create a new post
        """
        post_data = request.get_json(force=True)
        return create_post(post_data)

    def put(self, post_uuid):
        """
        Update a post
        :param post_uuid: UUID
        :return: Boolean
        """
        return update_post(post_uuid)

    def delete(self, post_uuid):
        """
        Delete a post
        :param post_uuid: UUID
        :return: Boolean
        """
        return delete_post(post_uuid)

# Functions
def get_post(post_uuid):
    """
    Pull data from db for a blog post
    :param post_uuid: UUID
    :return: Dict
    """
    # Mock Data
    return {
        "post_uuid": post_uuid,
        "title": "I'm a blog post!",
        "content": "Yes, this is a blog post about things and stuff but mainly about showing how to do microservices in a simple and easy way",
        "author": "Marie H.",
        "created": "12-03-2016 12:15:00",
        "updated": ""
    }

def get_all_posts():
    """
    Pull all posts from db
    :return: List of Dicts
    """
    # Mock Data
    return [
        {
            "post_uuid": "1234-5678-1234-5678",
            "title": "I'm a blog post!",
            "content": "A post about foo",
            "author": "Marie H.",
            "created": "12-03-2016 12:15:17",
            "updated": ""
        },
        {
            "post_uuid": "2345-6789-2345-6789",
            "title": "I'm a different blog post!",
            "content": "A different post about bar",
            "author": "Marie H.",
            "created": "12-03-2016 12:18:33",
            "updated": ""
        }
    ]

def create_post(post_data):
    """
    Create a new post
    :param post_data: Dict
    :return: Boolean
    """
    # Mock Data
    return True

def update_post(post_uuid):
    """
    Update a given post
    :param post_uuid: UUID
    :return: Boolean
    """
    # Mock Data
    return True

def delete_post(post_uuid):
    """
    Delete a given post
    :param post_uuid: UUID
    :return: Boolean
    """
    # Mock Data
    return True

if __name__ == "__main__":
    # Setup App
    app = Flask(__name__)
    app.config.from_object('config')
    api = Api(app)
    db = SQLAlchemy(app)
    # Setup Routes
    api.add_resource(
        Posts,
        '/v1/post',
        '/v1/post/',
    )
    # Run the App
    app.run(host="0.0.0.0")
models.py
from datetime import datetime
import uuid
class Posts(db.Model):
    """
    Our Database Model
    """
    id = db.Column(db.Integer, primary_key=True)
    post_uuid = db.Column(db.String(50))
    title = db.Column(db.String(100))
    content = db.Column(db.Text)
    author = db.Column(db.String(50))
    created = db.Column(db.DateTime)
    updated = db.Column(db.DateTime)
    def __init__(self, title, content, author):
        self.post_uuid = uuid.uuid4()
        self.title = title
        self.content = content
        self.author = author
        self.created = datetime.utcnow()
    def set_update_time(self):
        self.updated = datetime.utcnow()
config.py
"""
All of our App Configuration
"""
class Config(object):
    """
    Configuration Object
    """
    # DB Vars
    db_user = 'dbuser'
    db_pass = 'dbpassword'
    db_host = 'localhost'
    db_name = 'posts'
    # DB Settings
    SQLALCHEMY_TRACK_MODIFICATIONS = True
    SQLALCHEMY_DATABASE_URI = 'mysql://{db_user}:{db_pass}@{db_host}/{db_name}'.format(
  db_user=db_user,
  db_pass=db_pass,
  db_host=db_host,
        db_name=db_name
    )
    SQLALCHEMY_POOL_SIZE = 10
    SQLALCHEMY_POOL_TIMEOUT = 10
    SQLALCHEMY_POOL_RECYCLE = 500
requirements.txt
aniso8601==1.2.0
click==6.6
Flask==0.11.1
Flask-RESTful==0.3.5
Flask-SQLAlchemy==2.1
itsdangerous==0.24
Jinja2==2.8
MarkupSafe==0.23
python-dateutil==2.6.0
pytz==2016.7
six==1.10.0
SQLAlchemy==1.1.4
Werkzeug==0.11.11

If you don't know the first thing about setting up a Python application, don't worry we will build and deploy the application by the end of this article.

Tags

One of the great things about microservices is that it allows you to use the right tool for the job and doesn't limit you to a specific language, framework, technology, or infrastructure. So this service will be implemented in node.

tags.js
'use strict';
const Hapi = require('hapi');
const Boom = require('boom');
/*
 * Setup Server
 */
var server = new Hapi.Server();
server.connection({port: 5001});
/*
 * Setup Routes
 */
server.route({
  method: ['GET', 'POST', 'PUT', 'DELETE'],
  path: '/v1/tag/{tag_uuid?}',
  handler: function(request, reply) {
    /*
     * Request logging
     */
    var timestamp = new Date(request.info.received);
    console.log(`${timestamp} - ${request.info.remoteAddress} - ${request.method.toUpperCase()} ${request.info.host}${request.path}`);
    /*
     * Handle each method
     * with mock responses
     */
    switch ( request.method ) {
      /*
       * GET Handling
       */
      case 'get':
        if ( request.params.tag_uuid ) {
          return reply({
            "tag_uuid": request.params.tag_uuid,
            "name": "Sample",
            "id": 1,
            "type": 1
          })
        }
        return reply([
            {
              "tag_uuid": "1234-5667-1234-1234",
              "name": "Test",
              "id": 2,
              "type": 1
            },
            {
              "tag_uuid": "4321-1234-4321-1234",
              "name": "Javascript",
              "id": 3,
              "type": 1
            }
        ]);
        break;
      /*
       * POST Handling
       */
      case 'post':
        if ( request.params.tag_uuid ) {
          return reply(Boom.badRequest('Unsupported method'));
        }
        return reply(true);
        break;
      /*
       * PUT Handling
       */
      case 'put':
        if ( request.params.tag_uuid ) {
          return reply(true);
        }
        return reply(Boom.badRequest('Unsupported method'));
        break;
      /*
       * DELETE Handling
       */
      case 'delete':
        if ( request.params.tag_uuid ) {
          return reply(true);
        }
        return reply(Boom.badRequest('Unsupported method'));
        break;
    }
  }
});
/*
 * Start Server
 */
server.start(() => {
  console.log(`started at ${server.info.uri}`);
});
/*
 * Shutdown Server
 */
function shutdown() {
  server.stop(() => console.log('shutdown complete'));
}
process
.once('SIGINT', shutdown)
.once('SIGTERM', shutdown);
package.json
{
  "name": "tags",
  "version": "1.0.0",
  "description": "A Sample Tags Microservice",
  "main": "tags.js",
  "dependencies": {
    "boom": "^4.2.0",
    "hapi": "^16.0.1"
  },
  "devDependencies": {},
  "scripts": {
    "start": "node tags.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "Marie H.",
  "license": "ISC"
}

Categories

Yes! That’s right folks. You can write microservices in PERL!!! Perl is not dead, in fact it is alive and well. Just do a quick google search for Perl6.

categories.pl
use Mojolicious::Lite;
# A Perl Microservice Sample
# perl categories.pl daemon -m production -l http://*:8080
# 
# Setup Routes
#
get '/v1/category/:category_uuid' => 
sub {
    shift->render(
            json => {
                "id" => 1,
                "name" => "Perl",
                "category_uuid" => "1234-1234-1234-1234"
            }
        );
};
put '/v1/category/:category_uuid' =>
sub {
    shift->render(
        text => 'true'
    ); 
};
del '/v1/category/:category_uuid' =>
sub {
    shift->render(
        text => 'true'
    );
};
post '/v1/category' =>
sub {
    shift->render(
        text => 'true'
    );
};
get '/v1/category' => 
sub {
    shift->render(
        json => [
            {
                "id" => 1,
                "name" => "Perl",
                "category_uuid" => "1234-1234-1234-1234"
            },
            {
                "id" => 2,
                "name" => "Python",
                "category_uuid" => "4321-4321-4321-4321"
            }
        ]
    );
};
#
# Exception Handling
#
sub handle404 {
    my $self = shift;
    $self->rendered(404);
    $self->render(
        json => {
            "statusCode" => 404,
            "message" => "404 Not Found"
        }
    );
}
any '*' => &handle404;
any '/' => &handle404;

API Gateway

An API Gateway is a single source that is going to sit in front of all of our services and and handle routing requests to the proper service. Typically it would also handle common things such as authentication and authorization; however that is outside the scope of this tutorial.

main.go

Golang, a fast performant language. It can also handle a crap ton of requests! Maybe not the most elegant implementation but it works.

package main
import "log"
import "net/http/httputil"
import "net/http"
import "net/url"
import "regexp"
func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        uri := r.RequestURI
        // Pass request to post service
        post_matched, _ := regexp.MatchString("/v1/post.*", uri)
        if post_matched {
            proxy := reverseProxy("http://post-service/")
            proxy.ServeHTTP(w, r)
        } 

        // Pass request to category service
        category_matched, _ := regexp.MatchString("/v1/category.*", uri)
        if category_matched {
            proxy := reverseProxy("http://category-service/")
            proxy.ServeHTTP(w, r)
        } 

        // Pass request to tag service
        tag_matched, _ := regexp.MatchString("/v1/tag.*", uri)
        if tag_matched {
            proxy := reverseProxy("http://tag-service/")
            proxy.ServeHTTP(w, r)
        }
        log.Printf("%s %s %s%s", r.Header.Get("X-Forwarded-For"), r.Method, r.Host, r.RequestURI)
    })
    log.Fatal(http.ListenAndServe(":80", nil))
}
func reverseProxy(target string) *httputil.ReverseProxy {
    url, _ := url.Parse(target)
    return httputil.NewSingleHostReverseProxy(url)
}
build.sh
CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o gateway .

Now we have all the pieces of our mock application. Time to containerize and setup our development environment on minikube.

Setup development environment

Dockerizing applications

Below you will find the dockerfiles for each of the service written above.

Posts
FROM python:2-onbuild
CMD ["python", "./app.py"]
Tags
FROM node:5-onbuild
Categories
FROM perl:5.20
RUN curl -L https://cpanmin.us | perl - -M https://cpan.metacpan.org -n Mojolicious
COPY categories.pl /usr/src/app/categories.pl
WORKDIR /usr/src/app
CMD ["perl", "./categories.pl", "daemon", "-m", "category-service", "-l", "http://*:80"]
API Gateway
FROM scratch
ADD gateway /
CMD ["/gateway"]

Now that were all dockerized; let go ahead and setup are configuration files for kubernetes for each service.

Kubernetes configuration files

Updated 2026-03-20: The extensions/v1beta1 API group was removed in Kubernetes 1.22. All Deployments below have been updated to apps/v1, which requires an explicit selector.matchLabels field. Also note: python:2-onbuild and node:5-onbuild base images are EOL — use python:3.12-slim and node:20-alpine for new projects.

For a deeper dive into configuration see: https://kubernetes.io/docs/home/?path=users&persona=app-developer&level=foundational

Otherwise, here is the gist of what we are configuring for each service. Kubernetes has many kinds of objects that you can setup; in this tutorial we will only focus on deployments and services. A deployment configures other foundational kubernetes objects to support the running and maintenance of a container. A service exposes said deployment to the network. That's the skinny, again if you want to know more; check out the docs.

posts.yml
apiVersion: v1
kind: Service
metadata: 
  name: post-service
spec:
  ports:
    - port: 80
      targetPort: 5000
      name: http
  selector:
    name: post-service
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: post-service
spec:
  selector:
    matchLabels:
      name: post-service
  template:
    metadata:
      labels:
        name: post-service
    spec:
      containers:
        - name: post-service
          image: morissette/post-service
          ports:
            - containerPort: 5000
tags.yml
apiVersion: v1
kind: Service
metadata: 
  name: tag-service
spec:
  ports:
    - port: 80
      targetPort: 5001
      name: http
  selector:
    name: tag-service
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tag-service
spec:
  selector:
    matchLabels:
      name: tag-service
  template:
    metadata:
      labels:
        name: tag-service
    spec:
      containers:
        - name: tag-service
          image: morissette/tag-service
          ports:
            - containerPort: 5001
categories.yml
apiVersion: v1
kind: Service
metadata: 
  name: category-service
spec:
  ports:
    - port: 80
      name: http
  selector:
    name: category-service
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: category-service
spec:
  selector:
    matchLabels:
      name: category-service
  template:
    metadata:
      labels:
        name: category-service
    spec:
      containers:
        - name: category-service
          image: morissette/category-service
          ports:
            - containerPort: 80
gateway.yml
apiVersion: v1
kind: Service
metadata: 
  name: gateway-service
spec:
  type: NodePort
  ports:
    - port: 80
      name: http
  selector:
    name: gateway-service
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gateway-service
spec:
  selector:
    matchLabels:
      name: gateway-service
  template:
    metadata:
      labels:
        name: gateway-service
    spec:
      containers:
        - name: gateway-service
          image: morissette/gateway-service
          ports:
            - containerPort: 80

Setup local kubernetes cluster

minikube, is a package that allows you to setup a small virtual environment on your local machine running kubernetes.

Install minikube
Linux
curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.12.2/minikube-linux-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/
OSX
curl -Lo minikube https://storage.googleapis.com/minikube/releases/v0.12.2/minikube-darwin-amd64 && chmod +x minikube && sudo mv minikube /usr/local/bin/
Windows

I haven't touched a Windows box in years. Try: https://google.com

Install kubectl

kubectl, is a package that provides a CLI interface to the kubernetes API.

Create cluster

Unless you are using a different VM driver you will need to ensure you have VirtualBox and it's respective kernel modules installed.

[mharris@mori devops]$ minikube start
Starting local Kubernetes cluster...
Kubectl is now configured to use the cluster.
Set docker env for minikube

This command will set your local docker image repository to use minikube (so kubernetes knows where to pull your docker images).

eval $(minikube docker-env)
Build docker images

For all of the dockerfiles created above, after you run the docker-env command above. Run:

docker build -t <service-name> -f Dockerfile .

A pod is a kubernetes object that encompasses a collection of containers. Typically a pod only executes a single container. You can check the state of your services using the following kubectl command.

kubectl get pods

Initially the output should look like:

NAME                               READY     STATUS              RESTARTS   AGE
category-service-367250328-xxqhx   0/1       ContainerCreating   0          1m
gateway-service-3188116052-b34ev   0/1       ContainerCreating   0          1m
post-service-1383581485-g6ec9      0/1       ContainerCreating   0          1m
tag-service-746243472-1t71s        0/1       ContainerCreating   0          1m

However we want to wait for all of the pods to enter the READY state.

[mharris@mori devops]$ kubectl get pods
NAME                               READY     STATUS    RESTARTS   AGE
category-service-367250328-xxqhx   1/1       Running   0          4m
gateway-service-3188116052-b34ev   1/1       Running   0          4m
post-service-1383581485-g6ec9      1/1       Running   0          4m
tag-service-746243472-1t71s        1/1       Running   0          4m

Now that we have successfully built and deployed our microservice application; lets see some basic testing to make sure its running properly.

Test basics

First, lets get the exposed IP and port of the gateway service from minikube.

[mharris@mori devops]$ minikube service -n default --url gateway-service
http://192.168.99.100:30647

Now lets test some of our routes and look at the logs.

Can I curl services?
[mharris@mori devops]$ curl http://192.168.99.100:30647/v1/category
[{"category_uuid":"1234-1234-1234-1234","id":1,"name":"Perl"},{"category_uuid":"4321-4321-4321-4321","id":2,"name":"Python"}]
[mharris@mori devops]$ curl http://192.168.99.100:30647/v1/tag
[{"tag_uuid":"1234-5667-1234-1234","name":"Test","id":2,"type":1},{"tag_uuid":"4321-1234-4321-1234","name":"Javascript","id":3,"type":1}]
[mharris@mori devops]$ curl http://192.168.99.100:30647/v1/post
[{"updated": "", "post_uuid": "1234-5678-1234-5678", "author": "Marie H.", "created": "12-03-2016 12:15:17", "content": "A post about foo", "title": "I'm a blog post!"}, {"updated": "", "post_uuid": "2345-6789-2345-6789", "author": "Marie H.", "created": "12-03-2016 12:18:33", "content": "A different post about bar", "title": "I'm a different blog post!"}]
What do the logs show?
[mharris@mori devops]$ kubectl logs -f gateway-service-3188116052-b34ev
2016/12/04 00:19:45 172.17.0.1 GET 192.168.99.100:30647/v1/category
2016/12/04 00:19:56 172.17.0.1 GET 192.168.99.100:30647/v1/tag
2016/12/04 00:20:00 172.17.0.1 GET 192.168.99.100:30647/v1/post
[mharris@mori devops]$ kubectl logs -f tag-service-746243472-vqzv8
npm info it worked if it ends with ok
npm info using npm@3.8.6
npm info using node@v5.12.0
npm info lifecycle tags@1.0.0~prestart: tags@1.0.0
npm info lifecycle tags@1.0.0~start: tags@1.0.0
> tags@1.0.0 start /usr/src/app
> node tags.js
started at http://tag-service-746243472-vqzv8:5001
Sun Dec 04 2016 00:19:56 GMT+0000 (UTC) - 172.17.0.5 - GET 192.168.99.100:30647/v1/tag

Further reading

This is meant to be a mere primer. There is a lot more involved with microservices but hopefully this allows you to get your feet wet.

Check out the following links for more information: