Deploy custom Ingress on GKE

The GKE Ingress for HTTP(S) Load Balancing is the managed Ingress controller and implements Ingress resources as Google Cloud load balancers for HTTP(S) workloads in GKE.

This allows you to use the Google Cloud Load Balancing with Container-native load balancing and Cloud CDN, and more, to access the application deployed on GKE.

But, here comes the but, the default Ingress GCE that comes with GKE has some missing functionalities like Cloud CDN is missing multiple features in the BackendConfig as a result you cannot set the option “Use origin headers” or update the TTLs or disable the “Negative caching” option in Cloud CDN. You cannot update the values manually as they will be overwritten by the ingress.

If cannot wait until the official release and fix is available there are ways to install a custom version on your cluster and I will explain how.

Prerequisites

  • GCP project with billing enabled. If you don’t have one then sign-in to Google Cloud Platform Console and create a new project
  • Access to a standard internet browser

Solution

An overview of the solution:

  • get the source code from github.com
  • build a new docker image
  • disable the default GLBC plugin
  • deploy your version of the GLBC

WARNING: The following changes will disrupt traffic for your services. Do not run this unless you fully understand what you are doing and how this may impact your cluster’s state.

This is heavily inspired from docs/deploy/gke/gke-self-managed.sh and most of the commands are copied from this script.

All the commands can be executed from the Cloud Shell console.

Setup

Let’s define our variables first

ZONE=us-central1-a
CLUSTER_NAME=demo-gce-cluster
PROJECT_ID=`gcloud config list --format 'value(core.project)' 2>/dev/null`
REGISTRY="gcr.io/$PROJECT_ID"
GCP_USER=`gcloud config list --format 'value(core.account)' 2>/dev/null`

Clone the repository. For this demo I will use my branch

git clone --branch backendconfig-cdn-config \
    https://github.com/gabihodoroaga/ingress-gce.git 

Build the custom image

export REGISTRY=gcr.io/$PROJECT_ID
export VERSION=v1-custom
IMAGE_NAME=$REGISTRY/ingress-gce-glbc-amd64:$VERSION
make -C ingress-gce only-push-glbc

Now we have our own version pushed to the registry, let’s continue

Download the files required to deploy the image to your cluster from this Github Gist


curl https://codeload.github.com/gist/a5644451e1d309ad40b1312eb2369fe6/zip/70d0ec9fd78834ba4662ac7ee1ca46584d3d026c \
    --output deploy-ingress-gce.zip
unzip deploy-ingress-gce.zip
mv a5644451e1d309ad40b1312eb2369fe6-70d0ec9fd78834ba4662ac7ee1ca46584d3d026c \
    deploy-ingress-gce
cd deploy-ingress-gce

First let’s create a cluster, a really small one, so we will not spend too much resources just for a demo

gcloud container clusters \
        create $CLUSTER_NAME \
        --zone $ZONE --machine-type "e2-medium" \
        --enable-ip-alias \
        --num-nodes=2

Interesting fact: The --enable-ip-alias which means “Creating a VPC-native cluster” without this option you cannot use NEG and you will get some weird errors in logs like “Error 400: Invalid value for field ‘resource.ipAddress’: ‘10.0.x.x’”

To get credentials for the local machine to connect to the cluster:

gcloud container clusters get-credentials $CLUSTER_NAME \
    --zone $ZONE
NETWORK_NAME=$(basename $(gcloud container clusters \
    describe $CLUSTER_NAME --project $PROJECT_ID --zone=$ZONE \
    --format='value(networkConfig.network)'))
SUBNETWORK_NAME=$(basename $(gcloud container clusters \
    describe $CLUSTER_NAME --project $PROJECT_ID \
    --zone=$ZONE --format='value(networkConfig.subnetwork)'))
NETWORK_TAGS=$(gcloud compute instances describe \
    $(kubectl get nodes -o jsonpath='{.items[0].metadata.name}') \
    --zone=$ZONE --format="value(tags.items[0])")

sed "s/\[PROJECT\]/$PROJECT_ID/" gce.conf.tpl | \
sed "s/\[NETWORK\]/$NETWORK_NAME/" | \
sed "s/\[SUBNETWORK\]/$SUBNETWORK_NAME/" | \
sed "s/\[CLUSTER_NAME\]/$CLUSTER_NAME/" | \
sed "s/\[NETWORK_TAGS\]/$NETWORK_TAGS/" | \
sed "s/\[ZONE\]/$ZONE/" > gce.conf

Grant permission to current GCP user to create new k8s ClusterRole’s.

kubectl create clusterrolebinding one-binding-to-rule-them-all \
    --clusterrole=cluster-admin --user=${GCP_USER}

