Build a Gin application on AWS Elastic Beanstalk and scale it with Memcache

We’ll walk through how to create a simple Gin Gonic application, and how to deploy it using Amazon Elastic Beanstalk. Once the application is set up and deployed, we’ll explore ways that using Memcache can provide a solution to some common performance bottlenecks you might come across.

We’ll walk you through creating the application from start to finish, but you can view the finished product source code here.

Memcache is a technology that improves the performance and scalability of web apps and mobile app backends. You should consider using Memcache when your pages are loading too slowly or your app is having scalability issues. Even for small sites, Memcache can make page loads snappy and help future-proof your app.

Prerequisites

Before you complete the steps in this guide, make sure you have all of the following:

  • Familiarity with Go (and ideally Gin)
  • An AWS account. If you haven’t used AWS before, you can set up an account here.
  • The AWS CLI installed and configured on your computer.
  • Go, govendor, and the EB CLI installed on your computer.
  • Make sure the GOPATH environment variable is set. (You can check by running go env GOPATH.)

Deploying a Gin application with Elastic Beanstalk

Gin is a minimalist framework that doesn’t require an application skeleton. Simply create a Go app and add github.com/gin-gonic/gin as a dependency like so:

$ cd `go env GOPATH`/src
$ mkdir gin-memcache
$ cd gin-memcache
$ govendor init
$ govendor fetch github.com/gin-gonic/gin@v1.2

Now that we’ve installed the Gin framework, we can add our app code. We’ll create a page that calculates the largest prime number that’s smaller than a number a visitor submits.

Create application.go and paste the following code into it:

NOTE: Elastic Beanstalk requires that your main go file be named application.go. It compiles your application using the following command: go build -o bin/application application.go

package main

import (
  "net/http"
  "os"
  "strconv"

  "github.com/gin-gonic/gin"
)

func main() {
  port := os.Getenv("PORT")

  if port == "" {
    // This must  be port 5000 because EB listens on port 5000 by default.
    port = "5000"
  }

  router := gin.New()
  router.Use(gin.Logger())
  router.LoadHTMLGlob("templates/*.tmpl.html")
  router.Static("/static", "static")

  router.GET("/", func(c *gin.Context) {
    n := c.Query("n")
    if n == "" {
      // Render view
      c.HTML(http.StatusOK, "index.tmpl.html", nil)
    } else {
      i, err := strconv.Atoi(n)
      if err != nil || i < 1 || i > 10000 {
        // Render view with error
        c.HTML(http.StatusOK, "index.tmpl.html", gin.H{
          "error": "Please submit a valid number between 1 and 10000.",
        })
      } else {
        p := calculatePrime(i)
        // Render view with prime
        c.HTML(http.StatusOK, "index.tmpl.html", gin.H{"n": i, "prime": p})
      }
    }
  })

  router.Run(":" + port)
}

// Super simple algorithm to find largest prime <= n
func calculatePrime(n int) int {
  prime := 1
  for i := n; i > 1; i-- {
    isPrime := true
    for j := 2; j < i; j++ {
      if i%j == 0 {
        isPrime = false
        break
      }
    }
    if isPrime {
      prime = i
      break
    }
  }
  return prime
}

Now let’s add a corresponding view. Create the file templates/index.tmpl.html and paste the following code into it:

{{ define "index.tmpl.html" }}
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">

    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">

    <title>Gin caching example</title>
  </head>

  <body>
    <div class="container">
      <h1>
        Gin caching example
      </h1>

      <p class="lead">For any number N (max 10000), we'll find the largest prime number
        less than or equal to N.
      </p>
      <!-- Form to submit a number -->
      <form class="form-inline" action="/">
        <input type="text" class="form-control" name="n" />
        <input type="submit" class="btn btn-primary" value="Find Prime" />
      </form>

      <hr>
      <!-- Show the result -->
      {{ if .prime }}
        <div class="alert alert-primary">
          <p class="lead">Largest prime less or equal than {{ .n }} is {{ .prime }}</p>
        </div>
      {{ end }}

      <!-- Error handling -->
      {{ if .error }}
      <div class="alert alert-danger">
        <p class="lead">{{ .error }}</p>
      </div>
      {{ end }}

    </div>
  </body>
