KUBERNETES , JAVA , LINUX , OPENSHIFT

An Introduction to Running Java on Kubernetes

#kubectl , #pod , #deployment

An Introduction to Running Java on Kubernetes

You will need access to a container build tool, a container runtime environment, and a kubernetes environment. There will be suggestions along the way for acquiring these. You will need a virtualization technology to run the minikube or minishift VMs for your kubernetes development environment.

Build a Demo Spring Boot App

  1. Build an app

    $ mkdir -p my-java-app/container
    $ cd my-java-app
    $ git clone https://github.com/spring-guides/gs-spring-boot.git
    $ cd gs-spring-boot/complete
    $ ./mvn package
    $ cd ../../
    $ mv gs-spring-boot/complete/target/spring-boot-0.0.1-SNAPSHOT.jar container/spring-boot-0.0.1-SNAPSHOT.jar
  2. Test the app locally to make sure it built right and works!

    $ java -jar container/spring-boot-0.0.1-SNAPSHOT.jar
    $ curl http://localhost:8080

OpenShift

Build an OpenShift Development Environment

$ minishift start

Or CodeReady Containers

$ crc setup
$ crc start
$ oc new-project java-fun
$ oc new-build --name myapp .
$ oc start-build myapp --from-dir=. --follow
$ oc new-app myapp
$ oc logs -f myapp-1-kbqsf
$ oc create service clusterip myapp --tcp=8080:8080
$ oc expose service myapp
$ oc get pod/myapp-1-kbqsf -o yaml | less

