Home > Software engineering >  K8s jobs and pods differences as uses of host subdomain
K8s jobs and pods differences as uses of host subdomain

Time:04-09

I have K8s used by Helm 3.

  1. I need to access a k8s job while running in yaml file (created by helm).

The kubectl version:

Client Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.6", GitCommit:"d921bc6d1810da51177fbd0ed61dc811c5228097", GitTreeState:"clean", BuildDate:"2021-10-27T17:50:34Z", GoVersion:"go1.16.9", Compiler:"gc", Platform:"linux/amd64"} Server Version: version.Info{Major:"1", Minor:"21", GitVersion:"v1.21.6", GitCommit:"d921bc6d1810da51177fbd0ed61dc811c5228097", GitTreeState:"clean", BuildDate:"2021-10-27T17:44:26Z", GoVersion:"go1.16.9", Compiler:"gc", Platform:"linux/amd64"}

Helm version:

version.BuildInfo{Version:"v3.3.4", GitCommit:"a61ce5633af99708171414353ed49547cf05013d", GitTreeState:"clean", GoVersion:"go1.14.9"}

As the following link: DNS concept

It works fine for Pod, but not for job.

As explained, for putting hostname and subdomain in Pod's YAML file, and add service that holds the domain...

  1. Need to check the state if running.

for pod, it is ready state.

kubectl wait pod/pod-name --for=condition=ready ...

For job there is no ready state (while pod behind is running).

How can I check the state of pod behind the job (job is running) and how can I use host subdomain for jobs?

My code ... (I removed some security tags, but the same. Important - It may be complicated.

I create a listener - running when listen, with job that need to do some curl command, and this can be achieved whether it has access to that pod behind the job):

Listener (the pod is the last job):

What I added is hostname and subdomain (which work for Pod, and not for Job). If it ever was on Pod - no problem.

I also realized that the name of the Pod (created by the job) has a hash automatic extension.

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "my-project.fullname" . }}-listener
  namespace: {{ .Release.Namespace }}
  labels:
    name: {{ include "my-project.fullname" . }}-listener
    app: {{ include "my-project.fullname" . }}-listener
    component: {{ .Chart.Name }}
    subcomponent: {{ .Chart.Name }}-listener
  annotations:
    "prometheus.io/scrape": {{ .Values.prometheus.scrape | quote }}
    "prometheus.io/path": {{ .Values.prometheus.path }}
    "prometheus.io/port": {{ .Values.ports.api.container | quote }}
spec:
  template: #PodTemplateSpec (Core/V1)
    spec: #PodSpec (core/v1)
      hostname: {{ include "my-project.fullname" . }}-listener
      subdomain: {{ include "my-project.fullname" . }}-listener-dmn
      initContainers:
        # twice - can add in helers.tpl
        - name: wait-mysql-exist-pod
          image: {{ .Values.global.registry }}/{{ .Values.global.k8s.image }}:{{ .Values.global.k8s.tag | default "latest" }}
          imagePullPolicy: IfNotPresent
          env:
            - name: MYSQL_POD_NAME
              value: {{ .Release.Name }}-mysql
            - name: COMPONENT_NAME
              value: {{ .Values.global.mysql.database.name }}
          command:
            - /bin/sh
          args:
            - -c
            - |-
              while [ "$(kubectl get pod $MYSQL_POD_NAME 2>/dev/null | grep $MYSQL_POD_NAME | awk '{print $1;}')" \!= "$MYSQL_POD_NAME" ];do
                echo 'Waiting for mysql pod to be existed...';
                sleep 5;
              done
        - name: wait-mysql-ready
          image: {{ .Values.global.registry }}/{{ .Values.global.k8s.image }}:{{ .Values.global.k8s.tag | default "latest" }}
          imagePullPolicy: IfNotPresent
          env:
            - name: MYSQL_POD_NAME
              value: {{ .Release.Name }}-mysql
          command:
            - kubectl
          args:
            - wait
            - pod/$(MYSQL_POD_NAME)
            - --for=condition=ready
            - --timeout=120s
        - name: wait-mysql-has-db
          image: {{ .Values.global.registry }}/{{ .Values.global.k8s.image }}:{{ .Values.global.k8s.tag | default "latest" }}
          imagePullPolicy: IfNotPresent
          env:
            {{- include "k8s.db.env" . | nindent 12 }}
            - name: MYSQL_POD_NAME
              value: {{ .Release.Name }}-mysql
          command:
            - /bin/sh
          args:
            - -c
            - |-
             while [ "$(kubectl exec $MYSQL_POD_NAME -- mysql -uroot -p$MYSQL_ROOT_PASSWORD -e 'show databases' 2>/dev/null | grep $MYSQL_DATABASE | awk '{print $1;}')" \!= "$MYSQL_DATABASE" ]; do
                echo 'Waiting for mysql database up...';
                sleep 5;
             done
      containers:
        - name: {{ include "my-project.fullname" . }}-listener
          image:  {{ .Values.global.registry }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default "latest" }}
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          env:
          {{- include "k8s.db.env" . | nindent 12 }}
            - name: SCHEDULER_DB
              value: $(CONNECTION_STRING)
          command: {{- toYaml .Values.image.entrypoint | nindent 12 }}
          args: # some args ...
          ports:
            - name: api
              containerPort: 8081
          resources:
            limits:
              cpu: 1
              memory: 1024Mi
            requests:
              cpu: 100m
              memory: 50Mi
          readinessProbe:
            httpGet:
              path: /api/scheduler/healthcheck
              port: api
              scheme: HTTP
            initialDelaySeconds: 10
            periodSeconds: 5
            timeoutSeconds: 1
          livenessProbe:
            tcpSocket:
              port: api
            initialDelaySeconds: 120
            periodSeconds: 10
            timeoutSeconds: 5
          volumeMounts:
            - name: {{ include "my-project.fullname" . }}-volume
              mountPath: /etc/test/scheduler.yaml
              subPath: scheduler.yaml
              readOnly: true
      volumes:
      - name: {{ include "my-project.fullname" . }}-volume
        configMap:
          name: {{ include "my-project.fullname" . }}-config
      restartPolicy: Never