</html>
{{ end }}

You now have a working app that you can start by running go run application.go.

Deploying with Elastic Beanstalk

To deploy the app to Elastic Beanstalk (EB), you’ll need to create a Git repository. We’ll start by creating a .gitignore file with the following lines in it:

$ echo 'vendor/*' .gitignore
$ echo '!vendor/vendor.json' >.gitignore
$ echo 'bin/application' >.gitignore

In order to deploy to EB, we’ll need three aditional files: build.sh, Buildfile, and Procfile.

Procfile:

web: bin/application

Buildfile:

make: ./build.sh

build.sh

#!/usr/bin/env bash
# Stops the process if something fails
set -xe

# get all of the dependencies needed
go get "github.com/gin-gonic/gin"

# create the application binary that eb uses
GOOS=linux GOARCH=amd64 go build -o bin/application -ldflags="-s -w"

These files are what EB relies on to deploy your application. If you run into issues during deployment later, double check these files. Make sure your port is listening on 5000, and make sure your main file is labeled application.go.

Then, create the repository and commit the initial state of the app:

$ git init
$ git add .
$ git commit -m 'Initial gin app'

Next, we’ll need to create a EB CLI repository with enough information so EB knows how to run it. Start by creating the repository using eb init. We’ll walk through this now. NOTE: Don’t copy and paste:

$ eb init

# We'll stick with the default for now.
Select a default region
[...]
(default is 3): 3

Select an application to use
?) [ Create new Application ]
(default is 1): # Select whichever option lets you create a new application

# You can make the name whatever you like.
# By default it will match the file directory.
Enter Application Name
(default is "gin-memcache"): gin-memcache
Application gin-memcache has been created.

# This is a go tutorial, so we'll pick go.
Select a platform.
[...]
10) Go
[...]
(default is 1): 10


Select a platform version.
1) Go 1.10
2) Go 1.9
3) Go 1.8
4) Go 1.6
5) Go 1.5
6) Go 1.4
(default is 1): # Select your version of go.

# don't worry about this bit for now. You can always turn it on later using `eb init`
Note: Elastic Beanstalk now supports AWS CodeCommit; a fully-managed source control service. To learn more, see Docs: https://aws.amazon.com/codecommit/
Do you wish to continue with CodeCommit? (y/N) (default is n): n

# You can if you want, but we'll skip that that now.
Do you want to set up SSH for your instances?
(Y/n): n

Now that you’ve set up your application repository, we’ll need to create the EB instance.

$ eb create

# Can be whatever you want. We'll stick with the default for now.
Enter Environment Name
(default is gin-memcache-dev): gin-memcache-dev

# This will be the beginning of all of your URLs. We won't be doing anything
# with this, so we'll use the default option again.
Enter DNS CNAME prefix
(default is gin-memcache-dev): gin-memcache-dev

# In case you're sensing a theme here.. We'll stick with default for now.
Select a load balancer type
1) classic
2) application
3) network
(default is 1): 1

At this point, EB will go about creating and deploying your application. The first time we deploy it’ll take a couple of minutes. Go grab a coffee and come back. Once it’s done building, you can type eb open to see your new application. If you can view the configuration in the AWS console by using eb console.

Adding caching to Gin

Memcache is an in-memory, distributed cache. Its primary API consists of two operations: SET(key, value) and GET(key). Memcache is like a hashmap (or dictionary) that is spread across multiple servers, where operations are still performed in constant time.

The most common use for Memcache is to cache expensive database queries and HTML renders so that these expensive operations don’t need to happen over and over again.

Set up Memcache

