My Note

自己理解のためのブログ

tagprでPRベースでタグを管理する

はじめに

githubでタグでバージョン管理するときにtag打ちとリリースノートの作成を手動で行うのは面倒だと思っていました。 tagprというOSSがリリースされてtag打ちとリリースノートをworkflowで自動で行なってくれるので今回使ってみました。

github.com

設定

  • .github/workflows/tagpr.yml
name: tagpr
on:
  push:
    branches:
      - main
jobs:
  tagpr:
    runs-on: ubuntu-latest
    timeout-minutes: 3
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      
      - name: Tagpr
        uses: Songmu/tagpr@main
        id: tagpr
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  • .github/release.yml
    • リリースノートの設定について設定します。
    • PRに tagpr をタグを付与したものは除外する設定を追加
changelog:
  exclude:
    labels:
      - tagpr
# config file for the tagpr in git config format
# The tagpr generates the initial configuration, which you can rewrite to suit your environment.
# CONFIGURATIONS:
#   tagpr.releaseBranch
#       Generally, it is "main." It is the branch for releases. The tagpr tracks this branch,
#       creates or updates a pull request as a release candidate, or tags when they are merged.
#
#   tagpr.versionFile
#       A versioning file containing the semantic version needed to be updated at release.
#       It will be synchronized with the "git tag".
#       Often this is a meta-information file such as gemspec, setup.cfg, package.json, etc.
#       Sometimes the source code file, such as version.go or Bar.pm, is used.
#       If you do not want to use versioning files but only git tags, specify the "-" string here.
#
#   tagpr.vPrefix
#       Flag whether or not v-prefix is added to semver when git tagging. (e.g. v1.2.3 if true)
[tagpr]
    vPrefix = true
    releaseBranch = main
    versionFile = -

実際に利用してみる

  • tag: v0.0.3 の状態で#3 のPRをmainブランチに対してマージするとworkflowが走る
  • PR Release for v0.0.4 #4 が作成される(下記画像)

  • この状態で #5 のPRをmainブランチに対してマージするとworkflowが走り Release for v0.0.4 #4Whats Changed に追加される

このように、tagprのworkflowで指定しているブランチに対してPRを作成していき任意のタイミングでtagをきるさいにこの Release for v0.0.4 #4 のPRをマージする

  • この状態で Release for v0.0.4 #4 のPRをマージします。そうるすと、workflowが走りtagとreleaseノートが発行される

特にタグを指定しないとパッチバージョンがインクリメントされます。 ここで、マイナーバージョンを上げるためには tagpr:minor をラベルに付与します。メジャーバージョンを上げるには同様に tagpr:major をラベルに付与します。

ref) GitHub - Songmu/tagpr: automatically creates and updates a pull request for unreleased items, tag them when they are merged, and create releases. 今回は試しに、マイナーバージョンを上げるために tagpr:minor のラベルを Release for v0.0.5 に付与してPRをマージしました。↓

  • PRに tagpr:minor ラベルを付与

  • マイナーバージョンがアップデートされた (v0.0.4 --> v0.1.0)

tag発行をトリガーとして利用する

tagprで作成されたPRをマージするとworkflowが実行されtagが打たれてリリースされる。 このタイミングで何かしらアクション、(例えばデプロイ)などする場合に下記のようにして 任意の処理を実行することも可能です。

  • .github/workflows/tagpr.yml
name: tagpr
on:
  push:
    branches:
      - main

jobs:
  tagpr:
    runs-on: ubuntu-latest
    timeout-minutes: 3
    outputs:
      tagpr-tag: ${{ steps.exec-tagpr.outputs.tag }}
    env:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - name: Check out source code
        uses: actions/checkout@v3

      - name: Exec-tagpr
        id: exec-tagpr
        uses: Songmu/tagpr@v1

  deploy_something:
    runs-on: ubuntu-latest
    timeout-minutes: 3
    needs: tagpr
    env:
      TAG_VERSION: ${{ needs.tagpr.outputs.tagpr-tag }}

    steps:
      - name: Show tag
        if: needs.tagpr.outputs.tagpr-tag != ''
        run: |
          echo ${{ env.TAG_VERSION }}

さいごに

今回、githubのタグを管理する方法としてtagprを検証しました。PRベースでタグとリリースノートが自動で作成でき、バージョンアップのメジャー、マイナー、パッチも PRにタグを付与することで設定できたのでとても便利でした。個人開発のリポジトリに導入して利用していこうと思います。

GitHub-Actionsのworkflowについてまとめる

