echo("備忘録");

IT技術やプログラミング関連など、技術系の事を備忘録的にまとめています。

【Serverless Framework】Serverless FrameworkでAppSync APIを作成する

はじめに

前回、および前々回の記事で、AWS AppsSync(以下「AppSync」)のOpenID Connect認証、およびLambda認証について書きました。

となれば今回はIAM認証かな...とも思ったんですが、今回はちょっと話題を変えて、自分も大好きなServerless FrameworkでAppSync APIを作成する方法について書きたいと思います。

Serverless Framework

TL;DR

  • Serverless Frameworkでは、デフォルトではAppSyncはサポートされていない
  • でも「serverless-appsync-plugin」を使えば、AppSyncを作成できる
  • そのやり方

参考サイト

serverless-appsync-pluginについて

現在(2021/9/26時点)、Serverless Frameworkの公式機能としては、AppSyncはサポートされておらず、Lambdaのトリガ元として設定することもできません。(リゾルバが必ずしもLambdaとは限らないので、当然と言えば当然ですが)

ですのでServerless Framework標準機能でAppSyncを作成する場合、resourcesセクションで、生のCloudFormation(以下「Cfn」)のテンプレートを記載する必要があります。

しかし、Serverless FrameworkのPluginとして「serverless-appsync-plugin」というものが公開されており、これを使うことで、生のCfn構文を書かなくてもServerless FrameworkでAppSyncを作成することができます。

早速使ってみる

というわけで、まずはインストールします。
以下コマンドでインストールを行います。

> npm install serverless-appsync-plugin --save-dev

そして、serverless.ymlの「plugins」セクションに「serverless-appsync-plugin」を追加します。

plugins:
  - serverless-appsync-plugin

serverless.ymlを見ながら説明

「百聞は一見に如かず」ということで、ここからは実際のserverless.ymlを元に説明します。(関係ない定義は省略)

また全部説明するとキリがないので、appSyncキーの子要素&重要ポイントだけ説明します。(各項目の詳細説明は「参考サイト」のGitHubページに全て書いてあります。)

なお今回は、

というAppSync APIを作成しています。

あとLambda関数「authL」「treasureL」の処理内容は、前回の記事のそれと全く同じです。

service: 'terraform-sample'
frameworkVersion: '2'
  
custom:
  appSync:
    name: appsync-plugin-sample-api2
    # apiId: zn2mpenctja3neujg7m2vaiw3m
    authenticationType: AWS_LAMBDA
    schema: schema.graphql
    lambdaAuthorizerConfig:
      # functionName: # The function name in your serverless.yml. Ignored if lambdaFunctionArn is provided.
      # functionAlias: # optional, used with functionName
      lambdaFunctionArn: { Fn::GetAtt: [AuthLLambdaFunction, Arn] }
      # identityValidationExpression: # optional
      authorizerResultTtlInSeconds: 300
    # openIdConnectConfig:
    #   issuer:
    #   clientId:
    #   iatTTL:
    #   authTTL:
    logConfig:
      loggingRoleArn: arn:aws:iam::659547760577:role/administratorRole
      level: ALL
      excludeVerboseContent: false
    defaultMappingTemplates:
      request: false
      response: false
    mappingTemplates:
      - type: Query
        request: false
        response: false
        dataSource: dataSourceLambda
        field: treasureL
    dataSources:
      - type: AWS_LAMBDA
        name: dataSourceLambda
        description: 'Lambda DataSource for appsync-plugin-sample-api2.'
        config: # Either of functionName or lambdaFunctionArn must tbe provided. When both are present, lambdaFunctionArn is used.
          # functionName: graphql
          lambdaFunctionArn: { Fn::GetAtt: [TreasureLLambdaFunction, Arn] }
          serviceRoleArn: arn:aws:iam::659547760577:role/administratorRole
  
plugins:
  - serverless-appsync-plugin
  
functions:
  treasureL:
    handler: ./dist/treasureL/index.handler
  
  authL:
    handler: ./dist/authL/index.handler

大前提として「custom」セクション内に「appSync」キーを設定し、その中にAppSync APIの定義を書いていきます。

また上記serverless.ymlではしていませんが、AppSync APIの定義を配列で複数個定義することにより、一度に複数個のAppSync APIを作成できます。(同一スタックで管理したい場合)

「appSync」キーには、下表のキーが設定できます。

なお、とりあえず新規にAppSync APIを作る場合、

  • name/authenticationType/schemaを設定する
  • authenticationTypeに対応した認証設定を該当キー(lambdaAuthorizerConfigなど)で行う
  • dataSourcesでデータソースを定義する
  • mappingTemplatesでリゾルバの定義&データソースとの紐づけ(Query/Mutation)を行う

の手順を踏めばOKです。

項目名 説明 備考
name AppSync APIAPI
apiKey AppSync APIAPI KEYの値 既存のAppSync APIの更新/削除時のみ必要。(新規作成時は不要)
apiId AppSync APIAPI IDの値 指定した場合、該当のAppSync APIのみ更新する(?)(新規作成時は不要)
authenticationType APIの認証形式。API_KEY, AWS_IAM, AMAZON_COGNITO_USER_POOLS, OPENID_CONNECT, AWS_LAMBDAから選択 AWS::AppSync::GraphQLApiのそれと同じ
schema スキーマ定義ファイルのパス(globパターンでの指定可能) デフォルトは「schema.graphql」
caching キャッシュの設定
userPoolConfig 認証用Cognitoユーザープールの設定 authenticationTypeで「AMAZON_COGNITO_USER_POOLS」を選んだ場合のみ反映される
lambdaAuthorizerConfig 認証用Lambdaユーザープールの設定 authenticationTypeで「AWS_LAMBDA」を選んだ場合のみ反映される
openIdConnectConfig OIDC認証の設定 authenticationTypeで「OPENID_CONNECT」を選んだ場合のみ反映される
apiKeys 認証用API KEYの設定 authenticationTypeで「API_KEY」を選んだ場合のみ反映される
additionalAuthenticationProviders 「追加の認証プロバイダー」の設定
logConfig 「ログ記録」の設定 excludeVerboseContentは「詳細なコンテンツを含める」の設定(デフォルトはfalse)
defaultMappingTemplates 「MappingTemplates」のrequest/responseのデフォルト設定 ダイレクトLambdaリゾルバのように全部(または大部分)が同じ設定の場合、個々を設定すると個々の「mappingTemplates」で設定する必要がなくなる(あるいは省略できる)
mappingTemplates ゾルバのデータソース、およびマッピングテンプレートの設定 配列形式
functionConfigurations AppSync関数の設定 配列形式。省略可
dataSources データソースの設定 配列形式
substitutions mappingTemplatesで指定した全リゾルバに渡す変数(variables)の設定
xrayEnabled X-RAYの有効/無効の設定 デフォルトはfalse(=無効)
wafConfig WAF(AWS Web Application Firewall)の設定

