虎の穴開発室ブログ

虎の穴ラボ株式会社所属のエンジニアが書く技術ブログです

MENU

Deno を AWS Lambda 関数 で動かす ~ GitHub Actions で マイグレーションまで自動化 ~

皆さんこんにちは、急に寒くなりましたね。おっくんです。

虎の穴ラボ Advent Calendar 2021 - QiitaDeno | Advent Calendar 2021 - Qiitaの8日目の記事です。

qiita.com

qiita.com

7日目は、はっとりさんによるコンテナ開発に必須! VSCode拡張機能 Remote - Containers
@access3151fq さんによる【Deno】標準ライブラリを使ってテスタブルなサーバーを書くでした。
こちらもぜひご覧ください。

Deno の本番運用に向けて、最近はいろいろ試しています。
今回は、AWS Lambda 関数 で Deno を動かし、簡単な基本的なデータの CRUD 処理が動作する REST API を作成します。
併せて、データベースのマイグレーションなどの操作もすべて Deno で行い、Github Actions で自動化します。

参考

実装と構築

今回の REST API の作成に当たっては、以下の段階を踏みながら説明します。

  1. AWS Lambda 関数 用のコンテナイメージの選定
  2. AWS Lambda 関数 用のコンテナイメージの作成と簡単な実装
  3. AWS Lambda 関数 用のコンテナイメージのデプロイ
  4. API Gateway を調整
  5. AWS Lambda 関数のパスパラメータ対応
  6. データベースの利用と、マイグレーション
  7. AWS Lambda 関数 と データベース の連携
  8. GitHub Actions で、マイグレーション
  9. 本実装
  10. 動作確認
  11. AWS Lambda 関数 を調整

それでは、順を追って解説します。

AWS Lambda 関数 用のコンテナイメージの選定

AWS Lambda 関数 には、コードだけではなくコンテナイメージとしてのデプロイも可能です。

AWS - コンテナイメージを使用して Go Lambda 関数をデプロイするには、
ベースの Docker イメージとして、public.ecr.aws/lambda/provided:al2 を使用し、Go アプリケーションのデプロイについて書かれています。

docs.aws.amazon.com

この例に則り、public.ecr.aws/lambda/provided:al2 をベースとした Deno アプリケーションのコンテナを作成しデプロイできれば、動作させられるはずです。

そのうえで、 deno.land/x で、lambda で検索すると、 deno on AWS Lambdaというものが公開されています。

deno.land/xで公開されているものは、ライブラリ群だけですが、GitHub の hayd/deno-lambdaでは、Docker コンテナの利用例が公開されています。
この Docker イメージは、public.ecr.aws/lambda/provided:al2 をベースに作成されています。

github.com

念のため、docker history でも確認してみます。

$ docker pull public.ecr.aws/lambda/provided:al2
$ docker pull hayd/deno-lambda
docker history public.ecr.aws/lambda/provided:al2 --no-trunc
IMAGE                                                                     CREATED      CREATED BY                                                                                                           SIZE      COMMENT
sha256:b228d8c47d2feb64236c056967f90f61a550b162473c7673a2cb7d4b84181e7b   9 days ago   ENTRYPOINT [ "/lambda-entrypoint.sh" ]                                                                               0B
<missing>                                                                 9 days ago   ENV LAMBDA_RUNTIME_DIR=/var/runtime                                                                                  0B
<missing>                                                                 9 days ago   ENV LAMBDA_TASK_ROOT=/var/task                                                                                       0B
<missing>                                                                 9 days ago   ENV LD_LIBRARY_PATH=/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib   0B
<missing>                                                                 9 days ago   ENV PATH=/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin                                                        0B
<missing>                                                                 9 days ago   ENV TZ=:/etc/localtime                                                                                               0B
<missing>                                                                 9 days ago   ENV LANG=en_US.UTF-8                                                                                                 0B
<missing>                                                                 9 days ago   WORKDIR /var/task                                                                                                    0B
<missing>                                                                 9 days ago   ADD file:a255bd5066e818e03b2981273d1021319564f22f1802f433259f39f6327858a5 /                                          8.2MB
<missing>                                                                 9 days ago   ADD file:58f1486b5ac294f83f3b0fbc58ca7662258fa652983f174cbee214581279c2dd /                                          397B
<missing>                                                                 9 days ago   ADD file:8e10ebd36716b0298cef45a9b36d1e72a92a0909ce3cf5c270d3d3dc2a382848 /                                          816kB
<missing>                                                                 9 days ago   ADD file:971324dd0103ce69ea34f36783c652aaf43c7ff062753a39f386a214ed536d89 /                                          295MB
<missing>                                                                 9 days ago   ARCHITECTURE amd64                                                                                                   0B

docker history hayd/deno-lambda --no-trunc
IMAGE                                                                     CREATED      CREATED BY                                                            SIZE      COMMENT
sha256:21e02867e6a08807599c40950a505e1f1ee7ae07152c6cac499f86286916f5c4   8 days ago   /bin/sh -c yum install -q -y unzip  [省略]   95.2MB
<missing>                                                                 8 days ago   /bin/sh -c #(nop) ADD file:53644fddbb3c6351485a12f20096d1a6d31dd683e1732b89c9926e09da83fd0c in /var/runtime/bootstrap  9.05kB
<missing>                                                                 8 days ago   /bin/sh -c #(nop)  ENV DENO_INSTALL_ROOT=/usr/local                                                                    0B
<missing>                                                                 8 days ago   /bin/sh -c #(nop)  ENV DENO_DIR=.deno_dir                                                                              0B
<missing>                                                                 8 days ago   /bin/sh -c #(nop)  ENV DENO_VERSION=1.16.0                                                                             0B
<missing>                                                                 9 days ago   ENTRYPOINT [ "/lambda-entrypoint.sh" ]                                                                                 0B
<missing>                                                                 9 days ago   ENV LAMBDA_RUNTIME_DIR=/var/runtime                                                                                    0B
<missing>                                                                 9 days ago   ENV LAMBDA_TASK_ROOT=/var/task                                                                                         0B
<missing>                                                                 9 days ago   ENV LD_LIBRARY_PATH=/var/lang/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib:/opt/lib     0B
<missing>                                                                 9 days ago   ENV PATH=/var/lang/bin:/usr/local/bin:/usr/bin/:/bin:/opt/bin                                                          0B
<missing>                                                                 9 days ago   ENV TZ=:/etc/localtime                                                                                                 0B
<missing>                                                                 9 days ago   ENV LANG=en_US.UTF-8                                                                                                   0B
<missing>                                                                 9 days ago   WORKDIR /var/task                                                                                                      0B
<missing>                                                                 9 days ago   ADD file:a255bd5066e818e03b2981273d1021319564f22f1802f433259f39f6327858a5 /                                            8.2MB
<missing>                                                                 9 days ago   ADD file:58f1486b5ac294f83f3b0fbc58ca7662258fa652983f174cbee214581279c2dd /                                            397B
<missing>                                                                 9 days ago   ADD file:8e10ebd36716b0298cef45a9b36d1e72a92a0909ce3cf5c270d3d3dc2a382848 /                                            816kB
<missing>                                                                 9 days ago   ADD file:971324dd0103ce69ea34f36783c652aaf43c7ff062753a39f386a214ed536d89 /                                            295MB
<missing>                                                                 9 days ago   ARCHITECTURE amd64                                                                                                     0B

