← Back to list

Building a Production‑Ready Serverless Go Backend on Oracle Cloud Infrastructure [OCI]

How we shipped a real, production‑ready “serverless Go on OCI” stack using the Fn Project, Oracle Cloud Functions, API Gateway, Object…

YISUSVII · 2025-11-17 01:18 · 3 claps · 9.2 min read paywalled
#oracle-cloud #serverless #golang #terraform
Open on Medium ↗
Wiki topics: 🌐 · Web Development ☁️ · DevOps & Cloud

Building a Production‑Ready Serverless Go Backend on Oracle Cloud Infrastructure [OCI]

How we shipped a real, production‑ready “serverless Go on OCI” stack using the Fn Project, Oracle Cloud Functions, API Gateway, Object Storage, NoSQL, IAM, and Logging — complete with Terraform IaC and a push‑button deployment script.

TL;DR This guide walks through a working reference implementation — OCI Serverless Golang Backend — that you can deploy end‑to‑end on Oracle Cloud Infrastructure (OCI). You’ll provision the cloud resources with Terraform, build and deploy Go functions with Fn Project (Oracle Cloud Functions), secure them using resource principals, persist files in Object Storage, track metadata in NoSQL Database, and wire everything up behind API Gateway with centralized Logging. Code and IaC are here: **github.com/YISUSVII/oci-serverless-go**.

Why serverless Go on OCI?

Serverless lets you focus on code while OCI handles scaling, fault tolerance, and runtime security. When you combine Oracle Cloud Functions (powered by the Fn Project) with API Gateway, Object Storage, NoSQL, and Logging, you get a clean architecture that is:

  • Elastic (auto‑scales to zero)
  • Cost‑efficient (pay for invocation time)
  • Secure by default (resource principal authentication — no secrets on disk)
  • Observable (centralized logs in OCI Logging)
  • Composable (mix Functions, API Gateway, Object Storage, NoSQL, IAM)

This article is both beginner‑friendly and technical. You can follow it to stand up your own production‑ready backend and adapt it to your needs.

What we’ll build

Project: OCI Serverless Golang Backend with three functions, each independently versioned:

  1. healthcheck (v0.0.16; Go 1.23) — simple liveness/diagnostics endpoint Build image: fnproject/go:1.18-dev · Run image: fnproject/go:1.18 · Mem: 256 MB · Timeout: 30s
  2. upload_document (v0.0.6; Go 1.23) — accepts PDF uploads, writes to Object Storage, records metadata in NoSQL Build image: fnproject/go:1.18-dev · Run image: fnproject/go:1.18 · Mem: 256 MB · Timeout: 30s
  3. data_ops (v0.0.5; Go 1.18) — CRUD over the metadata stored in NoSQL Build image: fnproject/go:1.18-dev · Run image: fnproject/go:1.18 · Mem: 256 MB · Timeout: 30s

Key Go packages (and versions used in this project):

  • github.com/fnproject/fdk-go v0.0.65
  • github.com/google/uuid v1.3.0
  • github.com/oracle/oci-go-sdk/v65 v65.35.0 (Object Storage, NoSQL, IAM auth)
  • Indirect deps likely to appear: github.com/gofrs/flock v0.8.1, github.com/sony/gobreaker v0.5.0, github.com/stretchr/testify v1.11.1, golang.org/x/sys v0.6.0

Architecture overview (high level)

Components

  • API Gateway — Front‑door routing, HTTPS termination, request validation.
  • Functions (Fn Project / Oracle Cloud Functions) — Stateless Go functions.
  • Object Storage — Persistent, durable storage for uploaded PDFs.
  • NoSQL Database — Low‑latency metadata store (automatic indexing).
  • IAMResource principal authentication (no API keys in code).
  • Logging — Centralized logs for observability and troubleshooting.

Prerequisites & tools

  • OCI CLI v3.17+ (configured with API keys, tenancy, region)
  • Terraform v1.0+ (IaC for network, functions, API Gateway, NoSQL, Object Storage, Logging, IAM)
  • Go 1.18+ (project uses both Go 1.18 and 1.23)
  • Fn CLI (build & deploy functions to Oracle Cloud Functions)
  • Docker (containerized builds)
  • jq (JSON parsing in deployment scripts)

