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 ✨ |
---|---|
RegisterIn Slack, go to DeployTo deploy your app for the first time, go to |
RegisterThe primary means of interacting with HOPS is through the CLI. To register your application from step 1:
DeployTo deploy your app for the first time, run
This should deploy your application. Check it out at |
TODO: This step is too bit complex.
TODO: This is confusing. Explain why it is important.