To use Memcache in Gin, you first need to provision an actual Memcache cache. MemCachier provides a fast and flexible multi-tenant cache system that’s compatible with the protocol used by the popular memcached software. When you create a cache with MemCachier, you’re provided with one or more endpoints that you can connect to using the memcached protocol, accessing your cache just as if you had set up your own memcached server. So head over to https://www.memcachier.com, sign up for an account, and create a free development cache. If you need help getting it set up, follow the directions here.

There are three config vars to you’ll need for your application to be able to connect to your cache: MEMCACHIER_SERVERS, MEMCACHIER_USERNAME, and MEMCACHIER_PASSWORD. You can find these on your analytics dashboard. You’ll need to add these variables to EB.

$ eb setenv MEMCACHIER_USERNAME=<username> MEMCACHIER_PASSWORD=<password> MEMCACHIER_SERVERS=<servers>

We can confirm that they’ve been set by running:

$ eb printenv
Environment Variables:
     MEMCACHIER_SERVERS = mc1.dev.ec2.memcachier.com:11211
     MEMCACHIER_USERNAME = <your-cache-username>
     MEMCACHIER_PASSWORD = <your-cache-password>

To use the cache in Gin, we need to install mc with govendor:

$ govendor fetch github.com/memcachier/mc

Add it to your build.sh file:

#!/usr/bin/env bash
set -xe

# get all of the dependencies needed
go get "github.com/gin-gonic/gin"
go get "github.com/memcachier/mc"

# create the application binary that eb uses
GOOS=linux GOARCH=amd64 go build -o bin/application -ldflags="-s -w"

and configure it in application.go:

package main

import (
  // ...
  "github.com/memcachier/mc"
)

func main() {
  username := os.Getenv("MEMCACHIER_USERNAME")
  password := os.Getenv("MEMCACHIER_PASSWORD")
  servers := os.Getenv("MEMCACHIER_SERVERS")

  mcClient := mc.NewMC(servers, username, password)
  defer mcClient.Quit()
  // ...
}
// ...

Caching expensive computations

There are two reasons why caching the results of expensive computations is a good idea:

  1. Pulling the results from the cache is much faster, resulting in a better user experience.
  2. Expensive computations use significant CPU resources, which can slow down the rest of your app.

Our prime number calculator doesn’t really have any expensive computations, because we limit the input value to 10000. For the sake of the tutorial, however, let’s assume that calculating the prime is an expensive computation we would like to cache.

To achieve this, let’s modify the GET route in application.go and replace

// ...
p = calculatePrime(i)
// ...

with

// ...
key := "prime." + strconv.Itoa(i)
p := 0
// Look in cache
val, _, _, err := mcClient.Get(key)
if err != nil {
  // Prime not in cache (calculate and store)
  p = calculatePrime(i)
  val = strconv.Itoa(p)
  mcClient.Set(key, val, 0, 0, 0)
} else {
  // Found it!
  p, _ = strconv.Atoi(val)
}
// ...

Deploy these changes to Heroku and submit some numbers to find primes:

$ git commit -am 'Add caching'
$ eb deploy

The page should work just as before. However, under the hood, already calculated primes are now cached. To see what’s going on in your cache, open the MemCachier dashboard (which is where you found your environment variables.)

On the dashboard you can refresh the stats each time you request a prime. The first time you enter a number, the get misses will increase. For any subsequent request of the same number, you should get an additional get hit.

Caching rendered views

Rendering HTML views is generally an expensive computation, and you should cache rendered views whenever possible. In Gin, you can achieve this easily with gin-contrib/cache library. Fetch the library with govendor:

$ govendor fetch github.com/gin-contrib/cache

Once again add it to your build.sh file:

# \\ ...

# get all of the dependencies needed
go get "github.com/gin-gonic/gin"
go get "github.com/memcachier/mc"
go get "github.com/gin-contrib/cache"

# \\ ...

Now we can cache rendered views in application.go like so:

package main

import (
  // ...
  "github.com/gin-contrib/cache"
  "github.com/gin-contrib/cache/persistence"
  // ...
)

