echo("備忘録");

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

Serverless Framework はじめの一歩

はじめに

この記事は、QiitaのServerless Advent Calendar 2020 18日目の記事です。

qiita.com

なお来週12/25(金)にも、AWS Advent Calendar 2020最終日の記事として「aws-sdk-mockを使ったAWSのテスト」の記事を投稿しますので。そちらもよろしくお願いします。
qiita.com

今回の記事について

今年もAdvent Calendarが始まりました。
昨年と同様、Serverlessにもたくさんの記事が投稿されています。

そして、Serverless Frameworkに関する記事もたくさん投稿されています。

「Serverless」のアドベントカレンダーに参加するにあたり、最初はServerless FramewrokのTips系にしようと考えていました。(個人的にもServerless Framewrok大好きなので)

しかし、今回それ系の記事(=Serverless Frameworkをある程度知っている前提)を書く人が結構多いので、いっそのこと、Serverless Frameworkをあまり知らない人向けに記事を書くことにしました。

今回のメインターゲット

  • Serverless Frameworkを全く知らない
  • Serverless Frameworkは知っているが、やり方がよくわからない

Serverless Frameworkとは?

サーバーレスアプリを構成管理&デプロイするためのオープンソースツールです。
AWS CloudFormationやAzure Resource Managerなど、IaC(Infrastructure as Code)の機能を使い、コードベースでのサーバーレスアプリの構成管理を行ったり、各クラウドへのデプロイを行えたりします。

その他、AWS・Azure・GCPを始め、数多くのクラウドをサポートしています。(一番機能が豊富なのはAWS)

また、他にも下記のようなことができるのが特徴です。(あくまで一例)

  • 豊富なテンプレート(=プログラム言語対応)
  • Serverless FrameworkのダッシュボードからCI/CDを実現できる
  • 公式ドキュメントが結構充実している&英語も読みやすい(個人的には)
  • プラグイン(=拡張機能)をインストールして、機能を拡張できる

※Serverless Framework公式ページ: www.serverless.com

導入のメリットは?

※これ以降、すべてAWS前提で書きます。(先述の通り、一番機能が豊富なので)

もちろん先程挙げた点もメリットなのですが、1つだけ挙げるとするならば、個人的には

Lambda定義が非常にシンプル

な点だと思います。

実際、AWSのサーバーレスアプリで一番多く作成するリソースがLambdaだと思うので、Lambdaの定義が非常にシンプルなのは、非常に大きなメリットです。

具体例

例えばCloudFormation構文で、よくあるRest API的なLambdaを作成する場合、

  • API Gateway本体(Type: AWS::ApiGateway::RestApi)を作って...
  • API Gatewayのリソースパス(Type: AWS::ApiGateway::Resource)を作って...
  • API Gatewayのメソッド(AWS::ApiGateway::Method)を作って...
  • Lambdaに割り当てるIAM Role(AWS::IAM::Role)作って(Cloudwatchへの書き込み権限など)...
  • Lambda本体(AWS::Lambda::Function)を作って...
  • CloudWatchのLogGroup(AWS::Logs::LogGroup)作って...

みたいな感じで、すごく膨大な量の定義を作ることになり、めちゃくちゃ大変だと思います。(めちゃくちゃ長くなるので、ソースは省略)

これがServerless Frameworkだと、下記7行の定義だけで、上記のことがすべてできてしまいます。

# Serverless FrameworkのLambda定義例
Makky12Lambda:
  handler: makky12.handler
  name: Makky12Lambda
  events:
    - http:
        path: /hoge
        method: get

また、Lambdaはたいてい何かしらのトリガーで起動しますが、トリガ周りの定義も簡単です。 以下に一例を挙げます。(他にもたくさんあります。下記URLの各子項目を参照)

www.serverless.com

Makky12Lambda:
  handler: makky12.handler
  name: Makky12Lambda
  events:
    - http:  # API Gatewayトリガ
        path: /hoge
        method: get
    - s3:  #S3バケットトリガ(ファイル保存時に発生)
        bucket: makky12-bucket #バケット名
        event: s3:ObjectCreated:*
    - stream:  #DynamoDBトリガ(データ追加・変更・削除)
        type: dynamodb
        arn: (DynamoDBテーブルのARN)
    - schedule: cron(0 12 * * ? *)  #CloudWatchイベントトリガ(スケジューラ)

こんな感じで、Lambdaが非常に簡単に定義できてしまうのが、Serverless Frameworkの魅力の一つです。

インストール&プロジェクト作成

と、ここまででなんとなく概要は話しましたので、さっそくインストール&プロジェクト作成を行いましょう。

インストール

インストールは、下記コマンドで実施します。

# グローバルじゃなくてもいいけど、たぶんグローバルの方が便利だと思う。
> npm i serverless -g
プロジェクト作成

プロジェクト作成は、下記コマンドで行えます。

# 代表的なオプションは、下表を参照。
> serverless create --template aws-nodejs --name hoge  
  
# なお'serverless'コマンドには'sls'というエイリアスがあるので、  
# これでも実行可能。  
# ただしPowerShellだとエラーになる。(Select-Stringのエイリアスと
# 勘違いされる)
# 昔は'slss'というエイリアスがあったが、ver1.9あたりで削除された...
> sls create --template aws-nodejs --name hoge
オプション 意味 デフォルト値
--template 作成プロジェクト(言語など)のテンプレート 省略不可
--name プロジェクト名 Serverless Frameworkが命名
--path プロジェクト作成先フォルダ カレントフォルダ

※「--template」に指定するテンプレートの種類は、こちらを参照。 https://www.serverless.com/framework/docs/providers/aws/cli-reference/create/

プロジェクトを作成すると、プロジェクトを作成したフォルダに「handler.js」及び「serverless.yml」の2ファイルができると思います。(--templateで指定したテンプレートにより異なります。ここでは「aws-nodejs」を指定した前提で進めます)

※下画像では他のファイル・フォルダが出来ていますが、「handler.js」及び「serverless.yml」の2ファイルさえあればOKです。(「.serverless」フォルダはデプロイ時(正確にはパッケージ時)に出来ます) image.png

  • --templateで「aws-nodejs-typescript」を指定した場合、「serverless.yml」が「serverless.ts」になっていますが、定義する項目は同じです。
  • プロジェクトを作成するフォルダに既にpackage.jsonや(aws-nodejs-typescriptの場合)tsconfig.jsonがあるとエラーになるので、その場合は一度リネームしたり、別フォルダに退避してください。

serverless.ymlで確認する項目

といっても、ただ動かすだけならほとんど手を加える必要はないですが、「provider」セクションの下記項目だけ、確認&変更(コメント解除)しておきましょう。

項目 意味 デフォルト値 備考
region デプロイ先のリージョン。 us-east-1(バージニア北部) 多分、これ読んでる人は大抵がap-northeast-1(東京)かと
profile デプロイに使用するAWSプロファイル名 なし 設定すると、credential情報内の指定したprofile名の情報をデプロイ時などで使用する(いちいちコマンド実行時に指定しなくてよい)。
※コマンド実行時に指定することも可能。
  • credential情報は、下記コマンドやAWS CLIで作成できます。
    • AWS CLIについては各自調べてください
  • keyやsecretの値は、事前にIAMで作成しておいてください。
    • これも、わからなければ各自調べてください。
# keyはアクセスキー、secretはシークレットアクセスキー。  
# providerはクラウドごとに一意の値。(awsは「aws」固定)
# profileはプロファイル名(オプション)。省略時は「default」になる。
> serverless config credentials --provider aws --key key --secret secret --profile custom-profile

デプロイ&動作確認

では、ここまで終わったら、さっそくデプロイします。

「え?Lambdaは?」と思うかもしれませんが、serverless.ymlにはデフォルトでhandler.jsというjsファイル(=Lambdaのソースファイル)、およびこれをAPI Gateway経由で(=Rest APIとして)起動する定義がされていますので、Lambda周りは一切触らないでOKです。

デプロイは、下記コマンドで実施できます。

# -vオプションを付けると、デプロイ状況を確認できるようになる。
# (なくてもデプロイ自体はできる)
> serverless deploy -v

デプロイに問題なければ、下記のようなメッセージ作成され、実際にAWSを見ると、stackやAPI-Gateway、Lambdaが作成されているはずです。
image.png

動作確認ですが、Serverless Frameworkには「invoke」という、デプロイしたLambdaを実行できるコマンドがあるので、それを実施します。(もちろんcURLやRest Clientなどから実際に該当URLにリクエストを送って確認してもOKです。)

# ローカルで実行も可能だが(invoke local)、今回はデプロイしたLambdaの動作確認なので、実際にAWSのLambdaを実行する
> serverless invoke --function hello

レスポンスで200が返れば、OKです。 image.png

