TaskCatを使用して、CloudFormationテンプレートのテストを自動化する
以下のAWS公式ページで、CloudFormationテンプレートのテストを自動化する手法が紹介されています。
https://aws.amazon.com/jp/solutions/implementations/taskcat-ci/
この構成の意義について、以下のように説明されています。
この AWS ソリューションは、Amazon Web Services (AWS) クラウド上の AWS CloudFormation 用 TaskCat 継続的統合および継続的デリバリー (CI/CD) パイプラインをデプロイします。TaskCat を使って GitHub リポジトリから CloudFormation テンプレートのテストとデプロイを自動的に行いたいユーザー向けです。
AWS CloudFormation 向け TaskCat CI/CD パイプライン
本ページでは、上記を参考にした構成をご紹介します。
CodeCommit上の開発用ブランチにプッシュされたCloudFormationテンプレートを、TaskCatを使用してテストします。
テストが成功した場合は、パイプラインが継続され、開発用ブランチとマスターブランチをマージします。
構築する環境
CodePipelineでパイプラインを構成します。
パイプラインは以下の流れで動作します。
1つ目はCodeCommitです。
CodeCommitはCodePipelineのソースステージを担当します。
Gitリポジトリとして使用します。
リポジトリにはCloudFormationテンプレートファイル等を保存します。
2つ目はCodeBuildです。
CodeBuildはCodePipelineのビルドステージを担当します。
TaskCatを使用して、テンプレートファイルをテストします。
テスト結果をS3バケットに保存します。
3つ目はSNSです。
SNSはCodePipelineの承認ステージを担当します。
SNS経由でメール通知します。
メール通知を受けたユーザは、パイプラインのページにアクセスし、承認ボタンを押すとパイプラインが再開されます。
4つ目はLambda関数です。
Lambda関数はCodePipelineのデプロイステージを担当します。
この関数の働きは、開発用ブランチとマスターブランチをブランチをマージすることです。
CloudFormationテンプレートファイル
上記の構成をCloudFormationで構築します。
以下のURLにCloudFormationテンプレートを配置しています。
https://github.com/awstut-an-r/awstut-fa/tree/main/137
テンプレートファイルのポイント解説
CodePipeline
Resources:
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
ArtifactStore:
Location: !Ref ArtifactBucket
Type: S3
Name: !Ref Prefix
RoleArn: !GetAtt CodePipelineRole.Arn
Stages:
- Actions:
- ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeCommit
Version: 1
Configuration:
BranchName: !Ref SourceBranch
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: 2
Name: Build
- Actions:
- ActionTypeId:
Category: Approval
Owner: AWS
Provider: Manual
Version: 1
Configuration:
ExternalEntityLink: !Sub "https://${AWS::Region}.console.aws.amazon.com/s3/buckets/${ArtifactBucket}?region=${AWS::Region}&tab=objects"
NotificationArn: !Ref TopicArn
Name: Approval
Region: !Ref AWS::Region
RunOrder: 3
Name: Approval
- Actions:
- ActionTypeId:
Category: Invoke
Owner: AWS
Provider: Lambda
Version: 1
Configuration:
FunctionName: !Ref GitMergeFunction
Name: GitMerge
Region: !Ref AWS::Region
RunOrder: 4
Name: Deploy
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:
- s3:GetBucketVersioning
- s3:ListBucket
- s3:ListBucketVersions
Resource:
- !Sub "arn:aws:s3:::${ArtifactBucket}"
- Effect: Allow
Action:
- s3:GetObject
- s3:GetObjectVersion
- s3:PutObject
Resource:
- !Sub "arn:aws:s3:::${ArtifactBucket}/*"
- Effect: Allow
Action:
- cloudformation:CreateChangeSet
- cloudformation:CreateStack
- cloudformation:DeleteChangeSet
- cloudformation:DeleteStack
- cloudformation:DescribeChangeSet
- cloudformation:DescribeStacks
- cloudformation:ExecuteChangeSet
- cloudformation:SetStackPolicy
- cloudformation:UpdateStack
- cloudformation:ValidateTemplate
Resource: !Sub "arn:${AWS::Partition}:cloudformation:*:*:*"
- Effect: Allow
Action:
- iam:PassRole
Resource: "*"
Condition:
StringEquals:
iam:PassedToService: cloudformation.amazonaws.com
- Effect: Allow
Action:
- codebuild:BatchGetBuilds
- codebuild:StartBuild
Resource: !GetAtt CodeBuildProject.Arn
- Effect: Allow
Action:
- lambda:invokeFunction
Resource:
- !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${GitMergeFunction}"
- Effect: Allow
Action:
- codecommit:CancelUploadArchive
- codecommit:GetBranch
- codecommit:GetCommit
- codecommit:GetRepository
- codecommit:GetUploadArchiveStatus
- codecommit:UploadArchive
Resource:
- !GetAtt CodeCommitRepository.Arn
- Effect: Allow
Action:
- sns:Publish
Resource:
- !Ref TopicArn
Code language: YAML (yaml)
CodePipelineでパイプラインを作成します。
本パイプラインは4つのステージから構成されています。
ソースステージ
参考サイトではGithubをソースステージとするパイプラインでしたが、今回はCodeCommitを選択します。
BranchNameプロパティに開発用ブランチを指定します。
これによって、同ブランチ上に存在するCloudFormationテンプレートファイル等が、ソースステージのアウトプットアーティファクトとなります。
以下がCodeCommitです。
Resources:
CodeCommitRepository:
Type: AWS::CodeCommit::Repository
Properties:
RepositoryName: !Ref Prefix
Code language: YAML (yaml)
リポジトリ名だけを指定します。
他は特別な設定を行いません。
ビルドステージ
CodeBuildでCloudFormationテンプレートのテストを実施します。
テスト対象のテンプレートは、ソースステージで生成したアーティファクトです。
具体的には、開発用ブランチ上で管理されている各種ファイルです。
以下がCodeBuildです。
Resources:
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Artifacts:
Type: CODEPIPELINE
Cache:
Type: NO_CACHE
Environment:
ComputeType: !Ref ProjectEnvironmentComputeType
EnvironmentVariables:
- Name: ARTIFACT_BUCKET
Type: PLAINTEXT
Value: !Ref ArtifactBucket
- Name: REGION
Type: PLAINTEXT
Value: !Ref AWS::Region
- Name: REPOSITORY_URL
Type: PLAINTEXT
Value: !GetAtt CodeCommitRepository.CloneUrlHttp
- Name: RELEASE_BRANCH
Type: PLAINTEXT
Value: !Ref ReleaseBranch
- Name: SOURCE_BRANCH
Type: PLAINTEXT
Value: !Ref SourceBranch
Image: !Ref ProjectEnvironmentImage
ImagePullCredentialsType: CODEBUILD
Type: !Ref ProjectEnvironmentType
PrivilegedMode: true
LogsConfig:
CloudWatchLogs:
GroupName: !Ref LogGroup
Status: ENABLED
S3Logs:
Status: DISABLED
Name: !Sub "${Prefix}-project"
ServiceRole: !GetAtt CodeBuildRole.Arn
Source:
Type: CODEPIPELINE
BuildSpec: |
version: 0.2
env:
shell: bash
phases:
install:
runtime-versions:
python: 3.x
commands:
- echo "Entered the install phase..."
- echo "Installing system dependencies..."
- echo "Installing python dependencies..."
- pip3 -q install taskcat
pre_build:
commands:
- echo "Entered the pre_build phase..."
- echo "Current directory is $CODEBUILD_SRC_DIR"
- ls -lA
- dirname=`pwd`
- echo "Directory name $dirname"
- ls -lA
- echo "Verifying TaskCat installation..."
- taskcat
build:
commands:
- echo "Entered the build phase..."
- echo "Running TaskCat tests..."
- taskcat test run
- |
if $(grep -Fq "CREATE_FAILED" taskcat_outputs/index.html)
then
echo "Build failed!"
exit 1
else
echo "Build passed!"
exit 0
fi
finally:
- ls -1 taskcat_outputs
- ls -1 taskcat_outputs | while read LOG; do cat taskcat_outputs/$LOG; done
- >- # Do not remove
echo "Zipping and uploading report to S3 bucket: '$ARTIFACT_BUCKET'..."
- zip -r taskcat_report.zip taskcat_outputs
- aws s3 cp taskcat_report.zip s3://$ARTIFACT_BUCKET/taskcat_reports/$CODEBUILD_BUILD_ID.zip
Visibility: PRIVATE
CodeBuildRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Action:
- sts:AssumeRole
ManagedPolicyArns:
- !Sub "arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess"
Code language: YAML (yaml)
CodeBuildに関する詳細は以下のページをご確認ください。
buildspec.ymlがポイントです。
概ねは参考サイトと同様ですが、以下の点が異なります。
- CodeBuild環境をubuntu系からAmazon Linux系に変更する。
- Githubからプルしたテンプレートをテストするのではなく、CodeCommit上のものを前ステージからのアーティファクトとしてテストする。
- pre-commit等はインストールせず、実施もしない。
Taskcatの実行結果をS3バケットに保存します。
承認ステージ
SNS経由でメール通知します。
Resources:
Topic:
Type: AWS::SNS::Topic
Properties:
Subscription:
- Endpoint: !Ref MailAddress
Protocol: email
TopicName: !Ref Prefix
Code language: YAML (yaml)
通知先のメールアドレスを指定します。
その他は特別な設定は不要です。
SNSに関する詳細は以下のページをご確認ください。
デプロイステージ
Lambda関数を実行して、開発用ブランチとマスターブランチをマージします。
Resources:
GitMergeFunction:
Type: AWS::Lambda::Function
Properties:
Environment:
Variables:
REGION: !Ref AWS::Region
RELEASE_BRANCH: !Ref ReleaseBranch
REPOSITORY: !GetAtt CodeCommitRepository.Name
SOURCE_BRANCH: !Ref SourceBranch
Code:
ZipFile: |
import boto3
import json
import os
region = os.environ['REGION']
release_branch = os.environ['RELEASE_BRANCH']
repository = os.environ['REPOSITORY']
source_branch = os.environ['SOURCE_BRANCH']
codecommit_client = boto3.client('codecommit', region_name=region)
codepipeline_client = boto3.client('codepipeline', region_name=region)
def lambda_handler(event, context):
job_id = event['CodePipeline.job']['id']
try:
merge_response = codecommit_client.merge_branches_by_fast_forward(
repositoryName=repository,
sourceCommitSpecifier=source_branch,
destinationCommitSpecifier=release_branch
)
print(merge_response)
codepipeline_client.put_job_success_result(
jobId=job_id
)
return True
except Exception as e:
print(e)
codepipeline_client.put_job_failure_result(
jobId=job_id,
failureDetails={
'type': 'JobFailed',
'message': 'Something happened.'
}
)
FunctionName: !Sub "${Prefix}-GitMergeFunction"
Handler: !Ref LambdaHandler
MemorySize: !Ref LambdaMemory
Runtime: !Ref LambdaRuntime
Role: !GetAtt GitMergeFunctionRole.Arn
Timeout: !Ref LambdaTimeout
GitMergeFunctionRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Policies:
- PolicyName: GitMergePolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- codecommit:MergeBranchesByFastForward
Resource: !Sub
- "arn:${AWS::Partition}:codecommit:${AWS::Region}:${AWS::AccountId}:${Repository}*"
- Repository: !GetAtt CodeCommitRepository.Name
- Effect: Allow
Action:
- codepipeline:PutJobSuccessResult
- codepipeline:PutJobFailureResult
Resource: "*"
Code language: YAML (yaml)
Lambda関数で実行するコードをインライン表記で定義します。
詳細につきましては、以下のページをご確認ください。
参考サイトでは、GithubのエンドポイントにHTTPアクセスしてマージします。
今回はCodeCommitですので、Boto3のCodeCommitクライアントオブジェクトのmerge_branches_by_fast_forwardメソッドを実行して、両ブランチをマージします。
なおCodePipeline内でLambda関数を実行する場合は、CodePipelineクライアントオブジェクトのput_job_success_resultメソッドまたはput_job_failure_resultメソッドを使用する必要があります。
詳細は以下のページをご確認ください。
EventBridge
Resources:
EventsRule:
Type: AWS::Events::Rule
Properties:
EventPattern:
source:
- aws.codecommit
detail-type:
- CodeCommit Repository State Change
resources:
- !GetAtt CodeCommitRepository.Arn
detail:
event:
- referenceCreated
- referenceUpdated
referenceType:
- branch
referenceName:
- !Ref SourceBranch
Name: !Ref Prefix
Targets:
- Arn: !Sub "arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}"
Id: !Sub "${Prefix}-CodePipeline-CodeCommit"
RoleArn: !GetAtt EventsRuleRole.Arn
EventsRuleRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- events.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: PipelineExecutionPolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- codepipeline:StartPipelineExecution
Resource:
- !Sub "arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Pipeline}"
Code language: YAML (yaml)
EventBridgeを使用して、CodePipelineの開始をトリガーします。
以下のページを参考に、EventBridgeルールおよびIAMロールを作成します。
具体的には、開発用ブランチで作成・更新のイベントを検出した場合、トリガーします。
(参考)テストテンプレート
AWSTemplateFormatVersion: 2010-09-09
Parameters:
Handler:
Type: String
Memory:
Type: Number
Prefix:
Type: String
Runtime:
Type: String
Resources:
Function:
Type: AWS::Lambda::Function
Properties:
Environment:
Variables:
REGION: !Ref AWS::Region
MEMORY: !Ref Memory
Code:
ZipFile: |
import json
import os
region = os.environ['REGION']
memory = os.environ['MEMORY']
def lambda_handler(event, context):
data = {
'region': region,
'memory': memory
}
return {
'statusCode': 200,
'body': json.dumps(data, indent=2)
}
FunctionName: !Sub "${Prefix}-function"
Handler: !Ref Handler
MemorySize: !Ref Memory
Runtime: !Ref Runtime
Role: !GetAtt FunctionRole.Arn
Function2:
Type: AWS::Lambda::Function
Properties:
Environment:
Variables:
REGION: !Ref AWS::Region
MEMORY: !Ref Memory
Code:
ZipFile: |
import json
import os
region = os.environ['REGION']
memory = os.environ['MEMORY']
def lambda_handler(event, context):
data = {
'region': region,
'memory': memory
}
return {
'statusCode': 200,
'body': json.dumps(data, indent=2)
}
FunctionName: !Sub "${Prefix}-function2"
Handler: !Ref Handler
MemorySize: !Ref Memory
Runtime: !Ref Runtime
Role: !GetAtt FunctionRole.Arn
FunctionRole:
Type: AWS::IAM::Role
DeletionPolicy: Delete
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
Service:
- lambda.amazonaws.com
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
Code language: YAML (yaml)
サンプルのCloudFormationテンプレートです。
Taskcatでテストする対象です。
Lambda関数およびIAMロールを作成する内容です。
.taskcat.yml
project:
name: taskcat-ci
regions:
- ap-northeast-1
tests:
default:
parameters:
Handler: index.lambda_handler
Memory: 128
Prefix: taskcat-ci
Runtime: python3.8
template: ./test-template.yaml
Code language: YAML (yaml)
Taskcatの設定ファイルです。
テスト対象として先述のテンプレートを指定します。
テストスタックはap-northeast-1リージョンに作成します。
parametersでテストスタック作成時のパラメータを指定します。
今回は4つのパラメータを渡します。
環境構築
CloudFormationを使用して、本環境を構築し、実際の挙動を確認します。
CloudFormationスタックを作成し、スタック内のリソースを確認する
CloudFormationスタックを作成します。
スタックの作成および各スタックの確認方法については、以下のページをご確認ください。
各スタックのリソースを確認した結果、今回作成された主要リソースの情報は以下の通りです。
- CodePipeline:fa-137
- CodeCommitリポジトリ:fa-137
- CodeBuildプロジェクト:fa-137
- S3バケット:fa-137
- Lambda関数:fa-137-GitMergeFunction
- SNSトピック:fa-137
メールアドレスの認証
SNSトピックのサブスクライバーとしてメールアドレスを指定した場合、そのメールアドレスを認証する必要があります。
指定したメールアドレスに、以下のような認証メールが送られてきます。
「Confirm subscription」を押下して、認証を進めます。
上記のページが表示されて、認証が完了したことがわかります。
リソース確認
作成されたリソースをAWS Management Consoleから確認します。
SNSを確認します。
確かにSNSトピックが作成されています。
CodeCommitを確認します。
確かにリポジトリが作成されています。
CodeBuildを確認します。
確かにCodeBuildプロジェクトが作成されています。
Lambda関数を確認します。
正常に作成されています。
CodePipelineを確認します。
パイプラインの実行に失敗しています。
これはCloudFormationスタック作成時に、CodeCommitが作成されたことがきっかけとして、パイプラインが実行されたためです。
現時点ではCodeCommitにコードをプッシュしていないため、パイプライン実行の過程でエラーが発生しました。
動作確認
準備が整いましたので、CodeCommitにコードをプッシュします。
まずCodeCommitリポジトリをプルします。
$ git clone https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/fa-137
Cloning into 'fa-137'...
warning: You appear to have cloned an empty repository.
Code language: Bash (bash)
空のリポジトリがプルされました。
マスターブランチをコミット・プッシュします。
(master) $ git commit --allow-empty -m "initial commit"
[master (root-commit) 3aee162] initial commit
...
(master) $ git push
...
To https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/fa-137
* [new branch] master -> master
...
Code language: Bash (bash)
ファイルは存在しませんが、コミット・プッシュしました。
後述の開発用ブランチの作成や、両ブランチをマージするために必要な手続きです。
開発用ブランチを作成します。
(master) $ git branch dev
(master) $ git branch -a
dev
* master
remotes/origin/master
Code language: Bash (bash)
開発用ブランチに切り替えます。
(master) $ git checkout dev
Switched to branch 'dev'
Code language: Bash (bash)
テスト用のテンプレートやTaskcat用ファイルを配置します。
(dev) $ ls -al
total 8
drwxrwxr-x 3 ec2-user ec2-user 64 Jul 7 11:56 .
drwxrwxr-x 3 ec2-user ec2-user 20 Jul 7 11:43 ..
drwxrwxr-x 8 ec2-user ec2-user 166 Jul 7 11:55 .git
-rw-rw-r-- 1 ec2-user ec2-user 264 Jul 4 23:12 .taskcat.yml
-rw-rw-r-- 1 ec2-user ec2-user 3426 Jul 7 11:26 test-template.yaml
Code language: Bash (bash)
2ファイルを開発用ブランチにプッシュします。
(dev) $ git add .
(dev) $ git commit -m 'first commit'
[dev 8fee41a] first commit
...
2 files changed, 146 insertions(+)
create mode 100644 .taskcat.yml
create mode 100644 test-template.yaml
(dev) $ git push --set-upstream origin dev
...
To https://git-codecommit.ap-northeast-1.amazonaws.com/v1/repos/fa-137
* [new branch] dev -> dev
branch 'dev' set up to track 'origin/dev'.
Code language: Bash (bash)
正常にプッシュできました。
改めてCodeCommitを確認します。
確かに2ファイルがプッシュされています。
CodePipelineを確認します。
CodePipelineが開始されました。
CodeCommitの開発用ブランチが更新されたことがきっかけです。
Sourceステージが完了しました。
続いてBuildステージが開始されています。
しばらく待つとCodeBuildが正常に完了し、Buildステージも終了します。
Approvalステージで一時停止しています。
SNSトピックに指定したメールアドレスに、以下のようなメッセージが届きました。
承認するためのレビューを行います。
「Content to review」のURLにアクセスします。
ZIPファイルが保存されています。
中身を確認します。
Taskcatによるテスト結果です。
CloudFormationによって、ap-northeast-1リージョンにLambda関数とIAMロールが正常に生成できました。
テンプレートに問題ないことが確認できましたので、承認アクションを実行します。
これでパイプラインが再開されます。
しばらく待つと、Deployステージも完了します。
Lambda関数の実行ログを確認します。
正常に実行されたことがわかります。
つまり開発用ブランチとマスターブランチのマージが正常に実行されたということです。
最後にCodeCommitを確認します。
開発用ブランチに加えて、マスターブランチにもコミットされた形跡があります。
マスターブランチに2ファイルが格納されています。
つまり2ブランチがマージされたことによって、開発用ブランチにあったファイルがマスターブランチにも置かれました。
まとめ
CodeCommit上の開発用ブランチにプッシュされたCloudFormationテンプレートを、TaskCatを使用してテストしました。
テストが成功した場合は、パイプラインが継続され、開発用ブランチとマスターブランチをマージしました。