PR

Go言語(Golang)とFloci(フローシー)を使ったAWS Lambda開発入門

3. 応用

こんにちは。Tomoyuki(@tomoyuki65)です。

イベント駆動の非同期処理や軽量なAPIを作りたい場合、AWS Lambdaを使うケースが多かったりするため、Go言語(Golang)で Lambdaを開発方法について調べました。

そんなLambdaをローカルで開発したい場合、以前はLocalStackを利用する方法が一般的でしたが、現在は一部機能が有料化されており、気軽に試し辛くなっています。

そこで代替手段を探していたところ、ローカルAWSエミュレーターのOSS「Floci(フローシー)」を見つけました。

この記事では、Go言語(Golang)とFloci(フローシー)を使ったAWS Lambdaの開発方法について解説します。

 

Go言語(Golang)とFloci(フローシー)を使ったAWS Lambda開発入門

AWS Lambdaとは?

AWS Lambdaは、AWSのサーバーレスコンピューティングサービス(インフラの運用やスケーリングはAWSが自動で行う)で、イベント駆動でプログラムを実行でき、実行時間に応じた従量課金のサービスになっています。

 

OSS「Floci(フローシー)」とは?

Floci(フローシー)は、AWSの各種サービスをローカル環境でエミュレートできるOSS(オープンソースソフトウェア)です。

AWSアカウントを利用することなく、AWSのS3やLambdaをはじめとする様々なAWSサービスをローカル環境で動作させ、開発やテストが行えます。

そんなFlociはJava(Quarkus)で開発されており、GraalVMによるネイティブコンパイルを採用しているため、起動時間が短く、メモリ使用量も少ないなど、高いパフォーマンスを実現している点も特徴です。

これまでAWSのローカル開発環境では、AWSサービスをローカルでエミュレートできるLocalStackが広く利用されてきましたが、LocalStackのライセンスや提供方針の変更を受け、OSSの代替候補としてFlociが注目を集めています。

 

事前準備

この記事ではDockerやAWS CLIを利用するため、以降の内容を参考にしたい場合は、事前にこれらを利用できるように準備して下さい。

尚、パッケージ管理にHomebrew」を利用している場合は、以下のコマンドを実行してAWS CLIををインストールできます。

$ brew install awscli

 

インストール後、以下のコマンドを実行し、パスが通っていることを確認して下さい。

$ aws --version

 

次に以下のコマンドを実行し、AWS CLIのローカル用のコンフィグ設定を行います。

$ aws configure --profile local

 

コマンド実行後、AWS Access Key IDにtest、AWS Secret Access Keyにtest、Default region nameにap-northeast-1、Default output formatにjsonのようにダミー値を設定して下さい。

AWS Access Key ID [None]: test
AWS Secret Access Key [None]: test
Default region name [None]: ap-northeast-1
Default output format [None]: json

 

次にテキストエディタなどでターミナルの設定ファイル「.zshrc」を開き、ローカル用のAWS CLIコマンドのエイリアスを設定しておきます。

・・・

# ローカル用のAWS CLIコマンドのエイリアス
alias aws-local='aws --profile local --endpoint-url=http://localhost:4566'

・・・

 

ファイル修正後、以下のコマンドを実行し、変更を反映します。

$ source ~/.zshrc

 

ローカル開発環境の構築

次にローカル開発環境を構築するため、以下のコマンドを実行して各種ファイルを作成します。

$ mkdir go-lambda && cd go-lambda
$ mkdir -p deploy/docker/local/go && touch deploy/docker/local/go/Dockerfile
$ mkdir src
$ touch .env compose.yaml .gitignore

 

次に作成したファイルをそれぞれ以下のように記述します。

・「deploy/docker/local/go/Dockerfile」

FROM golang:1.26.5-alpine3.23

# インストール可能なパッケージ一覧の更新
RUN apk update && \
    apk upgrade && \
    # パッケージのインストール(--no-cacheでキャッシュ削除)
    apk add --no-cache \
            curl \
            zip

WORKDIR /go/src

COPY ./src .

