Affichage des articles dont le libellé est kubernetes. Afficher tous les articles
Affichage des articles dont le libellé est kubernetes. Afficher tous les articles
11/14/2019

Google Cloud Run - how to fix "ERROR: (gcloud.beta.run.deploy) Resource readiness deadline exceeded."

As a full-stack developer you might want to deploy your containers on Google Cloud Run instead of managing a full-blown Kubernetes cluster with knative on top of it. However please note that Google Cloud Run is currently still in beta so your experience may vary.

Yesterday I saw increased build failure related to a soon-to-be-merge branch from a co-worker, the CI gcloud run deploy command yields:

ERROR: (gcloud.beta.run.deploy) Resource readiness deadline exceeded.

Unlike in a traditional Kubernetes cluster, Readiness probes are not currently configurable in Cloud Run full managed mode and looking inside Cloud Run logs everything seemed to be fine, the app was listening to 8080 after some time.

After a deep dive into the code changes I found that some new code was delaying for up to 12 seconds the app socket listening step. Worst, those 12 seconds delay was a measure I took on my MacBook and this new code was highly Disk I/O bound so it explained why Cloud Run readiness probe timed out.

The fix was then to remove this delay and load what was needed from the filesystem on-demand (instead of loading everything — with a lot of never-used stuff — somewhere in the app life-cycle). I hope this post will help someone because I've found nothing on the web on that matter :) !

10/25/2019

Fix the SchemaError(io.k8s.api.apps.v1beta2.StatefulSetStatus): invalid object doesn't have additional properties error

Issue: Kubernetes API server version and kubectl CLI version mismatch.

  • Kubernetes API server version (e.g. v1.14.7) 
  • kubectl CLI version (e.g. {Major:"1", Minor:"10"})

Solution: upgrade kubectl 👍

5/03/2018

Pretty print Kubernetes events (kubectl get events -w)

Kubernetes events stream with kubectl get events --all-namespaces --watch are very hard to read, unless you got a very very (very) large screen.