※注意点

  • 上記serverless.ymlや「参考サイト」のGitHubページを見るとわかる通り、省略可能な項目もあります。(上表はとりあえず一通りappSyncキーの子要素を洗いだしてあります)
  • 「lambdaAuthorizerConfig」及び(dataSources.typeで「AWS_LAMBDA」を指定した場合の)「dataSources.config」では、Lambda関数を下記2つのキーのいずれかの方法で指定できます。
    • functionName(Lambda関数名)
    • lambdaFunctionArn(Lambda関数のARN)
  • 上記はいずれか一方のみの指定となります。(両方指定した場合、lambdaFunctionArnが優先される)

実行結果

上記「serverless.ymlを見ながら説明」のserverless.ymlをデプロイした結果が以下の画像の通りです。

AppSync APIが作成され、設定も正しく反映されているのが分かります。

f:id:Makky12:20210926161402p:plain
f:id:Makky12:20210926161421p:plain

また、Rest Clientで実際にリクエストを投げてみても、ちゃんとLambda認証やリゾルバが正しく動いているのが分かります。

f:id:Makky12:20210926161434p:plain
f:id:Makky12:20210926161445p:plain

まとめ

以上、Serverless FrameworkでAppSync APIを作成する方法について書きました。

Serverless Frameworkは、こういう便利なPluginがたくさん公開されているので便利です。こういうのをどんどん利用して、環境構築を便利にしていきたいです。

てか、自分でもなんかPluginを作成&公開できるようになりたいなあ。(マジで。てはハンズオンとかあったら、有料でも参加したい...)

告知

私が昨年登壇した「VS Code Conference」ですが、今年もオンラインで開催されるそうです。
また、登壇受付も9/30(木)まで行っているそうなので、興味のある方は応募されてみてはいかがでしょうか?

ちなみに僕は、ネタがなかなか思い浮かばないのと、予定の関係で、今年はどうするか悩んでおります。

vscode.connpass.com

では、今回はこの辺で。

【AppSync】Lambda認証でGraphQLクエリを実行する

はじめに

前回の記事にて、Auth0とOIDC(OpenID Connect)認証でAWS AppSync(以下「AppSync」)経由でGraphQLクエリを実行する方法について書きました。

ただし、もちろんAppSyncにはOIDC以外にも認証を行う方法はいくつかあります。

今回はその中から、前回の最後にも書いた「Lambda認証」について書きたいと思います。

やること

  • Lambda認証の仕様&認証Lambdaを書く
  • AppSyncの設定
  • クエリを実行する

Lambda認証の仕様について

Lambda認証については、下記公式ドキュメントに記載されています。

docs.aws.amazon.com

上記公式ドキュメントを見ると、まずイベント引数(event)には下記の値が格納されるようです。

項目 説明 備考
authorizationToken Authorizationヘッダの値
requestContext 下記「requestContextの内容」が格納されたオブジェクト

※requestContextの内容

項目 説明 備考
apiId 該当APIAPI ID
accountId 該当APIがあるAWSアカウントのアカウントID
requestId 該当APIの呼び出しごとに付けられる一意のID
queryString 実行されたクエリ/ミューテーションの内容
operationName 実行されたクエリ/ミューテーションの名前
variables 実行されたクエリ/ミューテーションの変数にとして設定されたキー&値

また、レスポンスには下記項目を格納したオブジェクトを返す必要があります。

項目 設定値 必須 備考
isAuthorized 認証処理の結果。(OK:true/NG:false)
deniedFields 認証NG時のNGフィールド名(?)の配列 ARN形式またはTypeName.FieldNameの形式。
ゾルバで設定しても、強制的にnullになるとのこと。
resolverContext ゾルバのレスポンスコンテキストに設定したフィールドの値
ttlOverride 認証結果(のレスポンス)をキャッシュする時間 0の場合はキャッシュしない。
また未指定の場合、後述の「AppSyncの設定」の「Authorizer Response Cache TTL」に指定した値が適用される。

Lambda認証を行う場合、とりあえず下記の2点をおさえておけばOKです。

  • 認証Lambdaのイベント引数の「authorizationToken」キーにAuthorizationヘッダの値が格納される
  • レスポンスの「isAuthorized」に認証結果(true/false)を設定する。

実際に認証Lambdaを書く

では、実際に認証Lambdaを書きましょう。

といっても、先程の仕様に沿って、普通にLambda関数を書くだけです。

今回はサンプルということで、単純に「Authorizationヘッダの値が'hogehoge'の場合のみOKとする」処理にしています。

// event引数、およびレスポンスオブジェクトの定義。  
// (どっかに定義ファイルがあるのかも)  
export interface AppSyncAuthLambdaEvent {
  authorizationToken: string;
  requestContext?: AppSyncAuthLambdaEventRequestContext;
}
  
interface AppSyncAuthLambdaEventRequestContext {
  apiId: string;
  accountId: string;
  requestId: string;
  queryString: string;
  operationName: string;
  variables: any;
}
  
export interface AppSyncAuthLambdaResponse {
  isAuthorized: boolean;
  deniedFields?: string[];
  resolverContext?: any;
  ttlOverride?: number;
}  
  
import {
  AppSyncAuthLambdaEvent,
  AppSyncAuthLambdaResponse
} from '../../interface';
  
// OKにするAuthorizationヘッダの値  
const CORRECT_TOKEN: string = 'hogehoge';
  
// 関数本体  
export async function handler(
  event: AppSyncAuthLambdaEvent
): Promise<AppSyncAuthLambdaResponse> {
  console.info('Received event {}', JSON.stringify(event));
  const authToken: string = event.authorizationToken ?? '';
  
  const response: AppSyncAuthLambdaResponse = {
    isAuthorized: authToken === CORRECT_TOKEN
  };
  
  console.info('Response JSON {}', JSON.stringify(response));
  
  return response;
}

AppSyncの設定

次にAppSyncの設定です。

まずは前回同様、作成したAppSync APIをクリックし、左ツリーの「設定」をクリックします。
その後設定画面の「デフォルトの認証モード」項目で、下記項目に下記の値を入力して「保存」をクリックします。

項目 設定値 必須 備考
APIレベル AWS Lambda
AWSリージョン 上記認証LambdaがあるAWSアカウントのリージョン 大抵は「AP-NORTHEAST-1(東京)」だと思う
関数のARN 上記認証LambdaのARN リストから選択。表示されない場合「関数が表示されませんか?」リンクをクリックすると直接入力できる
Authorizer Response Cache TTL 認証結果(のレスポンス)をキャッシュする時間 認証Lambdaのレスポンスに「ttlOverride」を指定しない場合、この値が適用される。
デフォルト300秒(=5分)、0の場合はキャッシュしない、というのはttlOverrideと同じ
Token regex 許可するAuthorizetionトークンの正規表現パターン ここのパターンに一致しない場合、認証NGとなる

f:id:Makky12:20210913104107p:plain