https://github.com/hayd/deno-lambda/blob/master/docker/base.dockerfileに記載されている、
public.ecr.aws/lambda/provided:al2 への操作が記載されているのが確認できます。 この、hayd/deno-lambda を使って、AWS Lambda 関数 へのデプロイを進めていきます。

AWS Lambda 関数 用のコンテナイメージの作成と簡単な実装

選定したhayd/deno-lambdaを使用して、手始めにドキュメントを参考にしながら動作確認を行います。

後ほど、 GitHub Actions を使うので、リポジトリを作成し、任意のディレクトリにクローンしてください。
次の Dockerfile と、hello.ts を作成します。

[Dockerfile]

FROM hayd/deno-lambda:1.6.1

COPY hello.ts .
RUN deno cache hello.ts


CMD ["hello.handler"]

[hello.ts]

// ソースは、https://github.com/hayd/deno-lambda/blob/master/example-docker-container/hello.ts にあるものそのまま
import {
  APIGatewayProxyEventV2,
  APIGatewayProxyResultV2,
  Context,
} from "https://deno.land/x/lambda/mod.ts";

// deno-lint-ignore require-await
export async function handler(
  event: APIGatewayProxyEventV2,
  context: Context
): Promise<APIGatewayProxyResultV2> {
  return {
    statusCode: 200,
    headers: { "content-type": "text/html;charset=utf8" },
    body: `Welcome to deno ${Deno.version.deno} 🦕`,
  };
}

続けて次の操作を行います。

$ docker build -t tmp_build . # <= tmp_build は任意のタグ名でOKです
$ docker run -it -p 8080:8080 tmp_build

コンテナが起動しますので、任意の REST クライアントで、http://localhost:8080/2015-03-31/functions/function/invocations{"payload":"[この部分はなんでもいい]"} を JSON として載せた POST リクエストを送って下さい f:id:toranoana-lab:20211122181517p:plain

curl では、次のようになります。

curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"payload":"hello world!"}'

後々作成するアプリケーションは hello に当たるような最初期の確認用途ではなくなるので、この時点で Dockerfile を修正し、app.tsをエントリポイントにするようにしておきましょう。

  • 実行対象のスクリプト名を変更
  • コンテナ内へのファイルコピーを hello.ts から、すべてのファイルに変更

変更した、Dockerfile は次のようになります。

FROM hayd/deno-lambda:1.6.1

COPY . .
RUN deno cache app.ts

CMD ["app.handler"]

以降は、この Dockerfile を使用します。

AWS Lambda 関数 用のコンテナイメージのデプロイ

ローカル環境で、Docker コンテナを使い AWS Lambda 関数 としての実行を確認できましたので、実際に AWS Lambda 関数 にデプロイを行います。

ここでは、次のことを順に行います。

  • AWS IAM ユーザーを作成
  • AWS ECR(Elastic Container Registry) にリポジトリを作成
  • GitHub に Actions secrets を登録
  • GitHub Actions を設定(ECR への push まで)
  • AWS Lambda 関数の作成
  • GitHub Actions を設定(AWS Lambda 関数の更新まで)

では、始めていきます。

AWS IAM ユーザーを作成

AWS IAM に新たなユーザーを作成し、次の 2 つのポリシーを付与してください。

  • AmazonEC2ContainerRegistryPowerUser
  • AWSLambda_FullAccess

ユーザー名はなんでも構いませんが、今回は、lambda-deploy-user としました。

f:id:toranoana-lab:20211122181519p:plain

作成時に発行される、キーとシークレットキーは後ほど使うので控えておきます。

AWS ECR(Elastic Container Registry) にリポジトリを作成

AWS ECR は、コンテナイメージの保管先になります。 こちらに今回使用するリポジトリを作成します。 名前はなんでも構いません。むやみに公開する必要も無いと思いますので、プライベートにだけしておきましょう。 今回は、lambda-deno としました。

f:id:toranoana-lab:20211122181515p:plain

GitHub に Actions secrets を登録

GitHub に Actions secrets を登録します。 ここでは、4 つのシークレットを登録します。

  • AWS_ACCESS_KEY_ID:IAM ユーザー作成時のキー
  • AWS_SECRET_ACCESS_KEY:IAM ユーザー作成時のシークレットキー
  • AWS_ECR_REPO_NAME:AWS ECR に作成したリポジトリ名(今回は、lambda-deno)
  • LAMBDA_FUNCTION_NAME:後ほど作成する AWS Lambda 関数名(今回は、lambda-deno-funcとします)

GitHub Actions を設定(ECR への push まで)

用意が整ったので、GitHub Actions を使用し、ECR にコンテナイメージを push します。 .github\workflows\deploy.yml を作成します。

[.github\workflows\deploy.yml]

name: AWS ECR Image Push & Lambda function deploy
on:
  push:
    tags:
      - deploy-*

jobs:
  build-and-push:
    runs-on: ubuntu-18.04
    timeout-minutes: 30

    steps:
      # ソースをチェックアウト
      - uses: actions/checkout@v1

      # AWS クレデンシャルの設定
      - name: Configure AWS credentials from Test account
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      # ECR へログイン
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      # ECR へ Push
      - name: AWS ECR push
        id: ecr-push
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPO_NAME }}
        run: |
          IMAGE_TAG=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g")
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
        # echo "::set-output name=hoge::fuga すると次以降のstepで step.ecr-push.hoge.outputs.fuga の形式で使えるので出力しておく

