← Blogs

Using Golang, AWS Lambda serverless function, SNS & EventBridge for stock notification.

Overview

Writing AWS Lambda serverless function handler in Go, which gets triggered by the EventBridge rule that runs on a schedule, and sends an email notification if product stock drops a specific number.

Lambda function in Go

AWS Go SDK makes it easy to create the Lambda handler function and to send notifications using SNS.

Set up Shopify environmental variables & fetch Products

main.go
package main

import(
    "os"
    "io/ioutil"
    "net/http"
)
func HandleRequest() {
    //Lambda environment variable
    apiKey := os.Getenv("SHOPIFY_API_KEY")
    password := os.Getenv("SHOPIPY_API_PASSWORD")	
    domain := os.Getenv("SHOPIFY_SHOPIFY_DOMAIN")

    url := fmt.Sprintf("https://%s:%s@%s.myshopify.com/admin/api/2022-04/products.json", apiKey, password, domain)

    resp, err := http.Get(url)
    if err != nil {
        panic(err)
    }

    defer resp.Body.Close()

    bytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }
}
func main() {
    lambda.Start(HandleRequest)
}

Extracting Product SKU of the which stock is below 10.

main.go
import(
    "os"
    "io/ioutil"
    "net/http"
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "strings"
)

func HandleRequest() {
    //Lambda environment variable
    apiKey := os.Getenv("SHOPIFY_API_KEY")
    password := os.Getenv("SHOPIPY_API_PASSWORD")	
    domain := os.Getenv("SHOPIFY_SHOPIFY_DOMAIN")

    url := fmt.Sprintf("https://%s:%s@%s.myshopify.com/admin/api/2022-04/products.json", apiKey, password, domain)

    resp, err := http.Get(url)
    if err != nil {
        panic(err)
    }

    defer resp.Body.Close()

    bytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }

    var products map[string]interface{}
    if err := json.NewDecoder(req.Body).Decode(&bytes); err != nil {
        panic(err)
    } 

    skuToStock := []string{}

    for _, productData := range products {
	    for _, products := range productData.([]interface{}) {
		    for key, product := range products.(map[string]interface{}) {
			    if key == "variants" {
				    for _, variant := range product.([]interface{}) {
					    if variant.(map[string]interface{})["inventory_quantity"].(float64) <= 10 {
						    sku := variant.(map[string]interface{})["SKU"]
						    skuToStock = append(skuToStock, sku.(string))
					     }
				    }
			    }
		    }
	    }
    }
}

Using AWS SDK to send notification

main.go
import(
    "os"
    "io/ioutil"
    "net/http"
    "bytes"
    "encoding/json"
    "fmt"
    "io"
    "strings"
    "github.com/aws/aws-lambda-go/lambda"
    "github.com/aws/aws-sdk-go-v2/config"
    "github.com/aws/aws-sdk-go-v2/service/sns"
)

type SNSPublishAPI interface {
	Publish(ctx context.Context,
		params *sns.PublishInput,
		optFns ...func(*sns.Options)) (*sns.PublishOutput, error)
}

func PublishMessage(c context.Context, api SNSPublishAPI, input *sns.PublishInput) (*sns.PublishOutput, error) {
	return api.Publish(c, input)
}

func HandleRequest() {
    //Lambda environment variable
    apiKey := os.Getenv("SHOPIFY_API_KEY")
    password := os.Getenv("SHOPIPY_API_PASSWORD")	
    domain := os.Getenv("SHOPIFY_SHOPIFY_DOMAIN")

    url := fmt.Sprintf("https://%s:%s@%s.myshopify.com/admin/api/2022-04/products.json", apiKey, password, domain)

    resp, err := http.Get(url)
    if err != nil {
        panic(err)
    }

    defer resp.Body.Close()

    bytes, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        panic(err)
    }

    var products map[string]interface{}
    if err := json.NewDecoder(req.Body).Decode(&bytes); err != nil {
        panic(err)
    }

    skuToStock := []string{}

    for _, productData := range products {
	    for _, products := range productData.([]interface{}) {
		    for key, product := range products.(map[string]interface{}) {
			    if key == "variants" {
				    for _, variant := range product.([]interface{}) {
					    if variant.(map[string]interface{})["inventory_quantity"].(float64) <= 10 {
						    sku := variant.(map[string]interface{})["SKU"]
						    skuToStock = append(skuToStock, sku.(string))
					     }
				    }
			    }
		    }
	    }
    }

    if len(skuToStock) > 0 {
	    snsMessage := strings.Join(skuToStock, ",")
	    topicARN := "<lambda-arn>"

	    cfg, err := config.LoadDefaultConfig(context.TODO())

	    if err != nil {
	            panic("configuration error, " + err.Error())
	    }

	    client := sns.NewFromConfig(cfg)
	    input := &sns.PublishInput{
		    Message:  &snsMessage,
		    TopicArn: &topicARN,
	    }

	    result, err := PublishMessage(context.TODO(), client, input)
	    if err != nil {
		    fmt.Println("Got an error publishing the message:")
		    fmt.Println(err)
	    }

	    fmt.Println("Message ID: " + *result.MessageId)
    }
}