func main() {
  // ...
  mcStore := persistence.NewMemcachedBinaryStore(servers, username, password, persistence.FOREVER)

  router.GET("/", cache.CachePage(mcStore, persistence.DEFAULT, func(c *gin.Context) {
    // ...
  }))
  // ...
}
// ...

This is easy enough and works well. However, if the view ever changes, we need to be careful. To illustrate the case of a changing page, let’s add a “Like” button to each number and its calculated largest prime. Let’s put the button just below the calculated prime in the index.tmpl.html file:

<!-- ... -->

<!-- Show the result -->
{{ if .prime }}
  <div class="alert alert-primary">
    <p class="lead">Largest prime less or equal than {{ .n }} is {{ .prime }}</p>
    <p>Likes: {{ .likes }}</p>
  </div>
  <form method='POST'>
    <input type="hidden" name="n" value="{{ .n }}" />
    <input type="submit" class="btn btn-primary" value="Like!" />
  </form>
{{ end }}

<!-- ... -->

We now need to create a controller for the POST route in application.go and store the posted like in a variable.

Storing likes in a variable is a bad idea. Each time the app restarts, it wipes all likes. We do this here only for convenience. In a production application, you should store such information in a database.

// ...
func main() {
  // ...

  likes := make(map[string]int)
  router.POST("/", func(c *gin.Context){
    n := c.PostForm("n")
    likes[n] += 1
    c.Redirect(http.StatusMovedPermanently, "/?n=" + n)
  })

  router.GET("/", cache.CachePage(mcStore, persistence.DEFAULT, func(c *gin.Context) {
    // ...
  }))
  //...
}
// ...

In addition, we also need to make sure the likes are passed to the HTML function in the GET controller:

// ...

// Render view with prime
c.HTML(http.StatusOK, "index.tmpl.html", gin.H{"n": i, "prime": p, "likes": likes[n] })

// ...

To illustrate the problem with changing pages, let’s commit our current implementation and test it:

$ git commit -am 'Add view caching'
$ eb deploy

If you submit a number, you will now get the largest prime below it, together with a Like button. However, when you click Like!, the like count doesn’t increase. This is because the view is cached.

To resolve this, we need to invalidate the cached view whenever it is updated:

// ...

router.POST("/", func(c *gin.Context){
  n := c.PostForm("n")
  likes[n] += 1
  mcStore.Delete(cache.CreateKey("/?n=" + n))
  c.Redirect(http.StatusMovedPermanently, "/?n=" + n)
})

// ...

Deploy again to EB:

$ git commit -am 'Fix view caching'
$ eb deploy

Now you can see the number of likes increase.

Session Caching

Memcache works well for storing information for short-lived sessions that time out. However, because Memcache is a cache and therefore not persistent, long-lived sessions are better suited to permanent storage options, such as your database.

To use sessions in Gin, you need `gin-contrib/session:``

$ govendor fetch github.com/gin-contrib/sessions
$ govendor fetch github.com/gin-contrib/sessions/memcached

build.sh:

# // ...

# get all of the dependencies needed
go get "github.com/gin-gonic/gin"
go get "github.com/memcachier/mc"
go get "github.com/gin-contrib/cache"
go get "github.com/gin-contrib/sessions"
go get "github.com/gin-contrib/sessions/memcached"

# // ...

The configuration in application.go is easy enough:

package main

import (
    // ...
  "github.com/gin-contrib/sessions"
  "github.com/gin-contrib/sessions/memcached"
  // ...
)

func main() {
  // ...
  // add below `router := gin.New()`
  sessionStore := memcached.NewMemcacheStore(mcClient, "", []byte("secret"))
  router.Use(sessions.Sessions("mysession", sessionStore))
  // ...
}
// ...

Now you can now use sessions as you please. For more information about session usage in Gin, check out the gin-contrib/sessions README.

Clean up

Once you’re done with this tutorial and don’t want to use it anymore, you can clean up your EB instance by using:

$ eb terminate

This will clean up all of the AWS resources.

Further reading & resources