これを作成し、次のように操作すると GitHub Actiins が動きます。

git add -A
git commit -m "GitHub Actions 設定を追加"
git push
git tag deploy-0.0.1
git push --tags

GitHub Actions 画面で成功できていることを確認したら、ECR のリポジトリを確認します。 イメージが登録されています。(キャプチャしたときは何回か実行した後でした。)

f:id:toranoana-lab:20211122181522p:plain

AWS Lambda 関数の作成

Docker イメージが、AWS ECR に登録されたので、AWS Lambda 関数 を作成します。 次の内容を設定して、作成します。

  • 「コンテナイメージ」を選択
  • 先に決めておいた、lambda-deno-func を関数名に指定
  • コンテナイメージ URI に「イメージを参照」で、ECR に登録したイメージを指定

f:id:toranoana-lab:20211122181525p:plain

続けて、「トリガーを追加」で、「API GateWay」を選択し、次の設定を行い、追加します。

  • API タイプ:HTTP API を選択
  • セキュリティ:オープンを選択(本番運用の際は検討が必要でしょう。)

f:id:toranoana-lab:20211122181528p:plain

作成できると、API エンドポイントが記載されているので、アクセスしてみます。

ローカルで、REST クライアントを使い確認したのと同じ出力が確認できます。

GitHub Actions を設定(AWS Lambda 関数の更新まで)

AWS Lambda 関数の動作確認まで出来たので、GitHub Actions で、AWS Lambda 関数 の更新まで行ってみます。

[.github\workflows\deploy.yml(AWS Lambda 関数の更新まで)]

name: AWS ECR Image Push & Lambda function deploy
on:
  push:
    tags:
      - deploy-*

jobs:
  build-and-push:
    runs-on: ubuntu-18.04
    timeout-minutes: 30

    steps:
      # ソースをチェックアウト
      - uses: actions/checkout@v1

      # AWS クレデンシャルの設定
      - name: Configure AWS credentials from Test account
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      # ECR へログイン
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      # ECR へ Push
      - name: AWS ECR push
        id: ecr-push
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPO_NAME }}
        run: |
          IMAGE_TAG=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g")
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"
      # echo "::set-output name=hoge::fuga すると次以降のstepで step.ecr-push.hoge.outputs.fuga の形式で使えるので出力しておく
      # AWS Lambda 関数の更新  <= 追加部分
      - name: Lambda function deploy
        id: lambda-function-deploy
        env:
          LAMBDA_FUNCTION_NAME: ${{ secrets.LAMBDA_FUNCTION_NAME }}
          LAMBDA_CONTAINER_IMAGE: ${{ steps.ecr-push.outputs.image }}
        run: |
          echo $LAMBDA_FUNCTION_NAME
          echo $LAMBDA_CONTAINER_IMAGE
          aws lambda update-function-code --function-name $LAMBDA_FUNCTION_NAME --image-uri $LAMBDA_CONTAINER_IMAGE

併せて、デプロイしたものとの差がわかるように app.ts は任意の書き換えを行っておくといいでしょう。 作成できたら、再度 push します。

git add -A
git commit -m "GitHub Actions で AWS Lambda 関数の更新設定を追加"
git push
git tag deploy-0.0.2
git push --tags

アクセスしてみて、変更内容が反映されていれば OK です。

API Gateway を調整

現在は、/default/lambda-deno-func のような関数名のパスでのアクセスしかコンテナ側に転送してくれません。 任意のパスでもアクセスできるように、API Gateway の調整を行います。

lambda-deno-func のリソースを削除します。

f:id:toranoana-lab:20211122181535p:plain

新しいリソースを作成します。

  • リソース名:任意
  • リソースパス:{}でくくって+をつければパス名は任意 {param+} のようになればいい。

f:id:toranoana-lab:20211122181533p:plain

ANY メソッドの作成で、先に作成している AWS Lambda 関数 lambda-deno-func を指定する。

f:id:toranoana-lab:20211122181530p:plain

ここまで出来たら API をデプロイします。

f:id:toranoana-lab:20211122181537p:plain

すると、/default/hoge/fuga/default/foo/barのように任意のパスにアクセスしてもコンテナから応答が返ってくるようになります。

AWS Lambda 関数のパスパラメータ対応

アプリケーションもパスパラメータに対応するようにします。 今回は、URLPattern を使用し実装します。

実装内容は次の通りです。 [app.ts(パスパラメータ対応)]

import {
  APIGatewayProxyEvent,
  Context,
} from "https://deno.land/x/lambda/mod.ts";

export async function handler(event: APIGatewayProxyEvent, context: Context) {
  const path = event.path;
  const method = event.httpMethod;
  const body = event.body;

  const message: {
    path: string,
    param: string | boolean | undefined,
  } = {
    path: "",
    param: "",
  };

  if (
    method === "GET" &&
    new URLPattern({ pathname: "/a/:id" }).test({ pathname: path })
  ) {
    message.path = "GroupA";
    message.param = new URLPattern({ pathname: "/a/:id" })?.exec({
      pathname: path,
    })?.pathname?.groups?.id;
  } else if (
    method === "POST" &&
    new URLPattern({ pathname: "/b/:id" }).test({ pathname: path })
  ) {
    message.path = "GroupB";
    message.param = new URLPattern({ pathname: "/b/:id" })?.exec({
      pathname: path,
    })?.pathname?.groups?.id;
  } else {
    message.path = "GroupC";
  }

  return {
    body: `path:${path} method:${method} message.path = ${message.path} message.param = ${message.param}`,
    headers: { "content-type": "text/html;charset=utf8" },
    statusCode: 200,
  };
}

実装できたら、AWS Lambda 関数を更新します。 REST クライアントを使用して、動作確認します。 次のように振り分けされます。

メソッド パス 結果
GET /a/1234 path:/a/1234 method:GET message.path = GroupA message.param = 1234
POST /b/5678 path:/a/5678 method:POST message.path = GroupB message.param = 5678
- - path:[任意] method:[任意] message.path = GroupC message.param =

データベースの利用と、マイグレーション

パスパラメータに対応したので、引き続き、データベースとの接続を行います。

ローカルでの開発用に次のファイルを用意します。

  • docker-compose.yml
  • MigrationDockerfile
  • .env

[docker-compose.yml]

