設定ファイルPushでゴールデンイメージを自動作成する構成を考えてみた
EC2 Image Builderで作成されるイメージはゴールデンイメージとなるため、カスタム済みの設定ファイルなどをイメージの中に含めるかと思います。設定ファイルをCodeCommitで管理しているケースを想定し、PushにてEC2 Image Builderを起動し、ゴールデンイメージの作成を自動化する構成を考えてみました。
構成
CodeCommitにPushした設定ファイルをパイプライン処理でS3にデプロイします。その後、Lambda Function呼び出し、EC2 Image Builderを起動させ、S3にデプロイされた設定ファイルを含む形でイメージを作成。イメージ作成完了でSNSへPublishするような構成です。
EC2 Image Builder
以下のビルドコンポーネントを有するイメージパイプラインを作成しました。ここではApacheをインストールして、設定ファイル(httpd.conf
)をS3より取得する定義になります。この設定ファイルをCodeCommitの管理対象としています。
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
Lambda Function
イメージパイプラインを実行するLambda Functionです。このLambda FunctionはCodePipelineより呼び出しされます。
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."
イメージパイプラインは非同期で実行されるため、イメージの取得完了を把握すべく継続トークンを利用しました。Lambda Functionの実行結果に継続トークンが含まれる限り、CodePipelineによってLambda Functionが再実行される仕組みです。
また、CodePipelineにLambda Functionの終了を返すには、PutJobSuccessResultなどが必要になります。(今回はPython3.8を利用しているので、put_job_success_result) これらを定義しないと、Lambda Functionの処理が終わっても、パイプラインステージは終了しませんのでご注意ください。
CodePipeline
ステージを3つ作成しました。
Sourceステージ
アクションプロバイダーはCodeCommitを指定し、設定ファイルを管理するリポジトリを指定しています。
Deployステージ
アクションプロバイダーはS3を指定し、CodeCommitにPushした設定ファイルをデプロイするS3バケットを指定しています。
Buildステージ
アクションプロバイダーはAWS Lambdaを指定し、先に作成したLambda Functionを指定しています。
Lambda Functionの終了でSNSへPublishできるよう、ユーザーパラメーターにパイプライン名、SNSトピックARNの情報をJSONで設定しています。このパラメーターはCodePipelineからLambda Functionが呼び出される際にイベントとして渡され、Lambda Function内で参照することができます。以下のJSONは見やすいように改行していますが、実際の設定は改行を含めずに設定しています。
{ "PipelineName": "TestCodePipeline", "SnsTopicArn": "arn:aws:sns:ap-northeast-1:XXXXXXXXXXXX:test-topic" }
動かしてみた
設定ファイルを更新(ここでは、Apacheのバージョンを非表示にする設定)して、CodeCommitにPushしました。Pushをトリガーにパイプライン処理が実行されます。
CodeCommitには、設定ファイルが格納されます。
Deployステージが終了すると、指定したS3バケットに設定ファイルがデプロイされています。
Buildステージでは、Lambda Functionが起動され、イメージパイプラインが実行されます。
Lambda Functionは継続トークンを利用しているため、イメージパイプラインが終了するまで繰り返しCodePipelineから呼び出しされます。
イメージパイプラインが終了すると、Buildステージも終了します。
- イメージパイプライン
-
Buildステージ
取得したイメージよりEC2を起動し、Pushした設定ファイルが含まれているか(設定が反映されているか)確認してみます。
Apacheへアクセスし、ヘッダ情報よりバージョンが非表示になっていることが確認できました。(トップページを配置していないので403が返っています)
$ curl -I http://54.238.185.199 HTTP/1.1 403 Forbidden Date: Thu, 23 Jan 2020 10:08:56 GMT Server: Apache Upgrade: h2,h2c Connection: Upgrade Last-Modified: Tue, 22 Oct 2019 22:56:48 GMT ETag: "e2e-59587b710ac00" Accept-Ranges: bytes Content-Length: 3630 Content-Type: text/html; charset=UTF-8
続いてはバージョンを表示する変更を加え、CodeCommitに設定ファイルをPushしました。Push後の動作は先程と同様で、パイプライン処理が実行されます。
新たに取得したイメージよりEC2を起動し、設定が反映されているか確認してみます。
ヘッダ情報にバージョンが表示され、設定が反映されていることが確認できました。
$ curl -I http://54.249.34.149 HTTP/1.1 403 Forbidden Date: Thu, 23 Jan 2020 10:10:39 GMT Server: Apache/2.4.41 () Upgrade: h2,h2c Connection: Upgrade Last-Modified: Tue, 22 Oct 2019 22:56:48 GMT ETag: "e2e-59587b710ac00" Accept-Ranges: bytes Content-Length: 3630 Content-Type: text/html; charset=UTF-8
さいごに
Lambda Function部分をCodeBuildに置き換えたり、Step Functionsを利用するなど同様のことは他の構成でもできそうな気はします。 比較的?手数が少なそうな構成で、リポジトリへのファイルPushでゴールデンイメージを自動作成する方法をご紹介しました。CodePipelineからAWS Lambdaの呼び出しアクションのタイムアウトは1時間となっていますので、イメージの作成に1時間以上かかる場合は、別の構成を検討する必要があります。ちなみに、今回の構成では実行から終了までざっくり20分〜30分程度でした。