Use Terraform to Deploy the Remark42 Commenting System to Kubernetes and Integrate it with a Hugo Website
Building upon our previous work, we will deploy Remark42 on Kubernetes with Terraform and integrate it with your existing Hugo website. Make sure to check out my previous posts about creating a Hugo Website and deploying an Azure Kubernetes Service cluster if you haven’t already.
About Remark42
Remark42 is a self-hosted, lightweight, and simple (yet functional) commenting system, which doesn’t spy on users.
I like simplicity, I am a privacy enthusiast, and I build this blog with this in mind. More popular, hands-off solutions like Disqus offer easier integration and more sophisticated features, like automated spam moderation and advertising. But for my intents, it’s too bloated and invasive. For low-traffic websites like mine, Remark42 is just the better fit.
Preparation
Besides a Hugo website and a Kubernetes cluster, you will have to install the following software on your workstation:
Converting the original docker-compose.yml
The official Remark42 repository includes a docker-compose.yml
that we can download:
curl -O https://raw.githubusercontent.com/umputun/remark42/master/docker-compose.yml
We then run kompose convert
to generate regular Kubernetes YAML manifests from the docker-compose.yml
file:
Kubernetes file "remark-deployment.yaml" created
Kubernetes file "remark-claim0-persistentvolumeclaim.yaml" created
To figure out what resources we need to create, we use the generated manifests as a starting point.
Create a Namespace
First, we create the Namespace
where our Remark42 resources will reside. We add the following Terraform code to a new file named remark42.yml
:
resource "kubernetes_namespace" "remark42" {
metadata {
name = "remark42"
}
}
Deploy the changes by running:
terraform plan -out infrastructure.tfplan
terraform apply infrastructure.tfplan
Create the Persistent Volume Claim
To enable persistence for Remark42, we create a PersistentVolumeClaim
(PVC) resource. Using the generate remark-claim0-persistentvolumeclaim.yaml
file as a blueprint, we can easily derive the Terraform equivalent from it and add it to the remark42.tf
file:
resource "kubernetes_persistent_volume_claim" "remark42" {
metadata {
name = "remark42-pvc"
namespace = kubernetes_namespace.remark42.metadata.0.name
labels = {
"app" = "remark42-pvc"
}
}
spec {
access_modes = ["ReadWriteOnce"]
storage_class_name = "azurefile"
resources {
requests = {
"storage" = "1Gi"
}
}
}
}
AKS comes pre-configured with multiple StorageClass
es. Here, I use the azurefile
storage class to dynamically provision a persistent volume with Azure Files. At the time of this writing, I use a B2ms
-sized VM as a node which is limited to four data disks. Using Azure Files whenever possible helps me circumventing this limitation.
Create ConfigMap and Secret
To store configuration parameters for our Remark42 deployment, we make use of Kubernetes ConfigMap
and Secret
resources.
To store sensitive values, we use Secret
:
resource "kubernetes_secret" "remark42" {
metadata {
name = "remark42-secret"
namespace = kubernetes_namespace.remark42.metadata.0.name
}
data = {
"SECRET" = random_password.remark42_secret.result
}
}
For non-sensitive stuff, we use ConfigMap
:
resource "kubernetes_config_map" "remark42" {
metadata {
name = "remark42-cm"
namespace = kubernetes_namespace.remark42.metadata.0.name
}
data = {
"REMARK_URL" = "https://${cloudflare_record.remark42.hostname}"
"SITE" = "schnerring.net"
}
}
For this post, I have kept the configuration to a minimum:
REMARK_URL
: URL to Remark42 serverSITE
: site name(s)SECRET
: secret key
Check the official documentation or my code on GitHub for more configuration options.
You might have noticed that I reference a cloudflare_record
in the REMARK_URL
part. That’s because I also manage my DNS records with Terraform. The DNS record for remark42.schnerring.net
pointing to the DNS record of my cluster looks like this:
resource "cloudflare_record" "remark42" {
zone_id = cloudflare_zone.schnerring_net.id
name = "remark42"
type = "CNAME"
value = cloudflare_record.traefik.hostname
proxied = true
}
Create the Deployment
Next, we add the Deployment
to the remark42.tf
file, using the remark-deployment.yaml
file as a model. We map the previously defined configuration parameters to environment variables and mount the PVC to /srv/var
.
resource "kubernetes_deployment" "remark42" {
metadata {
name = "remark42-deploy"
namespace = kubernetes_namespace.remark42.metadata.0.name
labels = {
"app" = "remark42"
}
}
spec {
replicas = 1
selector {
match_labels = {
"app" = "remark42"
}
}
strategy {
type = "Recreate"
}
template {
metadata {
labels = {
"app" = "remark42"
}
}
spec {
hostname = "remark42"
restart_policy = "Always"
container {
name = "remark42"
image = "umputun/remark42:${var.remark42_image_version}"
port {
container_port = 8080
}
env {
name = "REMARK_URL"
value_from {
config_map_key_ref {
key = "REMARK_URL"
name = "remark42-cm"
}
}
}
env {
name = "SECRET"
value_from {
secret_key_ref {
key = "SECRET"
name = "remark42-secret"
}
}
}
env {
name = "SITE"
value_from {
config_map_key_ref {
key = "SITE"
name = "remark42-cm"
}
}
}
volume_mount {
mount_path = "/srv/var"
name = "remark42-vol"
}
}
volume {
name = "remark42-vol"
persistent_volume_claim {
claim_name = "remark42-pvc"
}
}
}
}
}
}
Create the Service
To expose our deployment as a network service, we create a Service
resource by adding the following to the remark42.tf
file:
resource "kubernetes_service" "remark42" {
metadata {
name = "remark42-svc"
namespace = kubernetes_namespace.remark42.metadata.0.name
}
spec {
selector = {
"app" = "remark42"
}
port {
name = "http"
port = 80
target_port = 8080
}
}
}
Create the Ingress
I use Traefik 2 as Ingress Controller
in combination with the Traefik Kubernetes Ingress provider. To manage Let’s Encrypt certificates, I use cert-manager. So depending on your cluster configuration, the following steps might differ.
Let us add an Ingress
to the remark42.tf
file, to expose our service to the world:
resource "kubernetes_ingress_v1" "remark42" {
metadata {
name = "remark42-ing"
namespace = kubernetes_namespace.remark42.metadata.0.name
annotations = {
"cert-manager.io/cluster-issuer" = "letsencrypt-production"
"traefik.ingress.kubernetes.io/router.tls" = "true"
}
}
spec {
rule {
host = cloudflare_record.remark42.hostname
http {
path {
path = "/"
backend {
service {
name = "remark42-svc"
port {
number = 80
}
}
}
}
}
}
tls {
hosts = [cloudflare_record.remark42.hostname]
secret_name = "remark42-tls-secret"
}
}
}
Now run terraform apply
to deploy everything. Note that we are using letsencrypt-staging
as cluster issuer. We will have to change this to letsencrypt-production
once we finished testing.
Browse to the demo site at https://remark42.schnerring.net/web and check whether Remark42 works. If you want to post a comment on the demo site, make sure to add the remark
site ID to the SITE
environment variable, separated by a ,
(i.e., schnerring.net,remark
).
Integrate Remark42 with Hugo
To add the Remark42 comment widget to our Hugo site, we have to integrate it with our theme. At the time of this writing, I use the Hello Friend theme, which includes a partial template for the comment section. We add the Remark42 widget to the layouts/partials/comments.html
file:
<div id="remark42"></div>
<script>
var remark_config = {
host: "https://remark42.schnerring.net",
site_id: "schnerring.net",
theme: getHelloFriendTheme(),
show_email_subscription: false,
};
</script>
<script>
!(function (e, n) {
for (var o = 0; o < e.length; o++) {
var r = n.createElement("script"),
c = ".js",
d = n.head || n.body;
"noModule" in r ? ((r.type = "module"), (c = ".mjs")) : (r.async = !0),
(r.defer = !0),
(r.src = remark_config.host + "/web/" + e[o] + c),
d.appendChild(r);
}
})(remark_config.components || ["embed"], document);
</script>
You can find more configuration options for the widget in the Remark42 GitHub README.
Next, we implement the getHelloFriendTheme()
function, so Remark42 loads the correct theme. The Hello Friend theme stores the current theme in the local storage of the browser. Knowing that, implementing the function is pretty straight forward:
<script>
const defaultTheme = "light"; // same as defaultTheme in config.toml
function getHelloFriendTheme() {
const theme = localStorage && localStorage.getItem("theme");
if (!theme) {
return defaultTheme;
} else {
return theme;
}
}
</script>
The last thing we have to take care of is to also toggle the Remark42 theme, when clicking the theme toggle button of the Hello Friend theme. To do so, we register an additional click
event handler to the .theme-toggle
button and call the window.REMARK42.changeTheme()
function:
<script>
const themeToggle = document.querySelector(".theme-toggle");
themeToggle.addEventListener("click", () => {
setTimeout(() => window.REMARK42.changeTheme(getHelloFriendTheme()), 10);
});
</script>
We wait 10 ms before reading from local storage to avoid race conditions. All we have to do now is to enable comments by setting comments: true
via Hugo Front Matter.
What Do You Think?
You can find all the code on my GitHub. I also tagged the commits to make it easier to find the code for future reference:
- schnerring/infrastructure-core (Terraform, tag
v0.5.0
) - schnerring/schnerring.github.io (Hugo, tag
v1.1.0
)
Any feedback in the comments below is appreciated.