Fronting legacy services with Kubernetes

There are many benefits to Kubernetes but what’s not discussed so often is how to migrate your services from their legacy hosting to their new home in Kubernetes. Specifically, I’m looking at the case where you have a single server or a single public IP address and you want to run your services on that server with a mixture of legacy hosting and Kubernetes – either permanently or as part of a migration process.

Let’s suppose you are running an application like ownCloud in a standard way, with Apache httpd bound to ports 80 and 443, with port 80 redirecting to port 443 to force HTTPS/SSL. This is how the simplified config might look:

# /etc/httpd/conf.d/owncloud.conf

<VirtualHost *:80>
  ServerName owncloud.example.com

  DocumentRoot "/var/www/html/owncloud"

  # Redirect non-SSL traffic to SSL site
  RewriteEngine On
  RewriteCond %{HTTPS} off
  RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI}

</VirtualHost>

<VirtualHost *:443>
  ServerName owncloud.example.com

  DocumentRoot "/var/www/html/owncloud"

  ## SSL directives
  SSLEngine on
  SSLCertificateFile      /etc/letsencrypt/live/cert.pem
  SSLCertificateKeyFile   /etc/letsencrypt/live/privkey.pem
  SSLCertificateChainFile /etc/letsencrypt/live/chain.pem
  SSLCACertificatePath    /etc/pki/tls/certs

</VirtualHost>

Now suppose you want to add some new services in a one-node Kubernetes solution like MicroK8s. When you add your Ingress resource to start serving your applications, it will complain because it wants to bind to ports 80 and 443, but they are already reserved by your legacy Apache installation.

The neatest solution is to run your legacy application on a high port, without SSL, thus freeing up 80 and 443. Then set up your Kubernetes Ingress and let it bind to 80 and 443, terminate SSL for your legacy application, and proxy onwards to your application without SSL. You’ll be able to add other Kubernetes Service resources on the same Ingress on the same ports with ease – like Apache’s name-based virtual hosting.

Let’s have a look at the revised Apache config for ownCloud. Notice the Listen directive to bind to an arbitrary high port, and the lack of any SSL directives:

# /etc/httpd/conf.d/owncloud.conf

Listen 5678
<VirtualHost *:5678>
  ServerName owncloud.example.com

  DocumentRoot "/var/www/html/owncloud"

</VirtualHost>

Now we must consider how the Kubernetes infrastructure will look. The typical pattern is to use a Service resource to identify where the application is running, and an Ingress resource to expose the Service to the outside world.

Service resources are usually designed to point an applications running inside a Kubernetes cluster, but by setting the type to ExternalName, we can tell Kubernetes that our legacy service is running on localhost. You could consider an ExternalName type Service to be analogous to a DNS CNAME record.

Here’s how we configure it. Note that we don’t yet specify the port:

kind: Service
apiVersion: v1
metadata:
  name: owncloud
spec:
  type: ExternalName
  externalName: localhost

Now that Kubernetes knows it should look on localhost for your legacy ownCloud application, we need to configure the way it will be presented to the outside world. To begin with, we will set up a dumb proxy without SSL. All the relevant bits are in the spec section, which specifies the domain that the app should be served on, and then specifies the Service resource we created earlier, along with the port number.

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: owncloud
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
  - host: owncloud.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: owncloud
          servicePort: 5678

For bonus points, we can use cert-manager and Let’s Encrypt to add SSL, and fully automate the process of issuing SSL certificates. You will need to configure cert-manager in advance – this is beyond the scope of this blog post but there are good docs online. This revised Ingress config is the same as the one above, but with a few extra lines:

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: owncloud
  annotations:
    kubernetes.io/ingress.class: "nginx"
    cert-manager.io/issuer: "letsencrypt-prod"
spec:
  tls:
  - hosts:
    - owncloud.example.com
    secretName: owncloud-tls
  rules:
  - host: owncloud.example.com
    http:
      paths:
      - path: /
        backend:
          serviceName: owncloud
          servicePort: 5678

And that’s it! You can verify the config with the kubectl command:

[jonathan@zeus ~]$ kubectl get service
NAME                   TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
default-http-backend   ClusterIP      10.152.183.41   <none>        80/TCP    87d
kubernetes             ClusterIP      10.152.183.1    <none>        443/TCP   98d
owncloud               ExternalName   <none>          localhost     <none>    87d

[jonathan@zeus ~]$ kubectl get ingress
NAME       HOSTS                  ADDRESS     PORTS     AGE
owncloud   owncloud.example.com   127.0.0.1   80, 443   87d

Now your legacy ownCloud service is available at owncloud.example.com but fronted by Kubernetes, leaving you free to install as many other services in Kubernetes as you like without having to worry about port clashes.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s