クエリを実行する

では、実際にクエリを実行しましょう。 今回Rest Clientのみで確認します。

結果が下図ですが。「Authorization」ヘッダの値が「hogehoge」の場合は正しく結果が取得できており、逆に「hogehoge」ではない場合は「UnauthorizedException」が発生しているのが分かります。
ですのでLambda認証が正しく動いていると言えそうです。

f:id:Makky12:20210913104940p:plain
f:id:Makky12:20210913104959p:plain

また、一時的に「Token Regex」に以下の値(=半角数字のみ許可)を設定すると、例え「Authorization」ヘッダの値に「hogehoge」を設定しても「UnauthorizedException」が発生しているので、これも問題なさそうです。

f:id:Makky12:20210913105539p:plain f:id:Makky12:20210913105525p:plain

まとめ

以上がLambda認証でGraphQLクエリを実行する方法でした。

Lambda認証を使えば、独自処理を色々追加するなど、割とカスタマイズが出来そうなので、臨機応変というか柔軟な対応ができると思います。(その分管理とかが大変そうだけど)

さて次ですが、やはり残るAppSync認証のいずれかを記事にしようか...と考えてます。(IAMかCognitoあたり。APIキーはボリュームが少なそうで...)

あるいは、また別のネタでも書こうかな。(ネタ自体はそれなりにあるので)

それでは、今回はこの辺で。

【AppSync】Auth0 + OpenID Connect認証でGraphQLクエリを実行する(内容修正版)

はじめに

先日、初めて業務でAWS AppSync(以下AppSync)とGraphQLを使いました。
サブスクリプションが使えたり、クライアント側からフィールド指定出来たりできるのが良い感じです。
あと、エンドポイントの管理がAPI Gatewayより楽ですね。(無論、デメリットもありますが)

ただAPI Gatewayもそうですが、アプリとして使う以上避けては通れないのが「認証」です。

今回は、そんなAppSyncの認証の中から「OIDC(OpenID Connect)認証」のやり方を説明したいと思います。

またOIDC認可サーバーには、Auth0を使いたいと思います。

TL;DR

  • AppSyncでOpenID Connect(以下OIDC)認証を使う方法
  • OIDC認証にはAuth0を使用
  • AppSyncコンソール&クライアント(今回はRest Client)からクエリを実行する方法

注意

(2021/8/27記載) とりあえず自分の備忘録的にまとめたので、本当にメモ書き程度になってます。
時間があれば、もう少し丁寧な文章に直します。

※2021/9/11追記:直しました

参考blog

AWS AppSyncでAuth0を認証プロバイダーとしたOIDCを設定する | DevelopersIO

やること

  • Auth0の設定
  • AppSyncの設定
  • クエリ実行

Auth0の設定

※Auth0のアカウントは持ってる前提で進めます。
※持っていない場合はあらかじめ取得しておいてください。

APIの新規作成

左ツリーから[Applications] - [APIs]を選択し、右に表示される[Create API]ボタンをクリックしてください。

すると「New API」画面が表示されるので、下記項目を入力して「Create」ボタンをクリックしてAPIを新規作成してください。(すべて必須項目)

項目 説明
Name APIの名前
Identifier APIの一意の識別子。APIエンドポイントURLが推奨。(違っててもいい)
Signing Algorithm 署名アルゴリズム必ず「RS256」を選択すること!(「HS256」はダメ)

プロバイダードメインの確認(「AppSyncの設定」で必要)

左ツリーの[Applications] - [Application]を選択し、「APIの新規作成」の「Name」と同名のApplicationをクリックします。

するとアプリの情報が表示されますが、その「Settings」タブ内の「Domain」に記載のURLがプロバイダードメインになります。
f:id:Makky12:20210911190818p:plain

アクセストークンの確認(「クエリ実行」で必要)
再度左ツリーから[Applications] - [APIs]を選択し、「APIの新規作成」で作成したAPIをクリックします。
APIの情報が表示されるので、「Test」タブをクリックし、その中の「response」項目に移動します。(プログラム言語のタブは何でもいい)

その中の「access_token」キーの値が、そのままアクセストークンの値になります。
f:id:Makky12:20210911191800p:plain

てか、認証処理を実施せずともアクセストークンが確認できるのは、すごい便利だなと感じました。

AppSyncの設定

※AppSync APIは作成済の前提で進めます。
※作成していない場合はあらかじめ作成しておいてください。

作成したAppSync APIをクリックし、左ツリーの「設定」をクリックします。 すると設定画面が開くので、「デフォルトの認証モード」項目で、下記項目に下記の値を入力して「保存」をクリックします。

  • APIレベル」:「OpenID Connect」
  • OpenID Connect プロバイダードメイン (発行者 URL)」:「Auth0の設定」の「プロバイダードメインの確認」で確認したプロバイダードメイン
    • https://」始まりであることに注意してください。

f:id:Makky12:20210911194540p:plain

クエリ実行

では、準備はできたので、実際にクエリを実行しましょう。
今回はAppSyncコンソール、およびクライアントPCから実行ます。

AppSyncコンソールから実行

  • 作成したAppSync APIをクリックし、左ツリーの「クエリ」をクリックします。
  • その後、「使用する認証モード」が「AppSyncのOIDC認証設定」の「プロバイダードメイン名」が選択されていることを確認します。
    • これはデフォルトでなっているはずです。
  • 何かしらクエリを記載し、右上の「Authorization Token」に「Auth0の設定」の「アクセストークンの確認」で確認したアクセストークンをコピペします。
  • 「クエリ実行」ボタンをクリックし、クエリを実行します。

これでクエリが実行され、正しく実行結果が表示できるはずです。
f:id:Makky12:20210911194914p:plain

また、アクセストークンが未設定or間違っているなら、結果が「Request failed with status code 401」となります。
f:id:Makky12:20210911194945p:plain

クライアントから実行(今回はRest Client)
※今回はリクエストにVS Codeの定番拡張機能である「Rest Client」を使っていますが、リクエスト方法は何でもいいです。

下図のように、以下ヘッダを設定して、クエリをPOSTリクエストして実行します。

  • Method:POST(GETではない)
  • 「X-REQUEST-TYPE」:「GraphQL」固定
  • 「Authorization」:「アクセストークン確認方法」に記載のaccess_token
  • Bodyに実行するクエリ、および変数の値を設定する。
  • URLは- AppSyncコンソールの「設定」内の「API Details」-「API URL」に記載されているURL

これもアクセストークンが正しいなら、正しくクエリ実行結果がレスポンスで返ってくるはずです。
またアクセストークンが未設定or間違っているなら、「UnauthorizedException」が返ってくるはずです。      f:id:Makky12:20210911195851p:plain
f:id:Makky12:20210911195904p:plain

まとめ

以上がAuth0 + OpenID Connect認証でGraphQLクエリを実行する方法でした。

