CI/CDハンズオンの環境をCloudFormationで作成

# はじめに
CI/CDの勉強を行うにあたって、以下のハンズオンを行いました。

https://pages.awscloud.com/JAPAN-event-OE-Hands-on-for-Beginners-cicd-2020-reg-event-LP.html?trk=aws_blog_k

ハンズオンではコンソール操作で作成していましたが、CFnで作成してみようと思いました。
ハンズオンは以下の二つがあります。
- S3 をデプロイ先とした CI/CD 環境の構築【AWS CodeCommit, AWS CodePipeline を利用】
- EC2 インスタンスをデプロイ先とした CI/CD 環境の構築【AWS CodeCommit, AWS CodeBuild, AWS CodeDeploy, AWS CodePipeline を利用】

今回CFnで作成した対象は<b>S3 をデプロイ先とした CI/CD 環境の構築【AWS CodeCommit, AWS CodePipeline を利用】</b>のみです。



結果として、見えないところで色々なリソースが作られていることがわかりました。


# 参考

https://aws.amazon.com/jp/blogs/news/aws-hands-on-for-beginners-10/

https://qiita.com/mechamogera/items/a8742a040ad1fb0c3287

https://qiita.com/is_ryo/items/0382d183f514e0d06f4d

https://dev.classmethod.jp/articles/codepipeline-s3-deploy-cfn-web/


# 構成図

## 思っていたの
公式では以下の図を用いていました。（上の方のパイプラインになります）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/95a72089-c051-3daa-7e3a-c6db5e073fb1.png)

## 実際
今回作成したのは以下のようになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/48fc49da-ace7-e182-3dd9-346e62f66d80.png)

## さらに
ハンズオン（コンソールの操作）で作成されるパイプラインは、以下の様になるようです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/02aa373a-cdb5-4a7e-90db-821812a95de3.png)


# やったこと
## 環境
コンソールからの操作のみになります。

## 事前準備

### 初期ソースの用意
- 任意のS3にZip圧縮したソースをアップ
    - CodeCommit作成時に、ブランチ名とS3のオブジェクトを指定しておくと、そのファイルがコミットされた状態で作成されます。

#### 対象のソース
```html:index.html
<!DOCTYPE html>

<html lang="ja">
<head>
  <meta charset="utf-8">
  <title>S3 Static Web Hosting</title>
</head>
<body>
  Hello, AWS World!!
</body>
</html>
```
#### バケットの用意
任意のバケットでよいです。

#### Zip圧縮したHTMLファイルのアップロード
HTMLファイルをZipで固めて、先のバケットにアップロードしておきます。


## パイプラインの作成（CodeCommitの変更を直接検知）
以下のCFnを実行するだけです。
<details><summary>クリックで表示</summary><div>

```yaml
AWSTemplateFormatVersion: 2010-09-09

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Parameters:
          - RepositoryName
          - TargetBrranch
          - MySourceCodeBucket
          - MyKey
          - AllowedIps
          - PipelineName
Parameters:
  RepositoryName:
    Type: String
    Default: "repo20220926"
  TargetBrranch:
    Type: String
    Default: "main"
  MySourceCodeBucket:
    Type: String
  MyKey:
    Type: String
    Default: "index.zip"
  AllowedIps:
    Description: IP list to allow access.
    Type: CommaDelimitedList
    Default: "1.1.1.1/32, 2.2.2.2/32"
  PipelineName:
    Type: String
    Default: "pipeline20220926"


Resources:
  TargetRepository:
    Type: AWS::CodeCommit::Repository
    Properties:
      RepositoryName: !Ref RepositoryName
      Code:
        BranchName: !Ref TargetBrranch
        S3:
          Bucket: !Ref MySourceCodeBucket
          Key: !Ref MyKey

  TargetBucket:
    Type: AWS::S3::Bucket
    Properties:
      WebsiteConfiguration:
        IndexDocument: index.html
  TargetBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref TargetBucket
      PolicyDocument:
        Statement:
          - Effect: Allow
            Principal: '*'
            Action:
              - 's3:GetObject'
            Resource: !Sub arn:aws:s3:::${TargetBucket}/*
            Condition:
              IpAddress:
                'aws:SourceIp': !Ref AllowedIps

  # CodePipeline resoure
  ## Bucket for Artifactstore
  ArtifactstoreBucket:
    Type: AWS::S3::Bucket
    Properties:
      PublicAccessBlockConfiguration:
        BlockPublicAcls: TRUE
        BlockPublicPolicy: TRUE
        IgnorePublicAcls: TRUE
        RestrictPublicBuckets: TRUE
  ## IAM ROLE FOR Codepipeline
  PipelineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
         -  Effect: Allow
            Principal:
              Service:
               -  codepipeline.amazonaws.com
            Action:
             -  sts:AssumeRole
      Policies:
       -  PolicyName: !Sub AWSCodePipelineServiceRole-${AWS::Region}-${PipelineName}
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
             -  Effect: Allow
                Action:
                 - s3:*Object
                 - codecommit:UploadArchive
                 - codecommit:GetCommit
                 - codecommit:GetUploadArchiveStatus
                 - codecommit:GetRepository
                 - codecommit:GetBranch
                 - codecommit:CancelUploadArchive
                Resource:
                 - !GetAtt TargetRepository.Arn
                 - !Sub arn:aws:s3:::${TargetBucket}/*
                 - !Sub arn:aws:s3:::${ArtifactstoreBucket}/*
  ## Codepipeline
  ContentsPipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties: 
      RoleArn: !GetAtt PipelineRole.Arn
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactstoreBucket
      Stages:
       -  Name: Source
          Actions:
           -  Name: Source
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeCommit
              Configuration: 
                RepositoryName: !Ref RepositoryName
                BranchName: !Ref TargetBrranch
                PollForSourceChanges: true
              RunOrder: 1
              OutputArtifacts:
                -  Name: SourceArtifact
       -  Name: Deploy
          Actions:
           -  Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: 1
                Provider: S3
              InputArtifacts:
                -  Name: SourceArtifact
              Configuration: 
                BucketName: !Ref TargetBucket
                Extract: true
              RunOrder: 1

Outputs:
    TargetURL:
        Value: !Sub http://${TargetBucket}.s3-website-${AWS::Region}.amazonaws.com
```
</div></details>

