2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Angular×AWSのCI/CD術

Last updated at Posted at 2021-10-01

はじめに

以前、AngularとAWSでアプリをデプロイする方法についての記事を書きましたが、最近の開発だとCI/CDはほぼ必須と言えるほど重要視されています。
ということで今回はCI/CDの実現方法の一つを書いていこうと思います。

目標とするアーキテクチャ

codepipeline.png
ユーザーがGitHubにソースをpushするとCodePipelineが実行されCodeBuildでビルドし、成果物をCloudFrontが見ているS3に配置するというアーキテクチャを目標とします。

codestar-connection登録

今回、GitHubから取得するのにcodestar-connectionを使います。
settings.PNG
Codepipelineの設定から接続を選択し、接続名とプロバイダーを選択します。
ここでGitHubに接続するを押下するとGitHubのログイン画面が開くのでログインします。
settings2.PNG
上記画面が表示されるので新しいアプリをインストールするを押下します。
add_app - コピー.PNG
GitHubの画面が開きアプリをGitHub側でAWSから接続するためのアプリをインストールします。
ここで、repositoryを制限したい場合にはOnly select repositoriesでrepositoryを制限します。

※GitHub側でAWSに接続するアプリは一つしか設定できないのでAWSで使用するrepositoryはすべて登録しておきます。
戻った画面で接続ボタンを押下するとcodestar-connectionが作られるのでそのARNをメモしておきます。

CodeBuildのためのbuildspecを作成

AngularのビルドをCodeBuildで行うためのbuildspecを記載していきます。
全容は下記になります。

buildspec.yml
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のテンプレートで行っていきます。

パラメータ設定

template.yaml
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バケット

template.yaml
Resource:
  ArtifactStoreBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub ${AWS::StackName}-${AWS::Region}-cf-artirfactstore

codepipelineで成果物を受け渡すための入れ物としてS3バケットが必要となるのではじめに用意します。

Codepipeline作成

template.yaml
  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

template.yaml
  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周りの設定
template.yaml
  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
# 全体 前回の記事の内容を含めたテンプレートの全容です。
テンプレート全体
template.yml
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のテンプレートで記載してみましたが、環境を複数作ったり同じテンプレートを使いまわすという要件がない限りここまでテンプレート化するのはコスパが悪い気がしました。
ただそれぞれで何をやっているかという勉強には丁度いいと思うので是非お時間があればお試しいただければと思います。

参考

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?