Shami's Blog

DevOps because uptime is not optional

Automating Hugo Deployments With Webhooks, Bitbucket, and CloudFlare

I’ve been using Hugo for a while now and love it. I don’t update my blog much and with Hugo I don’t need to spend more time updating the CMS than actually blogging. The markdown files are hosted at BitBucket along with my other code and I use CloudFlare for protection as well as a CDN.

The process for adding new content was as follows:

  • Make change
  • Push to BitBucket
  • Log in to server
  • Pull changes
  • Generate the blog files
  • Compress static content to prevent nginx from having to compress files for each request
  • Clear CloudFlare cache

This is a relatively easy process, but I found it troublesome to have to log in to the server to publish my updates, and then clear the CloudFlare cache. I wanted to build my own webhook but always feared it might be insecure since I don’t know much web development.

Recntly my friends at Tarent introduced me to Go . It’s a very interesting language and I’m currently dabbling with it to see if it’s possible to use in production. While looking at the language I found Awesome Go . One of the projects I checked was Webhook . Looked simple enough and decided to give it a go.

First, the trigger, taken from the example page

 1[
 2  {
 3    "id": "AWESOME_HOOK",
 4    "execute-command": "/usr/local/bin/deploy.sh",
 5    "command-working-directory": "/root",
 6    "trigger-rule":
 7    {
 8      "match":
 9      {
10        "type": "ip-whitelist",
11        "ip-range": "127.0.0.1"
12      }
13    }
14  }
15]

You will notice that I set ip-range to localhost instead of the list provided by the example, this is because i will set up the restriction in nginx.

Now lets look at deploy.sh:

 1#!/bin/sh
 2
 3# This is not portable, but it works even if the env was not set
 4GIT=/usr/local/bin/git
 5TEMPPATH=/usr/local/www/shami.blog.hugo
 6WEBROOT=/usr/local/www/shami.blog
 7CURL=/usr/local/bin/curl
 8CHOWN=/usr/sbin/chown
 9FIND=/usr/bin/find
10GZIP=/usr/bin/gzip
11HUGO=/usr/local/bin/hugo
12
13if [ -d $TEMPPATH ]
14then
15	# If the Hugo source folder exists, just pull
16	cd $TEMPPATH
17	$GIT pull
18else
19	# If the Hugo source folder doesn't exists, clone
20	$GIT clone [email protected]:USER/REPO $TEMPPATH
21fi
22
23cd $TEMPPATH
24# Generate the blog files
25$HUGO --quiet
26
27# Pre-compress all files to make nginx work less
28$FIND $WEBROOT -type f \( -name '*.html' -o -name '*.js' -o -name '*.css' -o -name '*.xml' -o -name '*.svg' \) -exec $GZIP -k -f --best {} \;
29
30# Set up permissions
31$CHOWN -R www:www $WEBROOT
32
33# Clear the CloudFlare cache
34$CURL -X DELETE "https://api.cloudflare.com/client/v4/zones/CLOUDFLARE_ZONE_ID/purge_cache" \
35     -H "X-Auth-Email: CLOUDFLARE_EMAIL_ADDRESS" \
36     -H "X-Auth-Key: CLOUDFLARE_AUTH_KEY" \
37     -H "Content-Type: application/json" \
38     --data '{"purge_everything":true}'

Site configuration (Only the part to proxy the requests to Webhook):

 1	#
 2	location /hooks/AWESOME_HOOK {
 3		# Cloudflare servers
 4		set_real_ip_from 103.21.244.0/22;
 5		set_real_ip_from 103.22.200.0/22;
 6		set_real_ip_from 103.31.4.0/22;
 7		set_real_ip_from 104.16.0.0/12;
 8		set_real_ip_from 108.162.192.0/18;
 9		set_real_ip_from 131.0.72.0/22;
10		set_real_ip_from 141.101.64.0/18;
11		set_real_ip_from 162.158.0.0/15;
12		set_real_ip_from 172.64.0.0/13;
13		set_real_ip_from 173.245.48.0/20;
14		set_real_ip_from 188.114.96.0/20;
15		set_real_ip_from 190.93.240.0/20;
16		set_real_ip_from 197.234.240.0/22;
17		set_real_ip_from 198.41.128.0/17;
18		real_ip_header    X-Forwarded-For;
19
20		# Only allow requests to the webhook from BitBucket
21		allow 104.192.143.0/24;
22		deny all;
23
24		# Forward the request to webhook
25		proxy_set_header X-Forwarded-Host $host;
26		proxy_set_header X-Forwarded-Server $host;
27		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
28		proxy_set_header Host $host;
29		proxy_pass http://127.0.0.1:9000;
30		client_max_body_size 1M;
31	}

set_real_ip_from and real_ip_header tell nginx that CloudFlare will set the X-Forwarded-For header to the IP address of the client, this allows us to use the allow and deny directives as if clients were connecting directly.

All you need now is to run webhook inside tmux or something similar, then configure your webhook settings in BitBucket.

1/usr/local/bin/webhook -verbose -hooks /usr/local/etc/webhook.json

About Me

Dev gone Ops gone DevOps. Any views expressed on this blog are mine alone and do not necessarily reflect the views of my employer.

Categories