/app

Meal planning application

Meal Planner

In our house we plan our meals each week. This was not always the case, but it started when we had kids and needed to be a bit more methodical about shopping. Ideally we shop for food once, and then we have all we need to cook for the week.

This process started on scraps of paper, and then one day we found a week planner template (still paper) and started using that. The picture in the header of this blog article is an example of this.

This is not an article about the virtues of meal planning (which are many and varied), but rather it is about a technology solution for the same job. Being a software engineer as I am, I thought I’d make a small application to help us with this task.

TL;DR

If you are impatient, or just want to get straight to using it, you can head here: https://mealboard.richardjameskendall.com and see the app in action. You will need a Google account to access it.

The code is here: https://github.com/richardjkendall/mealboard

Functional Requirements

The app needs to be able to do some fairly simple things

  • Display a grid for each week, showing the days as columns and meal-times as rows
  • Allow the creation of meals, and the allocation of those meals to specific meal-times on specific days
  • Allows planning for the current week and future weeks
  • Copying meals planned for one week to subsequent weeks
  • A data model which allows multiple users to share and edit boards together, in ‘family’ groups

I thought I’d try to make it look as much like the bits of paper we use as possible.

Non-Functional Requirements

I have a couple of principles I generally like to design for:

  • Separation of concerns for authentication: I want a component outside of the application code to be responsible for understanding and verifying a user’s identity and for passing that identity to my application code
  • Lack of state in components except the persistence layer: so scaling up and down is easy

Design

I decided to use a stack I’m familiar with for this app, which is a React/Redux front-end with a Python/Postgres back-end. I bootstrapped the front-end with create-react-app and the back-end is Flask. I do like to learn something new with each of these that I do, so I also used SQLalchemy and a pre-built integration between SQLalchemy and Flask called (unimaginatively) Flask-SQLAlchemy.

I’m containerising the app to run in a Kubernetes environment, which is the first time I’ll have used it for a public-facing workload. More on that later, because using k8s has given me some strong opinions about it.

Mealboard: high level architecture diagram

The design achieves the second of my principles (no state outside of the persistence layer), but as yet does not deal with authentication.

Authentication

As a rule I don’t like implementing user/identity layers myself. There’s too much scope for error and on top of that most users already have enough usernames and passwords to remember, so I don’t want to add to that.

For a while now I’ve been a big fan of the Keycloak ecosystem and I use it a lot. For this stack I decided not to have a dedicated user layer but instead integrate with an OIDC compliant provider. I have a component I built just for this, which I call ‘OIDC reverse proxy’, it sits in front of a web-app and handles the integration with the OIDC IdP and then passes the connections to the app along with information about the user. This meets my first design goal, as my app just needs to rely on the information sent by the identity proxy layer which sites above.

Mealboard: authentication model diagram

For Mealboard, I’m going to use Google as my IdP, so anybody with a Google account can get access to the application. Having a dedicated Keycloak instance seems overkill, so this will be done directly with the Google OIDC IdP.

You can see the component I use for this here: https://github.com/richardjkendall/oidc-rproxy and the readme contains instructions on how to use it.

External Access

The application needs to be available to the outside world. I could deploy it on public cloud resources with external connectivity, but in this case I’m going to run it on a Kubernetes environment which I host myself. I recently discovered Cloudflare tunnels, and have connected my Kubernetes environment to Cloudflare using tunnels, which then allows me to expose any application in my cluster quite easily, while also gaining the protection of Cloudflare’s security and DDoS mitigation services.

When this is all put together, the approach resembles the architecture described in the diagram below. The application components are deployed on Kubernetes, each layer separated by a ‘service’ construct, which allows for easy scaling. When we need more capacity in a given layer, we can just scale the pods in that layer and the service will handle distributing traffic to those pods. The database is of course an exception to this, it is more complex to scale this layer.

Mealboard: diagram showing k8s architecture

Data Model

As I said in my requirements, I wanted a flexible structure which allowed for multiple people to belong to and manage family meal boards. I settled on the following design for the underlying data model and I used the SQLalchemy declarative system to define it.

Mealboard ER model

Tooling

