Creating an HTTP Function Using a Container Image Built with Go

Introduction

For general details about how to use a container image to create and execute an HTTP function, see Creating an HTTP Function Using a Container Image and executing the Function in the user manual.

This chapter describes how to create an image using the Go language and perform local verification for HTTP functions.

Note

You need to implement an HTTP server in the image listening to port 8000 to receive requests.

To initialize the function configuration, implement the “init()” function, which Go will execute automatically.

Constraints and Limitations

For constraints and limitations when building Go HTTP functions using container images, see General Constraints for HTTP Functions.

Step 1: Create the Project

In this example we use the gin framework to create an HTTP server.

Complete code for this sample can be found on Github: Container HTTP Sample

For details about gin framework, see:

Create Go Module

1. Create directories for your project and navigate to it

# create project directory and src subdirectory
mkdir -p container-http/src
# navigate to project directory
cd container-http

2. Initialize a Go module

Run the following command to initialize a new Go module:

go mod init container-http

3. Add dependencies

Run the following commands to add the necessary dependencies:

# add gin framework
go get -u github.com/gin-gonic/gin

4. Resulting go.mod

The resulting go.mod file should look like this:

module container-http

go 1.25.6

require (
  github.com/gin-gonic/gin v1.11.0
  github.com/opentelekomcloud-community/otc-functiongraph-go-runtime v0.0.0-00010101000000-000000000000
  go.uber.org/zap v1.27.1
)

replace github.com/opentelekomcloud-community/otc-functiongraph-go-runtime => ../../../otc-functiongraph-go-runtime

require (
  github.com/bytedance/gopkg v0.1.3 // indirect
  github.com/bytedance/sonic v1.15.0 // indirect
  github.com/bytedance/sonic/loader v0.5.0 // indirect
  github.com/cloudwego/base64x v0.1.6 // indirect
  github.com/gabriel-vasile/mimetype v1.4.12 // indirect
  github.com/gin-contrib/sse v1.1.0 // indirect
  github.com/go-playground/locales v0.14.1 // indirect
  github.com/go-playground/universal-translator v0.18.1 // indirect
  github.com/go-playground/validator/v10 v10.30.1 // indirect
  github.com/goccy/go-json v0.10.5 // indirect
  github.com/goccy/go-yaml v1.19.2 // indirect
  github.com/json-iterator/go v1.1.12 // indirect
  github.com/klauspost/cpuid/v2 v2.3.0 // indirect
  github.com/leodido/go-urn v1.4.0 // indirect
  github.com/mattn/go-isatty v0.0.20 // indirect
  github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
  github.com/modern-go/reflect2 v1.0.2 // indirect
  github.com/pelletier/go-toml/v2 v2.2.4 // indirect
  github.com/quic-go/qpack v0.6.0 // indirect
  github.com/quic-go/quic-go v0.59.0 // indirect
  github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
  github.com/ugorji/go/codec v1.3.1 // indirect
  go.uber.org/mock v0.6.0 // indirect
  go.uber.org/multierr v1.10.0 // indirect
  golang.org/x/arch v0.23.0 // indirect
  golang.org/x/crypto v0.47.0 // indirect
  golang.org/x/net v0.49.0 // indirect
  golang.org/x/sys v0.40.0 // indirect
  golang.org/x/text v0.33.0 // indirect
  google.golang.org/protobuf v1.36.11 // indirect
)

Implement the function

Change folder to src folder:

# navigate to src directory
cd src

The FunctionGraph program implements an HTTP server to process POST and GET requests on /index and give a response.

  • Create a main.go file,

  • import the gin dependency package,

  • implement a function named init() to initialize the configuration.

    Go runs this function named init automatically before any other part of the package.

  • implement a function to process POST requests on /index.

  • implement a function to process GET requests on /index.

The following code is an example implementation of the above steps.

src/main.go
package main

