CodePipelineにLambda関数を呼び出すアクションを定義して、Fargateタスクの希望数を変更する

CodePipelineにLambda関数を呼び出すアクションを定義して、Fargateタスクの希望数を変更する

CodePipelineにLambda関数を呼び出すアクションを定義して、Fargateタスクの希望数を変更する

CodePipelineにはさまざまなアクションを設定できますが、今回はパイプライン内で、Lambda関数を呼び出すことを考えます。

今回はパイプライン内でLambda関数を呼び出して、ECS(Fargate)サービスにおけるECSタスクの希望数を変更することを目指します。

構築する環境

Diagram of define action in CodePipeline calls Lambda function to change desired number of Fargate tasks.

CodePipelineを構成し、3つのリソースを連携させます。

1つ目はCodeCommitです。
CodeCommitはCodePipelineのソースステージを担当します。
Gitリポジトリとして使用します。

2つ目はCodeBuildです。
CodeBuildはCodePipelineのビルドステージを担当します。
CodeCommitにプッシュされたコードから、Dockerイメージをビルドします。
ビルドしたイメージをECRにプッシュします。

3つ目はLambda関数です。
この関数の働きは、ECS(Fargate)サービスのタスク希望数を変更することです。
具体的には希望数を0から1に変更します。

CodePipelineにデプロイステージを作成します。
後述のFargateにデプロイするように設定します。

SSMパラメータストアにDockerHubアカウント情報を保存します。
DockerBuildでイメージを生成する際に、DockerHubにサインインした上でベースイメージをプルするために、これらを使用します。

CodePipelineが開始されるきっかけですが、CodeCommitへのプッシュを条件とします。
具体的には、EventBridgeに上記を満たすルールを用意します。

プライベートサブネットに、FargateタイプのECSを作成します。

EC2インスタンスを作成します。
Fargate上に作成されたコンテナにアクセスするためのクライアントして使用します。

CloudFormationテンプレートファイル

上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。

https://github.com/awstut-an-r/awstut-fa/tree/main/079

テンプレートファイルのポイント解説

本ページはCodePipeline内でLambda関数を呼び出す方法を中心に取り上げます。

CodePipelineに関する基本的な事項については、以下のページをご確認ください。

あわせて読みたい
CodePipelineを使ってCodeCommitプッシュをトリガーにしてECRにイメージをプッシュする 【CodePipelineを使ってCodeCommitプッシュをトリガーにしてECRにイメージをプッシュする】 CodePipelineを使用することによって、CI/CD構成を構築することができます。...

CodePipelineにデプロイステージを作成し、ECS(Fargate)にデプロイする方法については、以下のページをご確認ください。

あわせて読みたい
CodePipelineでイメージをビルドしてFargateにデプロイする 【CodePipelineでイメージをビルドしてFargateにデプロイする】 以下のページで、CodePipelineを使ってパイプラインを構成して、CodeCommitとCodeBuildを連携させて、EC...

プライベートサブネットにFargateを構築する方法については、以下のページをご確認ください。

あわせて読みたい
プライベートサブネットにECS(Fargate)を作成する 【プライベートサブネットにECS(Fargate)を作成する】 以下のページでFargateタイプのECSコンテナを作成する方法をご紹介しました。 https://awstut.com/2022/01/25/int...

