Jérôme Decoster

Jérôme Decoster

3x AWS Certified - Architect, Developer, Cloud Practionner

11 Apr 2022

Kubernetes + Init Container Pattern + Minikube

The Goal
  • Run a voting application using an express website and redis
  • Run locally using docker images and docker-compose
  • Run locally using minikube
  • Explore the Init Container pattern

    architecture.svg

    Setup the project

    Get the code from this github repository :

    # download the code
    $ git clone \
        --depth 1 \
        https://github.com/jeromedecoster/k8s-init-container-pattern.git \
        /tmp/note
    
    # cd
    $ cd /tmp/note
    

    The Init Container pattern

    The Init Container pattern is the first of the structural models described in the Bilgin Ibryam and Roland Huß book

    book.jpg

    Kubernetes manages pods instead of containers and pods encapsulate containers. A pod may contain one or more containers.

    A pod that contains one container refers to a single container pod and it is the most common kubernetes use case.

    A pod that contains Multiple co-related containers refers to a multi-container pod.

    Init Containers are the containers that should run and complete before the startup of the main container in the pod. It provides a separate lifecycle for the initialization.

    • Init containers always run to completion.
    • It is possible to have 1 or more init containers.
    • Each init container must complete successfully before the next one starts.
    • So, init containers are executed sequentially, one after another.

    In the field of container orchestration, the ability to wait for an initialization phase is essential.

    The initContainers property is therefore part of the syntax of kubernetes manifests :

    apiVersion: v1
    kind: Pod
    metadata:
      name: myapp-pod
      labels:
        app: myapp
    spec:
      containers:
      - name: myapp-container
        image: busybox:1.28
        command: # ...
      initContainers:
      - name: init-myservice
        image: busybox:1.28
        command: # ...
    

    Init containers are used to :

    • Wait for a service to be available before launching a container that depends on it
    • Installing software
    • Setup a database
    • Setting permissions on the file system

    Run the projet locally

    The project is a simple voting application. A website that uses a Redis database.

    project.png

    To test our application we launch redis via a docker image :

    # run redis alpine docker image
    $ make redis
    
    1:C # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
        # ...
    1:M # * Ready to accept connections
    

    This command executes this script :

    $ docker run \
        --rm \
        --name redis \
        --publish 6379:6379 \
        redis:alpine
    

    We will interact with redis in a new terminal window :

    # connect to redis container
    $ CONTAINER_ID=$(docker ps --filter name=redis --format {{.ID}} | head -n 1)
    $ docker exec -ti $CONTAINER_ID /bin/sh
    
    /data # redis-cli KEYS '*'
    (empty array)
    /data # redis-cli incr up
    (integer) 1
    /data # redis-cli KEYS '*'
    1) "up"
    /data # redis-cli --raw get up
    1
    /data # exit
    
    # close redis container
    docker rm --force $CONTAINER_ID
    

    Test the website in development mode (without redis)

    Make sure Redis isn’t already running by closing all previous instances.

    We will test the website without Redis being active.

    # run local website using npm - dev mode (livereload + nodemon)
    $ make npm
    

    The site launches with a connection error :

    env.NODE_ENV: development
    env.WEBSITE_PORT: 3000
    env.REDIS_HOST: 127.0.0.1
    listening on port 3000
    [ioredis] Unhandled error event: Error: connect ECONNREFUSED 127.0.0.1:6379
        at TCPConnectWrap.afterConnect [as oncomplete] (node:net:1158:16)
    

    This error is generated by trying to connect to the Redis instance :

    const REDIS_HOST = process.env.REDIS_HOST || 'redis'
    
    // ...
    
    const redis = new Redis({
        port: 6379,
        host: REDIS_HOST
    })
    

    You can end the process with Ctrl+C

    Test the website in development mode (with redis)

    We will now launch the applications in the correct order, via different terminal windows :

    # run redis alpine docker image
    $ make redis
    
    # run local website using npm - dev mode (livereload + nodemon)
    $ make npm
    

    The output is :

    env.NODE_ENV: development
    env.WEBSITE_PORT: 3000
    env.REDIS_HOST: 127.0.0.1
    listening on port 3000
    

    We open http://localhost:3000

    localhost.png

    The test is conclusive. Both processes can be terminated.

    Local test with docker-compose

    Before testing in minikube, it’s a good idea to use docker-compose to orchestrate the containers that make up the application locally. It’s a good step.

    The docker-compose.yml file is quite simple :

    services:
      vote:
        build:
          context: ./vote
          dockerfile: Dockerfile.dev
          args:
            - NODE_ENV=development
            - WEBSITE_PORT=4000
        volumes:
          - "./vote:/app"
        ports:
          - "4000:4000"
          - "35729:35729"
        networks:
          - backend
        depends_on:
          - redis
        
      redis:
        image: redis:alpine
        ports:
          - "6379:6379"
        networks:
          - backend
    
    networks:
      backend: {}
    

    We execute the following command :

    # run the project using docker-compose (same as redis + npm)
    $ make compose-dev
    

    This command runs this script :

    $ export COMPOSE_PROJECT_NAME=k8s_init_container
    $ docker-compose \
        --file docker-compose.dev.yml \
        up \
        --remove-orphans \
        --force-recreate \
        --build \
        --no-deps
    

    The output is :

    redis_1  | 1:C 31 Mar 2022 08:25:22.753 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
    redis_1  | 1:C 31 Mar 2022 08:25:22.753 # Redis version=6.2.6, bits=64, commit=00000000, modified=0, pid=1, just started
    # ...
    vote_1   | env.NODE_ENV: development
    vote_1   | env.WEBSITE_PORT: 4000
    vote_1   | env.REDIS_HOST: undefined
    vote_1   | listening on port 4000
    

    We open http://localhost:4000

    project.png

    The test is conclusive. We can complete the process.

    Using mikinube

    We start minikube :

    $ minikube start --driver=docker
    

    If minikube won’t start with a PROVIDER_DOCKER_NOT_RUNNING error or another error, try a hard reset :

    $ minikube delete -p minikube
    

    We check the context :

    $ kubectl config current-context
    minikube
    

    We launch the Kubernetes dashboard in a new terminal window :

    # new terminal window
    $ minikube dashboard
    

    minikube-ns-1.png

    To be able to use our local docker images within minikube it is imperative to :

    1. Target the local docker registry
    2. Rebuild the docker images in this registry
    3. use the imagePullPolicy: Never attribute in kubernetes manifests
    # 1. target the local docker registry
    $ eval $(minikube -p minikube docker-env)
    
    # 2. rebuild the docker images in this registry
    # /!\ must be in the same terminal window than previous command
    $ make docker-build
    # ...
    Successfully tagged site:latest
    

    The attribute is visible below :

    spec:
        containers:
        - name: vote
        image: site:latest
        env:
        - name: REDIS_HOST
            value: "redis-svc"
        ports:
        - containerPort: 3000
            name: vote 
        # 3. use the attribute
        imagePullPolicy: Never
    

    We create the kubernetes namespace with the ns.yaml template :

    $ kubectl apply -f k8s/ns.yaml
    namespace/k8s-init-container created
    

    minikube-ns-2.png

    We launch the vote application with the vote.yaml template :

    $ kubectl apply -f k8s/vote.yaml
    deployment.apps/vote created
    service/vote-svc created
    

    Note that the Redis instance has not been started.

    We can query the init-vote container logs :

    $ kubectl logs -l app=vote -c init-vote -n k8s-init-container
    nc: bad address 'redis-svc'
    Waiting for redis-svc
    

    The init-vote container is a busybox that runs a shell script that loops until the redis service is available :

    initContainers:
        - name: init-vote
        image: busybox:latest
        command: ['sh', '-c', 'until nc -vz redis-svc 6379 ; do echo "Waiting for redis-svc"; sleep 1; done;']
    

    The current status shown in the Replica Sets panel :

    vote.png

    In a new terminal window we connect within the cluster via another busybox :

    $ kubectl run -i --tty --rm busybox \
        --image=busybox \
        --namespace=k8s-init-container \
        --restart=Never \
        -- sh
    /
    

    The current status shown in the Pods panel :

    busybox-pod.png

    We will query the state of the vote-svc service from our busybox :

    vote-svc.png

    We can use nc :

    / nc -vz vote-svc 9000
    # processus completed but nothing is returned
    

    Nothing is returned but the processus is completed

    If we test a bad name, we received an error :

    / nc -vz vote-something 9000
    nc: bad address 'vote-something'
    

    If we test a bad port, we received nothing :

    / nc -vz vote-svc 90
    # processus not completed ... waiting
    

    Nothing is returned and the processus is not completed

    We can use nslookup :

    / nslookup -type=srv vote-svc.k8s-init-container.svc.cluster.local
    Server:         10.96.0.10
    Address:        10.96.0.10:53
    
    vote-svc.k8s-init-container.svc.cluster.local   service = 0 100 9000 vote-svc.k8s-init-container.svc.cluster.local
    

    If we test a bad name, we received :

    / nslookup -type=srv vote-something.k8s-init-container.svc.cluster.local
    Server:         10.96.0.10
    Address:        10.96.0.10:53
    
    ** server cant find vote-something.k8s-init-container.svc.cluster.local: NXDOMAIN
    

    In a new terminal window we now launch the redis instance with the redis.yaml template :

    $ kubectl apply -f k8s/redis.yaml
    

    We test the service with nc :

    / nc -vz redis-svc 6379
    redis-svc (10.107.142.90:6379) open
    

    Or with nslookup :

    / # nslookup -type=srv redis-svc.k8s-init-container.svc.cluster.local
    Server:         10.96.0.10
    Address:        10.96.0.10:53
    
    redis-svc.k8s-init-container.svc.cluster.local  service = 0 100 6379 redis-svc.k8s-init-container.svc.cluster.local
    

    To test our site we search the access URL via the following command :

    $ minikube service list
    |----------------------|---------------------------|--------------|---------------------------|
    |      NAMESPACE       |           NAME            | TARGET PORT  |            URL            |
    |----------------------|---------------------------|--------------|---------------------------|
    | default              | kubernetes                | No node port |                           |
    | k8s-init-container   | redis-svc                 | No node port |                           |
    | k8s-init-container   | vote-svc                  |         9000 | http://192.168.49.2:31000 |
    | kube-system          | kube-dns                  | No node port |                           |
    | kubernetes-dashboard | dashboard-metrics-scraper | No node port |                           |
    | kubernetes-dashboard | kubernetes-dashboard      | No node port |                           |
    |----------------------|---------------------------|--------------|---------------------------|
    

    It is possible to use get it directly like this :

    $ minikube service --url vote-svc --namespace k8s-init-container
    http://192.168.49.2:31000
    

    By opening the URL we see that the website is working correctly :

    minikube-website.png

    The demonstration is over, we can destroy our resources.