これで「Serverless Framework はじめの一歩」はクリアです。お疲れさまでした。

まとめ

いかがでしたか? ザックリとではありますが、Serverless Frameworkの概要をつかんでもらえたかなと思います。

Serverless Frameworkはそれだけでも非常に便利なツールですし、(今回は紹介しなかったけど)プラグインを使って、驚くほど便利な機能を色々追加することもできます。

ぜひ有効活用して、効率的なサーバーレスアプリ開発を行っていただければと思いますし、この記事がその一助になればと思います。

以上です。

【AWS】cfn-diagramでCloudFormationテンプレートをプレビューする

はじめに

以前、以下の記事で「VS Code Conference Japan」で登壇したということを書きました。

makky12.hatenablog.com

その際の登壇資料の「Azure Template Viewer」のスライド(14ページ)で、「CloudFormationにも、こういう拡張機能が欲しい...」と発表したのですが...

※Azure Template Viewerは、Azure Resource Manager(AWSのCloudFormationに該当する機能)のテンプレートの内容をダイアグラムで視覚的に確認できる拡張機能 f:id:Makky12:20201212152830p:plain

なんとCloudFormationにも、そういうツールがありました。(VS Code拡張機能ではないけど)

というわけで、今回はそのツールの紹介です。

cfn-diagram

そのツールは「cfn-diagram」という名前で、npmモジュールとして公開されています。

github.com

これを使うことで、Azure Template Viewerのように、CloudFormationテンプレートの内容をダイアグラムで視覚的に確認することができます。

f:id:Makky12:20201211195548p:plain

また、下記形式で出力できます。

  • Draw.io
  • HTML
  • 画像(まだexperimentalな段階。今回は詳しく取り上げません)

Draw.io形式で出力

Draw.io形式で出力する場合、VS Code拡張機能である「Draw.io Integration」が必要なので、事前にインストールしておきます。(VS Code Conference Japanでも紹介したやつです)

marketplace.visualstudio.com

インストールしたら、下記コマンドを実行します。

> cfn-dia draw.io -t (CloudFormationテンプレートファイルのパス)  
# 例:  
> cfn-dia draw.io -t ./template.yaml

すると、カレントフォルダに「template.drawio」ファイルが作成され、ダイアグラムがDraw.io Integration内に表示されます。
f:id:Makky12:20201212083006p:plain

また、下記条件でフィルタをかけることもできます。

項目 説明 備考
Filter Resources by Type リソース種別でフィルタリングを設定する AWS::IAM::Role, AWS::Lambda::Function など
Filter Resources by Name リソース名でフィルタリングを設定する
Edge labels 矢印についているラベル(移動するリソース)の表示/非表示を設定する

HTML形式で出力

HTML形式で出力する場合、下記コマンドを入力するだけOKです。

> cfn-dia html -t (CloudFormationテンプレートファイルのパス)  
# 例:  
> cfn-dia html -t ./template.yaml

すると、ダイアグラムがブラウザに表示されます。
f:id:Makky12:20201212083829p:plain

また、 下記操作も行えます。

  • 拡大・縮小
  • ダイアグラム全体の移動
  • 個別アイテムの移動

画像

画像形式は先述の通りまだexperimentalな状態なので、詳しくは説明しませんが、

とのことです。

分かっている問題

公式ページの「Known issues」にもある通り、下記の問題があるそうです。

  • 全部のアイコンがそろっているわけじゃない(一部抜けがある)
  • WSLの環境でHTML出力をしようとすると「Error: spawn wslvar ENOENT」というエラーが出る
    • wsluというモジュールをインストールすれば解決する模様。(自分は未確認)

ここは理解した上で使用してください。

Serverless Frameworkで使用する

ここまでcfn-diagramの使い方を説明してきましたが、肝心の「Serverless Frameworkではどうなのか」というのを最後に紹介したいと思います。

www.serverless.com

結論

で、いきなり結論ですが、serverless.yml(serverless.ts含む)自体は、cfn-diagramではダイアグラム表示できません。(エラーになる)

まあ、当然と言えば当然です。(CloudFormationテンプレート形式じゃないし)

しかし裏を返せば「CloudFormationテンプレート形式なら可能」なので、パッケージング後の「cloudformation-template-update-stack.json」ファイルなら、cfn-diagramでダイアグラム表示できます。

  • 実は「Draw.io形式で出力」「HTML形式で出力」で表示したダイアグラムも、「cloudformation-template-update-stack.json」ファイルをダイアグラム表示したものです。
  • 「cloudformation-template-update-stack.json」ファイルは、デフォルトではルート直下の「.serverless」フォルダに保存されます。
  • 「cloudformation-template-create-stack.json」というファイルもありますが、これはパッケージング後の*.zipファイルを保存するS3バケットの定義になります。(serverless.ymlで定義したリソース定義とは別)

まとめ

いかがでしたでしょうか?

cfn-diagram、非常に導入も使い方も簡単なので、興味がある方は一度使ってみてはいかがでしょうか?

実際僕も便利だなあと思いますし、業務で開発しているサーバーレスアプリの開発でもいい感じに使っていますので、こういう便利なツールをもっと活用していきたいと考えております。

お知らせ

Qiitaで開催中の「Advent Calendar 2020」において、2記事を担当することになりました。
担当するテーマは、サーバーレスAWS です。

ちなみに、下記のタイトルで記事きます。(確定)

  • サーバーレス(12/18(金)):Serverless Frameworkを始めよう または Serverless Frameworkでクロススタック参照を行う
  • AWS(12/25(金)):aws-sdk-mockでAWSのテストを行う

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

【Serverless Framework】Serverless Framework + TypeScript + JestでAWS Lambdaの開発&テスト環境を構築する その1

はじめに

皆さん、先日はVS Code Conferenceの視聴、ありがとうございました。
※見てない人は下のURLから視聴できます。

VS Code Conference Japan - YouTube

今年はVS Codeで登壇を結構させて頂きましたが、肝心の&僕が愛してやまないServerless Frameworkでの登壇が少ないなあ...なんて思ってます。

そういう理由...でもないですが、今回は久々にServerless Frameworkのブログです。

Serverless Framework公式ページ:
www.serverless.com

やること

タイトルの通り、今回は下記の環境でAWS Lambdaの開発環境を構築し、Jestでのテストも行えるようにします。

  • Serverless Framework
  • TypeScript
  • Jest

特に、今までずっとNode.jsでコードを書いてたので、TypeScriptもしっかりやらないと...という思いが強いです。(むしろ、本来は静的型付け言語の方が好きなので)

前提

とりあえず、上記3つのインストールを済ませておきます。(下記コマンドは、公式サイトのものをそのまま記載)

※global(-g) installではない場合、この後の「プロジェクト作成」の後で実行した方が良いです。(理由は後述。ただServerless Frameworkはどうしようもないけど...)

# Serverless Framework  
> npm install serverless -g  
  
# TypeScript  
> npm install typescript -g  
  
# Jest(もちろんglobal(-g) installでもOK)  
> npm install --save-dev jest  

で、Jestの設定をjest.config.jsやpackage.jsonに書きます。(下記はpackage.jsonに記載した場合)

{  
  "jest": {
    "roots": [
      "<rootDir>/"
    ],
    "testMatch": [
      "**/__tests__/**/*.+(ts|tsx|js)",
      "**/?(*.)+(spec|test).+(ts|tsx|js)"
    ],
  },
}

Serverless Frameworkプロジェクト作成

まずはServerless Frameworkのプロジェクトを作成します。
「--template」に「aws-nodejs-typescript」を指定すれば、TypeScriptのオプションを作成できます。

# 下記にはないけど、「--path」オプションでプロジェクトを作成する
# フォルダを指定できる。 (未指定時はカレントフォルダに作成)    
> serverless create --template aws-nodejs-typescript --name serverless-typescript-test

注意点としては、下記の点です。

  • 対象フォルダにすでに「package.json」や「tsconfig.json」があると、エラーになりますので、一時的にリネームor別フォルダに退避しておいてください。
    • 先程「グローバルではないnpm installはプロジェクト作成の後で」と書いたのはそのため
  • TypeScriptpプロジェクトの場合、テンプレートファイルが「serverless.yml」ではなく「serverless.ts」ファイルになります。
    • もちろん定義内容はまったく同じ。むしろ型定義ファイルによるインテリセンスが効くので、なかなか便利。

テスト対象のLambda関数の作成

まずはテスト対象のLambda関数の作成ということで、下記ソースを書きました。
※今回、コードの詳細には触れません。概要はソースコメントを参照。

// Day.jsのimport
import dayjs, { Dayjs } from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
  
dayjs.extend(utc)
dayjs.extend(timezone)
  
