Skip to main content

Working with Compiled Languages

Working with compiled languages introduces a small additional challenge - the source needs to be compiled before it can run on the container. Raftt has various built-in options for supporting flexible work with compiled languages, including hot-reloading and debugging.

In this guide we will cover the general aspects of working with compiled languages, and the ways you can achieve great DevEx using Raftt. For language-specific examples, check out the debugging section. The examples we give here will be for Go, but the ideas are relevant for all compiled languages.

The following sections assume you are already connected to an environment, and are working on a workload in dev mode.

Building Artifacts

The most basic problem with compiled languages is that they need to be compiled. If your exiting image includes the toolchain, you can use it directly, and this section is less relevant. Otherwise, keep reading.

If the image does not include the toolchain, we have three options for building the artifact externally, somewhere where the toolchain does exist:

  1. Add a builder sidecar container and build on it - simplest
  2. Add a builder deployment and building on it - allows a shared build cache
  3. Building locally - not recommended, can take a long time to transmit the built artifact

Adding a Builder Sidecar Container

Going this route, all we need to do is add another container to the deployment, with an image that has the required toolchain. We also mount the repo to it:

dep = resources.deployments["my-deployment"]

# Define the container and add it to the workload
builder = k8s.Container(
name="builder",
image="golang:1.21.3-alpine3.18",
working_dir="/src"
)
dep.spec.template.spec.containers.append(builder)

# Mount the source code to the builder container
dep.mount(repo_root(), "/src", container="builder")

After running raftt rebuild, you should be able to run raftt sh my-deployment -c builder and build your code! Because the builder is bound to the lifetime of the deployment which uses its artifact, you will have a working build cache, and subsequent builds should be speedy.

Adding a Builder Deployment

Going this route, we will create a brand new deployment which we will use for building artifacts. This currently requires creating a file outside of the .raftt file, in which we write the deployment YAML:

apiVersion: apps/v1
kind: Deployment
metadata:
name: builder
spec:
selector:
matchLabels:
app: builder
template:
metadata:
labels:
app: builder
spec:
containers:
- image: golang:1.21.3-alpine3.18
name: server
workingDir: /src

Then in the .raftt file we would have:

builder_resources=k8s_manifests("./path/to/builder/yaml")
builder = builder_resources.deployments["builder"]

# Mount the source code to the builder container
builder.mount(repo_root(), "/src", container="builder")

After running raftt rebuild builder, you should be able to run raftt sh builder and build your code! Because the builder is not bound to the lifetime of any one deployment, you can have a shared build cache. If you are using Raftt in connect-mode, you can add:

deploy_on_connect(builder_resources)

Which will make the builder automatically be created once you connect to a new environment.

Build Locally

This mode is less recommended, because it can lead to significant delay while the built artifact is being uploaded to the remote cluster. No change needs to be made to the environment for this mode. In this case, use raftt cp to copy the file to the destinaton container.

Updating Artifacts

Now that the artifact is built, we need to update it in the main container. We have two main options:

  1. Have a shared mount between the builder and the main container - recommended
  2. Use raftt cp to copy the file over

Shared mount

To create a shared mount between the containers, we'll create a volume(), and then use mount it to both places:

build_out = volume("build_out")
dep.mount(build_out, "/out", container="builder", nocopy=True)
dep.mount(build_out, "/path/to/artifact/dir", init_on_rebuild=True)

Note the addition of nocopy to the builder, which means we do not try to initialize the volume with the contents of the image, and the init_on_rebuild on the main container, which means we always initialize the contents of the volume from the main container on rebuild.

raftt cp

We may want to use cp to copy the file directly in certain cases. Particularly when:

  1. The destination is in the root of the container or there are other files in the containing directory which we don't want to clobber with the mount
  2. We are building locally, in which case no need to make any modifications to the .raftt file - we will just use raftt cp locally.

To do that, first make sure the raftt CLI is available on the builder container:

dep.add_raftt_cli(container="builder")

Now, we can update the artifact in the destination container by running the build command and then raftt cp /out/artifact my-deployment:/path/to/artifact/dir/artifact.

File watching

File watching hooks work as usual, but we will want them to run on the builder, compile the artifact, copy it to the mount (or directly to the main container), and finally restart the process in the main container.

dep.add_raftt_cli(container="builder") # for the restart, and possibly the cp

register_hook(
on=events.OnFileChanged(
workload=dep,
container="builder",
patterns="/src/**/*.go"),
do=[
actions.CMD(
workload=dep,
container="builder",
cmd=["go", "build", "-o", "/out/artifact", "main.go"]
),
# If copying directly, you would add another CMD here with:
# ["raftt", "cp", "artifact", "my-deployment:/path/to/artifact/dir/artifact"]
actions.CMD(
workload=dep,
container="builder",
cmd=["raftt", "restart", "-n", "my-deployment"]
)
]
)

Note the -n we added to the raftt restart call, which makes the restart not interrupt an interactive (such as debugging) process currently running on the container.

Adding a Before-launch Task to the IDE

To make it easy to use the IDE directly, you can add a before-launch task to the build/run configuration which performs the actual compilation. See how it is defined in Jetbrains IDEs and VSSCode:

Add a Run on Raftt Workload "Before launch" task:

Add a Run on Raftt Workload Before launch task

And set it to what is needed for building the artifact:

Configure the Run on Raftt Workload Before launch task

More information on setting uo debug and run configurations is available in the debugging section.