Nuxt3のuseFetchの型定義を探索してみたら結構面白かった話

この記事は個人ブログと同じ内容です

kotamat.com

先日10/12にNuxt3がpublic betaになりました!🎉 Nuxt2から抜本的に変更されたNuxt3では面白い変更点が多いのですが、今回はuseFetchの挙動に関して探索してみようと思います。

useFetchとは?

Data Fetchingにて紹介されている非同期データ取得APIのうちの一つ。

useFetch(url: string, options?)

というような形で呼び出す、非常にシンプルなAPIではあるのですが、useAsyncData$fetch のラッパーであったり、自動生成されたローカルAPIのレスポンスの型を提供するということで、型定義はかなり複雑な形になっています。

まずは型定義から

useFetchの型定義は下記のようになっています。

export declare function useFetch<ReqT extends string = string, ResT = FetchResult<ReqT>, Transform extends (res: ResT) => any = (res: ResT) => ResT, PickKeys extends KeyOfRes<Transform> = KeyOfRes<Transform>>(url: ReqT, opts?: UseFetchOptions<ResT, Transform, PickKeys>): import("./asyncData").AsyncData<import("./asyncData").PickFrom<ReturnType<Transform>, PickKeys>>;

型パズル感すごいですね… ここからは一つずつ紐解いていくことにします

Transform剥がし

TransformはuseAsyncDataのオプションの一つである、返り値の変換器であるため、一旦削ってシンプルにしてみます。

export declare function _useFetch<ReqT extends string = string, ResT = FetchResult<ReqT>, PickKeys = KeysOf<ResT>>(url: ReqT, opts?: UseFetchOptions<ResT, (input: ResT)=> ResT, PickKeys>): import("./asyncData").AsyncData<import("./asyncData").PickFrom<ResT, PickKeys>>;

※この状況だとUseFetchOptionsの第3型引数がエラーになってしまいますが、型の抽象度を意図的に変えてしまったのが原因なので一旦無視します

返り値に注目

ここで返り値に注目してみます。

asyncData.d.tsでは下記のように定義されているため

// Tのうち、Kの配列の要素に指定されたkeyの要素だけを抽出して取り出す
export declare type PickFrom<T, K extends Array<string>> = T extends Record<string, any> ? Pick<T, K[number]> : T;
...
// Dataの型を、asyncDataが返してくれる型に変換する
export interface _AsyncData<DataT> {
    data: Ref<DataT>;
    pending: Ref<boolean>;
    refresh: (force?: boolean) => Promise<void>;
    error?: any;
}
export declare type AsyncData<Data> = _AsyncData<Data> & Promise<_AsyncData<Data>>;

返り値は「ResT から PickKeysで指定した要素だけをとりだし、asyncDataが返してくれる形に変換した型」となります。

ResTの探索

では ResT も探索してみます。

ResT = FetchResult<ReqT>;
export declare type Awaited<T> = T extends Promise<infer U> ? U : T;
export declare type FetchResult<ReqT extends string> = Awaited<ReturnType<$Fetch<unknown, ReqT>>>;

となっているため、「ResT$Fetch<unknown, ReqT>の返り値のPromiseを剥がしたもの、もしくはそれ自体」となります。

$Fetchの探索

$Fetchを見てみると下記の様になっています。

export declare interface $Fetch<T = unknown, R extends FetchRequest = FetchRequest> {
  (request: R, opts?: FetchOptions): Promise<TypedInternalResponse<R, T>>
  raw (request: R, opts?: FetchOptions): Promise<FetchResponse<TypedInternalResponse<R, T>>>
}

ResTを当てはめResTの算出に必要なものだけを残すと

export declare interface $Fetch<unknown, ReqT> {
  (request: R, opts?: FetchOptions): Promise<TypedInternalResponse<ReqT, unknown>>
}

となります。

TypedInternalResponseを見ていくと下記になっているので

export declare type TypedInternalResponse<Route, Default> =
  Default extends string | boolean | number | null | void | object
    // Allow user overrides
    ? Default
    : Route extends string
      ? MiddlewareOf<Route> extends never
        // Bail if only types are Error or void (for example, from middleware)
        ? Default
        : MiddlewareOf<Route>
      : Default

今回の型で置き換えると

export declare type TypedInternalResponse<ReqT, unknown> =
    ReqT extends string
      ? MiddlewareOf<ReqT> extends never
        // Bail if only types are Error or void (for example, from middleware)
        ? unknown
        : MiddlewareOf<ReqT>
      : unknown

となります。neverを一旦無視するとMiddlewareOf<ReqT>が返ってくるといえそうです。

残りの型定義は下記となるので

export declare interface InternalApi { }

export declare type ValueOf<C> = C extends Record<any, any> ? C[keyof C] : never

export declare type MatchedRoutes<Route extends string> = ValueOf<{
  // exact match, prefix match or root middleware
  [key in keyof InternalApi]: Route extends key | `${key}/${string}` | '/' ? key : never
}>

export declare type MiddlewareOf<Route extends string> = Exclude<InternalApi[MatchedRoutes<Route>], Error | void>

MiddlewareOf<ReqT>に当てはめて考えてみると

export declare interface InternalApi { }

export declare type ValueOf<C> = C extends Record<any, any> ? C[keyof C] : never

export declare type MatchedRoutes = ValueOf<{
  // exact match, prefix match or root middleware
  [key in keyof InternalApi]: ReqT extends key | `${key}/${string}` | '/' ? key : never
}>

export declare type MiddlewareOf = Exclude<InternalApi[MatchedRoutes<ReqT>], Error | void>

となります。ざっくりいうと、「InternalApiの中にReqTがキーとなる物があればそれのValueを返却する」というふうに解釈できますね。

つまり「ResTInternalApiのキーがReqTに相当するもののValueの型」と推察できます。

InternalApiの自動生成

上記型定義だとInternalApiはデフォルトで {}です。つまりこのままではなんの意味もないものになります。 この型定義をoverwriteしてくれるのがnitroというNuxt3のサーバーエンジンです。

nitroは色々な機能があるのですが、そのうちの一つが /server/ディレクトリに配置した関数の返り値を解釈し、型定義を生成してくれるというものです。

例えば/server/api/count.tsという下記のTSファイルを設置してみます。

let counter = 0;
export default (): { counter: number } => {
  counter++;
  return { counter };
};

すると.nuxt/nitro.d.tsという、下記の内容のファイルが生成されます。

declare module '@nuxt/nitro' {
  interface InternalApi {
    '/api/count': ReturnType<typeof import('../server/api/count').default>
  }
}
export {}

InternalApiが拡張され、/api/countに対する型定義が出現しました。 これにより、「ResTInternalApiの中にあるReqTに相当するもののValueの型」は 「ResTReqT/api/countのとき/server/api/countの返り値」となることができました。

useFetchの返り値

ここでuseFetchを実際に使ってみてこの効果を探ってみます。

const {data} = await useFetch('/api/count')