// Lambdaだったり、DynamoDBのimport
import { APIGatewayProxyEvent, APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda';
import 'source-map-support/register';
import { Context } from 'vm';
import * as AWS from 'aws-sdk';
import { DocumentClient } from 'aws-sdk/clients/dynamodb';
  
// ハンドラ関数
export async function hello(event:APIGatewayProxyEvent, _context:Context):Promise<APIGatewayProxyResult>{
  console.log(`[event] ${JSON.stringify(event)}`);
  let response: APIGatewayProxyResult = null;
  
  const items:DocumentClient.ItemList = await getDynamoData();
  const dateTime:string = getDate();
  const nodeVer: string = getNodeJsVersion();
  
  response = {
    statusCode: 200,
    body: JSON.stringify({
      items: items,
      dateTime: dateTime
    })
  };
  
  return response;
}
  
// DynamoDBからquery()関数で特定データを抽出する
export async function getDynamoData():Promise<DocumentClient.ItemList> {
  
  const documentClient: DocumentClient = new AWS.DynamoDB.DocumentClient();
  const param: DocumentClient.QueryInput = {
    TableName: 'nature-remo-events-history',
    KeyConditionExpression: 'app_name = :app_name and date_time_num = :date_time_num',
    ExpressionAttributeValues: {
      ':app_name': 'NatureRemoTest',
      ':date_time_num': 20201122073504
    }
  };
  
  const data: DocumentClient.QueryOutput = await documentClient.query(param).promise();
  console.info(`[data] ${JSON.stringify(data)}`);
  
  return data.Items;
}
  
// 現在の日付or指定した日時の日付をISO形式で返す
export function getDate(dateTime?:string | number, format?:string): string {
  const moment:Dayjs = (function():Dayjs {
    const momentInterim:Dayjs = dateTime ? dayjs(dateTime) : dayjs();
    return momentInterim.tz('Asia/Tokyo');
  })();
  
  const momentString:string = format ? moment.format(format) : moment.format();
  return momentString;
}

今回、「Day.js」という日付操作ライブラリを使ってます。
機能開発停止&新プロジェクトへの採用を非推奨としたmoment.jsが、その代替として推奨しているモジュールの一つです。
実際、関数なども大部分がmoment.jsと同じで、扱いやすいです。

day.js.org

上記ソースをデプロイして、無事にデプロイ&正常動作を確認できればOKです。

※デプロイ実行前に「npm install」コマンドでnpmモジュールをインストールしないとエラーになりますので、そこは注意です。(プロジェクト作成時にpackage.jsonに必要なモジュールを記載してくれるが、インストールはしてくれない)

テストを書く

では、上記Lambdaのテストを書きましょう。
とりあえずは、getDate()関数のテストを作成します。(ソースは下記)

※なお、jestの型定義ファイルをインストールしておくと、テストコードの作成時に便利です。

# jestの型定義ファイルのインストール
> npm install -D @types/node
// getDate()関数のテストソース  
import {getDate} from './handler';
  
describe('hello.tsのテスト', () => {
  
    describe('getDate()のテスト', () => {
        test('ISO形式の文字列で時間指定した場合、正しくその時間が返ること', () => {
            expect(getDate('2020-11-28T09:00:00+09:00')).toBe('2020-11-28T09:00:00+09:00');
        });
  
        test('UNIX数値(ミリ秒)で時間指定した場合も、正しくその時間が返ること', () => {
            expect(getDate(1606521600000)).toBe('2020-11-28T09:00:00+09:00');
        });
    })
});

で、上記ソースを「jest」コマンドで実行して、問題なければOKです。

が、下記エラーが発生するケースがあります。(私もそうだった)

Jest encountered an unexpected token.
This usually means that you are trying to import a file which Jest cannot parse, e.g. it's not plain JavaScript.
By default, if Jest sees a Babel config, it will use that to transform your files, ignoring "node_modules".

原因ですが、上のエラーにもある通り、「プレーンなJavaScriptに変換できないコードがある」のが原因です。
(テストソース的には「import {getDate} from './handler'」が該当)

というか、Jestの公式ドキュメントにも

Jest supports TypeScript, via Babel. First, make sure you followed the instructions on using Babel above. Next, install the @babel/preset-typescript via yarn

とある通り、JestでTypeScriptをテストする場合、Babelと「@babel/preset-typescript」モジュールを入れる必要があります。

なので、これらをインストールします。

Babel&@babel/preset-typescriptのインストール

といっても、ここからはJestの公式ドキュメントの記載とほとんど同じです。

まずはBabelを下記コマンドでインストールして、

# babel-jestはjestインストール時にインストールされるので、  
# ない場合のみインストールする。
> npm i -D babel-jest @babel/core @babel/preset-env

で、babel.confog.jsなりpackage.jsonに下記設定を追記します。(下記はpackage.jsonの場合)

"babel": {
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }
    ],
  ]
}

で、次に「@babel/preset-typescript」モジュールをインストールします。

> npm i -D @babel/preset-typescript

で、同じようにbabel.confog.jsなりpackage.jsonに設定を追記します。(下記はpackage.jsonの場合)
※なお、Jest公式ページでは「@babel/preset-typescript」を配列にしていませんが、配列にしないと正しく動かないので注意。

"babel": {
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": {
          "node": "current"
        }
      }
    ],
    [
      "@babel/preset-typescript" 
    ]
  ]
}

また「babel-jestはbabelの設定がある場合、自動でファイル変換を行う」とのことですが、自分でJestの「transform」に設定を定義することで、任意のtransform設定を定義出来ます。

{  
  "jest": {
    "roots": [
      "<rootDir>/"
    ],
    "testMatch": [
      "**/__tests__/**/*.+(ts|tsx|js)",
      "**/?(*.)+(spec|test).+(ts|tsx|js)"
    ],
    "transform": {
      "^.+\\.(ts|tsx)$": "babel-jest"
    },
  },
}

動作確認

では、改めて動作確認です。

上記設定をした後で「jest」コマンドを動かすと...

f:id:Makky12:20201203075406p:plain

正しくテストが実行されました。これでOKです。(skipしたテストは、その2で取り上げる予定です)

まとめ

これで、Serverless Framework + TypeScript + JestでのAWS Lambda開発環境が整いました。

