Need fast iterations with hot reload, debugging, and collaboration over k8s envs?

Monorepo-Based Development on Kubernetes Envs with Raftt

The Raftt Team
September 12, 2023
5
 min read

A Monorepo source control management strategy means all (or most, more likely) of your source code lives in a single large (”monolithic”) repository. This contrasts with using a multitude of repositories, with each repository containing the code for one component. We discussed this more deeply in the past, including pros and cons - go ahead and [check it out](https://www.raftt.io/post/development-challenges-of-working-with-monorepos-and-multirepos).

At Raftt we use the monorepo strategy internally - we have our primary code projects, documentation, and infrastructure all in the same repo. This creates some complexity for us, but allows us to easily bring up entire dev environments as needed.

To be clear, Raftt [the product] also supports working with multiple repositories in various configurations, which we’ll cover in a different blog post 🙂. Here we’ll concentrate on using Raftt for working on Kubernetes developer environments sourced from code stored in a monorepo.

## Why does the project being a monorepo matter?

Projects that use the monorepo strategy tend to have little relationships between repositories, but a complex internal structure. Various codebases live in the same repo, separated sometimes (but not always!) by directories, sometimes (but not always!) having a consistent scheme, etc. This means we need to support, as part of the Raftt definition:

1. The ability to flexibly work with files wherever they may be inside the repository.
2. A method to conveniently manage the Raftt configuration itself - we may need to define and work with several services inside the Raftt configuration for this repo, and it should be manageable.
3. The ability to dynamically query and interact with the repository structure to allow maximal configuration sharing.

## Working with repository structure

Some programming languages or frameworks have strong opinions on the directory structure of the code repository, while others are more free. A team using a monorepo could easily have multiple languages in the same repository, leading to even more complexity. To make sure Raftt works with all possible configurations, Raftt has only one requirement - that in the root of the repository a `raftt.yml` file is placed. That file contains a relative path within the repo to the main configuration file - the env definition file - usually called `env.raftt`.

The `env.raftt` is written in Starlark, a python-like domain-specific-language, and provides a powerful method for manipulating and defining Kubernetes resources that together make up the development environment. Among the common tasks performed in that file is mounting code, defining image builds, and loading Kubernetes resources. In the following sections we’ll take a look at how these look when we are using a monorepo.

### Mounting code subdirectories

The primary purpose of Raftt is to provide developers with fast iterations and a full-fledged developer experience when working with Kubernetes-based environments. A big part of that is accomplished by syncing the local code repository to the remote environment, and mapping it into services, allowing fast feedback on code modifications.

With a monorepo, it is critical to be able to mount specific subpaths of the repo to different containers. That would look something like this (check out our API docs on the [`repo_volume`](https://docs.raftt.io/rafttfile/index.html#api.volumes.RepoVolume) and the [`mount`](https://docs.raftt.io/rafttfile/index.html#api.workload.Workload.mount) methods):

```python
workload = resources.deployment["my-deployment"]
workload.mount(repo_volume().subpath("my-deployment-code"), "/code")
```

We could of course mount various subpaths to different locations as needed:

```python
workload = resources.deployment["my-deployment"]
repo = repo_volume()
workload.mount(repo.subpath("my-deployment-code"), "/code")
workload.mount(repo.subpath("my-shared-library"), "/code/library")
workload.mount(repo.subpath("config/my-config-file"), "/etc/my-deployment/config")
```

And that’s it! 🙂 After `raftt rebuild my-deployment` we will see that the defined mounts are synced live from the local computer.

### Referencing arbitrary locations for … things

Similar to the mounts, we may have various elements of our environments that are defined in different places in the repo structure. For example, we can define an image build (if your environment is set up to include image building) with a Dockerfile in one place, and the context in another (see the [`build_image`](https://docs.raftt.io/rafttfile/index.html#api.build_image) api reference):

```python

build_image(
"my-deployment-image", # image name
"./src/my-deployment-code", # build context
"./docker/my-deployment/Dockerfile") # dockerfile path
```

Or we can load a helm chart that is stored in a certain subdirectory, and reference a values file from anywhere in the repo (see the [`helm_local_chart`](https://docs.raftt.io/rafttfile/#api.helm_local_chart) api reference):

```python
resources = helm_local_chart(
   "release-name", "./helm/chart-path",
   values_files="./my-deployment/helm-values.yml")
```

There are more examples, but every command can work with paths from anywhere in the repo, making it easy to adapt to your own monorepo layout.

### Dynamic repository introspection

A super cool feature if you have a very well-structured repo is the ability to dynamically list the repo directories and files and generate configuration on the fly. For example, if your repo looks something like:

```python
/
 services
   service-1/
     src/
       main.py
       ...
   service-2/
     src/
       main.py
       ...
```

And the images that run these services look the same, you could mount the source code for each service using something like (using the [`list_dir`](https://docs.raftt.io/rafttfile/#api.fs.File.list_dir) method):

```python
load('filesystem.star', 'fs')

resources = ... # load resources from anywhere

repo = repo_volume()

for f in fs.File("services").list_dir():
 resources[f.name].mounnt(repo.subpath("services/" + f.name), "/code")
```

This allows you to share as much of the configuration code as possible between similar services.

## Splitting configuration

Depending on your use-case, you might have services that need customizable configuration. If you are working in a multi-repo environment, that can be done at the `env.raftt` level. In a monorepo, you can easily create per-service configs and load them to disperse the configuration for each service to near the rest of its assets.

### Per-service mini configs

For example, taking the example structure above, what if service-1 needs an overridden command, while service-2 needs an additional env variable? We could create a small YAML config:

```yaml
env:
 KEY: VALUE
command: []
```

And load and apply it for each service:

```python
load('filesystem.star', 'fs')
load("encoding/yaml.star", "yaml")

resources = ... # load resources from anywhere

repo = repo_volume()
for f in fs.File("services").list_dir():
config_file = fs.File("services/" + f.name + "/config.yaml")
 config = yaml.loads(config_file.read_text())

resources.deployments[f.name].add_env_vars(config.env)
 if config.command: # only override if non-empty
resources.deployments[f.name].get_container().command = config.command
```

Then you can modify the `config.yaml` files as needed and very easily adapt the services to your needs. Some truly awesome things are possible - different service types and templates, default values, and more.

## Configuring monorepos for use with Raftt

As I hope is now clear, it is super easy to use Raftt with monorepos of any size. Anything from using arbitrary paths in the repository to dynamically creating configuration based on the actual files is possible and easy.

Once you are configured, use Raftt to sync and hot-reload, debug services, and gain a feedback cycle just 1 second long for your Kubernetes development environment.

Want to get started with your repo? Check out our [documentation](https://docs.raftt.io/basics/onboard_project), or [book an onboarding session directly](https://www.raftt.io/contact) - we’d love to help.

The Raftt Team

Stop wasting time worrying about your dev env.
Concentrate on your code.

The ability to focus on doing what you love best can be more than a bottled-up desire lost in a sea of frustration. Make it a reality — with Raftt.