Prerequisites - AWS CLI & AWS Shell

AWS Command Line Interface(CLI) is used in this post to deploy the Lambda function. CLI needs to be installed and configured locally in order to use it.

AWS Shell helps with auto-completion, fuzzy searching, and more.

How to deploy it on AWS

We're uploading the Go binary file into AWS Lambda as a .zip file. In order to build for Linux, we're using the following command.

GOOS=linux GOARCH=amd64 go build -o main main.go
zip main.zip main

Creating IAM roles & policies.

Create the assume-role-policy.json file:

assume-role-policy.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "lambda.amazonaws.com"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

Create an IAM role

shell
iam create-role --role-name role-lambda-shopify --assume-role-policy-document file://assume-role-policy.json
response
{
    "Role": {
        "Path": "/",
        "RoleName": "role-lambda-shopify",
        "RoleId": "AROARKDOZJ3E6FZQIORPR",
        "Arn": "arn:aws:iam::090426658505:role/role-lambda-shopify",
        "CreateDate": "2022-06-25T21:30:57Z",
        "AssumeRolePolicyDocument": {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Principal": {
                        "Service": "lambda.amazonaws.com"
                    },
                    "Action": "sts:AssumeRole"
                }
            ]
        }
    }
}

Let's attach permission to this role. AWSLambdaBasicExecutionRole gives permission to write logs to cloudWatch.

iam attach-role-policy --role-name role-lambda-shopify --policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

Create an SNS topic

sns create-topic --name shopify-stock-notification
response
{
    "TopicArn": "arn:aws:sns:ap-southeast-2:090426658505:shopify-stock-notification"
}

To subscribe to the SNS topic:

sns subscribe --topic-arn arn:aws:sns:ap-southeast-2:090426658505:shopify-stock-notification --protocol email --notification-endpoint test@hotmail.com

we're going to attach an inline policy that grants access to the Lambda function to trigger SNS notification.

sns-policy-for-lambda.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sns:Publish",
      "Resource": "arn:aws:sns:ap-southeast-2:090426658505:shopify-stock-notification"
    }
  ]
}

iam put-role-policy --role-name role-lambda-shopify --policy-name publish-to-sns --policy-document file://sns-policy-for-lambda.json

Create/Update Lambda function

lambda create-function --function-name function-lambda-shopify-stock --zip-file fileb://main.zip --handler main --runtime go1.x --role arn:aws:iam::090426658505:role/role-lambda-shopify
response
{
    "FunctionName": "function-lambda-shopify-stock",
    "FunctionArn": "arn:aws:lambda:ap-southeast-2:090426658505:function:function-lambda-shopify-stock",
    "Runtime": "go1.x",
    "Role": "arn:aws:iam::090426658505:role/role-lambda-shopify",
    "Handler": "main",
    "CodeSize": 6662556,
    "Description": "",
    "Timeout": 3,
    "MemorySize": 128,
    "LastModified": "2022-06-25T22:04:02.491+0000",
    "CodeSha256": "uLsOQEp4VCFi294HpbKyl1i60uLGUIBwmpI2cg0wdeU=",
    "Version": "$LATEST",
    "TracingConfig": {
        "Mode": "PassThrough"
    },
    "RevisionId": "1ff52d69-6429-49d8-ba7f-492886e9aee3",
    "State": "Pending",
    "StateReason": "The function is being created.",
    "StateReasonCode": "Creating",
    "PackageType": "Zip",
    "Architectures": [
        "x86_64"
    ],
    "EphemeralStorage": {
        "Size": 512
    }
}

To add environmental variable to Lambda function:

lambda update-function-configuration --function-name function-lambda-shopify-stock --environment "Variables={SHOPIFY_API_KEY=<your-shopify-key>, SHOPIPY_API_PASSWORD=<your-shopify-password>, SHOPIFY_SHOPIFY_DOMAIN=c<your-shopify-domain>}

To update the Lambda function:

lambda update-function-code --function-name function-lambda-shopify-stock --zip-file fileb://main.zip

To invoke the Lambda function and store the output to a text file:

lambda invoke --function-name function-lambda-shopify-stock output.txt

EventBridge scheduler

EventBridge rule let you create a cron that triggers the Lambda function.

image

image

image

The code is available in Github repo