Infrastructure as Code on Google Cloud Platform: Load Balancers

Part 3 in an n part series on what I’ve learned using Google’s deployment-manager (GDM)

It’s been a while since my last post, and I could blame it on the holidays or my health or family, but I won’t. The real reason is procrastination.

Little did I know when I started this journey that load balancers would be this challenging to build in GDM. As it turns out, there is no such beast as a load balancer when you build it this way. Google Cloud Console walks you through a nice setup process, but under the hood there’s a lot more going on. As we walk through building an HTTP(S) load balancer, we’ll get to learn about building all sorts of nifty bits and pieces.

First (and simplest) thing first. A load balancer is of no value without an IP address, so let’s make one of those. This is probably the easiest resource you could create. Don’t forget to add that to your outputs: section in order to learn what that IP address actually is.

resources:
###################################
# IP addresses
###################################
- name: dav-ipaddress
type: compute.v1.globalAddress
outputs:
- name: publicIP
value: $(ref.dav-ipaddress.address)

It seems a little counterintuitive, but now we need to build a health check. This will be used by the load balancer to determine when the backend service is healthy.

###################################
# Health Checks
###################################
- name: dav-health-check
type: compute.v1.httpHealthCheck
properties:
requestPath: /robots.txt
port: 80
checkIntervalSec: 5
timeoutSec: 5
unhealthyThreshold: 2
healthyThreshold: 2

Now let’s create the backend services that will host our app. The backend service refers to an Google Compute Engine instance group, and for today’s purposes let’s assume that it has already been created. In your backend definition you can specify port and portName, but port has been deprecated in favor of portName which refers to a named port on the instance group that we will also assume has already been created. Notice that I’m using the same health check for both backends — as long as all the settings will work on both instance groups.

###################################
# Backend Services
###################################
- name: dav-backend-svc-1
type: compute.v1.backendService
properties:
backends:
- group: 'https://www.googleapis.com/compute/v1/projects/dav-project/zones/us-central1-b/instanceGroups/dav-instance-group-1'
maxUtilization: 0.8
healthChecks:
- $(ref.dav-health-check.selfLink)
portName: dav-named-port-1
- name: dav-backend-svc-2
type: compute.v1.backendService
properties:
backends:
- group: 'https://www.googleapis.com/compute/v1/projects/dav-project/zones/us-east1-b/instanceGroups/dav-instance-group-2'
maxUtilization: 0.8
healthChecks:
- $(ref.dav-health-check.selfLink)
portName: dav-named-port-2

Now we need to build a URL map which will route requests to the correct backend service. In the example below, paths matching /api will route to dav-backend-svc-2 and all other requests will route to dav-backend-svc-1.

###################################
# URL Maps
###################################
- name: dav-url-map
type: compute.v1.urlMap
properties:
defaultService: $(ref.dav-backend-svc-1.selfLink)
hostRules:
- hosts:
- dav.example.com
pathMatcher: path-matcher-1
pathMatchers:
- defaultService: $(ref.dav-backend-svc-1.selfLink)
name: path-matcher-1
pathRules:
- paths:
- /
service: $(ref.dav-backend-svc-1.selfLink)
- /api
service: $(ref.dav-backend-svc-2.selfLink)

And now we’ll set up the HTTP(S) proxies. With the configuration below, the load balancer will route either HTTP or HTTPS requests to the appropriate backend. The SSL certificate (which we assume already exists in GCP) will be hosted by the load balancer so you don’t need to on all of your instances. The load balancer will add the x-forwarded-proto http header to all requests, and you can configure nginx or apache (or your HTTP server of choice) to force SSL based on that header if you so desire. Remember NOT to redirect your health check path to HTTPS if you decide to redirect!

###################################
# HTTP Proxies
###################################
- name: dav-http-proxy
type: compute.v1.targetHttpProxy
properties:
urlMap: $(ref.dav-url-map.selfLink)
- name: dav-https-proxy
type: compute.v1.targetHttpsProxy
properties:
urlMap: $(ref.dav-url-map.selfLink)
sslCertificates:
- 'https://www.googleapis.com/compute/v1/projects/dav-project/global/sslCertificates/star-dav-example-com'

Finally we need to set up network forwarding rules to route traffic coming into our static IP that we created (remember that way up at the beginning?) to the appropriate HTTP(S) proxy.