今回はgithub-actionsのworkflowについてです。最近、workflowを書く機会が多く一度、整理するために書いていきます。

workflow実行時に選択式で値を渡す

name: test
on:
  workflow_dispatch:
    inputs:
      env:
        type: choice
        description: "envを選択"
        required: true
        options:
          - dev
          - stg
          - prd

workflow実行時に入力式で値を渡す

name: test
on:
  workflow_dispatch:
    inputs:
      env:
        description: "envを入力"
        required: true

permissions:
  id-token: write
  contents: write

jobs:
  test:
    name: "sample"
    runs-on: ubuntu-22.04
    timeout-minutes: 3

    steps:
      - name: Show ENV
        run: |
          echo "ENV=${{ github.event.inputs.env }}"

三項演算子ライクに値を設定する

  • inputsのenvが prd なら secrets.PRD を設定し
  • inputsのenvが stg なら secrets.STG を設定し
  • prdでもstgでもなければ、secrets.DEV を設定する
name: test
on:
  workflow_dispatch:
    inputs:
      env:
        type: choice
        description: "envを選択"
        required: true
        options:
          - dev
          - stg
          - prd

env:
  ENV_VALUE: |
    ${{
      github.event.inputs.env == 'prd' &&
        secrets.PRD ||
      github.event.inputs.env == 'stg' &&
        secrets.STG ||
      secrets.DEV
    }}

set-envを使わずに環境変数に値をセットする

  • echo "{name}={value}" >> $GITHUB_ENV と記述する
  • workflow実行時に設定したenvの情報を環境変数に設定する
name: test
on:
  workflow_dispatch:
    inputs:
      env:
        type: choice
        description: "envを選択"
        required: true
        options:
          - dev
          - stg
          - prd

permissions:
  id-token: write
  contents: write

jobs:
  test:
    name: "sample"
    runs-on: ubuntu-22.04
    timeout-minutes: 3
      - name: Set ENV
        run: |
          echo "ENV"=`echo ${{ github.event.inputs.env }}` >> $GITHUB_ENV

      - name: Show ENV
        run: |
          echo "ENV=${{ env.ENV }}"

■ 実行結果

repository_dispatch を利用して別のworkflowから実行する

注: このイベントは、ワークフローファイルがデフォルト ブランチにある場合にのみワークフローの実行をトリガーします。

  • 呼び出し元のworkflow
    • event_type は任意の文字列で設定する
name: call
on:
  push:
    branches:
      - test
jobs:
  call:
    runs-on: ubuntu-22.04
    timeout-minutes: 3

    steps:
      - name: Set request body
        run: |
          echo "REQUEST_BODY"='{"event_type": "call_workflow", "client_payload": {"sample_key": "sample_value" }}' >> $GITHUB_ENV

      - name: Trigger repository dispatch
        run: |
          curl \
          -X POST \
          -H "Accept: application/vnd.github.v3+json" \
          -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
          https://api.github.com/repos/yhidetoshi/github-actions-workflow-dev/dispatches \
          --data "$REQUEST_BODY"

■ 実行結果

  • 呼び出し先のworkflow
    • types: [call_workflow] 呼び出し元の event_type に合わせる
name: test_repository_dispatch
on:
  repository_dispatch:
    types: [call_workflow]

permissions:
  id-token: write
  contents: write

jobs:
  test:
    name: "sample"
    runs-on: ubuntu-22.04
    timeout-minutes: 3

    steps:
      - name: Hello world
        run: |
          echo "Hello world (test repository dispatch)"

■ 実行結果

Composite Actionを使う

workflowの処理をモジュール化するときに使える

name: use_composte
on:
  push:
    branches:
      - test

env:
  RELEASE_TAG: 0.0.2

jobs:
  call:
    runs-on: ubuntu-22.04
    timeout-minutes: 3

    steps:
      - name: Checkout composite-action
        uses: actions/checkout@v3
        with:
          repository: yhidetoshi/workflow-composite-action-dev
          path: ./.github/actions/composite-action
          token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
          ref: ${{ env.RELEASE_TAG }}

      - name: Call print composite
        uses: ./.github/actions/composite-action/print
  • composite-actionのworkflow
    • yhidetoshi/workflow-composite-action-dev
❯ tree .
.
├── README.md
└── print
    └── action.yml
name: print

runs:
  using: "composite"
  steps:
    - run: |
        echo "Hello world at composite"
      shell: bash

Lambdaレイヤーを利用してラインタイムPython上でgoバイナリを実行 & レイヤーをバージョン管理する

