Build and Deploy: With AWS (Lambda, API Gateway and DynamoDB) using Golang
Tags: go, aws, dynamodb, tutorial
Published: 2024-03-14 05:16:38

As of 2024, AWS has officially deprecated the `go1.x` runtime which came into effect on December 31,2023 as mentioned in their official blog [post](https://aws.amazon.com/blogs/compute/migrating-aws-lambda-functions-from-the-go1-x-runtime-to-the-custom-runtime-on-amazon-linux-2/). In this post, let's revisit and look at how you can build a REST API that interacts with DynamoDB and deploy it as a lambda function using Amazon Linux 2023 runtime. 

For demonstration, I have only shown two functionalities (fetch and create) in this article. But the complete code with all the functionalities can be found [here](https://github.com/Sourjaya/football-aws-lambda).

There are a few standard ways you can deploy the code as a lambda fucntion, such as using a container(Docker), using serverless framework, using terraform scripts or using AWS SAM. 
But using `.zip` file archives to deploy into AWS lambda is one of the more straight forward ways, as mentioned in the official AWS documentation.

---

##Table of contents
####1. Prerequisites
####2. WRITE the code for the Golang REST API
####3. BUILD the executable file
####4. DEPLOY to AWS
####5. TEST the API

---

<section id="prerequisites">
##1. Prerequisites

###1.1. Golang should be installed in your system.

To check wether `GO` is installed in your system or not use the command `go version` in your terminal. If it is not installed, check the official installation [steps](https://go.dev/doc/install).

###1.2. You should have an AWS account and preferrably logged in as an IAM user.

To sign in as a root or an IAM user follow the [steps](https://docs.aws.amazon.com/signin/latest/userguide/how-to-sign-in.html) mentioned in the official documentation.

###1.3. Use a code editor of your choice.

###1.4. AWS CLI should be installed and configured.
Creating policies and roles as well as deploying the lambda function can be done using the AWS management console as well as the AWS CLI(on the terminal). In case of using [AWS CLI](https://aws.amazon.com/getting-started/guides/setup-environment/module-three/), it should be properly configured.

---

##2. WRITE the code for the Golang REST API

###2.1. Folder Structure

```
└── 📁player-api
    └── 📁cmd
        └── main.go
    └── go.mod
    └── go.sum
    └── 📁pkg
        └── 📁handlers
            └── handlers.go
            └── response.go
        └── 📁player
            └── player.go
```

<dl>
  <dt><b>NOTE</b></dt>
  <dd>You may structure the project folder anyway you want. But it is always a good practice to follow a standard pattern to organize the folder. The above structure is one of the ways you can set up your <code>Go</code> projects.
  </dd>
</dl>

---

###2.2 Create your GO Project
Before writing your code you need to setup the directory where you will house your project. After that, open the terminal from the directory, and enter the following command to initialize your project.
```bash
# go mod init [module path]
go mod init github.com/Sourjaya/player-api
```
The go mod init command creates a `go.mod` file to track your code's dependencies. Using your own github repository will provide a unique module path for the project.
Now, in `main.go` write the following code:
{% embed  https://gist.github.com/Sourjaya/dd749a46ee42e683475e10ee03ce0c78%}

In `main.go` create a session by passing in the region, then  create an instance to the DynamoDB client and use `lambda.Start(handler)` to interact with an internal Lambda endpoint to pass requests to the handler. Then in the `handler` function check for the HTTP method type of the request and call the appropriate functions and return those functions.
Make sure you have an environment variable named `AWS_REGION` in your system. To create the environment variable in Linux, use this command:
```bash
#Open ~/.profile
sudo nano ~/.profile
```
Then write the `export` command at the end of the file:
```bash
#export AWS_REGION=[your_aws_region]
export AWS_REGION=ap-south-1
```
And then press `CTRL+S` to save and `CTRL+X` to exit nano. Now `logout`of your system and log back in.

After you have written the main file, in `pkg/handlers/handlers.go` define and write the functions for each http method type. Write the following code:
{% embed https://gist.github.com/Sourjaya/bcf5ab46900c5ece1f453eb74033b063 %}

The `GetPlayer` function calls `GetPlayerByID` if ID is provided as URL parameter, to fetch the player information with respect to the provided key, else it calls `GetPlayers` to fetch information of all the players in the table. The two functions are available in the `players` package. 

The `CreatePlayer` function calls `CreatePlayer` from the player package to add a new player record in the table. Both `GetPlayer` and `Create Player` takes `request`, `tablename` and `client` as function parameters and returns a `response` and a `http status code`. The `Unhandled` function return a `405` status code and a error message.

The response method is responsible to create the `APIGatewayProxyResponse` which will contain appropriate headers, status code and the json response body.

{% embed https://gist.github.com/Sourjaya/2cdb53833c72d0208585e94d74bf0ed8 %}

Now, in `player.go` write the functions that will interact with DynamoDB.

{% embed https://gist.github.com/Sourjaya/dc803b65f36f3f28d0e603d3cfc10291 %}

Function `GetPlayerByID` takes the ID, tablename and client as parameters and returns a `Player` structure containing the player info and an error(if found). 

Function `GetPlayers` takes tablename and client as parameters and returns a slice(list) containing info about each player in the database table along with an error(if found).

Function `Create Player` takes request, tablename and client as parameters and returns a `Player` structure containing the player info and an error(if found).
</section>

---

<section id="build">
##3. BUILD the executable file

###3.1 Install the dependencies
Now that you have written all the code needed to build the executable file, it is necessary to install all the dependencies and remove unused modules. To do that, in the terminal execute the command:
```bash
# This will generate go.sum file
go mod tidy
```

###3.2 Create the executable file and zip it

If you are using **Linux/Mac** system execute this command(according to your system architecture use any one of the following commands) to generate the executable:

#####For x86_64 architecture
```bash
GOOS=linux GOARCH=amd64 go build -tags lambda.norpc -o bootstrap main.go
```
#####For arm64 architecture
```bash
GOOS=linux GOARCH=arm64 go build -tags lambda.norpc -o bootstrap main.go
```
#####(Optional) To create a stable binary package for standard C library (libc) versions
```bash
GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go build -o bootstrap -tags lambda.norpc main.go
```

If you are using **Windows** system execute the following set of commands in terminal(using CGO_ENABLED is optional):

#####For x86_64 architecture
```bash
set GOOS=linux
set GOARCH=amd64
go build -tags lambda.norpc -o bootstrap main.go
```
#####For arm64 architecture
```bash
set GOOS=linux
set GOARCH=arm64
go build -tags lambda.norpc -o bootstrap main.go
```

<dl>
  <dt><b>NOTE</b></dt>
  <dd>Since we are using <code>provided.al2023</code>(Amazon Linux 2023 runtime) we have to name our go executable binary file as <code>bootstrap</code>.
  </dd>
</dl>

Now to create the `.zip` file in **Linux/Mac**:
```bash
# zip -jrm [zip file name along with the extension]
# the -m flag deletes the executable 
# file which is not needed anymore
zip -jrm football_api.zip bootstrap
```
And to create the `.zip` file in **Windows** :

```
tar -acf football_api.zip bootstrap
del bootstrap
```
</section>

---

<section id="aws">
## 4. DEPLOY to AWS
### 4.1 Create the Lambda Function
### 4.1.1 Using AWS CLI
#### Create Execution Policy

To create an execution policy create a json file with the following content:
**policy.json**
```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "Stmt1428341300017",
            "Action": [
                "dynamodb:DeleteItem",
                "dynamodb:GetItem",
                "dynamodb:PutItem",
                "dynamodb:Query",
                "dynamodb:Scan",
                "dynamodb:UpdateItem"
            ],
            "Effect": "Allow",
            "Resource": "*"
        },
        {
            "Sid": "",
            "Resource": "*",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Effect": "Allow"
        }
    ]
}
```
Now execute the command:
```bash
#Syntax
:'
aws iam create-policy \
    --policy-name [policy_name] \
    --policy-document file://[file_path/file_name]
'

#Command
aws iam create-policy \
    --policy-name lambda-api \
    --policy-document file://./policy.json
```
You have successfully created a policy. Remember to note the policy ARN from the response in the terminal. The policy ARN would look something like `arn:aws:iam::111111111111:policy/lambda-api`

#### Create Execution Role and attach Policy
To create an execution role create a json file with the following content:
**trust-policy.json**
```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```
Now execute the command:
```bash
#Syntax
:'
aws iam create-role --role-name [role_name] \
--assume-role-policy-document file://[file_path/file_name]
'

#Command
#Remember to note the role ARN as well.
aws iam create-role --role-name lambda-api \
--assume-role-policy-document file://./trust-policy.json
```
Now, we have to attach the `policy` we created to this `role`. 
To do that use this command:
```bash
#Syntax
:'
aws iam attach-role-policy --role-name [role_name] \
--policy-arn [policy_arn that was noted]
'
#Command
aws iam attach-role-policy --role-name lambda-api \
--policy-arn arn:aws:iam::111111111111:policy/lambda-api
```
#### Create Lambda function
To create a lambda function using AWS CLI use the following command in the terminal:
```bash
# Syntax
:'
aws lambda create-function --function-name [lambda_function_name] \
--runtime provided.al2023 --handler [handler_name] \
--architectures [your_system_architecture] \
--role arn:aws:iam::[aws_id]:role/[role_name] \
--zip-file fileb://[zip_file]
'

# Command
aws lambda create-function --function-name new-lambda \
--runtime provided.al2023 --handler bootstrap \
--role arn:aws:iam::111111111111:role/lambda-api \
--zip-file fileb://football_api.zip
```
<dl>
  <dt><b>NOTE</b></dt>
  <dd>
<ul>
<li>The <code>--architectures</code> option is only required if you're using arm64. The default value is x86_64.</li>
<li>For <code>--role</code>, specify the Amazon Resource Name (ARN) of the execution role.</li>
<li>I have not shown my AWS ID in any of the above commands. Put your 12-digit AWS ID in place of those 1's and use it.  </li>
<li>Replace <code>'\'</code> from the commands with <code>'^'</code> if you are using a  <b>Windows</b> system </li>
</ul>
  </dd>
</dl>

---

### 4.1.2 Using AWS Management Console
Login to the management console. And follow the steps.
#### Create Execution Policy
To create an execution policy:
1. Select _**Security credentials**_ from the dropdown when you click on your account at the top right.

![Security credential](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/djlvppg088ilklo2ijls.png)
2. Go to _**Access management-->Policies**_ .
3. Click on _**Create Policy**_ button.
4. Now in _**Policy editor**_ and under _**JSON**_ tab put the content of `policy.json` file.
5. Now click on _**Next**_ and give it a Name(lambda-api) and Description(optional) and then click _**Create Policy**_.

You have successfully created a policy.

#### Create Execution Role and attach Policy
To create an Execution Role:
1. Go to _**Access management-->Roles**_ .
2. Under _**Trusted entity type**_ select _**AWS service**_ and under _**Use case**_ select _**lambda**_ and click _**Next**_.
3. In _**Add permissions**_ search and select the policy(lamda-api) you created and click _**Next**_.
4. Now give it a Name(lambda-api) and click **Create role**.

#### Create Lambda function
To create a lambda function using AWS Console follow the steps:
1. Search _**Lambda**_ in the search box and go to the `Functions` page.
2. Click on _**Create Function**_ button.
3. Give the function a Name(new-lambda), select the Runtime(Amazon Linux 2023) and select the Architecture(x86_64 in my case).
4. Under _**Permissions**_ tab change the execution role.
5. You may choose the role that was created from _**Use an existing role**_ or choose **_Simple microservice permissions_** from **Policy templates** by selecting _**Create a new role from AWS policy templates**_ .
6. Now after you click on _**Create function**_ you have to _**Edit**_ the _**Runtime Settings**_ and change handler name to `bootstrap`.
7. The only step that is left is to upload the _**Code Source**_ which will be the `zip` file that was created.

You have successfully created a Lambda function.

### 4.2 Create a table in DynamoDB
We can create a table in DynamoDB using both the AWS CLI or the Console. I prefer to use the Console, as the table attributes are created through code. But if you want to learn how to use AWS CLI to use DynamoDB follow this [link](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/getting-started-step-1.html).

To create a table using DynamoDB:
1. Search _**DynamoDB**_ in the search box and go to the `Tables` page.
2. Click on _**Create Table**_ and give the table a Name(you have mentioned in your code).
3. Enter _**Partition Key**_(which will be the primary key). In this case it will be the `id` field.
4. Click on _**Create Table**_.

### 4.3 Create a REST API in APIGateway
To create the API:
1. Search _**API Gateway**_ in the search box and in the `API` page click on _**Create API**_.
2. Now scroll down and click _**Build**_ on _**REST API**_ tab. 
3. Under _**API details**_ tab select _**New API**_ and then give tha API a name and select `Regional` in _**API endpoint type**_.
4. After you click on _**Create API**_, the page will be redirected to _**Resources**_ under the API. Click on _**Create method**_ under _**Methods**_ tab.
5. Select `ANY` as _**Method type**_, Chose the _**Lambda function**_ name(or alias) and the region(in my case it was `ap-south-1`) .
6. Enable the _**Lambda proxy integration**_ and _**Default timeout**_ options and click on _**Create method**_.
7. The final step is to click on _**Deploy API**_, create a _**New Stage**_(staging) and _**Deploy**_. Make sure to note the `Invoke URL` which will look like this: `https://unp18fmgcc.execute-api.ap-south-1.amazonaws.com/staging`.

You will see something like this:

![Lambda Overview](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gpulnqb3adcq0uvjn6cg.png)
You have successfully deployed a REST API using a serverless stack. Now we will look at how we can Test the API and wether the endpoints are working or not.

---

</section>
<section id="test">
##5. TEST the API
The API will have the following Endpoints:

| HTTP Method | EndPoint         | Description              |
|-------------|------------------|--------------------------|
| GET         | /staging         | Retrieve all players.    |
| GET         | /staging?id={ID} | Retrieve a player by ID. |
| POST        | /staging         | Create a new player.     |

You can Test your API using the good old `curl` command. The commands are given below:
#### 1. Get All Players.
```bash
#Syntax
#curl -X GET [Invoke URL]
#Command
curl -X GET https://unp18fmgcc.execute-api.ap-south-1.amazonaws.com/staging
```

#### 2. Get Player by ID.

```bash
#Syntax
#curl -X GET [Invoke URL]?id=[id]
#Command
curl -X GET https://unp18fmgcc.execute-api.ap-south-1.amazonaws.com/staging?id=7ff4d720-a825-4727-ad95-e37dd62b90cf
```

#### 3. Create Player.

```bash
#Syntax
:'
curl --header "Content-Type: application/json" --request POST --data [JSON data] [Invoke URL]
'
#Command
curl --header "Content-Type: application/json" --request POST --data '{"firstName": "Luis", "lastName": "Suarez",    "country":"uru","position":"FW","club":"Inter Miami"}' https://unp18fmgcc.execute-api.ap-south-1.amazonaws.com/staging
```
**Postman** can also be used to Test the endpoints and you can make a  postman collection similar to:

![Postman](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgdfghqv18goql00zxeb.png) 
</section>

---

## References
1. [AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/lambda-golang.html)
2. [AWS DynamoDB](https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GettingStarted.html)
3. [AWS APIGateway](https://docs.aws.amazon.com/apigateway/latest/developerguide/getting-started-with-lambda-integration.html)
4. [AWS Tutorials](https://aws.amazon.com/getting-started/guides/setup-environment)

---

I wanted to document what I learned and how I deployed to AWS lambda in an article so that beginners like me can get help from it. 

If you found this article informative, and if you have any feedback, do drop a comment. And let's connect on [𝕏](https://twitter.com/sourjaya_das). Thank You.