はじめに
以前、AngularとAWSでアプリをデプロイする方法についての記事を書きましたが、最近の開発だとCI/CDはほぼ必須と言えるほど重要視されています。
ということで今回はCI/CDの実現方法の一つを書いていこうと思います。
目標とするアーキテクチャ
ユーザーがGitHubにソースをpushするとCodePipelineが実行されCodeBuildでビルドし、成果物をCloudFrontが見ているS3に配置するというアーキテクチャを目標とします。
codestar-connection登録
今回、GitHubから取得するのにcodestar-connectionを使います。
Codepipelineの設定から接続を選択し、接続名とプロバイダーを選択します。
ここでGitHubに接続するを押下するとGitHubのログイン画面が開くのでログインします。
上記画面が表示されるので新しいアプリをインストールするを押下します。
GitHubの画面が開きアプリをGitHub側でAWSから接続するためのアプリをインストールします。
ここで、repositoryを制限したい場合にはOnly select repositoriesでrepositoryを制限します。
※GitHub側でAWSに接続するアプリは一つしか設定できないのでAWSで使用するrepositoryはすべて登録しておきます。
戻った画面で接続ボタンを押下するとcodestar-connectionが作られるのでそのARNをメモしておきます。
CodeBuildのためのbuildspecを作成
AngularのビルドをCodeBuildで行うためのbuildspecを記載していきます。
全容は下記になります。
version: 0.2
phases:
install:
runtime-versions:
nodejs: 12
pre_build:
commands:
- npm ci
- npm run ng test
build:
commands:
- npm run ng build -- --c ${environment}
artifacts:
files:
- 'dist/hello-world/*'
name: app-$(date +%Y-%m-%d)
discard-paths: yes
まずinstall部分でビルド環境を指定します。
今回はnodejs 12が入っている環境を指定します。
install:
runtime-versions:
nodejs: 12
ビルド前にパッケージをインストールする必要があるためpre_buildフェーズでnpm ciおよびテストもビルド前に実行します。
pre_build:
commands:
- npm ci
- npm run ng test
ビルドの引数は環境によって異なることが多いので変数化しておきます。
build:
commands:
- npm run ng build -- --c ${environment}
成果物はdist/プロジェクト名/配下に入るのでパスをartifactで指定します。
artifacts:
files:
- 'dist/hello-world/*'
name: app-$(date +%Y-%m-%d)
discard-paths: yes
このbuildspec.ymlはプロジェクトのルートディレクトリに配置しておきます。
Template作成
今回もリソースの作成はCloudFormationのテンプレートで行っていきます。
パラメータ設定
Parameters:
CodeStarConnectionArn:
Type: String
Description: "repogitory connection"
RepositoryName:
Type: String
BranchName:
Type: String
BuildEnv:
Type: String
Description: "build parameter"
パラメータには下記の4つを設定します。
パラメータ名 | 説明 |
---|---|
CodeStarConnectionArn | codestar-connectionのARN |
RepositoryName | ソースが入っているRepository名 |
BranchName | ソースが入っているブランチ名 |
BuildEnv | 環境名(今回はAngularのビルド引数として利用) |
BuildEnvはこれまでの手順で作成したbuildspec.ymlの環境変数に使われます。 | |
※ブランチ名などは環境名のパラメータからMappingsで引いた方がいい場合もあります。。 |
S3バケット
Resource:
ArtifactStoreBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${AWS::StackName}-${AWS::Region}-cf-artirfactstore
codepipelineで成果物を受け渡すための入れ物としてS3バケットが必要となるのではじめに用意します。
Codepipeline作成
AppPipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
RoleArn: !GetAtt PipelineRole.Arn
ArtifactStore:
Location: !Ref ArtifactStoreBucket
Type: S3
Stages:
- Name: Source
Actions:
- Name: download-source
ActionTypeId:
Category: Source
Owner: AWS
Version: 1
Provider: CodeStarSourceConnection
Configuration:
ConnectionArn: !Ref CodeStarConnectionArn
FullRepositoryId: !Ref RepositoryName
BranchName: !Ref BranchName
OutputArtifacts:
- Name: SourceObject
- Name: Build
Actions:
- Name: CodeBuild
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
Configuration:
ProjectName: !Ref AppBuild
InputArtifacts:
- Name: SourceObject
OutputArtifacts:
- Name: BuildObject
- Name: Deploy
Actions:
- Name: Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: S3
Version: 1
Configuration:
BucketName: !Ref AngularBucket
Extract: true
ObjectKey: app
InputArtifacts:
- Name: BuildObject
ここではCodepipelineの一連の流れを記述しています。
Githubで取得したソースをSourceObjectとしてBuildActionに渡しAppBuild(後述)でアプリのビルドを行い成果物をDeployActionでS3に配置しています。
CodeBuild
AppBuild:
Type: AWS::CodeBuild::Project
Properties:
Artifacts:
Type: CODEPIPELINE
Environment:
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:5.0-21.04.23
Type: LINUX_CONTAINER
EnvironmentVariables:
- Name: environment
Value: !Ref BuildEnv
ServiceRole: !GetAtt BuildRole.Arn
Name: Angular_Build
Source:
Type: CODEPIPELINE
ビルドの詳細はソース上のbuildspec.ymlに書いてあるのでここはシンプルな記載となります。
buildspec.ymlで指定している環境変数名をenvironmentとしていたのでenvironmentという変数名でBuildEnvの中身を設定しています。
Role周り
Role周りは記事の主旨と異なるので割愛して今回設定したテンプレートのみ記載します。
Role周りの設定
BuildRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Version: '2012-10-17'
Path: "/"
Policies:
- PolicyDocument:
Statement:
- Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Effect: Allow
Resource: arn:aws:logs:*:*:*
Version: '2012-10-17'
PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSCodeBuildLogPolicy-CW
- PolicyDocument:
Statement:
- Action:
- s3:*
Effect: Allow
Resource:
- !Sub arn:aws:s3:::${AngularBucket}/*
- !Sub arn:aws:s3:::${AngularBucket}
- !Sub arn:aws:s3:::${ArtifactStoreBucket}/*
- !Sub arn:aws:s3:::${ArtifactStoreBucket}
- Action:
- codebuild:*
- lambda:InvokeFunction
Effect: Allow
Resource:
- '*'
Version: '2012-10-17'
PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-S3
RoleName: !Sub ${AWS::StackName}-${AWS::Region}-AWSCodeBuildPolicy-CW
PipelineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
Version: '2012-10-17'
Path: "/"
Policies:
- PolicyDocument:
Statement:
- Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Effect: Allow
Resource: arn:aws:logs:*:*:*
Version: '2012-10-17'
PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSCodePipelineLogPolicy-CW
- PolicyDocument:
Statement:
- Action:
- s3:*
Effect: Allow
Resource:
- !Sub arn:aws:s3:::${AngularBucket}/*
- !Sub arn:aws:s3:::${AngularBucket}
- !Sub arn:aws:s3:::${ArtifactStoreBucket}/*
- !Sub arn:aws:s3:::${ArtifactStoreBucket}
- Action:
- codebuild:*
Effect: Allow
Resource:
- '*'
- Action:
- codestar-connections:GetConnection
Effect: Allow
Resource:
- '*'
Version: '2012-10-17'
PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-S3
RoleName: !Sub ${AWS::StackName}-${AWS::Region}-AWSCodePipelinePolicy-CW
テンプレート全体
Parameters:
AngularBucketName:
Type: String
Default: "angular-app-bucket"
Description: "S3 bucket to Create"
AllowedPattern: "[a-zA-Z][a-zA-Z0-9_-]*"
DirName:
Type: String
Default: "app"
Description: "S3 bucket to Create"
AllowedPattern: "[a-zA-Z][a-zA-Z0-9_-]*"
CodeStarConnectionArn:
Type: String
Description: "repogitory connection"
RepositoryName:
Type: String
BranchName:
Type: String
BuildEnv:
Type: String
Description: "build parameter"
Resources:
AngularBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref AngularBucketName
AngularBucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref AngularBucket
PolicyDocument:
Statement:
- Action: s3:GetObject
Effect: Allow
Resource: !Sub arn:aws:s3:::${AngularBucket}/${DirName}/*
Principal:
AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}
AngularDir:
Type: Custom::S3CustomResource
Properties:
ServiceToken: !GetAtt AWSLambdaFunction.Arn
the_bucket: !Ref AngularBucket
dir_to_create: !Ref DirName
AWSLambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Description: "Work with S3 bucket"
FunctionName: !Sub '${AWS::StackName}-${AWS::Region}-lambda'
Handler: index.handler
Role: !GetAtt AWSLambdaExecutionRole.Arn
Timeout: 360
Runtime: python3.6
Code:
ZipFile: |
import boto3
import cfnresponse
def handler(event, context):
# Init ...
the_event = event['RequestType']
print("The event is: ", str(the_event))
response_data = {}
s_3 = boto3.client('s3')
# Retrieve parameters
the_bucket = event['ResourceProperties']['the_bucket']
dir_to_create = event['ResourceProperties']['dir_to_create']
try:
if the_event in ('Create', 'Update'):
print("Requested folders: ", str(dir_to_create))
print("Creating: ", str(dir_to_create))
s_3.put_object(Bucket=the_bucket, Key=(dir_to_create + '/'))
elif the_event == 'Delete':
print("Deleting S3 content")
b_operator = boto3.resource('s3')
b_operator.Bucket(str(the_bucket)).objects.all().delete()
print("Operation successfull")
cfnresponse.send(event,context,cfnresponse.SUCCESS, response_data)
except Exception as e:
print("Operation failed...")
print(str(e))
response_data['Data'] = str(e)
cfnresponse.send(event, context, cfnresponse.FAILED, response_data)
AWSLambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Version: '2012-10-17'
Path: "/"
Policies:
- PolicyDocument:
Statement:
- Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Effect: Allow
Resource: arn:aws:logs:*:*:*
Version: '2012-10-17'
PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-CW
- PolicyDocument:
Statement:
- Action:
- s3:PutObject
- s3:DeleteObject
- s3:List*
Effect: Allow
Resource:
- !Sub arn:aws:s3:::${AngularBucket}/*
- !Sub arn:aws:s3:::${AngularBucket}
Version: '2012-10-17'
PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-S3
RoleName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambdaExecutionRole
CloudForntDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Origins:
- DomainName: !GetAtt AngularBucket.DomainName
Id: AngularOrigin
OriginPath: !Sub /${DirName}
S3OriginConfig:
OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}
Enabled: 'true'
DefaultRootObject: index.html
DefaultCacheBehavior:
AllowedMethods:
- GET
- HEAD
TargetOriginId: AngularOrigin
ForwardedValues:
QueryString: 'false'
ViewerProtocolPolicy: allow-all
PriceClass: PriceClass_200
CloudFrontOriginAccessIdentity:
Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
Properties:
CloudFrontOriginAccessIdentityConfig:
Comment: !Ref AWS::StackName
AppBuild:
Type: AWS::CodeBuild::Project
Properties:
Artifacts:
Type: CODEPIPELINE
Environment:
ComputeType: BUILD_GENERAL1_SMALL
Image: aws/codebuild/standard:5.0-21.04.23
Type: LINUX_CONTAINER
EnvironmentVariables:
- Name: environment
Value: !Ref BuildEnv
ServiceRole: !GetAtt BuildRole.Arn
Name: Angular_Build
Source:
Type: CODEPIPELINE
AppPipeline:
Type: AWS::CodePipeline::Pipeline
Properties:
RoleArn: !GetAtt PipelineRole.Arn
ArtifactStore:
Location: !Ref ArtifactStoreBucket
Type: S3
Stages:
- Name: Source
Actions:
- Name: download-source
ActionTypeId:
Category: Source
Owner: AWS
Version: 1
Provider: CodeStarSourceConnection
Configuration:
ConnectionArn: !Ref CodeStarConnectionArn
FullRepositoryId: !Ref RepositoryName
BranchName: !Ref BranchName
OutputArtifacts:
- Name: SourceObject
- Name: Build
Actions:
- Name: CodeBuild
ActionTypeId:
Category: Build
Owner: AWS
Version: 1
Provider: CodeBuild
Configuration:
ProjectName: !Ref AppBuild
InputArtifacts:
- Name: SourceObject
OutputArtifacts:
- Name: BuildObject
- Name: Deploy
Actions:
- Name: Deploy
ActionTypeId:
Category: Deploy
Owner: AWS
Provider: S3
Version: 1
Configuration:
BucketName: !Ref AngularBucket
Extract: true
ObjectKey: app
InputArtifacts:
- Name: BuildObject
BuildRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Principal:
Service:
- codebuild.amazonaws.com
Version: '2012-10-17'
Path: "/"
Policies:
- PolicyDocument:
Statement:
- Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Effect: Allow
Resource: arn:aws:logs:*:*:*
Version: '2012-10-17'
PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSCodeBuildLogPolicy-CW
- PolicyDocument:
Statement:
- Action:
- s3:*
Effect: Allow
Resource:
- !Sub arn:aws:s3:::${AngularBucket}/*
- !Sub arn:aws:s3:::${AngularBucket}
- !Sub arn:aws:s3:::${ArtifactStoreBucket}/*
- !Sub arn:aws:s3:::${ArtifactStoreBucket}
- Action:
- codebuild:*
- lambda:InvokeFunction
Effect: Allow
Resource:
- '*'
Version: '2012-10-17'
PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-S3
RoleName: !Sub ${AWS::StackName}-${AWS::Region}-AWSCodeBuildPolicy-CW
ArtifactStoreBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub ${AWS::StackName}-${AWS::Region}-cf-artirfactstore
PipelineRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Statement:
- Action:
- sts:AssumeRole
Effect: Allow
Principal:
Service:
- codepipeline.amazonaws.com
Version: '2012-10-17'
Path: "/"
Policies:
- PolicyDocument:
Statement:
- Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
Effect: Allow
Resource: arn:aws:logs:*:*:*
Version: '2012-10-17'
PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSCodePipelineLogPolicy-CW
- PolicyDocument:
Statement:
- Action:
- s3:*
Effect: Allow
Resource:
- !Sub arn:aws:s3:::${AngularBucket}/*
- !Sub arn:aws:s3:::${AngularBucket}
- !Sub arn:aws:s3:::${ArtifactStoreBucket}/*
- !Sub arn:aws:s3:::${ArtifactStoreBucket}
- Action:
- codebuild:*
Effect: Allow
Resource:
- '*'
- Action:
- codestar-connections:GetConnection
Effect: Allow
Resource:
- '*'
Version: '2012-10-17'
PolicyName: !Sub ${AWS::StackName}-${AWS::Region}-AWSLambda-S3
RoleName: !Sub ${AWS::StackName}-${AWS::Region}-AWSCodePipelinePolicy-CW
こちらをCloudFormationのテンプレートに登録するとCIを含めたAngularのデプロイが出来ると思いますので是非お試しいただければと思います。
※必要であればCloudFrontのInvalidationのLambdaを最後に呼ぶとよりよいと思います。
おわりに
今回CIを含めたリソースの作成を全てCloudFormationのテンプレートで記載してみましたが、環境を複数作ったり同じテンプレートを使いまわすという要件がない限りここまでテンプレート化するのはコスパが悪い気がしました。
ただそれぞれで何をやっているかという勉強には丁度いいと思うので是非お時間があればお試しいただければと思います。