この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
設定ファイル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に格納したいと思います。
component.yaml
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
に今回利用するパッケージを追加します。
test-app/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-app/setting.ini
[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
lambda/lambda_function.py
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も作成しているので継続的に利用する際はご注意ください。
test_app/test_app_stack.py
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対応のアップデートに期待したいと思います。