TypeScriptは個人的に使いやすいと感じますし、型厳格な言語(C#とかJavaとか)が好きな人(自分も含め)にお薦めなので、これからもどんどん使っていきたいと思っています。

なお「てか、Serverless Frameworkほとんど関係ねえじゃん!」と思った人もいるかもしれませんが、その2でServerless Frameworkのプラグインを使ったテストを紹介する予定です。

告知

現在Qiitaで開催されている、「Advent Calendar2020」に「Serverless」と「AWS」で記事を公開します。
Serverlessは12/18(金)、AWSは12/25(金)に公開予定ですので、よろしければそちらもどうぞ。

なお、Serverlessは「Serverless Framework はじめの一歩」、AWSは「aws-sdk-mockを使ったAWSのテスト」について書く予定です。

qiita.com

qiita.com

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

【VS Code】VS Code Conference Japanで発表しました

はじめに

2020/11/21(土)に、Visual Studio Code5周年記念として、「VS Code Conference Japan」というイベントがオンラインで開催されました。

vscode.connpass.com

この中で、「アプリ開発&チーム管理で役立った拡張機能など」という内容で30分セッションを行いましたので、その内容を含め、VS Code Conference Japanについて書きたいと思います。

※ちなみに、発表資料は↓になります。
URL:https://www2.slideshare.net/MasakiSuzuki3/ss-239362824

www2.slideshare.net

お礼

管理者の皆様、いろいろ手筈を整えていただき、ありがとうございました。

また、当日聴いてくださった皆さん、ありがとうございました。

セッションで紹介した拡張機能について

  • セッションで紹介した拡張機能の概要+αを記載しています。(詳細は先述の発表資料を参照)
  • 個々のURLは載せてませんが、VS Code拡張機能検索やブラウザで名前をそのまま検索すれば、(多分)出てきます。

開発環境編

Setting Sync
  • 複数のPCでVS Code環境の同期を取れる。
  • GitHubアカウント(正確にはアクセストークンとGist ID)さえあればOK。
    • Gist IDはダウンロード時のみ必要
  • やることは「設定のアップロード」と「設定のダウンロード」だけ。

ちなみに、「複数PCでのVS Code環境の同期」機能は、2020/07にVS Codeに標準で搭載された。
ただしまだプレビュー版なので、使用前に警告が出る。

f:id:Makky12:20201123083949p:plain

Live Share
  • VS Code上で複数人で同時にソースコードの編集が行える、Micorsoft製の拡張機能
  • ソースの同期だけではなく、チャット&音声通話も可能。
  • withコロナ時代のリモートワーク全盛の今の時代、ものすごく真価を発揮している拡張機能では

ちなみにLive Shareを実際に動かした様子は、下記「VS Code Meetup#2 -Live Share編-」の動画を参照
https://www.youtube.com/watch?v=4wMlwWCeboQ

サーバーレスアプリ編

Azule Template Viewer
  • AzureのARM(Azure Template Manager。AWSでいうCloudFormationのような、Infrastructure as Codeを実現するリソース)のテンプレートファイルの内容について、事前にデプロイ後の内容を確認できる。
  • てか、マジでめっちゃ便利。
  • マジでCloudFormationにも欲しい...

f:id:Makky12:20201123084425p:plain

Rest Client
  • HTTPリクエスト送信&レスポンス受信をVS Code上で行える。
  • リスエスト定義&送信がめっちゃ簡単。(定義はスクショ参照。送信は[ctrl]+[alt]+[R]だけ)
  • この手の機能の拡張機能で、一番有名なのではないか?

f:id:Makky12:20201123084452p:plain

Swagger Viewer
  • Swagger定義ファイルの内容をプレビューできる。
  • 定義ファイルを変更したら、それがリアルタイムにプレビュー画面に反映されるのが良い。
  • Swagger Editor( https://editor.swagger.io/ )と同等の機能を持つ。

f:id:Makky12:20201123084532p:plain

AWS ToolKit
  • AWSのいくつかのリソース(Lambda, S3, CloudWatchなど)について、参照・AWS上で実行・削除を実施できる。
  • S3はアップロード&ダウンロードを行える。
  • いちいちコンソールを開かないでよい。
  • けど、編集ができないので、そこは今後の課題かな...
  • でも、Step FunctionsのSteta Machineプレビュー機能は便利(これも変更がりリアルタイムで反映される)。あとS3アップロード&ダウンロード機能とか。
  • セッションでも言ったけど、個人的にDynamoDBのCRUDができると、かなりいい感じではないかなと思う。

f:id:Makky12:20201123084604p:plain

コーディング補助編

ESLint
  • JavaScriptの静的検証ツール
  • 構文エラーとか、設定したコーディングルールに反したコードを知らせてくれる。
Prettier
  • コードフォーマッタ
  • コードを整形し、可読性の高いコードにしてくれる。
  • 一部静的検証も行ってくれる。
Grammarly
  • もともとはドキュメントの文法チェッカ
  • スペルが誤っている単語をお知らせしてくれる。
  • 「unofficial」となっているが、使用には全く問題ない。(もともとMSが公式で出していたらしいが、無くなったため)
Code Spell Checker
  • VS Code上でスペルチェックを行ってくれる。
  • スペルが違う単語についてお知らせしてくれて、修正候補も表示してくれる。
  • 「expectVarue」(正しくは「expectValue」)のような、複数の単語がつながった変数などのスペルミスも教えてくれる。

f:id:Makky12:20201123084719p:plain

Gi編

GitLens
  • ブランチ・ファイル・ファイル行単位で履歴を表示する
  • ブランチ/タグ/過去コミット間での差分を表示する
  • ソース内に変更者&コミットコメントを表示する

...など(他にもいろいろ)、「VS CodeでGit扱うなら、とりあえずこれは入れとけ」レベルで便利な拡張機能

f:id:Makky12:20201123084746p:plain

Git Graph
  • Gitリポジトリについて、ブランチ&変更履歴をツリー状に表示してくれる。
    • Backlog Gitの「ネットワーク」みたいな感じ
  • ツリー上から、Gitの各種操作(push/pull/checkoutなど)が可能
  • これも非常に便利だし「とりあえずこれは入れとけ」レベルでおススメ

f:id:Makky12:20201123084817p:plain

その他支援ツール編

Excel Viewer
  • 名前の通り、ExcelVS Code上で閲覧できる。(閲覧のみ。編集は不可)
  • Excelだけではなく、CSVファイルも閲覧可能。
    • というか、CSVビューワーとして非常に便利
  • ソート&フィルタ機能が標準で使用できるのが非常に良い。
  • データの調査&解析に向いている。

f:id:Makky12:20201123085151p:plain

Draw.io Integration
  • Draw.io(現Diagrams.net)のフローチャート作成機能をVS Code上で実行できる。
  • 微妙に違う部分があるかもしれないが、使った限りはさほど気にならないレベル。
  • 本家同様、AWS/Azureなどクラウド関連の図が豊富で、クラウド・サーバーレスのフロー図作成に便利。

f:id:Makky12:20201123084917p:plain

Hex Editor

f:id:Makky12:20201123084948p:plain

おまけ:諸事情で資料に載せられなかった拡張機能

Barcket Lens & Bracket Code Colorizer(2)
  • どちらも、コード内の各種カッコについて、対応するカッコを教えてくれる拡張機能
  • Bracket Code Colorizer(2)は色で、Bracket Lensは文字列で教えてくれる。
  • Bracket Code Colorizerは1と2があるが、基本的な機能は同じ。(2の方が性能が向上している、とのこと)

※左がBracket Lens, 右がBracket Code Colorizer2(なんかこのブログを書いているPCだと、同時使用ができなかった。たまたま?)

indent-rainbow
  • 名前の通り、インデントを深さごとに色分けして、コードを読みやすくしてくれる。
  • 色は設定可能

f:id:Makky12:20201123090320p:plain

余談

  • 最初は「サーバーレスアプリ開発で役立った拡張機能など」というタイトルを予定していましたが、構想を考えてるうちに「これ、ほとんど(Azule Template ViewerとAWS ToolKit以外)サーバーレス関係なくね?」と思い、運営さんにタイトルを変更してもらいました。(ありがとうございました)
  • 実は10月後半からずっと体調不良が続いており、本格的に資料を作成できたのがイベントの2日前からでした。
  • しかも、最初は10分くらいだとずっと思っていたので、イベント3日前のZoomリハーサルで30分と聞いたときは、マジで焦りました。
  • そして「まあリモートだし、始業前とかにも時間あるから、そこで進めるか」と思った時に限って、前日は3週間に一度の出社日というタイミングの悪さ...
  • 発表内容&このブログでも、AWS Toolkitがちょっと辛口だったかな?
  • でも個人的にはAWS Toolkitには期待しているし、実際クラウドの中ではAWSが一番好きなので、期待を込めてという意味で

お知らせ

12月にQiitaさんで行われる「Advent Calendar 2020」において、2記事を担当することになりました。
担当するテーマは、サーバーレスAWS です。

ちなみに、下記のタイトルで記事を書こうかな、と健闘中です。(確定ではない)

  • サーバーレス:Serverless Frameworkを始めよう または Serverless Frameworkでクロススタック参照を行う
  • AWSaws-sdk-mockでAWSのテストを行う

おわりに

ていうか、体調も戻ってきたし、そろそろブログも本格的に再開したいです。
Serverless Frameworkもいろいろ機能やプラグインが追加されましたし、いろいろ触って、いろいろ記事にしたいですね。本当に。
プラグインも自作したいですし。

それにAWSを始め、いろいろ技術が出てきたので、こんな時代でも仕事が絶えないように、時間の許す限りスキルアップしていきたいなと思ってます。

それではまた。

【Serverless Framework】IAM Roleを共有し複数プロジェクトでリソース毎に割り当てする

概要

AWS のIAM Role(≒実行権限)を共通プロジェクトでまとめて作成しておき、後で別の複数プロジェクトで使用する(=共有する)方法です。

経緯

先日Twitterをしていたら、こんなツイートを見つけました。

一つのterraformプロジェクトで一元管理してたリソースを、要望に合わせてIAMとそれ以外のterraformプロジェクトに分けて同じ名前のIAMを作ってから各リソースでPassRoleするようにしたらハマった arnが同じでも内部的には別のKeyを持っていて、destroyされたIAMを参照しようとしてるっぽい

で、それについて僕が書きのRTをしました。

これ、Serverless Frameworkならどうやるかな。親プロジェクトでIAMRole作って、それをOutputに設定して、子プロジェクトでそれをcf:stack変数で参照する感じかな?

そうしたら、元ツイのツイート主の方と色々お話が弾みまして、元ツイのツイート主の方から、下記疑問をいただきました。

今回の要望だと、IAMとそれ以外のプロジェクトに関してapplyできる組織を完全に分けたいという話だったので、slsでやるとしたらIAM用とそれ以外で完全にプロジェクト分けて、それ以外の方はarnをベタで書く感じになりますかね・・(slsでIAMだけdeployできるのかどうかは知らない)

(中略)

slsのIAMって、providerで指定するときにfunctionsにアタッチするpolicyを記述するイメージなので、functionsがない状態でのslsってどういう挙動になるのかは興味ありましね・・知人がslsで実装してるのですが、functionに権限渡してるだけですね・・

このツイートに対して、下記リプを返しました。

多分functionsがない場合、resourcesで普通にCloudFormationみたいに定義する感じですかね。てかslsでIAMだけってそういややったことないですね。たいてい一つはLambdaをデプロイするので…(最悪、ダミーLambdaを作る手はあるかもしれませんが…)

が、上記リプを返した所で、

  • 「そういやfunctionsを一つも作成しないパターン、やったことないな」
  • 「このやり方も実際に動かしたわけじゃないから、動く確証がないな...」
  • 「なら、実際に確かめてみるか!」

というわけで、実際に確かめてみることにしました。

参考

【Serverless Framework】API Gatewayを複数プロジェクト(serverless.yml)で共有する(Share API Gateway and API Resources)

※前回の記事ですが、前回と類似した処理を行いますので、こちらもあわせて参考にされるとよいと思います。

今回作成する成果物構成

プロジェクト

下記2つのプロジェクトを作成します。

プロジェクト名 説明 備考
iam-share-first IAM関連をまとめて作成するプロジェクト 初めにデプロイしておく必要がある
iam-share-second iam-share-firstで作成したIAMを使用するリソースを割り当てるプロジェクト iam-share-firstのデプロイ後にデプロイする必要がある
IAM Roleについて

「iam-share-first」で、下記2つのIAM Roleを作成します。

定義名 説明 備考
IamRoles3GetObject S3のGet系APIのみ実行を許可するIAM Role List系は不可
IamRoles3ListObjects S3のList系APIのみ実行を許可するIAM Role Get系は不可
Lambdaについて

「iam-share-second」で、下記2つのLambdaを作成します。

Lambda名 説明 割り当てるIAM Role 備考
GetObject S3.GetObjectのみ実行される(はず)のLambda IamRoles3GetObject S3.ListObjectsは権限がないからエラーになる(はず)
ListObjects S3.ListObjectsのみ実行される(はず)のLambda IamRoles3ListObject S3.GetObjectは権限がないからエラーになる(はず)

ちなみに、上記2つのLambdaはソースコード自体は全く同じで、その内容は下記になります。

※S3のGetObjectとListObjectsを実施するソースになります。ただ付与しているIAM Roleはそれぞれ異なるので、IAM Roleを付与していない方の処理は権限がなくてエラーになる...という確認です。

※「suzukima-iam-share-test-bucket」には、あらかじめ「I am iamTest.txt」という内容の「iamTest.txt」というテキストファイルを格納しておきます。

'use strict';  
  
const AWS = require("aws-sdk");
const S3 = new AWS.S3();
const BUCKET_NAME = "suzukima-iam-share-test-bucket";
  
module.exports.index = async event => {
  
  console.log(`[event] ${JSON.stringify(event)}`);
  
  try{
    await getObject();
    console.info('GetObject Succeeded');
  } catch(e) {
    console.warn(`[GetObject Error] ${e.message}`);
    console.warn('GetObject Failed');
  }
  
  try{
    await listObjects();
    console.info('listObjects Succeeded');
  } catch(ex) {
    console.warn(`[listObjects Error] ${ex.message}`);
    console.warn('listObjects Failed');
  }
  
  return;  
};
  
async function getObject() {
  const params = {
    Bucket: BUCKET_NAME,
    Key: "iamTest.txt"
  };
  
  const data = await S3.getObject(params).promise();
  console.info(`[GetObject Body] ${data.Body.toString()}`);
  
  return;
}
  
async function listObjects() {
  const params = {
    Bucket: BUCKET_NAME,
  };
  
  const lists = await S3.listObjects(params).promise();
  for(const content of lists.Contents) {
    console.info(`[listObjects Key] ${content.Key}`);
  }
  
  return;
}

iam-share-firstについて

※各プロジェクトのserverless.ymlについては、ブログの最後に載せておきます。
※検証当時はAPI Gatewayも共有すると勘違いして、API Gateway関連のリソースも定義していますが、IAM Roleの共有だけなら不要です。

ただし前回記事と異なり、iam-share-firstではLambdaを一切作成していませんので、その点では参考になると思います。)

resources.Resources

[resources.Resources]内で、先程の「IamRoles3GetObject」及び「IamRoles3ListObjects」の2つのIAM Roleを作成します。

そしてそれぞれに、(今回の対象バケットである)「suzukima-iam-share-test-bucket」に対する「S3.Getxxx」及び「S3.Listxxx」系のアクションを許可します。

またこの2つのIAM Roleは共にLambda関数に割り当てますが、そのLambda関数ではログを出力したいので、CloudWatch Logsに関するすべてのアクションも許可しています。

resources.Outputs

[resources.Outputs]では、[resources.Resources]で定義した2つのIAM Roleについて、「出力」タブに出力するように設定を行っています。
前回同様、こうすることによって、この2つのIAM RoleのARNをiam-share-secondで参照できるようにしています。

なお、 iam-share-first内で使用している「serverless-pseudo-parameters」プラグインですが、これはServerless FrameworkにてCloudFormationの(「AWS::AccountId」などの)疑似パラメータ参照を可能にするプラグインです。

www.serverless.com

CloudFormationの疑似パラメータ参照については、こちらを参照。

docs.aws.amazon.com

iam-share-secondについて

functions

iam-share-secondでは、[functions]で定義するLambda関数(「GetObject」及び「ListtObjects」)の「role」に、iam-share-firstで作成したIAM RoleのARNを指定するだけでOKです。

また、ここではServerless Frameworkの「${cf:[スタック名].[キー名]}」変数を使用して、iam-share-firstスタックの「出力」タブから各IAM RoleのARNを取得しています。

動作確認

実際に動作確認した結果(ログ)が、下記2つの画像になります。

GetObject関数では、S3.getObject()は正常に実施され、「iamTest.txt」の中身の「I am iamTest.txt」というログが表示されていますが、S3.listObjects()は実行権限がないため「Access Denied」となっています。

逆にListObjects関数では、S3.listObjects()は正常に実施され、バケット内の「iamTest.txt」がログに表示されていますが、S3.getObjects()は実行権限がないため「Access Denied」となっています。

このことからも、IAM Role、およびそのLambda関数への割り当てが正常に動作しているといえます。

GetObject

f:id:Makky12:20201006194537p:plain

ListObjects

f:id:Makky12:20201006194555p:plain

まとめ

以上が「IAM Roleを共有し複数プロジェクトでリソース毎に割り当てする」方法になります。

「なんでそんな面倒くさい事するの?」と思われるかもしれませんが、前回の記事でも書いた通り、プロジェクト自体が大きくなって、リソース管理やライフサイクルなどでのリソース分割を本格的に考えなければならなくなった際、リソース共有やプロジェクト分割は非常に重要になってきます。

というか、それらを本格的に考えなければならなくなった時には、すでに手遅れだったりする場合もあるので、リソース共有やプロジェクト分割は余裕のあるうちから考慮・対処をしておくのがおすすめです。(自分も痛い目にあっていますので...)

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

serverless.ymlについて

iam-share-first
  
service: iam-share-first  
provider:
  name: aws
  runtime: nodejs12.x
  
  apiGateway:  
    restApiResources:
      /s3:
        Ref: SuzukimaRestApiResourceS3
  
  stage: dev
  region: ap-northeast-1
  profile: default
  
plugins:
  - serverless-pseudo-parameters
  
custom:
  projectName: iam-share-test
  
package:  
  exclude:
    - node_modules
    - node_modules/**
  
# you can add CloudFormation resource templates here
resources:
  Resources:
    SuzukimaRestApi:
      Type: AWS::ApiGateway::RestApi
      Properties:
        Name: ${self:custom.projectName}-RestApi
    SuzukimaRestApiResourceS3:
      Type: AWS::ApiGateway::Resource
      Properties:
        RestApiId: !Ref SuzukimaRestApi
        ParentId:
          'Fn::GetAtt': [ SuzukimaRestApi, RootResourceId ]
        PathPart: 's3'
      DependsOn:
        - SuzukimaRestApi
    SuzukimaBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: suzukima-${self:custom.projectName}-bucket
    IamRoles3ListObjects:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            -
              Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action:
                - sts:AssumeRole
        Path: "/"
        Policies:
          -
            PolicyName: ${self:custom.projectName}-s3ListObjectsPolicy
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                -
                  Effect: Allow
                  Action:
                    - s3:List*
                  Resource:
                    'Fn::GetAtt': [SuzukimaBucket, Arn]
                -
                  Effect: Allow
                  Action:
                    - logs:*
                  Resource:
                    'Fn::Sub': arn:aws:logs:ap-northeast-1:#{AWS::AccountId}:log-group:/aws/lambda/*
        RoleName: ${self:custom.projectName}-s3ListObjectsRole
      DependsOn:
        - SuzukimaBucket
    IamRoles3GetObject:
      Type: AWS::IAM::Role
      Properties:
        AssumeRolePolicyDocument:
          Version: "2012-10-17"
          Statement:
            -
              Effect: Allow
              Principal:
                Service:
                  - lambda.amazonaws.com
              Action:
                - sts:AssumeRole
        Path: "/"
        Policies:
          -
            PolicyName: ${self:custom.projectName}-s3GetObjectPolicy
            PolicyDocument:
              Version: "2012-10-17"
              Statement:
                -
                  Effect: Allow
                  Action:
                    - s3:Get*
                  Resource:
                    'Fn::Join':
                      - "/"
                      - - 'Fn::GetAtt': [SuzukimaBucket, Arn]
                        - "*"
                -
                  Effect: Allow
                  Action:
                    - logs:*
                  Resource:
                    'Fn::Sub': arn:aws:logs:ap-northeast-1:#{AWS::AccountId}:log-group:/aws/lambda/*
        RoleName: ${self:custom.projectName}-s3GetObjectRole
      DependsOn:
        - SuzukimaBucket  
  
  Outputs:
    apiGatewayRestApiId:
      Value:
        Ref: SuzukimaRestApi
      Export:
        Name: ${self:custom.projectName}-restApiId-${self:provider.stage}
    apiGatewayRestApiRootResourceId:
      Value:
        'Fn::GetAtt': [SuzukimaRestApi, RootResourceId]
      Export:
        Name: ${self:custom.projectName}-rootResourceId-${self:provider.stage}
    apiGatewayResourcesS3:
      Value:
        Ref: SuzukimaRestApiResourceS3
      Export:
        Name: ${self:custom.projectName}-ResourcesS3-${self:provider.stage}
    iamRoleS3ListObjectsArn:
      Value:
        'Fn::GetAtt': [IamRoles3ListObjects, Arn]
      Export:
        Name: ${self:custom.projectName}-iamRoleS3ListObjectsArn-${self:provider.stage}
    iamRoleS3GetObjectArn:
      Value:
        'Fn::GetAtt': [IamRoles3GetObject, Arn]
      Export:
        Name: ${self:custom.projectName}-iamRoleS3GetObjectArn-${self:provider.stage}
iam-share-second
service: iam-share-second  
  
provider:
  name: aws
  runtime: nodejs12.x
  
  stage: dev
  region: ap-northeast-1
  profile: default
  
  apiGateway:
    restApiId: ${cf:${self:custom.parentStackName}.apiGatewayRestApiId}  
    restApiRootResourceId: ${cf:${self:custom.parentStackName}.apiGatewayRestApiRootResourceId}  
    restApiResources:
      /s3: ${cf:${self:custom.parentStackName}.apiGatewayResourcesS3}
  
custom:
  parentStackName: iam-share-first-dev
  projectName: iam-share-test

package:
  exclude:
    - node_modules
    - node_modules/**
    - restClient.txt

functions:
  GetObject:
    handler: iamTest.index
    name: GetObject
    role: ${cf:${self:custom.parentStackName}.iamRoleS3GetObjectArn}
    events:
      - http:
          path: /s3/get
          method: get
  
  ListObjects:
    handler: iamTest.index
    name: ListObjects
    role: ${cf:${self:custom.parentStackName}.iamRoleS3ListObjectsArn}
    events:
      - http:
          path: /s3/list
          method: get

【Serverless Framework】API Gatewayを複数プロジェクト(serverless.yml)で共有する(Share API Gateway and API Resources)

今回の内容

Serverless Frameworkを使用して、API Gatewayを複数プロジェクト(serverless.yml)で共有する(=あるプロジェクトで作成したAPI Gatewayを別のプロジェクトでも使いまわす)方法です。

Serverless Framework公式ページ

詳細

Serverless Frameworkでは、デフォルトではプロジェクト(serverless.yml)ごとに「functions」セクションで定義したLambdaのエンドポイントとなるAPI Gatewayが自動で作成されるため*1、「既存のAPI Gatewayを共有する」ことはできません。

しかし、状況によっては「既存のAPI Gatewayを共有したい(=今回もエンドポイントとして使用したい)」ケースが発生します。

それをServerless Frameworkで実現する方法です。

API Gatewayを共有する必要性

始めのうちはともかく、だんだんプロジェクトが大きくなると、デプロイ時に下記問題が発生します。

  • デプロイにとても時間がかかる
  • 1リソース(Lambdaなど)を追加するだけで、全リソースをフルデプロイしなきゃならない
    • 必要最小限だけデプロイすればいい
    • (例) Lambdaをデプロイするだけなら、S3やDynamoDBのデプロイは不要
  • リソース数が多くなると、CloudFormationの制限に引っかかる
    • CloudFormationでは、1スタックのリソースは最大200個まで

そのため、上記の解決策として下記のような「スタック分割」が行われます。

  • スタックのネスト
  • ライフサイクル(≒更新頻度)によるスタック分割
  • (マイクロサービス的な)一定のサービス単位によるリソース分割

この場合、

  • API GatewayとLambdaを別スタック(serverless.yml)に定義する
  • 別スタックで作成したAPI GatewayをLambdaのエンドポイントにする

ということがあります。(この場合「共有」ではないですが、「既存のAPI Gatewayを使いまわす」という意味では同じになります)

そのため、「既存のAPI Gatewayを共有する」必要が発生してくるわけです。

※この「スタック分割」については、別の機会に書こうと思ってます。

参考サイト

API Gatewayの共有の仕方

具体的なAPI Gatewayの共有の仕方ですが、これは実際のserverless.ymlを見ながら説明します。

ここでは「splitdeployfirst」「splitdeploysecond」という、2つのプロジェクト(=スタック)を使用します。(各serverless.ymlの内容を、末尾に掲載してあります)

「splitdeployfirst」(=先にデプロイする方のserverless.yml)

先にデプロイする「splitdeployfirst」ですが、こちらは特にAPI Gateway共有だからといって、大きく変わる部分はないと思います。(「apiGateway.restApiId」や「apiGateway.restApiRootResourceId」の定義も不要です)

ただ一点、大きく違うのは「resouces.Outputs」で、「apiGatewayRestApiId」「apiGatewayRestApiRootResourceId」という2つの項目のValueにそれぞれ

  • Ref: ApiGatewayRestApi
  • 'Fn::GetAtt': [ApiGatewayRestApi, RootResourceId]

を指定しています。(実際には必須ではないですが、こうしたほうが便利です」)

こうすることで、作成されるAPI Gatewayの「restApiId」及び「restApiRootResourceId」の値を取得できるので、それを「出力」に表示し「splitdeploysecond」プロジェクトから参照できるようにしています。

「ApiGatewayRestApi」について

※「apiGatewayRestApiId」及び「apiGatewayRestApiRootResourceId」のValueの指定で「ApiGatewayRestApi」という、serverless.ymlのどこにも定義していないキーを指定しています。

これなのですが、Serverless FrameworkではAPI Gatewayを作成する際に、「Type=AWS::ApiGateway::RestApi」のリソースを「ApiGatewayRestApi」という項目名で作成します。
ですのでこれらは「こう書くものなんだ」と思ってください。

※「apiGatewayResourcesSplitDeploy」については、後述します。

「splitdeploysecond」(=後でデプロイする方のserverless.yml)

後でデプロイする(=既存のAPI Gatewayを共有する)「splitdeploysecond」ですが、こちらで「restApiId」「restApiRootResourceId」など、API Gatewayの共有に必要な「provider.apiGateway」項目の定義を行います。

「restApiId」及び「restApiRootResourceId」の値について、「${cf:[スタック名].[キー名]}」という変数を使用しています。
Serverless Frameworkでは、この記載をすることで、CloudFormationスタックの「出力」タブ内の「キー」に該当する「値」を取得することができます。

※実際に取得できるのは「resources.Outputs」内の各項目の「Value」の値

こうしておくと、値を直書きした場合と違い、下記のメリットがあります。(splitdeployfirstで「resources.Outputsの定義は必須ではないが、こうしたほうが便利」と書いたのは、そのためです)

  • 環境を変えても(dev/stg/prodなど)、値を書き直す必要がない
  • spritdeployfirstをremove→再度デプロイしても、値を書き直す必要がない

【参考】https://www.serverless.com/framework/docs/providers/aws/guide/variables/#reference-cloudformation-outputs

※ちなみに「restApiId」及び「restApiRootResourceId」の値は、AWSコンソールからも確認できます。
API Gatewayから該当のAPI Gatewayを選択し、左のリストの「リソース」を選択すると、画面左上に表示されます。(下の画像では消してますが)

f:id:Makky12:20200901195527p:plain

restApiResourcesの指定

ところで、プロジェクト「spritdeployfirst」「spritdeploysecond」に定義したLambda(hello, guten)は、共にルートパス「splitdeploy」を参照しています。

この場合、それぞれパス「splitdeploy/hello」及び「splitdeploy/guten」のリソースがなんの問題もなくAPI Gatewayに作成されて欲しいところです。

しかし、Serverless Framework公式ページにも下記の記載があることからわかるように、実際は「spritdeploysecond」のデプロイ時にエラーになります。

The above example services both reference the same parent path /posts. However, Cloudformation will throw an error if we try to generate an existing path resource.

これは「spritdeploysecond」の「splitdeploy」を「spritdeployfirstで作成した「splitdeployと同じである」ことを認識してくれないため、下記の現象が発生し、エラーとなります。

  • 「spritdeploysecond」のデプロイ時に、ルートパス「splitdeploy」を持つリソースを新規に作成しようとする
  • しかしルートパス「splitdeploy」を持つリソースはすでに存在する
  • すでにルートパス「splitdeploy」を持つリソースは存在するのに、ルートパス「splitdeploy」を持つリソースを新規作成しようとしている
  • 重複エラー

そのため、既存のAPI Gatewayを共有する場合、パスの一部が共通のLambdaを作成する場合「パスsplitdeployは、既存のパスsplitdeployの事だよ」と教えてあげる必要があります。

そして、それを行っているのが下記の部分で、「restApiResources」に「/splitdeploy」、及びそのrestApiResourceIdを指定することで「このserverless.ymlのfunctionsのパスで指定している/splitdeployは、既存の/splitdeployの事だよ」と教えています。

こうすることで、既存のパス「/splitdeploy」を持つLambda(guten)があっても、正常にデプロイできるようになります。

provider:  
  apiGateway:
    restApiResources:
      /splitdeploy: ${cf:${self:custom.parentStackName}.apiGatewayResourcesSplitDeploy}
apiGatewayResourcesSplitDeployの内容

ところで、上記「/splitdeploy」の値を見て「splitdeployfirst(のserverless.yml)のresources.Outputs.SplitDeployFirstAuthorizerArn.Valueの値を取得している」ことは何となく分かると思います。

しかし、実際にそこに指定されている「ApiGatewayResourceSplitdeploy」という項目は、serverless.ymlのどこにもありません。

では、どこに指定されているのでしょうか?

結論を言いますと、Serverless Frameworkでは、定義内容をCloudFormation構文に変換する(serverless-state.jsonに書き込む)際、「events=http」であるLambdaのpathの内容について、区切り(/)ごとに「Type=AWS::ApiGateway::Resource」のリソースを作成します。
そしてそのキー名は「ApiGatewayResource + ルートパスからのpathの区切りごとの名前」となります。(先頭のみ大文字)

例えば、pathが「/splitdeploy/hello」の場合、キー名が下記の2つのリソースが作成されます。

  • ApiGatewayResourceSplitdeploy
  • ApiGatewayResourceSplitdeployHello

なので、それを見越して「Ref:ApiGatewayResourceSplitdeploy」と記載しているわけです。

Lambdaオーサライザの指定

API Gatewayにオーソライザを設定する場合があると思います。

Serverless Frameworkでは、Lambdaオーソライザを使用する場合、各Lambdaの「event.http」の子要素として「authorizer」を設定します。(splitdeploysecondのserverless.ymlを参照)

そしてここでは、splitdeployfirstにオーサライザ用のLambdaのみ設定し(AuthorizerLambda)、gutenでは「authorizer」にそのARNを指定しています。

この場合、今までのようにsplitdeployfirstのリソースの値(今回はARN)を参照する場合、resources.Outputsについつい下記定義をしようとしてしまいます。

resources:
  Outputs.
    SplitDeployFirstAuthorizerArn:
      Value:
        'Fn::GetAtt': [AuthorizerLambda, Arn]
      Export:
        Name: ${self:service}-SplitDeployFirstAuthorizerArn-${self:provider.stage}

しかし上記定義だと、デプロイ時に「AuthorizerLambdaなんて項目ねえよ」とエラーになってしまいます。ちなみに、Cognitoをオーソライザにした場合は、このエラーは発生しません。*2
なぜでしょうか?

これまたServerless Frameworkの仕様なのですが、Serverless Frameworkでは、定義内容をCloudFormation構文に変換する際、Lambda関数のキー名には、末尾に「LambdaFunction」というサフィックスを付けてserverless-state.jsonに書き込みます。

ですので「AuthorizerLambda」はserverless-state.jsonには「AuthorizerLambdaLambdaFunction」というキー名で書き込まれます。*3

「ApiGatewayRestApi」しかり「ApiGatewayResourceSplitdeploy」しかり、こういうServerless Framework独自の仕様を知らないと、思わぬところでハマるので注意が必要です。(serverless-state.jsonを追っていけばそのうち分かりますけどね)

まとめ

以上、長くなってしまいましたが、API Gatewayを共有する方法(別プロジェクトで使いまわす方法)でした。

始めのうちは気にする必要はないかもしれませんが、アプリが大きくなったり、運用面を考えると「スタックのネスト」「デプロイ分割」を考える必要があり、その際にAPI Gatewayの共有を考える必要が出てきます。

スタック(=serverless.yml)の再構成というのは、アプリがそれなりの規模になればなるほど困難になってしまうので、たとえ面倒でも始めのうちにしっかり考えて、最適な構成にしておきたいものです。

てか次回はその「スタックのネスト」「デプロイ分割」あたりについて書こうかな、と考えています。

それでは、長くなってしまいましたが、今回はこの辺で。

備考:「splitdeployfirst」「splitdeploysecond」のserverless.ymlの内容

# splitdeployfirstのserverless.yml
service: splitdeployfirst
  
provider:
  name: aws
  runtime: nodejs12.x
  
# you can overwrite defaults here  
  stage: dev
  region: ap-northeast-1
  profile: default
  
functions:
  hello:
    handler: handler.hello
    events:
      - http:
          path: /splitdeploy/hello
          method: get
  
  AuthorizerLambda:
    handler: handler.authorizer
  
resources:
  # Resources:
  Outputs:
    apiGatewayRestApiId:
      Value:
        Ref: ApiGatewayRestApi
      Export:
        Name: ${self:service}-restApiId-${self:provider.stage}
    apiGatewayRestApiRootResourceId:
      Value:
        'Fn::GetAtt': [ApiGatewayRestApi, RootResourceId]
      Export:
        Name: ${self:service}-rootResourceId-${self:provider.stage}
    apiGatewayResourcesSplitDeploy:
      Value:
        Ref: ApiGatewayResourceSplitdeploy
      Export:
        Name: ${self:service}-ResourcesSplitDeploy-${self:provider.stage}
    SplitDeployFirstAuthorizerArn:
      Value:
        'Fn::GetAtt': [AuthorizerLambdaLambdaFunction, Arn]
      Export:
        Name: ${self:service}-SplitDeployFirstAuthorizerArn-${self:provider.stage}
#splitdeploysecondのserverless.yml 
service: splitdeploysecond
custom:
  parentStackName: splitdeployfirst-dev
  
provider:
  name: aws
  runtime: nodejs12.x
  
  apiGateway:
    restApiId: ${cf:${self:custom.parentStackName}.apiGatewayRestApiId} 
    restApiRootResourceId: ${cf:${self:custom.parentStackName}.apiGatewayRestApiRootResourceId} 
    restApiResources:
      /splitdeploy: ${cf:${self:custom.parentStackName}.apiGatewayResourcesSplitDeploy}
 
# you can overwrite defaults here
  stage: dev
  region: ap-northeast-1
  profile: default
    
functions:
  guten:
    handler: handler.guten
    events:
      - http:
          path: /splitdeploy/guten
          method: get  
          authorizer: ${cf:${self:custom.parentStackName}.SplitDeployFirstAuthorizerArn} 

*1:event=httpのLambdaがある場合

*2:その場合Cognitoはresources.Resourcesに指定すると思いますが、resources.Resourcesに指定したキー名はserverless-state.jsonにもそのままのキー名で書き込まれるため

*3:明らかに「Lambda」がかぶってるのはどうなの?という感じですね...

【Serverless Framework】Serverless Jest Pluginで始めるLambdaテストの第一歩

はじめに

前々回の【AWS】単体テストを考慮したLambdaの構成を考えたで、単体テストについて少し触れました。

で、今回はそこから一歩踏み込んで「実際にテストを導入してみよう!」という内容になります。(なぜ前回書かなかったのかは置いといて)

また、どうせならServerless Framework経由で利用できるように「Serverless Jest Plugin」という、Serverless Frameworkのプラグインにて行う内容になっています。

Serverless Jest Pluginとは?

名前の通り、Serverless Framework上でJestを使用したテスト(テスト駆動開発:TDD)を行うためのプラグインになります。
www.serverless.com

ちなみに(今回は扱いませんが)、上記のMocha版である「Serverless Mocha Plugin」というのもあります。
www.serverless.com

このServerless Jest Pluginを使用して、以下のことができます。

  • テストの作成
  • テストの実行

インストール&設定

インストールですが、他のプラグイン同様、以下を行うだけです。

  • npm installの実施
  • serverless.ymlの「plugins」セクションに追加
# npm installを実施する  
> npm i serverless-jest-plugin --save-dev  
  
# serverless.ymlのpluginsセクションに追加する  
plugins:
  - serverless-jest-plugin

また「custom」セクションに「jest」というキーを設定し、その子要素に「Jestの設定項目:その値」というKey:Valueを用意することで、その項目の設定をそのまま反映することができます。

# 設定例  
# (例)下記の記載をすると、Jestのテスト時間を30秒に延ばすことができる。(デフォルトは5秒)  
custom:
  jest:
    testTimeout: 30000

テストの作成

で、まず行う「テストの作成」ですが、これは下記コマンドを実施すればOKです。

# Lambda関数作成と同時にテストを作成する場合  
> sls create function -f myFunction --handler ./index.handler --path tests
  
# 既存のLambda関数に対して、テストだけ作成する場合
> sls create test -f myFunction--path tests  

パラメータの意味は、以下の通りです。

パラメータ 意味 設定値 備考
-f Lambda定義名 serverless.ymlのfunctionsセクションに定義する(した)Lambda定義名
--handler ハンドラ関数名 Lambdaのハンドラ関数のパス(ファイル名&関数名) serverless.ymlのfunctionsセクションのhandlerに設定される値
--path テストファイルを格納するフォルダのパス ・未指定の場合、「__tests__」というフォルダを自動作成し、その中に格納する。
・「{function}」という値を指定すると、フォルダは作成せず、元のLambda関数と同じ階層にテストファイルを作成する
「{function}」は、実際のLambda定義名に置き換える...などではなく、「{function}」という固定の文字列

例えば、以下のコマンドを実行した場合、結果は下記の通りになります。

> sls create function -f pow--handler ./pow.index --path {function}
# serverless.ymlのfunctionsセクションに、以下の定義が追加される
functions:  
  pow:  
    handler: ./pow.index  

フォルダ:

  • <ルートフォルダ>
    • node_modules
    • pow.js
    • pow.test.js

また、作成されたテストファイル(pow.test.js)の中身は下記の通りです。

'use strict';

// tests for pow
// Generated by serverless-jest-plugin  
  
const mod = require('./pow');  
  
const jestPlugin = require('serverless-jest-plugin');  
const lambdaWrapper = jestPlugin.lambdaWrapper;  
const wrapped = lambdaWrapper.wrap(mod, { handler: 'index' });  
  
describe('pow', () => {  
  beforeAll((done) => {  
//  lambdaWrapper.init(liveFunction); // Run the deployed lambda  
  
    done();  
  });  
  
  it('implement tests here', () => {  
    return wrapped.run({}).then((response) => {  
      expect(response).toBeDefined();  
    });  
  });  
});  

テストの実行

もう一つの「テストの実行」ですが、これは下記コマンドを実施すればOKです。

> sls invoke test -f pow--stage dev --region ap-northeast-1

パラメータの意味は、以下の通りです。(すべて任意項目です)

パラメータ 意味 設定値 備考
-f テストするLambdaの定義名 serverless.ymlのfunctionsセクションに定義したLambda定義名 未指定の場合、全テストが実施される
--stage 実施するステージ Lambdaのテストを実施するステージを指定したい場合、設定する 不要なら未指定でもOK
--region 実施するリージョン Lambdaのテストを実施するリージョンを指定したい場合、設定する 不要なら未指定でもOK

実際に先程のテスト(pow.test.js)を実施すると、以下の結果になります。
f:id:Makky12:20200816133241p:plain

また、下記のようにテストを変更すると、ちゃんとテスト結果はfailになります。

// pow.test.jsに下記テストを追加する  
it('statusCode test', () => {
    return wrapped.run({}).then((response) => {
      expect(response.statusCode).toBe(200);
   });
});  
  
// pow.jsのハンドラ関数(index)を下記の通りに変更する  
module.exports.index = (event, context, callback) => {
  const response = {
    statusCode: 400,  // statusCodeを200→400に変更する
    body: JSON.stringify({
      message: 'Go Serverless v1.0! Your function executed successfully!',
      input: event,
    }),
  };

  callback(null, response);
};

f:id:Makky12:20200816133722p:plain

その他の事項

で、その他の事項ですが...公式ページのドキュメントに書いてあるのは、これで以上です。
おそらくは「Lambdaのハンドラ関数のテスト用プラグイン」という前提なんででしょう。

なので、簡単な使い方を説明するのは以上ですが、ちょっと調査したことを羅列したいと思います。

ローカル関数のテストは可能?

結論から言うと「可能」です。
ただし、ローカル関数が以下の条件を満たす場合のみです。

  • exportされている(まあこれは当然)
  • 非同期関数(async function)である

例えば、pow.jsを以下の通りに変更します。(event.numの2乗の数値をbody.powResultに設定してレスポンスを返すLambda関数です。)

'use strict';
  
const index = async (event, context, callback) => {
  
  const powResult = await main(event.num);
  
  const response = {
    statusCode: 200,
    body: JSON.stringify({
      message: 'Go Serverless v1.0! Your function executed successfully!',
      input: event,
      powResult: powResult
    }),
  };
  
  callback(null, response);
};
  
const main = async num => {
  return await funcPow(num);
}
  
const funcPow = async num => {
  return Math.pow(num, 2);
}
  
module.exports = {
  index: index,
  main: main,
  funcPow: funcPow
}

で、pow.test.jsに下記テストを設定します。

'use strict';
  
// tests for pow
// Generated by serverless-jest-plugin
  
const mod = require('./pow');
  
const jestPlugin = require('serverless-jest-plugin');
const lambdaWrapper = jestPlugin.lambdaWrapper;
const wrapped = lambdaWrapper.wrap(mod, { handler: 'index' });
const wrappedfuncPow = lambdaWrapper.wrap(mod, { handler: 'funcPow' });  // これを追加
  
it('main Function test', async () => {
  // ローカル関数funcPowのテスト
  const result = await wrappedfuncPow.run(4);
  expect(result).toBe(16);
});

上記テストを実施すると、ちゃんとローカル関数funcPow()のテストができます。

f:id:Makky12:20200816141353p:plain

しかし、以下のようにfuncPow()を同期関数に書き換えて(「async」を削除)テストを実施すると...

// pow.jsのfuncPowを同期関数に変更する
const funcPow = num => {
  console.log('funcPow Called');
  return Math.pow(num, 2);
}  
  
// pow.test.jsでfuncPowを呼ぶ
it('funcPow Function test', () => {
  const result = wrappedfuncPow.run(4);
  expect(result).toBe(16);
});

結果、テストは失敗します。
f:id:Makky12:20200820192818p:plain

というか、戻り値が空オブジェクトになっています。
また「funcPow Called」というログが出力されているので、funcPow関数自体は呼ばれていることが分かると思います。

これですが、元となるlambdaWrapperに下記のような処理がなされているため、戻り値がPromiseでないと、正しく処理されないためです。

if (this.handler) {
  const handlerResult = this.handler(event, lambdaContext, callback);
  // Check if the result itself is a promise
  if (handlerResult && handlerResult.then) {
    handlerResult.then(function(data) {
      // Avoid Maximum call stack size exceeded exceptions
      return setImmediate(function() {
        callback(null, data);
      });
    }).catch(function(err) {
       // Avoid Maximum call stack size exceeded exceptions
       return setImmediate(function() {
        callback(err);
      });
    });
  }
} else {  
// 以下略
}

この場合は、無理やりlambdaWrapper.wrapを使わず、素直に以下のような、Jest標準の記載をすればOKです。

// pow.jsのfuncPowを同期関数に変更する
const funcPow = num => {
  console.log('funcPow Called');
  return Math.pow(num, 2);
}  
  
// pow.test.jsでfuncPowを呼ぶ
it('funcPow Function test', () => {
  const result = mod.funcPow(4);
  expect(result).toBe(16);
});

この点からも、Serverless Jest Pluginは「Lambdaのハンドラ関数のテスト用プラグイン」という前提なんでしょう。

まとめ

というわけで、Serverless Jest Pluginについて簡単に説明しました。

決して「これ一つで劇的にテスト環境が改善する」とか「至れり尽くせり」ではないですが、Lambdaテスト導入の第一歩としては、悪くないツールだと思います。

こういうツールをきっかけに、テストについてある程度詳しくなって、そこから次のステップに進むのが良いのかな、と個人的に感じました。

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