# go.modがあれば依存関係をインストール
RUN for dir in ./*; do \
      if [ -d "$dir" ] && [ -f "$dir/go.mod" ]; then \
        echo "Downloading dependencies in $dir"; \
        (cd "$dir" && go mod download); \
      fi; \
    done

# 開発用のライブラリをインストール
RUN curl -sSfL https://golangci-lint.run/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.12.2
RUN go install go.uber.org/mock/mockgen@v0.6.0

EXPOSE 8080

 

・「.env」

ENV=local
PORT=8080

 

・「compose.yaml」

services:
  go-lambda:
    container_name: go-lambda
    build:
      context: .
      dockerfile: ./deploy/docker/local/go/Dockerfile
    command: >
      sh -c '
        if [ -n "$$LAMBDA_DIR" ]; then
          exec go -C "./$$LAMBDA_DIR" run main.go
        else
          echo "LAMBDA_DIR is not set."
          tail -f /dev/null
        fi
      '
    ports:
      - "${PORT}:${PORT}"
    volumes:
      - ./src:/go/src
    env_file:
      - .env
    environment:
      LAMBDA_DIR: ${LAMBDA_DIR:-}
  floci:
    container_name: floci
    image: floci/floci:latest
    ports:
      - "4566:4566"
    volumes:
      - floci_data:/app/data
      - /var/run/docker.sock:/var/run/docker.sock
    environment:
      TZ: Asia/Tokyo
      FLOCI_STORAGE_MODE: hybrid
volumes:
  floci_data:

 

・「.gitignore」

.DS_Store
.env

 

次に以下のコマンドを実行し、Dockerコンテナのビルドおよび起動を行います。

$ docker compose build --no-cache 
$ docker compose up -d

 

次に以下のコマンドを実行し、ステータスを確認します。

$ docker compose ps

 

実行後、以下のように各Dockerコンテナが起動していればOKです。

 

スポンサーリンク

イベント駆動用のLambda開発方法

次にイベント駆動用のLambdaを作ってみるため、以下のコマンドを実行して各種ファイルを作成します。

$ mkdir -p src/event-lambda
$ touch src/event-lambda/main.go src/event-lambda/main_test.go src/event-lambda/.golangci.yaml
$ touch Makefile

 

次に作成したファイルをそれぞれ以下のように記述します。

・「src/event-lambda/main.go」

package main

import (
    "context"
    "encoding/json"
    "fmt"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
)

// イベントの構造体を定義
type Event struct {
    EventType string `json:"eventType"`
    Message string `json:"message"`
}

func handler(ctx context.Context, event events.SQSEvent) error {
    // イベントのレコードをループ処理
    for _, record := range event.Records {
        // レコードのBodyをEvent構造体にデコード
        var e Event
        if err := json.Unmarshal([]byte(record.Body), &e); err != nil {
            return err
        }

        // イベントのメッセージをログ出力
        fmt.Println(e.Message)
    }
    return nil
}

func main() {
    lambda.Start(handler)
}

 

・「src/event-lambda/main_test.go」

//go:build unit

package main

import (
    "context"
    "testing"

    "github.com/aws/aws-lambda-go/events"
    "github.com/stretchr/testify/assert"
)

func TestHandler(t *testing.T) {
    t.Parallel()

    tests := []struct {
        name string
        event events.SQSEvent
        wantErr bool
    }{
        {
            name: "正常なメッセージを処理できる",
            event: events.SQSEvent{
                Records: []events.SQSMessage{
                    {
                        Body: `{
                            "eventType": "LogOutput",
                            "message": "Hello World !!"
                        }`,
                    },
                },
            },
            wantErr: false,
        },
        {
            name: "複数のメッセージを処理できる",
            event: events.SQSEvent{
                Records: []events.SQSMessage{
                    {
                        Body: `{
                            "eventType": "LogOutput",
                            "message": "first message"
                        }`,
                    },
                    {
                        Body: `{
                            "eventType": "LogOutput",
                            "message": "second message"
                        }`,
                    },
                },
            },
            wantErr: false,
        },
        {
            name: "レコードが空でもエラーにならない",
            event: events.SQSEvent{
                Records: []events.SQSMessage{},
            },
            wantErr: false,
        },
        {
            name: "不正なJSONの場合はエラーを返す",
            event: events.SQSEvent{
                Records: []events.SQSMessage{
                    {
                        Body: `{"eventType":`,
                   },
                },
            },
            wantErr: true,
        },
    }

    for _, tt := range tests {
        tt := tt

        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()

            err := handler(context.Background(), tt.event)

            if tt.wantErr {
                assert.Error(t, err)
            } else {
                assert.NoError(t, err)
            }
        })
    }
}

 

・「src/event-lambda/.golangci.yaml」

version: "2"

run:
  # タイムアウト設定
  timeout: 5m

formatters:
  # 実行するformatterを指定
  enable:
    - gofumpt
    - gci

  settings:
    gofumpt:
      # モジュールパス設定
      module-path: event-lambda

    gci:
      # 並び順設定
      custom-order: true
      sections:
        - standard
        - default
        - prefix(event-lambda)

linters:
  # デフォルト設定を全てOFF
  default: none

  # 実行するlintを指定
  enable:
    - govet
    - staticcheck
    - gosec

 

・「Makefile」

.PHONY: \
  go-mod-tidy-event-lambda fmt-event-lambda lint-event-lambda test-event-lambda \
    build-event-lambda deploy-local-event-lambda update-local-event-lambda

# ローカル環境用のawsコマンドの変数定義
AWS_LOCAL := aws --profile local --endpoint-url=http://localhost:4566

# =====================
# event-lambda用の定義
# =====================
# go.mod更新
go-mod-tidy-event-lambda:
  docker compose run --rm go-lambda go -C ./event-lambda mod tidy

# フォーマット修正
fmt-event-lambda:
  docker compose run --rm go-lambda \
    sh -c 'cd event-lambda && golangci-lint fmt -v ./...'

# lint実行
lint-event-lambda:
  docker compose run --rm go-lambda \
    sh -c 'cd event-lambda && golangci-lint run -v ./...'

# unitテスト実行
test-event-lambda:
  docker compose run --rm go-lambda \
    env ENV=testing \
    go -C ./event-lambda test -v -cover -tags=unit \
      $$(docker compose run --rm go-lambda env ENV=testing go -C ./event-lambda list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' -tags=unit ./...)

# ビルド
build-event-lambda:
  docker compose run --rm \
    -e GOOS=linux \
    -e GOARCH=amd64 \
    -e CGO_ENABLED=0 \
    go-lambda \
    go -C ./event-lambda build -tags lambda.norpc -o bootstrap main.go

  docker compose run --rm go-lambda \
    zip -j event-lambda/function.zip event-lambda/bootstrap

# ローカル環境にデプロイ
deploy-local-event-lambda:
  $(AWS_LOCAL) iam create-role \
    --role-name event-lambda-role \
    --assume-role-policy-document \
      '{}'

  $(AWS_LOCAL) iam create-policy \
    --policy-name event-lambda-policy \
    --policy-document \
      '{"Version":"2012-10-17","Statement":[]}'

  $(AWS_LOCAL) iam attach-role-policy \
    --role-name event-lambda-role \
    --policy-arn arn:aws:iam::000000000000:policy/event-lambda-policy

  $(AWS_LOCAL) lambda create-function \
    --function-name event \
    --runtime provided.al2023 \
    --handler bootstrap \
    --role arn:aws:iam::000000000000:role/event-lambda-role \
    --zip-file fileb://lambda/event-lambda/function.zip

# ローカル環境のlambdaを更新
update-local-event-lambda:
  $(AWS_LOCAL) lambda update-function-code \
    --function-name event \
    --zip-file fileb://lambda/event-lambda/function.zip

※利用する各種コマンドを簡単に実行するためにMakefileでまとめてます。

 

次に以下のコマンドを実行し、Goファイルの初期化をします。

$ docker compose run --rm go-lambda go -C ./event-lambda mod init event-lambda
$ docker compose run --rm go-lambda go -C ./event-lambda mod tidy

 

次に以下のコマンドを実行し、フォーマット修正や静的コード解析を行い、エラーや警告がないことを確認します。

$ make fmt-event-lambda
$ make lint-event-lambda

 

次に以下のコマンドを実行し、テストコードを試します。

$ make test-event-lambda

 

テスト実行後、以下のように全てのテストがパスすればOKです。

※カバレッジ(対象範囲に対して「どれだけ網羅しているか」を示す割合)は80%以上推薦です。

 

ビルドとデプロイ

次にGo言語で作成した上記のコードをビルドし、flociコンテナへデプロイします。

まずは以下のコマンドを実行し、Go言語のコードをビルドします。

$ make build-event-lambda

 

ビルド後、ファイル「src/event-lambda/bootstrap」、「src/event-lambda/function.zip」が作成されればOKです。

 

次に以下のコマンドを実行し、flociコンテナへデプロイします。

$ make deploy-local-event-lambda

 

実行後、以下のようにエラーが出ずに完了すればOKです。

※ターミナルが占有されますが、キーボードの「q」を押すと抜けれます。

 

Lambdaを実行して試す

次に以下のコマンドを実行し、flociコンテナへデプロイしたlambdaを実行して試します。

aws-local lambda invoke \
--function-name event \
--cli-binary-format raw-in-base64-out \
--payload '{"Records":[{"body":"{\"eventType\":\"LogOutput\",\"message\":\"Hello World !!\"}"}]}' \
/dev/null

※メッセージとして「Hello World !!」を送信します。

 

実行後、以下のように正常終了すればOKです。

 

次に以下のコマンドを実行し、flociコンテナのログ出力を確認します。

$ docker compose logs floci

 

実行後、以下のように想定通りのログ出力がされていればOKです。

※送信した「Hello World !!」がログ出力される。

 

イベント駆動でLambdaを利用する場合の補足

Lambdaをイベント駆動で利用する際は、AWSのSQS(メッセージキュー)やEventBridge(イベントバス)などのサービスと組み合わせるのが一般的です。

ただし、本記事ではLambdaの基本的な使い方に焦点を当てるため、これらのサービスは利用しません。

 

スポンサーリンク

API用のLambda開発方法

次にAPI用のLambda(主に軽量な処理用)を作ってみるため、以下のコマンドを実行して各種ファイルを作成します。

$ mkdir -p src/api-lambda/internal/usecase/hello
$ touch src/api-lambda/internal/usecase/hello/hello.go src/api-lambda/internal/usecase/hello/hello_test.go
$ mkdir -p src/api-lambda/internal/handler/hello
$ touch src/api-lambda/internal/handler/hello/hello.go src/api-lambda/internal/handler/hello/hello_test.go
$ mkdir -p src/api-lambda/internal/router && touch src/api-lambda/internal/router/router.go
$ mkdir -p src/api-lambda/tests/integration/hello && touch src/api-lambda/tests/integration/hello/hello_test.go
$ touch src/api-lambda/main.go src/api-lambda/.golangci.yaml

※尚、今回はGoのフレームワーク「Echo」のV5を利用して作成します。

 

次に作成したファイルをそれぞれ以下のように記述します。

・「src/api-lambda/internal/usecase/hello/hello.go」

package hello

import (
    "net/http"

    "github.com/labstack/echo/v5"
)

// インターフェース定義
type HelloUsecase interface {
    Exec(c *echo.Context) error
}

// 構造体定義
type helloUsecase struct{}

// コンストラクタ関数
func NewHelloUsecase() HelloUsecase {
    return &helloUsecase{}
}

// Execメソッド定義
func (u *helloUsecase) Exec(c *echo.Context) error {
    return c.JSON(http.StatusOK, map[string]string{
        "message": "Hello World !!",
    })
}

※これはユースケース用のファイルです。

 

・「src/api-lambda/internal/usecase/hello/hello_test.go」

//go:build unit

package hello

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/labstack/echo/v5"
    "github.com/stretchr/testify/assert"
)

func TestHelloUsecase_Exec(t *testing.T) {
    // echoのコンテキストを作成
    e := echo.New()
    req := httptest.NewRequest(http.MethodGet, "/hello", nil)
    rec := httptest.NewRecorder()
    c := e.NewContext(req, rec)

    // ユースケースのインスタンスを作成
    usecase := NewHelloUsecase()

    // テスト実行
    err := usecase.Exec(c)

    // 検証
    assert.NoError(t, err)
    assert.Equal(t, http.StatusOK, rec.Code)

    var actual map[string]string
    err = json.Unmarshal(rec.Body.Bytes(), &actual)
    assert.NoError(t, err)

    expected := map[string]string{
        "message": "Hello World !!",
    }

    assert.Equal(t, expected, actual)
}

 

・「src/api-lambda/internal/handler/hello/hello.go」

package hello

import (
    "github.com/labstack/echo/v5"

    "api-lambda/internal/usecase/hello"
)

// インターフェース定義
type HelloHandler interface {
    Exec(c *echo.Context) error
}

// 構造体定義
type helloHandler struct {
    usecase hello.HelloUsecase
}

// コンストラクタ関数
func NewHelloHandler(
    usecase hello.HelloUsecase,
) HelloHandler {
    return &helloHandler{
        usecase: usecase,
    }
}

// Execメソッド定義
func (h *helloHandler) Exec(c *echo.Context) error {
    return h.usecase.Exec(c)
}

※これはハンドラー用のファイルです。

 

・「src/api-lambda/internal/handler/hello/hello_test.go」

//go:build unit

package hello

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/labstack/echo/v5"
    "github.com/stretchr/testify/assert"
    "go.uber.org/mock/gomock"

    mock_hello "api-lambda/internal/usecase/hello/mock_hello"
)

func TestHelloHandler_Exec(t *testing.T) {
    // echoのコンテキストを作成
    e := echo.New()
    req := httptest.NewRequest(http.MethodGet, "/hello", nil)
    rec := httptest.NewRecorder()
    c := e.NewContext(req, rec)

    // モックコントローラー作成
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()

    // ユースケースのモック化
    mockUsecase := mock_hello.NewMockHelloUsecase(ctrl)

    // モックの期待値を設定
    mockUsecase.
        EXPECT().
        Exec(c).
        Return(nil).
        Times(1)

    // ハンドラーを作成
    handler := NewHelloHandler(mockUsecase)

    // テスト実行
    err := handler.Exec(c)

    // 検証
    assert.NoError(t, err)
    assert.Equal(t, http.StatusOK, rec.Code)
}

 

・「src/api-lambda/internal/router/router.go」

package router

import (
    "github.com/labstack/echo/v5"
    "github.com/labstack/echo/v5/middleware"

    handlerHello "api-lambda/internal/handler/hello"
    usecaseHello "api-lambda/internal/usecase/hello"
)

func NewRouter() *echo.Echo {
    e := echo.New()

    // 共通ミドルウェア設定
    e.Use(middleware.RequestID())
    e.Use(middleware.Recover())
    e.Use(middleware.RequestLogger())

    /*****************
    * ルーティング定義
    *****************/

    // グループ定義「/api/v1」
    apiV1 := e.Group("/api/v1")

    // GET /api/v1/hello
    helloUsecase := usecaseHello.NewHelloUsecase()
    helloHandler := handlerHello.NewHelloHandler(helloUsecase)
    apiV1.GET("/hello", helloHandler.Exec)

    return e
}

