Articles

New Website

Welcome to my new website. 👋🌍🖱️ This is a statically hosted website that replaces my previous one. The old website loaded content dynamically from various sources, and was made from scratch. Read about it in the old post. The source code is still available. To reduce hosting costs, I deleted my Kubernetes cluster which ran mostly obsolete projects and moved to a combination of Netlify for static sites, and a simple sub 10€ VPS on Greenhost.

Hosting Irssi in the Cloud

This post goes into the journey and reasons of why and how I am currently hosting the IRC client Irssi in my Kubernetes Cluster. Permanently Connected If you use IRC you know it is a badge of honor or rite of passage to have your nick always be online, and ideally also have access to the log of messages that happened while you were gone, as this is not the case by default in IRC, contrary to many new messaging platforms such as Discord.

How to Fix Empty Project in SonarQube Docker

This post shows one way to fix an issue that may cause a SonarQube project to show up as empty only when the scan is run in a container environment. TL;DR Concerning the issue: “This project is empty” in the SonarQube UI: SonarQube does not like being run as the root user. Specify a different user to be used in the container, and run the scan from this user’s home directory.

My Own Kubernetes Dashboard

I recently made my own dashboard displaying some information about my Kubernetes cluster. In this post I will gather some thoughts about the reasoning of why I built it, and some information about the technical details. The dashboard is currently deployed at k8s-dashboard.lolei.dev. Why - History I wanted to have a dashboard for my Kubernetes cluster, so I can see and monitor various information about the infrastructure at a glance, without having to have access to a terminal or the cloud provider interface.

How This Website Works

I have created this website as a way for me to play around with different technologies. It is not intended to be a masterpiece, especially on the UI design-end, but it has a few neat features you may be interested to read about. The following sections list some aspects of the website and its deployment. Frontend React.js and Next.js are used as web-frameworks (using TypeScript, of course). They may seem overkill for a website as simple as this one, but as it also handles dymanic content they are a good fit.

How This Website Works

Planted June 24, 2021

I have created this website as a way for me to play around with different technologies. It is not intended to be a masterpiece, especially on the UI design-end, but it has a few neat features you may be interested to read about. The following sections list some aspects of the website and its deployment.

Frontend

React.js and Next.js are used as web-frameworks (using TypeScript, of course). They may seem overkill for a website as simple as this one, but as it also handles dymanic content they are a good fit. More of this in the Backend section.

The main points about the frontend I want to touch on is that it is intended to be minimal and light weight, readable and responsive.

Styling

A few media queries are enough to make sure that the site also works on mobile devices. All of the styling is done using SCSS, which supports features like mixins to avoid deduplication, for example for the media queries. CSS grid is used for the main layout and the semantic HTML elements and of course flexbox for many centering use cases.

Pages

Each page after the root page / is defined in Next.js’s pages directory, which makes it serve them as distinct pages while still using JavaScript loading and URL replacement instead of forcing a slower full browser-reload on a page change.

pages
├── 404.tsx
├── about.tsx
├── api
│   └── health.ts
├── _app.tsx
├── index.tsx
├── portfolio
│   ├── index.tsx
│   └── legacy.tsx
└── posts
    ├── index.tsx
    └── [pid].tsx

Email Obfuscation

On my previous website I had the problem that my email address was easily parsed/scraped by spam bots, resulting in many spam emails. This time I wanted to avoid that and obfuscate the address, but without impacting usability for real users. Methods such as inserting [at] instead of @ were therefore discarded. A good solution is provided by react-obfuscate, which works by having the email address value reversed in the HTML, and only when the mailto link is hovered or clicked, the value is re-reversed via CSS. This should in theory prevent bots from getting the address. I will observe the effectiveness of this solution, although it may prove difficult, since I’m sure that my address is already in many spam databases.

<div className={styles.contact}>
  <span>Email:</span>{" "}
  <Obfuscate
    email={props.email}
    headers={{
      subject: "Contact from Homepage",
    }}
  />
</div>

Markdown Rendering

Since the posts are retrieved from an external source, a convenient markup language in which to write the posts is Markdown. The Markdown could of course be converted to HTML with something like pandoc, but since this is a React project the component react-markdown is used for that purpose.

This is again a reason why React is useful even on smaller websites, as these utility components are very easy to install and a hassle to replicate on your own.

// components includes some more code in order to enable syntax highlighting
<ReactMarkdown plugins={[gfm]} components={components}>
  {props.postData.content}
</ReactMarkdown>

Backend