The service (for the subdomain):

apiVersion: v1
kind: Service
metadata:
  name: {{ include "my-project.fullname" . }}-listener-dmn
spec:
  selector:
    name: {{ include "my-project.fullname" . }}-listener
  ports:
    - name: api
      port: 8081
      targetPort: 8081
  type: ClusterIP

Roles RoleBinding (to enable access for curl command):

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: {{ include "my-project.fullname" . }}-role
rules:
- apiGroups: [""] # "" indicates the core API group
  resources: ["pods"]
  verbs: ["get", "watch", "list", "update"]
- apiGroups: [""] # "" indicates the core API group
  resources: ["pods/exec"]
  verbs: ["create", "delete", "deletecollection", "get", "list", "patch", "update", "watch"]
- apiGroups: ["", "app", "batch"] # "" indicates the core API group
  resources: ["jobs"]
  verbs: ["get", "watch", "list"]

Role-Binding:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: {{ include "go-scheduler.fullname" . }}-rolebinding
subjects:
- kind: ServiceAccount
  name: default
roleRef:
  kind: Role
  name: {{ include "go-scheduler.fullname" . }}-role
  apiGroup: rbac.authorization.k8s.io

And finally a tester that doing a curl command:

(For check I put tail -f), and enter the pod.

apiVersion: batch/v1
kind: Job
metadata:
  name: {{ include "my-project.fullname" . }}-test
  namespace: {{ .Release.Namespace }}
  labels:
    name: {{ include "my-project.fullname" . }}-test
    app: {{ include "my-project.fullname" . }}-test 
  annotations:
    "prometheus.io/scrape": {{ .Values.prometheus.scrape | quote }}
    "prometheus.io/path": {{ .Values.prometheus.path }}
    "prometheus.io/port": {{ .Values.ports.api.container | quote }}
spec:
  template: #PodTemplateSpec (Core/V1)
    spec: #PodSpec (core/v1)
      initContainers:
        # twice - can add in helers.tpl
        #
        - name: wait-sched-listener-exists
          image: {{ .Values.global.registry }}/{{ .Values.global.k8s.image }}:{{ .Values.global.k8s.tag | default "latest" }}
          imagePullPolicy: IfNotPresent
          env:
            - name: POD_NAME
              value: {{ include "my-project.fullname" . }}-listener
          command:
            - /bin/sh
          args:
            - -c
            - |-
              while [ "$(kubectl get job $POD_NAME 2>/dev/null | grep $POD_NAME | awk '{print $1;}')" \!= "$POD_NAME" ];do
                echo 'Waiting for scheduler pod to exist ...';
                sleep 5;
              done
        - name: wait-listener-running
          image: {{ .Values.global.registry }}/{{ .Values.global.k8s.image }}:{{ .Values.global.k8s.tag | default "latest" }}
          imagePullPolicy: IfNotPresent
          env:
            - name: POD_NAME
              value: {{ include "my-project.fullname" . }}-listener
          command:
            - /bin/sh
          args:
            - -c
            - |-
              while [ "$(kubectl get pods 2>/dev/null | grep $POD_NAME | awk '{print $3;}')" \!= "Running" ];do
                echo 'Waiting for scheduler pod to run ...';
                sleep 5;
              done
      containers:
        - name: {{ include "my-project.fullname" . }}-test
          image:  {{ .Values.global.registry }}/{{ .Values.global.k8s.image }}:{{ .Values.global.k8s.tag | default "latest" }}
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          command:
            - /bin/sh
          args:
            - -c
            - "tail -f"
     # instead of above can be curl: "curl -H 'Accept: application/json' -X get my-project-listener.my-project-listener-dmn:8081/api/scheduler/jobs"

      restartPolicy: Never

