Getting started with Go in HOPS

Go is a small, statically typed, compiled, garbage collected, C-like language that is great for concurrent work. Go compiles to a single binary, is easy to use with containers and is fun to work with. It's a perfect match for HOPS!

Building a web server

We'll build a small web server that counts how many visitors have been to our site. Everything we need is provided by HOPS and Go's fantastic standard library.

The code for the tutorial can be found on GitHub, and is deployed with HOPS!

In HOPS, applications are configured using environment variables. Environment variables are pretty much magical global variables that an executable can see. HOPS sets a bunch of these variables (you can too!), so that your application can read them to get information about how it should behave.

We'll listen to the port specified in the environment variable PORT. Additionally, we need to respond to health checks. For now, optimistically responding 200 to everyone seems sufficient. The health checks are performed against a path conveniently specified in HOPS_READINESS_PATH.

// main.go
package main

import (
	"database/sql"
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	_ := migratedDB()

	// Count visitors.
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {panic("todo!")})

	// Handle health checks.
	http.HandleFunc(os.Getenv("HOPS_READINESS_PATH"), func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })

	log.Fatal(http.ListenAndServe(fmt.Sprintf(":%s", os.Getenv("PORT")), nil))
}

// migratedDB returns a database connection that is ready for use.
func migratedDB() *sql.DB {
	panic("todo!")
}

Let's implement the visitor counter. Nothing magical here, we just increment a counter.

In HOPS, we collect logs that your application prints to stdout, so we don't need to think about log files.

// main.go

const update = `UPDATE visits SET visits = visits + 1 WHERE id = 'hits' RETURNING visits`

// Count visitors.
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
    // Don't count requests to all sites, we don't want to count favicons and robots.
    if r.URL.Path != "/" {
        http.NotFound(w, r)
        return
    }

    // Increment and return counter
    var hits int
    if err := db.QueryRowContext(r.Context(), update).Scan(&hits); err != nil {
        log.Printf("Could not update number of visitors: %v", err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    w.Header().Set("Content-Type", "text/plain; charset=utf-8")
    w.WriteHeader(200)
    fmt.Fprintf(w, "Welcome, visitor number %d!\n", hits)
})

Finally, we'll create the database connection and create the schema. For simplicity, we just try this each time the app starts. When you request Postgres database, you automatically get the DATABASE_URL environment variable added to your apps! You'll see how we request a database later.

// main.go

// migratedDB returns a database connection that is ready for use.
func migratedDB() *sql.DB {
	db, err := sql.Open("pgx", os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatalf("could not connect to database: %v", err)
	}

	const migrate = `CREATE TABLE IF NOT EXISTS visits (id TEXT PRIMARY KEY, visits int4 NOT NULL);
INSERT INTO visits (id, visits) VALUES ('hits', 0) ON CONFLICT (id) DO NOTHING;`

	if _, err := db.Exec(migrate); err != nil {
		log.Fatalf("could not migrate database: %v", err)
	}
	return db
}

All done! 🚀 The next step is getting the app online.

Creating a Dockerfile

HOPS deploys containers, which makes it easy to build, ship and run your code anywhere. You can build the container and run it locally, and if it does run, you can be pretty confident it will run on our servers as well.

We'll create a simple multi-layer Dockerfile.1 This builds the program in a context where it has the heavy Go compiler, and then creates a new, blank slate, and copies just the finished program into it. This gives us a very small image that takes only seconds to move around.

Save the following as Dockerfile in the base your project folder. HOPS finds this and automatically builds it.

# Dockerfile
# Build in Alpine, a small Linux distribution.
FROM golang:1-alpine AS build
COPY . /build
RUN CGO_ENABLED=0 go build -C /build  -o /usr/local/bin/visitcounterd .

# Copy the compiled application into a Distroless container.
FROM gcr.io/distroless/static-debian11:nonroot
COPY --from=build /usr/local/bin/visitcounterd /usr/local/bin/visitcounterd
CMD ["/usr/local/bin/visitcounterd"]

Creating the iterapp.toml file

We need a configuration file. As we don't need a test environment, we can specify that we want the main branch to deploy to production. HOPS needs to know that our app requires a PostgreSQL database. Adding the [postgres] is all it takes to make a Postgres database available. You can read more about our Postgres setup here.

In HOPS, your app describes your entire application, and can have one or more applings, which are different containers running at the same time. In our example, we only have one appling, so we configure it directly in iterapp.toml.2

We'll leave the rest of the settings blank – we've already told our app how to deal handle the critical pieces, the port and the health checks.

# iterapp.toml
default_environment="prod"

[postgres]

Deploying our app

Create a repository on GitHub under the iterate organization, and push your code to it.

V2✨ V3 ✨

Register

In Slack, go to #iterapp-logs. Type /iterapp register {repository}, where {repository} is the part after iterate/. This configures HOPS to listen to your repository.

Deploy

To deploy your app for the first time, go to #iterapp-logs. Type /iterapp deploy {repository} prod main. This should deploy your application. Check it out at https://{repository}.app.iterate.no!

Register

The primary means of interacting with HOPS is through the CLI. To register your application from step 1:

  1. Install the CLI: (see CLI documentation for more)

    curl -SsLf https://cli.headless-operations.no/install.sh | sh
    
  2. Log in:

    hops v3 login
    
  3. Register:

    hops v3 register --cluster iterapp {repository}
    

    (Change {repository} to the the repository name including the organization, for example: iterate/dogs)

Deploy

To deploy your app for the first time, run

hops v3 deploy -a {repository} -e prod -r main`.

This should deploy your application. Check it out at https://{repository}.app.iterate.no!

1

TODO: This step is too bit complex.

2

TODO: This is confusing. Explain why it is important.