※これはルーター用のファイルです。ここでユースケースのインスタンスを作ってハンドラーにDI(依存注入)し、それをルーティングに設定しています。

 

・「src/api-lambda/tests/integration/hello/hello_test.go」

//go:build integration

package hello

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/labstack/echo/v5"
    "github.com/stretchr/testify/assert"

    handlerHello "api-lambda/internal/handler/hello"
    usecaseHello "api-lambda/internal/usecase/hello"
)

func TestHelloEndpoint(t *testing.T) {
    // ルーター設定
    e := echo.New()
    apiV1 := e.Group("/api/v1")
    helloUsecase := usecaseHello.NewHelloUsecase()
    helloHandler := handlerHello.NewHelloHandler(helloUsecase)
    apiV1.GET("/hello", helloHandler.Exec)

    // リクエスト設定
    req := httptest.NewRequest(http.MethodGet, "/api/v1/hello", nil)
    rec := httptest.NewRecorder()

    // テスト実行
    e.ServeHTTP(rec, req)

    // 検証
    assert.Equal(t, http.StatusOK, rec.Code)

    expectedBody := `{"message":"Hello World !!"}`
    assert.JSONEq(t, expectedBody, rec.Body.String())
}

 

・「src/api-lambda/main.go」

package main

import (
    "fmt"
    "log/slog"
    "os"

    "github.com/aws/aws-lambda-go/lambda"
    "github.com/awslabs/aws-lambda-go-api-proxy/httpadapter"

    "api-lambda/internal/router"
)