import (
  "bytes"
  "container-http/src/fghttpmiddleware"
  "io"
  "log"
  "time"

  "github.com/gin-gonic/gin" // Import the Gin framework
  "go.uber.org/zap"
  "go.uber.org/zap/zapcore"
)

// go init function is called before main function.
func init() {
  log.Println("init in main.go ")
}

// Logger is a middleware function used
// to record request and response information.
func LogRequestResponse(logger *zap.SugaredLogger) gin.HandlerFunc {
  return func(c *gin.Context) {
    ctx, logger := fghttpmiddleware.FGContext(c)
    logger.Infow("FunctionName", "functionName", ctx.GetFuncName())

    start := time.Now()

    reqBody, _ := c.GetRawData()

    logger.Infow("Request", "method", c.Request.Method, "uri", c.Request.RequestURI, "body", string(reqBody))

    // Assign Back the request body as body can only be read once
    // by default in Gin framework.
    c.Request.Body = io.NopCloser(bytes.NewBuffer(reqBody))

    c.Next()

    end := time.Now()
    latency := end.Sub(start)
    respBody := string(rune(c.Writer.Size()))

    logger.Infow("Response", "method", c.Request.Method, "uri", c.Request.RequestURI, "body", respBody, "latency", latency)
  }
}

func main() {
  // Create a Gin router.
  router := gin.New()

  // Configure zap logger to output logs in JSON
  config := zap.NewProductionConfig()

  config.EncoderConfig.TimeKey = "ts"

  // config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
  config.EncoderConfig.EncodeTime = zapcore.TimeEncoder(func(t time.Time, enc zapcore.PrimitiveArrayEncoder) {
    enc.AppendString(t.UTC().Format("2006-01-02T15:04:05.00000Z"))
  })

  // Create a logger
  logger, _ := config.Build()
  defer logger.Sync()

  // Create a sugared logger for easier logging with structured context.
  sugar := logger.Sugar()

  // Add middleware for FunctionGraph context and logging
  router.Use(fghttpmiddleware.InitFGMiddleware(sugar))

  // LogRequestResponse middleware logs the request and response information.
  router.Use(LogRequestResponse(sugar))

  // Recovery middleware recovers from any panics and writes
  // a 500 if there was one.
  router.Use(gin.Recovery())

  // Registers a route for handling HTTP POST requests at the path `/index`.
  // When a client sends a
  // POST request to `/index`, the Gin framework will call
  // the `InvokeIndexPOST` function to handle the request.
  router.POST("/index", InvokeIndexPOST)

  router.GET("/index", InvokeIndexGET)

  err := router.Run(":8000") // Starts the HTTP server, listening on port 8000.
  if err != nil {
    return
  }
}

The logic to process the event is implemented in the function invokeIndex in invokeAPIG.go.

src/invokeAPIG.go
package main

import (
  "container-http/src/fghttpmiddleware"
  "fmt"
  "net/http"

  "github.com/gin-gonic/gin" // Import the Gin framework.
)

// invokeIndexPOST is a handler function that processes requests
// to the POST /index route.
func InvokeIndexPOST(c *gin.Context) {
  ctx, logger := fghttpmiddleware.FGContext(c)

  // Read the request body.
  reqBody, _ := c.GetRawData()

  logger.Infof("Received request [%s] body: %s\n", ctx.GetRequestID(), string(reqBody))

  c.Request.Header.Get("")

  // fill API Gateway response structure.
  returnBody := "Hello from FunctionGraph!"

  c.String(http.StatusOK, returnBody)
}

// invokeIndexGET is a handler function that processes requests
// to the GET /index route.
func InvokeIndexGET(c *gin.Context) {
  // Read query parameters
  name := c.Query("name")
  if name == "" {
    name = "World"
  }

  returnBody := fmt.Sprintf("Hello, %s! This is a GET request.", name)

  c.String(http.StatusOK, returnBody)
}