IMPORTANT : Cross‑platform builds are handled with DOCKER_DEFAULT_PLATFORM=linux/amd64 so your Apple Silicon laptop won’t ship ARM images to an AMD64 runtime.

Cloning the repo

git clone https://github.com/YISUSVII/oci-serverless-go
cd oci-serverless-go

The repository contains:

  • terraform/ — IaC for API Gateway, Functions, IAM, NoSQL, Object Storage, Logging
  • functions/healthcheck, functions/upload_document, functions/data_ops
  • deploy_functions.sh — builds & deploys all functions via Fn CLI + Docker + jq

Step 1 — Provision infrastructure with Terraform

  1. Initialize modules and providers
cd terraform
terraform init

2. Review the execution plan

terraform plan -out tf.plan

3. Apply changes

terraform apply tf.plan

Terraform stands up:

  • An Fn application with three Oracle Cloud Functions
  • API Gateway with routes for /health, /documents (POST), /metadata (GET/PUT/DELETE)
  • Object Storage bucket for PDFs
  • NoSQL table for metadata (automatic indexing enabled)
  • OCI Logging sink for function logs
  • IAM policies and a dynamic group for resource principal auth

Example (simplified) policy ideas — adjust to “least privilege” in your compartment:

Allow dynamic-group <functions-dg> to manage objects in compartment <your-compartment>
Allow dynamic-group <functions-dg> to use nosql-family in compartment <your-compartment>
Allow service apigateway to use functions-family in compartment <your-compartment>

(Optional) API Gateway mapping with Terraform (illustrative)

resource "oci_apigateway_deployment" "api" {
  # ...
  path_prefix = "/v1"
path_routes {
    path    = "/health"
    methods = ["GET"]
    backend {
      type        = "ORACLE_FUNCTIONS_BACKEND"
      function_id = oci_functions_function.healthcheck.id
    }
  }
  path_routes {
    path    = "/documents"
    methods = ["POST"]
    backend {
      type        = "ORACLE_FUNCTIONS_BACKEND"
      function_id = oci_functions_function.upload_document.id
    }
  }
  path_routes {
    path    = "/metadata"
    methods = ["GET", "PUT", "DELETE"]
    backend {
      type        = "ORACLE_FUNCTIONS_BACKEND"
      function_id = oci_functions_function.data_ops.id
    }
  }
}

Step 2 — Function configuration (Fn Project)

Each function is configured with Fn’s func.yaml. This project pins the same base images and resources across functions:

# functions/healthcheck/func.yaml
name: healthcheck
version: 0.0.16
runtime: go
build_image: fnproject/go:1.18-dev
run_image: fnproject/go:1.18
memory: 256
timeout: 30
# functions/upload_document/func.yaml
name: upload_document
version: 0.0.6
runtime: go
build_image: fnproject/go:1.18-dev
run_image: fnproject/go:1.18
memory: 256
timeout: 30
# functions/data_ops/func.yaml
name: data_ops
version: 0.0.5
runtime: go
build_image: fnproject/go:1.18-dev
run_image: fnproject/go:1.18
memory: 256
timeout: 30

Although healthcheck and upload_document target Go 1.23 and data_ops uses Go 1.18, the build/run images shown above are standardized on fnproject/go:1.18-dev/1.18. In practice, keep your code compatible with the image you build with (or update the images to a newer Go tag if you rely on 1.23‑only features).

Step 3 — Code examples (Go + FDK)

All functions use **github.com/fnproject/fdk-go v0.0.65**. Below are trimmed versions to highlight the core ideas. (Omitted error handling and some types for clarity.)

1) healthcheck (Go 1.23)

**go.mod (excerpt)**

module example.com/healthcheck
go 1.23
require github.com/fnproject/fdk-go v0.0.65

**func.go (simplified)**

package main

import (
  "context"
  "encoding/json"
  "io"
  "net/http"
  "os"
  "runtime"
  "time"
  fdk "github.com/fnproject/fdk-go"
)
type hc struct {
  Status     string `json:"status"`
  Time       string `json:"time"`
  GoVersion  string `json:"goVersion"`
  Region     string `json:"region,omitempty"`
  Compartment string `json:"compartment,omitempty"`
}
func main() {
  fdk.Handle(fdk.HandlerFunc(handle))
}
func handle(ctx context.Context, in io.Reader, out io.Writer) {
  resp := hc{
    Status:    "ok",
    Time:      time.Now().UTC().Format(time.RFC3339),
    GoVersion: runtime.Version(),
    Region:    os.Getenv("OCI_REGION"),
  }
  w := json.NewEncoder(out)
  // Fn expects an HTTP-like response over the stream format.
  _ = w.Encode(resp) // handle errors in real code
  // Optionally write headers with fdk.SetResponseHeader(out, "Content-Type", "application/json")
  _ = http.StatusOK
}

