Migrating a Service to HTTPS on GKE
Writer’s note: Part of this blogpost was completed in October 2019 while renewing the certificates. These are rough notes, so please exercise care when doing any changes to your Kubernetes configs. Warning: some of the resources referenced in this blog post are paid resources on Google Cloud, so please exercise care!
Migrating lipcolourmatch.com from HTTP to HTTPS has been on my to-do list on a long time. I made a few attempts on the project previously, but always gave up, because the bits and pieces in the Google Cloud Docs were scattered around and there was no clear, newbie-friendly walkthrough that I could use. The few times that I had tried to cobble a good strategy for myself using the docs, I’d ended up borking lipcolourmatch.com and reverting my changes. Today, I came across a new write-up on this topic from LH Fong. Since it outlined the required steps in an approachable manner, I decided to try it out myself! Reader, I’m glad to day that after trials and errors, I have succeeded! In this blogpost, I want to outline the steps I went through and some of the issues I encountered as I went along. For the full write-up, please refer to LH Fong’s article linked above.
Starting with a test service
Previously, I’d gone straight for messing with Kubernetes configuration in prod, which, at least for me, is not advisable. While not super heavily traffiked, lipcolourmatch.com does receive a steady flow of traffic and I don’t want to subject users to unnecessary downtime. Thus it’s better to start with a test service provided by the Google Cloud tutorials. Following the steps outlined in the HTTP Balancer, we start with creating a deployment. In addition to that, I also created a separate namespace first so the pods would be easier to manage. Note: if you follow along, please be sure to cleanup any resources after you are done, because otherwise you will incur costs!.
Create a namespace
kubectl create namespace lbtest
Deploy a pod
kubectl --namespace=lbtest run web --image=gcr.io/google-samples/hello-app:1.0 --port=8080
Create a service, but instead of exposing it with a Load Balancer type (like I’ve done before), use NodePort.
kubectl expose deployment web --target-port=8080 --type=NodePort
Because the type is NodePort, this service won’t be available to out-of-cluster traffic until we create an ingress resource. We can save something like the below into a YAML file and then use the kubectl tool to create the resource.
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: basic-ingress
spec:
backend:
serviceName: web
servicePort: 5000
kubectl --namespace=lbtest apply -f myingress.yaml
If you wait for a bit and look up the ingress resource
kubectl --namespace=lbtest get ing basic-ingress
should see an external IP address, which you can navigate to with your browser to check that everything is ok.
This is as far as the Google Cloud HTTP(S) Balancer tutorial takes us. Next it’s time to see if we can enable HTTPS on this test service.
Creating certificates with Let’s Encrypt
Like LH Fong mentions in their article, the Google Cloud and Kubernetes docs mention that you can enable HTTPS on an outward facing service by creating a Kubernetes secret and then referencing this secret in the Ingress tls
section.
tls:
- secretName: testtls-secret
From LH Fong’s article, it seemed that it would be possible to specify both the secret and ingress in the same file like this
#[source](https://estl.tech/configuring-https-to-a-web-service-on-google-kubernetes-engine-2d71849520d)
apiVersion: v1
data:
tls.crt: base64 encoded cert
tls.key: base64 encoded private key
kind: Secret
metadata:
name: sslcerts
namespace: default
type: Opaque
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: tls-ingress
spec:
tls:
- secretName: sslcerts
backend:
serviceName: cm-web
servicePort: 80
but I decided not to try this approach, because I ran into some errors while trying to paste the contents of the cert and private key into the file. Instead, I created the secret in a separate step and then referenced it in the Ingress.
Before we can move on to that step, we need to use Let’s Encrypt (or another certificate authority) to obtain a certificate. Usually, this is automated in some fashion to minimize the manual work involved, but for now, I’ll just obtain a single certificate following the instructions in LH Fong’s article.
Install certbot. On my current Linux system, you can do this with
sudo apt-get install certbot
After that, follow the instructions in LH Fong’s artcle and run the interactive certbot
wizard.
At some point, the wizard will prompt you to create a TXT record in the form of _acme-challenge.DOMAIN_NAME
with your domain registrar (for me this was Route 53) and will pause until you confirm that the record has been created
and the name server is correctly responding to it.
Please deploy a DNS TXT record under the name
_acme-challenge.lipcolourmatch.com with the following value:
<random value>
Before continuing, verify the record is deployed.
Verifying the Let’s Encrypt challenge records
You can verify that the DNS TXT record has been created by using a Linux utility called dig
.
dig @8.8.8.8 TXT _acme-challenge.DOMAINNAME.com
I tried this with a domain that had the challenge secret and a domain that didn’t just to see the difference in the responses from dig
.
A domain that has the challenge record successfully deployed will have an ANSWER section like the one below
;; ANSWER SECTION:
_acme-challenge.DOMAINNAME.com. 299 IN TXT <some random string>
A domain that does not have the challenge record will not have an ANSWER section.
Random certbot failure
While trying to obtain a certificate from Let’s Encrypt, certbot
exited with the following error.
I simply re-ran the command and it was able to successfully complete, but I wanted
to record this stacktrace in case someone else encounters it.
2019-10-20 17:53:07,569:DEBUG:certbot.error_handler:Encountered exception:
Traceback (most recent call last):
File "/usr/lib/python2.7/dist-packages/certbot/auth_handler.py", line 73, in handle_authorizations
resp = self._solve_challenges(aauthzrs)
File "/usr/lib/python2.7/dist-packages/certbot/auth_handler.py", line 124, in _solve_challenges
resp = self.auth.perform(all_achalls)
File "/usr/lib/python2.7/dist-packages/certbot/plugins/manual.py", line 154, in perform
perform_achall(achall)
File "/usr/lib/python2.7/dist-packages/certbot/plugins/manual.py", line 213, in _perform_achall_manually
display.notification(msg, wrap=False, force_interactive=True)
File "/usr/lib/python2.7/dist-packages/certbot/display/util.py", line 123, in notification
input_with_timeout("Press Enter to Continue")
File "/usr/lib/python2.7/dist-packages/certbot/display/util.py", line 79, in input_with_timeout
rlist, _, _ = select.select([sys.stdin], [], [], timeout)
error: (4, 'Interrupted system call')
Creating a Kubernetes Secret
After certbot
successfuly completes, you should have two important files fullchain.pem
and privkey.pem
.
You can then create a Kubernetes Secret by giving it a name and pointing it to the paths of the
the files.
kubectl --namespace=namespacename create secret tls secretname --key /path/to/privkey.pem --cert /path/to/fullchain.pem
You should receive a response like this
secret "secretname" created
After that, it is time to update the Ingress resource with the name of the secret. I already had an existing ingress, so I exported it to a YAML
kubectl --namespace=namespacename get ingress ingressname --export -o yaml > ingressname.yml
opened the file and added the name of the Kubernetes Secret storing the keyfiles.
Here is an abridged YAML snippet.
spec:
rules: [insert rules here]
tls:
- secretName: secretname
Finally I saved the file and replaced the existing ingress with the new one using this command
kubectl --namespace=lipcolourmatch replace -f testing.yml --save-config
It might take a few minutes, but evetually you should see the lock item when you navigate to https.