version: "3"
services:
  migration:
    build:
      dockerfile: ./MigrationDockerfile
    networks:
      - default
    privileged: true
    entrypoint:
      - /sbin/init
    volumes:
      - .:/usr/src/app:cached
    tty: true
  db:
    image: mysql:5.7
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
    ports:
      - "${MYSQL_PORT:-3306}:3306"
    volumes:
      - ./db/mysql_data:/var/lib/mysql
    environment:
      - MYSQL_ROOT_PASSWORD=${MYSQL_ROOT_PASSWORD:-password_root}

[MigrationDockerfile]

# Deno 1.15を使います、後ほど使う dotenv がDeno 1.16 環境で上手く動かなかったため
FROM denoland/deno:centos-1.15.0


RUN yum update -y && \
    yum install -y wget which systemd-sysv crontabs && \
    yum install -y git tar gcc gcc-c++ make openssl-devel zlib-devel sqlite sqlite-devel bzip2 readline-devel mysql-devel mysql redis


RUN mkdir /usr/src/app
WORKDIR /usr/src/app

[.env]

MYSQL_PORT=3309
DATABASE_HOST=db
DATABASE_USER=[任意]
DATABASE_PASSWORD=[任意]
DATABASE_NAME=test_app

作成できたら、次の操作でコンテナに入り nessie を導入します。

nessieは、Deno 向けのマイグレーションツールです。

github.com

$ docker-compose up -d
$ docker compose exec migrate bash

$ deno install --unstable --allow-net=db:3306,deno.land --allow-env --allow-read=. --allow-write=nessie.config.ts,db -f  https://deno.land/x/nessie/cli.ts
$ nessie init --dialect mysql

nessie.config.ts が作成されているので、次のように書き換えます。

[nessie.config.ts]

import "https://deno.land/x/dotenv/load.ts";
import {
  ClientMySQL,
  NessieConfig,
} from "https://deno.land/x/nessie@2.0.1/mod.ts";

const client = new ClientMySQL({
  hostname: Deno.env.get("DATABASE_HOST"),
  port: 3306,
  username: Deno.env.get("DATABASE_USER"),
  password: Deno.env.get("DATABASE_PASSWORD"),
  db: Deno.env.get("DATABASE_NAME"),
});

/** This is the final config object */
const config: NessieConfig = {
  client,
  migrationFolders: ["./db/migrations"],
  seedFolders: ["./db/seeds"],
};

export default config;

追々、RDS にマイグレーションすることもあるのでパラメーターは一通り、環境変数にしておきます。 続けて、マイグレーションファイルを作りましょう。

$  nessie make:migration create_items
Created migration /usr/src/app/db/migrations/[数字列]_create_items.ts

db/migrations/[数字列]_create_users.ts が作成されるので、次のように書き換えます

[db/migrations/[数字列]_create_users.ts(修正後))]

import {
  AbstractMigration,
  Info,
  ClientMySQL,
} from "https://deno.land/x/nessie@2.0.1/mod.ts";

export default class extends AbstractMigration<ClientMySQL> {
  /** Runs on migrate */
  async up(info: Info): Promise<void> {
    await this.client.query(
      "CREATE TABLE items (id int AUTO_INCREMENT, name varchar(256), INDEX(id))"
    );
  }

  /** Runs on rollback */
  async down(info: Info): Promise<void> {
    await this.client.query("DROP TABLE items");
  }
}

データベースの作成は、nessie が見てくれない部分です。 先にデータベースを作っておきます。 続けて、マイグレーションを行います。

nessie migrate

これで、テーブルが作成されました。

AWS Lambda 関数 と データベース の連携

それでは、AWS Lambda 関数の実装に戻ります。 AWS Lambda 関数用コンテナのアプリケーション から データベースに接続する仮実装を行います。

データベースの接続には、denodrivers/mysqlを使用します。 github.com

[app.ts(データベース接続仮実装)]

import "https://deno.land/x/dotenv@v2.0.0/load.ts";
import {
  APIGatewayProxyEvent,
  Context,
} from "https://deno.land/x/lambda/mod.ts";

import { Client } from "https://deno.land/x/mysql/mod.ts";
import { configLogger } from "https://deno.land/x/mysql/mod.ts";

// Item 型定義
interface Item {
  id: number;
  name: string;
}

// コネクションの作成
const connectionParam = {
  hostname: Deno.env.get("DATABASE_HOST") as string,
  username: Deno.env.get("DATABASE_USER") as string,
  password: Deno.env.get("DATABASE_PASSWORD") as string,
  db: Deno.env.get("DATABASE_NAME") as string,
};

const client = await new Client().connect(connectionParam);

export async function handler(event: APIGatewayProxyEvent, context: Context) {
  const path = event.path;
  const method = event.httpMethod;
  const body = event.body;

  const items: Item[] = await client.query(`select * from items`);

  const message: {
    path: string;
    param: string | boolean | undefined;
  } = {
    path: "",
    param: "",
  };

  if (method === "GET" && new URLPattern({ pathname: "/a/:id" }).test({ pathname: path })) {
    message.path = "GroupA";
    message.param = new URLPattern({ pathname: "/a/:id" })?.exec({
      pathname: path,
    })?.pathname?.groups?.id;
  } else if (method === "POST" && new URLPattern({ pathname: "/b/:id" }).test({ pathname: path })) {
    message.path = "GroupB";
    message.param = new URLPattern({ pathname: "/b/:id" })?.exec({
      pathname: path,
    })?.pathname?.groups?.id;
  } else {
    message.path = "GroupC";
    message.param = JSON.stringify(items)
  }

  return {
    body: `//path:${path} method:${method} message.path = ${message.path} message.param = ${message.param}`,
    headers: { "content-type": "text/html;charset=utf8" },
    statusCode: 200,
  };
}

すべてのリクエストで、データベースにアクセスし、パターンマッチできなかったときに結果を返します。

実行環境に mysql クライアントのインストールが必要なので、Dockerfile を修正します。

[Dockerfile]

FROM hayd/deno-lambda:1.16.0

# 必要そうなものをまとめて導入
RUN yum update -y && \
    yum install -y tar gcc gcc-c++ make openssl-devel zlib-devel bzip2 readline-devel mysql-devel mysql

COPY . .
RUN deno cache app.ts

CMD ["app.handler"]

docker compose で立ち上げた環境の docker ネットワークを確認し、AWS Lambda 関数のコンテナに --net オプションを設定し起動します。