今回はLambdaレイヤーについて簡単な検証とレイヤーのバージョン管理について検証しました。

Lambdaレイヤーとは

docs.aws.amazon.com

Lambda レイヤーは、Lambda 関数で使用できるライブラリとその他の依存関係をパッケージ化するための便利な方法を提供します。レイヤーを使用することで、アップロードされたデプロイメントアーカイブのサイズを削減し、コードをデプロイするスピードを速めることができます。 レイヤーは、追加のコードまたはデータを含むことができる .zip ファイルアーカイブです。レイヤーには、ライブラリ、 カスタムランタイム 、データ、または設定ファイルを含めることができます。レイヤーを使用すると、コードの共有と責任の分離を促進し、ビジネスロジックの記述をより迅速に繰り返すことができます。

レイヤーを利用することで

  • 複数のlambdaで共通的に利用可能なライブラリを作れる
  • ライブラリの依存関係をパッケージ化できる
  • zipでコードやデータをアップロードして使える
  • レイヤーに持たせることでlambdaにデプロイするデータサイズを削減できる

試してみる

やりたい事は、goのバイナリをLambdaレイヤーに登録してpythonランタイムの関数からの実行です。 レイヤーに登録するGoのコード。これをコンパイルして利用しました。

package main

import "fmt"

func main() {
    fmt.Println("hello world lambda layer")
}

次はlambdaのレイヤーを作成するためのzipを用意します。

Lambda レイヤーの作成と共有 - AWS Lambda

このドキュメントの ライブラリの依存関係をレイヤーに含める の項目に すべてのランタイムは /bin にPATHが通っているようなので、 ./bin/main にバイナリを配置して zip化しました。

❯ ls
bin     main.go
❯ ls bin/
main
❯ zip -r go-cmd.zip bin

今回はAWSコンソールからポチポチしてレイヤーを作成しました。

レイヤーをLambda関数に追加して利用できる状態になったのでPythonでgoバイナリを実行します。 利用したのは以下のコードです。

  • lambda_function.py
import subprocess
def lambda_handler(event, context):
    cmd = ['main']
    out = subprocess.run(cmd, stdout=subprocess.PIPE)
    print(out.stdout.decode())

■ 実行結果

Function Logs
START RequestId: 68838428-bb76-4af7-86ab-4805669c676f Version: $LATEST
hello world lambda layer
END RequestId: 68838428-bb76-4af7-86ab-4805669c676f

hello world lambda layer

と表示されて実行できました。

レイヤーをバージョン管理する

レイヤーをコード管理する場合にバージョン管理する必要が出てくると思います。 そこで、バージョンに対してcommit-idやtag名などを付与できるか確認しました。

lambdaレイヤーを更新するために aws-cliを利用しました。(publish-layer-version) awscli.amazonaws.com バージョンは1からインクリメントされていくので、レイヤーを更新するときに description オプションを付与して仮のtag名(v0.0.1)を付与しました。 github actionsのworkflowを利用していればtag pushをトリガーにtag名を取得してレイヤーを更新するときに descriptionに付与できます。

  • lambdaレイヤーを更新するコマンド (aws-vaultを利用した場合)
❯ aws-vault exec ${profile} -- aws lambda publish-layer-version \
  --layer-name go-cmd \
  --description "v0.0.1" \
  --zip-file fileb://go-cmd.zip \
  --compatible-runtimes python3.9

新しくレイヤーを更新したら以下のようになりました。説明の部分に v0.0.1 タグを埋め込めるので何のバージョンのものかすぐにわかるようにできそうです。

APIGateway + Lambda + Go(Echo)をSAM(Serverless Application Model)でAWSとローカルにデプロイする

はじめに

前回、APIGW + Lambda + Go(Echo)のローカル環境をSAMで構築しました。 今回は、samでローカルではなく、AWS環境にデプロイをしました。ローカルも同じ設定で利用可能です。

yhidetoshi.hatenablog.com

本記事ではAWSにデプロイする部分にだけ記載します。詳細は↑の記事に記載済みです。

また、LambdaでGoフレームワークのEchoを動かす事についてまとめた記事は↓。

yhidetoshi.hatenablog.com

コード

ディレクトリ構成

serverless-sam-go-local
├── README.md
└── serverless
    ├── README.md
    ├── echo
    │   ├── Makefile
    │   ├── api
    │   │   └── healthcheck
    │   │       └── healthcheck.go
    │   ├── conf
    │   │   └── config.go
    │   ├── echo
    │   ├── go.mod
    │   ├── go.sum
    │   ├── handler
    │   │   └── auth
    │   │       └── auth.go
    │   └── main.go
    ├── events
    │   └── event.json
    ├── samconfig.toml
    └── template.yaml