func main() {
    // ルーターの初期化
    e := router.NewRouter()

    // AWS Lambda環境の場合
    if os.Getenv("AWS_LAMBDA_FUNCTION_NAME") != "" {
        echoLambda := httpadapter.NewV2(e)
        lambda.Start(echoLambda.ProxyWithContext)
        return
    }

    // ポート番号の取得
    port := os.Getenv("PORT")
    if port == "" {
        port = "8080"
    }

    // ローカル環境の場合
    if err := e.Start(fmt.Sprintf(":%s", port)); err != nil {
        slog.Error("failed to start server", "error", err)
    }
}

※AWS CLIのコマンド実行時は環境変数「AWS_LAMBDA_FUNCTION_NAME」が有効になりますが、通常時は無効のため、後述の処理でローカルサーバー起動が可能です。尚、今回はGoのフレームワークとして「echo」を利用しています。

 

・「src/api-lambda/.golangci.yaml」

version: "2"

run:
  # タイムアウト設定
  timeout: 5m

formatters:
  # 実行するformatterを指定
  enable:
    - gofumpt
    - gci

  settings:
    gofumpt:
      # モジュールパス設定
      module-path: api-lambda

    gci:
      # 並び順設定
      custom-order: true
      sections:
        - standard
        - default
        - prefix(api-lambda)
 