$ docker network ls
NETWORK ID     NAME                                            DRIVER    SCOPE
dd6c1d0b5cd4   XXXXXXXXXXXXXXXXXXXXXXX_default                 bridge    local
bd5619c85f89   YYYYYYYYYYYYYYYYYYYYYYY_default                 bridge    local
5467fdcea349   ZZZZZZZZZZZZZZZZZZZZZZZ_default                 bridge    local <= これがdocker compose で立ち上げた環境の ネットワークとします

$ docker build -t tmp_build .
$ docker run -it -p 8080:8080 --net 5467fdcea349 tmp_build

REST クライアントで、アクセスすると、次のような結果が返ってきます。 f:id:toranoana-lab:20211122181540p:plain

レスポンスにデータが載って返ってくることを確認できます。

GitHub Actions で、マイグレーション

AWS Lambda 関数が、データベースに接続できることを確認したので、引き続き GitHub Actions で、マイグレーションを行います。

ここでは、次の順に進めます。

  • マイグレーション先のデータベースインスタンスを作成
  • GitHub Actions secrets を設定
  • GitHub Actions を修正
  • AWS Lambda 関数に 環境変数を設定

マイグレーション先のデータベースインスタンスを作成

ローカルの確認で、MySQL を使用したので AWS RDS も、MySQL を使用します。

  • データベース作成方法を選択:簡単に作成
  • エンジンのタイプ:MySQL
  • DB インスタンスサイズ:無料利用枠
  • DB インスタンス識別子:lambda-deno-db
  • マスターユーザー名:任意
  • パスワード:任意
  • パブリックアクセス:パブリックアクセス可能

エンドポイント、ユーザー名、パスワードを控えておきます。

GitHub Actions secrets を設定

次のキーを設定します。

  • DATABASE_HOST:RDS に作成したデータベースのエンドポイント
  • DATABASE_USER:RDS に作成したデータベースのユーザー
  • DATABASE_PASSWORD:RDS に作成したデータベースのパスワード
  • DATABASE_NAME:test_app

GitHub Actions を修正

.github/workflows/deploy.yml を修正し、データベースの作成と、データベースのマイグレーションを行うステップを追加します。 修正したのが次のものです。

[.github/workflows/deploy.yml(データベースの作成と、マイグレーションを追加)]

name: AWS ECR Image Push & Lambda function deploy
on:
  push:
    tags:
      - deploy-*

jobs:
  build-and-push:
    runs-on: ubuntu-18.04
    timeout-minutes: 30

    steps:
      # ソースをチェックアウト
      - uses: actions/checkout@v1

      # データベースを作成、マイグレーションを実行
      - name: Create database & migration
        id: create-database
        env:
          DATABASE_HOST: ${{ secrets.DATABASE_HOST  }}
          DATABASE_USER: ${{ secrets.DATABASE_USER  }}
          DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD  }}
          DATABASE_NAME: ${{ secrets.DATABASE_NAME  }}
        run: |
          echo [client] > ./my.cnf
          echo host = $DATABASE_HOST >> ./my.cnf
          echo user = $DATABASE_USER >> ./my.cnf
          echo password = $DATABASE_PASSWORD >> ./my.cnf
          cat ./my.cnf
          chmod 600 ./my.cnf
          mysql --defaults-file=./my.cnf -e "CREATE DATABASE IF NOT EXISTS $DATABASE_NAME"

          echo DATABASE_HOST=$DATABASE_HOST >> ./.env
          echo DATABASE_USER=$DATABASE_USER >> ./.env
          echo DATABASE_PASSWORD=$DATABASE_PASSWORD >> ./.env
          echo DATABASE_NAME=$DATABASE_NAME >> ./.env
          cat ./.env

          echo deno install --unstable --allow-net=$DATABASE_HOST:3306,deno.land --allow-env --allow-read=. --allow-write=nessie.config.ts,db -f  https://deno.land/x/nessie/cli.ts >> fullmigrate.sh
          echo nessie migrate >> fullmigrate.sh

          docker compose up -d migrate 
          docker compose exec migrate bash fullmigrate.sh

          rm -y ./my.cnf
          rm -y ./.env
          rm -y ./fullmigrate.sh

      # AWS クレデンシャルの設定
      - name: Configure AWS credentials from Test account
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ap-northeast-1

      # ECR へログイン
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      # ECR へ Push
      - name: AWS ECR push
        id: ecr-push
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: ${{ secrets.AWS_ECR_REPO_NAME }}
        run: |
          IMAGE_TAG=$(echo ${{ github.ref }} | sed -e "s#refs/tags/##g")
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          echo "::set-output name=image::$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG"

      # AWS Lambda 関数の更新  <= 追加部分
      - name: Lambda function deploy
        id: lambda-function-deploy
        env:
          LAMBDA_FUNCTION_NAME: ${{ secrets.LAMBDA_FUNCTION_NAME }}
          LAMBDA_CONTAINER_IMAGE: ${{ steps.ecr-push.outputs.image }}
        run: |
          echo $LAMBDA_FUNCTION_NAME
          echo $LAMBDA_CONTAINER_IMAGE
          aws lambda update-function-code --function-name $LAMBDA_FUNCTION_NAME --image-uri $LAMBDA_CONTAINER_IMAGE

ポイントになる、Create database & migration ステップだけ切り出します。

      # データベースを作成、マイグレーションを実行
      - name: Create database & migration
        id: create-database
        env:
          DATABASE_HOST: ${{ secrets.DATABASE_HOST  }}
          DATABASE_USER: ${{ secrets.DATABASE_USER  }}
          DATABASE_PASSWORD: ${{ secrets.DATABASE_PASSWORD  }}
          DATABASE_NAME: ${{ secrets.DATABASE_NAME  }}
        run: |
         # 環境変数を参照し、my.cnf を作成、データベースが作成されていない時に作成する
          echo [client] > ./my.cnf
          echo host = $DATABASE_HOST >> ./my.cnf
          echo user = $DATABASE_USER >> ./my.cnf
          echo password = $DATABASE_PASSWORD >> ./my.cnf
          chmod 600 ./my.cnf
          mysql --defaults-file=./my.cnf -e "CREATE DATABASE IF NOT EXISTS $DATABASE_NAME"

          # 環境変数を参照し、.env を作成、マイグレーションを行うコンテナで使用する
          echo DATABASE_HOST=$DATABASE_HOST >> ./.env
          echo DATABASE_USER=$DATABASE_USER >> ./.env
          echo DATABASE_PASSWORD=$DATABASE_PASSWORD >> ./.env
          echo DATABASE_NAME=$DATABASE_NAME >> ./.env

          # 環境変数を参照し、接続先ホストを取得して nessie をインストールするスクリプトを作成
          echo deno install --unstable --allow-net=$DATABASE_HOST:3306,deno.land --allow-env --allow-read=. --allow-write=nessie.config.ts,db -f  https://deno.land/x/nessie/cli.ts >> fullmigrate.sh
          echo nessie migrate >> fullmigrate.sh

          # マイグレーション実行
          docker compose up -d migrate
          docker compose exec migrate bash fullmigrate.sh

          # 後でビルドするコンテナの中に含まれないように削除
      rm -y ./my.cnf
          rm -y ./.env
          rm -y ./fullmigrate.sh

