AWS

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

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

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テンプレートを配置しています。

awstut-fa/079 at main · awstut-an-r/awstut-fa
Contribute to awstut-an-r/awstut-fa development by creating an account on GitHub.

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

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

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

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

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

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

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

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

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スタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。

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

  • 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の詳細につきましては、以下のページをご確認ください。

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タスクの希望数を変更することができました。

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