としてみたとき、dataの型はシンプルに

Ref<Pick<{
    counter: number;
}, "counter">>

となります。 useFetchでは単にエンドポイントを指定しているだけに過ぎないのですが、その返り値がVue3で用いやすい型として抽出されている事がわかります。 \ InternalApiに型定義がなくても使えるような設計になっているため、useFetchの引数に対しての補完が効かないのが難点ではありますが、Nuxtのディレクトリ構造を探索すればファイル名だけである程度予想はできるので、便利に使えそうです。

とりあえず一旦まとめ

useFetchの返り値を探索するだけで結構長くなったので、一旦このへんで終わりにします。 \ useFetchはこれ以外にもuseAsyncData$fetchのオプションもサポートしているので、よかったら探索してみてください。オプションのサポートは上記に比べると大したことはないので、気構えずに見れると思います

Jupyter Notebook で Python を書くためのコンテナ開発環境を作成する

この記事は個人ブログと同じ内容です

www.ritolab.com


Python を使ったデータ分析や視覚化を気軽に試せるように、コンテナを使ってローカル開発環境を構築してみます。

Jupyter Notebook

Jupyter Notebook は、ブラウザ上で Python を記述して実行することができる web ベースの開発環境です。(言語については Python に限らない)

jupyter.org

開発環境

コンテナで環境を作成するので、Docker が入っている前提です。

  • Docker for Mac 4.1.0
    • Docker 20.10.8
    • Compose 1.29.2

また、最終的なディレクトリ・ファイル構成は以下になります。

.
├── Dockerfile
├── config
│   └── matplotlibrc
└─── docker-compose.yml

これから上記 3 つのファイルを作成していきます。

Dockerfile

まずは Dockerfile を作成してイメージを定義します。

Dockerfile

FROM python:3.9.7-buster

RUN apt-get update
RUN apt install -y locales && localedef -f UTF-8 -i ja_JP ja_JP.UTF-8
ENV LANG ja_JP.UTF-8
ENV LANGUAGE ja_JP:ja
ENV LC_ALL ja_JP.UTF-8
ENV TZ JST-9

# 日本語 font のインストール
RUN apt-get install -y fonts-noto-cjk

# python ライブラリのインストール
RUN python -m pip install pandas
RUN python -m pip install numpy
RUN python -m pip install matplotlib
RUN python -m pip install jupyterlab
RUN python -m pip install notebook
  • Jupyter Notebook を日本語で使いたいので、locales を入れて言語の設定を行なっています。
  • グラフ描画の際に、タイトルや凡例を日本語で表示させたいので、日本語のフォントをインストールしています。
  • 最後に記述されている Python ライブラリのインストールは、必要最低限だけ記述してあります。

なお、私は DebianPython に関しては弱者なのでイメージに buster を選択しています。この辺に詳しい場合は、適宜最適なイメージを指定すると良いと思います。

docker-compose.yml

次に、docker-compose.yml を作成します。

docker-compose.yml

version: '3'
services:
  python3:
    build: .
    container_name: python3
    working_dir: /root/opt
    tty: true
    volumes:
      - ./src:/root/opt
      - ./config:/root/.config/matplotlib
    ports:
      - "8888:8888"

volumes には、以下の 2 つを指定しつなげています。

  • Jupyter Notebook で作成したファイルや、分析で使うデータを設置するための src ディレクト
  • matplotlib の設定ファイルを設置するディレクト

グラフの日本語化

解析結果をチャートに描画する際に、タイトルや凡例などに日本語を使いたい場合はデフォルトのフォントだと日本語が無く文字化けしてしまいます。 イメージ作成の際に日本語フォントをインストールしたので、それを設定ファイルに指定して、日本語が表示されるようにしてやります。

config/matplotlibrc

font.family:  Noto Sans CJK JP

動作確認

設定が終わったので、以下のコマンドを順に流してコンテナを作成します。

# 1. イメージ作成
docker compose build

# 2. コンテナ起動
docker compose up -d

コンテナが起動したら、以下のコマンドで Jupyter Notebook を起動します。

# Jupyter Notebook 起動
docker compose exec python3 jupyter notebook --allow-root --ip=0.0.0.0

コマンドを実行すると、以下のように URL が出力されます。

f:id:ro9rito:20211011175416p:plain

出力された URL にアクセスすれば、Jupyter Notebook を使い始めることができます。

f:id:ro9rito:20211011175434p:plain

まとめ

データを統計解析してみたりそれをグラフなどで視覚化してみようと思った時に、ローカル環境でその全てを実現しようとすると Python 初心者にはなかなか難しいですが、Jupyter Notebook の環境を作れば簡単にその両方を叶えられるので、統計を学習したりする場合には手早くできてとても便利なのでオススメです。

AWS API Gateway(HTTP API) + Lambda + DynamoDB でサーバレスアーキテクチャを構築する

この記事は個人ブログと同じ内容です

www.ritolab.com


AWSAPI Gateway(HTTP API) + Lambda + DynamoDB を使ってサーバレスアーキテクチャを構築してみます。

開発環境 - Terraform v1.0.2

DynamoDB テーブルを作成

データベースを用意します。DynamoDB にテーブルを作成します。

構成としては、date(年月日)をパーティションキーにして、ソートキーに time(時分秒)を起きたいと思います。

先に見せるとこんな感じのテーブルを作ります

f:id:ro9rito:20210909170857p:plain

main.tf

resource "aws_dynamodb_table" "event_log_table" {
  name           = "EventLog"
  billing_mode   = "PROVISIONED"
  read_capacity  = 1
  write_capacity = 1
  hash_key       = "date"
  range_key      = "time"

  attribute {
    name = "date"
    type = "S"
  }

  attribute {
    name = "time"
    type = "S"
  }
}

DynamoDB のテーブル作成自体は簡単で、ミニマムは上記のように数行書けば作成できます。

f:id:ro9rito:20210909171633p:plain

Lambda function 作成

次に DynamoDB に情報を記録していく Lambda function を作成します。

CloudWatch Log Group と Role の作成

関数に行く前に Lambda function 実行のログを CloudWatchLogs に流すためにロググループと、そこへの書き込み、および DynamoDB にアクセスできる Lambda 用のロールを作成しておきます。

main.tf

# CloudWatch ロググループ作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group
resource "aws_cloudwatch_log_group" "log_events_lambda_function" {
  name = "/aws/lambda/${var.log_events_lambda_function_name}"
}

# Iam Role 作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role
resource "aws_iam_role" "lambda_function" {
  name = "${var.app_name}-log-events-lambda-function-role"

  assume_role_policy = jsonencode({
    Version : "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Principal = {
          Service = "lambda.amazonaws.com"
        },
        Action = "sts:AssumeRole"
      }
    ]
  })
}

