Build a rock for a Go application

In this tutorial, we’ll containerise a simple Go application into a rock using Rockcraft’s go-framework extension.

It should take 25 minutes for you to complete.

You won’t need to come prepared with intricate knowledge of software packaging, but familiarity with Linux paradigms, terminal operations, and Go is required.

Once you complete this tutorial, you’ll have a working rock for a Go application. You’ll gain familiarity with Rockcraft and the go-framework extension, and have the experience to create rocks for Go applications.

Setup

We recommend starting from a clean Ubuntu installation. If we don’t have one available, we can create one using Multipass:

Is Multipass already installed and active? Check by running

snap services multipass

If we see the multipass service but it isn’t “active”, then we’ll need to run sudo snap start multipass. On the other hand, if we get an error saying snap "multipass" not found, then we must install Multipass:

sudo snap install multipass

Then we can create the VM with the following command:

multipass launch --disk 10G --name rock-dev 24.04

Finally, once the VM is up, open a shell into it:

multipass shell rock-dev

LXD will be required for building the rock. Make sure it is installed and initialised:

sudo snap install lxd
lxd init --auto

In order to create the rock, we’ll need to install Rockcraft:

sudo snap install rockcraft --classic

We’ll use Docker to run the rock. We can install it as a snap:

sudo snap install docker

By default, Docker is only accessible with root privileges (sudo). We want to be able to use Docker commands as a regular user:

sudo addgroup --system docker
sudo adduser $USER docker
newgrp docker
sudo snap disable docker
sudo snap enable docker

Warning

There is a known connectivity issue with LXD and Docker. If we see a networking issue such as “A network related operation failed in a context of no network access”, make sure to apply one of the suggested fixes here.

Note that we’ll also need a text editor. We can either install one of our choice or simply use one of the already existing editors in the Ubuntu environment (like vi).

This tutorial requires the latest/edge channel of Rockcraft. Run sudo snap refresh rockcraft --channel latest/edge to get the latest edge version.

In order to test the Go application locally, before packing it into a rock, install go.

sudo snap install go --classic

Create the Go application

Start by creating the “Hello, world” Go application that will be used for this tutorial.

Create a new directory for this tutorial and enter it:

mkdir go-hello-world
cd go-hello-world

Initialise the new Go module:

go mod init go-hello-world

Create a main.go file, copy the following text into it and then save it:

package main

import (
	"fmt"
	"log"
	"net/http"
)

func helloWorldHandler(w http.ResponseWriter, req *http.Request) {
	log.Printf("new hello world request")
	fmt.Fprintln(w, "Hello, world!")
}

func main() {
	log.Printf("starting hello world application")
	http.HandleFunc("/", helloWorldHandler)
	http.ListenAndServe(":8000", nil)
}

Build the Go application so it can be run:

go build .

A binary called go-hello-world is created in the current directory. This binary is only needed for local testing, as Rockcraft will compile the Go application when we pack the rock.

Let’s Run the Go application to verify that it works:

./go-hello-world

The application starts an HTTP server listening on port 8000 that we can test by using curl to send a request to the root endpoint. We may need a new terminal for this – if using Multipass, run multipass shell rock-dev to get another terminal:

curl --fail localhost:8000

The Go application should respond with Hello, world!.

The Go application looks good, so let’s stop it for now with Ctrl + C.

Pack the Go application into a rock

First, we’ll need a rockcraft.yaml file. Rockcraft will automate its creation and tailor it for a Go application when we tell it to use the go-framework profile:

rockcraft init --profile go-framework

Open rockcraft.yaml in a text editor and check that the name key is set to go-hello-world. Ensure that platforms includes the architecture of the host. For example, if the host uses the ARM architecture, include arm64 in platforms.

Note

For this tutorial, we name the rock go-hello-world and assume we are running on an amd64 platform. Check the architecture of the system using dpkg --print-architecture.

The name, version and platform all influence the name of the generated .rock file.

As the go-framework extension is still experimental, export the environment variable ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS:

export ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true

Pack the rock:

rockcraft pack

Depending on the network, this step can take a couple of minutes to finish.

Once Rockcraft has finished packing the Go rock, we’ll find a new file in the working directory (an OCI image) with the .rock extension:

ls *.rock -l --block-size=MB

Run the Go rock with Docker

We already have the rock as an OCI archive. Load the image into Docker:

rockcraft.skopeo --insecure-policy \
  copy oci-archive:go-hello-world_0.1_amd64.rock \
  docker-daemon:go-hello-world:0.1

Check that the image was successfully loaded into Docker:

docker images go-hello-world:0.1

The output should list the Go container image, along with its tag, ID and size:

REPOSITORY       TAG       IMAGE ID       CREATED         SIZE
go-hello-world   0.1       f3abf7ebc169   5 minutes ago   15.7MB

Now we’re finally ready to run the rock and test the containerised Go application:

docker run --rm -d -p 8000:8000 \
  --name go-hello-world go-hello-world:0.1

Use the same curl command as before to send a request to the Go application’s root endpoint which is running inside the container:

curl --fail localhost:8000

The Go application again responds with Hello, world!.

View the application logs

When deploying the Go rock, we can always get the application logs with Pebble:

docker exec go-hello-world pebble logs go

As a result, pebble will give the logs for the go service running inside the container. We should expect to see something similar to this:

2024-10-04T08:51:35.826Z [go] 2024/10/04 08:51:35 starting hello world application
2024-10-04T08:51:39.974Z [go] 2024/10/04 08:51:39 new hello world request

We can also choose to follow the logs by using the -f option with the pebble logs command above. To stop following the logs, press Ctrl + C.

Stop the application

Now we have a fully functional rock for a Go application! This concludes the first part of this tutorial, so we’ll stop the container and remove the respective image for now:

docker stop go-hello-world
docker rmi go-hello-world:0.1

Update the Go application

As a final step, let’s update our application. For example, we want to add a new /time endpoint which returns the current time.

Start by opening the main.go file in a text editor and update the code to look like the following:

package main

import (
	"fmt"
	"log"
	"net/http"
	"time"
)

func helloWorldHandler(w http.ResponseWriter, req *http.Request) {
	log.Printf("new hello world request")
	fmt.Fprintln(w, "Hello, world!")
}

func timeHandler(w http.ResponseWriter, req *http.Request) {
	log.Printf("new time request")
	now := time.Now()
	fmt.Fprintln(w, now.Format(time.DateTime))
}

func main() {
	log.Printf("starting hello world application")
	http.HandleFunc("/", helloWorldHandler)
	http.HandleFunc("/time", timeHandler)
	http.ListenAndServe(":8000", nil)
}

Since we are creating a new version of the application, open the

rockcraft.yaml file and set version: "0.2".

Note

rockcraft pack will create a new image with the updated code even if we don’t change the version. It is recommended to change the version whenever we make changes to the application in the image.

Pack and run the rock using similar commands as before:

rockcraft pack
rockcraft.skopeo --insecure-policy \
  copy oci-archive:go-hello-world_0.2_amd64.rock \
  docker-daemon:go-hello-world:0.2
docker images go-hello-world:0.2
docker run --rm -d -p 8000:8000 \
  --name go-hello-world go-hello-world:0.2

Note

Note that the resulting .rock file will now be named differently, as its new version will be part of the filename.

Finally, use curl to send a request to the /time endpoint:

curl --fail localhost:8000/time

The updated application will respond with the current date and time.

Note

If we are not getting the current date and time from the /time endpoint, check the Troubleshooting steps below.

Cleanup

We can now stop the container and remove the corresponding image:

docker stop go-hello-world
docker rmi go-hello-world:0.2

Reset the environment

We’ve reached the end of this tutorial.

If we’d like to reset the working environment, we can simply run the following:

# delete all the files created during the tutorial
rm go.mod rockcraft.yaml main.go go-hello-world \
  go-hello-world_0.1_amd64.rock \
  go-hello-world_0.2_amd64.rock
If using Multipass...

If we created an instance using Multipass, we can also clean it up. Start by exiting it:

exit

And then we can proceed with its deletion:

multipass delete rock-dev
multipass purge

Next steps


Troubleshooting

Application updates not taking effect?

Upon changing the Go application and re-packing the rock, if the changes are not taking effect, try running rockcraft clean and pack the rock again with rockcraft pack.