「労働者が自分の仕事をうまくやりたいなら、まず自分の道具を研ぎ澄まさなければなりません。」 - 孔子、「論語。陸霊公」
表紙 > プログラミング > Go を使用した AWS Lambda、初期定型文

Go を使用した AWS Lambda、初期定型文

2024 年 11 月 6 日に公開
ブラウズ:178

Unsplash の Lukáš Vaňátko による写真

導入

Go はそのシンプルさから、常に私のお気に入りの言語の 1 つです。最近、Go で書かれたラムダ関数を使用した単純な定型的なサーバーレス プロジェクトを作成するには何が必要かを理解することにしました。ツールと開発者のエクスペリエンスに興味がありました。

ゴール

postgres db をデータ層として使用する REST API を作成したいと考えています。私の最初の要件は次のとおりです

  • OpenApi で定義する仕様とそこから生成されるモデル
  • AWS SAM を使用する
  • 各エンドポイントは個別のラムダ関数によって処理されます
  • できるだけ簡単なローカル開発
  • デプロイメントでも同様

前提条件

Go と AWS SAM をインストールする必要があります。プロジェクトを AWS にデプロイする場合、作成しているリソースに対して料金が請求される可能性があるため、リソースが必要ない場合は忘れずにクリアしてください。
DBにはsupabase

を使用します

プロジェクトの構築

コードはこのリポジトリで入手できます

まず sam init を実行しましょう。 Hello World テンプレート、Go with provided al.2023 env を選択しました。以前は Go のマネージド ランタイムがありましたが、現在は非推奨になっています。

OpenApi スキーマ

API スキーマを OpenApi 仕様として定義すると、明らかな利点がいくつかあります。これをドキュメントの生成、クライアント、サーバーなどの作成に使用できます。また、AWS HttpApi ゲートウェイの形状を定義するためにも使用します。

私のスキーマは単純です。唯一興味深い部分は、ラムダ統合との接続を可能にする x-amazon-apigateway-integration プロパティです。セットアップは言語に依存しません。

スキーマ ファイルはリポジトリにあります

SAM テンプレート

HttpApi とシークレット

# template.yaml
# ....
ItemsAPI:
    Type: AWS::Serverless::HttpApi
    Properties:
      StageName: Prod
      DefinitionBody:
        'Fn::Transform':
          Name: 'AWS::Include'
          Parameters:
            Location: './api.yaml'
      FailOnWarnings: false
DBSecret:
    Type: AWS::SecretsManager::Secret
    Properties:
      Name: my-db-secret
      Description: Postgres config string
      SecretString: 'host=172.17.0.1 port=5431 user=admin password=root dbname=lambdas sslmode=disable'
# ....

上で述べたように、ここでは Go に特有のものは何もありません。 HttpApi ゲートウェイは OpenApi に基づいて作成されます。

接続文字列を保存するためのシークレットもあります。デプロイ後に値を更新します

ラムダ関数

AWS SAM の Go サポートは非​​常に優れています。ラムダ ハンドラーを使用して CodeUri をフォルダーにポイントし、ビルド メソッドを go1.x

として定義できます。

Go に組み込まれた Lambda 関数は、単一の自己完結型バイナリを生成するため、provided.al2023 ランタイムを使用します。

関数の定義は次のようになります:

# template.yaml
# ....
  GetItemFunction:
    Type: AWS::Serverless::Function
    Metadata:
      BuildMethod: go1.x
    Properties:
      Tracing: Active
      CodeUri: lambda_handlers/cmd/get_item/
      Handler: bootstrap
      Runtime: provided.al2023
      Architectures:
        - x86_64
      Environment:
        Variables:
          DB_SECRET_NAME: !Ref DBSecret
          API_STAGE: Prod
      Events:
        HttpApiEvents:
          Type: HttpApi
          Properties:
            Path: /item/{id}
            Method: GET
            ApiId: !Ref ItemsAPI
      Policies: 
        - AWSLambdaBasicExecutionRole
        - AWSSecretsManagerGetSecretValuePolicy:
            SecretArn: !Ref DBSecret
# ....

SAM マジックのおかげで、HttpApi ゲートウェイとラムダ関数の間の接続が、必要なすべての権限で確立されます。

機能コード

プロジェクトの構造