NAMESPACE      LAST SEEN                        FIRST SEEN                       COUNT     NAME                                                        KIND                      SUBOBJECT                              TYPE      REASON                  SOURCE                                                         MESSAGE
image-charts   2018-05-03 20:36:08 +0200 CEST   2018-05-03 20:36:08 +0200 CEST   1         image-charts-deployment-57575d876f-5vjvj.152b364506573f50   Pod                                                              Normal    Scheduled               default-scheduler                                              Successfully assigned image-charts-deployment-57575d876f-5vjvj to gke-image-charts-us-w-production-pool-09fa8042-gmcd
image-charts   2018-05-03 20:36:09 +0200 CEST   2018-05-03 20:36:09 +0200 CEST   1         image-charts-deployment-57575d876f-5vjvj.152b36451cb11f32   Pod                                                              Normal    SuccessfulMountVolume   kubelet, gke-image-charts-us-w-production-pool-09fa8042-gmcd   MountVolume.SetUp succeeded for volume "default-token-psj7t"
image-charts   2018-05-03 20:36:11 +0200 CEST   2018-05-03 20:36:11 +0200 CEST   1         image-charts-deployment-57575d876f-5vjvj.152b3645963079f5   Pod                       spec.containers{image-charts-runner}   Normal    Pulled                  kubelet, gke-image-charts-us-w-production-pool-09fa8042-gmcd   Container image "eu.gcr.io/ic-18/image-charts:master-ef08ceeaf315c89f7e7dd4bc4cec8f9b0cef834a" already present on machine
image-charts   2018-05-03 20:36:11 +0200 CEST   2018-05-03 20:36:11 +0200 CEST   1         image-charts-deployment-57575d876f-5vjvj.152b364599f0352e   Pod                       spec.containers{image-charts-runner}   Normal    Created                 kubelet, gke-image-charts-us-w-production-pool-09fa8042-gmcd   Created container
image-charts   2018-05-03 20:36:12 +0200 CEST   2018-05-03 20:36:12 +0200 CEST   1         image-charts-deployment-57575d876f-5vjvj.152b3645c253b80f   Pod                       spec.containers{image-charts-runner}   Normal    Started                 kubelet, gke-image-charts-us-w-production-pool-09fa8042-gmcd   Started container
image-charts   2018-05-03 20:42:08 +0200 CEST   2018-05-03 20:42:08 +0200 CEST   1         image-charts-deployment-57575d876f-5vjvj.152b3698d837ff3c   Pod                       spec.containers{image-charts-runner}   Normal    Killing                 kubelet, gke-image-charts-us-w-production-pool-09fa8042-gmcd   Killing container with id docker://image-charts-runner:Need to kill Pod
image-charts   2018-05-03 20:20:38 +0200 CEST   2018-05-03 20:20:38 +0200 CEST   1         image-charts-deployment-57575d876f-v74c8.152b356c77a29f53   Pod                                                              Normal    Scheduled               default-scheduler                                              Successfully assigned image-charts-deployment-57575d876f-v74c8 to gke-image-charts-us-w-production-pool-09fa8042-gmcd
image-charts   2018-05-03 20:20:39 +0200 CEST   2018-05-03 20:20:39 +0200 CEST   1         image-charts-deployment-57575d876f-v74c8.152b356c88794874   Pod                                                              Normal    SuccessfulMountVolume   kubelet, gke-image-charts-us-w-production-pool-09fa8042-gmcd   MountVolume.SetUp succeeded for volume "default-token-psj7t"
image-charts   2018-05-03 20:20:40 +0200 CEST   2018-05-03 20:20:40 +0200 CEST   1         image-charts-deployment-57575d876f-v74c8.152b356ce43ac4e3   Pod                       spec.containers{image-charts-runner}   Normal    Pulled                  kubelet, gke-image-charts-us-w-production-pool-09fa8042-gmcd   Container image "eu.gcr.io/ic-18/image-charts:master-ef08ceeaf315c89f7e7dd4bc4cec8f9b0cef834a" already present on machine
image-charts   2018-05-03 20:20:40 +0200 CEST   2018-05-03 20:20:40 +0200 CEST   1         image-charts-deployment-57575d876f-v74c8.152b356ceadd2e40   Pod                       spec.containers{image-charts-runner}   Normal    Created                 kubelet, gke-image-charts-us-w-production-pool-09fa8042-gmcd   Created container
image-charts   2018-05-03 20:20:41 +0200 CEST   2018-05-03 20:20:41 +0200 CEST   1         image-charts-deployment-57575d876f-v74c8.152b356d1403821b   Pod                       spec.containers{image-charts-runner}   Normal    Started                 kubelet, gke-image-charts-us-w-production-pool-09fa8042-gmcd   Started container
image-charts   2018-05-03 20:26:09 +0200 CEST   2018-05-03 20:26:09 +0200 CEST   1         image-charts-deployment-57575d876f-v74c8.152b35b95bfa97e4   Pod                       spec.containers{image-charts-runner}   Normal    Killing                 kubelet, gke-image-charts-us-w-production-pool-09fa8042-gmcd   Killing container with id docker://image-charts-runner:Need to kill Pod
image-charts   2018-05-03 20:20:38 +0200 CEST   2018-05-03 20:20:38 +0200 CEST   1         image-charts-deployment-57575d876f.152b356c778b2a36         ReplicaSet                                                       Normal    SuccessfulCreate        replicaset-controller                                          Created pod: image-charts-deployment-57575d876f-v74c8
image-charts   2018-05-03 20:26:08 +0200 CEST   2018-05-03 20:26:08 +0200 CEST   1         image-charts-deployment-57575d876f.152b35b94cd06d75         ReplicaSet                                                       Normal    SuccessfulDelete        replicaset-controller                                          Deleted pod: image-charts-deployment-57575d876f-v74c8
image-charts   2018-05-03 20:36:08 +0200 CEST   2018-05-03 20:36:08 +0200 CEST   1         image-charts-deployment-57575d876f.152b364505f8a022         ReplicaSet                                                       Normal    SuccessfulCreate        replicaset-controller                                          Created pod: image-charts-deployment-57575d876f-5vjvj
image-charts   2018-05-03 20:42:08 +0200 CEST   2018-05-03 20:42:08 +0200 CEST   1         image-charts-deployment-57575d876f.152b3698d19cba5f         ReplicaSet                                                       Normal    SuccessfulDelete        replicaset-controller                                          Deleted pod: image-charts-deployment-57575d876f-5vjvj
image-charts   2018-05-03 20:42:08 +0200 CEST   2018-05-03 12:27:10 +0200 CEST   21        image-charts-deployment.152b1b9630769766                    Deployment                                                       Normal    ScalingReplicaSet       deployment-controller                                          Scaled down replica set image-charts-deployment-57575d876f to 3
image-charts   2018-05-03 20:36:08 +0200 CEST   2018-05-03 12:47:08 +0200 CEST   20        image-charts-deployment.152b1cad1828a4bc                    Deployment                                                       Normal    ScalingReplicaSet       deployment-controller                                          Scaled up replica set image-charts-deployment-57575d876f to 4
image-charts   2018-05-03 20:36:08 +0200 CEST   2018-04-23 22:32:29 +0200 CEST   205       image-charts-hpa.15282ad08af5393c                           HorizontalPodAutoscaler                                          Normal    SuccessfulRescale       horizontal-pod-autoscaler                                      New size: 4; reason: cpu resource utilization (percentage of request) above target
image-charts   2018-05-03 20:42:08 +0200 CEST   2018-04-23 22:38:29 +0200 CEST   205       image-charts-hpa.15282b2472554668                           HorizontalPodAutoscaler                                          Normal    SuccessfulRescale       horizontal-pod-autoscaler                                      New size: 3; reason: All metrics below target

