CDK(Python)でCodePipelineからAWS Lambdaを呼び出す構成を作ってみた
設定ファイルPushでゴールデンイメージを自動作成する構成を考えてみたで紹介しました、以下の構成をCDKとAWS CLIを利用して構築してみたいと思います。
CDKまたはCFnで一撃で作成したかったのですが、現時点でEC2 Image BuilderがCDK、CFn未対応のため、EC2 Image BuilderはAWS CLIで、それ以外のリソースはCDKで作成してみたいと思います。
前提
- EC2 Image Builderのビルド環境(VPC等)が作成済みであること
- コンポーネント定義のアップロード先バケットが作成済みであること
- aws-cli 1.17.9
- cdk 1.22.0
EC2 Image Builder構築
リソース名定義
以降コマンドベースで操作するため、コマンドの再利用がしやすいよう、イメージパイプラインの名称等を環境変数に定義します。
BUCKET_NAME=test-imagebuilder-buildcomponent # コンポーネント定義のアップロード先バケット COMPONENT_NAME=ApacheConfig # コンポーネント名 RECIPE_NAME=TestRecipe # レシピ名 INFRA_CONFIG_NAME=TestInfraConfig # イメージパイプラインインフラ設定名 PIPELINE_NAME=TestImagePipeline # イメージパイプライン名 SORCE_IMAGE='arn:aws:imagebuilder:ap-northeast-1:aws:image/amazon-linux-2-x86/2020.1.8' # レシピに定義するソースイメージ #以下、イメージパイプラインのインフラ設定 INSTANCE_TYPES='t3.micro' INSTANCE_PROFILE_NAME='TestEC2ImageBulderRole' SECURITY_GROUP_ID='sg-0d8ac4fccc77c8f16' SUBNET_ID='subnet-0888e5f3d06784cf7' KEY_PAIR='test'
意図した通りに設定されているか確認しましょう。
cat << ETX BUCKET_NAME: ${BUCKET_NAME} COMPONENT_NAME: ${COMPONENT_NAME} RECIPE_NAME: ${RECIPE_NAME} INFRA_CONFIG_NAME: ${INFRA_CONFIG_NAME} PIPELINE_NAME: ${PIPELINE_NAME} SORCE_IMAGE: ${SORCE_IMAGE} INSTANCE_TYPES: ${INSTANCE_TYPES} INSTANCE_PROFILE_NAME: ${INSTANCE_PROFILE_NAMEs} SECURITY_GROUP_ID: ${SECURITY_GROUP_ID} SUBNET_ID: ${SUBNET_ID} KEY_PAIR: ${KEY_PAIR} ETX
コンポーネントドキュメント作成
以下の定義でcomponent.yaml
を作成します。ここではコンポーネントドキュメントはS3に格納したいと思います。
name: Apache Config description: This is Apache Config testing document. schemaVersion: 1.0 phases: - name: build steps: - name: InstallLinuxUpdate action: UpdateOS - name: InstallApache action: ExecuteBash inputs: commands: - sudo yum install httpd -y - sudo systemctl enable httpd - sudo systemctl start httpd - name: DownloadConfig action: S3Download inputs: - source: s3://test-pipeline-deploy/etc/httpd/conf/httpd.conf destination: /etc/httpd/conf/httpd.conf - name: StartApache action: ExecuteBash inputs: commands: - sudo systemctl enable httpd
作成したドキュメントをS3バケットにアップロードします。
aws s3 cp component.yaml s3://${BUCKET_NAME}/component.yaml
ビルドコンポーネント作成
アップロードしたドキュメント定義でビルドコンポーネントを作成します。
aws imagebuilder create-component \ --name ${COMPONENT_NAME} \ --semantic-version 1.0.0 \ --platform Linux \ --uri s3://${BUCKET_NAME}/component.yaml
後続のコマンドで、作成したコンポーネントのARNを利用したいのですが、上記コマンドの戻り値にARNが含まれていないため、コンポーネント名をキーにコンポーネントのARNを取得します。
COMPONENT_ARN=`aws imagebuilder list-components \ --filters "name=name,values=${COMPONENT_NAME}" \ --query "componentVersionList[0].arn"`
レシピ作成
作成したコンポーネントを利用するレシピを作成します。
IMAGE_RECIPE_ARN=`aws imagebuilder create-image-recipe \ --name ${RECIPE_NAME} \ --semantic-version 1.0.0 \ --components componentArn="${COMPONENT_ARN}" \ --parent-image "${SORCE_IMAGE}" \ --query "imageRecipeArn"`
オプション設定
IAMロールやサブネットなど、ビルドを行うインフラ環境を設定します。
INFRA_CONFIG_ARM=`aws imagebuilder create-infrastructure-configuration \ --name "${INFRA_CONFIG_NAME}" \ --instance-types "${INSTANCE_TYPES}" \ --instance-profile-name "${INSTANCE_PROFILE_NAME}" \ --security-group-ids "${SECURITY_GROUP_ID}" \ --subnet-id "${SUBNET_ID}" \ --key-pair "${KEY_PAIR}" \ --query "infrastructureConfigurationArn"`
イメージパイプライン作成
作成したレシピ、オプション設定を指定し、イメージパイプラインを作成します。
IMAGE_PIPELINE_ARN=`aws imagebuilder create-image-pipeline \ --name ${PIPELINE_NAME} \ --image-recipe-arn ${IMAGE_RECIPE_ARN//'"'/} \ --infrastructure-configuration-arn ${INFRA_CONFIG_ARM//'"'/} \ --query "imagePipelineArn"`
なお、イメージパイプラインのARNは後続のCDKの中で利用します。
ちなみに、レシピ、インフラ設定のARNに"
があるとうまく動作しなかったので、変数展開の際に"
を除いています。
正常に作成できると、イメージパイプラインが作成されます。
$ aws imagebuilder list-image-pipelines { "requestId": "30391592-3d3f-46f2-9259-c65b411e6f7c", "imagePipelineList": [ { "arn": "arn:aws:imagebuilder:ap-northeast-1:XXXXXXXXXXXX:image-pipeline/testimagepipeline", "name": "TestImagePipeline", "platform": "Linux", "imageRecipeArn": "arn:aws:imagebuilder:ap-northeast-1:XXXXXXXXXXXX:image-recipe/testrecipe/1.0.0", "infrastructureConfigurationArn": "arn:aws:imagebuilder:ap-northeast-1:XXXXXXXXXXXX:infrastructure-configuration/testinfraconfig", "imageTestsConfiguration": { "imageTestsEnabled": true, "timeoutMinutes": 720 }, "status": "ENABLED", "dateCreated": "2020-01-30T14:32:17.441Z", "dateUpdated": "2020-01-30T14:32:17.441Z", "tags": {} } ] }
CodePipelineとかその他リソース構築
CDK プロジェクト作成
CDKプロジェクトの用のディレクトリを作成します。
$ mkdir test-app && cd test-app
cdk init
コマンドにて、プロジェクトの初期化を行います。
$ cdk init --language python
パッケージインストール
setup.py
に今回利用するパッケージを追加します。
... install_requires=[ "aws-cdk.core", "aws-cdk.aws-s3", "aws-cdk.aws-lambda", "aws-cdk.aws_codecommit", "aws-cdk.aws_codepipeline", "aws-cdk.aws_codepipeline_actions", "aws-cdk.aws_iam", ], ...
先に実行したcdk init
にて、virtualenvを利用した仮想環境も作成されていますので、仮想環境を有効化します。こちらで、既存の環境に影響を与えずに任意のパッケージをインストールすることが可能になります。
$ source .env/bin/activate (.env) $ pip install -r requirements.txt
外部定義
Lambdaがイメージパイプラインを起動する際にARNが必要になります。一連の流れでシェル変数には定義されていますが、ログアウト等でCDKが再利用できなくなってしまうので、ここでは外部ファイルに定義しています。作成したイメージパイプラインのARN、CodePipelineでデプロイを行うS3バケット、処理終了後にPublishするSNS Topicを外部ファイルに定義しておきます。
[test] IMAGE_PIPELINE_ARN = arn:aws:imagebuilder:ap-northeast-1:XXXXXXXXXXXX:image-pipeline/testimagepipeline DEPLOY_BUCKET_NAME = test-pipeline-deploy SNS_TOPIC_ARN = arn:aws:sns:ap-northeast-1:XXXXXXXXXXXX:test-topic
Lambda Function用のコード作成
Lambda Function用のコード、格納ディレクトリを作成します。
$ mkdir lambda
import boto3 import logging import json import os import traceback logger = logging.getLogger() logger.setLevel(logging.INFO) codepipeline_client = boto3.client('codepipeline') imagebuilder_client = boto3.client('imagebuilder') sns_client = boto3.client('sns') #正常終了 def put_job_success(job_id): logger.info('Putting job success') codepipeline_client.put_job_success_result(jobId=job_id) #処理継続/CodePipelineに継続トークン返却 def continue_job_later(job_id,image_build_version_arn): logger.info('Putting job continuation') continuation_token = json.dumps({'ImageBuildVersionArn':image_build_version_arn}) codepipeline_client.put_job_success_result( jobId=job_id, continuationToken=continuation_token ) #異常終了 def put_job_failure(job_id, err): logger.error('Putting job failed') message = str(err) codepipeline_client.put_job_failure_result( jobId=job_id, failureDetails={ 'type': 'JobFailed', 'message': message } ) #SNS通知 def sns_publish(sns_topic_arn, pipeline_name, job_id, job_status): logger.info('Publish to SNS topic') message = 'PipelineName: ' + pipeline_name + '\n' message += 'JobId: ' + job_id + '\n' message += 'Status: ' + job_status + '\n' res = sns_client.publish( TopicArn=sns_topic_arn, Message=message ) messaeg_id = res['MessageId'] logger.info('SNS Messaeg ID is %s', messaeg_id) def lambda_handler(event, context): try: job_id = event['CodePipeline.job']['id'] job_data = event['CodePipeline.job']['data'] image_pipeline_arn = os.environ['IMAGE_PIPELINE_ARN'] #CodePipelineユーザーパラメーター取得 user_parameters = json.loads( job_data['actionConfiguration']['configuration']['UserParameters'] ) pipeline_name = user_parameters['PipelineName'] sns_topic_arn = user_parameters['SnsTopicArn'] logger.info('ImagePipelineArn is %s', image_pipeline_arn) logger.info('CodePipeline Event is %s',event['CodePipeline.job']) #継続トークン有無確認 if 'continuationToken' in job_data: continuation_token = json.loads(job_data['continuationToken']) image_build_version_arn = continuation_token['ImageBuildVersionArn'] logger.info(image_build_version_arn) #ビルドの状態取得 response = imagebuilder_client.get_image( imageBuildVersionArn = image_build_version_arn ) build_status = response['image']['state']['status'] logger.info(build_status) if build_status == 'AVAILABLE': sns_publish(sns_topic_arn, pipeline_name, job_id, job_status='success') put_job_success(job_id) elif build_status == 'FAILED': sns_publish(sns_topic_arn, pipeline_name, job_id, job_status='failed') errmsg='Build Error' put_job_failure(job_id, errmsg) else: continue_job_later(job_id,image_build_version_arn) else: #ビルド実行 response = imagebuilder_client.start_image_pipeline_execution( imagePipelineArn=image_pipeline_arn ) image_build_version_arn = response['imageBuildVersionArn'] logger.info('imageBuildVersionArn is %s', image_build_version_arn) continue_job_later(job_id,image_build_version_arn) except Exception as err: logger.error('Function exception: %s', err) traceback.print_exc() sns_publish(sns_topic_arn, pipeline_name, job_id, job_status='failed') put_job_failure(job_id, 'Function exception: ' + str(err)) logger.info('Function complete') return "Complete."
スタック定義
スタックを定義しているtest_app_stack.py
に、デプロイしたいリソースを定義します。ここでは、CodeCommitも作成しているので継続的に利用する際はご注意ください。
import configparser from aws_cdk import ( core, aws_s3, aws_lambda, aws_codecommit, aws_codepipeline, aws_codepipeline_actions, aws_iam ) # 設定ファイル読み込み config = configparser.ConfigParser() config.read('setting.ini') class TestAppStack(core.Stack): def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: super().__init__(scope, id, **kwargs) # デプロイ用S3バケット作成 deploy_bucket = aws_s3.Bucket(self, "MyBucket", bucket_name = config['test']['DEPLOY_BUCKET_NAME'], versioned = True, ) # IAMロール(Lambda Function) lambdaRole = aws_iam.Role( self,'lambdaRole', role_name= 'TestRole', assumed_by= aws_iam.ServicePrincipal('lambda.amazonaws.com') ) lambdaRole.add_managed_policy(aws_iam.ManagedPolicy.from_aws_managed_policy_name('AWSImageBuilderFullAccess')) lambdaRole.add_managed_policy(aws_iam.ManagedPolicy.from_aws_managed_policy_name('AWSLambdaExecute')) lambdaRole.add_managed_policy(aws_iam.ManagedPolicy.from_aws_managed_policy_name('AmazonSNSFullAccess')) # Lambda Function fn = aws_lambda.Function(self, "TestFunction", runtime=aws_lambda.Runtime.PYTHON_3_8, handler="lambda_function.lambda_handler", role= lambdaRole, code=aws_lambda.AssetCode(path="./lambda"), environment={ "IMAGE_PIPELINE_ARN": config['test']['IMAGE_PIPELINE_ARN'] } ) # CodeCommit作成 repo = aws_codecommit.Repository(self, "Repository", repository_name="TestRepository" ) # CodePipeline作成 pipeline = aws_codepipeline.Pipeline(self, "MyPipeline", pipeline_name="TestCodePipeline" ) source_output = aws_codepipeline.Artifact() #パイプラインアクション作成 source_action = aws_codepipeline_actions.CodeCommitSourceAction( action_name="CodeCommit", repository=repo, output=source_output ) deploy_action = aws_codepipeline_actions.S3DeployAction( bucket=deploy_bucket, action_name="S3", input=source_output ) build_action = aws_codepipeline_actions.LambdaInvokeAction( action_name="Lambda", user_parameters={"PipelineName":"TestCodePipeline","SnsTopicArn":config['test']['SNS_TOPIC_ARN']}, lambda_=fn ) # CodePipelineステージ追加 source_stage = pipeline.add_stage( stage_name="SourceStage", actions=[source_action] ) deploy_stage = pipeline.add_stage( stage_name="DeployStage", actions=[deploy_action] ) build_stage = pipeline.add_stage( stage_name="BuildStage", actions=[build_action] )
修正したファイルは以下となります。(修正していないファイルはツリーから除いています。)
. ├── lambda │ └── lambda_function.py ├── setting.ini ├── setup.py └── test_app └── test_app_stack.py
デプロイ
スタックを確認します。
(.env) % cdk ls test-app
デプロイします。
(.env) % cdk deploy This deployment will make potentially sensitive changes according to your current security approval level (--require-approval broadening). Please confirm you intend to make the following modifications: (省略) Do you wish to deploy these changes (y/n)? y test-app: deploying... (省略) ✅ test-app Stack ARN: arn:aws:cloudformation:ap-northeast-1:XXXXXXXXXXXX:stack/test-app/5b7ce630-4378-11ea-9329-06cfba976b48
CodePipelineが作成されました。
(.env) % aws codepipeline list-pipelines { "pipelines": [ { "name": "TestCodePipeline", "version": 1, "created": 1580435174.207, "updated": 1580435174.207 } ] }
動かしてみた
動作については「設定ファイルPushでゴールデンイメージを自動作成する構成を考えてみた」と同様になりますので詳細は割愛しますが、 CodeCommitへのPushでパイプライン処理が起動します。処理が終わるとPushした設定ファイルを含むAMIが取得されます。
CodePipeline
EC2 Image Builder
さいごに
イメージパイプラインのARNを外部に定義したり結果的に手が入ってしまったので、EC2 Image Builderの作成もBoto 3を利用してPythonスクリプト内で完結させた方がシンプルだったなーと思いました。EC2 Image BuildeのCDK、CFn対応のアップデートに期待したいと思います。