CodePipelineにLambda関数を呼び出すアクションを定義して、Fargateタスクの希望数を変更する
CodePipelineにはさまざまなアクションを設定できますが、今回はパイプライン内で、Lambda関数を呼び出すことを考えます。
今回はパイプライン内でLambda関数を呼び出して、ECS(Fargate)サービスにおけるECSタスクの希望数を変更することを目指します。
構築する環境
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にデプロイステージを作成し、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)のクラスター・サービスを確認します。
正常にクラスター・サービスが作成されています。
ポイントは希望数が0となっている点です。
つまりFargateの初期構築時は、1つもタスクが起動しないという点です。
CodePipelineを確認します。
パイプラインの実行に失敗しています。
これは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を確認します。
パイプラインが正常に完了しました。
Fargateを確認します。
希望数が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タスクの希望数を変更することができました。