Let's specify a go-template and improve this:

kubectl get events --all-namespaces --watch -o 'go-template={{.lastTimestamp}} {{.involvedObject.kind}} {{.message}} ({{.involvedObject.name}}){{"\n"}}'

2018-05-03T18:36:08Z Pod Successfully assigned image-charts-deployment-57575d876f-5vjvj to gke-image-charts-us-w-production-pool-09fa8042-gmcd (image-charts-deployment-57575d876f-5vjvj)
2018-05-03T18:36:09Z Pod MountVolume.SetUp succeeded for volume "default-token-psj7t"  (image-charts-deployment-57575d876f-5vjvj)
2018-05-03T18:36:11Z Pod Container image "eu.gcr.io/killbug-165718/image-charts:master-ef08ceeaf315c89f7e7dd4bc4cec8f9b0cef834a" already present on machine (image-charts-deployment-57575d876f-5vjvj)
2018-05-03T18:36:11Z Pod Created container (image-charts-deployment-57575d876f-5vjvj)
2018-05-03T18:36:12Z Pod Started container (image-charts-deployment-57575d876f-5vjvj)
2018-05-03T18:42:08Z Pod Killing container with id docker://image-charts-runner:Need to kill Pod (image-charts-deployment-57575d876f-5vjvj)
2018-05-03T18:20:38Z Pod Successfully assigned image-charts-deployment-57575d876f-v74c8 to gke-image-charts-us-w-production-pool-09fa8042-gmcd (image-charts-deployment-57575d876f-v74c8)
2018-05-03T18:20:39Z Pod MountVolume.SetUp succeeded for volume "default-token-psj7t"  (image-charts-deployment-57575d876f-v74c8)
2018-05-03T18:20:40Z Pod Container image "eu.gcr.io/killbug-165718/image-charts:master-ef08ceeaf315c89f7e7dd4bc4cec8f9b0cef834a" already present on machine (image-charts-deployment-57575d876f-v74c8)
2018-05-03T18:20:40Z Pod Created container (image-charts-deployment-57575d876f-v74c8)
2018-05-03T18:20:41Z Pod Started container (image-charts-deployment-57575d876f-v74c8)
2018-05-03T18:26:09Z Pod Killing container with id docker://image-charts-runner:Need to kill Pod (image-charts-deployment-57575d876f-v74c8)
2018-05-03T18:20:38Z ReplicaSet Created pod: image-charts-deployment-57575d876f-v74c8 (image-charts-deployment-57575d876f)
2018-05-03T18:26:08Z ReplicaSet Deleted pod: image-charts-deployment-57575d876f-v74c8 (image-charts-deployment-57575d876f)
2018-05-03T18:36:08Z ReplicaSet Created pod: image-charts-deployment-57575d876f-5vjvj (image-charts-deployment-57575d876f)
2018-05-03T18:42:08Z ReplicaSet Deleted pod: image-charts-deployment-57575d876f-5vjvj (image-charts-deployment-57575d876f)
2018-05-03T18:42:08Z Deployment Scaled down replica set image-charts-deployment-57575d876f to 3 (image-charts-deployment)
2018-05-03T18:36:08Z Deployment Scaled up replica set image-charts-deployment-57575d876f to 4 (image-charts-deployment)
2018-05-03T18:36:08Z HorizontalPodAutoscaler New size: 4; reason: cpu resource utilization (percentage of request) above target (image-charts-hpa)
2018-05-03T18:42:08Z HorizontalPodAutoscaler New size: 3; reason: All metrics below target (image-charts-hpa)