linters:
  # デフォルト設定を全てOFF
  default: none

  # 実行するlintを指定
  enable:
    - govet
    - staticcheck
    - gosec

 

次にファイル「Makefile」を以下のように修正します。

・「Makefile」

.PHONY: \
  go-mod-tidy-event-lambda fmt-event-lambda lint-event-lambda test-event-lambda \
    build-event-lambda deploy-local-event-lambda update-local-event-lambda \
  go-mod-tidy-api-lambda fmt-api-lambda lint-api-lambda test-unit-api-lambda test-integration-api-lambda \
    start-server-api-lambda \
    build-api-lambda deploy-local-api-lambda update-local-api-lambda \
    create-local-func-url-api-lambda delete-local-func-url-api-lambda \
    create-local-apigateway-api-lambda

# ローカル環境用のawsコマンドの変数定義
AWS_LOCAL := aws --profile local --endpoint-url=http://localhost:4566

# =====================
# event-lambda用の定義
# =====================
# go.mod更新
go-mod-tidy-event-lambda:
  docker compose run --rm go-lambda go -C ./event-lambda mod tidy

# フォーマット修正
fmt-event-lambda:
  docker compose run --rm go-lambda \
    sh -c 'cd event-lambda && golangci-lint fmt -v ./...'

# lint実行
lint-event-lambda:
  docker compose run --rm go-lambda \
    sh -c 'cd event-lambda && golangci-lint run -v ./...'

