こんにちは。テックラボの佐々木です。主にバックエンドを担当しています。
フォースタートアップス(以下フォースタ)では、サーバーレスなアプリケーションの構築にAWS Lambdaを利用しています。
以前はLambdaの開発や運用が属人化していても問題なかったのですが、プロダクトやエンジニアが増えるに従いLambdaの数も増え、管理できなくなってきました。
そのため、現在はServerless Frameworkというオープンソースのツールを利用してLambdaの管理を行なっています。
この記事ではServerless Frameworkについてあまり知らない人向けに、Serverless Frameworkの概要とフォースタでの利用例を紹介します。
Serverless Frameworkとは
言葉が混同しないように、以後サーバーレスという概念のことを「サーバーレス」、Serverless Frameworkというツールのことを「Serverless」と書きます。
Serverlessはサーバーレスなアプリケーションを簡単に開発、デプロイするためのNode.js製のツールです。
AWS、Azure、GCP等のクラウドサービスによらず利用することができ、ランタイムの言語もクラウドサービス側で許されているものであれば利用することができます。(フォースタはAWSを利用しているため、この記事ではAWSを例に説明します。)
デプロイコマンドを実行すると、各クラウドサービスのリソース構築処理が実行されます。AWSを例に挙げると、デプロイコマンド実行時にCloudFormationのテンプレートが作成され、Lambdaおよびその他必要なリソースが作成されます。
Serverlessを利用して感じたメリットは以下の3点です。
デプロイが簡単
手動でLambdaをデプロイするのは非常に手間だし、ヒューマンエラーを起こす可能性もあるので危険です。
AWS CLIを利用すればデプロイを自動化できますが、自分でデプロイ用のスクリプトを書く必要があり、管理するリソースが多いとスクリプトも複雑になってしまいます。
Serverlessであれば設定ファイルという形でLambdaを定義することができ、様々なサーバーレスアーキテクチャのパターンが想定されているため、柔軟で記述量も少なくて済みます。また、`serverless deploy`というコマンド一つでデプロイできます。
インフラのコード化が可能
Lambdaの定義や必要なリソースを設定ファイルに記述できるため、Lambda周りのインフラの品質保証ができ、再利用性が高まります。
フォースタではInfrastructure as Codeの手段としてTerraformも利用していますが、Lambdaに関してはサーバーレスアーキテクチャに特化したServerlessを利用しています。
プラグインが豊富
サーバーレスアプリケーション構築の幅を広げてくれるプラグインが1000以上あります。
例を挙げると以下のようなものがあります。
- 指定したドメインでAPIアクセスができるようになるServerless Domain Manager
- Webpackによるビルドを提供するServerless Webpack
- Lambdaのコードストレージから不要なコードを削除するServerless Prune Plugin
これらは多くが個人によって開発されているオープンソースで、Github上に公開されています。
Serverlessの仕組み
続いて、Serverlessの仕組みを知るために簡単なチュートリアルを行い、裏側で何が起こっているのかを確認していきます。
このチュートリアルで作成されるのは、実行すると文字列を返すLambdaです。
事前準備
Serverlessを初めて利用する場合は、ServerlessのインストールやAWSリソースにアクセスするための認証情報の設定が必要です。
Serverlessのインストール
$ npm install -g serverless
なお、ServerlessはNode.jsのバージョン6以上が必要です。
IAMユーザーの作成
続いてAWSのマネジメントコンソール上で、管理ポリシーが「AdministratorAccess」で「プログラムによるアクセス」を許可するユーザーを作成します。
作成後に表示されるアクセスキーIDとシークレットアクセスキーは次の手順で利用します。
認証情報の設定
AWSの認証情報を作成します。
$ serverless config credentials --provider aws --key アクセスキーID --secret シークレットアクセスキー
Lambdaの作成
事前準備が完了したので、ここからLambdaを作成していきます。
Lambdaの定義ファイルを作成
以下コマンドで作業ディレクトリとLambdaの定義ファイル作成します。
$ serverless create --template aws-python --path myService
「myService」という作業ディレクトリを作成し、Pythonランタイムで動くLambda用のテンプレート「aws-python」が以下のように作業ディレクトリに展開されます。
serverless.ymlがLambdaの設定ファイル、handler.pyが実行時に動くコードです。
myService
├─.gitignore
├─handler.py
└─serverless.yml
デプロイ
$ serverless deploy
コンソールで確認すると以下のようなLambdaが作成されていることがわかります。非常に簡単にデプロイすることができたのではないでしょうか。
裏側で起こっていること
serverless deployコマンド実行後、以下のようにCloudFormationでリソースが作成されています。
Lambdaの実行ロールとして作成されたIAMロールには、CloudWatchの書き込みポリシーが付与されています。
このチュートリアルではこのポリシーのみですが、serverless.yml上で実行ロールを定義することで、S3やRDS等のリソースへのアクセスポリシーを付与することができます。
S3バケットにはCloudFormationのテンプレートとLambdaのデプロイパッケージが作成されています。
今回はこれだけでしたが、他にもserverless.ymlを変更したり、プラグインを追加することで、API GatewayやRoute53等のリソースも作成されます。
フォースタでの利用例
フォースタでは、Googleドライブ上のPDFをテキストに変換する処理を、AWSのHTTP APIとLambdaを利用して構築しています。
VPC等のネットワーク周り、Elasticsearch Service、中間ファイルを置くためのS3バケットは事前に作成する必要がありますが、それ以外のリソースについてはServerlessを利用して作成しています。
対応概要
この仕組みの構築のために、大きく以下の3つの作業がありました。
後ほどコードを見ながら詳細に説明していきます。
Lambda Layer構築
以下のパッケージは容量が重くデプロイに時間がかかるため、Lambda Layerとして事前にデプロイします。Lambda LayerもServerlessを利用してデプロイができます。
- pdfminer.six(PDFのテキスト変換)
- pydrive(Googleドライブアクセス)
- elasticsearch-py(Elasticsearchアクセス)
- rollbar(エラー通知)
Lambda関数作成
以下2つのLambda関数の構築とHTTP APIのドメイン設定をServerlessで行います。
- pdfParse:APIアクセスをトリガにGoogleドライブからPDFを取得し、テキストに変換してS3に配置
- updateDocument:S3へファイルが置かれたことをトリガにElasticsearchを更新
HTTP APIのカスタムドメインを作成しないと、デプロイの度にAPIのエンドポイントが変わってしまいます。
serverless-domain-managerプラグインを利用することでカスタムドメインの作成、HTTP APIとの紐付けを行います。
HTTP APIのオーソライザーの設定(事前準備)
HTTP APIのアクセス制御のためにオーソライザーの設定をする必要があります。
今回はAuth0を利用して、以下のようなオーソライザーを設定しました。
ここで設定した値は、後ほどLambda関数の定義時に利用します。
この手順の詳細については以下の通りですが、ServerlessにてHTTP APIとJWTオーソライザーの紐付け等全て実施してくれるので、Configure the JWT Authorizerのセクションに記載のAuth0のコンソール上での設定のみでよいです。
Auth0については以下記事もご参照ください。
構成図
Lambda Layer構築
パッケージインストール
まずLayerにするパッケージをインストールします。
$ pip install -t pdfminer.six/python/lib/python3.7/site-packages pdfminer.six
$ pip install -t pydrive/python/lib/python3.7/site-packages pydrive
$ pip install -t elasticsearch/python/lib/python3.7/site-packages elasticsearch
$ pip install -t rollbar/python/lib/python3.7/site-packages rollbar
フォルダ構成が以下のようになるようにファイルを配置します。
実はここが一番大切で、ディレクトリ構成をしっかりと守らないとデプロイ時にエラーが発生します。
layer
├── elasticsearch
│ └── python
│ └── lib
│ └── python3.7
│ └── site-packages
│ └── …
├── pdfminer.six
│ └── python
│ └── lib
│ └── python3.7
│ └── site-packages
│ └── ...
├── pydrive
│ └── python
│ └── lib
│ └── python3.7
│ └── site-packages
│ └── ...
├── rollbar
│ └── python
│ └── lib
│ └── python3.7
│ └── site-packages
│ └── ...
└── serverless.yml
Lambda Layerの定義
Lambda Layerの定義もserverless.ymlを利用しますが、Lambda関数定義用のserverless.ymlとは別ファイルで管理します。
Lambda関数の定義時に、利用するLambda Layerのリソースを指定する必要があるため、resourcesのOutputsパラメータを指定して後々参照できるようにしておく必要があります。
layer/serverless.yml
service: test-layer
provider:
name: aws
runtime: python3.7
stage: production
region: ap-northeast-1
layers:
pdfminer:
path: pdfminer.six
description: pdfminer layer
CompatibleRuntimes:
- python3.7
pydrive:
path: pydrive
description: pydrive layer
CompatibleRuntimes:
- python3.7
elasticsearch:
path: elasticsearch
description: elasticsearch layer
CompatibleRuntimes:
- python3.7
rollbar:
path: rollbar
description: rollbar layer
CompatibleRuntimes:
- python3.7
resources:
Outputs:
PdfminerLayerExport:
Value:
Ref: PdfminerLambdaLayer
Export:
Name: PdfminerLambdaLayer
PydriveLayerExport:
Value:
Ref: PydriveLambdaLayer
Export:
Name: PydriveLambdaLayer
ElasticsearchLayerExport:
Value:
Ref: ElasticsearchLambdaLayer
Export:
Name: ElasticsearchLambdaLayer
RollbarLayerExport:
Value:
Ref: RollbarLambdaLayer
Export:
Name: RollbarLambdaLayer
デプロイ
layer/serverless.ymlがあるディレクトリにてLambda Layerをデプロイします。
Lambda LayerもLambda関数も同じコマンドでデプロイできるのは楽ですね。
$ serverless deploy
Lambda関数作成
Serverlessプラグインのインストール
今回は以下3つのServerlessプラグインを利用しました。
serverless pluginコマンドでプラグインをインストールできます。
- serverless-python-requirements
依存するPythonライブラリをインストールせずとも、デプロイ時にrequirements.txtやPipfileを参照してデプロイパッケージを作ってくれます。
$ serverless plugin install -n serverless-python-requirements
- serverless-prune-plugin
Lambdaのコードストレージから過去のコードを削除してくれます。
$ serverless plugin install -n serverless-prune-plugin
- serverless-domain-manager
このプラグインによってHTTP APIのカスタムドメインの設定を行うことができます。serverless.ymlを記述することでカスタムドメイン作成だけでなく、Route53へのレコード追加も行ってくれます。
このプラグインだけ、npmでインストールすることを奨励されていました。
$ npm install serverless-domain-manager --save-dev
これらのプラグインをインストールすると、package.jsonにプラグイン情報が記述されるだけでなく、serverless.ymlにも以下が追記されます。
function/serverless.yml
plugins:
- serverless-python-requirements
- serverless-prune-plugin
- serverless-domain-manager
Lambda関数の定義
Lambda関数の構築とHTTP APIのドメイン設定をServerlessで行います。
なお、PDFをテキスト変換する処理(parse.py)、Elasticsearchを更新する処理(update.py)についてはServerlessの話から逸脱するため省略します。
以下のようなフォルダ構成でファイルを配置します。
function
├── package-lock.json
├── package.json
├── parse.py
├── requirements.txt
├── serverless.yml
├── service-account.json # Googleドライブアクセス用
└── update.py
serverless.ymlを記述していきます。
ポイントは以下の設定部分です。
function/serverless.yml
service: test
provider:
name: aws
runtime: python3.7
stage: production
region: ap-northeast-1
environment:
LAYER_SERVICE: test-layer
TMP_BUCKET: ${self:provider.s3.tmpBucket.name}
OUTPUT_PATH: example/
ROLLBAR_KEY: rollbarのアクセストークン
logRetentionInDays: 30
iamRoleStatements: # 適切に設定
- Effect: Allow
Action: '*'
Resource: '*'
httpApi:
authorizers:
auth0:
identitySource: $request.header.Authorization
issuerUrl: Auth0のテナントURL
audience:
- https://auth0-jwt-authorizer
s3:
tmpBucket:
name: testBucket
package:
exclude:
- .git/**
- layer/**
- package.*
functions:
parse:
handler: parse.post
name: parsePdf
package: {}
events:
- httpApi:
method: POST
path: /example
authorizer:
name: auth0
layers:
- ${cf:${self:provider.environment.LAYER_SERVICE}-${opt:stage, self:provider.stage}.PdfminerLayerExport}
- ${cf:${self:provider.environment.LAYER_SERVICE}-${opt:stage, self:provider.stage}.PydriveLayerExport}
- ${cf:${self:provider.environment.LAYER_SERVICE}-${opt:stage, self:provider.stage}.RollbarLayerExport}
update:
handler: update.put
name: updateDocument
package: {}
events:
- s3:
bucket: tmpBucket
event: s3:ObjectCreated:Put
rules:
- prefix: ${self:provider.environment.OUTPUT_PATH}
vpc:
securityGroupIds:
- 作成したセキュリティグループ
subnetIds:
- 作成したプライベートサブネット
layers:
- ${cf:${self:provider.environment.LAYER_SERVICE}-${opt:stage, self:provider.stage}.ElasticsearchLayerExport}
- ${cf:${self:provider.environment.LAYER_SERVICE}-${opt:stage, self:provider.stage}.RollbarLayerExport}
plugins:
- serverless-python-requirements
- serverless-prune-plugin
- serverless-domain-manager
custom:
prune:
automatic: true
includeLayers: true
number: 5
customDomain:
domainName: 設定したいカスタムドメイン名
certificateName: 証明書名
endpointType: regional
securityPolicy: tls_1_2
apiType: http
HTTP APIのオーソライザー
オーソライザーの設定はAWSのコンソールからももちろん設定できますが、出来る限りServerlessで管理したいので、serverless.ymlの記述をすることでオーソライザーの設定を行います。
「HTTP APIのオーソライザーの設定」のセクションで設定したIdentifierをHTTP APIのパラメータとして登録します。
httpApi:
authorizers:
auth0:
identitySource: $request.header.Authorization
issuerUrl: Auth0のテナントURL
audience:
- https://auth0-jwt-authorizer
Lambda関数のトリガとなるHTTP APIのドメインを固定するためにServerless Domain Managerプラグインを利用しています。
このプラグインを利用すると、serverless.ymlに以下を記述することでカスタムドメインの設定をすることができます。
custom:
customDomain:
domainName: 設定したいカスタムドメイン名
certificateName: 証明書名
endpointType: regional
securityPolicy: tls_1_2
apiType: http
デプロイ
Lambda関数のデプロイの前にカスタムドメインを作成します。
$ serverless create_domain
10分程度で初期化され、AWS上でカスタムドメインが作成されたことを確認できます。
カスタムドメイン作成、Lambda関数のデプロイを行います。
$ serverless deploy
Lambda関数がデプロイされ、HTTP APIのオーソライザーも設定されたことが確認できました。
おわりに
Serverlessの概要とフォースタにおける利用例を紹介しました。
Serverlessを利用することで、サーバーレスアプリケーションのデプロイが簡単になるだけでなく、インフラがコード化できるため品質保証でき、再利用性が高まります。
フォースタでも実際に導入して、サーバーレスアプリケーション開発の質も効率も格段に上がりました。
サーバーレスは今後も大きなトレンドだと思うので、Serverlessのような便利なツールを適宜使い、効果的に開発していければと思います。