「AWS Hands-on for Beginners Serverless #1」を参考にサーバーレスアーキテクチャ を Terraform で実装してみた
2022.6.16
はじめに
はじめまして。CloudLead チームの Cold-Airflow です。
タイトルにあるように、 AWS が公開している AWS Hands-on for Beginners の 「サーバーレスアーキテクチャで翻訳 Web API を構築する」 を Terraform を使ってデプロイしてみた内容となっております。
AWS Hands-on for Beginners とは?
AWS が様々なアーキテクチャーの構成を動画で解説してくれているので、それにそって実際に手を動かしながら AWS サービスについて学べるコンテンツです。
実際に手を動かして学ぶ!AWS Hands-on for Beginners のご紹介 | Amazon Web Services ブログ
今回使用するサーバーレスアーキテクチャのハンズオン以外にも AWS の基礎から機械学習や IaC などと盛りだくさんのコンテンツです。
ほかのハンズオンにご興味がある方は下記のリンクからご覧ください。
ハンズオン資料 | AWS クラウドサービス活用資料集
今回は数あるコンテンツの中からサーバーレスを使ったハンズオンをもとに構築を行いたいと思います。
翻訳 Web API の構築を通して、サーバーレスアーキテクチャの基本を学んでいただきます。
AWS Hands-on for Beginners | AWS
本記事について
想定読者
- Terraform を使って何かを作りたい方
- Terraform でサーバレスアーキテクチャを作成したい方
なお、以下については説明しておりません。
- Terraform のインストール手順
- 作成する AWS リソースの説明
本記事では、Terraform の内容に注力して解説を行います。
そのため使用するアーキテクチャや AWS サービスの内容は実際の AWS ハンズオンをご覧ください。
ゴール
- AWS Lambda, Amazon API Gateway, Amazon DynamoDB サービスを組み合わせて、サーバーレスな Web API を作成する
構築環境
Terraform のバージョンは以下のとおりです。
C:\Users>terraform --version Terraform v1.1.6 on windows_amd64
ディレクトリ構造は以下のとおりです。
Lambda のコードは、contents
配下に配置します。
C:. │ main.tf │ variables.tf │ └─contents ├─src │ lambda_function.py │ └─zip lambda.zip
Terraform 構築開始
作成するリソースは下記のとおりです。
- Amazon API Gateway
- AWS Lambda
- Amazon DynamoDB
ハンズオンと同じ順番で作成します。
なお、Amazon Translate はリソースを作成するものではなく、API として Lambda で呼び出しで使用します。
コード全体をご覧になりたい方は本記事末尾に飛んでください。
AWS 構成図
今回作成するハンズオンの AWS 構成図です。
翻訳 API を AWS サーバレスアーキテクチャで実装します。
API Gateway 経由で Lambda を呼び出します。
Lambda では受け取った値 Translate に投げて翻訳します。
さらに、DynamoDB に入力と出力結果を記録するアーキテクチャーとなっています。
DynamoDB の作成
まずは、データを格納する DynamoDB を作成します。
役割としては、API Gateway から送信されたデータと翻訳したデータを格納するために使用します。
resource "aws_dynamodb_table" "basic-dynamodb-table" { name = "translate-history" billing_mode = "PROVISIONED" read_capacity = 1 write_capacity = 1 hash_key = "timestamp" attribute { name = "timestamp" type = "S" } }
Lambda の作成
次は、API のデータを処理する Lambda を作成します。
Lambda のプログラムは Python で実装しています。
コードは ZIP 形式にして Lambda を作成すると同時にアップロードしています。
variable "lambda_file_name" { default = "/contents/src/lambda_function.py" } variable "lambda_file_zip_name" { default = "/contents/zip/lambda.zip" } data "archive_file" "sample_function" { type = "zip" source_file = "${path.module}${var.lambda_file_name}" output_path = "${path.module}${var.lambda_file_zip_name}" } data "aws_iam_policy_document" "AWSLambdaTrustPolicy" { statement { actions = ["sts:AssumeRole"] effect = "Allow" principals { type = "Service" identifiers = ["lambda.amazonaws.com"] } } } resource "aws_iam_role" "function_role" { name = "${var.project}-function_role" assume_role_policy = data.aws_iam_policy_document.AWSLambdaTrustPolicy.json } resource "aws_iam_role_policy_attachment" "lambda_policy" { role = aws_iam_role.function_role.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } resource "aws_iam_role_policy_attachment" "Translate_policy" { role = aws_iam_role.function_role.name policy_arn = "arn:aws:iam::aws:policy/TranslateFullAccess" } resource "aws_lambda_function" "test_lambda" { filename = data.archive_file.sample_function.output_path function_name = "${var.project}-lambda-function" role = aws_iam_role.function_role.arn handler = var.handler_name publish = true source_code_hash = data.archive_file.sample_function.output_base64sha256 runtime = "python3.9" memory_size = 256 timeout = 10 } resource "aws_lambda_permission" "allow_cloudwatch" { statement_id = "AllowExecutionFromCloudWatch" action = "lambda:InvokeFunction" function_name = aws_lambda_function.test_lambda.function_name principal = "events.amazonaws.com" }
Lambda では、「翻訳のためにTranslate
」 と 「データを保存するためにDynamoDB
」にアクセスをするため権限を Lambda に追加しています。
ハンズオンのため、アクセス権限はフルで与えてますが適宜権限を絞ってください。
Translate
resource "aws_iam_role_policy_attachment" "Translate_policy" { role = aws_iam_role.function_role.name policy_arn = "arn:aws:iam::aws:policy/TranslateFullAccess" }
DynamoDB
resource "aws_iam_role_policy_attachment" "dynamodb_policy" { role = aws_iam_role.function_role.name policy_arn = "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess" }
Point 1: ソースコードを読み込むために Data Source archive_file
Lambda のコードをアップロードする必要があります。
Terraform のベストプラクティスはarchive_file
を使うことです。
Terraform で Lambda[Python]のデプロイするときのプラクティス | DevelopersIO
data "archive_file" "sample_function" { type = "zip" source_file = "${path.module}${var.lambda_file_name}" output_path = "${path.module}${var.lambda_file_zip_name}" }
このarchive_file
はファイルを ZIP 形式に変換してくれるものです。
使い方は簡単、Lambda のプログラミングの PATH と ZIP ファイルの格納先を指定するだけです。
archive_file | Data Sources | hashicorp/archive | Terraform Registry
PATH を指定する際にも、工夫をしております。
variable "lambda_file_name" { default = "/contents/src/lambda_function.py" } variable "lambda_file_zip_name" { default = "/contents/zip/lambda.zip" }
${path.module}
:モジュールのディレクトリを指します。
${var.lambda_file_name}
:Lambda プログラムソースファイルパス
${var.lambda_file_zip_name}
:ZIP ファイルパス
Point 2: 外部ソースに Lambda Function のアクセス権限を与える aws_lambda_permission
コンソールから作成すると自動的に追加されますが、Terraform からやる場合はそうならないです。
そのため、各種リソースを連携してもアクセス権限ないため実行できないことがよくあります。
外部ソース(EventBridge Rule、SNS、S3 など)に Lambda 関数へのアクセス権限を与える。
aws_lambda_permission | Resources | hashicorp/aws | Terraform Registry
CloudWatch
:ログを記録するため
resource "aws_lambda_permission" "allow_cloudwatch" { statement_id = "AllowExecutionFromCloudWatch" action = "lambda:InvokeFunction" function_name = aws_lambda_function.test_lambda.function_name principal = "events.amazonaws.com" }
API Gateway
:API の内容を処理するため
resource "aws_lambda_permission" "allow_apigateway" { statement_id = "AllowExecutionFromAPIGateway" action = "lambda:InvokeFunction" function_name = aws_lambda_function.test_lambda.function_name principal = "apigateway.amazonaws.com" source_arn = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.example.id}/*/${aws_api_gateway_method.example.http_method}${aws_api_gateway_resource.example.path}" }
プログラムコード
やっていること
- API Gateway から値を取得
- Translate で受け取った値を翻訳
- 受け取った値と翻訳した値を DynamoDB に格納
- 値を整形して API Gateway に返す
import json import boto3 import datetime def lambda_handler(event, context): translate = boto3.client('translate') dynamodb_translate_histroy_tbl = boto3.resource( 'dynamodb').Table("translate-history") input_text = event["queryStringParameters"]["input_text"] response = translate.translate_text( Text=input_text, SourceLanguageCode='ja', TargetLanguageCode='en' ) output_text = response.get('TranslatedText') dynamodb_translate_histroy_tbl.put_item( Item={ 'timestamp': datetime.datetime.now().strftime("%Y%m%d%H%M%S"), 'input_text': input_text, 'output_text': output_text } ) return { 'statusCode': 200, 'body': json.dumps({ 'output_text': output_text }), 'isBase64Encoded': False, 'headers': {} }
API Gateway の作成
最後に API のエンドポイントとして API Gateway を作成して Lambda と連携します。
Terraform で API Gateway を作成する場合、他のリソースと違ってかなり細かくリソースが分かれているため注意が必要です。
resource "aws_api_gateway_rest_api" "example" { name = "${var.project}-api-gateway" } resource "aws_api_gateway_resource" "example" { parent_id = aws_api_gateway_rest_api.example.root_resource_id path_part = "translate" rest_api_id = aws_api_gateway_rest_api.example.id } resource "aws_api_gateway_method" "example" { authorization = "NONE" http_method = "GET" resource_id = aws_api_gateway_resource.example.id rest_api_id = aws_api_gateway_rest_api.example.id request_parameters = { "method.request.querystring.input_text" = true } } resource "aws_api_gateway_integration" "example" { http_method = aws_api_gateway_method.example.http_method resource_id = aws_api_gateway_resource.example.id rest_api_id = aws_api_gateway_rest_api.example.id type = "AWS_PROXY" integration_http_method = "POST" uri = aws_lambda_function.test_lambda.invoke_arn } resource "aws_api_gateway_method_response" "response_200" { rest_api_id = aws_api_gateway_rest_api.example.id resource_id = aws_api_gateway_resource.example.id http_method = aws_api_gateway_method.example.http_method status_code = "200" response_models = { "application/json" = "Empty" } } resource "aws_api_gateway_deployment" "example" { rest_api_id = aws_api_gateway_rest_api.example.id triggers = { redeployment = sha1(jsonencode([ aws_api_gateway_resource.example.id, aws_api_gateway_method.example.id, aws_api_gateway_integration.example.id, ])) } lifecycle { create_before_destroy = true } } resource "aws_api_gateway_stage" "example" { deployment_id = aws_api_gateway_deployment.example.id rest_api_id = aws_api_gateway_rest_api.example.id stage_name = "dev" } data "aws_iam_policy_document" "apigateway" { statement { actions = ["sts:AssumeRole"] effect = "Allow" principals { type = "Service" identifiers = ["apigateway.amazonaws.com"] } } } resource "aws_iam_role" "apigateway_role" { name = "${var.project}-apigateway_role" assume_role_policy = data.aws_iam_policy_document.apigateway.json } resource "aws_iam_role_policy_attachment" "apigateway_policy" { role = aws_iam_role.apigateway_role.name policy_arn = "arn:aws:iam::aws:policy/TranslateFullAccess" } data "aws_region" "current" {} data "aws_caller_identity" "current" {} resource "aws_lambda_permission" "allow_apigateway" { statement_id = "AllowExecutionFromAPIGateway" action = "lambda:InvokeFunction" function_name = aws_lambda_function.test_lambda.function_name principal = "apigateway.amazonaws.com" source_arn = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.example.id}/*/${aws_api_gateway_method.example.http_method}${aws_api_gateway_resource.example.path}" }
Point 1: Lambda との紐づけ aws_api_gateway_integration
API Gateway と Lambda はaws_api_gateway_integration
で紐づけをします。
resource "aws_api_gateway_integration" "example" { http_method = aws_api_gateway_method.example.http_method resource_id = aws_api_gateway_resource.example.id rest_api_id = aws_api_gateway_rest_api.example.id type = "AWS_PROXY" integration_http_method = "POST" uri = aws_lambda_function.test_lambda.invoke_arn }
integration_http_method
注意:ややこしいですが、Lambda との通信を行う際のメソッドタイプです。
uri
注意:Lambda の ARN について属性が違うので気をつけてください。
arn – Lambda Function を識別する Amazon Resource Name (ARN) です。
invoke_arn – Lambda Function を API Gateway から呼び出す際に使用する ARN – aws_api_gateway_integration の uri で使用します。
aws_lambda_function | Resources | hashicorp/aws | Terraform Registry
aws_lambda_function.test_lambda.arn
と定義すると怒られます。
実行結果
API Gateway のエンドポイントに向けて cURL を実行すれば翻訳されます。
エンドポイントは、ステージから dev を選択したら画面上部にあります。
エンドポイントに、翻訳する文章を追加してリクエストを投げます。
PS C:\Users> curl https://8zrom9un9d.execute-api.us-east-1.amazonaws.com/dev/translate?input_text=おはようございます StatusCode : 200 StatusDescription : OK Content : {"output_text": "Good morning"} RawContent : HTTP/1.1 200 OK Connection: keep-alive x-amzn-RequestId: bd91ebd1-6235-4254-8695-2b002fbe7ea1 x-amz-apigw-id: TowmOH7GoAMF50A= X-Amzn-Trace-Id: Root=1-62a6948e-7681872b231eec422df9017b;Sampled=0 ... Forms : {} Headers : {[Connection, keep-alive], [x-amzn-RequestId, bd91ebd1-6235-4254-869 5-2b002fbe7ea1], [x-amz-apigw-id, TowmOH7GoAMF50A=], [X-Amzn-Trace-I d, Root=1-62a6948e-7681872b231eec422df9017b;Sampled=0]...} Images : {} InputFields : {} Links : {} ParsedHtml : mshtml.HTMLDocumentClass RawContentLength : 31
さらに、AWS CLI を使ってデータが格納されたか DynamoDB のデータを確認します。
PS C:\Users> aws dynamodb scan --table-name translate-history { "Items": [ { "input_text": { "S": "おはようございます" }, "output_text": { "S": "Good morning" }, "timestamp": { "S": "20220613013615" } } ], "Count": 1, "ScannedCount": 1, "ConsumedCapacity": null }
おかたづけ
使ったリソースをdestroy
しておきます。
Destroy complete! Resources: 17 destroyed.
まとめ
サーバレスとしては今回の内容は簡潔でわかりやすかったのではないでしょうか。
ですが、Terraform として実装する場合には API Gateway がかなり複雑でした。
全体 Terraform コード
main.tf
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 4.0" } } } provider "aws" { region = "us-east-1" profile = "default" default_tags { tags = { "Name" = var.tag_name_test } } } # Lambda data "archive_file" "sample_function" { type = "zip" source_file = "${path.module}${var.lambda_file_name}" output_path = "${path.module}${var.lambda_file_zip_name}" } data "aws_iam_policy_document" "AWSLambdaTrustPolicy" { statement { actions = ["sts:AssumeRole"] effect = "Allow" principals { type = "Service" identifiers = ["lambda.amazonaws.com"] } } } resource "aws_iam_role" "function_role" { name = "${var.project}-function_role" assume_role_policy = data.aws_iam_policy_document.AWSLambdaTrustPolicy.json } resource "aws_iam_role_policy_attachment" "lambda_policy" { role = aws_iam_role.function_role.name policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" } resource "aws_iam_role_policy_attachment" "Translate_policy" { role = aws_iam_role.function_role.name policy_arn = "arn:aws:iam::aws:policy/TranslateFullAccess" } resource "aws_lambda_function" "test_lambda" { filename = data.archive_file.sample_function.output_path function_name = "${var.project}-lambda-function" role = aws_iam_role.function_role.arn handler = var.handler_name publish = true source_code_hash = data.archive_file.sample_function.output_base64sha256 runtime = "python3.9" memory_size = 256 timeout = 10 } resource "aws_lambda_permission" "allow_cloudwatch" { statement_id = "AllowExecutionFromCloudWatch" action = "lambda:InvokeFunction" function_name = aws_lambda_function.test_lambda.function_name principal = "events.amazonaws.com" } # API Gateway resource "aws_api_gateway_rest_api" "example" { name = "${var.project}-api-gateway" } resource "aws_api_gateway_resource" "example" { parent_id = aws_api_gateway_rest_api.example.root_resource_id path_part = "translate" rest_api_id = aws_api_gateway_rest_api.example.id } resource "aws_api_gateway_method" "example" { authorization = "NONE" http_method = "GET" resource_id = aws_api_gateway_resource.example.id rest_api_id = aws_api_gateway_rest_api.example.id request_parameters = { "method.request.querystring.input_text" = true } } resource "aws_api_gateway_integration" "example" { http_method = aws_api_gateway_method.example.http_method resource_id = aws_api_gateway_resource.example.id rest_api_id = aws_api_gateway_rest_api.example.id type = "AWS_PROXY" integration_http_method = "POST" uri = aws_lambda_function.test_lambda.invoke_arn } resource "aws_api_gateway_method_response" "response_200" { rest_api_id = aws_api_gateway_rest_api.example.id resource_id = aws_api_gateway_resource.example.id http_method = aws_api_gateway_method.example.http_method status_code = "200" response_models = { "application/json" = "Empty" } } resource "aws_api_gateway_deployment" "example" { rest_api_id = aws_api_gateway_rest_api.example.id triggers = { redeployment = sha1(jsonencode([ aws_api_gateway_resource.example.id, aws_api_gateway_method.example.id, aws_api_gateway_integration.example.id, ])) } lifecycle { create_before_destroy = true } } resource "aws_api_gateway_stage" "example" { deployment_id = aws_api_gateway_deployment.example.id rest_api_id = aws_api_gateway_rest_api.example.id stage_name = "dev" } data "aws_iam_policy_document" "apigateway" { statement { actions = ["sts:AssumeRole"] effect = "Allow" principals { type = "Service" identifiers = ["apigateway.amazonaws.com"] } } } resource "aws_iam_role" "apigateway_role" { name = "${var.project}-apigateway_role" assume_role_policy = data.aws_iam_policy_document.apigateway.json } resource "aws_iam_role_policy_attachment" "apigateway_policy" { role = aws_iam_role.apigateway_role.name policy_arn = "arn:aws:iam::aws:policy/TranslateFullAccess" } data "aws_region" "current" {} data "aws_caller_identity" "current" {} resource "aws_lambda_permission" "allow_apigateway" { statement_id = "AllowExecutionFromAPIGateway" action = "lambda:InvokeFunction" function_name = aws_lambda_function.test_lambda.function_name principal = "apigateway.amazonaws.com" source_arn = "arn:aws:execute-api:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:${aws_api_gateway_rest_api.example.id}/*/${aws_api_gateway_method.example.http_method}${aws_api_gateway_resource.example.path}" } # DynamoDB resource "aws_dynamodb_table" "basic-dynamodb-table" { name = "translate-history" billing_mode = "PROVISIONED" read_capacity = 1 write_capacity = 1 hash_key = "timestamp" attribute { name = "timestamp" type = "S" } } resource "aws_iam_role_policy_attachment" "dynamodb_policy" { role = aws_iam_role.function_role.name policy_arn = "arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess" }
variables.tf
各値は各自でご自由に置き換えてください。
variable "tag_name_test" { type = string default = "cold-airflow-handson-serverless" description = "Resource Tag Name" } variable "project" { type = string default = "handson-serverless" description = "project name" } variable "lambda_file_name" { default = "/contents/src/lambda_function.py" } variable "lambda_file_zip_name" { default = "/contents/zip/lambda.zip" } variable "test_name" { default = "handson-serverless" type = string description = "instance name" } variable "handler_name" { type = string description = "lambda function name" default = "lambda_function.lambda_handler" }
Lambda の Python コード
import json import boto3 import datetime def lambda_handler(event, context): translate = boto3.client('translate') dynamodb_translate_histroy_tbl = boto3.resource( 'dynamodb').Table("translate-history") input_text = event["queryStringParameters"]["input_text"] response = translate.translate_text( Text=input_text, SourceLanguageCode='ja', TargetLanguageCode='en' ) output_text = response.get('TranslatedText') dynamodb_translate_histroy_tbl.put_item( Item={ 'timestamp': datetime.datetime.now().strftime("%Y%m%d%H%M%S"), 'input_text': input_text, 'output_text': output_text } ) return { 'statusCode': 200, 'body': json.dumps({ 'output_text': output_text }), 'isBase64Encoded': False, 'headers': {} }
テックブログ新着情報のほか、AWSやGoogle Cloudに関するお役立ち情報を配信中!
Follow @twitter2021年新卒入社。インフラエンジニアです。RDBが三度の飯より好きです。 主にデータベースやAWSのサーバレスについて書く予定です。あと寒いのは苦手です。
Recommends
こちらもおすすめ
-
Terraform import ブロックの実活用
2023.12.16
Special Topics
注目記事はこちら
データ分析入門
これから始めるBigQuery基礎知識
2024.02.28
AWSの料金が 10 %割引になる!
『AWSの請求代行リセールサービス』
2024.07.16