Cloud9・Codeサービス・Taskcatで実現するCloudFormationテンプレート開発環境を作る
はじめに
西藤です。
多くの場所で言及されているかと思いますが、AWSクラウドの利活用において、インフラのコード化(Infrastructure as Code・"IaC")は欠かせないテーマです。
さらに、そのIaCを組織立った形で展開していく際にはAWS Service Catalogなども使われ、CloudFormationベースでのテンプレート開発は避けては通れないものになっているはずです。
今回は、そのCloudFormationテンプレートを開発するための環境構築を、AWS内で完結できるよう検討したので、その内容をまとめてみたいと思います。
やりたいこと・アーキテクチャ
実現する体制としては、以下のようなものを想定しています。
- コード編集に使う開発環境はCloud9を利用する
- Cloud9で編集したソースコード(CloudFormationテンプレート)をCodeCommitリポジトリにプッシュする
- CodePipelineでCodeCommitの変更を検知し、CodePipelineが起動する
- CodeBuildでビルドを実行する
- CodeBuildで各種lintおよびTaskCatによるテストを実行する
- TaskCatがCloudFormationテンプレートに基づきStackを暫定デプロイし、テストを実行する(テストが完了したらStackを自動的に削除する)
- テスト結果およびTestをパスしたCloudFormationテンプレートをS3にアップロードする
- 開発者が実行結果を確認する
という具合です。
Cloud9を採用したのは、開発者の持つPCによる環境差異をなくすためです。macOSとWindowsでの開発環境の違いなどによって開発者の体験が変わってしまうことを防ぎます。
また、CodeCommitに代表されるCodeシリーズのサービスを利用することで、AWS内で完結させることを目指しています。
では、各サービス内での設定を見ていきましょう。
Cloud9環境の作成
まず、Cloud9を利用して開発環境を構築します。
今回の開発においては、Cloud9のマシン上で重たいビルド処理のようなものは実行しないので、最小限のインスタンスタイプで構築しています。
また、自動的に作られるIAMのリソースを最小限にしておきたかったので、ネットワーク設定は「セキュアシェル(SSH)」を選択しました。
CodeCommitリポジトリの作成
今回の開発のソースコード管理にはCodeCommitを利用します。
まずは空っぽのリポジトリを作成し、README.mdを作成しておきます。
そうすると、以下のようなリポジトリが作成されます。(だいぶ作業を進めてしまってからのスクリーンショットなので他のファイルも写ってますが)
Cloud9環境内にCodeCommitリポジトリをクローンする
CodeCommitリポジトリが作成されたら、このリポジトリを同期先としてCloud9の開発環境に登録します。
「URLのクローン」画面から「HTTPSのクローン」をクリックします。これによりこのリポジトリをクローンするためのURLがクリップボードにコピーされます。そして、Cloud9の開発環境を開きます。
作成直後のCloud9環境はWelcome画面が表示され、環境内には何もコードがない状況です。
そこからgit操作用の画面を開きます。
そして、"Clone Repository"をクリック
URLの入力がもとめられるので、先ほどコピーしたURLを貼り付けます。
Clone先のディレクトリの指定が求められるので、好みに合わせて指定。
すると、Cloud9の開発環境にリポジトリがクローンされて、以降はCloud9内で編集したコードをCodeCommitとgit連携した形で管理できるようになります。
CodeBuildプロジェクトの作成
次に、CodeBuildプロジェクトを作成します。
基本的にはデフォルトで、以下のような構成で設定しています。
- 上記で作成したCodeCommitリポジトリをソースとして指定
- ビルド環境はAmazon LinuxのEC2型の環境を指定
- ビルドスペックはCodeCommitリポジトリ内の
buildspec.yml
を参照するように指定 - アーティファクト設定はCodePipeline側で行うのでここでは指定しない
これで、後述する各種lintやTaskCatによるテストを実行するためのBuild環境ができました。
CodePipelineの作成
次に、CodePipelineを作成します。
構成としては以下のとおりです
- Source Stageとして上記で作成したCodeCommitリポジトリを指定
- Build Stageとして上記で作成したCodeBuildプロジェクトを指定
- Deploy StageとしてS3を指定(S3バケットは別途作成しておく)
以上で、CodeCommitへのcommit pushをトリガーにパイプラインが動作するような体制ができ上がります。
ソフトウェアのインストール
冒頭に記載したように、今回はCloudFormationテンプレート開発するに際して各種lintツールを利用しますが、CodeBuildでの自動実行のほかに、Cloud9上での手動で実行できる体制も作ります。(コード編集しながらlintテストを実行できるようにするため)
そのため、各種ツールを以下のようにインストールします。
cfn-lint
https://github.com/aws-cloudformation/cfn-lint
CloudFormationのlintツールです。
CloudFormationの文法に沿って、テンプレートの構文チェックを行います。
たとえば、「存在しないEC2インスタンスタイプを指定している」などのチェックができ、「yamlの構文的には正しいが、CloudFormationとしては正しくない」ようなケースを検知できます。
インストールは以下のコマンドで行います。
pip install cfn-lint
yamllint
https://github.com/adrienverge/yamllint
こちらはyamlのlintツールです。
yamlの構文チェックを行うことで、CloudFormationテンプレートのデプロイ時にエラーがあった時に「yamlの構文的に誤りがあった」という観点でのエラーを防止し、原因の絞り込みに役立ちます。
インストールは以下のコマンドで行います。
pip install yamllint
TaskCat
https://aws-ia.github.io/taskcat/
TaskCatは、CloudFormationテンプレートを実際にデプロイしてテストしてくれるツールです。
「文法的にも正しかったが、いざAWS環境内にデプロイしたら別の要因で失敗した」というエラーを防止することに役立ちます。
インストールは以下のコマンドで行います。
pip3 install taskcat
ファイル構成と個別ファイルについて
次に、リポジトリ内のファイル構成について説明します。
今回は、以下のような構成にしています。
.
├── .cfnlintrc.yaml
├── .gitignore
├── .taskcat.yml
├── .yamllint.yaml
├── README.md
├── buildspec.yaml
├── cloudformation
│ └── main.yaml
└── junit_xml.py
それぞれのファイルについて説明します。
.cfnlintrc.yaml(cfn-lint用設定ファイル)
このファイルは、CloudFormationのlinterであるcfn-lintの設定ファイルです。
今回の構成では以下のような設定にしています。
---
templates:
- cloudformation/*.yaml
この記載により、cloudformation
ディレクトリ配下のyamlファイルを対象にlintを実行するように設定しています。
.gitignore
後述するTaskcatは、テストするとローカルの環境内にテスト結果を出力します。
Taskcatを実行するたびにテスト結果のファイルが生成されてくるので、gitの管理下には入らないようにするため、.gitignoreには、そのテスト結果が出力されるディレクトリを指定します。
taskcat_outputs/*
.taskcat.yml(TaskCat用設定ファイル)
TaskCatの設定ファイルです。
書き方については
https://aws-ia.github.io/taskcat/docs/schema/taskcat_schema/
に準拠していただければと思いますが、今回の方針としては
- "project"
- レポート出力用のS3バケットを指定
- "test"
- テスト時に一時的にデプロイを行うリージョンを指定
- テスト対象のテンプレートを指定
- テスト時にパラメータとして渡す値を指定
というような形で設定しています。例として以下のような形です。
---
project:
name: cfn-development
s3_bucket: "taskcat-output-xxxxxxxxxxxx"
tests:
main:
regions:
- ap-northeast-1
template: "cloudformation/main.yaml"
parameters:
VpcCidr: "10.0.0.0/16"
PublicSubnetCidr: "10.0.1.0/24"
PrivateSubnetCidr: "10.0.2.0/24"
これによりTaskCatのテスト実行時には、cloudformation/main.yaml
をap-northeast-1
リージョンにデプロイし、その際のパラメータとしてVpcCidr
、PublicSubnetCidr
、PrivateSubnetCidr
を渡すようになります。
テスト完了後は自動的にStackは削除されて、テスト時のログが指定のS3バケットに出力されます。
.yamllint.yaml(yamllint用設定ファイル)
yamllintの設定ファイルです。
https://yamllint.readthedocs.io/en/stable/configuration.html
に準拠してデフォルトの設定を入れました。指摘項目を変更したい時には、このファイルを編集することで調整できます。
---
yaml-files:
- '*.yaml'
- '*.yml'
- '.yamllint'
rules:
anchors: enable
braces: enable
brackets: enable
colons: enable
commas: enable
comments:
level: warning
comments-indentation:
level: warning
document-end: disable
document-start:
level: warning
empty-lines: enable
empty-values: disable
float-values: disable
hyphens: enable
indentation: enable
key-duplicates: enable
key-ordering: disable
line-length: enable
new-line-at-end-of-file: enable
new-lines: enable
octal-values: disable
quoted-strings: disable
trailing-spaces: enable
truthy:
level: warning
buildspec.yaml(CodeBuild用設定ファイル)
次に、CodeBuildでの各ビルド処理の設定ファイルです。
---
version: 0.2
phases:
install:
runtime-versions:
python: 3.12
commands:
- pip install yamllint
- pip install cfn-lint
- pip3 install taskcat
pre_build:
commands:
- yamllint --version
- cfn-lint --version
- taskcat --version
- aws s3 rm s3://cfn-development-xxxxxxxxxxxx/cloudformation --recursive
build:
commands:
- yamllint .
- cfn-lint
- taskcat test run
post_build:
commands:
- cat taskcat_outputs/*.txt
- report=`ls -rt taskcat_outputs/*.txt |tail -n 1`
- python3 junit_xml.py $report
reports:
taskcat_outputs:
files:
- taskcat_outputs/*.xml
file-format: JunitXml
artifacts:
files:
- 'cloudformation/*'
- 'taskcat_outputs/*'
大枠としては
phases
- install
- python3.12を実行環境として指定
- yamllint, cfn-lint, taskcatをインストール
- pre_build
- yamllint, cfn-lint, taskcatのバージョンを確認
- taskcatの実行前にテンプレートの出力先S3バケットを空にする
- build
- yamllintでlintを実行
- cfn-lintでlintを実行
- taskcatでテストを実行
- post_build
- taskcatのテスト結果を出力
- テスト結果をCodeBuildのレポート機能で表示できるようにJUnit形式に変換(後述するスクリプトを実行する)
reports
- taskcatのテスト結果のファイルを指定(CodeBuildのレポート機能で表示できるようにするため)
artifacts
- テストをパスしたCloudFormationテンプレートをS3にアップロードするために指定
- taskcatの実行ログ群をS3にアップロードするために指定
と言うような形になっています。
junit_xml.py
CodeBuildには、テスト結果をレポートとして表示する機能があります。
https://docs.aws.amazon.com/ja_jp/codebuild/latest/userguide/test-reporting.html
に沿った形で結果ファイルを出力して引き渡せば、AWSマネジメントコンソール上で実施結果を確認できるようになります。
このスクリプトはTaskCatのテスト結果をJUnit形式に変換するためのものです。
import sys
import os
import xml.etree.ElementTree as ET
def convert_to_junit_xml(log):
# Create the root element
testsuite = ET.Element("testsuite")
# Create a testsuite element
testsuite = ET.SubElement(testsuite, "testsuite", name="taskcat-cnf-development")
# Parse the log and create testcases
lines = log.split("\n")
success_count = 0
failure_count = 0
for line in lines:
if "CREATE_COMPLETE" in line or "CREATE_FAILED" in line:
date, time, resource_status, resource_type, logical_resource_id, *rest = line.split()
if len(rest) >= 6:
resource_status_reason = ' '.join(rest)
else:
resource_status_reason = ""
testcase = ET.SubElement(testsuite, "testcase", classname=resource_type, name=logical_resource_id)
if resource_status == "CREATE_COMPLETE":
ET.SubElement(testcase, "success")
success_count += 1
else:
ET.SubElement(testcase, "failure", message=resource_status_reason)
failure_count += 1
# Add summary information
testsuite.set("tests", str(success_count + failure_count))
testsuite.set("failures", str(failure_count))
# Return the string representation of the XML
return ET.tostring(testsuite, encoding="unicode")
if __name__ == "__main__":
# Check if the log file path is provided as a command line argument
if len(sys.argv) < 2:
print("Please provide the path to the log file as a command line argument.")
sys.exit(1)
# Read the log file
log_file_path = sys.argv[1]
with open(log_file_path, "r") as file:
log = file.read()
# Convert the log to JUnit XML format
junit_xml = convert_to_junit_xml(log)
print(junit_xml)
# Save the XML as a file named "report.xml" in the same directory as the log file
log_file_directory = os.path.dirname(log_file_path)
report_file_path = os.path.join(log_file_directory, "report.xml")
with open(report_file_path, "w") as file:
file.write(junit_xml)
ビルドの実行
上記の設定により冒頭に記載したCloud9で開発をしつつ、テスト済みのCloudFormationテンプレートがS3に格納される体制ができ上がりました。
パイプラインの仕組みとしては、main
ブランチにcommitがpushされる(つまりはPullRequestがマージされる)ことを起点に、CodeBuildが実行されます。
では、実際にビルドを実行してみましょう。
CloudFormationのテンプレートは以下のようなものだとします
---
AWSTemplateFormatVersion: "2010-09-09"
Description: "CloudFormation template"
Parameters:
VpcCidr:
Type: String
Description: "The CIDR block for the VPC"
Default: 10.0.0.0/16
PublicSubnetCidr:
Type: String
Description: "The CIDR block for the public subnet"
Default: 10.0.1.0/24
PrivateSubnetCidr:
Type: String
Description: "The CIDR block for the private subnet"
Default: 10.0.2.0/24
Resources:
MyVPC:
Type: "AWS::EC2::VPC"
Properties:
CidrBlock: !Ref VpcCidr
Tags:
- Key: Name
Value: MyVPC
MyInternetGateway:
Type: "AWS::EC2::InternetGateway"
MyVPCGatewayAttachment:
Type: "AWS::EC2::VPCGatewayAttachment"
Properties:
VpcId: !Ref MyVPC
InternetGatewayId: !Ref MyInternetGateway
MyPublicSubnet:
Type: "AWS::EC2::Subnet"
Properties:
VpcId: !Ref MyVPC
CidrBlock: !Ref PublicSubnetCidr
AvailabilityZone: !Select [0, !GetAZs ""]
MyPrivateSubnet:
Type: "AWS::EC2::Subnet"
Properties:
VpcId: !Ref MyVPC
CidrBlock: !Ref PrivateSubnetCidr
AvailabilityZone: !Select [0, !GetAZs ""]
MyRouteTable:
Type: "AWS::EC2::RouteTable"
Properties:
VpcId: !Ref MyVPC
MyPublicRoute:
Type: "AWS::EC2::Route"
DependsOn: MyVPCGatewayAttachment
Properties:
RouteTableId: !Ref MyRouteTable
DestinationCidrBlock: "0.0.0.0/0"
GatewayId: !Ref MyInternetGateway
MyPublicSubnetRouteTableAssociation:
Type: "AWS::EC2::SubnetRouteTableAssociation"
Properties:
SubnetId: !Ref MyPublicSubnet
RouteTableId: !Ref MyRouteTable
MyPrivateSubnetRouteTableAssociation:
Type: "AWS::EC2::SubnetRouteTableAssociation"
Properties:
SubnetId: !Ref MyPrivateSubnet
RouteTableId: !Ref MyRouteTable
そして、CodeCommitでPullRequestをmain
ブランチに向けてmergeする
そうすると、CodePipelineが起動する。
CodeBuild内からTaskCatが実行され、CloudFormationのスタックが一時的に作成される。
問題なく作成が行われる。
作成が終わり、テスト結果が得られたら自動的に削除が開始される。
以上で、TaskCatによる一時的なデプロイ実施によるテストは完了。そのテスト結果はレポートとして出力されます。
また、テストをパスしたCloudFormationテンプレートがS3(CodePipelineのArtifactの格納先として指定されているS3バケット)にアップロードされます。
テスト結果の確認
上記のようにCloudFormationのマネジメントコンソール画面を確認しても良いですが、個々のリソースが問題なく作成できるようになっているかはCodeBuildのレポート画面からも確認できるようになっています。
このように一つひとつのリソースをテストケースと捉えて、作成の結果を確認できます。
また、エラーを起こした場合は、以下のようにエラーの原因も表示され、その際のメッセージも表示されるようにしてあります。
(この例では、ParameterとしてPublicSubnetCidrの値をわざと不正なものにしてエラーを起こしました。)
まとめ
以上で、AWS内で完結させるCloudFormationテンプレート開発環境の構築ができました。
TaskCatの活用により、CloudFormationのテンプレート開発において、テンプレートの文法チェックだけでなく、実際にAWS環境内にデプロイしてテストすることが実現できたのでより安心感を得ることができるようになっています。
振り返りですが、
- yamllintによってYAMLの観点での構文チェックを行う
- cfn-lintによってCloudFormationの観点での構文チェックを行う
- TaskCatによって実際にAWS環境内にデプロイを行えるかという観点でチェックを行う
というような形で多重にチェックを行うことで、CloudFormationテンプレートの品質を確保することができるようになっています。
また、CodeBuildのレポート機能によってテスト結果が速やかに確認しやすいようにして開発者にとっての負担感を極力少ないものになるようにしています。
なお、課題はいくつか残っており、
1つは、「CodeBuildのレポート機能への出力時に処理時間(duration)を出力できていない」というものがあります。CloudFormationのデプロイ処理なので「待つしかない」という側面はあり、これが分かったところで利点は少ないかもしれませんが、TaskCatのレポートをJUnit XMLに変換するPythonスクリプトを改良していこうと思います。
2つ目は、「一連の構成のCloudFormation化」です。今回、CloudFormationのテンプレートを開発する環境を作る話をしておりましたが、この環境自体の構築は手動で、テンプレート化できていませんでした。
自分でもこの開発環境を使い続けていき、動作が不安定なところがないか確認しつつ、テンプレート化していきたいと思います。
以上。本記事がCloudFormationテンプレートを開発する人々との役に立てば幸いです。