Next.js also serves as the server/backend for the website. I’ve touched on Next.js’s routing in the previous section.

Data Fetching

Another feature used is Next.js’s data-fetching getServerSideProps method, which lets you define code to retrieve data in a component in the pages directory, which can then be passed to that component. This code is however executed on the server only and not included in the browser bundle. It is also used to pre-fetch the data on actions like hovering over a link. This reduces the time-to-interaction, as a page does not have to wait for all data to be loaded until it is displayed and/or the elements of the page are filled in.

// Fetching the post list
export const getServerSideProps: GetServerSideProps = async () => {
  const database = Cache.Instance;

  const repopulateIfNecessary = async (): Promise<void> => {
    if (await database.datastorePostList.needsRepopulate()) {
      await database.datastorePostList.populate();
    }
  };

  const posts = await database.datastorePostList.getAll();
  repopulateIfNecessary();

  if (posts == null || posts.length === 0) {
    return {
      notFound: true,
    };
  }

  return {
    props: {
      postListings: posts.sort((a, b) => {
        return a.name > b.name ? -1 : 1;
      }),
    },
  };
};

Dynamic Elements

The current dynamic elements are the posts and the items in the portfolio. The posts are retrieved from a repository of Markdown files, the portfolio items from a JSON document containing links to GitHub and GitLab repositories, from which the information like the description, stars and the main language is fetched. This is done via the GitHub and GitLab API, within the previously mentioned Next.js server-side data fetching methods. However, since those APIs impose rate limits on their users the results of an API fetch is stored in an in-memory database, and only updated once a certain period of time has passed and a new request is made to that section of that website. This makes sure the API limits are never reached. Since this “changing” data is retrieved from outside of the website, adding posts for example is as easy as committing a new Markdown file to the posts repository.

CI/CD

GitHub Actions

Github Actions is used to enable CI/CD. There are two workflows, one which is run on every push to master and on pull requests to master from non-fork sources, which has a few preliminary checks such as formatting, linting, building and running tests, and another workflow, which runs when tags are pushed via bash2version, which builds the application into a container image, pushes it to the GitHub Container Registry, and deploys the new image to the Kubernetes cluster, which is described in further detail in the next top-level section.

Container

The containerfile is based on the one officially recommended by Next.js’s documentation. It uses a multi-stage build in order to make the image as small as possible. In addition to that, since when building the image in a fresh virtual environment within a GitHub Actions workflow layer caching cannot be conventionally used, the build-push-action is utilized, which uses buildx/BuildKit to enable caching again.

The ability to easily include community-made “actions” in your workflow is one very convenient feature of GitHub Actions, and a couple of other ones are used as well.

The built image is automatically pushed to the GitHub Container Registry as a package, from which it can be pulled by Kubernetes, or other users.

Deployment

The main Kubernetes deployment manifest consists of a fairly standard Deployment, Service and Ingress setup. The more noteworthy parts out pointed out below.

Secrets

One interesting aspect is the usage of sealed secrets. Since I wanted a way to also track my Kubernetes secrets with git, but storing them publicly would be insecure due to plainly visible base64-encoded secret values, I found sealed-secrets which provides a solution that encrypts the secret values. These encrypted secrets can be pushed to a public repository. In the cluster they can be used as “recipes” to create real secrets, since the cluster-localized sealed-secrets controller has the key to decrypt and “unseal” the values.

apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  creationTimestamp: null
  name: homepage-secret
  namespace: default
spec:
  encryptedData:
    GITHUB_PAT: AgAD3k7z3I3QimLVBujPitFm9bLzot6B8vCoYE1lxxjSMPFW/v...
  template:
    data: null
    metadata:
      annotations:
        sealedsecrets.bitnami.com/managed: "true"
      creationTimestamp: null
      name: homepage-secret
      namespace: default
    type: Opaque

SSL Certificates

Let’s Encrypt’s SSL certificates are handled by cert-manager. Provided a cluster issuer, it creates a certificate and secret for each Ingress resource that specifies cert-manager.io/cluster-issuer: <issuer_name> in its annotations.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: homepage-ingress
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  tls:
    - hosts:
        - lolei.dev
      secretName: homepage-tls # This secret will be created automatically
  rules:
    - host: lolei.dev
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: homepage-svc
                port:
                  number: 80

Conclusion

I hope this gives some insight into the inner-workings of this website and why I chose to build it. In the future it may be used to host more blog posts, however it may also be taken down temporarily in order to save hosting costs, because realistically, not many people are constantly looking at my content. At least not at the moment 😉.