OIDC認証と聞くと難しそうですが、Auth0を使うと驚くほど簡単に設定できてしまうので、自分でもびっくりしました。
てかAuth0、めちゃくちゃ便利ですね。マジで衝撃でした。

さて今後ですが、AppSync認証にはOIDC以外にも方法があるので、その中からLambda認証あたりをブログにしようかな?なんて考えてます。

それでは、今回はこの辺で。

参考:RestAPI(API Gateway)と比べたGraphQL API(AppSync)のメリット・デメリット

メリット

  • Web API実行URLの管理が容易
    • API Gatewayと違い、AppSyncはクエリ/ミューテーションがどれだけ増えようと、Web API実行URLが1つなので、URLの管理が容易です。
      • API GatewayはWeb APIが増えれば増えるほど、Web API実行URLが増えます。
    • ただしAppSyncは「スキーマの管理が厄介」の問題があるので、結果イーブンかも
  • サブスクリプション機能で、変更を即座にクライアントに通知できる
    • GraphQLには「サブスクリプション」の機能があり、これを使用することで、データに変更があった場合、即座にクライアントにそれを通知できます。
    • ただしサブスクリプションの使用には、WebSocketが必要になります。
  • 必要なフィールドをクエリ発行側(≒クライアント)が指定できる
    • 必要なフィールドをクエリ発行側が指定することができ、以下のメリットがあります。
      • 必要最小限なフィールドを指定する事で、データ通信量の削減が可能
      • (少ない通信量で)同一クエリの使いまわしが可能

デメリット

  • スキーマの管理が厄介
    • GraphQLでは、「スキーマ」と呼ばれる定義ファイルにクエリ/ミューテーション/サブスクリプションなどを定義し、管理することになります。
    • Web API実行URLは1つですが、その分スキーマの管理の手間が増えます。(さっき「結果イーブンかも」と書いたのはそのため)
  • マッピングテンプレートが面倒
    • リソルバには「マッピングテンプレート」があり、リクエスト/レスポンスそれぞれでこのマッピングテンプレートを記載する必要があります。
    • これは「Apache Velocity テンプレート言語 (VTL)」で書くのですが、これがなかなか面倒だったりします。
    • ただし、リソルバに「ダイレクトLambdaリソルバ」を使用する場合に限り、マッピングテンプレートを記載せずともOKです。
  • クエリ/ミューテーション個別の設定がやりにくい
    • Web API実行URLは1つである分、クエリ/ミューテーション個別の設定はやりにくいです。
    • 例えば「クエリAは未ログインでも実行可能/クエリBはログイン者のみ実行可能」という場合でも、クエリA/Bそれぞれに個別にオーソライザを設定できないため、不便になります。

【DynamoDB】DynamoDBデータのページネーション処理について

はじめに

お久しぶりです。だいぶ間が開いてしまいました。

前回から色々ありまして、結果として8月から新しい就業先で働いてます。(この辺はまた別の機会に書きます)

で、新しい環境でもAWSに携わっていますが、 コードレビューなどを通して、(知ってたつもりでも)色々知らなかった事や勉強になる事が多いなあ...と感しています。

そこで今回は基本に立ち返るという意味で、ビギナー向けネタとして「DynamoDBデータのページネーション処理」について書こうと思います。

前提:ページネーション処理について

元々は「Webページに長い文章を掲載する際に、同じデザインの複数のページに分割し、各ページへのリンクを並べたもの」という意味です。

が、今回はDynamoDBのデータについて「該当データを一定個数ごとに区切る処理」という意味合いで使います。

例えばGoogle等が「検索結果を一定データ数ごとにページ区切りする」感じです。 f:id:Makky12:20210821082533p:plain

DynamoDBのデータ取得の制限

DynamoDBでデータ取得する場合、主にQueryやScanを使うと思いますが(※)、QueryやScanで取得できるデータには「最大1MBまで」という制限があります。

なので「該当データを(データ量や件数に関係なく)全件取得する」という事はできません。

※Getはページネーション自体がいらない(=1件しか取得できない)ので省略。

docs.aws.amazon.com

なので、データ量が多い場合、全データを取得する場合は「何回かに分けて取得する」処理(≒ページネーション処理)が必要になってきます。

基本的なページネーション方法(LastEvaluatedKey, ExclusiveStartKey)

先程のAWS公式サイトの説明にもある通り、DynamoDBのQueryやScanには、戻り値の中に「LastEvaluatedKey」というキーが定義されています。

QueryやScanの実行時に全件取得できなかった場合、この「LastEvaluatedKey」に最後のデータのパーティションキー情報が格納されます。(全件取得できた場合は、そもそも「LastEvaluatedKey」キーが存在しない(※))

そして「LastEvaluatedKey」から続きのデータを取得する方法ですが、QueryやScanには実行時のパラメータに「ExclusiveStartKey」というキーがあり、ここに先程の「LastEvaluatedKey」キーの値を指定することで、前回取得したデータの続きから取得することができます。

※ただし後述の「Limit」設定時に「ちょうど全データ取得できた場合」はLastEvaluatedKeyに値が入ってきます。

例えば、末尾に記載した(僕の記事ではお約束の)「ドルアーガの塔 宝物取得リスト」に対して下記ソースを実行すると...

const AWS = require('aws-sdk');
  
async function scanSample() {  
  
    const dc = new AWS.DynamoDB.DocumentClient();
  
    const data = await dc.scan({
        TableName: 'tower-of-druaga',
        Limit: 6
    }).promise();
  
    console.log(`[data] ${JSON.stringify(data)}`);
  
    const data2 = await dc.scan({
        TableName: 'tower-of-druaga',
        Limit: 6,
        ExclusiveStartKey: data.LastEvaluatedKey
    }).promise();
  
    console.log(`[data2] ${JSON.stringify(data2)}`);
  
    return;
}

結果として、下記ログが書き込まれます。

[data] {
    "Items": [
        // Floor1~6までの情報
    ],
    "Count":6,
    "LastEvaluatedKey":{
        "Floor":6,
        "Type":"treasure"
    }
}

[data2] {
    "Items": [
        // Floor7~10までの情報
    ],
    "Count":4,
}  
  
/*ちなみに、Limitを5にするとこうなる */  
[data] {
    "Items": [
        // Floor1~5までの情報
    ],
    "Count":5,
    "LastEvaluatedKey":{
        "Floor":5,
        "Type":"treasure"
    }
}

[data2] {
    "Items": [
        // Floor6~10までの情報
    ],
    "Count":4,
    "LastEvaluatedKey":{
        "Floor":10,
        "Type":"treasure"
    }
} 

問題点

ただし、先述の通りDynamoDBには「最大1MBまで」という制限があるので、各データの内容によっては件数にばらつきが生じる(=最大1MBを超えない最大件数になる)ことがあります。

例えば取得対象データが下表のような場合、QueryやScanを実行すると取得件数は1回目は5件、2回目は2件となります。(さすがにこれは極端ですが)