ソースコードはこちら。 github.com

APIGWとLambdaの設定

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  description

Globals:
  Function:
    Timeout: 30

Resources:
  MyAPI:
    Type: AWS::Serverless::Api
    Properties:
      Name: sam-echo-test
      StageName: v1
      EndpointConfiguration: REGIONAL
  
  SAMEchoFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: echo
      Handler: echo
      Runtime: go1.x
      Architectures:
        - x86_64
      Events:
        GetApi:
          Type: Api
          Properties:
            Path: /api/{proxy+}
            Method: get
            RestApiId: !Ref MyAPI
            Auth:
              ResourcePolicy:
                CustomStatements: [{
                  "Effect": "Allow",
                  "Principal": "*",
                  "Action": "execute-api:Invoke",
                  "Resource": "arn:aws:execute-api:ap-northeast-1:*:*/*",
                  "Condition": {
                    "IpAddress": {
                      "aws:SourceIp": "X.X.X.X/32"
                    }
                  } 
                }]
      #Environment:
      #  Variables:
      #    PARAM1: VALUE

デプロイ

AWSにデプロイする

初回実行に初期設定が必要なため --guided オプションを付与して、2回目以降の設定反映には不要です。

❯ aws-vault exec my-dev -- sam deploy --guided

Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Not found

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]: sam-echo-test
        AWS Region [ap-northeast-1]: 
        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [y/N]: y
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: y
        #Preserves the state of previously provisioned resources when an operation fails
        Disable rollback [y/N]: y
        HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
        Save arguments to configuration file [Y/n]: y
        SAM configuration file [samconfig.toml]: 
        SAM configuration environment [default]: dev

        Looking for resources needed for deployment:
        Creating the required resources...
        Successfully created!
(処理の進捗が表示されるので省略)

生成されたsamconfig.tomlの中身は以下のとおりになりました。 スタック名やawsのリソース情報などが記載されています。

  • samconfig.toml
version = 0.1
[dev]
[dev.deploy]
[dev.deploy.parameters]
stack_name = "sam-echo-test"
s3_bucket = "aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxx"
s3_prefix = "sam-echo-test"
region = "ap-northeast-1"
confirm_changeset = true
capabilities = "CAPABILITY_IAM"
disable_rollback = true
image_repositories = []

作成したstackを削除する場合は sam delete コマンドを実行する

  • sam delete を実行するとstack名を問われるので入力する
❯ aws-vault exec my-dev -- sam delete
        Enter stack name you want to delete: sam-echo-test
        Are you sure you want to delete the stack sam-echo-test in the region ap-northeast-1 ? [y/N]: y
        Are you sure you want to delete the folder sam-echo-test in S3 which contains the artifacts? [y/N]: y
        - Deleting S3 object with key sam-echo-test/xxxxxxxx
        - Deleting S3 object with key sam-echo-test/xxxxxxxx.template
        - Deleting Cloudformation stack sam-echo-test

Deleted successfully

awsのAPIGWのエンドポイントに対してAPIコールしてOKを確認

❯ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/v1/api/healthcheck
{"status":200,"message":"Success to connect echo"}

ローカルにデプロイする

template.yamlを特に変更せずlocalに展開するコマンドを実行してました。

❯ sam local start-api
Mounting SAMEchoFunction at http://127.0.0.1:3000/api/{proxy+} [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2022-11-05 16:44:31  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)

動作確認でヘルスチェックAPIを実行してOK

❯ curl http://127.0.0.1:3000/api/healthcheck
{"status":200,"message":"Success to connect echo"}

さいごに

今回、SAMを使ってローカルとAWSにサーバレス環境をデプロイしました。 SAMの機能をまだまだ使いこなせていきたいので他にも試していきたいと思います。

APIGateway + Lambda + Go(Echo)で開発するためのローカル環境をSAM(Serverless Application Model)で再現する

はじめに

前回の記事では、サーバレス環境をローカルに用意するためにserverless frameworkを利用しました。そこで今回はSAMを利用して同じ環境を構築してみます。 基本的にServerlessFrameworkを利用してきたのでSAMを使うのは今回が初めてでした。 構成は Client --> APIGW --> Lambdaで進めます。

yhidetoshi.hatenablog.com

ローカル環境構築

aws-cliがインストールされている前提で進めます。

brew tap aws/tap
brew install aws-sam-cli

