こちらの記事はDIVXアドベントカレンダー2023の18日目の記事です。
はじめに
こんにちは、株式会社divxエンジニアの関口です。
今回のブログでは、実務で急に関わることにもなりやすい「AWS Lambda」や「Serverless Framework」を使った実践的な実装まで行いました。
「AWS Lambda」や「サーバレス」に関心がある方に一読していただけると幸いです。それでは、早速進めていきましょう。
AWS Lambdaって一体なにがすごいのか
「AWS Lambda」は2014年に登場しました。最近では「最新の注目技術!」という感じではないかもしれませんが、実際に多くの業務で活用されている印象があります。
ところが、具体的に「Lambda」が何をできるのか、AWS公式サイトを見てみましたが…
「AWS Lambdaを使用すれば、サーバのプロビジョニングや管理なしでコードを実行できます。課金は実際に使用したコンピューティング時間に対してのみ発生し、コードが実行されていないときには料金も発生しません。」
この説明だけでは、「Lambda」の魅力が十分に理解できないかもしれません。でも、「Lambdaを使用すれば、どのような種類のアプリケーションやバックエンドサービスでも、管理の手間をかけずに実行できるんです」という点は、やっぱりなんだかすごそうです。
そこで、この記事では、AWS Lambdaのすごさをもっと理解するために、実際に手を動かしながら掘り下げてみたいと思います。AWS Lambdaの魅力を一緒に探っていきましょう。
実行したい処理だけを開発すれば良いサーバレスのすごさ
AWS Lambdaは、AWSにおいて「サーバーレスコンピューティングとアプリケーション」として紹介されています。要するに、Lambdaはサーバーレスを実現するための特別なサービスなんです。しかし、サーバーレスという言葉を聞いても、「サーバーがない」とは理解できても、それが具体的にどういうことかは疑問ですよね。
「サーバーレスは、プログラムを実行するために自分でサーバーを用意する必要がないというアイデアです。そして、そのプログラムの実行環境を提供するのが、AWS Lambdaなんです」と言っても、まだ分かりにくいかもしれませんね。
そこで、もう少し具体的な例を考えてみましょう。
AWS上でWebアプリケーションを運用する場合、通常はEC2インスタンスを起動し、WindowsやLinuxの環境を設定します。さらに、Webサーバーが必要ですし、動的なシステムを構築するなら、Javaなどのプログラムを実行するためのWebアプリケーションサーバーも必要です。
しかし、Lambdaを使えば、Webサーバーもアプリケーションサーバーも用意する必要がありません。単にプログラムを開発するだけで、実行環境はAWSが提供してくれるのです。これはすごいですね!
Lambdaで具体的に何ができるの?
このようにLambdaを使用すると、データベースとSlackの間でデータの自動送信プロセスを構築できます。たとえば、データベース内の新しいエラーログを検出し、それをSlackチャンネルに通知して、運用チームが問題を早期に把握できるようにするなど、さまざまな用途で活用できます。
このイベントトリガーとして、AWSのストレージであるS3やデータベースのDynamoDBが利用できることがあります。さらに具体的な用途を見てみましょう。
たとえば、Lambdaを使用して、データベースから情報を取得し、その情報をSlackに通知する場面を考えてみます。
1. データベースへの接続: Lambda関数は、データベースへのアクセスに必要な情報を持っています。AWS LambdaはセキュリティグループやIAMロールを使ってデータベースへのアクセスを管理し、安全に接続できます。
2. データベースから情報の取得: Lambda関数内で、必要なデータをデータベースからクエリやAPI呼び出しを使って取得します。たとえば、ユーザー情報、ログデータなどを取得することが考えられます。
3. Slackへの通知: データの取得が完了したら、Lambda関数はSlackのAPIを利用して通知を送信します。通知の内容やフォーマットは、Slackメッセージの形式に合わせて設定します。Slack APIを呼び出すために必要なトークンや認証情報をLambda関数に設定します。
4. イベントトリガー: Lambda関数をトリガーするイベントが発生します。例えば、データベースの特定の情報が更新される、新しいファイルがS3バケットにアップロードされる、定時スケジュールに基づく実行などが考えられます。
5. Lambda関数の実行: このイベントに応じて、Lambda関数が自動的に実行されます。この関数の中で、必要なデータの処理や取得が行われます。
6. メッセージの送信: Lambda関数がデータベースなどから必要な情報を取得した後、その情報を含むメッセージをSlackの特定チャンネルに送信します。このメッセージには、例えばデータベースからのクエリ結果や関連する情報が含まれることになります。
このようにLambdaを使用すると、データベースからの情報取得とSlackへの通知を自動化でき、例えば新しいエラーログの検出や問題の早期通知など、さまざまな用途で役立つことがあるんですね。
DynamoDBからタスクの納期を取得して、SLACKに通知してみる
では、実際に手を動かして、Lambdaで実装してみます。
AWS Cloud9で実装環境を用意する
まずは、Webブラウザ上でコードを実装できる、総合開発環境であるAWS Cloud9をAWSのEC2(サーバ)上で動かして使います。
早速、AWSのマネジメントコンソールでcloud9と検索して、Cloud9のページにアクセスします。そして、”環境を作成”をクリックして、環境を作成していきます。以下の設定を参考にしてください。
💡 名前:任意の名前
環境タイプ:新しい EC2 インスタンス
インスタンスタイプ:t2.micro
プラットフォーム:Amazon Linux 2
タイムアウト: 30分
Node.jsのインストール
次に、Could9を開き、ターミナルでNode.jsをインストールします。
💡 nvm install 16でNode.jsをインストールします。
次に、node -vを実行して、Node.jsがインストールされている確認してください。
Serverless Frameworkのインストール
またCould9を開き、ターミナルでServerless Frameworkをインストールします。
💡 npm install -g serverless@3.24.1
次に、以下のコマンドでインストールできているか確認します。
serverless -v
Serverless Frameworkのテンプレートの準備
💡 以下のコマンドでテンプレートを作成します
serverless create —template aws-nodejs-ecma-script
テンプレート作成後、serverless.ymlファイルを開きます。
次に、今回はバージョン16のNode.jsを使うので、以下のように修正してください。
💡
provider:
name: aws
runtime: nodejs16.x
region: ap-northeast-1
Node.jsのライブラリーのインストール
ターミナルで以下のコマンドを実行すると、package.jsonに従ってNode.jsのライブラリーがインストールされます。
次に、 serverlessフレイムワークをインストールします。
💡 npm install —save-dev serverless@3.23.1
定期実行される関数の作成
serverless.ymlのfunctionsの箇所に新しい関数を作成します。
functions:
taskList:
handler: src/task.list
events:
- httpApi:
method: get
path: /tasks
# 日本時間で毎朝9時
- schedule: cron(0 0 * * ? *)
そして、srcディレクトリにtask.jsファイルを作成します。
// Slackへ通知する関数
export async function scheduledNotify(event, context) {
return {
message: "Hello Serverless Framework"
}
上記まで完了したら、npx serverless deployコマンドを実行します。
デプロイが完了後、AWS Lambdaのページに移動すると、今回作成した関数が作成されています。
npx serverless deploy というコマンドを実行する際、以下のようなエラーが発生する可能性があります。
💡
CREATE_FAILED: IamRoleLambdaExecution (AWS::IAM::Role)
The security token included in the request is invalid (Service:
AmazonIdentityManagement; Status Code: 403; Error Code: InvalidClientTokenId;
Request ID: 84eeb584-e3e7-3c97-e6a2-9037afa8b3a6; Proxy: null)
こちらは、Cloud9環境の権限不足に起因するエラーです。
・Cloud9のAMTCを無効にする
・Cloud9のEC2にIAM Roleをアタッチする(IAM Roleには「AdministratorAccess」の権限を付与する)
という2つの手順を実施すると、Cloud9からIAM Roleを作成できるようになり、npx serverless deploy コマンドのエラーが解消します。
新しい関数の詳細を見ると、トリガーの箇所にEventBridgeと表示されています。
EventBridgeをクリックすると、EventBridgeの設定を確認できます。イベントスケジュールでは、日本時間のいつ関数が実行されるか確認できます。(もし時刻がUTCだったら、ローカルタイムゾーンに変更してください。)
Slack通知機能を実装
プログラムからslackに通知する機能を作りたい場合、slackのWebhook機能を使用します。
通知させたいチャンネルを指定して、Webhook URLを取得します。(こちらのブログでは、Webhook URLを作成する方法は割愛いたします。)
AWS Systems ManagerにWebhook URLを保存
プログラムにWebhook URLなどの機密情報を記述するのではなく、Lambda関数をデプロイするときにAWS Systems Managerから値を取得して、Lambda関数の環境変数として設定します。
Systems Managerのページにいき、パラメータストアでパラメータを作成します。
以降、プログラムでは、作成したパラメータを使用します。
ここまで完了しましたら、serverless.ymlを編集します。taskListにenvironmentとSLACK_WEBHOOK_URLを追記します。
functions:
taskList:
handler: src/task.list
events:
- httpApi:
method: get
path: /tasks
# 日本時間で毎朝9時
- schedule: cron(0 0 * * ? *)
environment:
SLACK_WEBHOOK_URL: ${ssm:notify-slack}
上記の設定が完了したら、デプロイして変更が反映されるか確認します。そして、Lambdaのページで、関数の詳細を確認します。設定から環境変数の箇所にSLACK_WEBHOOK_URLが設定されているか確認してください。ちなみに以下の値はコードで参照することができます。
では、Cloud9に戻り、Slackライブラリーをインストールします。
💡 npm install @slack/webhook@6.1.0
インストール後、task.jsファイルに戻り、インストールしたSlackライブラリーを使用します。
// Slackへ通知する関数
export async function scheduledNotify(event, context) {
const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL;
const slackWebhook = new IncomingWebhook(slackWebhookUrl);
await slackWebhook.send("hello Slack")
}
上記の編集ができましたら、デプロイします。デプロイ後、またLambdaのページに戻り、テストを実行します。実行結果が成功になりましたら、Slackにデータベースで取得したデータが通知できます。
IAMの設定
次に、LambdaからDynamoDBにアクセスして、データを取得するためにIAMを設定します。
まずserverlessのフレームワークのプラグインをインストールします。
npm install --save-dev serverless-iam-roles-per-function@3.2.0
上記のインストールが完了したら、serverless.ymlを以下の通りに追記します。
function:
taskList:
handler: src/task.list
events:
- httpApi:
method: get
path: /tasks
- schedule: cron(0 0 * * ? *)
environment:
SLACK_WEBHOOK_URL: ${ssm:notify-slack}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Scan
Resource: 'arn:aws:dynamodb:*:*:table/tasks'
ここまで完了したら、デプロイします。デプロイが成功しましたら、Lambdaの設定を確認します。設定のアクセス権限で、アクション(dynamodb:Scan)が追加されていたら、テストしてみます。実行結果が成功だったら、Slackにデータベースで取得したデータが通知できます。
Severless API Gateway Throttingの導入
Lambdaは実行時間の分だけ課金されるため、Lambdaを使ったWeb APIをインターネットに公開していると、大量アクセスによる高額請求が発生する可能性があります。(EDoS攻撃と呼びます)
EDoS攻撃への対策として、API Gatewayが許可するリクエストを少なく制限します。
Cloud9に戻り、ターミナルで以下をインストールします。
npm install --save-dev serverless-api-gateway-throtting@2.0.2
インストールが完了したら、severless.ymlを開いて、プラグインの設定を行います。
# Add the serverless-webpack plugin
plugins:
- serverless-webpack
- serverless-iam-roles-per-function
- serverless-api-gateway-throttling
custom:
apiGatewayThrottling:
maxRequestsPerSecond: 5
maxConcurrentRequests: 5
デプロイが完了しましたら、Severless API Gateway Throttingの導入は完了です。
DynamoDBのテーブル作成
DynamoDBのページから”テーブルの作成”を選択し、以下の項目を入力します。
💡 テーブル名:任意のテーブル名
パーティションキー(プライマリーキー):id
ソートキー:空のまま
デフォルト:デフォルトのまま
テーブルが作成されたので、項目を追加していきます。
項目は、”新しい属性の追加”を選択して、追加することができます。
GUIでDynamoDBのテーブルを作成できますが、今回はServerless FrameworkでのDynamoDBのテーブルを作成してみます。
まず、Severless.ymlファイルを開き、以下のように追記していきます。
resources:
Resources:
TasksDynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: 任意のテーブル名
AttributeDefinitions:
- AttributeName: id
AttributeType: S //String型
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
CloudFormationのrecourcesの書き方は調べれば参考になる記事があるので、暗記する必要はありません。上記の記述が完了したら、デプロイします。デプロイが完了しましたら、DynamoDBのページでテーブルが作成されているので、確認します。
タスクと納期を登録するAPIの実装
まず、Severless.ymlファイルを開き、新しい関数を定義していきます。
今回はDynamoDBにアクセスする必要があるので、iamRoleStatementも記述します。
updateTask:
handler: src/task.post
events:
- httpApi:
method: post
path: /任意のテーブル名
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:PutItem
Resource: 'arn:aws:dynamodb:*:*:table/tasks'
次に、srcディレクトリ直下にupdateTask.jsファイルを追加します。
以下のファイルでは、タスクと納期を追加するための内容です。
import { DynamoDB } from 'aws-sdk'
import crypto from 'crypto'
export async function post(event, context) {
const requestBody = JSON.parse(event.body)
//タイトル作成
const item = {
id: { S: crypto.randomUUID() },
title: { S: requestBody.title },
deadline: { S: requestBody.deadline }
}
const dynamodb = new DynamoDB({
region: 'ap-northeast-1'
})
//保存処理
await dynamodb
.putItem({
TableName: '任意のテーブル名',
Item: item
})
.promise()
return item
}
次に、デプロイします。デプロイが完了しましたら、ターミナルに新規APIが作成されていることが確認できます。
タスクと納期を取得するAPIの実装
まず、serverless.ymlファイルを開き、新しい関数の定義を追加します。
taskList:
handler: src/task.list
events:
- httpApi:
method: get
path: /任意のテーブル名
- schedule: cron(0 0 * * ? *)
environment:
SLACK_WEBHOOK_URL: ${ssm:notify-slack}
iamRoleStatements:
- Effect: Allow
Action:
- dynamodb:Scan
Resource: 'arn:aws:dynamodb:*:*:table/任意のテーブル名'
次に、task.jsファイルを開き、listという関数を作成します。
export async function list(event, context) {
const dynamodb = new DynamoDB({
region: 'ap-northeast-1'
})
const result = await dynamodb
.scan({
TableName: '任意のテーブル名'
})
.promise()
const tasks = result.Items.map((item) => {
return {
id: item.id.S,
title: item.title.S,
deadline: item.deadline.S
}
})
return { tasks: tasks }
}
ここまで完了しましたら、ターミナルでデプロイします。デプロイが完了しましたら、新しいGETのAPIが追加されます。
S3バケットの設定
フロントエンドのホスティングで使用するS3バケットは、serverless.yamlファイルに構築していきます。bucketNameは、他と被らないような名前にしてください。
custom:
apiGatewayThrottling:
maxRequestsPerSecond: 5
maxConcurrentRequests: 5
bucketName: notify-lambda-任意の名前
resources:
Resources:
TasksDynamoDBTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: tasks
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
ProvisionedThroughput:
ReadCapacityUnits: 5
WriteCapacityUnits: 5
StaticSiteS3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: ${self:custom.bucketName}
PublicAccessBlockConfiguration:
BlockPublicAcls: false
BlockPublicPolicy: false
IgnorePublicAcls: false
RestrictPublicBuckets: false
WebsiteConfiguration:
IndexDocument: index.html
StaticSiteS3BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket:
Ref: StaticSiteS3Bucket
PolicyDocument:
Statement:
- Sid: PublicReadGetObject
Effect: Allow
Principal: '*'
Action:
- s3:GetObject
Resource: 'arn:aws:s3:::${self:custom.bucketName}/*'
上記の追記が完了しましたら、デプロイします。
デプロイ後、Amazon S3ページに作成したバケットが追加されます。
次に、デプロイ時にS3にファイルをアップロードするため、Serverless S3 Syncプラグインをインストールします。ターミナルで以下のコマンドを実行します。
npm install --save-dev serverless-s3-sync@3.1.0
インストールが完了しましたら、serverless.ymlファイルに追記していきます。
custom:
apiGatewayThrottling:
maxRequestsPerSecond: 5
maxConcurrentRequests: 5
bucketName: notify-lambda-leon
s3Sync:
buckets:
- bucketName: ${self:custom.bucketName}
localDir: static
index.htmlファイルを作成
staticディレクトリを作成して、その中にindex.htmlファイルを作成します。
<!DOCTYPE html>
<html lang="ja">
<body>
<h1>テスト</h1>
</body>
</html>
デプロイ後、Amazon S3ページにいき、自分で作成したバケット名をクリックします。
リロードすると、index.htmlが作成されます。実際に同画面のプロパティから静的ウェブサイトホスティングのURLをクリックすると、実装したhtmlの内容がブラウザに表示されます。
補足ですが、index.htmlファイルを右クリックするとプレビュ機能があり、Cloud9で実装した内容が表示され便利です。
次に、index.htmlファイルを編集していきます。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8" />
<title>タスク管理</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<h1>タスク管理</h1>
<input id="task-title-input" type="text" placeholder="タスクを入力" />
<input id="task-deadline-input" type="text" placeholder="納期を入力" />
<button id="task-add-button">追加</button>
<div class="list-titles">
<h2>タスク一覧</h2>
<h2>納期一覧</h2>
</div>
<div class="task-deadline-container">
<ul id="task-list"></ul>
<ul id="deadline-list"></ul>
</div>
<script type="module" src="main.js"></script>
</body>
</html>
Javascriptの実装
index.htmlと同様にstaticディレクトリにmain.jsを追加します。
ひとまず以下の内容を記述して、デプロイしてみます。
デプロイが完了しましたら、S3ページにmain.jsがアップロードされています。
ブラウザ上で検証ツールを開き、コンソールで”テスト”と表示されていたら成功です。
次に、ターミナルで表示されている呼び出すAPIをコピーして貼り付けます。
本ブログでは、Javascriptの解説は割愛いたします。
const API_URL_PREFIX =
'<https://fk4eth1u3d.execute-api.ap-northeast-1.amazonaws.com>'
const taskListElement = document.getElementById('task-list')
const deadlineListElement = document.getElementById('deadline-list')
const taskTitleInputElement = document.getElementById('task-title-input')
const taskDeadlineInputElement = document.getElementById('task-deadline-input')
const taskAddButtonElment = document.getElementById('task-add-button')
async function loadTasks() {
const response = await fetch(API_URL_PREFIX + '/tasks')
const responseBody = await response.json()
const tasks = responseBody.tasks
while (taskListElement.firstChild) {
taskListElement.removeChild(taskListElement.firstChild)
}
while (deadlineListElement.firstChild) {
deadlineListElement.removeChild(deadlineListElement.firstChild)
}
tasks.forEach((task) => {
const titleLiElement = document.createElement('li')
titleLiElement.innerText = task.title
taskListElement.appendChild(titleLiElement)
const deadlineLiElement = document.createElement('li')
deadlineLiElement.innerText = task.deadline
deadlineListElement.appendChild(deadlineLiElement)
})
}
async function registerTask() {
const title = taskTitleInputElement.value
const deadline = taskDeadlineInputElement.value
const requestBody = {
title: title,
deadline: deadline
}
await fetch(API_URL_PREFIX + '/tasks', {
method: 'POST',
body: JSON.stringify(requestBody)
})
await loadTasks()
}
async function main() {
taskAddButtonElment.addEventListener('click', registerTask)
await loadTasks()
}
main()
ここまで完了しましたら、デプロイします。
CORSの設定
CORS(Cross-origin resource sharing)とは、異なるオリジンへのアクセスを許可するための仕組みです。
S3をつかったWebサイトからAPI Gatewayにアクセスして、データを取得できるように設定します。まず、serverless.ymlファイルを編集していきます。
provider:
name: aws
runtime: nodejs16.x
region: ap-northeast-1
httpApi:
cors:
allowedOrigins:
- '<http://$>{self:custom.bucketName}.s3-website-ap-northeast-1.amazonaws.com'
デプロイ後に、ブラウザでタスクと納期が一覧表示できるようになります。
SLACK通知の設定
task.jsファイルを編集していきます。以下のコードでは、以下の3つの処理を行なっています。
1. データベースからタスクを取得する。
2. 取得したタスクをSlackに通知する。
3. 新しいタスクをデータベースに追加する。
import { DynamoDB } from 'aws-sdk'
import crypto from 'crypto'
import { IncomingWebhook } from '@slack/webhook';
// Slackへ通知する関数
export async function scheduledNotify(event, context) {
const dynamodb = new DynamoDB({
region: 'ap-northeast-1'
});
const result = await dynamodb.scan({
TableName: '任意のテーブル名'
}).promise();
const tasks = result.Items.map(item => ({
id: item.id.S,
title: item.title.S,
deadline: item.deadline.S
}));
}
async function notifySlack(tasks) {
const slackWebhookUrl = process.env.SLACK_WEBHOOK_URL;
const slackWebhook = new IncomingWebhook(slackWebhookUrl);
let message = 'タスクリスト:\\n';
tasks.forEach(task => {
message += `* ${task.title} - 締め切り: ${task.deadline}\\n`;
});
await slackWebhook.send({
text: message
});
}
export async function list(event, context) {
const dynamodb = new DynamoDB({
region: 'ap-northeast-1'
})
const result = await dynamodb
.scan({
TableName: 'tasks'
})
.promise()
const tasks = result.Items.map((item) => {
return {
id: item.id.S,
title: item.title.S,
deadline: item.deadline.S
}
})
// タスクをSlackに通知
await notifySlack(tasks);
return { tasks: tasks }
}
export async function post(event, context) {
const requestBody = JSON.parse(event.body)
const item = {
id: { S: crypto.randomUUID() },
title: { S: requestBody.title },
deadline: { S: requestBody.deadline }
}
const dynamodb = new DynamoDB({
region: 'ap-northeast-1'
})
await dynamodb
.putItem({
TableName: 'tasks',
Item: item
})
.promise()
return item
}
CSSの設定
staticディレクトリにcssディレクトリを作成して、style.cssファイルを追加します。
.list-titles {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.list-titles h2 {
text-align: center;
flex: 1; /* 各タイトルが均等な幅を持つようにする */
}
.task-deadline-container {
display: flex;
justify-content: space-between;
}
#task-list,
#deadline-list {
list-style: none;
padding: 0;
width: 45%; /* 各リストの幅を調整 */
}
#task-list li,
#deadline-list li {
border-bottom: 1px solid #ddd;
padding: 8px;
}
実際にSLACKに通知させてみましょう
まず入力フォームにタスクと納期を入力して、追加ボタンをクリックします。
追加ボタンを押しましたら、タスクと納期が一覧表示されます。
SLACKのチャンネルにタスクと納期のリストが通知されています。
これでタスクと納期をSLACKに通知することができました!
まとめ
Lambdaは、様々なデータや結果などをSlackへ通知できるのでとても便利だと感じました。
今回は、AWSの公式ドキュメントや参考資料を参考にしながら、サンプルアプリを作成しました。
実際に手を動かして学ぶことにより、Lambdaの機能性とサーバーレスアーキテクチャへの理解を一層深めることができました。
divxでは一緒に働ける仲間を募集しています。
興味があるかたはぜひ採用ページを御覧ください。