CloudFormationカスタムリソースを使用して、CloudFormationスタック削除時に、自動的にS3バケット内のオブジェクトと、ECRリポジトリ内のイメージを削除します。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CFNカスタムリソースでS3オブジェクトを作成・削除する 【CloudFormationカスタムリソースを使って、スタック生成/削除時にS3オブジェクトを作成/削除する方法】 CloudFormationカスタムリソースはスタック操作(作成、更新、...
あわせて読みたい
CFNカスタムリソースでECRイメージを削除する 【CloudFormationカスタムリソースでECRイメージを削除する】 CloudFormationを使ってECRを作成し、そこにイメージをプッシュすると、CloudFormationスタック時にエラー...

テンプレートファイルのポイント解説

CodePipeline

Resources:
  Pipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties:
      ArtifactStore:
        Location: !Ref BucketName
        Type: S3
      Name: !Ref Prefix
      RoleArn: !GetAtt CodePipelineRole.Arn
      Stages:
        - Actions:
            - ActionTypeId:
                Category: Source
                Owner: AWS
                Provider: CodeCommit
                Version: 1
              Configuration:
                BranchName: !Ref BranchName
                OutputArtifactFormat: CODE_ZIP
                PollForSourceChanges: false
                RepositoryName: !GetAtt CodeCommitRepository.Name
              Name: SourceAction
              OutputArtifacts:
                - Name: !Ref PipelineSourceArtifact
              Region: !Ref AWS::Region
              RunOrder: 1
          Name: Source
        - Actions:
            - ActionTypeId:
                Category: Build
                Owner: AWS
                Provider: CodeBuild
                Version: 1
              Configuration:
                ProjectName: !Ref CodeBuildProject
              InputArtifacts:
                - Name: !Ref PipelineSourceArtifact
              Name: Build
              OutputArtifacts:
                - Name: !Ref PipelineBuildArtifact
              Region: !Ref AWS::Region
              RunOrder: 1
          Name: Build
        - Actions:
            - ActionTypeId:
                Category: Deploy
                Owner: AWS
                Provider: ECS
                Version: 1
              Configuration:
                ClusterName: !Ref ECSClusterName
                FileName: !Ref ImageDefinitionFileName
                ServiceName: !Ref ECSServiceName
              InputArtifacts:
                - Name: !Ref PipelineBuildArtifact
              Name: Deploy
              Region: !Ref AWS::Region
              RunOrder: 1
          Name: Deploy
        - Actions:
            - ActionTypeId:
                Category: Invoke
                Owner: AWS
                Provider: Lambda
                Version: 1
              Configuration:
                FunctionName: !Ref ECSFunctionName
              InputArtifacts: []
              Name: Invoke
              OutputArtifacts: []
              Region: !Ref AWS::Region
              RunOrder: 1
          Name: Invoke
Code language: YAML (yaml)

StagesプロパティにLambda関数の呼び出しステージを定義します。
Configurationプロパティ内で、呼び出す関数を設定します。具体的には、FunctionNameプロパティで呼び出す関数名を指定します。
InputArtifactsおよびOutputArtifactsプロパティには、有効なアーティファクを設定しません。今回呼び出す関数の働きはECSタスクの希望数を調整することですので、アーティファクトの読み書きは発生しないためです。

以下がCodePipeline用のIAMロールです。

Resources:
  CodePipelineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - codepipeline.amazonaws.com
            Action:
              - sts:AssumeRole
      Policies:
        - PolicyName: PipelinePolicy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - lambda:invokeFunction
                Resource:
                  - !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${ECSFunctionName}"
              - Effect: Allow
                Action:
                  - codecommit:CancelUploadArchive
                  - codecommit:GetBranch
                  - codecommit:GetCommit
                  - codecommit:GetRepository
                  - codecommit:GetUploadArchiveStatus
                  - codecommit:UploadArchive
                Resource:
                  - !GetAtt CodeCommitRepository.Arn
              - Effect: Allow
                Action:
                  - codebuild:BatchGetBuilds
                  - codebuild:StartBuild
                  - codebuild:BatchGetBuildBatches
                  - codebuild:StartBuildBatch
                Resource:
                  - !GetAtt CodeBuildProject.Arn
              - Effect: Allow
                Action:
                  - s3:PutObject
                  - s3:GetObject
                  - s3:GetObjectVersion
                  - s3:GetBucketAcl
                  - s3:GetBucketLocation
                Resource:
                  - !Sub "arn:aws:s3:::${BucketName}"
                  - !Sub "arn:aws:s3:::${BucketName}/*"
              - Effect: Allow
                Action:
                  - ecs:*
                Resource: "*"
              - Effect: Allow
                Action:
                  - iam:PassRole
                Resource: "*"
                Condition:
                  StringLike:
                    iam:PassedToService:
                      - ecs-tasks.amazonaws.com
Code language: YAML (yaml)

Lambda関数を呼び出すための権限を設定します。

Lambda関数

Resources:
  ECSFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import boto3
          import os

          cluster_name = os.environ['CLUSTER_NAME']
          count = int(os.environ['COUNT'])
          service_name = os.environ['SERVICE_NAME']

          ecs_client = boto3.client('ecs')
          codepipeline_client = boto3.client('codepipeline')

          def lambda_handler(event, context):
            job_id = event['CodePipeline.job']['id']

            try:
              describe_services_response = ecs_client.describe_services(
                cluster=cluster_name,
                services=[
                  service_name
                ]
              )
              print(describe_services_response)

              if describe_services_response['services'][0]['desiredCount'] > 0:
                codepipeline_client.put_job_success_result(
                  jobId=job_id
                )
                return

              update_service_response = ecs_client.update_service(
                cluster=cluster_name,
                service=service_name,
                desiredCount=count
              )
              print(update_service_response)

              codepipeline_client.put_job_success_result(
                jobId=job_id
              )

            except Exception as e:
              print(e)
              codepipeline_client.put_job_failure_result(
                jobId=job_id,
                failureDetails={
                  'type': 'JobFailed',
                  'message': 'Something happened.'
                }
              )
      Environment:
        Variables:
          CLUSTER_NAME: !Ref ECSClusterName
          COUNT: 1
          SERVICE_NAME: !Ref ECSServiceName
      FunctionName: !Sub "${Prefix}-function-ecs"
      Handler: !Ref Handler
      Runtime: !Ref Runtime
      Role: !GetAtt ECSFunctionRole.Arn
Code language: YAML (yaml)

Lambda関数で実行するコードをインライン表記で定義します。
詳細につきましては、以下のページをご確認ください。

あわせて読みたい
CloudFormationでLambdaを作成する3パータン(S3/インライン/コンテナ) 【CloudFormationでLambdaを作成する】 CloudFormationでLambdaを作成する場合、大別すると以下の3パターンあります。 S3バケットにコードをアップロードする インライ...

Environmentプロパティで関数に渡すことができる環境変数を定義できます。
具体的には、ECSタスク数を調整するECSクラスターとサービス、そして調整後の値です。

コードの内容は以下の通りです。

  • describe_servicesメソッドを使って、ECSサービスの状況を取得し、希望数を確認する。
  • タスク数を確認し、希望数が0の場合は処理を継続する。
  • update_serviceメソッドに使って、ECSサービスの希望数を変更する。
  • CodePipeline用のAPIをコールする。

最後のAPIがポイントです。
AWS公式ページでは、以下の通り言及されています。

Lambda 関数の実装の一部として、PutJobSuccessResult API または PutJobFailureResult API への呼び出しが必要です。それ以外の場合、このアクションの実行は、アクションがタイムアウトするまでハングします。

AWS Lambda

つまり希望数を変更後に、上記の2つのAPIの内、どちらかを実行する必要があるということです。
今回は正常に希望数を変更できた場合はPutJobSuccessResult API(put_job_success_resultメソッド)を、処理中に何らかにエラーが発生した場合はPutJobFailureResult API(put_job_failure_result)を実行するように設定します。

(参照)アプリコンテナ

Dockerfile

FROM amazonlinux

RUN yum update -y && yum install python3 python3-pip -y

RUN pip3 install bottle

COPY main.py ./

CMD ["python3", "main.py"]

EXPOSE 8080
Code language: Dockerfile (dockerfile)

アプリコンテナ用のイメージは、Amazon Linux 2をベースとして作成します。

Python製WebフレームワークのBottleを使用します。
ですからPythonおよびpipをインストール後、これをインストールします。

アプリロジックを記載したPythonスクリプト(main.py)をコピーし、これを実行するように設定します。

先述の通り、アプリは8080/tcpでHTTPリクエストを待ち受けますので、このポートを公開します。

main.py

from bottle import route, run

@route('/')
def hello():
  return 'Hello CodePipeline.'


if __name__ == '__main__':
  run(host='0.0.0.0', port=8080)
Code language: Python (python)

Bottleを使用して、簡易的なWebサーバを構築します。
8080/tcpでHTTPリクエストを待ち受け、「Hello CodePipeline.」を返すという単純な構成です。

環境構築

CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。

CloudFormationスタックを作成し、スタック内のリソースを確認する

CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

あわせて読みたい
CloudFormationのネストされたスタックで環境を構築する 【CloudFormationのネストされたスタックで環境を構築する方法】 CloudFormationにおけるネストされたスタックを検証します。 CloudFormationでは、スタックをネストす...

各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。

  • ECR:fa-079
  • CodeCommit:fa-079
  • CodeBuild:fa-079
  • CodePipeline:fa-079
  • Lambda関数:fa-079-function-ecs

作成されたリソースをAWS Management Consoleから確認します。
ECS(Fargate)のクラスター・サービスを確認します。

Detail of ECS 1.
Detail of ECS 2.

正常にクラスター・サービスが作成されています。
ポイントは希望数が0となっている点です。
つまりFargateの初期構築時は、1つもタスクが起動しないという点です。

CodePipelineを確認します。

Detail of CodePipeline 1.

パイプラインの実行に失敗しています。
これはCloudFormationスタック作成時に、CodeCommitが作成されたことがきっかけとして、パイプラインが実行されたためです。
現時点ではCodeCommitにコードをプッシュしていないため、パイプライン実行の過程でエラーが発生しました。

作成されているステージに注目します。
Invokeという名前で、Lambda関数を呼び出すステージが、パイプラインの一番最後に用意されています。
つまりCodeCommitによってプッシュされたコードから、Dockerイメージをビルドして、Fargateにイメージをデプロイした後に、Fargateタスクの希望数を変更するという流れになります。

動作確認

準備が整いましたので、CodeCommitにコードをプッシュします。

まずCodeCommitをプルします。

$ git clone https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/fa-079
Cloning into 'fa-079'...
warning: You appear to have cloned an empty repository.
Code language: Bash (bash)

空のリポジトリがプルされました。

リポジトリにDockerfileおよびmain.pyを加えます。

$ ls -al
total 8
drwxrwxr-x 3 ec2-user ec2-user  51 Aug 20 08:31 .
drwxrwxr-x 3 ec2-user ec2-user  20 Aug 20 08:31 ..
-rw-rw-r-- 1 ec2-user ec2-user 187 Aug 12 11:01 Dockerfile
drwxrwxr-x 7 ec2-user ec2-user 119 Aug 20 08:31 .git
-rw-rw-r-- 1 ec2-user ec2-user 681 Aug 20 02:57 main.py
Code language: Bash (bash)

2ファイルをCodeCommitにプッシュします。

$ git add .

$ git commit -m "first commit"
[master (root-commit) 7e41437] first commit
...
 2 files changed, 39 insertions(+)
 create mode 100644 Dockerfile
 create mode 100644 main.py

$ git push
...
 * [new branch]      master -> master
Code language: Bash (bash)

正常にプッシュできました。

しばらく待機した後、改めてCodePipelineを確認します。

Detail of CodePipeline 2.

パイプラインが正常に完了しました。

Fargateを確認します。

Detail of ECS 3.
Detail of ECS 4.

希望数が1になりました。
Lambda関数が実行されて希望数が変更になったということです。
希望数が1となったため、ECSタスクが自動生成されました。
このタスクの詳細を見ると、割り当てられたプライベートアドレスが「10.0.2.234」であることがわかります。

コンテナにHTTPリクエストを行うために、EC2インスタンスにアクセスします。
インスタンスへのアクセスはSSM Session Managerを使用します。

% aws ssm start-session --target i-0c41c5926230b480c

Starting session with SessionId: root-0c76e08548a26fec6
sh-4.2$
Code language: Bash (bash)

SSM Session Managerの詳細につきましては、以下のページをご確認ください。

あわせて読みたい
LinuxインスタンスにSSM Session Manager経由でアクセスする 【LinuxインスタンスにSSM Session Manager経由でアクセスする】 EC2インスタンスにSSM Session Manager経由でアクセスする構成を確認します。 Session Manager は完全...

curlコマンドを使って、タスク内のコンテナにアクセスします。

sh-4.2$ curl http://10.0.2.224:8080/
Hello CodePipeline.
Code language: Bash (bash)

コンテナから応答がありました。
確かにBottleアプリで設定した文字列です。
このことから、CodePipeline内でLambda関数を呼び出すことによって、ECSタスク数を変更できることがわかりました。

まとめ

パイプライン内でLambda関数を呼び出して、ECS(Fargate)サービスにおけるECSタスクの希望数を変更することができました。