Much better right?

Last thing, we do not want to remember this so let's define a function in ~/.zshrc (or equivalent):

kubectl-events() {
  kubectl get events --all-namespaces --watch -o 'go-template={{.lastTimestamp}} {{.involvedObject.kind}} {{.message}} ({{.involvedObject.name}}){{"\n"}}'
}

Done 👍

Note: if you wish to be a Kubernetes power-user take a look at kubectx cli!

Special thanks to @m_pousse that wrote the initial go-template version based on a kubernetes event stream app I was working on!

4/17/2018

Step by step guide to setup a multi-region Kubernetes app

Who doesn't want to spread one of its SaaS across continents? I finally had the opportunity to do it for my Image-Charts SaaS!

Image-charts was at first hosted on a Kubernetes cluster in europe-west Google Cloud Engine region and it was kind of an issue considering where our real users where (screenshot courtesy of Cloudflare DNS traffic):

As we can see, an important part of our traffic come from the US zone. Good news, it was time to try multi-region kubernetes. Here is a step by step guide on how to do it.

New Kubernetes cluster region on GCE

First thing first, let's create a new kubernetes cluster in us-west coast:

You should really (really) enable the preemptible nodes feature. Why? Because you get chaos engineering for free and continuously test your application architecture and configuration for robustness! Preemptible VM means that cluster nodes won't last more than 24 hours and good news : I observed cluster cost reduction by two.

Get a static IP from GCE

Next we need to create a new IP address on GCE. We will associate this IP to our kubernetes service so it will always stay with a static IP.

gcloud compute addresses create image-charts-us-west1 --region us-west1

Wait for it...

gcloud compute addresses list
NAME                       REGION        ADDRESS         STATUS
image-charts-europe-west1  europe-west1  35.180.80.101   RESERVED
image-charts-us-west1      us-west1      35.180.80.100   RESERVED

Kubernetes YAML

The above app YAML defines multiple Kubernetes objects: a namespace (app-ns), a service that will give access to our app from outside (my-app-service), our app deployment object (my-app-deployment), auto-scaling through horizontal pod autoscaler and finally a pod disruption budget for our app.

# deploy/k8s.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: app-ns
---
apiVersion: v1
kind: Service
metadata:
  labels:
    app: my-app
    zone: __ZONE__
  name: my-app-service
  namespace: app-ns
spec:
  type: NodePort
  ports:
  - name: "80"
    port: 80
    targetPort: 8080
    protocol: TCP
  selector:
    app: my-app
  type: LoadBalancer
  loadBalancerIP: __IP__
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
  namespace: app-ns
  name: my-app-deployment
  labels:
    app: my-app