No サイズ(KB)
1 200
2 200
3 200
4 200
5 200
6 500
7 500
8 700

もちろんそれが問題にならない場合は良いんですが、例えば「画面表示の関係で、毎回決まった件数を取得したい」というような場合が問題です。

対策

上記問題への対策ですが、QueryやScanの実行時パラメータには他にも「Limit」というものがあり、「取得するデータ件数」を指定することができます。

なのでこれを指定することで、取得件数を固定することができます。(例えば上表の場合、Limit=1にすれば毎回1件のみの取得に固定することができます)

ただしLimitを付けたとしても、「最大1MBまで」の制限はあるので注意です。(例えば上表の場合に「Limit=3」にしても、3ページ目はNo.7の1件のみになる)

これに対しては、下記のような対策があります(設計段階での対策)。

  • 「Limit(=1ページに表示する件数) × 1データのデータサイズ」が、1MBよりも余裕をもって下回るようにLimitの値を設定する
  • 各データのデータサイズにあまり大きな差が出ないようにしておく

あえて全件取得するには?

ここまでは「何回かに分けて全データを取得する方法」について書きました。

ただし実際にDynamoDBからデータを取得するケースとして「クライアントからAPI Gateway等にリクエストして、そこからLambdaで取得処理を実行して...」というケースも多いと思います。

そしてその場合、何回もリクエストが発生するのを防ぐために「可能であれば1回のリクエスト(=Lambda実行)で全データを取得したい」というケースもあると思います。

その場合は、1回のLambdaの処理で何度もQueryやScanを実行して全データを取得するような処理を(ループ処理や再起処理で)書けばOKです。(サンプルコードを下記に記します)

なおAPI Gateway(AppSyncも)には30秒という時間制限があるので、あまりにも大量のデータを1度に取得しようとすると、タイムアウトが発生してしまうので注意です。(これはRDSでも同じです)

/*
* ループで書く方法
*/   
async function scanSampleLoop() {

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

    let items = [];
    let exclusiveStartKey = null;  
  
    while(true) {
  
        const param = {
            TableName: 'tower-of-druaga',
            Limit: 5
        };
  
        if (exclusiveStartKey) param.ExclusiveStartKey = exclusiveStartKey;
  
        const data = await dc.scan(param).promise();
  
        items = items.concat(data.Items);
        if (!data.LastEvaluatedKey) break;
  
        exclusiveStartKey = data.LastEvaluatedKey;
    }
  
    return;
}  
   
/*
 * 再起処理で書く方法
*/
async function scanSampleRecursive(exclusiveStartKey = null) {
  
   const dc = new AWS.DynamoDB.DocumentClient();
  
    const param = {
        TableName: 'tower-of-druaga',
        Limit: 5
    };
  
    if (exclusiveStartKey) param.ExclusiveStartKey = exclusiveStartKey;
  
    const data = await dc.scan(param).promise();
  
    let items = [];
    if (data.LastEvaluatedKey) {
        items = data.Items.concat(await scanSampleRecursive(data.LastEvaluatedKey));
    } else {
        items = items.concat(data.Items);
    };
  
    return items;
}

まとめ

というわけで、今回は基本に立ち返って、ビギナーの方向けに記事を書きました。

冒頭にも書いた通り「今まで知ってたつもりでも、意外と理解しきれていなかった」と感じるところが多かったので、自分の復習も兼ねて、今後もこういったビギナー向けの記事を書くのも良いなあと感じています。

もちろん、ビギナー向けではないような記事も書いていくつもりです。(バタバタも落ち着いて、新しい就業先で働いたのもありますし)

それでは、今回はこの辺で。

参考:「ドルアーガの塔 宝物取得リスト」について

ドルアーガの塔 宝物取得リスト」は「ドルアーガの塔」というゲームの宝物情報(全60階分)をKey-Value形式でまとめたものです。(ただし、今回は10階まで)

※「ドルアーガの塔」が分からない人はググってみてください。

構造は下記の通りとなっています。

フィールド名 キーの種類 説明
Type パーティションキー 格納情報の種類を表す。今回は「treasure」固定
Floor ソートキー 階数(1~60)が格納される。実質RDBでいう主キー。
Detail なし 実際の宝物情報(名前、効果、出現条件など)が格納されている。(今回の記事では未使用)

【Node.js】Node.js version16の新機能 その2(Sleep機能)

はじめに

先日(2021/6/28)、「Node学園 36時限目 オンライン」というイベントで「Node16の新機能の紹介」という内容で発表させて頂きました。(視聴&リアクションくださった方々、ありがとうございました)

nodejs.connpass.com

その中の発表資料でSleep機能(C#JavaのThread.sleep()みたいな機能)のコードを書いていたのですが、発表後のトークTwitterで「Node.js16から、Sleep機能がCore機能でサポートされたんですよ」ということを教えて頂きました。

それは知らなかった!ということで、Node.js16でのSleep機能をさっそく試してみました。

Node.jsでのSleep機能について

Sleep機能について、Node.js14まではCore APIでのサポートがなかったため、下記の方法で代用する必要がありました。

  • コードで代替(setTimeOut関数を使用、下記ソース参照)
  • 各種npmモジュールを使用する
// Sleep処理を実行するコード
const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));  
  
async function main() {
    // 指定したミリ秒数、処理をSleepする。
    await _sleep(1000);
}

しかし、Node.js16から「Timers Promiss API」が使えるようになり(※)、これを使うことで、代替コードを書くことなしでSleep機能を使用することができます。

※ 正確には、Node.js15でExperimentalとして追加されたものが、Node.js16でStableになった

実際実装してみる

では、実際にSleep機能を実装してみます。

Timers Promise APIは「timers/promises」をimport(またはrequire)することで使用できます。
また、実際のSleep機能はTimers Promise APIの「setTimeout」関数を使用することで実現できます。(結果として、setTimeout関数を使うのは同じ)

参考:https://nodejs.org/dist/latest-v16.x/docs/api/timers.html#timers_timers_promises_api

async function main() {  
    // const { setTimeout: sleep } = require('timers/promises');
    const { setTimeout } = require('timers/promises');
    
    // 指定したミリ秒数、処理をSleepする。
    // Timers Promise APIのsetTimeOut関数だけで実装できるので、
    // 独自コードは不要になった。  
    const before = Date.now();
    await setTimeout(3000);  
    const after = Date.now();  
    
    // 大体3000くらいの値になる。
    console.log(`経過時間:${after - before}`);  
}

なお、エイリアスをつけておけば、従来のsetTimeout関数との併用もできます。

const { setTimeout: sleep } = require('timers/promises');
const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); 
  
async function main() {  
    
    // timers/promisesのsetTimeout関数と従来のsetTimeout関数を併用する。
    const before = Date.now();
    await sleep(3000); 
    await _sleep(5000);   
    const after = Date.now();  
    
    // 大体8000くらいの値になる。
    console.log(`経過時間:${after - before}`);  
}

