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 runninggo 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() {
:= os.Getenv("PORT")
port
if port == "" {
// This must be port 5000 because EB listens on port 5000 by default.
= "5000"
port }
:= gin.New()
router .Use(gin.Logger())
router.LoadHTMLGlob("templates/*.tmpl.html")
router.Static("/static", "static")
router
.GET("/", func(c *gin.Context) {
router:= c.Query("n")
n if n == "" {
// Render view
.HTML(http.StatusOK, "index.tmpl.html", nil)
c} else {
, err := strconv.Atoi(n)
iif err != nil || i < 1 || i > 10000 {
// Render view with error
.HTML(http.StatusOK, "index.tmpl.html", gin.H{
c"error": "Please submit a valid number between 1 and 10000.",
})
} else {
:= calculatePrime(i)
p // Render view with prime
.HTML(http.StatusOK, "index.tmpl.html", gin.H{"n": i, "prime": p})
c}
}
})
.Run(":" + port)
router}
// Super simple algorithm to find largest prime <= n
func calculatePrime(n int) int {
:= 1
prime for i := n; i > 1; i-- {
:= true
isPrime for j := 2; j < i; j++ {
if i%j == 0 {
= false
isPrime break
}
}
if isPrime {
= i
prime 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:
- Pulling the results from the cache is much faster, resulting in a better user experience.
- 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
// ...
= calculatePrime(i)
p // ...
with
// ...
:= "prime." + strconv.Itoa(i)
key := 0
p // Look in cache
, _, _, err := mcClient.Get(key)
valif err != nil {
// Prime not in cache (calculate and store)
= calculatePrime(i)
p = strconv.Itoa(p)
val .Set(key, val, 0, 0, 0)
mcClient} else {
// Found it!
, _ = strconv.Atoi(val)
p}
// ...
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() {
// ...
:= persistence.NewMemcachedBinaryStore(servers, username, password, persistence.FOREVER)
mcStore
.GET("/", cache.CachePage(mcStore, persistence.DEFAULT, func(c *gin.Context) {
router// ...
}))
// ...
}
// ...
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() {
// ...
:= make(map[string]int)
likes .POST("/", func(c *gin.Context){
router:= c.PostForm("n")
n [n] += 1
likes.Redirect(http.StatusMovedPermanently, "/?n=" + n)
c})
.GET("/", cache.CachePage(mcStore, persistence.DEFAULT, func(c *gin.Context) {
router// ...
}))
//...
}
// ...
In addition, we also need to make sure the likes are passed to the HTML function in the GET
controller:
// ...
// Render view with prime
.HTML(http.StatusOK, "index.tmpl.html", gin.H{"n": i, "prime": p, "likes": likes[n] })
c
// ...
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:
// ...
.POST("/", func(c *gin.Context){
router:= c.PostForm("n")
n [n] += 1
likes.Delete(cache.CreateKey("/?n=" + n))
mcStore.Redirect(http.StatusMovedPermanently, "/?n=" + n)
c})
// ...
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()`
:= memcached.NewMemcacheStore(mcClient, "", []byte("secret"))
sessionStore .Use(sessions.Sessions("mysession", sessionStore))
router// ...
}
// ...
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.