設定ファイルPushでゴールデンイメージを自動作成する構成を考えてみた

設定ファイルをCodeCommitで管理しているケースを想定し、PushにてEC2 Image Builderを起動し、ゴールデンイメージの作成を自動化する構成を考えてみました。
2020.01.31

EC2 Image Builderで作成されるイメージはゴールデンイメージとなるため、カスタム済みの設定ファイルなどをイメージの中に含めるかと思います。設定ファイルをCodeCommitで管理しているケースを想定し、PushにてEC2 Image Builderを起動し、ゴールデンイメージの作成を自動化する構成を考えてみました。

構成

00

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分程度でした。

参考