はじめに
こんにちは。SCSKのふくちーぬです。
今回は、Amazon API Gateway REST API を自動デプロイするためのちょっとした工夫についてお話しします。
REST APIの場合、2回目のAPIのデプロイ以降、APIの内容に変更があった際に手動で再デプロイする必要があります。
API を更新するたびに、API を既存のステージまたは新しいステージに再デプロイする必要があります。API の更新には、ルート、メソッド、統合、オーソライザー、ステージ設定以外の変更が含まれます。
弊社広野の記事にも、その旨が説明されています。
Amazon API Gateway は設定を変更した後に必ずデプロイをする必要があります。そうしないと変更が反映されません。
HTTP API であれば自動デプロイの設定があり、設定変更後に自動でデプロイしてくれます。特別な事情がなければ、安全のために自動デプロイを活用した方が良いと思います。
REST API には自動デプロイの設定がありません。無理やり自動デプロイさせる仕組みも作り込みできなくはないのですが大変です。ここは今後の改善が待たれるところです。
では、この忘れがちな手動での再デプロイの手間をなくすためのを方法を紹介します。
構成とシナリオの確認
構成図
今回作成する構成は、以下の通りです。
一般的な Amazon API Gateway + AWS Lambda の構成ですね。APIに対して、2つのGETメソッドが存在します。
1つ目のGETメソッドを叩くと、UTC(イギリス時間)にて現在時刻が返却されます。
2つ目のGETメソッドを叩くと、JST(日本時間)にて現在時刻が返却されます。
シナリオ
シナリオは、以下の通りです。
UTCを取得できるAPIを開発するよう依頼されました。しかし仕様変更が発生してJSTでも取得できるよう依頼され、即座にAPIの更新・デプロイをする必要がでてきました。
これを AWS CloudFormation (以降、CFN) テンプレートで表現していきます。
①まずは、UTCを取得するためのメソッドを備えたAPIを作成します。
②UTCが取得できることを確認します。
③CFNテンプレートを加筆修正して、JSTを取得するためのメソッドも備えたAPIへ自動デプロイ(更新)します。
④UTC・JSTが取得できることを確認します。
環境準備編(UTC APIの構築)
REST APIやLambdaを準備するために、以下のCFNテンプレートにて構築します。
CloudFormationコンソールやCLIにてデプロイをします。
AWSTemplateFormatVersion: '2010-09-09' Parameters: ResourceName: Type: String APIStage: Type: String Resources: # ------------------------------------------------------------# # IAM Role # ------------------------------------------------------------# LambdaRole: Type: AWS::IAM::Role Properties: RoleName: !Sub ${ResourceName}-lambda-role AssumeRolePolicyDocument: Version: '2012-10-17' Statement: - Effect: Allow Action: sts:AssumeRole Principal: Service: - lambda.amazonaws.com ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole # ------------------------------------------------------------# # APIGateway UtcnowLambda # ------------------------------------------------------------# UtcnowFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Sub ${ResourceName}-lambda-function-utcnow Role: !GetAtt LambdaRole.Arn Runtime: python3.11 Handler: index.lambda_handler Code: ZipFile: !Sub | import json from datetime import datetime, timedelta def lambda_handler(event, context): utc_now = datetime.utcnow() format_time = utc_now.strftime("%Y-%m-%d %H:%M:%S") print(utc_now) return { 'statusCode': 200, 'body': json.dumps(format_time) } RestApi: Type: AWS::ApiGateway::RestApi Properties: Name: !Sub ${ResourceName}-apigateway UtcnowResource: Type: AWS::ApiGateway::Resource Properties: RestApiId: !Ref RestApi ParentId: !GetAtt RestApi.RootResourceId PathPart: utcnow UtcnowLambdaPermission: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref UtcnowFunction Action: lambda:InvokeFunction Principal: apigateway.amazonaws.com DependsOn: UtcnowResource UtcnowMethod: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref RestApi ResourceId: !Ref UtcnowResource AuthorizationType: NONE HttpMethod: GET Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${ResourceName}-lambda-function-utcnow/invocations DependsOn: UtcnowLambdaPermission DeploymentVer1: ##(必須)デプロイメントIDを変更するため、バージョンが分かるよう記載しておく。 Type: AWS::ApiGateway::Deployment Properties: RestApiId: !Ref RestApi Description: ver1 ##(任意)Descriptionを変更するため、バージョンが分かるよう記載しておく。 DependsOn: - UtcnowMethod #- JstnowMethod Stage: Type: AWS::ApiGateway::Stage Properties: StageName: !Ref APIStage Description: dev stage RestApiId: !Ref RestApi DeploymentId: !Ref DeploymentVer1 ##(必須)デプロイメントIDを変更するため、バージョンが分かるよう記載しておく。 #------------------------------------------------------------# # APIGateway JstnowLambda #------------------------------------------------------------# # JstnowFunction: # Type: AWS::Lambda::Function # Properties: # FunctionName: !Sub ${ResourceName}-lambda-function-jstnow # Role: !GetAtt LambdaRole.Arn # Runtime: python3.11 # Handler: index.lambda_handler # Code: # ZipFile: !Sub | # import json # from datetime import datetime, timedelta # def lambda_handler(event, context): # utc_now = datetime.utcnow() # jst_now = utc_now + timedelta(hours=9) # format_time = jst_now.strftime("%Y-%m-%d %H:%M:%S") # print(jst_now) # return { # 'statusCode': 200, # 'body': json.dumps(format_time) # } # JstnowResource: # Type: AWS::ApiGateway::Resource # Properties: # RestApiId: !Ref RestApi # ParentId: !GetAtt RestApi.RootResourceId # PathPart: jstnow # DependsOn: # - RestApi # JstnowLambdaPermission: # Type: AWS::Lambda::Permission # Properties: # FunctionName: !Ref JstnowFunction # Action: lambda:InvokeFunction # Principal: apigateway.amazonaws.com # DependsOn: JstnowResource # JstnowMethod: # Type: AWS::ApiGateway::Method # Properties: # RestApiId: !Ref RestApi # ResourceId: !Ref JstnowResource # AuthorizationType: NONE # HttpMethod: GET # Integration: # Type: AWS_PROXY # IntegrationHttpMethod: POST # Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${ResourceName}-lambda-function-jstnow/invocations # DependsOn: JstnowLambdaPermission
デプロイ後のURLを叩きます。
restapiidは、各々異なるため自身のものをご確認ください。
現在時刻のUTCが取得できましたね!
API更新編(JST APIの追加)
上記テンプレートを修正して、現在時刻がJSTでも取得でき、自動デプロイされるようにします。
CFNテンプレートの記述テクニック
- Deploymentの論理IDを強制的に変更することで、デプロイメントが再作成されるようにする。
これだけで、自動デプロイが可能になります。加筆・修正箇所をそれぞれ見ていきましょう。
JST APIの追加
1番簡単な箇所から説明します。UTC APIの時と同様に、JSTにて現在時刻が取得できるようリソースを追加します。
コメントアウトを外すだけです。
#------------------------------------------------------------# # APIGateway JstnowLambda #------------------------------------------------------------# JstnowFunction: Type: AWS::Lambda::Function Properties: FunctionName: !Sub ${ResourceName}-lambda-function-jstnow Role: !GetAtt LambdaRole.Arn Runtime: python3.11 Handler: index.lambda_handler Code: ZipFile: !Sub | import json from datetime import datetime, timedelta def lambda_handler(event, context): utc_now = datetime.utcnow() jst_now = utc_now + timedelta(hours=9) format_time = jst_now.strftime("%Y-%m-%d %H:%M:%S") print(jst_now) return { 'statusCode': 200, 'body': json.dumps(format_time) } JstnowResource: Type: AWS::ApiGateway::Resource Properties: RestApiId: !Ref RestApi ParentId: !GetAtt RestApi.RootResourceId PathPart: jstnow DependsOn: - RestApi JstnowLambdaPermission: Type: AWS::Lambda::Permission Properties: FunctionName: !Ref JstnowFunction Action: lambda:InvokeFunction Principal: apigateway.amazonaws.com DependsOn: JstnowResource JstnowMethod: Type: AWS::ApiGateway::Method Properties: RestApiId: !Ref RestApi ResourceId: !Ref JstnowResource AuthorizationType: NONE HttpMethod: GET Integration: Type: AWS_PROXY IntegrationHttpMethod: POST Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${ResourceName}-lambda-function-jstnow/invocations DependsOn: JstnowLambdaPermission
デプロイメントの修正
デプロイメントの記述部分を修正します。
- デプロイメントのリソースIDについて、”DeploymentVer1″から”DeploymentVer2″に修正してください。これにより、devステージへ自動で再デプロイされるようにします。
- “Description”についても、後々分かりやすいように”ver2″と修正しておきましょう。
- 最後に”DependsOnについて、”JstnowMethod”を追加しましょう。JST APIのリソース・メソッドが作成された後に、デプロイされるようコントロールしています。
DeploymentVer2: ##(必須)DeploymentVer2に変更。
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId: !Ref RestApi
Description: ver2 ##(任意)ver2に変更。
DependsOn:
- UtcnowMethod
- JstnowMethod
完成したテンプレート
完成した加筆修正後のテンプレートは、以下の通りです。
AWSTemplateFormatVersion: '2010-09-09'
Parameters:
ResourceName:
Type: String
APIStage:
Type: String
Resources:
# ------------------------------------------------------------#
# IAM Role
# ------------------------------------------------------------#
LambdaRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${ResourceName}-lambda-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# ------------------------------------------------------------#
# APIGateway UtcnowLambda
# ------------------------------------------------------------#
UtcnowFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub ${ResourceName}-lambda-function-utcnow
Role: !GetAtt LambdaRole.Arn
Runtime: python3.11
Handler: index.lambda_handler
Code:
ZipFile: !Sub |
import json
from datetime import datetime, timedelta
def lambda_handler(event, context):
utc_now = datetime.utcnow()
format_time = utc_now.strftime("%Y-%m-%d %H:%M:%S")
print(utc_now)
return {
'statusCode': 200,
'body': json.dumps(format_time)
}
RestApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: !Sub ${ResourceName}-apigateway
UtcnowResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref RestApi
ParentId: !GetAtt RestApi.RootResourceId
PathPart: utcnow
UtcnowLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref UtcnowFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
DependsOn: UtcnowResource
UtcnowMethod:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref RestApi
ResourceId: !Ref UtcnowResource
AuthorizationType: NONE
HttpMethod: GET
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${ResourceName}-lambda-function-utcnow/invocations
DependsOn: UtcnowLambdaPermission
DeploymentVer2: ##(必須)DeploymentVer2に変更。
Type: AWS::ApiGateway::Deployment
Properties:
RestApiId: !Ref RestApi
Description: ver2 ##(任意)ver2に変更。
DependsOn:
- UtcnowMethod
- JstnowMethod
Stage:
Type: AWS::ApiGateway::Stage
Properties:
StageName: !Ref APIStage
Description: dev stage
RestApiId: !Ref RestApi
DeploymentId: !Ref DeploymentVer2 ##(必須)DeploymentVer2に変更
#------------------------------------------------------------#
# APIGateway JstnowLambda
#------------------------------------------------------------#
JstnowFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub ${ResourceName}-lambda-function-jstnow
Role: !GetAtt LambdaRole.Arn
Runtime: python3.11
Handler: index.lambda_handler
Code:
ZipFile: !Sub |
import json
from datetime import datetime, timedelta
def lambda_handler(event, context):
utc_now = datetime.utcnow()
jst_now = utc_now + timedelta(hours=9)
format_time = jst_now.strftime("%Y-%m-%d %H:%M:%S")
print(jst_now)
return {
'statusCode': 200,
'body': json.dumps(format_time)
}
JstnowResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref RestApi
ParentId: !GetAtt RestApi.RootResourceId
PathPart: jstnow
DependsOn:
- RestApi
JstnowLambdaPermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref JstnowFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
DependsOn: JstnowResource
JstnowMethod:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref RestApi
ResourceId: !Ref JstnowResource
AuthorizationType: NONE
HttpMethod: GET
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${ResourceName}-lambda-function-jstnow/invocations
DependsOn: JstnowLambdaPermission
スタックの更新
上記テンプレートを使用して、スタックの更新を行ってください。
スタックの更新が完了したら、リソースの作成順序を確認してみましょう。
JST Resource作成 ⇒ JST Lambda作成 ⇒ JST Method作成 ⇒ Deployment Ver2作成 ⇒ Dev Stage更新 ⇒ Deployment Ver1削除
つまりデプロイメントが再作成されていることを示しています。
マネジメントコンソールから手動で再デプロイせずに、自動デプロイが完了しました!
API呼び出し
では、早速APIリクエストをしてみましょう。
UTC APIリクエスト
JST APIリクエスト
今回の場合は、”https://lefldh5ct1.execute-api.ap-northeast-1.amazonaws.com/dev/utcnow”となります。
リソース部分が異なるだけですね、JSTでのAPIリクエストも成功しました!
あれ、日本時間の深夜1時にリクエストしていますね。。いえいえただの夜更かしです(笑)
注意点
- 自動デプロイを利用する際には、CFNテンプレートを別途Gitでバージョン管理・保管しておくこと
まとめ
いかがだったでしょうか。REST APIを自動デプロイするためのテクニックをご紹介しました。メリット・デメリットも感じましたが、要件に応じて導入も検討して頂ければと思います。そしてREST APIの自動デプロイがサポートされることを心待ちにしています。
本記事が皆様のお役にたてば幸いです。
ではサウナラ~🔥