My code lives on Github, which I’ve written about before, and locally I run the Gocd CI/CD platform. Therefore I’m using Github and Gocd to manage the build process. Gocd is not a popular CI/CD choice, but it is one I’m quite familiar with, and I like the way it works.

I use the Gocd Elastic Agents plugin to create build agents on demand on my Kubernetes cluster.

Building container images inside containers is not super simple, but fortunately the ‘img’ tool exists which supports standalone, rootless image building. You can find the tool here https://github.com/genuinetools/img. There is more information about it in this blog article from the author of the tool: https://blog.jessfraz.com/post/building-container-images-securely-on-kubernetes/

I had some troubles getting this tool working, and in the end I created a hybrid build agent by modifying the docker-gocd-agent-alpine-3.15 image to be built from the r.j3ss.co/img source image. This works because they are both based on alpine. My version of this image is here https://github.com/richardjkendall/docker-gocd-agent-alpine-3.15.

After doing this I realised the issue I had was related to badly configured environment variables, more on that later.

Mealboard CI/CD tooling

Build

Building Smaller Images

The application consists of a front end which I build with yarn (an alternative to npm). The build process downloads tens of thousands of small files which can span multiple gigabytes. If the build is done inside what becomes the application image, you can end up with a bloated image containing large amounts of files that you don’t need.

To combat this I use layers when building my application images. You can see that in the Dockerfile for the mealboard app: https://github.com/richardjkendall/mealboard/blob/main/Dockerfile

This allows you to have multiple ‘containers’ in your Dockerfile and you can then reference them in the copy commands. Here’s an example where I build my UI layer in one container, and then I copy only the resulting built files into my target application container:

FROM node:14 as uibuilder
RUN mkdir -p /build/ui
WORKDIR /build
ADD ui ui

RUN cd ui; yarn install
RUN cd ui; yarn build

FROM ubuntu:focal

… other Dockerfile commands

COPY --from=uibuilder /build/ui/build /app/static/

The COPY line contains the switch --from which allows you to reference one of the earlier layers, in this example the uibuilder container. I can copy the built files into my application container.

Building images on kubernetes

Lots of people build their container images using the docker toolset. Docker runs as a daemon and can be problematic to use in CI/CD environments which run inside containers themselves. My gocd agents run on Kubernetes, and I found the ‘img’ tool which I referenced earlier which can be used to build images without needing the docker daemon.

My initial experiments with this tool did not work, I had build agents based on centos and ubuntu which I tried running img on, but it was always failing with errors when I was running apt-get

#6 [stage-1  2/14] RUN apt-get update
#6 0.411 mkdir /run/runc: permission denied
#6 ERROR: executor failed running [/bin/sh -c apt-get update]: runc did not terminate successfully

To solve this, I tried switching to a goagent image based on alpine which I modified to use a new source image. This new source image was also based on alpine, and had ‘img’ installed and configured to work.

After doing this I still had problems:

#5 sha256:d7bfe07ed8476565a440c2113cc64d7c0409dba8ef761fb3ec019d7e6b5952df 28.57MB / 28.57MB 11.1s done
#5 unpacking docker.io/library/ubuntu:focal@sha256:fd92c36d3cb9b1d027c4d2a72c6bf0125da82425fc2ca37c414d4f010180dc19 0.0s done
#5 ERROR: failed to create temp dir: mkdir /run/user/1000/containerd-mount977164537: permission denied

Which I traced to some missing permissions and environment variables. It is important that there are entries for your user in the subuid and sugid files. The complete example is here: https://github.com/richardjkendall/docker-gocd-agent-alpine-3.15 (you can ignore the part of the file which adds my own root certificate to the images).

… other Dockerfile commands

ARG UID=1010

RUN mkdir -p /run/go/${UID} && \
chown -R go /run/go/${UID} && \
echo go:165536:65536 | tee /etc/subuid | tee /etc/subgid

… other Dockerfile commands

ENV USER go
ENV HOME /home/go
ENV XDG_RUNTIME_DIR=/run/go/${UID}
USER go

Still to do

There's a few things I'd still like to do with this app

  • Create a mobile friendly interface
  • Extend the meal model to store ingedients and generate lists of ingredients needed for a given week (a shopping list...)

If you have any feedback on the article or from using the application, then please let me know!

-- Richard, Jul 2022