I enter the test pod

kubectl exec -it my-tester-<hash> -- /bin/sh

... and run the command:

ping my-project-listener.my-project-listener-dmn

Got:

ping: bad address 'my-project-listener.my-project-listener-dmn'

When doing that for pod:

PING pod-hostname.pod-subdomain (): ... data bytes

CodePudding user response:

There's a lot here, but I think you should be able to resolve all of this with a couple of small changes.

In summary, I'd suggest changing:

apiVersion: apps/v1
kind: Deployment     # <-- not a Job
metadata: &original-job-metadata-from-the-question
spec:
  template:
    metadata:
      labels:   # vvv matching the Service selector
        name: {{ include "my-project.fullname" . }}-listener
    spec:
      # delete all of the initContainers:
      containers: &original-container-list-from-the-question
      volumes: &original-volume-list-from-the-question
      # delete restartPolicy: (default value Always)

Delete the Role and RoleBinding objects; connect to the Service http://my-project-listener-dmn:8081 and not an individual Pod; and you can kubectl wait --for=condition=available on the Deployment.

Connect to Services, not individual Pods (or Jobs or Deployments). The Service is named {{ include "my-project.fullname" . }}-listener-dmn and that is the host name you should connect to. The Service acts as a very lightweight in-cluster load balancer, and will forward requests on to one of the pods identified by its selector.

So in this example you'd connect to the Service's name and port, http://my-project-listener-dmn:8081. Your application doesn't answer the very-low-level ICMP protocol and I'd avoid ping(1) in favor of a more useful diagnostic. Also consider setting the Service's port to the default HTTP port 80; it doesn't necessarily need to match the Pod's port.

The Service selector needs to match the Pod labels (and not the Job's or Deployment's labels). A Service attaches to Pods; a Job or a Deployment has a template to create Pods; and it's those labels that need to match up. You need to add labels to the Pod template:

spec:
  template:
    metadata:
      labels:
        name: {{ include "my-project.fullname" . }}-listener

Or, in a Helm chart where you have a helper to generate these labels,

      labels: {{- include "my-project.labels" | nindent 8 }}

The thing to check here is kubectl describe service my-project-listener-dmn. There should be a line at the bottom that says Endpoints: with some IP addresses (technically some individual Pod IP addresses, but you don't usually need to know that). If it says Endpoints: <none> that's usually a sign that the labels don't match up.

You probably want some level of automatic restarts. A Pod can fail for lots of reasons, including code bugs and network hiccups. If you set restartPolicy: Never then you'll have a Failed Pod, and requests to the Service will fail until you take manual intervention of some sort. I'd suggest setting this to at least restartPolicy: OnFailure, or (for a Deployment) leaving it at its default value of Always. (There is more discussion on Job restart policies in the Kubernetes documentation.)

You probably want a Deployment here. A Job is meant for a case where you do some set of batch processing and then the job completes; that's part of why kubectl wait doesn't have the lifecycle option you're looking for.
I'm guessing you want a Deployment instead. With what you've shown here I don't think you need to make any changes at all besides

apiVersion: apps/v1
kind: Deployment

Everything so far about Services and DNS and labels still applies.

You can kubectl wait for a Deployment to be available. Since a Job is expected to run to completion and exit, that's the state kubectl wait allows. A Deployment is "available" if there is at least a minimum number of managed Pods running that pass their health checks, which I think is the state you're after.

kubectl wait --for=condition=available deployment/my-project-listener

There are simpler ways to check for database liveness. A huge fraction of what you show here is an involved sequence with special permissions to see if the database is running before the pod starts up.

What happens if the database fails while the pod is running? One common thing that will happen is you'll get a cascading sequence of exceptions and your pod will crash. Then with restartPolicy: Always Kubernetes will try to restart it; but if the database still isn't available, it will crash again; and you'll get to a CrashLoopBackOff state. If the database does become available again then eventually Kubernetes will try to restart the Pod and it will succeed.

This same logic can apply at startup time. If the Pod tries to start up, and the database isn't ready yet, and it crashes, Kubernetes will by default restart it, adding some delays after the first couple of attempts. If the database starts up within 30 seconds or so then the application will be up within a minute or so. The restart count will be greater than 0, but kubectl logs --previous will hopefully have a clear exception.

This will let you delete about half of what you show here. Delete all of the initContainers: block; then, since you're not doing any Kubernetes API operations, delete the Role and RoleBinding objects too.

If you really do want to force the Pod to wait for the database and treat startup as a special case, I'd suggest a simpler shell script using the mysql client tool, or even the wait-for script that makes basic TCP calls (the mechanism described in Docker Compose wait for container X before starting Y). This still lets you avoid all of the Kubernetes RBAC setup.

  • Related