Store the nodePort for default-http-backend

NODE_PORT=`kubectl get svc default-http-backend -n kube-system -o yaml \
    | grep "nodePort:" | cut -f2- -d:`
echo $NODE_PORT

Disable the default GLBC from your cluster

gcloud container clusters update ${CLUSTER_NAME} --zone=${ZONE} \
    --update-addons=HttpLoadBalancing=DISABLED

This will take a while as it requires restarting the master node.

Make sure the pod and the service for default-backend do not exists anymore

kubectl get svc -n kube-system | grep default-http-backend
kubectl get pod -n kube-system | grep default-backend

You should not get anything back from the above commands.

Create a new service account for glbc and give it a ClusterRole allowing it access to API objects it needs.

kubectl apply -f rbac.yaml

Generate the default-http-backend.yaml from the template

sed "s/\[NODE_PORT\]/$NODE_PORT/" default-http-backend.yaml.tpl \
    > default-http-backend.yaml

Recreate the default-backend

kubectl create -f default-http-backend.yaml

Create new GCP service account

gcloud iam service-accounts create glbc-service-account \
  --display-name "Service Account for GLBC"

Give the GCP service account the appropriate roles

gcloud projects add-iam-policy-binding ${PROJECT_ID} \
  --member serviceAccount:glbc-service-account@${PROJECT_ID}.iam.gserviceaccount.com \
  --role roles/compute.admin

Create a key for the GCP service account

gcloud iam service-accounts keys create key.json \
  --iam-account glbc-service-account@${PROJECT_ID}.iam.gserviceaccount.com

Store the the service account key as secret

kubectl create secret generic glbc-gcp-key \
    --from-file=key.json -n kube-system

Create the gce.conf ConfigMap. This config map is mounted as a volume in glbc.yaml

kubectl create configmap gce-config --from-file=gce.conf -n kube-system

Generate the glbc.yaml from the template

sed "s~\[IMAGE_URL\]~${IMAGE_NAME}~" glbc.yaml.tpl > glbc.yaml

Deploy the new GLBC controller

kubectl apply -f glbc.yaml -n kube-system

Done! You are now using your version of ingress-gce in your cluster.

Let’s test our new deployed ingress controller and see how it works.

Create a deployment for nginx, add the service, and the BackendConfig

kubectl apply -f app-deployment.yaml

Create the ingress

kubectl apply -f app-ingress.yaml

Creating the ingress takes a lot of time, so be patient.

When is done let’s get the ip of the load balancer

IP_ADDRESS=$(kubectl get ingress nginx-ingress \
             -o jsonpath='{.status.loadBalancer.ingress[0].ip}') \
             && echo $IP_ADDRESS

and test if everything is configured correctly

curl -v http://$IP_ADDRESS

If everything worked well you should be able to check your Cloud CDN configuration and see that the option selected for Cache mode is “Use origin headers”.

You can update the BackendConfig options in app-deployment.yaml.

If you want to see the complete list of the supported options run the following command.

kubectl describe crd backendconfigs.cloud.google.com

Cleaning up

Make sure you still have your variables setup. if not got to Setup.

Delete the test ingress, deployment, service, and all related resources

kubectl delete -f app-ingress.yaml
kubectl delete -f app-deployment.yaml

Delete the glbc deployment and all related resources

kubectl delete -f glbc.yaml
kubectl delete secret glbc-gcp-key -n kube-system
kubectl delete configmap gce-config -n kube-system
kubectl delete -f rbac.yaml
kubectl delete clusterrolebinding one-binding-to-rule-them-all
gcloud projects remove-iam-policy-binding ${PROJECT_ID} \
    --member serviceAccount:glbc-service-account@${PROJECT_ID}.iam.gserviceaccount.com \
    --role roles/compute.admin
gcloud iam service-accounts delete glbc-service-account@${PROJECT_ID}.iam.gserviceaccount.com
kubectl delete -f default-http-backend.yaml

Enable the default GLBC on the GKE master

gcloud container clusters update ${CLUSTER_NAME} --zone=${ZONE} \
    --update-addons=HttpLoadBalancing=ENABLED

Delete the cluster - WARNING!!!

gcloud container clusters delete CLUSTER_NAME

Delete the registry image

gcloud -q container images delete $IMAGE_NAME

Conclusion

Sometimes things in GCP are not perfect. This is it! But the fact that you can disable the default addons in GKE and replace them with your own custom version is very nice, and it allows you to benefit from the new development and bug fixes without waiting for months until the official releases are published. Although, you must assume the risks of more bugs and untested functionalities.