パラメータとして以下を指定します。
- 作成するCodeCommitのリポジトリ名
    - 対象のブランチ
- 最初にPUSHする（Zip圧縮された）コードファイルが置いてあるバケット名
    - ファイルのパス（Key)
- 静的ホスティングサイトとして許可するIP群
- パイプラインの名称


### 解説
コードパイプラインのところに癖があったので説明します
#### パイプライン起動のトリガー


```yaml:一つ目のステージのConfiguration
              Configuration: 
                RepositoryName: !Ref RepositoryName
                BranchName: !Ref TargetBrranch
                PollForSourceChanges: true
```
この`PollForSourceChanges`は以下の様に動作する模様です。
- true：対象のブランチにPUSHしたら、パイプラインが起動
- false：PUSHしても、起動しない
    - 別途、EventBridge等から起動させる場合に使う模様
        - ハンズオンにて作成される（＝コンソールで作成する）環境は、falseになっており、EventBridgeから起動している

詳細は公式にあります。(<b>Default settings for the PollForSourceChanges parameter</b>の箇所です)

https://docs.aws.amazon.com/codepipeline/latest/userguide/reference-pipeline-structure.html#action-requirements



#### S3にデプロイする際に、展開する指定
```yaml:二つ目のステージのConfiguration
              Configuration: 
                BucketName: !Ref TargetBucket
                Extract: true
```
この`Extract`はfalseだと圧縮されたままアップロードされるようです。詳細は公式に。

https://docs.aws.amazon.com/codepipeline/latest/userguide/action-reference-S3Deploy.html#action-reference-S3Deploy-config

## ページ確認
CloudFormationのOutputsにURLを出力するよう設定したので、CFnの出力タブにあるURLで確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/017330f7-4f21-0258-dd42-1bc4f2743129.png)

## 編集
CodeCommitであれば、コンソールから編集できます。
リポジトリから、index.htmlを選ぶと、編集できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/fedd9a74-1c01-d88c-a692-90e846c1c861.png)

以下の様に変更すると、パイプラインが動き、ページに反映されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/39c6e79b-9f58-0d99-40d4-7e93d71d4e1d.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/4c4a201f-6081-4f37-1dc9-6dbecc8e96cf.png)

実行履歴を見ると、ソースの変更がトリガーになっていることがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/01cc1662-c593-a659-05bd-d93a5e148083.png)


## パイプラインの作成（CodeCommitの変更をEventBridgeが検知）
上記を作成した際に、ハンズオンで作成した環境と異なることに気が付きました。
そのためそれらを比較したところ、EventBridgeがトリガーとなり動いており、そちらもCFnで作成してみました。


<details><summary>クリックで表示</summary><div>