やり方はかなり悩みましたが、my.cnf、 .env、fullmigrate.sh を作成し実行する・実行するときに使う形を取りました。 ローカルの開発時に.env を使用していることから互換性も考慮して、このようにしています。

ここまでできたら、改めて GitHub へ push します。

git add -A
git commit -m "GitHub Actions でデータベースをマイグレーション"
git push
git tag deploy-0.0.3
git push --tags

データベースにアクセスし、マイグレーションが行われていることを確認できていれば OK です。

AWS Lambda 関数に 環境変数を設定

アプリケーションが使用する次の 4 つの環境変数を、AWS Lambda 関数 に設定します。

  • DATABASE_HOST
  • DATABASE_USER
  • DATABASE_PASSWORD
  • DATABASE_NAME

f:id:toranoana-lab:20211122181547p:plain

本実装

ここまでで、GitHub Actions で、次のことができるようになりました。

  • データベースマイグレーション
  • AWS Lambda 関数用コンテナの登録
  • AWS Lambda 関数のデプロイ

ここでは、最終的に作成したコードを紹介します。 始めにディレクトリ構成から示します。

$ tree
.
├── app
│   ├── controller
│   │   ├── base_controller.ts            コントローラー機能の基礎部分を提供
│   │   ├── error_controller.ts            /items に対応しないリクエストの対応用コントローラ
│   │   └── items_controller.ts            /items に対応したコントローラー機能を提供
│   ├── model
│   │   ├── base.ts                        テーブル問い合わせ基礎機能を提供
│   │   └── item.ts                        items テーブルへの問い合わせ
│   └── query
│       └── item_query.ts                  items テーブル用クエリ作成を機能提供
├── app.ts
├── db
│   ├── migrations
│   │   └── 20211119084431_create_items.ts
│   ├── mysql_data
│   │   └── 以下ディレクトリは省略           .gitignore で管理対象から除外
│   └── seeds
├── deps.ts                                サードパーティモジュール管理
├── docker-compose.yml
├── Dockerfile
├── MigrationDockerfile
├── nessie.config.ts
├── router.ts                              ルーター機能を提供
├── type.d.ts                              複数ファイルで使用する型定義
└── util.ts                                便利関数登録先として使用

それぞれのファイルの詳細を確認します

deps.ts

deps.ts で外部依存しているライブラリ群を管理します。

[deps.ts]

export {
  APIGatewayProxyEvent,
  Context,
} from "https://deno.land/x/lambda@1.16.0/mod.ts";

export { Query, Where } from "https://deno.land/x/sql_builder@v1.9.1/mod.ts";
export { Client as MySqlClient } from "https://deno.land/x/mysql@v2.10.1/mod.ts";

app.ts

エントリポイントとなる app.ts は、各機能を分離した実装に切り替えたのでシンプルになります。

[app.ts]

import "https://deno.land/x/dotenv@v2.0.0/load.ts";
import { APIGatewayProxyEvent, Context } from "./deps.ts";
import { LambdaRequest } from "./type.d.ts";
import { router } from "./router.ts";
import { requestMethod } from "./util.ts";

export async function handler(event: APIGatewayProxyEvent, context: Context) {
  const request: LambdaRequest = {
    path: event.path,
    method: requestMethod(event.httpMethod),
    body: event.body,
  };
  return await router(request);
}

type.d.ts

複数のファイルで使用する型定義は、type.d.ts に分離しました。

[type.d.ts]

export type Methods = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";

interface LambdaRequest {
  method: Methods;
  path: string;
  body?: any;
}

interface Param {
  id?: number;
}

export { LambdaRequest, Param };

util.ts

便利関群の登録先として、util.ts を用意しました。 機能を複数作っていく場合には、utils ディレクトリを切りいくつかのファイルに分割する必要があります。

[util.ts]

import { Methods } from "./type.d.ts";

// string で渡された リクエストメソッドを定義したものでフィルタする
export const requestMethod = (src: string): Methods => {
  if ("GET" === src) {
    return src;
  } else if ("POST" === src) {
    return src;
  } else if ("PUT" === src) {
    return src;
  } else if ("DELETE" === src) {
    return src;
  } else if ("PATCH" === src) {
    return src;
  }

  throw new Error("Unkown method");
};

router.ts

ルーティングとコントローラーの呼び出しを担当します。

[router.ts]

import { LambdaRequest, Param } from "./type.d.ts";
import { ItemsController } from "./app/controller/items_controller.ts";
import { ErrorController } from "./app/controller/error_controller.ts";

const router = async (request: LambdaRequest) => {
  const errorController = new ErrorController(request);

  if (
    request.method === "GET" &&
    new URLPattern({ pathname: "/items/:id" }).test({ pathname: request.path })
  ) {
    const id = new URLPattern({ pathname: "/items/:id" })?.exec({
      pathname: request.path,
    })?.pathname?.groups?.id;

    if ("string" !== typeof id) {
      return errorController.error();
    }

    const param: Param = {
      id: Number(id),
    };

    const controller = new ItemsController(request, param);
    return controller.show();
  } else if (
    request.method === "PATCH" &&
    new URLPattern({ pathname: "/items/:id" }).test({ pathname: request.path })
  ) {
    const id = new URLPattern({ pathname: "/items/:id" })?.exec({
      pathname: request.path,
    })?.pathname?.groups?.id;

    if ("string" !== typeof id) {
      return errorController.error();
    }

    const param: Param = {
      id: Number(id),
    };

    const controller = new ItemsController(request, param);
    return controller.update();
  } else if (
    request.method === "DELETE" &&
    new URLPattern({ pathname: "/items/:id" }).test({ pathname: request.path })
  ) {
    const id = new URLPattern({ pathname: "/items/:id" })?.exec({
      pathname: request.path,
    })?.pathname?.groups?.id;

    if ("string" !== typeof id) {
      return errorController.error();
    }

    const param: Param = {
      id: Number(id),
    };

    const controller = new ItemsController(request, param);
    return controller.destroy();
  } else if (
    request.method === "GET" &&
    new URLPattern({ pathname: "/items" }).test({ pathname: request.path })
  ) {
    const controller = new ItemsController(request);
    return controller.index();
  } else if (
    request.method === "POST" &&
    new URLPattern({ pathname: "/items" }).test({ pathname: request.path })
  ) {
    const controller = new ItemsController(request);
    return controller.create();
  } else {
    return errorController.error();
  }
};