# Iam Policy 作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy
resource "aws_iam_policy" "lambda_function_put_log_events" {
  name        = "log-events-lambda-function-policy"
  description = "IAM policy for log events Lambda function."

  policy = jsonencode({
    Version : "2012-10-17",
    Statement = [
      {
        Action   = "logs:CreateLogGroup",
        Effect   = "Allow",
        Resource = "arn:aws:logs:${var.aws_region}:${var.aws_id}:*"
      },
      {
        Action = [
          "logs:CreateLogStream",
          "logs:PutLogEvents",
        ],
        Effect = "Allow",
        Resource = [
          "${aws_cloudwatch_log_group.log_events_lambda_function.arn}:*"
        ]
      },
      // AWS Lambda: Lambda 関数に Amazon DynamoDB テーブルへのアクセスを許可します
      // https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/reference_policies_examples_lambda-access-dynamodb.html
      {
        Action = [
          "dynamodb:Scan",
          "dynamodb:GetItem",
          "dynamodb:PutItem",
        ]
        Effect   = "Allow"
        Resource = aws_dynamodb_table.event_log_table.arn
      },
    ]
  })
}

# Policy を Role にアタッチ
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment
resource "aws_iam_role_policy_attachment" "lambda_function_put_log_events" {
  policy_arn = aws_iam_policy.lambda_function_put_log_events.arn
  role       = aws_iam_role.lambda_function.name
}

Lambda function 作成

function は node.js で、ソースコードは terraform 管理です。

index.js

const AWS = require("aws-sdk");

const dynamo = new AWS.DynamoDB.DocumentClient();

const tableName = "EventLog";

exports.handler = async (event, context) => {
    let body;
    let statusCode = 200;
    const headers = {
        "Content-Type": "application/json"
    };

    try {
        switch (event.routeKey) {
            case "GET /events":
                body = await dynamo.scan({
                    TableName: tableName
                }).promise();
                break;
            case "POST /events":
                let requestJSON = JSON.parse(event.body);
                await dynamo
                .put({
                    TableName: tableName,
                    Item: {
                        date: requestJSON.date,
                        time: requestJSON.time,
                        userId: requestJSON.userId,
                        eventType: requestJSON.eventType
                    }
                })
                .promise();
                body = `Put item ${requestJSON.userId}`;
                break;
            default:
                throw new Error(`Unsupported route: "${event.routeKey}"`);
        }
    } catch (err) {
        statusCode = 400;
        body = err.message;
    } finally {
        body = JSON.stringify(body);
    }

    return {
        statusCode,
        body,
        headers
    };
};

Lambda 関数自体は API Gatewayチュートリアルを参考にしています。

docs.aws.amazon.com

main.tf

# ファイルの ZIP 化
## https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/archive_file
data "archive_file" "log_events_lambda_function" {
  type        = "zip"
  source_dir  = "path/to/functions/${var.log_events_lambda_function_name}"
  output_path = "path/to/functions/upload/${var.log_events_lambda_function_name}.zip"
}

# Lambda function 作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function
resource "aws_lambda_function" "log_events" {
  filename      = data.archive_file.log_events_lambda_function.output_path
  function_name = var.log_events_lambda_function_name
  role          = aws_iam_role.lambda_function.arn
  handler = "index.handler"

  source_code_hash = filebase64sha256(data.archive_file.lambda_function.output_path)

  runtime = "nodejs14.x"

  depends_on = [
    aws_iam_role_policy_attachment.lambda_function_put_log_events,
    aws_cloudwatch_log_group.log_events_lambda_function
  ]
}
  • Lambda function を適用させる際に ZIP で上げるため、作成した js ファイルを ZIP 化しています。
    • source_code_hash で ZIP ファイルをハッシュ化しています(filebase64sha256() 関数は Terraform 0.11.12 以降。それ以前は base64sha256() と file() 関数を使う)
    • apply や plan 時に ZIP ファイルが作成される
  • role でに先程作成した IAM Role を指定しています。

apply すると Lambda 関数が作成されます

f:id:ro9rito:20210909171217p:plain

API Gateway エンドポイント作成

ここからは Amazon API Gateway を使って先程作成した lambda function を利用する際のエンドポイントを作成していきます。

今回は HTTP API で作成します。作成に必要なステップは以下になります。

  1. API 作成
  2. 統合設定
  3. ルート設定
  4. ステージ設定

API 作成

まずは大本になる API を作成します。

main.tf

# API 作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_api
resource "aws_apigatewayv2_api" "log_events" {
  name          = "xxxxxxxxxxxx"
  protocol_type = "HTTP"

  cors_configuration {
    allow_origins = ["*"]
    allow_methods = ["GET", "POST"]
    allow_headers = ["*"]
  }
}

cors_configuration - CORS の設定を行っています。設定できる項目は以下(すべて optional)

  • allow_origins
  • allow_headers
  • allow_methods
  • allow_credentials
  • expose_headers
  • max_age

HTTP API の CORS の設定

docs.aws.amazon.com

統合設定(API と Lambda function の関連付け)

先程作成した API と Lambda function を紐付けます。

main.tf

# 統合設定(API と Lambda function の紐付け)
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_integration
resource "aws_apigatewayv2_integration" "log_events" {
  api_id           = aws_apigatewayv2_api.log_events.id
  integration_type = "AWS_PROXY"
  integration_method = "POST"
  integration_uri = aws_lambda_function.log_events.invoke_arn
  payload_format_version = "2.0"
}

integration_type - 統合タイプを選択。HTTP API の場合は websocket もサポートしているので選択肢がいくつかあり、Lambda function と統合させる場合は AWS_PROXY を設定する。

選択できる項目

  • AWS(WebSocket API でのみサポート)
  • AWS_PROXY
  • HTTP(WebSocket API でのみサポート)
  • HTTP_PROXY
  • MOCK(WebSocket API でのみサポート)

docs.aws.amazon.com

integration_method - 統合先へのリクエストの HTTP method を指定。Integration_type が MOCK でない場合は指定は必須。Lambda との統合なので POST を指定。

docs.aws.amazon.com

ルート設定

作成した API に対してメソッドやリソースを設定します。

docs.aws.amazon.com

main.tf

# ルート設定
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_route
variable "routeList" {
  type    = list(string)
  default = ["GET /events", "POST /events"]
}
resource "aws_apigatewayv2_route" "log_events" {
  count  = length(var.routeList)

  api_id = aws_apigatewayv2_api.log_events.id
  route_key = element(var.routeList, count.index)

  target = "integrations/${aws_apigatewayv2_integration.log_events.id}"
}

target - アタッチする統合を指定する - Integrations/{IntegrationID} の形式で指定 - IntegrationID は aws_apigatewayv2_integration リソースの識別子

route_key - "GET /events" のように「<HTTP メソッド> <リソースパス>」で指定 (今回は 2 メソッド分まとめた記述になっている)

ステージ設定

ステージを設定します。

docs.aws.amazon.com

リクエストのログも残したいので CloudWatch のロググループも併せて作成して設定します。

main.tf