```yaml
AWSTemplateFormatVersion: 2010-09-09

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      - Parameters:
          - RepositoryName
          - TargetBrranch
          - MySourceCodeBucket
          - MyKey
          - AllowedIps
          - PipelineName
Parameters:
  RepositoryName:
    Type: String
    Default: "repo20220928"
  TargetBrranch:
    Type: String
    Default: "main"
  MySourceCodeBucket:
    Type: String
  MyKey:
    Type: String
    Default: "index.zip"
  AllowedIps:
    Description: IP list to allow access.
    Type: CommaDelimitedList
    Default: "1.1.1.1/32, 2.2.2.2/32"
  PipelineName:
    Type: String
    Default: "hoge20220928"


Resources:
  TargetRepository:
    Type: AWS::CodeCommit::Repository
    Properties:
      RepositoryName: !Ref RepositoryName
      Code:
        BranchName: !Ref TargetBrranch
        S3:
          Bucket: !Ref MySourceCodeBucket
          Key: !Ref MyKey

  TargetBucket:
    Type: AWS::S3::Bucket
    Properties:
      WebsiteConfiguration:
        IndexDocument: index.html
  TargetBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref TargetBucket
      PolicyDocument:
        Statement:
          - Effect: Allow
            Principal: '*'
            Action:
              - 's3:GetObject'
            Resource: !Sub arn:aws:s3:::${TargetBucket}/*
            Condition:
              IpAddress:
                'aws:SourceIp': !Ref AllowedIps

  # CodePipeline resoure
  ## Bucket for Artifactstore
  ArtifactstoreBucket:
    Type: AWS::S3::Bucket
    Properties:
      PublicAccessBlockConfiguration:
        BlockPublicAcls: TRUE
        BlockPublicPolicy: TRUE
        IgnorePublicAcls: TRUE
        RestrictPublicBuckets: TRUE
  ## IAM ROLE FOR Codepipeline
  PipelineRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
         -  Effect: Allow
            Principal:
              Service:
               -  codepipeline.amazonaws.com
            Action:
             -  sts:AssumeRole
      Policies:
       -  PolicyName: !Sub AWSCodePipelineServiceRole-${AWS::Region}-${PipelineName}
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
             -  Effect: Allow
                Action:
                 - s3:*Object
                 - codecommit:UploadArchive
                 - codecommit:GetCommit
                 - codecommit:GetUploadArchiveStatus
                 - codecommit:GetRepository
                 - codecommit:GetBranch
                 - codecommit:CancelUploadArchive
                Resource:
                 - !GetAtt TargetRepository.Arn
                 - !Sub arn:aws:s3:::${TargetBucket}/*
                 - !Sub arn:aws:s3:::${ArtifactstoreBucket}/*
  ## Codepipeline
  ContentsPipeline:
    Type: AWS::CodePipeline::Pipeline
    Properties: 
      RoleArn: !GetAtt PipelineRole.Arn
      ArtifactStore:
        Type: S3
        Location: !Ref ArtifactstoreBucket
      Stages:
       -  Name: Source
          Actions:
           -  Name: Source
              ActionTypeId:
                Category: Source
                Owner: AWS
                Version: 1
                Provider: CodeCommit
              Configuration: 
                RepositoryName: !Ref RepositoryName
                BranchName: !Ref TargetBrranch
                PollForSourceChanges: false
              RunOrder: 1
              OutputArtifacts:
                -  Name: SourceArtifact
       -  Name: Deploy
          Actions:
           -  Name: Deploy
              ActionTypeId:
                Category: Deploy
                Owner: AWS
                Version: 1
                Provider: S3
              InputArtifacts:
                -  Name: SourceArtifact
              Configuration: 
                BucketName: !Ref TargetBucket
                Extract: true
              RunOrder: 1

  ## IAM ROLE FOR EventBridge
  EventBridgeRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
         -  Effect: Allow
            Principal:
              Service:
               -  events.amazonaws.com
            Action:
             -  sts:AssumeRole
      Policies:
       -  PolicyName: !Sub AWSCodePipelineServiceRole-${AWS::Region}-${PipelineName}
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
             -  Effect: Allow
                Action:
                 - codepipeline:StartPipelineExecution
                Resource:
                 - !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${ContentsPipeline}

  EventRuleCodeCommit:
    Type: AWS::Events::Rule
    Properties:
      EventBusName: default
      State: ENABLED
      EventPattern:
        source:
         -  aws.codecommit
        detail-type: 
         -  "CodeCommit Repository State Change"
        resources: 
         - !GetAtt TargetRepository.Arn
        detail:
          event:
            - "referenceCreated"
            - "referenceUpdated"
          referenceType:
            - "branch"
          referenceName:
            - !Ref TargetBrranch
      Targets: 
       -  Arn: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${ContentsPipeline}
          Id: !Ref AWS::StackName
          RoleArn: !GetAtt EventBridgeRole.Arn

Outputs:
    TargetURL:
        Value: !Sub http://${TargetBucket}.s3-website-${AWS::Region}.amazonaws.com

```
</div></details>

パラメータは、先のものと一緒です。

違いは、パイプライン起動のトリガーが、CodePipeline内では無効化されている点です。
```diff_yaml:一つ目のステージのConfiguration
              Configuration: 
                RepositoryName: !Ref RepositoryName
                BranchName: !Ref TargetBrranch
-               PollForSourceChanges: true
+               PollForSourceChanges: false
```

EventBridgeについては、以下の公式等を参考にしました。

https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/update-change-detection.html#update-change-detection-cfn

先程と同様に、コードを変更させると反映されます。
履歴にはCloudWatchEvent（EventBridgeの昔の名前？）で動いたことがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/36ddef00-5059-82c0-ad09-870f3181b904.png)



## 片づけ
両方ともCFnで作成したので、削除するのは簡単です。
1. バケットを空にする。
    - 公開用バケット
    - Artifact用バケット
1. 対象のスタックを削除する。

# おわりに
最も簡単（と思われる）なCI/CDパイプライン環境をCFnから作成してみました。
CI/CDの超基礎を勉強しつつ、同時にコンソールがどれだけ良しなに構築しているのか、大変参考になりました。

実業務で使うには簡単すぎる構成なので、もう少し複雑な環境も勉強していけたらと考えています。