前提条件
リポジトリやブランチを切った時にいちいち手動で静的解析&テストパイプラインなんて作ってられないよね!という人向け。
以下の経験があると分かりやすいはず。
- CodePipelineで簡単なパイプラインを作ったことがある
- buildspec.ymlをちょっと書いたことがある
- CloudFormationテンプレートをちょっと書いたことがある
- SonarQubeをなんとなく知ってる(前回、前々回を読んでSonarQubeのサーバが起動できる)
パイプラインを作るためのIaCとビルド仕様
今回もいきなりCloudFormationテンプレートとbuildspec.ymlから。
buildspec.ymlは本当にビルドするときの名前にしたいので、buildspec_sonar.ymlと別名にしている。別にsonar/buildspec.ymlとかディレクトリを掘っても問題はない。
せっかくだから、静的解析が終わってエラーが見つかった場合はパイプラインが異常終了して分かるようにしておこう。
また、直接ファイルに書き込みたくないので、Parameter StoreからSonarQubeのエンドポイントとトークンを取得するようにする。
AWSTemplateFormatVersion: "2010-09-09"
Description:
CI/CD Pipeline for Lambda Create
Parameters:
Prefix:
Description: "Project name prefix"
Type: "String"
Default: "Default"
PrefixLower:
Description: "Project name prefix(for S3 Bucket)"
Type: "String"
Default: "default"
CodeCommitRepositoryName:
Description: "Repository name on CodeCommit"
Type: "String"
Default: "Default"
S3BucketNameSuffix:
Description: "S3 bucket name for Artifact"
Type: "String"
Default: "-artifact-bucket"
PipelineNameSuffix:
Description: "PipelineName on CodePipeline"
Type: "String"
Default: "-Pipeline"
BuildProjectNameSuffix:
Description: "BuildProjectName on CodeBuild"
Type: "String"
Default: "-Sonar-Project"
CodeCommitBranchName:
Description: "Branch name on Repository"
Type: "String"
Default: "Issue"
CodeBuildRoleSuffix:
Description: "CodeBuild Service Role"
Type: "String"
Default: "-service-role"
CodeBuildPolicyNameSuffix:
Description: "IAM Policy Name for CodeBuild"
Type: "String"
Default: "-CodeBuild-Polycy"
CodeBuildLogGroupNameSuffix:
Description: "LogGroup Name for CodeBuild"
Type: "String"
Default: "-CodeBuild-LogGroup"
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: "Project name prefix"
Parameters:
- Prefix
- PrefixLower
- Label:
default: "S3 Configuration"
Parameters:
- S3BucketNameSuffix
- Label:
default: "Pipeline Configuration"
Parameters:
- PipelineNameSuffix
- CodeCommitRepositoryName
- CodeCommitBranchName
- BuildProjectNameSuffix
- Label:
default: "IAM Role/Policy Configuration"
Parameters:
- CodeBuildRoleSuffix
- CodeBuildPolicyNameSuffix
- Label:
default: "Log Configuration"
Parameters:
- CodeBuildLogGroupNameSuffix
Resources:
# ------------------------------------------------------------#
# IAM Role
# ------------------------------------------------------------#
# ★1
CODEBUILDIAMROLE:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub codebuild-${Prefix}${CodeBuildRoleSuffix}
Description: !Sub Branch CI role for ${CodeCommitRepositoryName} ${CodeCommitBranchName}
Path: /serivice-role/
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: !Sub ${Prefix}${CodeBuildPolicyNameSuffix}
PolicyDocument:
Version: "2012-10-17"
Statement:
- Sid: AllowS3Bucket
Effect: Allow
Action:
- "s3:GetBucketAcl"
- "s3:GetBucketLocation"
Resource: !GetAtt S3BUCKET.Arn
- Sid: AllowS3Object
Effect: Allow
Action:
- "s3:PutObject"
- "s3:GetObject"
- "s3:GetObjectVersion"
Resource: !Join [ "/", [ !GetAtt S3BUCKET.Arn, "*" ] ]
- Sid: AllowCloudWatchLogs
Effect: Allow
Action:
- "logs:CreateLogGroup"
- "logs:CreateLogStream"
- "logs:PutLogEvents"
Resource: !GetAtt CODEBUILDLOGGROUP.Arn
- Sid: ParameterStorePutandAssumeRole
Effect: Allow
Action:
- "ssm:GetParameters"
- "sts:AssumeRole"
Resource: '*'
# ------------------------------------------------------------#
# S3 Bucket
# ------------------------------------------------------------#
S3BUCKET:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${PrefixLower}${S3BucketNameSuffix}
# ------------------------------------------------------------#
# Cloud Watch Log Group
# ------------------------------------------------------------#
CODEBUILDLOGGROUP:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub ${Prefix}${CodeBuildLogGroupNameSuffix}
# ------------------------------------------------------------#
# CodeBuild
# ------------------------------------------------------------#
CODEBUILD:
Type: AWS::CodeBuild::Project
Properties:
Name: !Sub ${Prefix}${BuildProjectNameSuffix}
Source:
Type: CODEPIPELINE
BuildSpec: buildspec_sonar.yml
Artifacts:
Type: CODEPIPELINE
# ★2
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
EnvironmentVariables:
- Name: REPOSITORY_NAME
Type: PLAINTEXT
Value: !Sub ${CodeCommitRepositoryName}
- Name: BRANCH_NAME
Type: PLAINTEXT
Value: !Sub ${CodeCommitBranchName}
LogsConfig:
CloudWatchLogs:
GroupName: !Sub ${Prefix}${CodeBuildLogGroupNameSuffix}
Status: ENABLED
Cache:
Type: LOCAL
Modes:
- LOCAL_CUSTOM_CACHE
ServiceRole: !GetAtt CODEBUILDIAMROLE.Arn
# ------------------------------------------------------------#
# CodePipeline
# ------------------------------------------------------------#
PIPELINE:
Type: AWS::CodePipeline::Pipeline
DependsOn: CODEBUILDIAMROLE
Properties:
Name: !Sub ${Prefix}${PipelineNameSuffix}
ArtifactStore:
Location: !Ref S3BUCKET
Type: S3
RoleArn: !Sub arn:aws:iam::${AWS::AccountId}:role/CodePipelineRole
Stages:
- Name: Source
Actions:
- RunOrder: 1
Name: Source
ActionTypeId:
Category: Source
Owner: AWS
Provider: CodeCommit
Version: 1
Configuration:
RepositoryName: !Sub ${CodeCommitRepositoryName}
BranchName: !Sub ${CodeCommitBranchName}
OutputArtifacts:
- Name: SourceArtifact
- Name: SonarQube
Actions:
- RunOrder: 2
Name: Build
ActionTypeId:
Category: Build
Owner: AWS
Provider: CodeBuild
Version: 1
Configuration:
ProjectName: !Ref CODEBUILD
InputArtifacts:
- Name: SourceArtifact
version: 0.2
# ★3
env:
parameter-store:
SONARQUBE_ENDPOINT: "SONARQUBE_ENDPOINT"
SONARQUBE_TOKEN: ${REPOSITORY_NAME}-${BRANCH_NAME}-token
phases:
install:
runtime-versions:
java: corretto8
build:
commands:
- SONAR_PROJECTNAME=${REPOSITORY_NAME}-${BRANCH_NAME}
- echo ${SONAR_PROJECTNAME}
- echo build started on `date`
- mvn install
- echo build finished on `date`
- echo SonarScanner started on `date`
- echo command line sonar-scanner -Dsonar.projectName=${SONAR_PROJECTNAME} -Dsonar.projectKey=${SONAR_PROJECTNAME} -Dsonar.login=${SONARQUBE_TOKEN} -Dsonar.host.url=${SONARQUBE_ENDPOINT}
# ★4
- mvn sonar:sonar -Dsonar.projectName=${SONAR_PROJECTNAME} -Dsonar.projectKey=${SONAR_PROJECTNAME} -Dsonar.login=${SONARQUBE_TOKEN} -Dsonar.host.url=${SONARQUBE_ENDPOINT}
- echo SonarScanner finished on `date`
post_build:
commands:
# ★5
- TASKID=`grep ceTaskId target/sonar/report-task.txt | sed s/ceTaskId=//`
- "echo \"SonarQube Task Id: ${TASKID}\""
- TASKSTATUS_URI=`grep ceTaskUrl target/sonar/report-task.txt | sed s/ceTaskUrl=//`
- |
TASKSTATUS="PENDING";
while [ "${TASKSTATUS}" != "SUCCESS" ]; do
if [ "${TASKSTATUS}" = "FAILED" ] || [ "{$TASKSTATUS}" = "CANCELLED" ]; then
echo "SonarQube task ${TASKID} failed";
exit 1;
fi
sleep 5;
TASKSTATUS=`curl ${TASKSTATUS_URI} | jq -r ".task.status"`;
echo "SonarQube analysis status is ${TASKSTATUS}";
done
- ANALYSISID=`curl ${TASKSTATUS_URI} | jq -r ".task.analysisId"`
- QUALITYSTATUS=`curl ${SONARQUBE_ENDPOINT}api/qualitygates/project_status\?analysisId=${ANALYSISID} | jq -r ".projectStatus.status"`
- |
if [ "${QUALITYSTATUS}" = "OK" ]; then
echo "SonarQube analysis complete. Quality Gate Passed.";
exit 0;
elif [ "${QUALITYSTATUS}" = "ERROR" ]; then
echo "SonarQube analysis complete. Quality Gate Failed.";
exit 1;
else
echo "An unexpected error occurred while attempting to analyze with SonarQube.";
exit 1;
fi
cache:
paths:
- '/root/.m2/**/*'
★1 IAMロール
アーティファクトを受け渡すためのS3バケットとCloudWatch Logs関連のポリシに加えて、今回はパラメータストアから情報を取得するために以下のポリシを加えてある。
- Sid: ParameterStorePutandAssumeRole
Effect: Allow
Action:
- "ssm:GetParameters"
- "sts:AssumeRole"
Resource: '*'
★2 環境変数によるリポジトリ・ブランチ名の受け渡し
ここでビルドプロジェクトに環境変数を設定し、環境変数でリポジトリ名とブランチ名をbuildspec.ymlに渡してあげることで、buildspecを変更することなく複数のリポジトリ名とブランチ名を扱うことができる。
EnvironmentVariables:
- Name: REPOSITORY_NAME
Type: PLAINTEXT
Value: !Sub ${CodeCommitRepositoryName}
- Name: BRANCH_NAME
Type: PLAINTEXT
Value: !Sub ${CodeCommitBranchName}
★3 Parameter Storeからの値の取得
冒頭に記載した通り、Parameter Storeからトークンの値を取得する。
トークンの値はあらかじめ[リポジトリ名]-[ブランチ名]-token
で作っておく。
Community版使わないなら、ブランチ名いらないということらしいけどね……ケチケチな対応。
★2で作った環境変数が、ここで活きてくる。
env:
parameter-store:
SONARQUBE_ENDPOINT: "SONARQUBE_ENDPOINT"
SONARQUBE_TOKEN: ${REPOSITORY_NAME}-${BRANCH_NAME}-token
★4 SonarScannerの起動
インターネットで検索するとSonarScannerはmvn sonar:sonar
で実施する方法と、sonar-scannerをダウンロードする方法が出てくるが、後者は検査結果の情報をファイルダンプしてくれなくてハンドリングが面倒。前者の場合、target/sonar/report-task.txt
に情報が出力されるので、post_buildの記述量が減る。
各パラメータは以下のような意味。他にもチューニングパラメータはたくさんあるが、今回はこれくらい抑えておけば充分。
パラメータ名 | 意味 |
---|---|
sonar.projectName | SonarQubeの解析結果が画面のタイトルになる部分。指定しないと、Mavenの場合はpom.xmlのartifactIdかnameの値が使われる |
sonar.projectKey | SonarQubeでプロジェクトを一位に識別する識別子 |
sonar.login | 事前に払い出していたプロジェクトのトークン |
sonar.host.url | SonarQubeのサーバが起動しているエンドポイントのルートパス。間違えて末尾の"/"が重複するようなことをしてはいけない。謎のExceptionに丸一日悩まされた…… |
sonar.java.binaries | ビルド結果がtargetディレクトリ以外に出力されるようであれば指定する。デフォルトはtarget |
★5 解析結果によってCodeBuildの実行結果コードを変える
SonarScannerが出力したtarget/sonar/report-task.txt
から以下のレコードを取得する。
ceTaskId=[タスクID]
ceTaskUrl=http://[SonarQubeのエンドポイント]/api/ce/task?id=[タスクID]
ここからタスクの実行状況を監視しているのがwhile文の部分。
さらに、while文の結果からjqコマンドでJSONを解析して、
http://[SonarQubeのエンドポイント]/api/qualitygates/project_status?analysisId=[解析結果のanalysisId]
をcurlで実行して解析結果を取得し、if文で正常/異常判定をしている。
実行するためのCLI
このCloudFormationテンプレートを実行するためのCLIは以下。
リポジトリ名、ブランチ名を組み合わせれば、他から呼び出して自動でパイプラインを作ったりもできるはず。
$ aws cloudformation create-stack --stack-name [スタック名] \
--template-body file://[CloudFormationテンプレートファイル] \
--capabilities CAPABILITY_NAMED_IAM \
--parameters ParameterKey=Prefix,ParameterValue=[各リソースのプレフィックス] \
ParameterKey=PrefixLower,ParameterValue=[S3向けプレフィックス] \
ParameterKey=CodeCommitRepositoryName,ParameterValue=[リポジトリ名] \
ParameterKey=CodeCommitBranchName,ParameterValue=[ブランチ名]