spec:
  replicas: 3
  # how much revision history of this deployment you want to keep
  revisionHistoryLimit: 2
  strategy:
    type: RollingUpdate
    rollingUpdate:
      # specifies the maximum number of Pods that can be unavailable during the update
      maxUnavailable: 25%
      # specifies the maximum number of Pods that can be created above the desired number of Pods
      maxSurge: 200%
  template:
    metadata:
      labels:
        app: my-app
    spec:
      affinity:
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              topologyKey: "kubernetes.io/hostname"
              labelSelector:
                matchExpressions:
                - key: app
                  operator: In
                  values:
                  - my-app
      restartPolicy: Always
      containers:
      - image: __IMAGE__
        name: my-app-runner
        resources:
          requests:
            memory: "100Mi"
            cpu: "1"
          limits:
            memory: "380Mi"
            cpu: "2"
        # send traffic when:
        readinessProbe:
          httpGet:
            path: /_ready
            port: 8081
          initialDelaySeconds: 20
          timeoutSeconds: 4
          periodSeconds: 5
          failureThreshold: 1
          successThreshold: 2
        # restart container when:
        livenessProbe:
          httpGet:
            path: /healthz
            port: 8081
          initialDelaySeconds: 20
          timeoutSeconds: 2
          periodSeconds: 2
          # if we just started the pod, we need to be sure it works
          # so it may take some time and we don't want to rush things up (thx to rolling update)
          failureThreshold: 5
          successThreshold: 1
        ports:
        - name: containerport
          containerPort: 8080
        - name: monitoring
          containerPort: 8081
---
apiVersion: autoscaling/v1
kind: HorizontalPodAutoscaler
metadata:
  name: my-app-hpa
  namespace: app-ns
spec:
  scaleTargetRef:
    apiVersion: extensions/v1beta1
    kind: Deployment
    name: my-app-deployment
  minReplicas: 3
  maxReplicas: 5
  targetCPUUtilizationPercentage: 200
---
apiVersion: policy/v1beta1
kind: PodDisruptionBudget
metadata:
  name: my-app-pdb
  namespace: app-ns
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: my-app

Some things of note:

__IMAGE__, __IP__ and __ZONE__ will be replaced during the gitlab-ci deploy stage (see continuous delivery pipeline section).

  kind: Service
  # [...]
  type: LoadBalancer
  loadBalancerIP: __IP__

type: LoadBalancer and loadBalancerIP tell kubernetes to creates a TCP Network Load Balancer (the IP must be a regional one, just as we did).

It's always a good practice to split up monitoring and production traffic that's why monitoring is on port 8081 (for liveness and readiness probes) and production traffic on port 8080.

Another good practice is to setup podAntiAffinity to tell kubernetes scheduler to place our app pod replica between available nodes and not put all our app pods on the same node.

And the good news is: this principle can be easily applied in Kubernetes thanks to requests and limits.

Finally we use PodDisruptionBudget policy to tell Kubernetes scheduler how to react when disruptions occurs (e.g. one of our preemptible node dies).

Continuous Delivery Pipeline

I used Gitlab-CI here because that's what power Image-charts & Redsmin deployments but it will work with other alternatives as well.

# gitlab.yaml

# https://docs.gitlab.com/ce/ci/yaml/
image: docker

# fetch is faster as it re-uses the project workspace (falling back to clone if it doesn't exist).
variables:
  GIT_STRATEGY: fetch
  # We use overlay for performance reasons
  # https://docs.gitlab.com/ce/ci/docker/using_docker_build.html#using-the-overlayfs-driver
  DOCKER_DRIVER: overlay

services:
  - docker:dind

stages:
  - build
  - test
  - deploy