2) upload_document (Go 1.23)

**go.mod (excerpt)**

module example.com/upload_document

go 1.23
require (
  github.com/fnproject/fdk-go v0.0.65
  github.com/google/uuid v1.3.0
  github.com/oracle/oci-go-sdk/v65 v65.35.0
)

**func.go (core flow; resource principals; Object Storage + NoSQL)**

package main

import (
  "context"
  "encoding/base64"
  "encoding/json"
  "io"
  "time"
  fdk "github.com/fnproject/fdk-go"
  "github.com/google/uuid"
  "github.com/oracle/oci-go-sdk/v65/common/auth"
  "github.com/oracle/oci-go-sdk/v65/objectstorage"
  "github.com/oracle/oci-go-sdk/v65/nosql"
)
type UploadRequest struct {
  FileName      string            `json:"fileName"`
  ContentBase64 string            `json:"contentBase64"` // for demo; in production prefer multipart/form-data
  Metadata      map[string]string `json:"metadata"`
}
type UploadResponse struct {
  ID        string `json:"id"`
  ObjectURI string `json:"objectUri"`
}
func main() {
  fdk.Handle(fdk.HandlerFunc(handler))
}
func handler(ctx context.Context, in io.Reader, out io.Writer) {
  var req UploadRequest
  _ = json.NewDecoder(in).Decode(&req)
  id := uuid.New().String()
  b, _ := base64.StdEncoding.DecodeString(req.ContentBase64)
  // Resource principal provider: no keys needed
  provider, _ := auth.ResourcePrincipalConfigurationProvider()
  // 1) Put the PDF into Object Storage
  osClient, _ := objectstorage.NewObjectStorageClientWithConfigurationProvider(provider)
  bucket := "pdf-uploads" // set via env/config
  namespace := "mytenancyns" // fetch via osClient.GetNamespace
  putReq := objectstorage.PutObjectRequest{
    NamespaceName: &namespace,
    BucketName:    &bucket,
    ObjectName:    &req.FileName,
    PutObjectBody: io.NopCloser(bytes.NewReader(b)),
    ContentType:   common.String("application/pdf"),
  }
  _, _ = osClient.PutObject(ctx, putReq)
  // 2) Insert metadata into NoSQL
  noClient, _ := nosql.NewNosqlClientWithConfigurationProvider(provider)
  table := "documents"
  item := map[string]any{
    "id":        id,
    "fileName":  req.FileName,
    "bucket":    bucket,
    "createdAt": time.Now().UTC().Format(time.RFC3339),
    "metadata":  req.Metadata,
  }
  _ = putRow(ctx, noClient, table, item) // implement with nosql.PutRowRequest
  _ = json.NewEncoder(out).Encode(UploadResponse{
    ID:        id,
    ObjectURI: "oci:///" + bucket + "/" + req.FileName,
  })
}

Notes: • The OCI Go SDK v65.35.0 exposes Resource Principal auth via common/auth. • For NoSQL, use the nosql client’s PutRow, GetRow, UpdateRow, DeleteRow requests. In production, validate file type, limit sizes, and handle transactional upserts carefully.

3) data_ops (Go 1.18)

This function exposes CRUD for metadata stored in NoSQL.

**go.mod (excerpt)**

module example.com/data_ops

go 1.18
require (
  github.com/fnproject/fdk-go v0.0.65
  github.com/google/uuid v1.3.0
  github.com/oracle/oci-go-sdk/v65 v65.35.0
)

**func.go (CRUD sketch)**

package main

