I remember when I started blogging in 2008. It was a nice learning experience, and it was rewarding, too – both the challenge of formulating my thoughts clearly and the feedback I received, and still am receiving, on some of the oldest blog posts. I have collected some of my blog posts under my own website me-dba.com, I don’t have too many views a month, but that doesn’t mean the blog has been inactive. I’ve used my blogging site for learning, too, and I’ve changed its backend several times trying to find a convenient and inexpensive way of running it without sacrificing the availability or scalability, and trying to find a solution that I was happy with.
This post is the first half of a two-part article where I’ll explain the current technical implementation of my blog site. The first part will explain how I implemented the “Blog as Code” principle to remove the dependencies on specific services and platforms. The second part will explain how the blog site was set up on Google Cloud Platform in a way that requires minimal cost and maintenance overhead. I believe the implementation is quite modern and interesting, and I’d be happy if these instructions helped at least one person to start his or her blogging journey.
It all started back in 2008 with wordpress.com’s free blog (it used a subdomain of wordpress.com, which I didn’t like). Then I purchased the wordpress.com’s subscription for $5/month to be able to use my own domain (I thought it was too expensive for just a few hundred views a month). After that, I installed WordPress on my NAS and hosted the blog from there (but it was too unreliable, and I had quite a few downtimes). The cloud era came and I migrated my WordPress installation to AWS EC2, which worked fine (but I still had to manage it by applying the updates).
Some time ago, I started learning AWS and GCP for my certifications, and I came up with a challenge to run my simple blog with the following in mind:
- It needed to be free (or as inexpensive as possible – not counting the domain costs).
- Accessible over HTTPS only (https is the new normal, why stick with anything less?). Let’s Encrypt SSL/TLS certificates are available for free!
- The implementation should be scalable (not that I’d require it at the moment, but I want to build it future-proof). :)
- Use some of the modern DevOps things I’ve been learning too (git, serverless, continuous deployment, Docker, etc).
- No maintenance overhead for the services that I use (no manual updates).
- Posting a new blog post should be painless and simple (without activities like “connect there, do this, then check that, …”)
I quickly figured out that I needed to simplify the whole implementation to have more options on the desk. I started by experimenting with Jekyll, which in the end allowed me to convert the blog to static web pages and get rid of the heavy WordPress platform. I tried hosting the blog from an AWS S3 Bucket with Route 53 or a GCP Cloud Storage Bucket with HTTP Load Balancer. I looked at GCP’s Cloud (HTTP) Functions and Triggers, I tried running it from a tiny VM instance, and I even tried GitHub Pages, but there was always one inefficiency or another to work around: no SSL support, cost increase, no scaling, insufficient theme support, etc.
My final solution, described here, is a serverless deployment of a static webpage blog that’s generated from the blog’s source stored in a GCP Cloud Repository using Jekyll. GCP Cloud Build Trigger is used to generate the new static web pages automatically upon a commit to the blog’s source repository master branch, and it also deploys the new version to the GCP App Engine that actually serves the web pages. The whole solution fits under the GCP Free Tier’s Always Free limits unless the visitor numbers grow very quickly, and even then this could be a very cost-effective solution. Of course, you should review the description of the Always Free limits carefully to make sure your region/service/configuration is covered before you configure your own blog.
Here’s a quick diagram on how different services interact to automatically deploy the new blog posts after the whole solution is implemented.
The rest of this article will go through the configuration details, hopefully in sufficient detail for you to follow and set up your own blogging platform!
Blog as Code
Even when I had a WordPress blog, I used a text editor instead of the WYSIWYG editor. I frequently ran into formatting issues with the latter and would switch to the text editor to have more control over what was going on. I discovered Markdown later and really liked the cleanliness of the text files that could be translated into HTML. I even installed some WordPress plugins to support Markdown for blogging.
Having already familiarized myself with Markdown, the next logical step to take was “Blog as Code.” The main idea was to keep everything that’s required for rendering the whole blog in a Git repository. It makes a lot of sense to me not only for the usual benefits Git provides (you can google “why should I use Git” to discover more about it), but also because if this worked, I’d be able to get rid of the database backend and the heavier application tier (WordPress), which would simplify everything a lot.
Jekyll – A Static Site Generator
There are many different options for static site generators, but Jekyll is the most popular at the moment, which is why I chose it. It’s quite simple to set up locally, but since I’m going to use a Docker container later, it makes sense to start using it now. I’ll be using the jekyll/jekyll:latest Docker image.
- Setting up Docker is out of scope for this article, but it needs to be done before continuing.
- Download the latest Jekyll image and create an empty folder for the source code.
$ docker pull jekyll/jekyll $ mkdir tempblog.me-dba.com $ cd tempblog.me-dba.com
- Start a container and obtain an interactive bash session, and create the initial blog site structure by running
jekyll new, then run
jekyll serveto test the blog. Notice that the current location (tempblog.me-dba.com) is attached as “/srv/jekyll” to the container, and the created files will be located outside the container.
jekyll newwill install all the required dependencies, ruby gems and themes, so this might take a while.
$ docker run --rm --publish 4000:4000 --volume="$PWD:/srv/jekyll" --privileged -it jekyll/jekyll:latest bash $ jekyll new /srv/jekyll $ jekyll serve
- At this point, the blog should be accessible on the Docker host ip, port 4000. In my case the IP was 192.168.33.13, so the following two URLs served the demo pages included in the new blog structure (in your case the IP may differ): http://192.168.33.13:4000/ – is the front page of your blog.
- We’ll use this moment to commit the container changes to the image from another session on our Docker host machine. Thus, all the downloaded items will be added as a new layer to the Jekyll’s Docker image, and they won’t need to be downloaded again when we start a container next time. Note: the container ID will be different in your case.
# from another session $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 04418b39cec7 e220e1251707 "/usr/jekyll/bin/ent…" 23 minutes ago Up 23 minutes 0.0.0.0:4000->4000/tcp, 35729/tcp laughing_benz # Use the ID from the previous output $ docker commit -m "created by adding all Gems" 04418b39cec7 jekyll/jekyll:latest sha256:9667c6cbe572e78a8d3b552286b21eb0e34f8264c6c3c7c2b63850680b6cc891
- Next, switch back to the first session, and use
Ctrl-Cto terminate the
jekyll servefollowed by
exitto exit and remove the container.
At this stage, we have generated the skeleton of the source code of the blog. The structure of the current directory should look similar to this :
$ find . . ./.gitignore ./.sass-cache ./.sass-cache/04702c00cd9fa60073a601ea6f7a61c45d2d95af ./.sass-cache/04702c00cd9fa60073a601ea6f7a61c45d2d95af/minima.scssc ./.sass-cache/2644ddacc47d466a8fd18366436f273677dc9e09 ./.sass-cache/2644ddacc47d466a8fd18366436f273677dc9e09/_base.scssc ./.sass-cache/2644ddacc47d466a8fd18366436f273677dc9e09/_layout.scssc ./.sass-cache/2644ddacc47d466a8fd18366436f273677dc9e09/_syntax-highlighting.scssc ./404.html ./_config.yml ./_posts ./_posts/2019-01-21-welcome-to-jekyll.markdown ./about.md ./index.md ./Gemfile ./Gemfile.lock ./_site ./_site/404.html ./_site/about ./_site/about/index.html ./_site/index.html ./_site/assets ./_site/assets/main.css ./_site/assets/minima-social-icons.svg ./_site/feed.xml ./_site/jekyll ./_site/jekyll/update ./_site/jekyll/update/2019 ./_site/jekyll/update/2019/01 ./_site/jekyll/update/2019/01/21 ./_site/jekyll/update/2019/01/21/welcome-to-jekyll.html
Only the highlighted files are the source code of the blog. The
_site directory is the generated version of the static pages, and
.sass-cache is some caching mechanism that Jekyll uses. Both of these locations are excluded in
.gitignore. This is a good point to edit the
_config.yml to customize the blog – change the theme of the blog, add plugins and adjust other configurables. Having completed the modifications, test them with
jekyll serve again (note: the correct use of whitespace indentation is critical in YAML files):
$ cat _config.yml | grep -v "^ *#" title: My Awesome Serverless Blog on GCP email: email@example.com description: >- # this means to ignore newlines until "baseurl:" This is the example blog created by Maris Elsins in 2019 to support the article named "Building a Cost Efficient Serverless Blogging Platform on GCP". baseurl: "" # the subpath of your site, e.g. /blog url: "https://tempblog.me-dba.com" twitter_username: MarisDBA markdown: kramdown theme: minima plugins: - jekyll-feed $ docker run --rm --publish 4000:4000 --volume="$PWD:/srv/jekyll" --privileged -it jekyll/jekyll:latest jekyll serve
Disqus – Adding Comments to the Blog Posts
Native support for the Disqus comment platform will depend on the theme you choose for your blog. In this case, I used the default theme called minima-2.5.0 that already includes the required code to add the Disqus comment threads. I think many themes already include this code, but it’s not really complicated to customize other themes to add the required code too.
Before adding the comment sections to the blog posts, the new site needs to be registered in the Disqus platform. Go to https://disqus.com/admin/create/ (register a new user if you don’t have one yet), and follow these steps to register your new site:
- Choose a name for your site, note the unique Disqus URL as its first part is the Disqus shortname that will be required later in Step 4 (in this case it’s “tempblog-me-dba-com”):
- If you’re presented with the choice, pick “Jekyll” as the blogging platform.
- Change the configuration of the site based on your preference, make sure you set the blog URL correctly to your production URL (that still doesn’t serve anything at this moment):
Once the site is registered, the shortname needs to be added to the
disqus: shortname: tempblog-me-dba-com
As instructed in the theme’s
README.md, the site needs to be regenerated in “production mode” by setting the
JEKYLL_ENV=production before running
jekyll serve or
jekyll build like this:
$ docker run --rm --publish 4000:4000 --volume="$PWD:/srv/jekyll" --privileged -it jekyll/jekyll:latest bash -c "JEKYLL_ENV=production jekyll serve"
And that’s it. The comments section should already be rendered correctly, like this, which means we’ve completed setting up the initial version of the blog’s source code:
GCP Source Repository
For the purposes of this demonstration, I’ll store the blog’s code in GCP Source Repositories. Technically, Github would work too, but later I would be required to grant access privileges to all my repositories to GCP Cloud Builder to automate the deployment of the new blog posts. In my case, I have a few private GitHub repositories which I don’t want to grant this access to, so I use the GCP Source Repository.
Let’s create the repository and commit our code to it! This requires several steps:
- Create a GCP Project, or use an existing project. In my case the name of the project is
- Install Google Cloud SDK on the machine where your source code is located.
- Initialize the configuration of the SDK by running
gcloud init, log on as your user and set the newly created project as the default.
$ gcloud init ... $ gcloud config list [core] account = Maris.Elsins@me-dba.com disable_usage_reporting = True project = tempblog-me-dba ...
- Create the source repository from the command line:
$ gcloud source repos create tempblog-me-dba-repo API [sourcerepo.googleapis.com] not enabled on project . Would you like to enable and retry (this will take a few minutes)? (y/N)? y Enabling service [sourcerepo.googleapis.com] on project ... Waiting for async operation operations/acf.6999a26b-ba23-42d3-b1f0-b97ddced5057 to complete... Operation finished successfully. The following command can describe the Operation details: gcloud services operations describe operations/tmo-acf.6999a26b-ba23-42d3-b1f0-b97ddced5057 API [sourcerepo.googleapis.com] not enabled on project . Would you like to enable and retry (this will take a few minutes)? (y/N)? y Created [tempblog-me-dba-repo]. WARNING: You may be billed for this repository. See https://cloud.google.com/source-repositories/docs/pricing for details.
gitif you haven’t done it yet. I recommend using version 2.0.1+ as it supports Authentication for GCP better. My code was stored on a Centos Machine, so I followed https://tecadmin.net/install-git-2-0-on-centos-rhel-fedora/ to set it up.
- Clone the empty repository from GCP to your workstation:
$ cd $ gcloud source repos clone tempblog-me-dba-repo Cloning into '/home/vagrant/tempblog-me-dba-repo'... Checking connectivity... done. warning: remote HEAD refers to nonexistent ref, unable to checkout. Project [tempblog-me-dba] repository [tempblog-me-dba-repo] was cloned to [/home/vagrant/tempblog-me-dba-repo].
- Copy all the source files from the original location into the cloned repository (it would have been more efficient to create the repository in the beginning). :)
$ cd ~/tempblog.me-dba,com $ cp -vrp ./* ../tempblog-me-dba-repo/ $ cp -vrp .gitignore ../tempblog-me-dba-repo/ $ cp -vrp .sass-cache ../tempblog-me-dba-repo/
- Commit the new files to the master and push them to the remote repository.
$ git add -A $ git status On branch master Initial commit Changes to be committed: (use "git rm --cached ..." to unstage) new file: .gitignore new file: 404.html new file: Gemfile new file: Gemfile.lock new file: _config.yml new file: _posts/2019-01-21-welcome-to-jekyll.markdown new file: about.md new file: index.md $ git commit -m "initial commit" ... $ git push origin master Counting objects: 11, done. Delta compression using up to 2 threads. Compressing objects: 100% (10/10), done. Writing objects: 100% (11/11), 3.90 KiB | 0 bytes/s, done. Total 11 (delta 0), reused 0 (delta 0) To https://source.developers.google.com/p/tempblog-me-dba/r/tempblog-me-dba-repo * [new branch] master -> master
That’s it! You can verify the remote repository was updated by visiting https://source.cloud.google.com/repos.
We’ve come to the end of the first part of the article. We’ve set up all the required tools to generate a static html-based blog site with the help of Jekyll, and we’ve also configured Disqus for the only dynamic content that there is – the comments. This implementation doesn’t require any database behind it so all the configuration and the blog posts itself can be easily stored in a source repository. We’ve also started up the blog locally by using the
jekyll serve from a Docker instance we used in the process. Of course, these are only the preparation tasks. We still need to set up all the infrastructure on GCP to serve the blog and make it available to the public. That’s the story for the second part of this series.
See you soon in Part 2!
Interested in working with Maris? Schedule a tech call.