setTimeout以外の関数

setTimeout以外にも、タイマー系関数の「setInterval, setImmediate」もTimers Promise APIでサポートされています。

※ただsetImmediateは、正直Timers Promise APIでの用途がいまいちわかっていない...コールバック関数の実行ならわかるんだけど、戻り値を返すって、どういう用途に使うんだろうか?

const { setInterval, setImmediate } = require('timers/promises');
  
async function main() {  
    
    // timers/promisesのsetIntervalを使用する。
    console.log('timerPromise started');
    const iterators = setInterval(1000);
    let cnt = 0;
  
    for await (const iterator of iterators) {
        cnt++;
        console.log(`${cnt.toString()} 回目の実行`);
        if (5 <= cnt) break;
    }  
    
    // timers/promisesのsetImmediateを使用する。  
    const res = await setImmediate("immediate!");  
    console.log(`res is ${res}`);  
} 

結果:
f:id:Makky12:20210701101849p:plain

まとめ

Sleep機能を始め、setIntervalのpromiseを公式がサポートしてくれたのは良いなあと思いました。

ていうか、もっといろいろとチェックしておかないとなあ、とも感じました。

それでは、今回はこの辺で。

【Node.js】Node.js version16(ES2021)の新機能 ※2021/6/26更新

はじめに

2021年4月に、Node.jsのversion10がサポート終了となり、AWSからも「Lambdaのバージョンを更新してください」という案内が来てました。

そして、それと同じタイミングでversion16がリリースされました。
今年の10月にステータスがActiveになりますので、そうなったらLambdaでもサポートされると思われます。

そこで、Node.js version16(=ES2021)の新機能について、確認してみました。

version16の新機能一覧

  • replaceAll
  • Promise.any
  • WeakRefs
  • Finalizers
  • numeric Separators
  • Logical Assignment

※numeric separator, 及び演算子(operator)系は省略します。
→ 2021/6/26 追記しました。

参照ページ

replaceAll

これは、名前の通り「(文字列の)一括置換」を行うメソッドです。(直球)

今まで一括置換を行う場合、replace()関数で置換対象文字列に正規表現リテラルを指定しなければならなかったですが、replaceAll()関数を使えば、単に文字列を指定すればOKです。(下記ソース参照)

知らないと結構ハマる部分だと思うので(特に多言語を知っている人がJavaScriptを触る場合)、専用の関数ができたことで分かりやすくなったと思います。

const baseStr = 'hoge|fuga|piyo';  
  
// これだとreplacedは「hoge_fuga|piyo」になる。(最初の一致文字列しか置換しない)  
const replaced = baseStr.replace('|', '_');   
  
// replace関数の場合、一括置換は正規表現リテラルを指定しないといけない。  
const replacedGlobal = baseStr.replace(/|/g, '_');  
  
// replaceAll関数なら、正規表現リテラルを指定しなくても一括置換してくれる。    
const replacedAll = baseStr.replaceAll('|', '_');  

Promise.any

version12のPromise.allSettledに続き、Promiseの機能追加です。

これは「引数に指定した複数のPromiseのうち、いずれか1つでもresolveされたら、そのうち最初にresolveされたPromiseを返す」というものです。
逆に全Promiseがrejectされた場合は、「AggregateError」というエラーをthrowします。(≒rejectされる)

Promise.raceと似ていますが、Promise.raceとの違いは「いずれか1つでもresolveされたPromiseがあったら、エラーにならない」という点です。(Promise.raceは、最初に結果が返ってきたPromiseがrejectならその時点でエラーになる)

なので例えば下記ソースのexecute関数を実行した場合、

  • Promise.any:「promise3 is resolved」とINFOログが表示される。(=正常終了)
  • Promise.race:「promise1 is rejected」とERRORログが表示される(=エラー発生)

という違いがあります。

const execute = async () => {
    try {
        // ここでPromise.anyかPromise.raceのいずれかを実行する。  
        // const promise = await Promise.any([promise1(), promise2(), promise3()]);  
        // const promise = await Promise.race([promise1(), promise2(), promise3()]);
        console.info(promise);
    } catch (e) {
        console.error(`error message is ${e.message}`);
    }
};

// 指定ms間処理をwaitさせる処理(多言語のsleepみたいなもの)
const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
  
// 1秒待ってエラーをthrow(=reject)
async function promise1() {
    await _sleep(1000);
    throw new Error("promise1 is rejected");
}
  
// 2秒待ってエラーをthrow(=reject)
async function promise2() {
    await _sleep(2000);
    throw new Error("promise2 is rejected");
}
  
// 3秒待って正常終了(=resolve)
async function promise3() {
    await _sleep(3000);
    return "promise3 is resolved";
}  

ちなみに、Promise.xxx系の各メソッドの挙動は下記のとおりです。

関数 正常終了する条件 エラーになる条件 正常終了した場合の戻り値
Promise.race 最初に結果が返ってきたPromiseがresolveされた 最初に結果が返ってきたPromiseがrejectされた 最初に結果が返ってきたPromise
Promise.all 全Promiseがresolveされた 1つでもrejectされたPromiseがある 全Promise(配列)
Promise.allSettled 必ず正常終了する なし(エラーになることはない) 全Promiseのstatus、およびvalue(resolve時)またはreason(reject時)(配列)
Promise.any 1つでもresolveされたPromiseがある 全Promiseがrejectされた 最初にresolveされたPromise

WeakRefs

今回の新機能で、一番分かりにくい部分だと思います。

直訳すれば「弱い参照」、MSNのサイトでは、下記の通りに記載されています。

A WeakRef object lets you hold a weak reference to another object, without preventing that object from getting garbage-collected.

訳:WeakRefオブジェクトは、ガベージコレクションを妨げることのない他のオブジェクトへの弱い参照を保持します。

とありますが、概要としてはこんな感じです。

  • WeakRefsを使用して、あるオブジェクトを参照できる。(値の取得など)
  • 「あるオブジェクト」は、WeakRefs以外から一切参照されなくなったら、ガベージコレクションの対象になる。(=破棄される)
  • 「あるオブジェクト」がガベージコレクションされたら、WeakRefsからも参照不可になる。

なおWeakRefsを使用するケースとしては「サイズが大きいオブジェクトのキャッシュやマッピング」です。

ソースで説明すると、下記の様な感じになります

  1. obj.valueにオブジェクトを定義。
  2. WeakRefインスタンスを作成し、参照するオブジェクトにobj.valueを設定し、その内容をログ出力。
  3. obj.valueをnullにし、WeakRef以外からの参照を無くし(=ガベージコレクションの対象にする)、その直後のWeakRefの弱参照オブジェクトの内容を出力
  4. 1秒ごとに、WeakRefの弱参照オブジェクトの内容を出力。(どこかでobj.valueガベージコレクションされる)
let logs = "";  
  