今回はGoで環境構築するので go1.xをruntimeに指定してプロジェクトを作成します。

❯ sam init --runtime go1.x --name serverless
Which template source would you like to use?
    1 - AWS Quick Start Templates
    2 - Custom Template Location
Choice: 1

Choose an AWS Quick Start application template
    1 - Hello World Example
    2 - Infrastructure event management
    3 - Multi-step workflow
Template: 1

Based on your selections, the only Package type available is Zip.
We will proceed to selecting the Package type as Zip.

Based on your selections, the only dependency manager available is mod.
We will proceed copying the template using mod.

Would you like to enable X-Ray tracing on the function(s) in your application?  [y/N]: n

Cloning from https://github.com/aws/aws-sam-cli-app-templates (process may take a moment)

sam-initすると自動でディレクトリとファイルが作成されます。今回は hello-world を削除して echo ディレクトリを配置しました。 こちらは前回の記事と同じもので、echoフレームワークでヘルスチェックAPIを実装しています。 ソースコードはこちらです。

github.com

ディレクトリ構成

serverless
├── README.md
├── echo
│   ├── Makefile
│   ├── api
│   │   └── healthcheck
│   │       └── healthcheck.go
│   ├── conf
│   │   └── config.go
│   ├── echo
│   ├── go.mod
│   ├── go.sum
│   ├── handler
│   │   └── auth
│   │       └── auth.go
│   └── main.go
├── events
│   └── event.json
└── template.yaml

echoフレームワークの部分は前回の記事になどに記載していますので今回は省略します。 goをコンパイルするときに利用するMakefileと samを実行する template.yaml のコードを記載します。

PHONY: deps clean build

deps:
    go get -u ./...

clean:
    rm -rf echo # 変更

build:
    GOOS=linux GOARCH=amd64 go build -o echo ./main.go 
  • template.yaml (# 修正 の部分を自動生成されたファイルを修正しました)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  description

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 30 # 修正

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    Properties:
      CodeUri: echo # 修正
      Handler: echo # 修正
      Runtime: go1.x
      Architectures:
        - x86_64
      Events:
        CatchAll:
          Type: Api # More info about API Event Source: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#api
          Properties:
            Path: /api/{proxy+} # 修正
            Method: GET
      Environment: # More info about Env Vars: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#environment-object
        Variables:
          PARAM1: VALUE

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldAPI:
    Description: "API Gateway endpoint URL for Prod environment for First Function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "First Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn

このtemplate.yamlですが、AWSに展開することも考えて以下のように書き換えました。どちらもローカルでは同じ動きをします。

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  description

Globals:
  Function:
    Timeout: 30

Resources:
  MyAPI:
    Type: AWS::Serverless::Api
    Properties:
      Name: sam-echo-test
      StageName: v1
      EndpointConfiguration: REGIONAL
  
  SAMEchoFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: echo
      Handler: echo
      Runtime: go1.x
      Architectures:
        - x86_64
      Events:
        GetApi:
          Type: Api
          Properties:
            Path: /api/{proxy+}
            Method: get
            RestApiId: !Ref MyAPI
            Auth:
              ResourcePolicy:
                CustomStatements: [{
                  "Effect": "Allow",
                  "Principal": "*",
                  "Action": "execute-api:Invoke",
                  "Resource": "arn:aws:execute-api:ap-northeast-1:*:*/*",
                  "Condition": {
                    "IpAddress": {
                      "aws:SourceIp": "X.X.X.X/32"
                    }
                  } 
                }]
      #Environment:
      #  Variables:
      #    PARAM1: VALUE

デプロイと動作確認

❯ make build
GOOS=linux GOARCH=amd64 go build -o echo ./main.go
  • ローカルでsam実行
❯ sam local start-api

Mounting HelloWorldFunction at http://127.0.0.1:3000/api/{proxy+} [GET]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2022-11-03 16:35:45  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)

Invoking echo (go1.x)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-go1.x:rapid-1.61.0-x86_64.

Mounting /Users/hidetoshi/github-repos/github.com/yhidetoshi/serverless-sam-go-local/serverless/echo as /var/task:ro,delegated inside runtime container
START RequestId: 22427e50-7eb8-425a-a06e-2739449f6d7a Version: $LATEST
2022/11/03 07:38:23 echo cold start
END RequestId: 22427e50-7eb8-425a-a06e-2739449f6d7a
REPORT RequestId: 22427e50-7eb8-425a-a06e-2739449f6d7a  Init Duration: 113.63 ms        Duration: 735.62 ms     Billed Duration: 736 ms Memory Size: 128 MB     Max Memory Used: 128 MB
2022-11-03 16:38:23 127.0.0.1 - - [03/Nov/2022 16:38:23] "GET /api/healthcheck HTTP/1.1" 200 -
  • clientからヘルスチェクAPIを実行
❯ curl http://127.0.0.1:3000/api/healthcheck
{"status":200,"message":"Success to connect echo"}

リクエスト実行中には以下のコンテナが起動して lambdaをエミュレートして処理終了したらコンテナは削除されました。

❯ docker ps
CONTAINER ID   IMAGE                                                    COMMAND                  CREATED        STATUS                  PORTS                      NAMES
7eb1599fd838   public.ecr.aws/sam/emulation-go1.x:rapid-1.61.0-x86_64   "/var/rapid/aws-lamb…"   1 second ago   Up Less than a second   127.0.0.1:5959->8080/tcp   focused_wilson

(追記) ローカルではなく、AWSにデプロイする方法については、↓の記事にまとめました。

yhidetoshi.hatenablog.com

さいごに

前回の記事と同様にサーバーレス環境(APIGW + Lambda + Go(echo))のローカル環境を構築することができました。 この程度のローカル環境を構築するだけだと SAMとServerlessFrameworkはほとんど負担感は変わらない感じでした。 ServerlessFrameworkとSAMで機能や特徴が異なると思うので構築する環境で使い分けていければと思います。

APIGateway + Lambda + Go(Echo)で開発するためのローカル環境をServerlessFrameworkで再現する

はじめに

以前の記事で APIGW + Lambdaで GoのフレームワークであるEchoを動かす検証を実施しました。 実際にデプロイすることができたので今回は開発を行うためにローカル環境をServerlessFrameworkで構築しました。

yhidetoshi.hatenablog.com

構成

Client --> APIGW --> Lambda(Goコンテナ)

をローカル環境に構築します。ServerlessFrameworkで実行すれば、この構成を簡単に用意できます。

ローカル環境

  • serverless-offline をインストールする
npm install --save-dev serverless-offline
  • プロジェクトを作成する
    • sls create -t <テンプレート> -n <プロジェクト名>
sls create -t aws-go -n serverless-echo-lambda

*) テンプレートについては以下が対応していました。色々ありますね。

