AWS CloudFormation、CodePipelineとGitHubで快適CI/CD環境構築
2021年08月14日

はじめに
プログラムでアプリケーションを構築する場合、定期的や、自動的に、アプリケーションをデプロイするいわゆるCI/CD環境を構築することも多いです。昔は、自分でJenkinsサーバーを立てて、毎晩最新のコードをSubversionから落としてきて、自動ビルド、自動テスト環境を構築していました。
AWS等のクラウド環境をよく使用することになった今は、CodePipelineを利用して、CI/CD環境を最初に構築することは、皆さんも実施している言わばAWSでアプリケーションを開発する定石だと思います。
また、AWSは、CloudFormationというInfrastructure as Codeを実現するサービスがあり、これを利用して、AWS上のサーバーなどの環境をコードとして管理することができます。インフラ環境をコードで定義し、それをGitHubなどの構成管理サービスで管理することで、
- インフラの定義の変更の履歴を管理できる
- テンプレートとして、様々なプロジェクトに共有できる
- 成果物として提出できる
など、様々な恩恵を受けることができます。
今回の記事では、GitHub⇒CodePipeline⇒CloudFormationのように処理をつなげて、AWSのインフラをCI/CD環境で構築します。
なお、この記事で作成したコードは、こちらで公開しています。
実現したいこと
最終的に実現したいCI/CD環境は以下の流れです。
- GitHubのリポジトリに登録されているCloudFormationのyamlファイルを修正して、push
- pushをトリガーにして、CodePipelineを自動的に実行し、CloudFormationの変更セットを自動的に生成
- CodePipelineの承認依頼をSlackに通知する
- CodePipelineで承認依頼を承認する
- CloudFormationの変更セットを適用して、インフラを更新
- 更新結果をSlackに通知
なお、実際に業務で使用する場合は、上記の5で作成するインフラの中に、さらにソフトウェアのCI/CDを実現するCodePipelineが含まれており、インフラのCodePipelineとは別に、ソフトウェアのビルド&デプロイのCI/CD環境の2段構成になります。
前提条件
まず、この記事の内容は以下を前提にしています。
- AWSのアカウントを所有している
- GitHubのアカウントを所有している
- 管理者権限を持っているSlackのWorkspaceを所有している
前準備
最初に、いくつかのリソースをマネジメントコンソールから手動で作成します。この部分も、CloudFormationで管理できるのかもしれませんが、
- 複数のプロジェクトで共通的に利用することがある
- リソースを更新することがほとんどなく、手動で作成したほうが楽
などの理由で、コード化していません。
AWS ChatBotを設定してSlackに通知
最初にAWS ChatBotを利用して、SlackにSNSから通知を簡単に飛ばせるようにします。
マネジメントコンソールで、Chatbotのページに行き、チャットクライアントを設定で、チャットクライアントにSlackを選択し、クライアントを設定を実行します。
すると、以下のように、SlackにAWSからのアクセス権限を許可するかどうかの画面が表示されるので、許可するを選択します。
これで、SlackのワークスペースがChatbotに追加されました。次に、通知先のチャネルを設定します。
新しいチャネルを設定からSlackチャネルを設定画面を開き、以下のように設定します
- 設定名:適当な名前を付けてください
- チャネルタイプ:通知先のSlackのチャネルがプライベートかパブリックか選択します。今回はパブリックなチャネルに通知します
- パブリックチャネル名:通知先のSlackのチャネルを選択します。今回はaws通知というパブリックなチャンネルを作成して選択します
- IAM ロール:テンプレートを使用してIAMロールを作成するを選択します。すでにIAMロールがあるのであればそれを利用してください
- ロール名:適当に名前を付けてください
これで前準備は終了です。
AWS CodeCommitにリポジトリを作成する
次にCodeCommitにリポジトリを作成します。GitHubから直接CodePipelineに連携しても良いのですが、私の場合、委託案件でAWSを利用することが多く、必然的にお客様のAWS環境に構築することが多いので、GitHubと、CodeCommitをミラーリングするように設定し、開発用のGitHubを更新すると、そのまま、お客様のAWS環境に同期して、そのままお客様にソースコードが渡るようにしておくことで、納品時の手間を減らしています。
マネジメントコンソールでCodeCommit開き、リポジトリを作成から、リポジトリを作成します。リポジトリ名はわかりやすい名前をつけてください(私は、いつも、後述するGitHubのリポジトリ名と同じにしています)
パスフレーズなし秘密鍵を作成する
手元のローカル環境で、以下のコマンドで秘密鍵を作成します。
ssh-keygen -t rsa -b 4096 -m PEM -C <githubアカウントメールアドレス>
IAMユーザーのAWS CodeCommit の SSH キーに公開鍵を登録
次に作成した公開鍵を、自分のIAMユーザーに登録します。
マネジメントコンソールでIAMから、自分のIAMを選択し、認証情報タブから、SSHパブリックキーのアップロードを選択して、先ほど作成した公開鍵(.pubがついているほう)の中身をコピーして貼り付けます。
GitHubのSecretsに秘密鍵と、SSHキーIDを登録する
次に、ミラーリングしたいGitHubのリポジトリのSecretsに以下を追加します。
- CODECOMMITSSHPRIVATE_KEY:秘密鍵の中身
- CODECOMMITSSHPRIVATEKEYID:SSH ID(IAMに公開鍵を追加すると取得できる
GitHub Actionを設定
.github/workflows/にmain.ymlを作成し、以下のように記述します。
name: Mirroring
on: [ push, delete ]
jobs:
to_codecommit:
runs-on: ubuntu-18.04
steps:
- uses: actions/checkout@v1
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url:
ssh://git-codecommit.<somewhere>.amazonaws.com/v1/repos/<target_repository_name>
ssh_private_key:
${{ secrets.CODECOMMIT_SSH_PRIVATE_KEY }}
ssh_username:
${{ secrets.CODECOMMIT_SSH_PRIVATE_KEY_ID }}
なお、<somewhere>, <targetrepositoryname>に関しては、ミラーリング先のCodeCommitに合わせて修正してください。
これでミラーリングが完了です。GitHubにpushすると、自動的にCodeCommitのミラーリングされます。
yamlファイル群を作成する
次に、CloudFormationをCI/CDするCodePipelineのCloudFormationのyamlファイルと、CI/CDで構築されるCloudFormationのyamlファイルを作成します(ややこしくてすいません)。
どちらもまとめて同じリポジトリで管理しておいた方が楽なので、以下のようなリポジトリ構成にします。
ファイルは以下のような内容になっています。
ci-cd.yml
CloudFormationをCI/CDするCodePipelineのCloudFormationのyamlファイルです。なかでは、シンプルに、CodeCommitを起点として、変更セットを作成して、承認、デプロイするようになっています。
AWSTemplateFormatVersion: 2010-09-09
Description: cfs CI/CD Pipeline
Parameters:
PJPrefix:
Type: String
RepositoryName:
Type: String
Default: aws-cfn-template
Description: aws codecommit repository name
ChatBotArn:
Type: String
Description: AWS ChatBot ARN
StackConfig:
Type: String
Default: param.json
TemplateFilePath:
Type: String
Default: packaged.yml
Metadata:
AWS::CloudFormation::Interface:
ParameterGroups:
- Label:
default: CodePipeline Configuration
Parameters:
- PJPrefix
- RepositoryName
- ChatBotArn
- StackConfig
- TemplateFilePath
ParameterLabels:
PJPrefix:
default: Project Prefix
RepositoryName:
default: CodeCommit repository name
ChatBotArn:
default: Slack Notification Chatbot
StackConfig:
default: Stack Configuration
TemplateFilePath:
default: Template File Path
Resources:
ArtifactStoreBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Join [ '-', [ !Ref PJPrefix, 'infra-artifacts-bucket' ] ]
LifecycleConfiguration:
Rules:
- Id: !Join [ '-', [ !Ref PJPrefix, 'infra-artifacts-bucket', 'life-cycle-rule' ] ]
Status: Enabled
ExpirationInDays: 14
CodeBuildBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Join [ '-', [ !Ref PJPrefix, 'infra-code-build-bucket' ] ]
LifecycleConfiguration:
Rules:
- Id: !Join [ '-', [ !Ref PJPrefix, 'infra-code-build-bucket', 'life-cycle-rule' ] ]
Status: Enabled
ExpirationInDays: 14
CodeBuildRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: codebuild.amazonaws.com
Path: /
Policies:
- PolicyName: CodeBuildAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Sid: CloudWatchLogsAccess
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Resource:
- !Sub arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/codebuild/*
- Sid: S3Access
Effect: Allow
Action:
- s3:PutObject
- s3:GetObject
- s3:GetObjectVersion
Resource:
- !Sub arn:aws:s3:::${ArtifactStoreBucket}
- !Sub arn:aws:s3:::${ArtifactStoreBucket}/*
- !Sub arn:aws:s3:::${CodeBuildBucket}
- !Sub arn:aws:s3:::${CodeBuildBucket}/*
- Sid: CloudFormationAccess
Effect: Allow
Action: cloudformation:ValidateTemplate
Resource: "*"
CodeBuildProject:
Type: AWS::CodeBuild::Project
Properties:
Name: !Join [ '-', [ !Ref PJPrefix, 'infra-code-build' ] ]
ServiceRole: !GetAtt CodeBuildRole.Arn
Artifacts:
Type: CODEPIPELINE
Environment:
Type: LINUX_CONTAINER
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/amazonlinux2-x86_64-standard:3.0
EnvironmentVariables:
- Name: AWS_REGION
Value: !Ref AWS::Region
- Name: S3_BUCKET
Value: !Ref CodeBuildBucket
Source:
Type: CODEPIPELINE
CFnRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
Service: cloudformation.amazonaws.com
Path: /
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AdministratorAccess
PipelineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Action: sts:AssumeRole
Effect: Allow
Principal:
Service: codepipeline.amazonaws.com
Path: /
Policies:
- PolicyName: CodePipelineAccess
PolicyDocument:
Version: 2012-10-17
Statement:
- Sid: S3FullAccess
Effect: Allow
Action: s3:*
Resource:
- !Sub arn:aws:s3:::${ArtifactStoreBucket}
- !Sub arn:aws:s3:::${ArtifactStoreBucket}/*
- Sid: FullAccess
Effect: Allow
Action:
- cloudformation:*
- iam:PassRole
- codecommit:GetRepository
- codecommit:ListBranches
- codecommit:GetUploadArchiveStatus
- codecommit:UploadArchive
- codecommit:CancelUploadArchive
- codecommit:GetBranch
- codecommit:GetCommit
Resource: "*"
- Sid: CodeBuildAccess
Effect: Allow
Action:
- codebuild:BatchGetBuilds
- codebuild:StartBuild
Resource: !GetAtt CodeBuildProject.Arn
Pipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
Name: !Join [ '-', [ !Ref PJPrefix, 'infra-code-pipeline' ] ]
RoleArn: !GetAtt PipelineRole.Arn
ArtifactStore:
Type: S3
Location: !Ref ArtifactStoreBucket
Stages:
- Name: Source
Actions:
- Name: download-source
ActionTypeId:
Category: Source
Owner: AWS
Version: 1
Provider: CodeCommit
Configuration:
RepositoryName: !Ref RepositoryName
BranchName: main
OutputArtifacts:
- Name: SourceOutput
- Name: Test
Actions:
- InputArtifacts:
- Name: SourceOutput
Name: testing
ActionTypeId:
Category: Test
Owner: AWS
Version: 1
Provider: CodeBuild
OutputArtifacts:
- Name: TestOutput
Configuration:
ProjectName: !Ref CodeBuildProject
- Name: Build
Actions:
- InputArtifacts:
- Name: TestOutput
Name: create-changeset
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CloudFormation
OutputArtifacts:
- Name: BuildOutput
Configuration:
ActionMode: CHANGE_SET_REPLACE
ChangeSetName: changeset
RoleArn: !GetAtt CFnRole.Arn
Capabilities: CAPABILITY_IAM
StackName: !Join [ '-', [ !Ref PJPrefix, 'infra-stack' ] ]
TemplatePath: !Sub TestOutput::${TemplateFilePath}
TemplateConfiguration: !Sub TestOutput::${StackConfig}
- Name: Approval
Actions:
- Name: approve-changeset
ActionTypeId:
Category: Approval
Owner: AWS
Version: 1
Provider: Manual
- Name: Deploy
Actions:
- Name: execute-changeset
ActionTypeId:
Category: Deploy
Owner: AWS
Version: 1
Provider: CloudFormation
Configuration:
StackName: !Join [ '-', [ !Ref PJPrefix, 'infra-stack' ] ]
ActionMode: CHANGE_SET_EXECUTE
ChangeSetName: changeset
RoleArn: !GetAtt CFnRole.Arn
PipelineNotificationRule:
Type: AWS::CodeStarNotifications::NotificationRule
Properties:
Name: !Join [ '-', [ !Ref PJPrefix, 'infra-stack-pipeline-notification-rule' ] ]
DetailType: FULL
Resource: !Join [ '', [ 'arn:aws:codepipeline:', !Ref 'AWS::Region', ':', !Ref 'AWS::AccountId', ':', !Ref Pipeline ] ]
EventTypeIds:
- codepipeline-pipeline-pipeline-execution-succeeded
- codepipeline-pipeline-pipeline-execution-failed
- codepipeline-pipeline-pipeline-execution-canceled
- codepipeline-pipeline-manual-approval-needed
Targets:
-
TargetType: AWSChatbotSlack
TargetAddress: !Ref ChatBotArn
Outputs:
Pipeline:
Value:
Ref: Pipeline
buildspec.yml
version: 0.1
phases:
install:
commands:
- |
pip install -U pip
pip install -r requirements.txt
pre_build:
commands:
- |
[ -d .cfn ] || mkdir .cfn
aws configure set default.region $AWS_REGION
for template in src/* cfn.yml; do
echo "$template" | xargs -I% -t aws cloudformation validate-template --template-body file://%
done
build:
commands:
- |
aws cloudformation package \
--template-file cfn.yml \
--s3-bucket $S3_BUCKET \
--output-template-file .cfn/packaged.yml
artifacts:
files:
- .cfn/*
- params/*
discard-paths: yes
requirements.txt
awscli>=1.11.61
cfn.yml
CI/CDで構築されるCloudFormationのyamlファイルです。今回は、Webアプリなど複雑なことはせずに、S3のバケットをつくるだけです。一度、この仕組みを構築した以降は、このファイルを修正して、GitHubにpushすると、環境が自動的に更新されていくイメージです。
AWSTemplateFormatVersion: 2010-09-09
Description: CloudFormation Main Stack
Parameters:
PJPrefix:
Type: String
Description: Abbreviation for the project (alphanumeric)
AllowedPattern: "[0-9a-zA-Z\\-\\_]+"
Resources:
DeploymentBucket:
Type: 'AWS::S3::Bucket'
DeletionPolicy: Retain
Properties:
BucketName: !Join [ '-', [ !Ref PJPrefix, 'deployment-bucket' ] ]
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
param.json
cfn.ymlのパラメータを定義するjsonファイルです。
{
"Parameters": {
"PJPrefix": "sample"
}
}
CloudFormationのスタックを作成する
マネジメントコンソールでCloudFormationを開き、スタックの作成から、ci-cd.ymlをテンプレートファイルのアップロードでアップロードし、スタックを作成すると、CodePipelineが作成、実行されAWSのリソースが自動的に生成されます。
終わりに
以上で、AWSのリソースのCloudFormationの定義ファイルをGitHubで管理して、pushをトリガとして自動的に更新する仕組みが完成しました。なお、実際の用途では、さらに、検証用環境と、本番環境を分けたり、色々他にも実施することはあります。
参考文献
- GitHubリポジトリをCodeCommitリポジトリにミラーリング(連携)する!
- GitHub/CodeBuild/CodePipelineを利用してCloudFormationのCI/CDパイプラインを構築する