正直に言うと、フォルダー構造はおそらく慣用的ではありません。しかし、私は一般的な Go パターンに従おうとしました

lambda_handlers
|--/api
|--/cmd
|--/internal
|--/tools
|--go.mod
|--go.sum

cmd は実際のラムダ ハンドラーを含むメイン フォルダーです
内部にはハンドラー間で共有されるコードが保持されます
tools はプロジェクトで使用する追加のツールを定義します
OpenAPI ジェネレーター構成および生成されたモデル用の API

関数ハンドラー

ラムダ ハンドラーの最初の定型文は次のようになります:

// ...
func handleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {
    // handler logic
}

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

通常、最初に尋ねるべき質問は、AWS SDK クライアントの初期化、データベース接続、およびコールドスタート中に処理したいその他のものをどこに配置するかということです。

こちらにはオプションがございます。 1 つ目は、AWS ドキュメントの例のパターンに従い、init() 関数内でサービスを初期化することです。私はこのアプローチが好きではありません。単体テストでハンドラーを使用することが難しくなるからです。

lambda.Start() メソッドが関数を入力として受け取るという事実のおかげで、それをカスタム構造体でラップし、必要なサービスで初期化できます。私の場合、コードは次のようになります:

package main

// imports ...

type GetItemsService interface {
    GetItem(id int) (*api.Item, error)
}

type LambdaHandler struct {
    svc GetItemsService
}

func InitializeLambdaHandler(svc GetItemsService) *LambdaHandler {
    return &LambdaHandler{
        svc: svc,
    }
}

func (h *LambdaHandler) HandleRequest(ctx context.Context, request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) {

    strId, err := helpers.GetPathParameter("id", request.PathParameters)

    if err != nil {
        return helpers.SendResponse("id is required", 400), nil
    }

    id, err := strconv.Atoi(*strId)

    if err != nil {
        return helpers.SendResponse("id must be an integer", 400), nil
    }

    log.Printf("id: %d", id)

    result, err := h.svc.GetItem(id)

    if err != nil {
        if err.Error() == "Item not found" {
            return helpers.SendResponse("Item not found", 404), nil
        }
        return helpers.SendResponse("error", 500), nil
    }

    jsonRes, err := json.Marshal(result)

    if err != nil {
        log.Printf("error marshalling response: %s", err.Error())
        return helpers.SendResponse("internal server error", 500), nil
    }

    return helpers.SendResponse(string(jsonRes), 200), nil
}

func main() {

    dbSecretName := os.Getenv("DB_SECRET_NAME")

    log.Printf("dbSecretName: %s", dbSecretName)

    cfg, err := awssdkconfig.InitializeSdkConfig()

    if err != nil {
        log.Fatal(err)
    }

    secretsClient := awssdkconfig.InitializeSecretsManager(cfg)

    connString, err := secretsClient.GetSecret(dbSecretName)

    if err != nil {
        log.Fatal(err)
    }

    conn, err := db.InitializeDB(connString)

    if err != nil {
        log.Fatal(err)
    }

    defer conn.Close()

    log.Println("successfully connected to db")

    svc := items.InitializeItemsService(conn)

    handler := InitializeLambdaHandler(svc)

    lambda.Start(handler.HandleRequest)
}

main() 関数 (コールド スタート中に実行される) で、secretsmanager からシークレットを取得し、DB との接続を初期化します。どちらの機能も共通ヘルパーとして内部フォルダー内に定義されているため、他のハンドラーで再利用できます。最後に、作成した DB 接続を使用して ItemsService が初期化され、ラムダ ハンドラーの作成に使用されます。

HandleRequest はパス パラメーターから ID を解析し、ItemsService を呼び出して DB からアイテムを取得します。

内部モジュール

機能がシンプルなため、ビジネスロジックはあまりありません。 ItemsServise は、特定のアイテムの db を呼び出すだけです

package items

import (
    "database/sql"
    "errors"
    api "lambda_handlers/api"
    "log"
)

type ItemsService struct {
    conn *sql.DB
}

func InitializeItemsService(conn *sql.DB) *ItemsService {
    return &ItemsService{
        conn: conn,
    }
}