Supported templates are: "aws-clojure-gradle", "aws-clojurescript-gradle", "aws-nodejs", "aws-nodejs-docker", "aws-nodejs-typescript", "aws-alexa-typescript", "aws-nodejs-ecma-script", "aws-python", "aws-python3", "aws-python-docker", "aws-groovy-gradle", "aws-java-maven", "aws-java-gradle", "aws-kotlin-jvm-maven", "aws-kotlin-jvm-gradle", "aws-kotlin-jvm-gradle-kts", "aws-kotlin-nodejs-gradle", "aws-scala-sbt", "aws-csharp", "aws-fsharp", "aws-go", "aws-go-dep", "aws-go-mod", "aws-ruby", "aws-provided", "tencent-go", "tencent-nodejs", "tencent-python", "tencent-php", "azure-csharp", "azure-nodejs", "azure-nodejs-typescript", "azure-python", "cloudflare-workers", "cloudflare-workers-enterprise", "cloudflare-workers-rust", "fn-nodejs", "fn-go", "google-nodejs", "google-nodejs-typescript", "google-python", "google-go", "kubeless-python", "kubeless-nodejs", "knative-docker", "openwhisk-java-maven", "openwhisk-nodejs", "openwhisk-php", "openwhisk-python", "openwhisk-ruby", "openwhisk-swift", "spotinst-nodejs", "spotinst-python", "spotinst-ruby", "spotinst-java8", "twilio-nodejs", "aliyun-nodejs", "plugin", "hello-world".

プロジェクト作成コマンドを実行すると自動で以下が作成されました。

❯ tree .
.
├── Makefile
├── hello
│   └── main.go
├── serverless.yml
└── world
    └── main.go

今回は、前回の記事で作成したコードを利用するので、サンプルで自動作成された hello/worldディレクトリを削除しました。 今回用意したディレクトリ構成は以下になります。Echoで ヘルスチェックのAPIを実装しており、ソースコードはこちらのGitHubにあります。

github.com