export { router };

app/controller/base_controller.ts

各コントローラーの基礎部分を提供します。

[app/controller/base_controller.ts]

import { LambdaRequest, Param } from "../../type.d.ts";

export class BaseController {
  readonly request: LambdaRequest;
  readonly param?: Param;

  constructor(request: LambdaRequest, param?: Param) {
    this.request = request;
    if (!!param) {
      this.param = param;
    }
  }
}

app/controller/error_controller.ts

該当のルーティングが存在しなかったときに対応するコントローラーです。

[app/controller/error_controller.ts]

import { BaseController } from "./base_controller.ts";

export class ErrorController extends BaseController {
  error() {
    return {
      headers: { "content-Type": "application/json; charset=utf-8" },
      statusCode: 404,
    };
  }
}

app/controller/items_controller.ts

/items のルーティングに対応した処理を行うコントローラーです。

[app/controller/items_controller.ts]

import { BaseController } from "./base_controller.ts";
import {
  createItem,
  deleteItemById,
  getItemById,
  getItems,
  updateItemById,
  Result,
} from "../model/item.ts";

export class ItemsController extends BaseController {
  async index() {
    const result = await getItems();
    return this.responce(result);
  }
  async show() {
    if (!this.param) {
      throw new Error("Not have param");
    }

    if (typeof this.param.id !== "number") {
      throw new Error("Not have param.id");
    }

    const result = await getItemById(this.param.id);
    return this.responce(result);
  }
  async create() {
    const body = JSON.parse(this.request.body);

    if (typeof body.name !== "string") {
      throw new Error("Not have name");
    }

    const result = await createItem(body);
    return this.responce(result);
  }
  async update() {
    if (!this.param) {
      throw new Error("Not have param");
    }

    if (typeof this.param.id !== "number") {
      throw new Error("Not have param.id");
    }

    const body = JSON.parse(this.request.body);

    if (typeof body.name !== "string") {
      throw new Error("Not have name");
    }

    const result = await updateItemById(this.param.id, body);
    return this.responce(result);
  }
  async destroy() {
    if (!this.param) {
      throw new Error("Not have param");
    }

    if (typeof this.param.id !== "number") {
      throw new Error("Not have param.id");
    }

    const result = await deleteItemById(this.param.id);
    return this.responce(result);
  }
  responce(result: Result) {
    return {
      body: JSON.stringify(result),
      headers: { "content-Type": "application/json; charset=utf-8" },
      statusCode: 200,
    };
  }
}

app/model/base.ts

データベースへの接続機能の基礎を提供します。

[app/model/base.ts]

import { MySqlClient } from "../../deps.ts";

// コネクションの作成
const connectionParam = {
  hostname: Deno.env.get("DATABASE_HOST") as string,
  username: Deno.env.get("DATABASE_USER") as string,
  password: Deno.env.get("DATABASE_PASSWORD") as string,
  db: Deno.env.get("DATABASE_NAME") as string,
};

const client = await new MySqlClient().connect(connectionParam);
export { client };

app/model/item.ts

items テーブルに対する CRUD 処理を担当する関数群です。

[app/model/item.ts]

import { client } from "./base.ts";
import {
  queryCreateItem,
  queryDeleteItemById,
  queryGetItemById,
  queryGetItemsAll,
  queryUpdateItemById,
} from "../query/item_query.ts";

export interface CreateItem {
  name: string;
}

// Item 型定義
export interface Item {
  id: number;
  name: string;
}

export interface Result {
  status: boolean;
  record?: Item;
  records?: Item[];
}

const getItems = async (): Promise<Result> => {
  const { rows } = (await client.execute(queryGetItemsAll())) as {
    fields: any;
    rows: Item[];
  };
  return { status: true, records: rows };
};

const getItemById = async (id: number | string): Promise<Result> => {
  const { rows } = (await client.execute(queryGetItemById(id))) as {
    fields: any;
    rows: Item[];
  };
  return rows.length == 1
    ? { status: true, record: rows[0] }
    : { status: false };
};

const updateItemById = async (
  id: number | string,
  param: { name: string },
): Promise<Result> => {
  const result = (await client.execute(queryUpdateItemById(id, param))) as {
    affectedRows: number;
    lastInsertId: number;
  };
  return result.affectedRows == 1
    ? { status: true, record: (await getItemById(id)).record }
    : { status: false };
};

const deleteItemById = async (id: number | string): Promise<Result> => {
  const result = (await client.execute(queryDeleteItemById(id))) as {
    affectedRows: number;
    lastInsertId: number;
  };
  return { status: result.affectedRows == 1 };
};

const createItem = async (item: CreateItem): Promise<Result> => {
  const result = (await client.execute(queryCreateItem(item))) as {
    affectedRows: number;
    lastInsertId: number;
  };
  return result.affectedRows == 1
    ? { status: true, record: (await getItemById(result.lastInsertId)).record }
    : { status: false };
};

export { createItem, deleteItemById, getItemById, getItems, updateItemById };

app/query/item_query.ts

items テーブルへの操作 SQL を作成する機能を提供する関数群です。 SQL の作成には、manyuanrong/sql-builderを使用しています。

github.com

[app/query/item_query.ts]

import { Query, Where } from "../../deps.ts";

const queryGetItemById = (id: number | string): string => {
  const builder = new Query();

  const sql = builder
    .table("items")
    .where(Where.field("id").eq(id))
    .select("*")
    .build();
  return sql;
};

const queryUpdateItemById = (
  id: number | string,
  param: { name: string }
): string => {
  const builder = new Query();

  const sql = builder
    .table("items")
    .where(Where.field("id").eq(id))
    .update(param)
    .build();
  return sql;
};