func (svc ItemsService) GetItem(id int) (*api.Item, error) {

    log.Printf("Getting item id %v", id)

    query := `SELECT * FROM items WHERE id = $1`

    var item api.Item

    err := svc.conn.QueryRow(query, id).Scan(&item.Id, &item.Name, &item.Price)

    log.Printf("Got item id %v", id)

    if err != nil {
        log.Printf("Error getting item %v: %v", id, err)
        if err == sql.ErrNoRows {
            return nil, errors.New("Item not found")
        }
        return nil, err
    }

    return &item, nil

}

現時点では、これ以上何も必要ありません。

ツール

私の目標は、プロジェクトの依存関係に追加できる追加のツールを使用することです。そのため、開発者のマシンにインストールされているツールに依存する必要はありません。

Go でそれを行う 1 つの方法は、oapi-codegen をツール パッケージに保持することです

//go:build tools
//  build tools

package main

import (
    _ "github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen"
)

api_gen.go 内から呼び出します

//go:generate go run github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen --config=cfg.yaml ../../api.yaml

package api

この方法では、oapi-codegen バイナリを個別にインストールせずに gogenerate を実行できます。

地域発展

建てる

私のビルド プロセスには 2 つのステップが必要です。OpenAPI からモデルを生成し、プロジェクト自体をビルドします。後者の処理は AWS SAM に任せました。

これが私の Makefile です

.PHONY: build local deploy

generate:
    cd lambda_handlers && go generate ./...

build: generate
    rm -rf .aws-sam
    sam build

local: build
    sam local start-api --env-vars parameters.json

deploy: build
    sam deploy

初期展開

私にとって、API Gateway をローカルでテストする最も簡単な方法は、sam local start-api を実行することです
私たちの関数は環境変数に依存しているため、環境変数を sam local

に渡すように paramters.json ファイルを作成しました。

サーバーレス向けに開発する場合、ある時点でローカル開発であってもクラウド リソースの使用を開始したくなる場合があります。私の場合は、すぐに Secret Manager を利用して DB の接続文字列を保存します。つまり、ローカル開発で使用できるように、最初にスタックをデプロイする必要があります。

makedeploy を実行しますが、今のところ、デプロイメント全体をチェックせず、コンソールからシークレット名を取得するだけです。また、正しい接続文字列が保持されるように、コンソールでシークレットを更新する必要もあります。

テストのために、supabase に DB を作成し、いくつかのダミー レコードをシードしました

ローカルテスト

make local を実行すると、API をローカルでテストできるようになります

AWS Lambda with Go, initial boilerplate

Go はコンパイル言語であるため、変更するたびにプロジェクトを再構築し、start-api を再度実行する必要があります。 Go コンパイラーの驚くべき速度を考慮すると、それは大したことではありません。

AWS でテストする

API ゲートウェイ URL は、デプロイ後にコンソールに出力され、AWS コンソールから直接取得することもできます。

エンドポイントを呼び出すと、期待どおりに動作します:

AWS Lambda with Go, initial boilerplate

コールド スタートは、初期化に約 300 ミリ秒かかるため、少し時間がかかります。これは主にシークレットの取得と DB への接続の確立が含まれるためです。しかし、正直に言うと、これはまともな結果以上のものです。

まとめ

指定されたプロジェクトは、Go でサーバーレス REST API を作成するための開始点です。スキーマには OpenAPI を使用し、デプロイメントとローカル テストの管理には AWS SAM を使用します。

シークレット マネージャーから接続文字列を取得するために、外部 postgres データベースと AWS SDK を使用しました。

ラムダ ハンドラーとアイテム サービスの単体テストもあります

ほとんどの時間は AWS 部分の設定に費やしましたが、これはすべての言語で同じです。 Go コードは非常に単純です (この単純な使用例の場合)。

リリースステートメント この記事は次の場所に転載されています: https://dev.to/aws-builders/aws-lambda-with-go-initial-boilerplate-2787?1 侵害がある場合は、[email protected] に連絡して削除してください。
最新のチュートリアル もっと>

免責事項: 提供されるすべてのリソースの一部はインターネットからのものです。お客様の著作権またはその他の権利および利益の侵害がある場合は、詳細な理由を説明し、著作権または権利および利益の証拠を提出して、電子メール [email protected] に送信してください。 できるだけ早く対応させていただきます。

Copyright© 2022 湘ICP备2022001581号-3