Kubernetes

  1. Create the components of your container image

    $ ls
    container  gs-spring-boot-master
    
    $ cd container
    
    $ echo 'FROM openjdk:11
    
    COPY spring-boot-0.0.1-SNAPSHOT.jar /spring-boot-0.0.1-SNAPSHOT.jar
    
    CMD java -jar /spring-boot-0.0.1-SNAPSHOT.jar' > Dockerfile
    
    $ ls
    Dockerfile spring-boot-0.0.1-SNAPSHOT.jar
  2. Build the container image

    $ docker build . --tag myapp
    Sending build context to Docker daemon 19.29 MB
    Step 1/4 : FROM openjdk:11
     ---> a548e8a50190
    Step 2/4 : COPY spring-boot-0.0.1-SNAPSHOT.jar /spring-boot-0.0.1-SNAPSHOT.jar
     ---> 512817bf40b1
    Removing intermediate container ca9e8a40576b
    Step 3/4 : CMD java -jar /spring-boot-0.0.1-SNAPSHOT.jar
     ---> Running in 8c978eb3bfc3
     ---> 4dd65bb22f39
    Removing intermediate container 8c978eb3bfc3
    Step 4/4 : LABEL "myapp" ''
     ---> Running in ed2ed797b412
     ---> e2a1859a0bb8
    Removing intermediate container ed2ed797b412
    Successfully built e2a1859a0bb8
  3. Tag the image to a registry you have access to pull from

    $ docker images | grep myapp
    myapp                                                               latest              e2a1859a0bb8        About a minute ago   646 MB
    $ docker tag myapp quay.io/bward/myapp:latest
    $ docker images | grep myapp
    myapp                                                               latest              e2a1859a0bb8        3 minutes ago       646 MB
    quay.io/bward/myapp                                                 latest              e2a1859a0bb8        3 minutes ago       646 MB
  4. Push the image to the remote repository from which your image will pull.

    $ docker login quay.io
    Username: bward
    Password:
    Login Succeeded
    
    $ docker push quay.io/bward/myapp
    The push refers to a repository [quay.io/bward/myapp]
    86fdd08f4977: Pushed
    3fb986725e60: Pushed
    a6170c7cf572: Pushed
    33783834b288: Pushed
    5c813a85f7f0: Pushed
    bdca38f94ff0: Pushed
    faac394a1ad3: Pushed
    ce8168f12337: Pushed
    latest: digest: sha256:268ee7f59472b1a34d374cbdaa3176f73986f404a3771d0a5a5cc84ac4a5e789 size: 2006
  5. Create a Kubernetes dev environment.[1]

    $ curl -Lo minikube https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64 \
    >   && chmod +x minikube
      % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                     Dload  Upload   Total   Spent    Left  Speed
    100 46.7M  100 46.7M    0     0  7054k      0  0:00:06  0:00:06 --:--:-- 6249k
    
    $ install minikube ~/bin/
    
    $ minikube start --vm-driver=kvm2
    😄  minikube v1.7.2 on Fedora 30
    ✨  Using the kvm2 driver based on user configuration
    💾  Downloading driver docker-machine-driver-kvm2:
        > docker-machine-driver-kvm2.sha256: 65 B / 65 B [-------] 100.00% ? p/s 0s
        > docker-machine-driver-kvm2: 13.82 MiB / 13.82 MiB  100.00% 8.36 MiB p/s 2
    💿  Downloading VM boot image ...
        > minikube-v1.7.0.iso.sha256: 65 B / 65 B [--------------] 100.00% ? p/s 0s
        > minikube-v1.7.0.iso: 166.68 MiB / 166.68 MiB [-] 100.00% 9.22 MiB p/s 19s
    🔥  Creating kvm2 VM (CPUs=2, Memory=2000MB, Disk=20000MB) ...
    🐳  Preparing Kubernetes v1.17.2 on Docker 19.03.5 ...
    💾  Downloading kubelet v1.17.2
    💾  Downloading kubeadm v1.17.2
    💾  Downloading kubectl v1.17.2
    🚀  Launching Kubernetes ...
    🌟  Enabling addons: default-storageclass, storage-provisioner
    ⌛  Waiting for cluster to come online ...
    🏄  Done! kubectl is now configured to use "minikube"
    
    $ minikube status
    host: Running
    kubelet: Running
    apiserver: Running
    kubeconfig: Configured
    
    $ kubectl version
    Client Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.2", GitCommit:"59603c6e503c87169aea6106f57b9f242f64df89", GitTreeState:"clean", BuildDate:"2020-01-18T23:30:10Z", GoVersion:"go1.13.5", Compiler:"gc", Platform:"linux/amd64"}
    Server Version: version.Info{Major:"1", Minor:"17", GitVersion:"v1.17.2", GitCommit:"59603c6e503c87169aea6106f57b9f242f64df89", GitTreeState:"clean", BuildDate:"2020-01-18T23:22:30Z", GoVersion:"go1.13.5", Compiler:"gc", Platform:"linux/amd64"}
    
    $ kubectl get namespaces
    NAME              STATUS   AGE
    default           Active   3m26s
    kube-node-lease   Active   3m28s
    kube-public       Active   3m28s
    kube-system       Active   3m28s
    
    $ kubectl create namespace java-fun-k8s
    namespace/java-fun created
    
    $ kubectl get ns
    NAME              STATUS   AGE
    default           Active   3m49s
    java-fun-k8s      Active   8s
    kube-node-lease   Active   3m51s
    kube-public       Active   3m51s
    kube-system       Active   3m51s
    
    $ kubectl config set-context --current --namespace=java-fun-k8s
  6. Create a pod from a pod spec.

    $ echo 'apiVersion: v1
    kind: Pod
    metadata:
      generateName: myapp-
      labels:
        app: myapp
      namespace: java-fun-k8s
    spec:
      containers:
      - image: quay.io/bward/myapp
        imagePullPolicy: Always
        name: myapp' | kubectl create -f -

    In execution:

    $  echo 'apiVersion: v1
    > kind: Pod
    > metadata:
    >   generateName: myapp-
    >   labels:
    >     app: myapp
    >   namespace: java-fun-k8s
    > spec:
    >   containers:
    >   - image: quay.io/bward/myapp
    >     imagePullPolicy: Always
    >     name: myapp' | kubectl create -f -
    pod/myapp-wc7b9 created
    
    $ kubectl get pods
    NAME          READY   STATUS    RESTARTS   AGE
    myapp-wc7b9   1/1     Running   0          63s
    
    $ kubectl logs myapp-wc7b9 | tail -3
    webServerFactoryCustomizerBeanPostProcessor
    websocketServletWebServerCustomizer
    welcomePageHandlerMapping
  7. Create a set of pods from a deployment spec.

    $ echo 'apiVersion: apps/v1
    kind: Deployment
    metadata:
      labels:
        app: myapp
      name: myapp
      namespace: java-fun-k8s
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: myapp
          deployment: myapp
      template:
        metadata:
          labels:
            app: myapp
            deployment: myapp
        spec:
          containers:
          - image: quay.io/bward/myapp
            imagePullPolicy: Always
            name: myapp' | kubectl create -f -

    In Execution:

    $ echo 'apiVersion: apps/v1
    > kind: Deployment
    > metadata:
    >   labels:
    >     app: myapp
    >   name: myapp
    >   namespace: java-fun-k8s
    > spec:
    >   replicas: 3
    >   selector:
    >     matchLabels:
    >       app: myapp
    >       deployment: myapp
    >   template:
    >     metadata:
    >       labels:
    >         app: myapp
    >         deployment: myapp
    >     spec:
    >       containers:
    >       - image: quay.io/bward/myapp
    >         imagePullPolicy: Always
    >         name: myapp' | kubectl create -f -
    deployment.apps/myapp created
    
    $ kubectl get pods
    NAME                   READY   STATUS    RESTARTS   AGE
    myapp-5b9dbdbb-fpqtl   1/1     Running   0          40s
    myapp-5b9dbdbb-gcvlg   1/1     Running   0          40s
    myapp-5b9dbdbb-zgtcj   1/1     Running   0          40s
    myapp-wc7b9            1/1     Running   0          63m
    
    $ kubectl get all
    NAME                       READY   STATUS    RESTARTS   AGE
    pod/myapp-5b9dbdbb-fpqtl   1/1     Running   0          46s
    pod/myapp-5b9dbdbb-gcvlg   1/1     Running   0          46s
    pod/myapp-5b9dbdbb-zgtcj   1/1     Running   0          46s
    pod/myapp-wc7b9            1/1     Running   0          63m
    
    NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/myapp   3/3     3            3           46s
    
    NAME                             DESIRED   CURRENT   READY   AGE
    replicaset.apps/myapp-5b9dbdbb   3         3         3       46s
    
    $ kubectl get replicaset.apps/myapp-5b9dbdbb -o yaml
    apiVersion: apps/v1
    kind: ReplicaSet
    metadata:
      annotations:
        deployment.kubernetes.io/desired-replicas: "3"
        deployment.kubernetes.io/max-replicas: "4"
        deployment.kubernetes.io/revision: "1"
      creationTimestamp: "2020-02-12T19:14:18Z"
      generation: 1
      labels:
        app: myapp
        deployment: myapp
        pod-template-hash: 5b9dbdbb
      name: myapp-5b9dbdbb
      namespace: java-fun-k8s
      ownerReferences:
      - apiVersion: apps/v1
        blockOwnerDeletion: true
        controller: true
        kind: Deployment
        name: myapp
        uid: 072d3369-17d5-4006-a186-2758263ca17d
      resourceVersion: "10392"
      selfLink: /apis/apps/v1/namespaces/java-fun-k8s/replicasets/myapp-5b9dbdbb
      uid: 28978f77-201a-4ee6-bb81-580e23ba2ca0
    spec:
      replicas: 3
      selector:
        matchLabels:
          app: myapp
          deployment: myapp
          pod-template-hash: 5b9dbdbb
      template:
        metadata:
          creationTimestamp: null
          labels:
            app: myapp
            deployment: myapp
            pod-template-hash: 5b9dbdbb
        spec:
          containers:
          - image: quay.io/bward/myapp
            imagePullPolicy: Always
            name: myapp
            resources: {}
            terminationMessagePath: /dev/termination-log
            terminationMessagePolicy: File
          dnsPolicy: ClusterFirst
          restartPolicy: Always
          schedulerName: default-scheduler
          securityContext: {}
          terminationGracePeriodSeconds: 30
    status:
      availableReplicas: 3
      fullyLabeledReplicas: 3
      observedGeneration: 1
      readyReplicas: 3
      replicas: 3
  8. Create a Service to load balance the pods

    $ kubectl create service clusterip myapp --tcp=8080:8080
    service/myapp created
    
    $ kubectl get all
    NAME                       READY   STATUS    RESTARTS   AGE
    pod/myapp-5b9dbdbb-fpqtl   1/1     Running   0          79m
    pod/myapp-5b9dbdbb-gcvlg   1/1     Running   0          79m
    pod/myapp-5b9dbdbb-zgtcj   1/1     Running   0          79m
    pod/myapp-wc7b9            1/1     Running   0          142m
    
    NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)    AGE
    service/myapp   ClusterIP   10.104.138.14   <none>        8080/TCP   5s
    
    NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
    deployment.apps/myapp   3/3     3            3           79m
    
    NAME                             DESIRED   CURRENT   READY   AGE
    replicaset.apps/myapp-5b9dbdbb   3         3         3       79m

    Notice how the service IP 10.104.138.14 maps to endpoints created for each pod.

    $ kubectl describe endpoints
    Name:         myapp
    Namespace:    java-fun-k8s
    Labels:       app=myapp
    Annotations:  endpoints.kubernetes.io/last-change-trigger-time: 2020-02-12T20:33:58Z
    Subsets:
      Addresses:          172.17.0.4,172.17.0.5,172.17.0.6,172.17.0.7
      NotReadyAddresses:  <none>
      Ports:
        Name       Port  Protocol
        ----       ----  --------
        8080-8080  8080  TCP
    
    Events:  <none>

    Now tunnel into the cluster to check the load balancer URL for your app.

    $ curl https://10.104.138.14:8080
    <NO RESPONSE, SINCE YOUR ARE NOT INSIDE THE KUBERNETES NETWORK>
    
    $ minikube ssh
                             _             _
                _         _ ( )           ( )
      ___ ___  (_)  ___  (_)| |/')  _   _ | |_      __
    /' _ ` _ `\| |/' _ `\| || , <  ( ) ( )| '_`\  /'__`\
    | ( ) ( ) || || ( ) || || |\`\ | (_) || |_) )(  ___/
    (_) (_) (_)(_)(_) (_)(_)(_) (_)`\___/'(_,__/'`\____)
    
    $ curl http://10.104.138.14:8080
    Greetings from Spring Boot!$

Note that we still do not have ingress setup to reach this service. OpenShift handles this using Routes and an HAProxy ingress router. This can be achieved several different ways using native Kubernetes ingress object, and is not covered here.