const queryDeleteItemById = (id: number | string): string => {
  const builder = new Query();

  const sql = builder
    .table("items")
    .where(Where.field("id").eq(id))
    .delete()
    .build();
  return sql;
};

const queryGetItemsAll = (): string => {
  const builder = new Query();

  const sql = builder.table("items").select("*").build();
  return sql;
};

const queryCreateItem = (param: { name: string }): string => {
  const builder = new Query();

  const sql = builder.table("items").insert(param).build();
  return sql;
};
export {
  queryCreateItem,
  queryDeleteItemById,
  queryGetItemById,
  queryGetItemsAll,
  queryUpdateItemById,
};

ここまでのファイル群により REST API サーバーアプリケーションを構成しています。 細かいところでの調整は必要に思いますが基本的に必要な機能は提供できているのでは無いでしょうか?

動作確認

改めて GitHub に新たにタグも設定して push を行いデプロイします。 コンソールでリクエストを行い基本的 CRUD 動作を確認すると、次のようになります。

# データの登録と確認
$ curl -XGET "https://[AWS Lamda 関数エンドポイント]/default/items"
{
  "status": true,
  "records": []
}

$ curl -XPOST "https://[AWS Lamda 関数エンドポイント]/default/items" -d '{"name":"AAA"}' -s |jq
{
  "status": true,
  "record": {
    "id": 22,
    "name": "AAA"
  }
}

$ curl -XPOST "https://[AWS Lamda 関数エンドポイント]/default/items" -d '{"name":"BBB"}' -s |jq
{
  "status": true,
  "record": {
    "id": 23,
    "name": "BBB"
  }
}

$ curl -XGET "https://[AWS Lamda 関数エンドポイント]/default/items" -s | jq
{
  "status": true,
  "records": [
    {
      "id": 22,
      "name": "AAA"
    },
    {
      "id": 23,
      "name": "BBB"
    }
  ]
}

$ curl -XGET "https://[AWS Lamda 関数エンドポイント]/default/items/22" -s | jq
{
  "status": true,
  "record": {
    "id": 22,
    "name": "AAA"
  }
}

# データの更新
$ curl -XPATCH "https://[AWS Lamda 関数エンドポイント]/default/items/22" -d '{"name":"AAA-2"}' -s |jq
{
  "status": true,
  "record": {
    "id": 22,
    "name": "AAA-2"
  }
}

$ curl -XGET "https://[AWS Lamda 関数エンドポイント]/default/items" -s | jq
{
  "status": true,
  "records": [
    {
      "id": 22,
      "name": "AAA-2"
    },
    {
      "id": 23,
      "name": "BBB"
    }
  ]
}

$ curl -XGET "https://[AWS Lamda 関数エンドポイント]/default/items/22" -s | jq
{
  "status": true,
  "record": {
    "id": 22,
    "name": "AAA-2"
  }
}

# データの削除
$ curl -XDELETE "https://[AWS Lamda 関数エンドポイント]/default/items/22" -s |jq
{
  "status": true
}

$ curl -XGET "https://[AWS Lamda 関数エンドポイント]/default/items/22" -s | jq
{
  "status": false
}

$ curl -XGET "https://[AWS Lamda 関数エンドポイント]/default/items" -s | jq
{
  "status": true,
  "records": [
    {
      "id": 23,
      "name": "BBB"
    }
  ]
}

AWS Lambda 関数 を調整

現在の使用している データベースは、接続元 IP を制限していない パブリックアクセス許可になっており、消して褒められる状態ではありません。 AWS Lambda 関数からデータベースへの接続を内部ネットワークを使用、データベースのパブリックアクセスを不可にして解説を終わりたいと思います。

AWS Lambda 関数のロールを修正

AWS Lambda 関数の設定から、実行ロールを開きます。

f:id:toranoana-lab:20211122181542p:plain

AWSLambdaVPCAccessExecutionRole ポリシーをロールに追加します。

AWS Lambda 関数の設定から、VPC の編集を開きます。

f:id:toranoana-lab:20211122181550p:plain

次のように設定します。

  • VPC:RDS の使用している VPC を指定
  • サブネット:一旦すべての AZ を指定
  • セキュリティグループ:一旦 VPC 内で有ればどこでも良いものとします。

f:id:toranoana-lab:20211122181545p:plain

パブリックアクセスを閉じる

RDS の設定から、パブリックアクセスの許可を外します。 ここまで設定すると、AWS Lambda 関数からはデータベースにアクセスできながら、外部ネットワークから直接アクセスすることは防ぐことができます。

まとめ

今回は AWS Lambda 関数を Deno が動作するカスタムコンテナで作成、データベースのマイグレーションを含めた操作も Deno を使用し、GitHub Actions でデプロイを自動化しました。
Deno を本番サービスで導入を検討するにあたり、AWS EC2 のようなインスタンスで個別に稼動、AWS ECS のようなコンテナオーケストレーションサービス、そして今回の AWS Lambda のようなサーバーレスなどの実行環境が選択肢として挙げられるかと思います。 これらを検討する方への検討の一助となれば幸いです。

これに、先んじて、個人的に AWS Fargate で Deno アプリケーションを 2 週間ほど稼動させたのですが、思ったよりも費用がかかってしまいました。 AWS Lambda でのアプリケーション開発は、リクエスト回数とリクエスト実行時間に依存したうえで無料枠でまかなうことができる部分もあり、 規模感があまり大きくなければ、Deno の実行環境として AWS Lambda はおすすめできる実行環境になると感じました。

今回の実装とインフラ構築では、デプロイ時には事前にデータベースのパブリックアクセスの許可をする必要があります。 この点まで自動化できるとより完全な自動化になります。

また今回の自動化を実現する中で、タスクの実行イメージである ubuntu-18.04 には、mysql や AWS cli もインストール済みになっていました。 他にもできることはまだまだありそうです。

明日は、@undefsan さんより、ワーケーションに関する記事と、 @biga816 さんより、「DenoでイケハヤのNFTを監視する対話型CLIを作る。」 です。

P.S.

採用情報
■募集職種
yumenosora.co.jp

カジュアル面談も随時開催中です
■お申し込みはこちら!
news.toranoana.jp

■ToraLab.fmスタートしました!
メンバーによるPodcastを配信中!
是非スキマ時間に聞いて頂けると嬉しいです。
anchor.fm
■Twitterもフォローしてくださいね!
ツイッターでも随時情報発信をしています
twitter.com