###################################
# Network Forwarding Rules
###################################
- name: dav-http-forwardingrule
type: compute.v1.globalForwardingRule
properties:
target: $(ref.dav-http-proxy.selfLink)
IPAddress: $(ref.dav-ipaddress.address)
IPProtocol: TCP
portRange: 80-80
- name: dav-https-forwardingrule
type: compute.v1.globalForwardingRule
properties:
target: $(ref.dav-https-proxy.selfLink)
IPAddress: $(ref.dav-ipaddress.address)
IPProtocol: TCP
portRange: 443-443

Yay! We’re done! Wait — you say it doesn’t work for you? Just when we though we were done, there’s one more thing to do. We need firewall rules to allow all of this traffic to move around the project. In the example below, ports: refers to the port number of the named ports on our backend services. The entry in sourceRanges: below is the CIDR address range used by Google for all of their load balancers, so you can use that value as it exists below.

###################################
# Firewall Rules
###################################
- name: allow-port-80
type: compute.v1.firewall
properties:
allowed:
- IPProtocol: TCP
ports: [ 80 ]
sourceRanges: [ 130.211.0.0/22 ]

NOW we’re done. After applying the configuration, all HTTP(S) traffic to dav.example.com will now route to one of our 2 backends based on the URL requested. So you can see the whole shebang as a single config, here it is all put together.

resources:
###################################
# IP addresses
###################################
- name: dav-ipaddress
type: compute.v1.globalAddress
###################################
# Health Checks
###################################
- name: dav-health-check
type: compute.v1.httpHealthCheck
properties:
requestPath: /robots.txt
port: 80
checkIntervalSec: 5
timeoutSec: 5
unhealthyThreshold: 2
healthyThreshold: 2
###################################
# Backend Services
###################################
- name: dav-backend-svc-1
type: compute.v1.backendService
properties:
backends:
- group: 'https://www.googleapis.com/compute/v1/projects/dav-project/zones/us-central1-b/instanceGroups/dav-instance-group-1'
maxUtilization: 0.8
healthChecks:
- $(ref.dav-health-check.selfLink)
portName: dav-named-port-1
- name: dav-backend-svc-2
type: compute.v1.backendService
properties:
backends:
- group: 'https://www.googleapis.com/compute/v1/projects/dav-project/zones/us-east1-b/instanceGroups/dav-instance-group-2'
maxUtilization: 0.8
healthChecks:
- $(ref.dav-health-check.selfLink)
portName: dav-named-port-2
###################################
# URL Maps
###################################
- name: dav-url-map
type: compute.v1.urlMap
properties:
defaultService: $(ref.dav-backend-svc-1.selfLink)
hostRules:
- hosts:
- dav.example.com
pathMatcher: path-matcher-1
pathMatchers:
- defaultService: $(ref.dav-backend-svc-1.selfLink)
name: path-matcher-1
pathRules:
- paths:
- /
service: $(ref.dav-backend-svc-1.selfLink)
- /api
service: $(ref.dav-backend-svc-2.selfLink)
###################################
# HTTP Proxies
###################################
- name: dav-http-proxy
type: compute.v1.targetHttpProxy
properties:
urlMap: $(ref.dav-url-map.selfLink)
- name: dav-https-proxy
type: compute.v1.targetHttpsProxy
properties:
urlMap: $(ref.dav-url-map.selfLink)
sslCertificates:
- 'https://www.googleapis.com/compute/v1/projects/dav-project/global/sslCertificates/star-dav-example-com'
###################################
# Network Forwarding Rules
###################################
- name: dav-http-forwardingrule
type: compute.v1.globalForwardingRule
properties:
target: $(ref.dav-http-proxy.selfLink)
IPAddress: $(ref.dav-ipaddress.address)
IPProtocol: TCP
portRange: 80-80
- name: dav-https-forwardingrule
type: compute.v1.globalForwardingRule
properties:
target: $(ref.dav-https-proxy.selfLink)
IPAddress: $(ref.dav-ipaddress.address)
IPProtocol: TCP
portRange: 443-443
###################################
# Firewall Rules
###################################
- name: allow-port-80
type: compute.v1.firewall
properties:
allowed:
- IPProtocol: TCP
ports: [ 80 ]
sourceRanges: [ 130.211.0.0/22 ]
outputs:
- name: publicIP
value: $(ref.dav-ipaddress.address)

You may have noticed that I used references for any resources created within this configuration. This is a good practice to follow because it will A) create implicit dependencies within your configuration so that resources are not created prior to the resources they rely upon, and B) you won’t need to try and predict those values or run your build in multiple steps.

If you managed to make it this far, you’re probably pretty committed to using deployment-manager. If that’s the case, stay tuned for my next periodic installment in this series. Over the last week or so I’ve been learning how to use GDM templates, and once I get a bit further I will post about that experience.