# template that we can reuse
.before_script_template: &setup_gcloud
  image: lakoo/node-gcloud-docker:latest
  retry: 2
  variables:
    # default variables
    ZONE: us-west1-b
    CLUSTER_NAME: image-charts-us-west1
  only:
    # only
    - /master/
  before_script:
    - echo "Setting up gcloud..."
    - echo $GCLOUD_SERVICE_ACCOUNT | base64 -d > /tmp/$CI_PIPELINE_ID.json
    - gcloud auth activate-service-account --key-file /tmp/$CI_PIPELINE_ID.json
    - gcloud config set compute/zone $ZONE
    - gcloud config set project $GCLOUD_PROJECT
    - gcloud config set container/use_client_certificate False
    - gcloud container clusters get-credentials $CLUSTER_NAME
    - gcloud auth configure-docker --quiet
  after_script:
    - rm -f /tmp/$CI_PIPELINE_ID.json

build:
  <<: *setup_gcloud
  stage: build
  retry: 2
  script:
    - docker build --rm -t ${DOCKER_REGISTRY}/${PACKAGE_NAME}:${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA} .
    - mkdir release
    - docker push ${DOCKER_REGISTRY}/${PACKAGE_NAME}:${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA}

test:
  <<: *setup_gcloud
  stage: test
  coverage: '/Lines[^:]+\:\s+(\d+\.\d+)\%/'
  retry: 2
  artifacts:
    untracked: true
    expire_in: 1 week
    name: "coverage-${CI_COMMIT_REF_NAME}"
    paths:
     - coverage/lcov-report/
  script:
    - echo 'edit me'

.job_template: &deploy
  <<: *setup_gcloud
  stage: deploy
  script:
    - export IMAGE=${DOCKER_REGISTRY}/${PACKAGE_NAME}:${CI_COMMIT_REF_NAME}-${CI_COMMIT_SHA}
    - cat deploy/k8s.yaml | sed s#'__IMAGE__'#$IMAGE#g | sed s#'__ZONE__'#$ZONE#g | sed s#'__IP__'#$IP#g > deploy/k8s-generated.yaml
    - kubectl apply -f deploy/k8s-generated.yaml
    - echo "Waiting for deployment..."
    - (while grep -v "successfully" <<<$A]; do A=`kubectl rollout status --namespace=app-ns deploy/my-app-deployment`;echo "$A\n"; sleep 1; done);
  artifacts:
    untracked: true
    expire_in: 1 week
    name: "deploy-yaml-${CI_COMMIT_REF_NAME}"
    paths:
     - deploy/

deploy-us:
  <<: *deploy
  variables:
    ZONE: us-west1
    CLUSTER_NAME: image-charts-us-west1
    IP: 35.180.80.100

deploy-europe:
  <<: *deploy
  variables:
    ZONE: europe-west1
    CLUSTER_NAME: image-charts-europe-west1
    IP: 35.180.80.101

Here are the environment variables to setup in Gitlab-CI pipeline settings UI:

DOCKER_REGISTRY (e.g. eu.gcr.io/project-100110): container registry to use, easiest way is to leverage GCE container registry.

GCLOUD_PROJECT (e.g. project-100110): google cloud project name.

GCLOUD_SERVICE_ACCOUNT: a base64 encoded service account JSON. When creating a new service account on GCE console you will get a JSON like this:

{
  "type": "service_account",
  "project_id": "project-100110",
  "private_key_id": "1000000003900000ba007128b770d831b2b00000",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQE\nhOnLxQa7qPrZFLP+2S3RaSudsbuocVo4byZH\n5e9gsD7NzsD/7ECJDyInbH8+MEJxFBW/yYUX6XHM/d\n5OijyIdA4+NPo6KpkJa2WV8I/KPtoNLSK7d6oRdEAZ\n7ECJDyInbH8+MEJxFBW/yYUX6XHM/d\n5OijyIdA4+NPo6KpkJa2WV8I/KPtoNLSK7d6oRdEAZ\n7ECJDyInbH8+MEJxFBW/yYUX6XHM/d\n5OijyIdA4+NPo6KpkJa2WV8I/KPtoNLSK7d6oRdEAZ\n7ECJDyInbH8+MEJxFBW/yYUX6XHM/d\n5OijyIdA4+NPo6KpkJa2WV8I/KPtoNLSK7d6oRdEAZ\n7ECJDyInbH8+MEJxFBW/yYUX6XHM/d\n5OijyIdA4+NPo6KpkJa2WV8I/KPtoNLSK7d6oRdEAZ\n7ECJDyInbH8+MEJxFBW/yYUX6XHM/d\n5OijyIdA4+NPo6KpkJa2WV8I/KPtoNLSK7d6oRdEAZ\n7ECJDyInbH8+MEJxFBW/yYUX6XHM/d\n5OijyIdA4+NPo6KpkJa2WV8I/KPtoNLSK7d6oRdEAZ\n7ECJDyInbH8+MEJxFBW/yYUX6XHM/d\n5OijyIdA4+NPo6KpkJa2WV8I/KPtoNLSK7d6oRdEAZ\n7ECJDyInbH8+MEJxFBW/yYUX6XHM/d\n5OijyIdA4+NPo6KpkJa2WV8I/KPtoNLSK7d6oRdEAZ\n7ECJDyInbH8+MEJxFBW/yYUX6XHM/d\n5OijyIdA4+NPo6KpkJa2WV8I/KPtoNLSK7d6oRdEAZ\n7ECJDyInbH8+MEJxFBW/yYUX6XHM/d\n5OijyIdA4+NPo6KpkJa2WV8I/KPtoNLSK7d6oRdEAZ\n\n-----END PRIVATE KEY-----\n",
  "client_email": "gitlab@project-100110.iam.gserviceaccount.com",
  "client_id": "118000107442952000000",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://accounts.google.com/o/oauth2/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/gitlab%40project-100110.iam.gserviceaccount.com"
}

To make it work on our CI, we need to transform it to base64 in order to inject it as an environment variable.

cat service_account.json | base64

PACKAGE_NAME (e.g. my-app): container image name.

The YAML syntax below defines a template...

.before_script_template: &setup_gcloud

... that we can then reuse with:

<<: *setup_gcloud

I'm particularly fond of the one-liner below that makes gitlab-ci job wait for deployment completion:

(while grep -v "successfully" <<<$A]; do A=`kubectl rollout status --namespace=app-ns deploy/my-app-deployment`;echo "$A\n"; sleep 1; done);

It's so simple and does the job.

Note: the above pipeline only runs when commits are pushed to master, I removed other environments code for simplicity.

The important part is:

deploy-us:
  <<: *deploy
  variables:
    ZONE: us-west1
    CLUSTER_NAME: image-charts-us-west1
    IP: 35.180.80.100

This is where the magic happens, this job includes shared deploy job template (that itself includes setup_gcloud template) and specify three variables: the ZONE, CLUSTER_NAME and the IP address to expose) that alone is sufficient to make our pipeline deploy to multiple Kubernetes cluster on GCE. Bonus: we store the generated YAML as an artifact for future inspections.

Pushing our code you should have something like this:

Now lets connect to one of our kubernetes cluster. Tips: to easily switch between kubectl context I use kubectx

# on us-west1 cluster
kubectl get service/my-app-service -w --namespace=image-charts
NAME                          TYPE           CLUSTER-IP      EXTERNAL-IP      PORT(S)        AGE
my-app-service                LoadBalancer   10.59.250.180   35.180.80.100    80:31065/TCP   10s

From this point you should be able to access both public static IP with curl.

Configuring Geo-DNS

Same warning as before, I used Cloudflare for the geo-dns part but you could use another provider. It costs $25/mo to get basic geo-dns load-balancing between region: basic load balancing ($5/mo), up to 2 origin servers ($0/mo), check every 60 seconds ($0/mo), check from 4 regions ($10/mo), geo routing ($10/mo).

Once activated, create a two pools (one per region using the static IP we just created):

... then create a monitor and finally the load-balancer that distribute load based on the visitor geolocation.

And... we're done 🎉

»
 
 
Made with on a hot august night from an airplane the 19th of March 2017.