SAM+TerraformでLambdaの管理を楽にする
今回は基本はTerraformでインフラを構築しつつも、部分的にはSAMを使用してLambda+API Gatewayをデプロイしたいと思います。
Lambdaのアーカイブ化やS3へのアップロードをSAMにやってもらうことで、Terraform側でのタスクを軽減することができます。
今回の記事の元ネタは以下のスライドです。 IaCについていろいろな知見が得られると思うのでおすすめです。
SAMとは
SAMはLambdaなどのサーバーレスアプリケーションの開発・デプロイを補助するツールでCloudFormationのような形式のファイルを用いてこれらを定義することができます。
Lambdaを開発・デプロイする場合について考えると、必要となる工程は煩雑です。開発ではローカルでの実行やランタイムの管理などをしたくなりますし、デプロイでは依存するパッケージの設置、Zipファイルへのアーカイブ化、アップロードなども自動化できるとうれしいです。
SAMではこれらの煩雑なタスクをある程度自動化してくれるので便利です。
詳しくは以下のリンクを読んでみてください。
TerraformでLambdaを管理したくない理由
TerraformのAWSプロバイダーではLambdaを定義する際にアプリケーションがまとめられたファイルが要求されます。 以下はTerraformのAWS Providerから引用した例です。
resource "aws_lambda_function" "test_lambda" { filename = "lambda_function_payload.zip" function_name = "lambda_function_name" role = aws_iam_role.iam_for_lambda.arn handler = "exports.test" # The filebase64sha256() function is available in Terraform 0.11.12 and later # For Terraform 0.11.11 and earlier, use the base64sha256() function and the file() function: # source_code_hash = "${base64sha256(file("lambda_function_payload.zip"))}" source_code_hash = filebase64sha256("lambda_function_payload.zip") runtime = "nodejs12.x" environment { variables = { foo = "bar" } } }
このlambda_function_payload.zip
を用意するためには先述のようなパッケージのインストール、アーカイブ化などの作業が伴います。変更の度に毎回こういった作業をやるのはコストが高いです。
タスクランナーなどを用いてある程度は自動化できますが、どうせならSAMをタスクランナーの一部として活用することで導入コストを減らしたいというのが今回の記事のモチベーションです。
やってみる
今回はSAMの機能でCloudFormationのテンプレート作成とS3へのファイルのアップロードまでをやってもらいます。 生成されたテンプレートをTerraformを用いてデプロイすることでTerraform⇄CloudFormation間の変数のやりとりもスムーズに行えます。
今回のプロジェクトの最終的なディレクトリ構造は次のような感じです(一部省略してます)。 基本的にはSAMで生成したプロジェクトがTerraformのプロジェクトに含まれている感じです。
├── Makefile ├── cloudformation.tf ├── cloudfront.tf ├── main.md ├── main.tf └── sam-app ├── Pipfile ├── __init__.py ├── hello_world │ ├── __init__.py │ ├── app.py │ └── requirements.txt ├── template.yaml └── tests
手順
今回は例としてCloudFront+API Gateway+Lambdaのシステムを構築してみましょう。
1. Terraformのセットアップ
以下のようなTerraformを使うためのプロバイダーなどの情報が入ったファイルを用意しましょう。
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 3.0" } } } provider "aws" { region = "ap-northeast-1" profile = "XXXXX" }
以下のコマンドでTerraformのプロジェクトを始めましょう。
$ terraform init
2. SAMのセットアップ
SAMはプロジェクトを開始するにあたってインタラクティブに情報を入力することができます。 今回は以下のように設定しました。 成功するとプロジェクト名(sam-app)と同じディレクトリが作られます。
$ sam init Which template source would you like to use? 1 - AWS Quick Start Templates 2 - Custom Template Location Choice: 1 What package type would you like to use? 1 - Zip (artifact is a zip uploaded to S3) 2 - Image (artifact is an image uploaded to an ECR image repository) Package type: 1 Which runtime would you like to use? 1 - nodejs14.x 2 - python3.8 3 - ruby2.7 4 - go1.x 5 - java11 6 - dotnetcore3.1 7 - nodejs12.x 8 - nodejs10.x 9 - python3.7 10 - python3.6 11 - python2.7 12 - ruby2.5 13 - java8.al2 14 - java8 15 - dotnetcore2.1 Runtime: 2 Project name [sam-app]: sam-app Cloning from https://github.com/aws/aws-sam-cli-app-templates AWS quick start application templates: 1 - Hello World Example 2 - EventBridge Hello World 3 - EventBridge App from scratch (100+ Event Schemas) 4 - Step Functions Sample App (Stock Trader) 5 - Elastic File System Sample App Template selection: 1 ----------------------- Generating application: ----------------------- Name: sam-app Runtime: python3.8 Dependency Manager: pip Application Template: hello-world Output Directory: . Next steps can be found in the README file at ./sam-app/README.md
今回は自分のPythonのバージョンとSAMの方で指定したバージョンが一致しなかったため、pipenvを用いてそれを解決しています。
$ cd sam-app $ pipenv --python 3.8
3. S3バケットの用意
SAMではS3に保存したファイルからLambda関数を作成するので、バケットが必要になります。
$ aws s3api --profile XXXXX \ create-bucket \ --acl private \ --bucket XXXXX \ --create-bucket-configuration LocationConstraint=ap-northeast-1
4. Lambda関数の用意
今回はSAMで作られたテンプレートをほぼそのまま流用しましょう。
以下のファイルを次のように書き換えましょう。
環境変数からNAME
を読み込んでレスポンスとして返しています。
import json from os import environ def lambda_handler(event, context): return { "statusCode": 200, "body": json.dumps({ "message": "hello world", "name": environ.get("NAME") }), }
一緒にSAMの方のテンプレートも書き換えましょう。
このテンプレートを元にCloudFormationのテンプレートが生成されます。
今回は環境変数としてNAME
をパラメータから参照しています。
また、アウトプットとしてAPIのドメインを出力しています。
AWSTemplateFormatVersion: '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: > sam-app Sample SAM Template for sam-app Globals: Function: Timeout: 3 Resources: HelloWorldFunction: Type: AWS::Serverless::Function Properties: CodeUri: hello_world/ Handler: app.lambda_handler Runtime: python3.8 Environment: Variables: NAME: !Ref Name Events: HelloWorld: Type: Api Properties: Path: /hello Method: get Parameters: Name: Type: String Outputs: HelloWorldApiDomain: Value: !Sub "${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com"
これでSAMの準備は完了です。
5. CloudFrontの用意
Terraformを利用してCloudfFrontをセットアップしましょう。
まず初めにSAMによって生成されるCloudFormationのスタックをTerraformから利用できるようにしましょう。
ここでのtemplate.yaml
はsam-app/template.yaml
ではなく、後に生成されるCloudFormationのテンプレートである点に注意が必要です。
ここでparameters
として設定したName
がCloudformationの方で利用可能となります。
resource "aws_cloudformation_stack" "sam" { name = "sam" template_body = file("template.yaml") capabilities = ["CAPABILITY_IAM", "CAPABILITY_AUTO_EXPAND"] parameters = { "Name" = "Terraform" } } data "aws_cloudformation_stack" "sam" { name = "sam" depends_on = [ aws_cloudformation_stack.sam ] }
CloudFrontの設定をしましょう。
今回はカスタムオリジンとしてAPI Gatewayを指定しています。
この際、domain_name
としてCloudFormationのoutputs
が利用できます。
resource "aws_cloudfront_distribution" "api_dist" { origin { domain_name = data.aws_cloudformation_stack.sam.outputs["HelloWorldApiDomain"] origin_id = "sam-api-gateway" custom_origin_config { https_port = 443 http_port = 80 origin_protocol_policy = "https-only" origin_ssl_protocols = ["TLSv1.2"] } } enabled = true default_cache_behavior { allowed_methods = [ "GET", "HEAD" ] cached_methods = ["GET", "HEAD"] target_origin_id = "sam-api-gateway" forwarded_values { query_string = false cookies { forward = "none" } } viewer_protocol_policy = "https-only" min_ttl = 0 default_ttl = 3600 max_ttl = 86400 } restrictions { geo_restriction { restriction_type = "whitelist" locations = [ "JP" ] } } viewer_certificate { cloudfront_default_certificate = true } }
6. タスクランナーを準備する
今回はタスクランナーとしてGNU Makeを利用します。
PROFILE=XXXXX SAM_APP_DIR=sam-app SAM_BUCKET=XXXXX TEMPLATE_FILE=template.yaml deploy: cd $(SAM_APP_DIR) && pipenv run sam build cd $(SAM_APP_DIR) && sam package --profile $(PROFILE) --s3-bucket $(SAM_BUCKET) --output-template-file ../$(TEMPLATE_FILE) terraform apply
デプロイまでの流れは次のような感じです。
sam build
で必要なファイルをまとめるsam package
でZipアーカイブ化、S3にアップロード、CloudFormationのテンプレートを生成- Terraformでデプロイ
CI/CDだったらSAM_BUCKET
を環境変数から取得したり、Terraformのデプロイ前の確認をスキップしたりする必要があるでしょう。
以下のコマンドでデプロイできます。
$ make deploy
実行するとプロジェクトのルートにtemplate.yaml
が生成されていると思います。
7. 確認する
実際に動いているか確認してみましょう。 CloudFrontに対してcurlでリクエストを送ります。
$ curl https://XXXXX.cloudfront.net/Prod/hello {"message": "hello world", "name": "Terraform"}
Terraformから渡した値が返ってきましたね。
8. 全てのリソースを削除する
デプロイしたリソースのほとんどは以下のコマンドで削除できます。
terraform destroy
最後にSAM用のS3バケットを削除すれば全て完了です。
感想
今回はSAM+Terraformという組み合わせでやりましたが、SAMはCloudFormationの拡張なのでそちら側でも必要なリソースの定義をすることができます。その場合Terraformを使わず、makeなどのタスクランナーは不要なのでもう少しシンプルになるでしょう。 また、TerraformだけでLambdaやAPI Gatewayの管理を行うという選択肢もあります。
今回のような組み合わせが必要になるのは以下のような局面だと思います。
- Terraformをどうしても使いたい
- すでに多くのリソースがTerraformで管理されている
- Terraformに習熟している
- CloudFormationだけで作るには複雑すぎる
- AWS以外のプロバイダーもつかう
- Lambda関数のコードが頻繁に変更される
- API Gatewayの設定項目が多い
SAMを用いてCloudFormationのテンプレートを作成するというのはいい手法だと思いました。 Terraform側との変数のやりとりも簡単なのも良い点です。 1からタスクランナーを設定するよりは楽に開発ができると思います。