Run and Test from source code

To test the function from source code, execute in the container-http/src folder:

go run .

The server starts listening on port 8000.

You can use curl to send a POST request to the function using a new shell.

curl -X POST \
  -H 'Content-Type: application/json' \
  -d '{"message": "Hello from API Gateway POST request"}' \
  http://localhost:8000/index

The expected response is:

Hello from FunctionGraph!

Create a Makefile

To simplify the development and testing process, create a Makefile in the container-http folder:

Makefile
TARGET_PATH=target
#DOCKER_FILE=Dockerfile
DOCKER_FILE=Dockerfile.alpine

IMAGE_NAME=custom_container_http_example

build: clean
  mkdir -p $(TARGET_PATH)
  GOARCH=amd64 GOOS=linux CGO_ENABLED=0 && \
  cd src && \
  go build -o ../$(TARGET_PATH)/httpfunction .

run_local:
  cd $(TARGET_PATH) && ./httpfunction

docker_build: build
  docker buildx build \
    --platform linux/amd64 \
    --build-arg FILE_PATH=$(TARGET_PATH) \
    --file $(DOCKER_FILE) \
    --tag $(IMAGE_NAME):latest .

docker_run_local: docker_build
  docker container run \
    --rm \
    --platform linux/amd64 \
    --publish 8000:8000 \
    --name container_http_example \
    $(IMAGE_NAME):latest

docker_push: docker_build
  # see: https://docs.otc.t-systems.com/software-repository-container/umn/image_management/obtaining_a_long-term_valid_login_command.html#swr-01-1000
  docker login -u $(OTC_SDK_PROJECTNAME)@$(OTC_SDK_AK) -p $(OTC_SWR_LOGIN_KEY) $(OTC_SWR_ENDPOINT)
  docker tag $(IMAGE_NAME):latest $(OTC_SWR_ENDPOINT)/$(OTC_SWR_ORGANIZATION)/$(IMAGE_NAME):latest
  docker push $(OTC_SWR_ENDPOINT)/$(OTC_SWR_ORGANIZATION)/$(IMAGE_NAME):latest

docker_all: build docker_build docker_push

test_local:
  # execute a curl 
  @curl -X POST \
    -H "X-Cff-Request-Id: $(shell uuidgen)" \
    -H "X-Cff-Func-Name: 0@default@sample-container-http" \
    -H "X-Cff-Func-Version: latest" \
    -H 'Content-Type: application/json' \   
    -d '{"message": "Hello from API Gateway POST request"}' \
    http://localhost:8000/index
  @echo ""

clean:
  rm -rf $(TARGET_PATH)

all: build docker_build

.PHONY: build run_local docker_build docker_run_local docker_push docker_all test_local clean

Build the program

Build the program either using go build or the Makefile target build:

Run the following command in the container-http folder to build the program:

# create output folder (if not already existing)
mkdir -p target

# build the program
GOARCH=amd64 GOOS=linux CGO_ENABLED=0 && cd src && go build -o ../target/httpfunction .

Run and Test program locally

1. Run the program locally

Run the program locally either using executable or the Makefile target run_local:

Run the following command in the container-http folder to run the compiled program:

./target/httpfunction

In the terminal, you should see output similar to the following:

init in main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env:   export GIN_MODE=release
- using code:  gin.SetMode(gin.ReleaseMode)

