CodePipelineで承認ステージ付きのパイプラインを作成する(CloudFormationテンプレート付)

# はじめに
CodePipelineは便利。これをCloudFormationで自動構築して、フットワークの軽いデプロイを実現するぜ！
と言っても、商用リソースまで全自動でデプロイするようなクラウドネイティブに振り切ったシステムばかりではないよね。といった望みに、AWSはしっかり答えてくれている。

今回は、リリース直前に「ちょっと待った」をしてくれる承認ステージについての理解を深めてみる。

# 前提条件
- さすがにCodePipelineの概要くらいは抑えている

ハードル低め。最低限、以下の記事くらいのことができるのが望ましい。
Windowsのあたりは蛇足なので飛ばしても問題ない。
[WindowsのEclipseからCodePipelineを起動してEC2にデプロイ＆ECS on Fargateにアプリをデプロイする](https://qiita.com/neruneruo/items/454feea586febabfe13d)

ただし、↑の記事で扱っているECS on FargateのパイプラインはCloudFormationで書くと少し面倒なので、今回ベースにするのは、以下の記事のLambdaのパイプラインをベースに考えてみる。

[Lambda関数をBlue/GreenデプロイメントするCodePipelineをCloudFormationで自動構築する](https://qiita.com/neruneruo/items/3f2514b92137a43515c0)

ということで、スタート時点では「CodeCommitにPushしたらCodeBuildでビルドしてSAMテンプレートをCloudFormationで実行するパイプライン」があることを前提とする。

# 承認ステージを作って承認してみる
まず、上のパイプラインに承認ステージを追加してみる。

- 編集ボタンを押す
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/a264d1c8-3023-9fa6-6ec1-a086c0ddee0b.png)

- 今回は、デプロイ直前で寸止めをしてみるとして、Buildステージの後に追加をする
![キャプチャ2.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/35eb2650-c924-a1f0-4f12-4557704cac54.png)

- 出てきたダイアログに適当に名前をつける
- ![キャプチャ3.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/d45aca3d-b405-5dde-9873-d36a0f1e2d72.png)

- つけた名前のステージができるので、アクショングループを追加する
![キャプチャ4.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/66cd1664-33bc-e61b-7b09-88b872fcdd8e.png)

- 適当に設定してみる
- アクション名に適当な名前をつける
- アクションプロバイダーは```Mannual approval```を選択
- コメントを適当に入れる
- 完了ボタンを押す
![キャプチャ5.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/291332bd-cfbf-344e-9bcb-9b4016fc6031.png)

- パイプラインを走らせると、先ほど作ったApprovalのステージでちゃんとデプロイが「待った」された！
- ということで、承認をしてみる。
![キャプチャ6.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/f17ce356-c0ef-d76b-f660-03897a5219fb.png)

- 適当にコメントを入れて「承認します」ボタンを押す
![キャプチャ7.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/00d023ca-d3ca-5c9c-35af-ee970b2282e1.png)

- ちゃんとパイプラインが進んだ！
![キャプチャ8.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/5cb8ed85-8f8f-393f-fe56-8ddb6d556a17.png)

# 承認ステージで否認してみる
- 今度は承認画面で「却下します」ボタンを押す
![キャプチャ9.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/96210e04-9226-0297-7796-d66b552c7a51.png)

- ちゃんとパイプラインが停まった！(下の方に出ているのは1回前のデプロイ)
![キャプチャ10.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/9f8ba5c3-6917-d6b3-14e5-4460dd5069cf.png)

# IAMで承認権限のあるIAMユーザのみ承認可能にする
## IAMユーザの準備
以下を参考にしながら。

【AWS公式】[CodePipeline の IAM ユーザーに承認権限を付与する](https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/approvals-iam-permissions.html)

以下のIAMポリシーを持ったマネコンログイン可能なIAMユーザを作成する。
テストなので、管理ポリシーでもインラインポリシーでも何でもよい。
ポイントは```codepipeline:PutApprovalResult```の権限の有無。これがあると、パイプラインの承認ステージで承認が可能になる。

- IAMユーザ名: ```can-approve```

```JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "codepipeline:ListPipelines",
                "codepipeline:GetPipeline",
                "codepipeline:GetPipelineState",
                "codepipeline:GetPipelineExecution",
                "codepipeline:PutApprovalResult"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}
```

- IAMユーザ名: ```cannot-approve```

```JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "codepipeline:ListPipelines",
                "codepipeline:GetPipeline",
                "codepipeline:GetPipelineState",
                "codepipeline:GetPipelineExecution"
            ],
            "Resource": "*",
            "Effect": "Allow"
        }
    ]
}
```

なお、今回はおためしのためにResourceを*で指定しているが、当然ながら実運用をする際は最小権限を意識した方が良い。↑のAWS公式のドキュメントにも記載されているように、リソースを

```JSON
"Resource": "arn:aws:codepipeline:us-east-2:80398EXAMPLE:MyFirstPipeline"
```

でパイプライン単位に絞ることができるし、パイプラインで複数の承認ステージがある場合(承認者が複数いてそれぞれで承認可能なステージを変えたい場合)は

```JSON
"Resource": "arn:aws:codepipeline:us-east-2:80398EXAMPLE:MyFirstPipeline/MyApprovalStage/MyApprovalAction"
```

といった具合に、承認ステージ名や、その中の識別子で絞ることも可能なようだ。

## 権限のないユーザでの承認行為
上記のcannot-approveユーザで承認しようとすると、当然ながらエラーになる。
直前の「承認します」ボタンまでは押せて、そこで↓このエラーが出るので、設定が失敗したのではないかとちょっと心配になった……。
![キャプチャ11.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/9e454984-dc15-82c5-e778-130a0f090eb5.png)

## 権限のあるユーザでの承認行為
can-approveユーザで承認すると、パイプラインは継続した。
「承認された でした」って何やねん……って感じだけど…。
![キャプチャ12.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/fa5f406a-bb78-32d0-cb42-aa39a2fb54e2.png)

# 承認者に対して通知を送る
CodePipelineは、承認ステージに到達するとAmazon SNSと連携する機能があるので、これで承認者に対して通知を行うことができる。

## Amazon SNSの設定
まずはマネコンのAmazon SNSの設定画面に移動し、新規のSNSトピックを作る。
名前や表示名は適当に決めよう。これ以外は、今はひとまずデフォルトのままにしておく。
![キャプチャ13.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/36932f26-49a4-3166-f689-65fc72ef2713.png)

トピックを作ったら、サブスクリプションを設定する。これ以外もデフォルトのままにしておく。
![キャプチャ14.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/d019462e-3835-f526-a523-8fb26e31842c.jpeg)
![キャプチャ14.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/b3541ce4-8821-aef2-d81d-278831ba0b65.png)

サブスクリプションの設定ができたら、EメールのConfirmを行っておく(やっておかないとこの後の通知がされない)

## CodePipeline実行IAMロールに対するIAMポリシの設定
CodePipeline実行に使うIAMロールに、Amazon SNSに対してPublishする権限がないとエラーになってしまう。しかも、権限がなくて実行失敗したときは却下されたかのように見えてエラー解析が大変になってしまうので、忘れず設定しておく。

```JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": "sns:Publish",
            "Resource": "*"
        }
    ]
}
```

## CodePipelineの承認ステージの設定
承認ステージの「SNSトピックのARN」に、↑のSNSトピックを設定すると、承認依頼のメールが配信されるようになる。

![キャプチャ15.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/0a0093dc-f876-fbf3-9add-1cca6f600570.png)

## 承認依頼メール
メールは↓こんな感じで届く。

![キャプチャ16.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/111314/8c8895d6-c512-8a48-9235-23df05956621.png)

リンクを踏むと、パイプラインの詳細画面に遷移するので、そこで普通に承認/否認すれば良い。

# IaCで書く
[前の記事](https://qiita.com/neruneruo/items/3f2514b92137a43515c0)で作ったCloudFormationテンプレートに、以下を追加する。
こちらでは、できる限り最小権限を目指す。
また、CodePipelineのサービスロールと、承認ユーザのIAMユーザはあらかじめ作っておく前提とする(この辺のユーザは、パイプラインが増えても共通であることが多いので、ポリシを付け足していく考え方にする)。承認ユーザには、共通的に使うであろう以下の権限もあらかじめアタッチしておく。

```JSON
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": [
                "codepipeline:ListPipelines"
            ],
            "Resource": [
                "*"
            ],
            "Effect": "Allow"
        }
    ]
}
```

- パラメータ

```YAML
  SNSTopicNameSuffix:
    Description: "SNS Topic Name for Approval stage on CodePipeline"
    Type: "String"
    Default: "-Pipeline-Approval-Request"
```

- SNSトピック/サブスクリプション

トピックにインラインでサブスクリプションを記述すると、削除時に消えなかったような気がするので、サブスクリプションも横着せずリソースとして作成するようにする。

```YAML
  SNSTOPIC:
    Type: AWS::SNS::Topic
    Properties: 
      TopicName: !Sub ${Prefix}${SNSTopicNameSuffix}
      DisplayName: [トピックの表示名(メールのSubject)]

  SNSSUBSCRIPTION:
    Type: AWS::SNS::Subscription
    Properties:
      Protocol: email
      Endpoint: [通知メールの宛先]
      TopicArn: !Ref SNSTOPIC
```

- CodePipelineのサービスロールに付与するIAMポリシ
 
```YAML
  CODEPIPELINEIAMPOLYCY:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: CodePipeline-SNS-Publish
      Roles:
        - CodePipelineRole
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: !Sub ${Prefix}AmazonSNSPublish
            Effect: Allow
            Action:
              - "sns:Publish"
            Resource: !Ref SNSTOPIC
```

- 承認権限を持ったIAMユーザに付与するIAMポリシ

```YAML
  APPROVALIAMPOLYCY:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: CodePipeline-Approval
      Users:
        - can-approve
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Sid: !Sub ${Prefix}CodePipelineAccess
            Effect: Allow
            Action:
              - "codepipeline:GetPipeline"
              - "codepipeline:GetPipelineState"
              - "codepipeline:GetPipelineExecution"
            Resource: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Prefix}${PipelineNameSuffix}
          - Sid: !Sub ${Prefix}CodePipelineApproval
            Effect: Allow
            Action:
              - "codepipeline:PutApprovalResult"
            Resource: !Sub arn:aws:codepipeline:${AWS::Region}:${AWS::AccountId}:${Prefix}${PipelineNameSuffix}/Approval/Approval
```

- パイプライン
好きなところに差し込む。後続のアクションのRunOrderをずらすのを忘れないように。

```YAML
        - Name: Approval
          Actions:
            - RunOrder: 3
              Name: Approval
              ActionTypeId: 
                Category: Approval
                Owner: AWS
                Provider: Manual
                Version: 1
              Configuration:
                NotificationArn: !Ref SNSTOPIC
                CustomData: [コメント]
```