resource "aws_cloudwatch_log_group" "log_events_http_api" {
  name = "/aws/apigateway/${var.log_events_api_name}"
}

# ステージ設定
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_stage
resource "aws_apigatewayv2_stage" "log_events" {
  api_id      = aws_apigatewayv2_api.log_events.id
  name        = "$default"
  auto_deploy = true

  access_log_settings {
    destination_arn = aws_cloudwatch_log_group.log_events_http_api.arn

    format = jsonencode({
      requestId : "$context.requestId",
      ip : "$context.identity.sourceIp",
      requestTime : "$context.requestTime",
      httpMethod : "$context.httpMethod",
      routeKey : "$context.routeKey",
      status : "$context.status",
      protocol : "$context.protocol",
      responseLength : "$context.responseLength",
      errorMessage : "$context.error.message",
      errorResponseType : "$context.error.responseType"
      authorizerError : "$context.authorizer.error",
      integrationErrorMessage : "$context.integrationErrorMessage"
    })
  }
}

access_log_settings でアクセスをログに記録するための設定を行なっています。format でアクセスログ 1 行の形式を指定、$context 変数で値を指定します。

docs.aws.amazon.com

APIGateway に Lambda へのアクセスを許可

APIGateway 側の一通りの設定が済んだので、最後に APIGateway から Lambda へのアクセスを許可します。

main.tf

# APIGateway に Lambda へのアクセスを許可
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission
resource "aws_lambda_permission" "execution_log_access_api_gateway" {
  statement_id  = "test-AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.log_events.function_name
  principal     = "apigateway.amazonaws.com"

  source_arn = "${aws_apigatewayv2_api.log_events.execution_arn}/*/*"
}

動作確認

ここまでで一連の構成は定義できたので、apply して環境を構築し、エンドポイントへリクエストを送信すれば API Gateway, Lambda, DynamoDB が連携して動作し、データが保存されます。

f:id:ro9rito:20210909172147p:plain

カスタムドメイン設定

API Gateway で作成されたエンドポイントの URL ですが、でデフォルトでは

https://<api_id>.execute-api.<region>.amazonaws.com/xxxxxxx...

という形式になっています。

カスタムドメインを設定すると、自分のドメインを使ってエンドポイントの URL を作成できるので、こちらの設定を行なってみます。