❯ tree .
.
├── Makefile
├── README.md
├── api
│   └── healthcheck
│       └── healthcheck.go
├── bin
│   └── main
├── conf
│   └── config.go
├── go.mod
├── go.sum
├── handler
│   └── auth
│       └── auth.go
├── main.go
└── serverless.yml
  • serverless.ymlも自動で作成されますが、以下のとおりに変更しました。
    • "追記" の部分を追加しています。自動作成されたコメント部分は削除しています。
    • pluginsserverless-offline を指定します
    • customuseDocker で trueを指定します
      • 設定しないと以下のエラーで apiを実行できませんでした
        • GET /v1/api/healthcheck (λ: main) ✖ Unhandled exception in handler 'main'. ✖ ENOENT: no such file or directory, open '.go'

    • timeout はデフォルト値では timeoutになってしまったので定義しました
    • 主な原因は、Lambda(go) のコンテナを起動するのに時間がかかるためでした
service: serverless-echo-lambda
frameworkVersion: '3'

# 追記
custom:
  serverless-offline:
    useDocker: true

provider:
  name: aws
  runtime: go1.x
  stage: v1 #追記
  timeout: 30 #追記

package:
  patterns:
    - '!./**'
    - ./bin/**

functions:
  main:
    handler: bin/main
    events:
      - http: # RESTAPI
          path: /api/{proxy+}
          method: get
# 追記
plugins:
  - serverless-offline

デプロイと動作確認

.PHONY: build clean deploy deploy-local

build:
    env GOARCH=amd64 GOOS=linux go build -ldflags="-s -w" -o bin/main ./main.go

clean:
    rm -rf ./bin

deploy-local: build
    sls offline

deploy: clean build
    sls deploy --verbose
  • ローカル環境を起動します。
    • sls offline
    • goのコードをコンパイルしてデプロイする場合は make deploy-local でもOK
❯ sls offline


Starting Offline at stage v1 (us-east-1)

Offline [http for lambda] listening on http://localhost:3002
Function names exposed for local invocation by aws-sdk:
           * main: serverless-echo-lambda-v1-main

   ┌────────────────────────────────────────────────────────────────────────┐
   │                                                                        │
   │   GET | http://localhost:3000/v1/api/{proxy*}                          │
   │   POST | http://localhost:3000/2015-03-31/functions/main/invocations   │
   │                                                                        │
   └────────────────────────────────────────────────────────────────────────┘

Server ready: http://localhost:3000 🚀
  • ヘルスチェックAPIをコールする
❯ curl http://localhost:3000/v1/api/healthcheck
{"status":200,"message":"Success to connect echo"}

リクエストを投げると Dockerイメージを起動してからリクエストを受け付けてくれました。リクエスト終了後に確認したら割とすぐにコンテナは停止していました。

❯ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

❯ curl http://localhost:3000/v1/api/healthcheck
{"status":200,"message":"Success to connect echo"}%

❯ docker ps
CONTAINER ID   IMAGE                 COMMAND                  CREATED         STATUS         PORTS                     NAMES
8179a26361e2   lambci/lambda:go1.x   "/var/runtime/aws-la…"   4 seconds ago   Up 3 seconds   0.0.0.0:53718->9001/tcp   great_grothendieck
  • APIGWの機能はServerlessFrameworkで再現してくれています。
    • serverlessFrameworkが 3000番Listenして apigw --> lambdaコンテナに接続されています
❯ sudo lsof -i -P | grep "LISTEN" | grep 3000
node      44358       hidetoshi   32u  IPv6 0x8764e5xxxxxxxxxx      0t0    TCP localhost:3000 (LISTEN)

さいごに

今回は、ServerlessFrameworkで Client --> APIGW --> Lambda(Echo) の環境をローカルに再現しました。 ローカルでAWS環境を再現して開発できるのはとても便利ですね。

APIGateway(HTTP API) + Lambda環境に echo(Goのweb Framework) を ServerlessFramework でデプロイする

はじめに

前回の記事で APIGatewayの REST API で EchoをAWS Lambdaで簡単なAPIをデプロイしました。 今回は、APIGatewayの HTTP API に変更します。差分について記述していきます。

yhidetoshi.hatenablog.com

変更部分

main.goのみを記載します。他のコードは前回の記事に記載しています。

  • main.go
package main

import (
    "context"
    "log"
    "yhidetoshi/go-echo-lambda/api/healthcheck"
    "yhidetoshi/go-echo-lambda/handler/auth"

    "github.com/aws/aws-lambda-go/events"
    "github.com/aws/aws-lambda-go/lambda"
    echoadapter "github.com/awslabs/aws-lambda-go-api-proxy/echo"

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

//var echoLambda *echoadapter.EchoLambda   // APIGW(REST)
var echoLambda *echoadapter.EchoLambdaV2 // APIGW(HTTP) <--- V2 を利用する

func init() {
    log.Printf("echo cold start")

    e := echo.New()
    e.Use(middleware.Recover())
    e.Use(auth.BasicAuth())
    e.GET("/api/healthcheck", healthcheck.Healthcheck)

    //echoLambda = echoadapter.New(e) // APIGW(REST)
    echoLambda = echoadapter.NewV2(e) // APIGW(HTTP) <--- V2を利用する
}

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

//func Handler(ctx context.Context, req events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { // APIGW(REST)
func Handler(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) { // APIGW(HTTP) // V2HTTPRequest と V2HTTPResponseを利用する
    return echoLambda.ProxyWithContext(ctx, req)
}

変更した部分の V2をそれぞれのライブラリで見てみると、APIGatewayの HTTP APIに対応している事がわかりました。

echoadapter "github.com/awslabs/aws-lambda-go-api-proxy/echo" から抜粋

aws-lambda-go-api-proxy/adapterv2.go at fb2efb1b553c23556ff79b843e2c11fa989db78f · awslabs/aws-lambda-go-api-proxy · GitHub

// EchoLambdaV2 makes it easy to send API Gateway proxy V2 events to a echo.Echo.
// The library transforms the proxy event into an HTTP request and then
// creates a proxy response object from the http.ResponseWriter
type EchoLambdaV2 struct {
    core.RequestAccessorV2

    Echo *echo.Echo
}

// NewV2 creates a new instance of the EchoLambda object.
// Receives an initialized *echo.Echo object - normally created with echo.New().
// It returns the initialized instance of the EchoLambdaV2 object.
func NewV2(e *echo.Echo) *EchoLambdaV2 {
    return &EchoLambdaV2{Echo: e}
}

// Proxy receives an API Gateway proxy V2 event, transforms it into an http.Request
// object, and sends it to the echo.Echo for routing.
// It returns a proxy response object generated from the http.ResponseWriter.
func (e *EchoLambdaV2) Proxy(req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
    echoRequest, err := e.ProxyEventToHTTPRequest(req)
    return e.proxyInternal(echoRequest, err)
}

// ProxyWithContext receives context and an API Gateway proxy V2 event,
// transforms them into an http.Request object, and sends it to the echo.Echo for routing.
// It returns a proxy response object generated from the http.ResponseWriter.
func (e *EchoLambdaV2) ProxyWithContext(ctx context.Context, req events.APIGatewayV2HTTPRequest) (events.APIGatewayV2HTTPResponse, error) {
    echoRequest, err := e.EventToRequestWithContext(ctx, req)
    return e.proxyInternal(echoRequest, err)
}

github.com/aws/aws-lambda-go/events から抜粋

aws-lambda-go/apigw.go at 67965281f13ade83c809a78acdac095e84d701fd · aws/aws-lambda-go · GitHub

// APIGatewayV2HTTPRequest contains data coming from the new HTTP API Gateway
type APIGatewayV2HTTPRequest struct {
    Version               string                         `json:"version"`
    RouteKey              string                         `json:"routeKey"`
    RawPath               string                         `json:"rawPath"`
    RawQueryString        string                         `json:"rawQueryString"`
    Cookies               []string                       `json:"cookies,omitempty"`
    Headers               map[string]string              `json:"headers"`
    QueryStringParameters map[string]string              `json:"queryStringParameters,omitempty"`
    PathParameters        map[string]string              `json:"pathParameters,omitempty"`
    RequestContext        APIGatewayV2HTTPRequestContext `json:"requestContext"`
    StageVariables        map[string]string              `json:"stageVariables,omitempty"`
    Body                  string                         `json:"body,omitempty"`
    IsBase64Encoded       bool                           `json:"isBase64Encoded"`
}

ServerlessFramework

www.serverless.com

  • serverless.yamlの変更部分
    • 全体は前回の記事に記載しています
functions:
  GoEchoLambda:
    handler: main
    role: GoEchoLambda
    timeout: 10
    description: go echo lambda test 
    memorySize: 128
    events:
      - httpApi:  # httpApi に変更する
      #- http:
          path: /api/{proxy+}
          method: any
          #integration: lambda

動作確認

❯ curl -u test:pass https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/api/healthcheck
{"status":200,"message":"Success to connect echo"

さいごに

今回は、前回の記事においてAPIGatewayの REST API ではなく HTTP API を使うように変更しました。 HTTP APIでも利用できる事を確認できました!

ソースコード

github.com