この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
今回は基本は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から引用した例です。
Lambdaの定義
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を使うためのプロバイダーなどの情報が入ったファイルを用意しましょう。
main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 3.0"
}
}
}
provider "aws" {
region = "ap-northeast-1"
profile = "XXXXX"
}
以下のコマンドでTerraformのプロジェクトを始めましょう。
Terraformの準備
$ terraform init
2. SAMのセットアップ
SAMはプロジェクトを開始するにあたってインタラクティブに情報を入力することができます。 今回は以下のように設定しました。 成功するとプロジェクト名(sam-app)と同じディレクトリが作られます。
SAMの準備
$ 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を用いてそれを解決しています。
pipenvの準備
$ cd sam-app
$ pipenv --python 3.8
3. S3バケットの用意
SAMではS3に保存したファイルからLambda関数を作成するので、バケットが必要になります。
S3バケットの作成
$ aws s3api --profile XXXXX \
create-bucket \
--acl private \
--bucket XXXXX \
--create-bucket-configuration LocationConstraint=ap-northeast-1
4. Lambda関数の用意
今回はSAMで作られたテンプレートをほぼそのまま流用しましょう。
以下のファイルを次のように書き換えましょう。
環境変数からNAME
を読み込んでレスポンスとして返しています。
sam-app/hello_world/app.py
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のドメインを出力しています。
sam-app/template.yaml
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の方で利用可能となります。
cloudformation.tf
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
が利用できます。
cloudfront.tf
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を利用します。
Makefile
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での削除
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からタスクランナーを設定するよりは楽に開発ができると思います。