To illustrate the deployment workflow, we will develop two basic Golang HTTP services: the and the .
Both services have similar implementation details; therefore, we will focus primarily on the implementation of one service, the workout-management-service.
The workout-management-service
This service is a straightforward HTTP service providing a REST API to manage workouts. Its primary functions include creating workouts and querying the created workouts.
Creating the golang project
We begin by setting up our Golang project. The first step is to create a project directory and initialize a Go module within it.
# create directory
mkdir workout-management-service
# here, we'll use 'github.com/samirmarin/workout-management-service' as the module name
go mod init github.com/samirmarin/workout-management-service
In this instance, the module name incorporates my GitHub account and the repository name. This naming convention is particularly relevant for importing packages from this project into others. However, since we won’t be doing such imports in this project, feel free to choose any module name you prefer.
Executing this command will generate a go.mod file in the project's root directory. Additionally, a go.sum file will also be created as we start importing external packages into our project.
The http server
For our HTTP server framework, we will utilize . To install Echo, execute the following command from the root directory of our project:
go get github.com/labstack/echo/v4
Next, create a main.go file in the root directory of our project. Insert the code below to set up a basic HTTP server that listens on port 1323:
package main
import (
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.Logger.Fatal(e.Start(":1323"))
}
To build and run this server, use the following commands from the root of our project:
go build -o workout-management-service
./workout-management-service
Executing these commands will activate the server on port 1323. To test the server, you can use the command:
curl localhost:1323
Since we haven’t configured any routes yet, this should return a 404 response.
Adding routes
The create route
Let's implement a route for creating a workout. We'll set up an endpoint to handle POST requests at /create.
We'll modify our main.go file to include a create function:
In this case, we use a POST request for the get route to enable passing a request body. This body will contain the owner's name and the name of the workout we wish to retrieve.
To test this route, use the following commands:
go build -o workout-management-service
./workout-management-service
curl -v -X POST localhost:1323/get
This will also return a 200 response, but like the create route, it currently only prints out "Getting workout."
Creating a workout pkg
Let's develop a package responsible for workout creation and retrieval. We'll name this package workout and include a functionx for creating workouts and getting workouts.
First, set up the necessary directory structure and files:
Start by defining the Workout struct in workout.go:
package workout
type Workout struct {
Owner string `json:"owner"`
Name string `json:"name"`
Category string `json:"category"`
Equipment Equipment `json:"equipment"`
Exercises []Exercise `json:"exercises"`
}
type Equipment struct {
Name string `json:"name"`
Description string `json:"description"`
}
type Exercise struct {
Name string `json:"name"`
Description string `json:"description"`
Sets int `json:"reps"`
Time int `json:"time"`
}
Adding Create and Get Functions
Now, add the CreateWorkout and GetWorkout functions to handle creating and querying workouts:
package workout
import "fmt"
type Workout struct {
Owner string `json:"owner"`
Name string `json:"name"`
Category string `json:"category"`
Equipment Equipment `json:"equipment"`
Exercises []Exercise `json:"exercises"`
}
type Equipment struct {
Name string `json:"name"`
Description string `json:"description"`
}
type Exercise struct {
Name string `json:"name"`
Description string `json:"description"`
Sets int `json:"reps"`
Time int `json:"time"`
}
// CreateWorkout creates a workout, save new workout to db
func (w *Workout) CreateWorkout() error {
fmt.Println(w)
return nil
}
// GetWorkout gets a workout from db
func (w *Workout) GetWorkout() error {
fmt.Println(w)
return nil
}
Integrating with with our routes
In the main package, update route handling to use these new functions. We'll use echo.Context to parse the request body into a Workout struct:
You can test these routes with the following curl commands:
first rebuild and run the service:
go build -o workout-management-service
./workout-management-service
then run the follwoing curl commands:
curl -X POST http://localhost:1323/create \
-H "Content-Type: application/json" \
-d '{
"owner": "samir@gmail.com",
"name": "Run The Interval",
"category": "running",
"equipment": {
"name": "Running Equipment",
"description": "If indoors we need a threadmill, if outdoors a good place to run fast for 3 min without interruption"
},
"exercises": [
{
"name": "Warmup",
"description": "20min jog"
},
{
"name": "3 min by 5 interval",
"description": "3min x5 at 5k pace with 1min jog"
},
{
"name": "Cooldown",
"description": "20min cool down"
}
]
}'
curl -X POST http://localhost:1323/create \
-H "Content-Type: application/json" \
-d '{
"owner": "samir@gmail.com",
"name": "Run The Interval",
}'
Currently, these routes will simply print the deserialized Workout structs to the console. This allows us to verify that the POST request bodies are being correctly deserialized into Workout structs.
The Database
For our database, we will use Amazon DynamoDB Local, a popular NoSQL database choice for microservices in AWS cloud environments. DynamoDB Local simulates a DynamoDB instance in the cloud, making it an ideal choice for local development and testing in GitHub Actions workflows.
We choose DynamoDB due to its widespread use in industry, particularly for microservices hosted on AWS. However, it's worth noting that other databases could be substituted depending on specific requirements or preferences.
dynamodb package
We'll start by creating a DynamoDB package in our internal directory. This involves setting up a dynamodb.go file in the internal/dynamodb directory. This package will initially be used in both services, but later we'll explore how to extract it into its own repository for better reusability.
DynamoDB Client
Here's the initial setup for our DynamoDB client:
package dynamodb
import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/dynamodb"
"os"
)
type Client struct {
Dynamodb *dynamodb.DynamoDB
TableName string
}
func NewClient(tableName string) *Client {
sess := session.Must(session.NewSessionWithOptions(session.Options{
SharedConfigState: session.SharedConfigEnable,
}))
// Optional: Override with local endpoint if an environment variable is set
var svc *dynamodb.DynamoDB
if localEndpoint := os.Getenv("DYNAMODB_LOCAL_ENDPOINT"); localEndpoint != "" {
svc = dynamodb.New(sess, &aws.Config{
Endpoint: aws.String(localEndpoint),
Region: aws.String("us-west-2"),
// provide test credentials when connecting to DynamoDB local
Credentials: credentials.NewStaticCredentials("test", "test", ""),
// Disable SSL for local non-production use
DisableSSL: aws.Bool(true),
})
} else {
svc = dynamodb.New(sess)
}
return &Client{
Dynamodb: svc,
TableName: tableName,
}
}
Our client is designed to work with both local and cloud instances of DynamoDB. By default, it connects to a local instance if the DYNAMODB_LOCAL_ENDPOINT environment variable is set, or to a cloud instance based on AWS credentials otherwise.
Storable Interface and Functions
Let's define a Storable interface and add StoreItem and GetItem functions to interact with DynamoDB:
We use the Storable interface to keep our functions generic and reusable. This allows our Workout struct to implement methods that convert it to DynamoDB-compatible formats.
Integrating with Workout pkg
Next, integrate these methods with our Workout pkg: Add the ToDynamoDbAttribute and ToDynamoDbItemInput functions to our workout pkg. This ensures that our workout pkg implements the Storable interface.
These methods ensure that our Workout struct can be stored and retrieved from DynamoDB. By converting our workout struct to a dynamodb attribute and a dynamodb item input.
the dynamodb attribute is what we will use to store our workout in the database
the dynamodb item input is what we will use to search for our workout in the database.
Modifying Workout Functions
Update the CreateWorkout and GetWorkout functions in the workout package to interact with the database:
...
var tableName = "Workout"
// CreateWorkout creates a workout, save new workout to db
func (w *Workout) CreateWorkout() error {
dynamoDbClient := dynamodb.NewClient(tableName)
err := dynamoDbClient.StoreItem(w)
if err != nil {
return err
}
return nil
}
func (w *Workout) GetWorkout() error {
dynamoDbClient := dynamodb.NewClient(tableName)
err, getItemOutput := dynamoDbClient.GetItem(w)
if err != nil {
return err
}
err = awsDynamoDbAttribute.UnmarshalMap(getItemOutput.Item, w)
return nil
}
With these modifications, our application can now create and retrieve workouts from the database.
Setting Up DynamoDB Local
To run DynamoDB locally, we'll use Docker Compose:
touch docker-compose.yaml
Add the following configuration to docker-compose.yaml:
Now that we have integrated our service with the DynamoDB database, it's time to test the create and get workout functionalities.
Rebuilding and Running the Service
First, rebuild and run the service:
go build -o workout-management-service
./workout-management-service
Testing the Create Route
To create a new workout, execute the following curl command:
curl -X POST http://localhost:1323/create \
-H "Content-Type: application/json" \
-d '{
"owner": "samir@gmail.com",
"name": "Run The Interval",
"category": "running",
"equipment": {
"name": "Running Equipment",
"description": "If indoors we need a threadmill, if outdoors a good place to run fast for 3 min without interruption"
},
"exercises": [
{
"name": "Warmup",
"description": "20min jog"
},
{
"name": "3 min by 5 interval",
"description": "3min x5 at 5k pace with 1min jog"
},
{
"name": "Cooldown",
"description": "20min cool down"
}
]
}'
This command should now store the workout details in the database.
Testing the Get Route
To retrieve the workout you just created, use the following curl command:
curl -X POST http://localhost:1323/create \
-H "Content-Type: application/json" \
-d '{
"owner": "samir@gmail.com",
"name": "Run The Interval",
}'
This request should return the details of the "Run The Interval" workout from the database.
Unit tests
Our services need to be equipped with a set of tests that can be executed with the go test. To demonstrate this, we will craft a few basic unit tests.
We will focus our unit testing on four key functions:
ToDynamoDbAttribute
ToDynamoDbItemInput
CreateWorkout
GetWorkout
While covering these functions does not exhaustively test all functionality of the service, it provides a fundamental level of coverage. These tests serve as a representative sample to illustrate the process of unit testing.
All tests will utilize Go's standard testing package. The core principle of each test is to validate the expected output of a function or series of functions. This pattern will be consistently applied across all unit tests.
ToDynamoDbAttribute unit test
For the ToDynamoDbAttribute unit test, we will invoke the function with a predefined Workout struct and verify that the output aligns with our expectations:
The ToDynamoDbItemInput test follows a similar structure. We'll invoke ToDynamoDbItemInput and validate that the function's output matches our expected result.
CreateWorkout and GetWorkout unit test
The CreateWorkout and GetWorkout functions interact with the DynamoDB instance, requiring a running local instance of DynamoDB (facilitated by Docker Compose). These tests are be designed to:
Use CreateWorkout to store a Workout in DynamoDB, verifying that no errors occur.
Retrieve the same Workout using GetWorkout, ensuring the fetched data matches the stored data.
This process not only tests the functionality of each method but also validates their interaction with the database.
Running the unit test
To execute the unit tests, ensure that the local DynamoDB instance is active and the necessary tables are set up. Run the tests using the following command in the terminal:
go test -v ./...
The user-management-service
The user-management-service, much like the workout-management-service, is an HTTP service that provides a REST API for managing user data. Its core functionalities include creating user profiles and retrieving information about existing users.
This script sets up a Workout table with Owner and Name as the primary key components. The Owner attribute serves as the partition key, and Name as the sort key, allowing multiple workouts per owner with unique names. For more details on dynamodb primary keys see .
Reference for implementation details.
Given that the implementation of the user-management-service closely mirrors that of the workout-management-service, we will not repeat the detailed discussion here. Instead, if you are interested in understanding its implementation specifics, you are encouraged to refer to the dedicated repo.