// ログの記載(これは毎回使う)
const log = (...args) => {
    log += args.map(a => JSON.stringify(a)).join("  ") + "\n"
    console.log(logs);
}
  
// 1
const obj = {
    value: { hoge: 'fuga'}
}
  
// 2  
// WeakRefの引数は、オブジェクトじゃないとダメ。(1とか'a'とかはNG)  
// WeakRef.deref()メソッドで、弱参照しているオブジェクトを取得できる
const ref = new WeakRef(obj.value)
log(ref.deref())
  
// 3
obj.value = null
log(ref.deref())
  
// 4
let n = 0
setInterval(() => {
    log(++n, ref.deref())
}, 1000)

で、結果はこうなりました。
f:id:Makky12:20210605074840p:plain

obj.valueをnullにしてから17秒後にガベージコレクションにより、ref.deref()の値が参照できなくなりました。

とはいえ、いつ参照できなくなるかはガベージコレクション次第なので、当然結果は毎回違います。
なので、MDNを始めいろんなサイトに書かれている通り、WeakRefは「できる限り使用しない」のが良いと思います。(使用するとしたら「異常にメモリを使用しているソースで、何がメモリを大量に使用しているか調べる」場合でしょうか?)

Finalizer

Finalizerは先程のWeakRefsに関連する機能で「WeakRefが弱参照しているオブジェクトがガベージコレクションにより参照不可になったこと」をトリガにして起動するコールバック関数を提供する機能です。
FinalizationRegistryクラス、およびそのregisterメソッドを使用して、Finalizerを実装します。

詳細はソースを見た方が早いと思いますので、下記ソースを参照。(WeakRefsのソースにFinalizerの実装を追加してます)

const log = (...args) => {
    document.getElementById("weakRefs").innerText += args.map(a => JSON.stringify(a)).join("  ") + "\n"
}
  
const obj = {
    value: { hoge: 'fuga'}
}
  
const ref = new WeakRef(obj.value);
  
// ここからがFinalizer実装部分。  
// FinalizationRegistryインスタンスを生成する。  
// コンストラクタ引数に、コールバック関数を定義する。  
// valueには、register()メソッドの第2引数が代入される。
const registry = new FinalizationRegistry(value => {
    log(`${value} は参照不可になりました。`);
});  
  
// FinalizationRegistry.register()メソッドにて対象のオブジェクト、  
// およびコールバック関数に渡す引数を設定する。  
// ここの第二引数(今回は文字列「obj.value.hoge」)が、コールバック関数のvalueに入る。  
// 
// なお第三引数のtokenObjは「登録解除トークンオブジェクト」で、unregister関数では  
// この「登録解除トークンオブジェクト」をキーに、対象のオブジェクト(今回はobj.value)を 
// Finalizerの登録から解除する。
// 今回は便宜上別のオブジェクトにしたが、第一引数と同じオブジェクトでOK。  
// (というより、同じにするのが一般的らしい)
const tokenObj = {token : 'token'};
registry.register(obj.value, "obj.value.hoge", tokenObj);  
  
// unregister関数を用いることで、Finalizerに登録したオブジェクト(今回はobj.value)を登録解除できる。  
// 引数に指定するのは「登録解除トークンオブジェクト」で、対象のオブジェクト自体ではない。  
// registry.unregister(tokenObj);
  
log(ref.deref())
  
obj.value = null
log(ref.deref())
  
let n = 0
setInterval(() => {
    log(++n, ref.deref())
}, 1000)

で、上記ソースの実行結果はこちら。(ちなみにWeakRefの時と、ガベージコレクションのタイミングが違っていることがわかると思います。このことからもWeakRefの挙動はガベージコレクション次第だというのが分かると思います。)
f:id:Makky12:20210605080605p:plain

確かにobj.valueガベージコレクションにより参照不可になった時点でFinalizerのコールバック関数が起動し、「obj.value.hogeは参照不可になりました」というログが出力されています。

ただ、WeakRefsが「できる限り使用しないのが良い」ので、Finalizerも出番は少ないかもしれませんね。(出番があれとすれば、やはり調査系でしょうか)

numeric separator(2021/6/26追加)

これは一言でいえば「数値の桁区切り」です。(「,」みたいなもの)
ソースコード上の数値を「_」で桁区切り記載する子tができます。

また10進数だけではなく、2進数や16進数の数値にも対応しています。

これはソースを見た方が早いので、ソースで説明します。

// NGの書き方の一例  
// const ng1 = _100_200_  // _が先頭や末尾にある   
// const ng2 = 100__200  // _が2つ以上連続してる
// const ng3 = 0_100_200 // 先頭(桁区切りの最上位の値)が0である  
    
// ちなみに区切り桁数に制限はないので、各区切りで桁数がバラバラという書き方も可能。  
// もちろん可読性が悪くなるだけなので、やる意味は皆無。
// const sample = 1_23_456_7890;  
  
// 10進数の桁区切り
const num1 = 100_000_000;
const num2 = 12_345_678;  
  
// もちろん普通に計算も可能
console.log(`num1 + num2 = ${num1 + num2}`);
  
// 16進数&2進数の桁区切り。  
// もちろんparseIntでの基数変換も可能
const hexNum = 0x01_2F;
console.log(`hexNum is ${parseInt(hexNum)}`);

const binNum = 0b1010_0101;
console.log(`binNum is ${parseInt(binNum)}`);
  
// parseIntで基数変換する場合、第一引数の指定に注意。  
// 本来文字列を指定するが、桁区切り文字列を指定すると、  
// 正しく動作しないので注意。
console.log(`${parseInt(hexNum)}`);  // OK  
console.log(`${parseInt(hexNum.toString(16), 16)}`);  // OK
console.log(`${parseInt("0x01_2F", 16)}`);  // NG(値は「1(=_の左側のみ)」になる

Logical Assignment(2021/6/26追加)

これは「代入演算子」ですが、下表の3つが追加されました。
それぞれ、下記のような代入が可能です。

代入演算子 主な使い方 動作の説明 詳細
??= a ??= b aがnullかundefinedの時のみ、aの値にbを代入する null, undefiend以外の場合は何もしない。
||= a ||= b aがfalsyな値の場合のみ、aの値にbを代入する falsyな値:null, undefiend, 0, false, 空文字
&&= a &&= b aがtruthyな値の場合のみ、aの値にbを代入する truthyな値:falsyではない値

サンプルソースはこちら。

// ??=の確認。  
// aはnullなので、a='a'となる。
let a = null;
a ??= 'a';
console.log(a);
  
// bはnullでもundefinedでもないので、b=''とならない。(0のまま)
let b = 0;
b ??= 'b';
console.log(b);
  
// ||=の確認。  
// cはfalsyな値なので、c='c'となる。
let c = false;
c ||= 'c';
console.log(c);
  
// dはfalsyな値ではないので、d='d'とならない。(trueのまま)
let d = true;
d ||= 'd';
console.log(d);
  