[GIN-debug] POST   /index                    --> main.invokeIndex (4 handlers)
[GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.
[GIN-debug] Listening and serving HTTP on :8000

2. Test the program locally

Test the program locally either using curl or the Makefile target test_local:

Run the following command in a new terminal to test the program using a curl command:

curl -X POST \
  -H 'Content-Type: application/json' \
  -d '{"message": "Hello from API Gateway POST request"}' \
  localhost:8000/index

You should see output similar to the following:

Hello from FunctionGraph!

Step 2: Build the Image

Create a Dockerfile

Create a Dockerfile in the container-http folder to define the image.

Note

  • In the cloud environment, UID 1003 and GID 1003 are used to start the container by default.
    The two IDs can be modified by choosing Configuration > Basic Settings > Container Image Override
    on the function details page. They cannot be root or a reserved ID.
  • If the base image of the Alpine version is used, run the addgroup and adduser instead of groupadd and useradd commands.
  • You an use any base image that meets your application requirements.

Following are two example Dockerfiles, one using Ubuntu base image and another using Alpine base image.

Note

  • Ubuntu images are larger in size but come with more pre-installed libraries.

  • Alpine images are smaller in size but may require additional libraries depending on the application requirements.

Example Dockerfile using Ubuntu base image:

Dockerfile
# Example Dockerfile for OTC FunctionGraph Container Event Function 
# using Ubuntu base image
FROM amd64/ubuntu:22.04

# set environment variables
ENV HOME=/home/paas

ENV GROUP_ID=1003
ENV GROUP_NAME=paas_user

ENV USER_ID=1003
ENV USER_NAME=paas_user

# Set timzone to UTC
ENV TZ=Etc/UTC

# create non root user and home directory
RUN mkdir -m 550 ${HOME} && \
   # add group and user with specific IDs
   groupadd -g ${GROUP_ID} ${GROUP_NAME} && \
   # add user with specific UID and GID
   useradd -u ${USER_ID} -g ${GROUP_ID} ${USER_NAME}

# copy content of target folder
ADD ./target ${HOME}

# adjust permissions
RUN chown -R ${USER_ID}:${GROUP_ID} ${HOME} && \
    chmod -R 775 ${HOME}

# switch to non root user
USER ${USER_NAME}

# switch to working directory
WORKDIR ${HOME}

# expose port and entrypoint
# FunctionGraph invokes container event function through port 8000
EXPOSE 8000

ENTRYPOINT ["/home/paas/httpfunction"]

Build and verify the image locally

1. Build the image

Build the image either using docker build or the Makefile target docker_build:

Run the following command in the container-http folder to build the image:

docker buildx build \
   --platform linux/amd64 \
   --build-arg FILE_PATH=target \
   --file Dockerfile \
   --tag custom_container_http_example:latest .

Note

Replace Dockerfile with Dockerfile.alpine in the above command to build the image using the Alpine base image.

2. Run the image locally

Run the image either using docker run or the Makefile target docker_run_local:

Run the following command in the container-http folder to run the image:

docker container run --rm \
  --platform linux/amd64 \
  --publish 8000:8000 \
  --name container_http_example \
  custom_container_http_example:latest

3. Test the image locally

Test the image either using curl or the Makefile target test_local:

Run the following command in a new terminal to test the image using a curl command:

curl -X POST \
  -H 'Content-Type: application/json' \
  -d '{"message": "Hello from API Gateway POST request"}' \
  localhost:8000/invoke

You should see output similar to the following:

Hello from FunctionGraph!

Step 3: Upload the Image to SWR

For details on SWR (SoftWare Repository for Container), see:

Prerequisites

Upload the image to SWR

To upload the container image to SWR, following values are needed:

Parameter

Description

OTC_SDK_PROJECTNAME

Your project name.
To obtain this, see: Obtaining a Project ID in API usage guide but use the project name instead of the project ID.

OTC_SDK_AK

Your Access Key

OTC_SWR_LOGIN_KEY

The login key for SWR.
For details see: Obtaining a Long-Term Docker Login Command in the Software Repository for Container user manual.

It can be generated using the access key ${OTC_SDK_AK} and secret key ${OTC_SDK_SK} as follows:
export OTC_SWR_LOGIN_KEY=$(printf "${OTC_SDK_AK}" | \
        openssl dgst -binary -sha256 -hmac "${OTC_SDK_SK}" | \
        od -An -vtx1 | sed 's/[ \n]//g' | sed 'N;s/\n//')

OTC_SWR_ENDPOINT

SWR endpoint, e.g. swr.eu-de.otc.t-systems.com

OTC_SWR_ORGANIZATION

Your SWR organization name

IMAGE_NAME

The name of your container image

Set the environment variables:
export OTC_SDK_PROJECTNAME=<your_project_name>
export OTC_SDK_AK=<your_access_key>
export OTC_SDK_SK=<your_secret_key>
export OTC_SWR_LOGIN_KEY=$(printf "${OTC_SDK_AK}" | \
        openssl dgst -binary -sha256 -hmac "${OTC_SDK_SK}" | \
        od -An -vtx1 | sed 's/[ \n]//g' | sed 'N;s/\n//')
export OTC_SWR_ENDPOINT=swr.eu-de.otc.t-systems.com
export OTC_SWR_ORGANIZATION=<your_swr_organization>
export IMAGE_NAME=custom_container_http_example

Upload the image to SWR either using shell commands or the Makefile target docker_push:

Run the following commands in the container-http folder to upload the image to SWR:

1. Login to SWR
  docker login -u $(OTC_SDK_PROJECTNAME)@$(OTC_SDK_AK) -p $(OTC_SWR_LOGIN_KEY) ${OTC_SWR_ENDPOINT}
2. Tag the image
  docker tag $(IMAGE_NAME):latest ${OTC_SWR_ENDPOINT}/$(OTC_SWR_ORGANIZATION)/$(IMAGE_NAME):latest
3. Push the image to SWR
  docker push ${OTC_SWR_ENDPOINT}/$(OTC_SWR_ORGANIZATION)/$(IMAGE_NAME):latest

Step 4: Create FunctionGraph HTTP Function

  1. In the left navigation pane of the management console, choose Compute > FunctionGraph. On the FunctionGraph console, choose Functions > Function List from the navigation pane.

  2. Click Create Function in the upper right corner. On the displayed page, select Container Image for creation mode.

  3. Set the basic function information.

    • Function Type: Select HTTP Function.

    • Region: The default value is used. You can select other regions.

      Regions are geographic areas isolated from each other. Resources are region-specific and cannot be used across regions through internal network connections. For low network latency and quick resource access, select the nearest region.

    • Function Name: Enter e.g. custom_container_http.

    • Enterprise Project: The default value is default. You can select the created enterprise project.

      Enterprise projects let you manage cloud resources and users by project.

    • Agency: Select an agency with the SWR Admin permission. If no agency is available, create one by referring to Creating an Agency.

    • Container Image: Enter the image uploaded to SWR. The format is: {SWR_endpoint}/{organization_name}/{image_name}:{tag}.

      Example: swr.eu-de.otc.t-systems.com/my_organization/custom_container_http_example:latest.

  4. Advanced Settings: Collect Logs is disabled by default. If it is enabled, function execution logs will be reported to Log Tank Service (LTS). You will be billed for log management on a pay-per-use basis.

    Parameter

    Description

    Log Configuration

    You can select Auto or Custom.

    • Auto: Use the default log group and log stream. Log groups prefixed with “functiongraph.log.group” are filtered out.

    • Custom: Select a custom log group and log stream. Log streams that are in the same enterprise project as your function.

    Log Tag

    You can use these tags to filter function logs in LTS.
    You can add 10 more tags.
    Tag key/value: Enter a maximum of 64 characters.
    Only digits, letters, underscores (_), and hyphens (-) are allowed.
  5. After the configuration is complete, click Create Function.

See also: Step 4: Creating Function in the user manual.

Step 5: Test using console

To test using console you have to create a test event in the same structure as an API Gateway would send the request to the function.

Create the test event using FunctionGraph console, in Code section, select Configure Test Event and click Test.

In the displayed dialog box, create a test event:

  • Select API Gateway (Dedicated) (only available option),

  • set Event Name to helloworld,

  • modify the test event as follows:

apig_post_index.json
{
  "body": "{\"name\": \"Hello World\"}",
  "requestContext": {
      "apiId": "bc1dcffd-aa35-474d-897c-d53425a4c08e",
      "requestId": "11cdcdcf33949dc6d722640a13091c77",
      "stage": "RELEASE"
  },
  "queryStringParameters": {
      "responseType": "application/json"
  },
  "httpMethod": "POST",
  "pathParameters": {},
  "headers": {
      "accept-language": "q=0.5,en-US;q=0.3,en;q=0.2",
      "accept-encoding": "gzip, deflate, br",
      "x-forwarded-port": "443",
      "x-forwarded-for": "103.218.216.98",
      "accept": "application/json",
      "upgrade-insecure-requests": "1",
      "host": "host",
      "x-forwarded-proto": "https",
      "pragma": "no-cache",
      "cache-control": "no-cache",
      "x-real-ip": "103.218.216.98",
      "user-agent": "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:57.0) Gecko/20100101 Firefox/57.0"
  },
  "path": "/index",
  "isBase64Encoded": false
}

Note

Make sure to modify the payload send as body to your FunctionGraph accordingly. The Test function in FunctionGraph console uses these settings to call your function:

  • path: set to the path defined in your function (e.g. /index),

  • httpMethod: set to the method defined in your function (e.g. POST),

  • accept header: set to application/json,

  • responseType parameter in queryStringParameters to application/json to get the expected response.

  • and click Create.

See also: Step 5: Test the Function in the user manual.

Step 6: View the Execution Result

Click Test and view the Execution Result on the right.

You should see output similar to the following:

Trigger settings

The body field contains the base64 encoded response body:

SGVsbG8gZnJvbSBGdW5jdGlvbkdyYXBoIQ==

To decode the response body, use the following command:

echo "SGVsbG8gZnJvbSBGdW5jdGlvbkdyYXBoIQ==" | base64 --decode

The decoded response body is:

Hello from FunctionGraph!
  • The Log Output section displays the logs generated during function execution.

    Note

    This page displays a maximum of 2K logs.

  • The Summary section displays key information from the Log.

Step 7: Creating an APIG (Dedicated) trigger

Create an APIG (dedicated) trigger by referring to Step 7: Configure the trigger information in the user guide.

Set following parameters:

Parameter

Description

Trigger Type

API Gateway (Dedicated)

API instance

Create a new API instance or select an existing API instance.

API Name

e.g. MyGoHttpAPIContainer

API Group

Create a new API group or select an existing API group.

Environment

RELEASE

Security Authentication

None for debugging

Protocol

HTTPS

Method

ANY

The API_GATEWAY_TRIGGER_URL is displayed after the trigger is created.

See the following figure:

Trigger settings

Step 8: Test using APIG trigger

Test using browser

Copy the URL of the APIG trigger and add /index to the address box of the browser.

${API_GATEWAY_TRIGGER_URL}/index

This will test the HTTP function using an HTTP GET request.

Result

The following information is displayed:

You can also add a query parameter name to the URL, e.g.:

${API_GATEWAY_TRIGGER_URL}/index?name=John
Result with query parameter

The following information is displayed:

Test using curl

Use curl to send a GET request to the function: .. code-block:: shell

curl -X GET ${API_GATEWAY_TRIGGER_URL}/index

The expected response is:

Hello, World! This is a GET request.

Use curl to send a POST request to the function:

curl -X POST \
  -H 'Content-Type: application/json' \
  -d '{"message": "Hello from API Gateway POST request"}' \
  ${API_GATEWAY_TRIGGER_URL}/index

The expected response is:

Hello from FunctionGraph!

Deploy the HTTP Function using Terraform

For details on how to deploy using Terraform, see Deploying an HTTP Function container Image using Terraform.