import (
  "context"
  "encoding/json"
  "io"
  "net/http"
  fdk "github.com/fnproject/fdk-go"
  "github.com/oracle/oci-go-sdk/v65/common/auth"
  "github.com/oracle/oci-go-sdk/v65/nosql"
)
func main() { fdk.Handle(fdk.HandlerFunc(route)) }
func route(ctx context.Context, in io.Reader, out io.Writer) {
  // Parse a simple envelope: { "op": "get|put|delete|list", "id": "...", "item": {...} }
  var req map[string]any
  _ = json.NewDecoder(in).Decode(&req)
  provider, _ := auth.ResourcePrincipalConfigurationProvider()
  noClient, _ := nosql.NewNosqlClientWithConfigurationProvider(provider)
  table := "documents"
  // Switch by op and call helpers (getRow, putRow, deleteRow, queryRows)
  // Each helper uses nosql.* requests with table name/id and key attributes.
  // Finally, write JSON response with status and rows/items.
  _ = http.StatusOK
}

Step 4 — Deploy functions with the script

The repository includes a helper script that builds and deploys all functions with Fn CLI and Docker. It also shows jq usage to extract IDs if you need to wire them back into Terraform or logs.

**deploy_functions.sh (excerpt)**

#!/usr/bin/env bash
set -euo pipefail

export DOCKER_DEFAULT_PLATFORM=linux/amd64
export FN_APP_NAME="oci-serverless-go"
export FN_REGISTRY="iad.ocir.io/<tenancy-namespace>/<repo>" # set yours
echo "Building & deploying functions to app: ${FN_APP_NAME}"
for dir in functions/healthcheck functions/upload_document functions/data_ops; do
  pushd "$dir" >/dev/null
  echo "Tidying modules in $(pwd)"
  go mod tidy
  echo "Deploying $(basename "$dir")"
  fn -v deploy --app "${FN_APP_NAME}" --registry "${FN_REGISTRY}"
  # Optional: capture function OCID (output may vary)
  FN_INFO=$(fn inspect function "${FN_APP_NAME}" "$(basename "$dir")" --format json || true)
  FN_OCID=$(echo "${FN_INFO}" | jq -r '.id // empty')
  [[ -n "${FN_OCID}" ]] && echo "$(basename "$dir") OCID: ${FN_OCID}"
  popd >/dev/null
done
echo "Done."

Why the DOCKER_DEFAULT_PLATFORM=linux/amd64 export? It ensures images built on Apple Silicon or other ARM hosts run reliably in the AMD64 environment used by the Fn runtime on OCI.

Step 5 — Test the endpoints

Once Terraform has created the API Gateway deployment and you’ve deployed the functions, you’ll have a public base URL (e.g., https://<gateway-id>.apigateway.<region>.oci.customer-oci.com/v1).

  • Healthcheck
curl -s https://<gateway>/v1/health | jq
  • Upload a document (simplified base64 JSON flow)
PDF_B64=$(base64 -w 0 sample.pdf) curl -s -X POST https://<gateway>/v1/documents \   -H 'Content-Type: application/json' \   -d "{\"fileName\":\"sample.pdf\",\"contentBase64\":\"${PDF_B64}\",\"metadata\":{\"source\":\"web\"}}" | jq
  • Metadata ops (examples)
# GET curl -s -X GET https://<gateway>/v1/metadata?id=<doc-id> | jq  
# PUT (update metadata) curl -s -X PUT https://<gateway>/v1/metadata 
\   -H 'Content-Type: application/json' \ 
  -d '{"id":"<doc-id>","item":{"tags":["receipt","2025"]}}' | jq 

 # DELETE curl -s -X DELETE "https://<gateway>/v1/metadata?id=<doc-id>"

Troubleshooting (save yourself hours)

1) “exec format error” or image architecture mismatch

  • Symptom: Function fails after deploy; logs mention bad exec format.
  • Fix: Build for AMD64 explicitly:
export DOCKER_DEFAULT_PLATFORM=linux/amd64 fn deploy ...

2) Authentication errors (“not authorized”, “signature invalid”)

  • Symptom: Calls to Object Storage / NoSQL fail from inside the function.
  • Fixes:
  • Ensure you are using Resource Principal auth:
provider, err := auth.ResourcePrincipalConfigurationProvider()
  • Verify your dynamic group matches your function resources.
  • Add policies with least privilege for objectstorage and nosql in the target compartment.

3) API Gateway 404 / route not found

  • Symptom: API returns 404 even though function exists.
  • Fixes:
  • Check path prefix + route path in the deployment (e.g., /v1 + /health).
  • Make sure methods include your verb (GET, POST, etc.).
  • Confirm the gateway deployment is active and the function OCIDs are correct.

