Logo
開発全般

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環境は以下の流れです。

  1. GitHubのリポジトリに登録されているCloudFormationのyamlファイルを修正して、push
  2. pushをトリガーにして、CodePipelineを自動的に実行し、CloudFormationの変更セットを自動的に生成
  3. CodePipelineの承認依頼をSlackに通知する
  4. CodePipelineで承認依頼を承認する
  5. CloudFormationの変更セットを適用して、インフラを更新
  6. 更新結果を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をトリガとして自動的に更新する仕組みが完成しました。なお、実際の用途では、さらに、検証用環境と、本番環境を分けたり、色々他にも実施することはあります。

参考文献