(ネイキッドドメインは Route53 に登録済みの前提です。

ACM 証明書発行

まずは、利用するサブドメインの証明書を ACM で作成しておきます。

main.tf

# 作成済みホストゾーン情報の取得
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/route53_zone
data "aws_route53_zone" "myDomain" {
  name = var.domain_name
}

# ACM 証明書作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate
resource "aws_acm_certificate" "APIGatewayHttpApi" {
  domain_name       = "samle-htttp-api.ritolab.com"
  validation_method = "DNS"
}

## ACM 検証用 CNAME レコード
### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record
resource "aws_route53_record" "api_gateway_http_api_acm_c" {
  for_each = {
  for d in aws_acm_certificate.APIGatewayHttpApi.domain_validation_options : d.domain_name => {
    name   = d.resource_record_name
    record = d.resource_record_value
    type   = d.resource_record_type
  }
  }
  zone_id         = data.aws_route53_zone.myDomain.zone_id
  name            = each.value.name
  type            = each.value.type
  records         = [each.value.record]
  ttl             = 60
  allow_overwrite = true
}

## ACM 証明書 / CNAME レコード 連携
### https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/acm_certificate_validation
resource "aws_acm_certificate_validation" "APIGatewayHttpApi" {
  certificate_arn         = aws_acm_certificate.APIGatewayHttpApi.arn
  validation_record_fqdns = [for record in aws_route53_record.api_gateway_http_api_acm_c : record.fqdn]

  depends_on = [
    aws_acm_certificate.APIGatewayHttpApi,
    aws_route53_record.api_gateway_http_api_acm_c
  ]
}

カスタムドメイン設定

カスタムドメインを設定します。

main.tf

## カスタムドメイン登録
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_domain_name
resource "aws_apigatewayv2_domain_name" "http_api" {
  domain_name = "sub.domain.com"

  domain_name_configuration {
    certificate_arn = aws_acm_certificate.APIGatewayHttpApi.arn
    endpoint_type   = "REGIONAL"
    security_policy = "TLS_1_2"
  }

  // 証明書作成が完了してから
  depends_on = [
    aws_acm_certificate_validation.APIGatewayHttpApi,
  ]
}

## API マッピング
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_api_mapping
resource "aws_apigatewayv2_api_mapping" "http_api" {
  api_id      = aws_apigatewayv2_api.log_events.id
  domain_name = aws_apigatewayv2_domain_name.http_api.id
  stage       = aws_apigatewayv2_stage.log_events.id
}

# route53 A レコード作成
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route53_record
resource "aws_route53_record" "http_api" {
  name    = aws_apigatewayv2_domain_name.http_api.domain_name
  type    = "A"
  zone_id = data.aws_route53_zone.myDomain.zone_id

  alias {
    name                   = aws_apigatewayv2_domain_name.http_api.domain_name_configuration[0].target_domain_name
    zone_id                = aws_apigatewayv2_domain_name.http_api.domain_name_configuration[0].hosted_zone_id
    evaluate_target_health = false
  }
}

カスタムドメインを登録し、API マッピングを行なった後に、route53 へ使用するサブドメインの A レコードを作成しています。

ポイントとしては、カスタムドメイン登録のときに depends_on を指定し、証明書の作成を待ってからカスタムドメインの登録を行うようにします。 依存関係を指定しておかないと証明書が有効になる前にカスタムドメインを登録しようとしてエラーになります。

これでカスタムドメインでエンドポイントへアクセスできるようになります。

% curl https://sample-http-api.ritolab.com/events
{"Items":[{"date":"2021-08-28","eventType":"test","time":"14:19:00.000","userId":"EX8C9MRZfhL"},{"date":...

IAM 認証

現状ではエンドポイントは開放されている状態なので、IAM 認証をかけてアクセス制限を掛けます。(既に Cognito とか Auth0 とかで認証のあるサービスなら IAM 認証ではなく Lambda オーソライザー使うのが良さそう)

API Gateway のルートの設定において、認証を IAM に変更します。

main.tf

resource "aws_apigatewayv2_route" "log_events" {
  .
  .
  .
  authorization_type = "AWS_IAM"
}

これだけです。あとはリクエスタとしての IAM ユーザーを作成して、適宜必要な権限を設定してあげます。

main.tf

# IAM Role for ApiGateway
resource "aws_iam_policy" "api_gateway_log_event_requester" {
  name        = "xxxxxxxxx"

  policy = jsonencode({
    Version : "2012-10-17",
    Statement = [
      {
        Effect: "Allow",
        Action: [
          "execute-api:Invoke"
        ],
        Resource: [
          "arn:aws:execute-api:${var.aws_region}:${var.aws_id}:${aws_apigatewayv2_api.log_events.id}/*/GET/events",
          "arn:aws:execute-api:${var.aws_region}:${var.aws_id}:${aws_apigatewayv2_api.log_events.id}/*/POST/events",
        ],
      },
    ]
  })
}

# IAM User for ApiGateway execution
## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user
resource "aws_iam_user" "api_gateway_log_event_requester" {
  name = "api-gateway-log-event-requester"
}

## https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_policy_attachment
resource "aws_iam_user_policy_attachment" "api_gateway_log_event_requester" {
  user       = aws_iam_user.api_gateway_log_event_requester.name
  policy_arn = aws_iam_policy.api_gateway_log_event_requester.arn
}

これで、アクセスが許可されたユーザー以外からのリクエストは遮断されます。

f:id:ro9rito:20210909172439p:plain

一方で、許可された IAM ユーザーからのリクエストは受け入れられデータが取得できている事を確認できました。

f:id:ro9rito:20210909172457p:plain

まとめ

エンドポイントからバックエンド、DB まで一連をサーバレスアーキテクチャで構築しました。

レスポンスは遅くないので、これでサーバ運用の手間が減ると考えるとサーバレス化は手段としてはアリだなと感じました。

あとやはり個々のサービスが疎結合になるため、必要に応じて最適なプログラミング言語を選択できるのも場合によっては利点になり得る要素だと思いました。

それと、API Gateway でのエンドポイントを今回は HTTP API で構築してみましたが、オートデプロイが行えるのは現時点では HTTP API のみで REST API には無い機能だったので、開発時にデプロイを気にしなくて良いのはとても便利でした。

AWS Well-Architected フレームワークの「パフォーマンス効率の柱 設計の原則」においてサーバレスアーキテクチャが推奨されているように、サーバレス構成は適所で採用すればとても使えるアーキテクチャでした。

docs.aws.amazon.com

データサイエンスの始め方

この記事は 個人Qiita と同じ内容です

qiita.com/sekiyaeiji

プロダクトのグロースにデータを活用したい...

サービスグロースにデータを活用したいと思ったとき、わたしはまず最初にどこに着目し、ナニから始めればいいのだろう?

よくわからなかったので調べ始めてみた。

小さなテーマからはじめよう

データサイエンスは、最初、できるだけコンパクトに小さく始めて、その成果をもとに大きく波及させるのが、うまくいきやすいポイントらしい。

小さなテーマを100個ぐらい仕込んで、大きく化けるテーマを探すらしい。

小さく始めると、失敗しても影響が小さく、軌道修正も容易で、何度でもチャレンジできるメリットがあるとのこと。

...で、ナニをやるんだろう

始め方はわかったけど、小さくナニを始めるのか?

活用目的と、活用ストーリーと、分析ストーリー

このデータ分析は何のためにやるのか、を明確にするのが、活用目的を決めることだが、 データサイエンスの世界では、目的を決めるだけでは不十分らしく、 その目的に対して、分析データの活用ストーリーを明確にする必要がある。

分析結果をただ提示しても利用してもらえない

「分析結果がAの場合、1の施策を、また結果がBの場合は、2の対策を実践する」、のような、 結果と活用方法の対応が分かる情報を、 「活用ストーリー」として分析結果とセットで提示すれば、 分析結果は上手に活用してもらえるらしい

また、分析結果と「活用ストーリー」を事前にしっかり設計することにより、 提供すべき分析結果の精度や、確実に活用してもらえるデータ提供が可能になると思われる

そしてさらに、一元的な分析結果ではなく、いくつかの中間データを経て分析結果を得る場合に、 その各ステップや全体の構造を説明したものを、「分析ストーリー」と呼ぶことができる

多層構造を要する分析においてはこの分析ストーリーを明確にすることが 保守の面においても「活用ストーリー」の設計においても大切になってくる

筋のいいテーマ

以上の3つの要素、

  • 活用目的
  • 活用ストーリー
  • 分析ストーリー

が明確で、さらに

  • 成果が大きい
  • やりやすい

テーマは、"筋のいいテーマ"らしい

"筋のいいテーマ"を見つけることが、 確実かつスピーディーに成果を出せるテーマを選択できるコツ、と言ってよさそうだ

よって、必須3要素である、活用目的、活用ストーリー、分析ストーリーを 上手に設計するトレーニングを重ねることが データ分析上達の鍵な気がする

モデルの選び方

チートシート

分析を設計する際に必要になる統計解析・機械学習モデルについて、
世にはチートシートというサンプルモデルも出回っているらしい

モデルの種類と特徴

採りたいデータごとに分析方法を選択して利用する

  • フィッシュボーンチャート 特性要因図
    • 目的変数と複数の説明変数からなる魚の骨状の図
    • わかりやすく、要素を組み立てやすくてかなり便利
  • 線形判別モデル
  • ロジスティック回帰モデル
  • クラスター分析
  • 主成分分析
    • 似たような傾向を持つデータ項目(変数)を集約する
  • グラフィカルモデリング
    • データ項目間(変数間)の構造を描く

分析の実践に役立ついくつかの手法 メモ

小さく始める際に、ジョハリの窓における「開放の窓」を狙うことで、 現場の感覚とズレのない項目から着手する

選択肢の分岐点となる閾値を算出するために、決定木(ディシジョンツリー)を利用する

ニーズ(needs) より ウォンツ(wants) 変革(change) よりも 改善(improvement) つまり、ウォンツ ✕ 改善 の象限から着手する

痛み よりも 楽になること 全体最適 よりも 個々の部署のメリット(メリットの平準化) つまり、楽になる ✕ メリットの平準化 の象限、 できるだけ多くの関係者がメリットを感じられるテーマから着手する

「実験計画法」(少ない実験で効率的にデータを取得する方法論)で、 データ取得計画を作り、実験しデータを取得し、 取得データから「応答曲面法」で設計変数、品質特性の関係性を数式化し、 その数式を使い「数理計画法」により最適な「設計変数値」を算出する

改善・変革系データサイエンスと、データエコノミー系データサイエンス

日本で成果が出ているデータサイエンスはSQCのような改善・変革系データサイエンス

GAFA系が実現したデータエコノミー系データサイエンスへの発展を目指す場合、 各社がふつうのタスクとしてあたりまえに取り組むことと、 社内で始めて自社外への拡大を実践することで、市場向けのデータサイエンスは可能になる

まとめ

データサイエンスに着手するための基本情報を以上の通りピックアップしてみた

大枠として以下を意識してまずは実績を作成してみるのがよさそうだ

  • 小さなテーマを100個作成して、大きく成長するテーマを探す
  • 活用目的、活用ストーリー、分析ストーリーの3要素で設計する
  • 用途に応じて使えるツールと手法のテンプレートが多く存在する

ネクストアクション

次はこれを読んで情報をまとめる予定

それともう一冊、

データサイエンスにおいて『孫氏』は必読の書、らしいが、

どういうことかよくわからないので、とりあえず読まなければならない

参考図書

本稿では以下の書籍を参考にさせていただきました

Amazon API Gateway エンドポイント(REST API)のカスタムドメイン設定・認可・アクセス制限

この記事は個人ブログと同じ内容です

www.ritolab.com


サーバレスアーキテクチャ構築の第二弾です。

今回は Amazon API Gateway で作成したエンドポイントを使いやすくしたり制限をかけたりしていきます。

開発環境

今回の開発環境は以下になります。

Terraform v1.0.2
なお、今回は前回の続きになるので、操作する Amazon API Gateway については、AWS Lambda / Amazon API Gateway の連携・エンドポイント作成 で作成したものをベースに行っていきます。

API のタイプは REST API です)

カスタムドメインを設定する

デフォルトでは API Gateway で作成されるエンドポイントは以下のようになります。

https://<REST_API_ID>.execute-api.<REGION>.amazonaws.com/<LAMBDA_FUNCTION_NAME>/<RESOURCE_NAME>...

これをカスタムドメインを設定することで以下のように短くする事ができます。

https://<CUSTOM_DOMAIN>/<RESOURCE_NAME>...

以下、カスタムドメインを設定していきます。

ACM 証明書発行

ACM で証明書を発行します。ネイキッドドメインは Route 53 に登録済みの前提です。

main.tf

locals {
  api_gateway_sub_domain_name = "${var.sub_domain_host_name}.${var.domain_name}"
}

# 作成済みホストゾーン情報の取得
data "aws_route53_zone" "main" {
  name = var.domain_name
}

# ACM 証明書作成
resource "aws_acm_certificate" "APIGateway" {
  domain_name       = local.api_gateway_sub_domain_name
  validation_method = "DNS"

  tags = {
    Name = var.domain_name
  }
}

## ACM 検証用 CNAME レコード
resource "aws_route53_record" "api_gateway_acm_c" {
  for_each = {
    for d in aws_acm_certificate.APIGateway.domain_validation_options : d.domain_name => {
      name   = d.resource_record_name
      record = d.resource_record_value
      type   = d.resource_record_type
    }
  }
  zone_id         = data.aws_route53_zone.main.zone_id
  name            = each.value.name
  type            = each.value.type
  records         = [each.value.record]
  ttl             = 60
  allow_overwrite = true
}

## ACM 証明書 / CNAME レコード 連携
resource "aws_acm_certificate_validation" "APIGateway" {
  certificate_arn         = aws_acm_certificate.APIGateway.arn
  validation_record_fqdns = [for record in aws_route53_record.api_gateway_acm_c : record.fqdn]

  depends_on = [
    aws_acm_certificate.APIGateway,
    aws_route53_record.api_gateway_acm_c
  ]
}

API Gateway カスタムドメイン登録

API Gateway でカスタムドメインを適用します。

main.tf

# カスタムドメイン
resource "aws_api_gateway_domain_name" "main" {
  domain_name              = local.api_gateway_sub_domain_name
  regional_certificate_arn = aws_acm_certificate.APIGateway.arn
  security_policy          = "TLS_1_2"

  endpoint_configuration {
    types = ["REGIONAL"]
  }
}

## API マッピング
### ドメイン名から API ステージへのパスをマッピング
resource "aws_api_gateway_base_path_mapping" "main" {
  api_id      = aws_api_gateway_rest_api.to_lambda_node.id
  stage_name  = aws_api_gateway_stage.hello_world.stage_name
  domain_name = aws_api_gateway_domain_name.main.domain_name
}

# route53 A レコード作成
resource "aws_route53_record" "APIGatewayCustomDomain" {
  name    = aws_api_gateway_domain_name.main.domain_name
  type    = "A"
  zone_id = data.aws_route53_zone.main.id

  alias {
    evaluate_target_health = true
    name                   = aws_api_gateway_domain_name.main.regional_domain_name
    zone_id                = aws_api_gateway_domain_name.main.regional_zone_id
  }
}

カスタムドメイン名を登録したら APIマッピングを行い、カスタムドメイン名を A レコードで登録(ルーティング先に API Gateway ドメイン名を指定)します。

これでエンドポイントがカスタムドメインを用いた URI になります。

https://<CUSTOM_DOMAIN_NAME>/hello_world こんな感じです。

IAM 認証を導入してエンドポイントに認可制限を設ける

前回までの時点ではこのエンドポイントはどこから・誰からでも叩ける状態なので、必要に応じて何らかの制限を掛けてあげる必要があります。

サーバレスという事で今回のアプリケーションを静的サイトと想定した場合、S3 にホスティングして CloudFront で配信の構成が多いと考えます。

その場合 VPC や IP で制限を掛けるといった事が難しいので、IAM 認証を導入して API Gateway で作成したエンドポイントに認可制限を掛けたいと思います。

IAM ユーザー作成

まずはエンドポイントにリクエストする際の IAM ユーザーを作成します。

main.tf

# IAM Role for ApiGateway
## AWS管理ポリシー
data "aws_iam_policy" "AmazonAPIGatewayInvokeFullAccess" {
  arn = "arn:aws:iam::aws:policy/AmazonAPIGatewayInvokeFullAccess"
}

# IAM User for ApiGateway execution
resource "aws_iam_user" "api_gateway_requester" {
  name = "api_gateway_requester"
}

resource "aws_iam_user_policy_attachment" "api_gateway_requester" {
  user       = aws_iam_user.api_gateway_requester.name
  policy_arn = data.aws_iam_policy.AmazonAPIGatewayInvokeFullAccess.arn
}

今回はテストなのでポリシーは AWS 管理ポリシーである AmazonAPIGatewayInvokeFullAccess を使用しています。

サービスやエンドポイントによって細かくリクエストを制限したい場合は管理ポリシーではなく個別にリソースを指定します。

メソッドの authorization を IAM 認証へ変更

次に、作成したメソッド(GET)の authorization を IAM 認証へ変更します

main.tf

### メソッド設定
resource "aws_api_gateway_method" "hello_world" {
  rest_api_id   = aws_api_gateway_rest_api.to_lambda_node.id
  resource_id   = aws_api_gateway_resource.hello_world.id
  http_method   = "GET"
  authorization = "AWS_IAM" // <-  ここを NONE から AWS_IAM へ変更
}

これまで NONE としていた部分を AWS_IAM へ変更します。

動作確認

AWS 側の設定はこれだけなので、動作確認を行なってみます。

まずはローカルのターミナルから curl コマンドを叩いてみた結果です。

% curl -IX GET  https://<CUSTOM_DOMAIN_NAME>/hello_world
HTTP/2 403
content-type: application/json
content-length: 42
x-amzn-requestid: xxxx-xxx-xxx-xxxx
x-amzn-errortype: MissingAuthenticationTokenException
x-amz-apigw-id: xxxxxxxxxxxxx

認可エラーを示す HTTP ステータスコード 403 が返ってきたので、しっかり制限がかかっている事が確認できます。

一方で、IAM 認証を行なった上でリクエストを行えば、認可で弾かれずにエンドポイントにアクセスする事ができます。

SigV4 - API リクエストに認証情報を追加する

AWS API Gateway で作成したエンドポイントのリクエストで IAM 認証を通すためには、Signature Version 4 (SigV4) を用いて AWS API リクエストに認証情報を追加する必要があります。

docs.aws.amazon.com

docs.aws.amazon.com

ここはアプリケーション側の実装になりますが、JavaScript 及び PHPAWS SDK を用いて実装してみたので参考までに記します。

AWS SDK for JavaScript v2(SigV4 部分を抜粋)

import aws from 'aws-sdk'
import core from  'aws-sdk/lib/core'

-----------------------------------------------

const awsAccessKey = '<AWS_ACCESS_KEY>';
const awsSecretKey = '<AWS_SECRET_KEY>'
const awsRegion  = '<AWS_REGION>';
const awsComponentServiceName    = "execute-api";
const awsAPiGatewayHelloWorldUrl = 'https://<CUSTOM_DOMAIN>/hellow_world';

const splits = awsAPiGatewayHelloWorldUrl.split('?');
const host   = splits[0].substr(8, splits[0].indexOf("/", 8) - 8);
const path   = splits[0].substr(splits[0].indexOf("/", 8));
const query  = splits[1];

const options = {
  url: awsAPiGatewayHelloWorldUrl,
  region: awsRegion,
  method: 'GET',
  headers: {
    host: host,
  },
  pathname: () => path,
  search: () =>  query ? query : '',
};

const signer = new core.Signers.V4(options, awsComponentServiceName);

signer.addAuthorization(new aws.Credentials(awsAccessKey, awsSecretKey), new Date());

const response = await axios.get(awsAPiGatewayHelloWorldUrl, {
  'headers': {
    'authorization': options.headers['Authorization'],
    'x-amz-date': options.headers['X-Amz-Date']
  }
});
// => Hello from Lambda!

こちらについては以下の記事を参考にさせていただきました

dev.classmethod.jp

API Gateway で生成された JavaScript SDK を使用

var apigClient = apigClientFactory.newClient({
    accessKey: awsAccessKey,
    secretKey: awsSecretKey,
    region: awsRegion
});

apigClient.helloWorldGet()
            .then(function(result) {
                console.log(result.data);
                // => Hello from Lambda!
            })
            .catch(function(result) {
                console.log(result);
            });

docs.aws.amazon.com

AWS SDK for PHP v3

/** @var \GuzzleHttp\Client $client */
$client = new Client();

/** @var \GuzzleHttp\Psr7\Request $request */
$request = new Request('GET', 'https://<CUSTOM_DOMAIN>/hellow_world');

/** @var \Aws\Credentials\Credentials $credentials */
$credentials = new Credentials('<AWS_ACCESS_KEY>', '<AWS_SECRET_KEY>');

/** @var \Aws\Signature\SignatureV4 $signer */
$signer = new SignatureV4('execute-api', 'ap-northeast-1');

// クレデンシャルを使用し必要なヘッダーをリクエストに追加することで指定されたリクエストに SigV4 で署名します。
$requestWithSign = $signer->signRequest($request, $credentials);

// エンドポイントへ HTTP リクエストを送信
$response = $client->send($requestWithSign);

$response->getBody()->getContents()
// -> "Hello from Lambda!"

指定している AWSACCESS_KEY と SECRET_KEY は、API Gateway へのリクエスタとして(このセクションの冒頭で)作成した IAM ユーザーのものになります。

IP Address でアクセス制限を掛ける

API Gateway のエンドポイントに対して IP 制限をかけたい場合は、リソースポリシーを用いることで実現できます。

docs.aws.amazon.com

main.tf

resource "aws_api_gateway_rest_api_policy" "to_lambda_node" {
  rest_api_id = aws_api_gateway_rest_api.to_lambda_node.id
  policy = jsonencode({
    Version : "2012-10-17",
    Statement : [
      {
        Effect : "Allow",
        Principal : "*",
        Action : "execute-api:Invoke",
        Resource : "arn:aws:execute-api:${var.aws_region}:${var.aws_id}:${aws_api_gateway_rest_api.to_lambda_node.id}/*",
        Condition : {
          // IP アドレス制限
          "IpAddress": {
            "aws:SourceIp": "xx.xxx.xx.xxx/32"
          }
        }
      }
    ]
  })
}

CORS の有効化を行う

作成したリソースの CORS を有効化するのなら、terraform モジュール api-gateway-enable-cors が簡単でいい感じに使えます。

OPTIONS メソッドを追加してクロスオリジンリソースシェアリング(CORS)プリフライトリクエストを許可する Terraform モジュールです。

main.tf

## CORS 設定
module "cors" {
  source  = "squidfunk/api-gateway-enable-cors/aws"
  version = "0.3.3"

  api_id          = aws_api_gateway_rest_api.to_lambda_node.id
  api_resource_id = aws_api_gateway_resource.hello_world.id

  allow_headers = ["Content-Type", "authorization", "x-amz-date"]
  allow_methods = ["OPTIONS", "GET"]
  allow_origin  = "*"
}

必要な項目を設定すると、OPTIONS メソッドが作成され CORSプリフライトリクエストが可能になります。

今回はここまでになります。Amazon API Gateway で作成したエンドポイントの方はあらかた使いやすくなったので、次回は DynamoDB を絡めてデータ操作周りをやっていこうと思います。

image-magick-lambda-layer を使ってオリジナル LGTM 画像を作ろう

この記事は個人ブログと同じ内容です

image-magick-lambda-layer を使ってオリジナル LGTM 画像を作ろう


LGTM画像といえば、LGTMoon、いつもお世話になっています。 でも、たまにはオリジナリティ出したいですよね。

そこで、今回は5秒でオリジナルLGTM画像を生成するサービスを作成します。

準備するもの

lambda layer とは

Lambdaレイヤーは、追加のコードまたはデータを含むことができる .zip ファイルアーカイブです。レイヤーには、ライブラリ、 カスタムランタイム 、データ、または設定ファイルを含めることができます。レイヤーを使用すると、コードの共有と責任の分離を促進し、ビジネスロジックの記述をより迅速に繰り返すことができます。

素の lambda にライブラリを追加する方法。layer に分けることで処理速度を上げることができる。今回はAWSで用意されているレイヤーを使用するが、もちろん独自のレイヤーを作成することもできる。関数に追加できるLayerは5つまでで、合計サイズが250MB以下となる必要がある。

lambdaファンクションを作成する

lambda > 関数 に行き、「関数の作成」をします。 関数名は適当に lambda-image-magick(任意)、ランタイムは Node.js 14.x とします。

API Gateway、POST メソッドを作成する

これから作成する API Gateway の POST エンドポイントに画像のURLをリクエストし、 lambda で受け取れるようにしていきます。

{
  url: '画像のアドレス'
}

API Gateway から REST API を作成、アクション > メソッドの作成 から POST を作成。先程作成した lambda 関数名を指定します。

CORS を有効化する

「CORSを有効にして既存のCORSヘッダーを置換」をクリックし、「はい、既存の値を置き換えます」をクリックするとOPTIONSメソッドが作成されます。

image-magick-lambda-layer をデプロイする

image-magick-lambda-layer にアクセスしてデプロイをクリックします。

デプロイしただけでは使用できないので有効化します。

レイヤー > レイヤーを追加。 AWSレイヤーに出てこなかったため、ARN を指定してレイヤーを追加しました。

コードを用意

ImageMagick, GraphicsMagick を node.js で扱えるようにする gm を入れます。axios は画像ダウンロードで使用。

$ yarn init -y
$ yarn add axios gm 

ローカルで動作を確認する場合はインストールをお忘れなく。

$ brew install imagemagick
$ brew install graphicsmagick

重ねる lgtm.png を用意します。

LGTM画像は以下のように、適当に数パターンサイズを用意。 (当初は送られてきた画像のサイズに合わせてリサイズを考えましたが、lambda上に一時的に画像を保存しておくことができないため断念。)

├── assets
│   ├── lgtm.png
│   ├── lgtm100.png
│   ├── lgtm150.png
│   ├── lgtm200.png
│   ├── lgtm250.png
│   ├── lgtm300.png
│   ├── lgtm500.png
│   └── lgtm700.png
├── index.js
├── node_modules
├── package.json
└── utils.js

lambda 関数を作成

  1. 画像パスを受け取り、画像をダウンロード
  2. 画像とあらかじめ用意している lgtm.png と合成
  3. 合成したバッファをリサイズ(出力サイズは width 300px とした)
  4. response 返却
// index.js
const { downloadImage, composite, resize } = require('./utils');

exports.handler = async (event) => {
  try {
    const buf = await downloadImage(event.url); // 1
    const composited = await composite(buf); // 2
    const resized = await resize(composited, 300); // 3
    const base64 = 'data:image/png;base64,' + Buffer.from(resized).toString('base64');
    const response = {
      statusCode: 200,
      headers: {
          'Content-Type': 'image/png',
          'Access-Control-Allow-Origin': '*',
      },
      body: base64,
      isBase64Encoded: false,
    };
    return response; // 4
  } catch(e) {
    console.error(e);
  }
};

処理の詳細はこちら

// utils.js
const GM = require('gm');
const gm = GM.subClass({ imageMagick: true });
const axios = require('axios');

exports.downloadImage = async (url) => {
  const res = await axios.get(url, { responseType: 'arraybuffer' });
  return Buffer.from(res.data);
}

const getBufferSize = (buf) => {
  return new Promise((resolve, reject) => {
    gm(buf)
      .size((err, { width, height }) =>
        err ? reject(err) : resolve({ width, height }))
  })
}

const getLgtmPng = (width) => {
  if (width >= 1000) {
    return './assets/lgtm700.png'
  } else if (width < 1000 && width >= 800) {
    return './assets/lgtm500.png'
  } else if (width < 800 && width >= 500) {
    return './assets/lgtm300.png'
  } else if (width < 500 && width >= 300) {
    return './assets/lgtm150.png'
  } else if (width < 300) {
    return './assets/lgtm100.png'
  }
}

const getPosition = (lgtmPath, width, height) => {
  return new Promise((resolve, reject) => {
    gm(lgtmPath).size((err, size) => {
      const centerWidth = width / 2;
      const centerHeight = height / 2;
      const left = Math.floor(centerWidth - (size.width / 2));
      const top = Math.floor(centerHeight - (size.height / 2));
      const geometry = '+' + left + '+' + top;
      return err ? reject(err) : resolve(geometry)
    });
  });
}

const getCompositedBuffer = (buf, lgtmPath, geometry) => {
  return new Promise((resolve, reject) => {
    return gm(buf)
      .composite(lgtmPath)
      .geometry(geometry)
      .quality(100)
      .noProfile()
      .toBuffer((err, buffer) =>
         err ? reject(err) : resolve(buffer));
  })
}

exports.composite = async (buf) => {
  try {
    const { width, height } = await getBufferSize(buf) 
    const lgtmPath = getLgtmPng(width)
    const geometry = await getPosition(lgtmPath, width, height);
    return await getCompositedBuffer(buf, lgtmPath, geometry);
  } catch(e) {
    throw e;
  }
}

exports.resize = async (buf, width, height) => {
  return new Promise((resolve, reject) => {
    gm(buf)
      .resize(width, height)
      .noProfile()
      .toBuffer('PNG', (err, buffer) =>
        err ? reject(err) : resolve(buffer));
  });
};

デプロイ

コードができたら zip にします。

$ zip -r deploy.zip ./

lambda の画面から .zip ファイルをアップロード。

完成 🎉

ブラウザの任意の jpegpng の画像のURLをエンドポイントにリクエストする bookmarklet を作成する。

Next.js にレイアウトに関するドキュメントが追加されました

この記事は個人ブログと同じ内容です

zenn.dev

 

 

Next.js の Layout 周りのドキュメントが新しく追加されました。

単一のレイアウトを扱う場合

単一のレイアウトで十分な場合はカスタマイズした <Layout/><Component /> タグを囲むだけで実装できます。

// pages/_app.js

import Layout from '../components/layout'

export default function MyApp({ Component, pageProps }) {
  return (
    <>
      // Layout でアプリケーションのコンポーネントを囲むだけ
      <Layout>
        <Component {...pageProps} />
      </Layout>
    </>
  )
}

ページごとに異なるレイアウトを扱う場合

ページごとに異なるレイアウトが必要な場合は getLayout でページごとにレイアウトを定義します。 getLayout を使用してレイアウトの切り替えを行うことで、コンポーネントツリーがページ遷移間で維持されるため永続的にレイアウト内の state を保持することができます。 コンポーネントツリーが維持されると React は state を維持しつつ、必要な要素のみ再レンダリングします。

// pages/index.js

import Layout from '../components/layout'
import NestedLayout from '../components/nested-layout'

export default function Page() {
  return {
    /** Your content */
  }
}
// 個別のページごとにレイアウトを定義する
Page.getLayout = (page) => (
  <Layout>
    <NestedLayout>{page}</NestedLayout>
  </Layout>
) 
// pages/_app.js

export default function MyApp({ Component, pageProps }) {
  // ページごとに定義されたレイアウトがある場合はそれを使用する
  const getLayout = Component.getLayout || ((page) => page)

  return getLayout(<Component {...pageProps} />)
}

データフェッチ

Layout 内でのデータフェッチはクライアント側でのみ可能です。 Layout はページではないため、今のところ getStaticPropsgetServerSideProps は使えません。

プレビュー

Next.js 公式リポジトリのサンプルがわかりやすいのでぜひ触ってみてください。

Open in StackBlitz