4) Timeouts on large uploads

  • Symptom: upload_document times out at ~30s.
  • Fixes:
  • Start by increasing timeout in func.yaml (e.g., to 60 or 120).
  • Consider a pre‑signed URL pattern to upload directly to Object Storage, then call a light‑weight metadata function.

5) Go version & base image drift

  • Symptom: You’re using Go 1.23 features, but the fnproject/go base image is 1.18.
  • Fixes:
  • Either keep your code within 1.18‑compatible features or update the build/run images to a newer Go tag across functions to match your toolchain.

6) “Bucket not found” or 404 on PutObject

  • Fixes: Confirm namespace and bucket name; use the correct region; ensure policies grant your dynamic group permissions to manage objects.

7) Observability tips

  • Logs go to OCI Logging automatically (stdout/stderr).
  • Add request IDs to logs for easier correlation.
  • Store function OCIDs and API Gateway IDs in outputs (Terraform) and annotate dashboards.

Best practices & lessons learned

  • Use resource principals — it’s the cleanest way to do server‑to‑service auth on OCI. No secrets, no rotated keys in CI.
  • Split responsibilities — we separated upload_document (file writes + metadata) from data_ops (metadata CRUD) and a healthcheck for basic diagnostics. That keeps handlers small, testable, and failure‑isolated.
  • IaC everything — Terraform tracks OCIDs and relationships (e.g., mapping API Gateway routes to function OCIDs). Version your infra like you version code.
  • Keep functions stateless — focus on idempotence and pass context/IDs around; rely on NoSQL for state.
  • Optimize cold starts — bundle only what you need; keep image layers small.
  • Standardize memory/timeouts — we used 256 MB and 30s, which fits many I/O‑bound tasks. Adjust if you do heavy parsing or large uploads.
  • Version independently — this project tags functions separately (healthcheck v0.0.16, upload_document v0.0.6, data_ops v0.0.5). It makes rollbacks and staged releases safer.
  • Validate inputs — especially for uploads. Enforce Content-Type: application/pdf, size limits, checksum, and virus scanning as appropriate.

Feature recap (what you get out of the box)

  • API Gateway with clean routes and HTTPS
  • Oracle Cloud Functions (Fn Project) running Go handlers
  • Object Storage for durable PDF storage
  • NoSQL Database for metadata with automatic indexing
  • IAM Resource Principals for keyless intra‑cloud auth
  • Centralized Logging for observability and debugging
  • Terraform v1.0+ IaC to reproduce environments
  • Push‑button deploy via deploy_functions.sh (Fn CLI + Docker + jq)
  • Cross‑platform builds with DOCKER_DEFAULT_PLATFORM=linux/amd64

Where to go next

  • Add request validation and rate limiting at API Gateway.
  • Move to multipart/form-data uploads and/or pre‑signed URLs for large files.
  • Introduce gobreaker (circuit breaker — already listed as an indirect dep) around NoSQL/Object Storage calls if you expect transient errors.
  • Implement structured logging and correlation IDs.
  • Add integration tests (stretchr/testify is already in deps) and CI/CD.
  • Consider async processing via OCI Streaming/Queue for heavy post‑upload tasks (OCR, virus scanning, indexing).

Conclusion

If you’re looking for a serverless Go on OCI blueprint that scales from “hello world” to production, this stack hits the sweet spot: small, composable functions; security with resource principals; durable storage; a low‑latency NoSQL control plane; and a clean, automated Terraform deployment. It’s simple enough for a first project and robust enough to run in production.

👉 Explore the code and IaC here: **github.com/YISUSVII/oci-serverless-go** ⭐ If it helps you, star the repo and share what you build with it.


메타데이터
post_id
7c41bf0ce0ff
slug
building-a-production-ready-serverless-go-backend-on-oracle-cloud-infrastructure-oci-7c41bf0ce0ff
url
https://medium.com/@yisusvii/building-a-production-ready-serverless-go-backend-on-oracle-cloud-infrastructure-oci-7c41bf0ce0ff
canonical_url
https://medium.com/@yisusvii/building-a-production-ready-serverless-go-backend-on-oracle-cloud-infrastructure-oci-7c41bf0ce0ff
author_url
https://medium.com/@yisusvii
status
ok
fetched_at
2026-07-15 07:46:41