# unitテスト実行
test-event-lambda:
  docker compose run --rm go-lambda \
    env ENV=testing \
    go -C ./event-lambda test -v -cover -tags=unit \
      $$(docker compose run --rm go-lambda env ENV=testing go -C ./event-lambda list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' -tags=unit ./...)

# ビルド
build-event-lambda:
  docker compose run --rm \
    -e GOOS=linux \
    -e GOARCH=amd64 \
    -e CGO_ENABLED=0 \
    go-lambda \
    go -C ./event-lambda build -tags lambda.norpc -o bootstrap main.go

  docker compose run --rm go-lambda \
    zip -j event-lambda/function.zip event-lambda/bootstrap

# ローカル環境にデプロイ
deploy-local-event-lambda:
  $(AWS_LOCAL) iam create-role \
    --role-name event-lambda-role \
    --assume-role-policy-document \
      '{}'

  $(AWS_LOCAL) iam create-policy \
    --policy-name event-lambda-policy \
    --policy-document \
      '{"Version":"2012-10-17","Statement":[]}'

  $(AWS_LOCAL) iam attach-role-policy \
    --role-name event-lambda-role \
    --policy-arn arn:aws:iam::000000000000:policy/event-lambda-policy

  $(AWS_LOCAL) lambda create-function \
    --function-name event \
    --runtime provided.al2023 \
    --handler bootstrap \
    --role arn:aws:iam::000000000000:role/event-lambda-role \
    --zip-file fileb://lambda/event-lambda/function.zip

# ローカル環境のlambdaを更新
update-local-event-lambda:
  $(AWS_LOCAL) lambda update-function-code \
    --function-name event \
    --zip-file fileb://lambda/event-lambda/function.zip

# =====================
# api-lambda用の定義
# =====================
# go.mod更新
go-mod-tidy-api-lambda:
  docker compose run --rm go-lambda go -C ./api-lambda mod tidy

# フォーマット修正
fmt-api-lambda:
  docker compose run --rm go-lambda \
    sh -c 'cd api-lambda && golangci-lint fmt -v ./...'

# lint実行
lint-api-lambda:
  docker compose run --rm go-lambda \
    sh -c 'cd api-lambda && golangci-lint run -v ./...'

# unitテスト実行
test-unit-api-lambda:
  docker compose run --rm go-lambda \
  env ENV=testing \
  go -C ./api-lambda test -v -cover -tags=unit \
    $$(docker compose run --rm go-lambda env ENV=testing go -C ./api-lambda list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' -tags=unit ./...)

# integrationテスト実行
test-integration-api-lambda:
  docker compose run --rm go-lambda \
  env ENV=testing \
  go -C ./api-lambda test -v -cover -tags=integration \
    $$(docker compose run --rm go-lambda env ENV=testing go -C ./api-lambda list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' -tags=integration ./...)

# ローカルサーバー起動
start-server-api-lambda:
  LAMBDA_DIR=api-lambda docker compose up -d

# ビルド
build-api-lambda:
  docker compose run --rm \
    -e GOOS=linux \
    -e GOARCH=amd64 \
    -e CGO_ENABLED=0 \
    go-lambda \
    go -C ./api-lambda build -tags lambda.norpc -o bootstrap main.go

  docker compose run --rm go-lambda \
    zip -j api-lambda/function.zip api-lambda/bootstrap

# ローカル環境にデプロイ
deploy-local-api-lambda:
  $(AWS_LOCAL) iam create-role \
    --role-name api-lambda-role \
    --assume-role-policy-document \
      '{}'

  $(AWS_LOCAL) iam create-policy \
    --policy-name api-lambda-policy \
    --policy-document \
      '{"Version":"2012-10-17","Statement":[]}'

  $(AWS_LOCAL) iam attach-role-policy \
    --role-name api-lambda-role \
    --policy-arn arn:aws:iam::000000000000:policy/api-lambda-policy

  $(AWS_LOCAL) lambda create-function \
    --function-name api \
    --runtime provided.al2023 \
    --handler bootstrap \
    --role arn:aws:iam::000000000000:role/api-lambda-role \
    --zip-file fileb://src/api-lambda/function.zip

# ローカル環境のlambdaを更新
update-local-api-lambda:
  $(AWS_LOCAL) lambda update-function-code \
    --function-name api \
    --zip-file fileb://src/api-lambda/function.zip

# Lambda Function URLの作成
create-local-func-url-api-lambda:
  $(AWS_LOCAL) lambda create-function-url-config \
    --function-name api \
    --auth-type NONE

# Lambda Function URLの削除
delete-local-func-url-api-lambda:
  $(AWS_LOCAL) lambda delete-function-url-config \
    --function-name api

# API Gatewayの作成
create-local-apigateway-api-lambda:
  @set -e; \
  API_ID=$$($(AWS_LOCAL) apigatewayv2 create-api \
    --name api-gateway \
    --protocol-type HTTP \
    --target arn:aws:lambda:ap-northeast-1:000000000000:function:api \
    --query ApiId \
    --output text); \
  echo "API_ID=$$API_ID"; \
  INTEGRATION_ID=$$($(AWS_LOCAL) apigatewayv2 create-integration \
    --api-id "$$API_ID" \
    --integration-type AWS_PROXY \
    --integration-method POST \
    --payload-format-version 2.0 \
    --integration-uri arn:aws:lambda:ap-northeast-1:000000000000:function:api \
    --query IntegrationId \
    --output text); \
  echo "INTEGRATION_ID=$$INTEGRATION_ID"; \
  $(AWS_LOCAL) apigatewayv2 create-route \
    --api-id "$$API_ID" \
    --route-key '$$default' \
    --target "integrations/$$INTEGRATION_ID"; \
  $(AWS_LOCAL) apigatewayv2 create-stage \
    --api-id "$$API_ID" \
    --stage-name '$$default' \
    --auto-deploy; \
  echo "URL=http://localhost:4566/execute-api/$$API_ID/\$$default"

※利用する各種コマンドを簡単に実行するためにMakefileでまとめてます。また、API Gatewayを作成する際、実際にはコマンド「apigatewayv2 create-api」だけでクイック作成できるようですが、2026年7月時点のFlociのバージョン「1.5.31」では未対応?のため、「apigatewayv2 create-integration」、「apigatewayv2 create-route」、「apigatewayv2 create-stage」も実行する必要があります。

 

次に以下のコマンドを実行し、Goファイルの初期化をします。

$ docker compose run --rm go-lambda go -C ./api-lambda mod init api-lambda
$ docker compose run --rm go-lambda go -C ./api-lambda mod tidy

 

次に以下のコマンドを実行し、ユースケースのモック化用のファイルを作成します。

docker compose run --rm go-lambda \
mockgen \
-source=./api-lambda/internal/usecase/hello/hello.go \
-destination=./api-lambda/internal/usecase/hello/mock_hello/mock_hello.go \
-package=mock_hello \
-imports=echo=github.com/labstack/echo/v5

※ライブラリ「mockgen」を使い、ファイル「api-lambda/internal/usecase/hello/hello.go」のインターフェース定義からモック用のファイル「api-lambda/internal/usecase/hello/mock_hello/mock_hello.go」を作る処理です。

 

次に以下のコマンドを実行し、フォーマット修正や静的コード解析を行い、エラーや警告がないことを確認します。

$ make fmt-api-lambda
$ make lint-api-lambda

 

次に以下のコマンドを実行し、ユニットテストを実行します。

$ make test-unit-api-lambda

 

ユニットテスト実行後、以下のように全てのテストがパスすればOKです。

 

次に以下のコマンドを実行し、インテグレーションテストを実行します。

$ make test-integration-api-lambda

 

インテグレーションテスト実行後、以下のように全てのテストがパスすればOKです。

 

ローカルサーバーを起動してAPIを試す

次に以下のコマンドを実行し、ローカルサーバーを起動して試します。

$ docker compose down
$ make start-server-api-lambda

 

次にcurlコマンドを利用し、APIを実行して試します。

$ curl -v http://localhost:8080/api/v1/hello

 

実行後、以下のように想定通りのレスポンスが返ってこればOKです。

 

ビルドとデプロイ

次にGo言語で作成した上記のコードをビルドし、flociコンテナへデプロイします。

まずは以下のコマンドを実行し、Go言語のコードをビルドします。

$ make build-api-lambda

 

ビルド後、ファイル「src/api-lambda/bootstrap」、「src/api-lambda/function.zip」が作成されればOKです。

 

次に以下のコマンドを実行し、flociコンテナへデプロイします。

$ make deploy-local-api-lambda

 

実行後、以下のようにエラーが出ずに完了すればOKです。

※ターミナルが占有されますが、キーボードの「q」を押すと抜けれます。

 

Lambda Function URLを作成してAPIを試す

次にLambda Function URLを作成してAPIを試します。

まずは以下のコマンドを実行し、Lambda Function URLを作成します。

$ make create-local-func-url-api-lambda

 

実行後、ログにFunction URLが出力されるので確認します。

 

次にcurlコマンドを利用し、APIを実行して試します。

$ curl -v http://fc838810b69534b7adf685b03c3be369.lambda-url.ap-northeast-1.localhost:4566/api/v1/hello

※URLの部分は出力された値に修正して実行して下さい。

 

実行後、以下のように想定通りのレスポンスが返ってこればOKです。

 

上記で作成したLambda Function URLを削除するには以下のコマンドを実行します。

$ make delete-local-func-url-api-lambda

 

次に以下のコマンドを実行し、コンテナを再起動します。

$ docker compose restart

 

次に上記で実行したcurlコマンドを再度実行します。

$ curl -v http://fc838810b69534b7adf685b03c3be369.lambda-url.ap-northeast-1.localhost:4566/api/v1/hello

※URLの部分は出力された値に修正して実行して下さい。

 

実行後、ステータスコード「404」が返ってこれば削除されているのでOKです。

 

API Gatewayを作って試す

API用のLambdaを本番環境に公開する際は、API Gatewayを組み合わせるのが一般的なので、次はこれを試します。

まずは以下のコマンドを実行し、API Gatewayを作成します。

$ make create-local-apigateway-api-lambda

 

実行後、ログにAPIのURLを出力するようにしているので確認します。

 

次にcurlコマンドを利用し、APIを実行して試します。

curl -v \
--aws-sigv4 'aws:amz:ap-northeast-1:execute-api' \
--user 'test:test' \
'http://localhost:4566/execute-api/588397df6a/$default/api/v1/hello'

※URLの部分は出力された値に修正して実行して下さい。また、API GatewayのAPIを実行する際は、AWS署名を付けてリクエストを送信する必要があるため、「–aws-sigv4 ‘aws:amz:ap-northeast-1:execute-api’」や「-user ‘test:test’」を付けています。

 

実行後、以下のように想定通りのレスポンスが返ってこればOKです。

 

また、もしPostmanからAPIを実行したい場合は、タブ「認可」から認可タイプ「AWS署名」を選択し、アクセスキー(必須、ダミー値)、シークレットキー(必須、ダミー値)、AWSリージョン(必須、設定しているリージョン)、サービス名(任意)を設定してからAPIを実行して下さい。

API実行後、以下のように想定通りのレスポンスが返ってこればOKです。

 

API用のLambdaを作る際の補足

今回、API用のLambdaを作成する際には、Goのフレームワーク「Echo」を利用していますが、もちろん、Go言語だけでLambdaを実装することも可能です。

ただし、その場合はAPI Gateway側でHTTPメソッドやパスごとのルーティングを設定するか、Lambda側で受け取ったリクエストを判別して処理を振り分ける必要があります。

一方、フレームワークを利用することで、ルーティングをアプリケーション側で一元管理できるため、API Gatewayの設定をシンプルに保てます。

また、エンドポイントの追加や変更もコード上で完結するため、APIの拡張や保守がしやすいというメリットがあります。

そのため、単発でシンプルなAPIであればGo言語だけで実装するのも選択肢ですが、将来的な機能追加や保守性を考慮するのであれば、Echoのようなフレームワークを利用する構成がおすすめです。

 

スポンサーリンク

最後に

今回はGo言語(Golang)とFloci(フローシー)を使ったAWS Lambdaの開発方法について解説しました。

AWS Lambdaは実務でも利用される機会が多いため、早めにキャッチアップしておく価値があります。

また、イベント駆動による非同期処理を学ぶ前に、Lambdaの基本的な使い方を理解しておくと、その後の学習もスムーズに進められるでしょう。

今回利用したFloci(フローシー)は、無料で利用できるOSSでありながら、軽量かつ高速に動作するため、ローカル環境でAWS LambdaやS3などを快適に検証できます。

これからGo言語でLambdaの開発をしたい方は、ぜひ一度試してみて下さい。

 

この記事を書いた人
Tomoyuki

SE→ブロガーを経て、現在はSoftware Engineer(Web/Gopher)をしています!

Tomoyukiをフォローする
3. 応用
スポンサーリンク
Tomoyukiをフォローする

コメント

タイトルとURLをコピーしました