REST API を自動デプロイするための AWS CloudFormation テンプレートの記述テクニック

はじめに

こんにちは。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を叩きます。

今回の場合は、”https://lefldh5ct1.execute-api.ap-northeast-1.amazonaws.com/dev/utcnow”となります。

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

デプロイメントの修正

デプロイメントの記述部分を修正します。

  1. デプロイメントのリソースIDについて、”DeploymentVer1″から”DeploymentVer2″に修正してください。これにより、devステージへ自動で再デプロイされるようにします。
  2. “Description”についても、後々分かりやすいように”ver2″と修正しておきましょう。
  3. 最後に”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リクエスト

先ほどど同様のリクエストURLとなります。

 

JST APIリクエスト

今回の場合は、”https://lefldh5ct1.execute-api.ap-northeast-1.amazonaws.com/dev/utcnow”となります。

リソース部分が異なるだけですね、JSTでのAPIリクエストも成功しました!

あれ、日本時間の深夜1時にリクエストしていますね。。いえいえただの夜更かしです(笑)

注意点

自動デプロイ実施時は、ステージに関連付いているデプロイ履歴が1つしか保有できません。

つまり、以下のような状態になります。

ステージのデプロイ履歴から、以前のバージョン(ver1)に戻すことができないのでご注意ください。この事象は、デプロイメントを再作成しているのが起因です。もし以前のバージョンに戻す際は、以前のCFNテンプレートを利用しデプロイメントを再作成する流れとなります。

  • 自動デプロイを利用する際には、CFNテンプレートを別途Gitでバージョン管理・保管しておくこと

まとめ

いかがだったでしょうか。REST APIを自動デプロイするためのテクニックをご紹介しました。メリット・デメリットも感じましたが、要件に応じて導入も検討して頂ければと思います。そしてREST APIの自動デプロイがサポートされることを心待ちにしています。

本記事が皆様のお役にたてば幸いです。

ではサウナラ~🔥

タイトルとURLをコピーしました