// &&=の確認。  
// eはtruthyな値ではないので、e='e'とならない。(falseのまま)
let e = false;
e &&= 'e';
console.log(e);
  
// fはtruthyな値なので、f='f'となる。
let f = true;
f &&= 'f';
console.log(f);

まとめ

一部省略しましたが、Node.js verison16(ES2021)の新機能をざっと紹介しました。

個人的に劇的に大きな変更というのはあまり無い感じですが、replaceAll, Promise.anyなんかは使いどころがありそうだなあ、と思いました。(特に前者)

便利な機能はどんどん実際のプロダクトにも導入して、ソース実装を便利にしていきたいですね。

告知

6/28(月) 19:00開催の「Node学園 36時限目 オンライン」というイベントで「node10→12/14で便利になった点」という内容で登壇することになりました。

内容は主にLambda実装の観点で「node version10→version12以降で役に立った機能」について紹介する予定です。
あと「node10→12/14」とありますが、時間があれば一部version16の機能も紹介する予定です。

※2021/6/26修正
登壇内容ですが、都合により「Node.js version16の新機能」に変更となりました。(ちょうど今回のブログのような登壇内容となります)
nodejs.connpass.com

それでは、今回はこの辺で。

【AWS】【Serverless Framework】CloudFormationでのリソースまとめ方(Part2)

はじめに

2か月以上前になりますが、「CloudFormationテンプレート(Serverless Frameworkのserverless.yml含む)でのリソースのまとめ方」について、ここに「同一プロジェクトで管理する場合」のメリット・デメリットを書きました。

あれから2か月、だいぶ間が開いてしまいましたが(プライベートでいろいろとありまして...)、今度は「別プロジェクトで管理する場合」について記載します。

別プロジェクトで管理する場合

「別プロジェクトで管理する」場合は、単に「どの単位でプロジェクトを分けるか」だと思います。

個人的に、主には以下の2つかなと思います。(もちろん他にもあるかもしれませんが)

  • ライフサイクル単位で分ける
  • サービス単位で分ける

なお、いずれの場合もフォルダ&ファイル構成はこうなります f:id:Makky12:20210530190110p:plain

下記ファイル&フォルダの説明は以下の通り

フォルダ名 説明 package.jsonおよびserverless.ymlの説明
rootPj ルートプロジェクト(全ての子プロジェクトを管理するためのフォルダ) ・複数プロジェクトで必要になるリソースの定義
・全プロジェクトのデプロイコマンドの定義
childPj(1~n) 子プロジェクト(特定の単位で分けたプロジェクト) ・自分のプロジェクトでのみ必要になるリソースの定義
・自分のプロジェクトのみのデプロイコマンドの定義

ライフサイクル単位で分ける

例えばストレージ系(S3など)は、一度デプロイしてしまえば、そうそう設定を変更することはないので、あまり追加でデプロイが必要になるケースは少ないと思います。

逆に、Lambdaなどは一度デプロイしても、機能追加・機能改修・不具合修正など、頻繁にデプロイする機会が多いと思います。(一番デプロイする機会が多いのがLambdaかも?)

また、同じLambdaのでも重要度が高い・アクセス頻度が高いなどで頻繁にデプロイするものもあれば、重要度が低い・実施頻度が低いなどで、デプロイ頻度が異なるものがあると思います。

こういった時に、ライフサイクルでプロジェクトを分けておけば、デプロイ頻度が高いリソースのデプロイ特に、毎回デプロイ更新が低いリソースも一緒にデプロイしなければならない...ということが減ります。

ただし、プロジェクト間でリソースの依存関係があった場合に、ちょっと厄介だったりします。(クロススタック参照など)

  • API Gatewayの情報を別プロジェクトのLambdaに渡す。
  • S3の情報を別プロジェクトのLambdaに渡す。(S3トリガなどの場合)

サービス単位で分ける

これはマイクロサービスアーキテクチャなどで複数のマイクロサービスを作成する際、マイクロサービスごとにプロジェクトを作成する方法です。(Azureの「リソースグループ」に似ています)

そのようにプロジェクトを分けることで、プロジェクト単位でデプロイできるため、マイクロサービスの管理がしやすくなります。

また、マイクロサービス単位で完結するようにすれば、ライフサイクル単位に比べて、依存関係の問題は発生しにくくなります。

ただし、同一マイクロサービス内で更新頻度の高いリソースと低いリソースが混在する場合に、やはりしなくていいリソースのデプロイが発生してしまいます。

また、プロジェクト間でリソースの依存関係の問題も、完全に解決する...というわけではありません。

メリット・デメリットまとめ(一覧表)

管理方法 メリット デメリット
1ファイルにすべて記載 リソースが少なければ管理が容易 ・リソースが多いと管理が大変
・リソース数の上限問題
スタックのネスト(親子関係をつける) リソース数の上限を気にしなくていい ・ネストのやり方に慣れるまでが大変
デプロイに時間がかかる
リソース種別で管理 ・不要なリソースのデプロイが減らせる
デプロイ時間が減らせる
・依存性の管理
サービス単位で管理 ・管理がしやすい
・依存性の問題が発生しにくい
・不要なデプロイが発生するケースもある
・依存性の問題がなくなるわけではない

まとめ

と、時間が空いてしまいましたが、CloudFormationでのリソースまとめ方について、2回に分けて説明しました。

実は1回目の記事を公開後、ちょっと間が開いてしまったのですが、5/18(火)にクラスメソッドさんの「AKIBA.AWS ONLINE #03 -IaC を語りたい 編- 」というイベントがありました。
dev.classmethod.jp

で、その中で

CloudFormationを使用していますが、Lambdaを1つ更新したいだけなのに全リソースを一括デプロイしなければならず面倒です。何か良い管理方法はないでしょうか?

という質問があり、「めちゃくちゃわかる!!!×100」と思った際、1回目の記事のことを思い出したので、今回その続きを書いた次第です。

ちなみに

私事なのですが、5/28(金)を持ちまして、2年間就業した今までの契約先での業務が終了になりました。

思えばここと契約しなければ、クラウドAWS、そしてServerless Frameworkに出会うこともなければ、各種イベントで登壇するなんてこともなかったわけで、そういう意味では私のエンジニア人生の転機となった場所であることは間違いないと思います。

ここ半年間、いろいろ思うところやそううつ病の関係、そして自分の将来を考えたときに「ずっとコンフォートゾーンにとどまっていていいんだろうか...」などと色々葛藤する部分があり、いろいろ悩みんだり、迷惑をかけてしまった部分もありましたが、本当に素晴らしい環境&メンバーと過ごすことができた、本当に幸せな2年間でした。

で、今はニートなわけですが、まずはしっかり休んでリフレッシュしながら、次の案件(or正社員での就職先)を探そうと思います。

また、今までなかなかできなかったAWSの勉強(AWS資格試験とか)を始め、各種スキルアップ、そして健康・体調面をしっかり治そうと思います。

それでは、今回はこの辺で。