AWS CodeシリーズをAWS Step Functionsで実行し継続的デプロイする

# 概要

* AWSでCI/CDを構築しようとなった時に様々な選択肢があると思います。
* AWSに限らずで言えば最近だとGitHub Actionsやcircleciなんかがよく技術選定する際に出てきますかね。
* 今回は**AWSでCI/CDを構築する時のパターンとして、Codeシリーズを使用した内容となるのですが、CodePipelineを使って作るパイプラインとCodeシリーズの1つCodeBuildをAWS Step Functionsを使用し逐次実行するパイプラインを比較するという内容**です。
* なぜ、CodePipelineを使用せずAWS Step Functionsなのか。何が嬉しいのかついて説明できれば幸いです。

## AWS Codeシリーズ

* ではさっそく内容に入っていくのですが、まずはCodeシリーズとは。について説明していきます。

* Codeシリーズというのは、AWSの下記のサービスをまとめた総称のことを指します。
  * [AWS CodeCommit](https://aws.amazon.com/jp/codecommit/)
  * [AWS CodeBuild](https://aws.amazon.com/jp/codebuild/)
  * [AWS CodeDeploy](https://aws.amazon.com/jp/codedeploy/)
  * [AWS CodePipeline](https://aws.amazon.com/jp/codepipeline/)

### AWS CodeCommit

* フルマネージド型のソース管理サービスです。
* 既存のGitツールともシームレスに連携に連携できます。
* 料金体系は、月々のアクティブユーザ数やAPIリクエストの数、利用容量などで課金されるという仕組みのようです。

=> ソース管理をするサーバーを管理したくなければ利用を考えるとよいでしょう。

### AWS CodeBuild

* フルマネージド型ビルドサービスです。
* 利用シーンとしては、ソースコードをコンパイルしたり、テスト実行し、デプロイ可能なソフトウェアパッケージを作成させたりします。

=> ビルド用のサーバをプロビジョニング・管理する必要がなくなるというのがこのサービスの一番の特徴です。

**このCodeBuildなのですが、AWS的にはCodeシリーズの中でビルドサービスとして位置付けていますが、利用シーンは多岐にわたります。**

* ちょっとしたAWS CLIの実行を自動化したい時とかにCodeBuildを利用し、さらに定期実行するのであれば[EventBridge](https://aws.amazon.com/jp/eventbridge/)からCronでCodeBuildを実行させるといったこともできます。

### AWS CodeDeploy

* 様々なコンピューティングリソース(ECS、EC2、Lambda、オンプレなど)に対してデプロイを行うサービスです。
* AutoScalingする構成に対しても連動しデプロイすることができます。
* あとはCodeDeployのデプロイ設定として、インプレースやBlue/Greenといったデプロイタイプを選ぶことができます。
  * **※ デプロイタイプは、選択するリソースによって選択できるか/できないかがあります。**
  * **EC2: インプレースもしくはBlue/Greenで選択可能**
  * **LambdaとECS: はBlue/Greenのみ**

=> AWSの様々なコンピューティングリソースに対しデプロイするCodeDeployですが、一番の特徴はデプロイタイプとしてBlue/Green　やインプレースをマネージドでやってくれる(AutoScalingの構成と連動し)というのが一番のメリットです。

### AWS CodePipeline

* フルマネージドな継続的デリバリーを作成/実行できるサービスです。
* コードリポジトリと連動し、ソースコードの変更をトリガーに、ビルド、デプロイといった一連の流れを管理し、自動で実行できます。

今回は、このCodePipelineとAWS Step Functionsをしたい為、もう少しCodePipelineについては仕様を説明していきます。

#### CodePipelineの仕様

* CodePipelineの実行には、アプリケーションのソースコードを管理するGitHubやCodeCommitなど(ソース管理サービス)の**ソースステージ**から始まり、設定したステージごとに処理が直列に流れます。
* **このステージの組み合わせは自由に設定することができます。**
* ステージの中には**アクション**を仕込むことができ、ここで**CodeBuild**などのプロバイダを指定すると、指定したアクションを含むステージのステップになった際に、設定したアクションであるCodeBuildがキックされます。
* また、ステージの中に仕込むアクションについては、複数指定することができます。

![スクリーンショット 2021-07-07 17.22.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/11d74b9a-55e4-188d-b4cf-153c646772b3.png)

[画像引用元](https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/concepts-devops-example.html)


#### ステージに設定できるアクション例

* アクションとしては以下のようなものが設定できます。
* CodePipelineに設定できるアクションの特徴としては、AWSのリソース以外にも例えばJenkinsを指定できたりSnykを呼び出したりできる点です。
* また、ここいいなと思ったのが**継続的デリバリーを実現する中であるステージが終わり次のステージに行く前に手動承認がほしいといったパターンがあったりします。その時にマネージドのアクションで手動承認が予め用意されていることはCodePipelineを使ってもいいなと思う点になりそうですね。**

![スクリーンショット 2021-07-08 10.24.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/02b34657-5992-1e1d-a0fe-2a9eb63e8364.png)

![スクリーンショット 2021-07-08 10.24.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/13acda96-f4c7-9990-ae8e-d998c2f6f0df.png)

* なお、ステージは直列に実行されるものの、ステージ内で指定したアクションについては並列実行することができるので、複数のアプリを同時にデプロイしたいケースがある場合にも対応できます。

## AWS Step Functions

* Step Functionsは、AWSサービスを組み合わせ実行することができるサーバーレスオーケストレーションサービスです。
* AWSコンソールよりGUIでAWSの各サービスをコンポーネントとしワークフローという形式で可視化することができます。(Workflow Studio)
* Step Functionsではステートマシン(これがワークフローを表す)とタスク(これがStep Functionsでやりたい操作やAWSの各サービスの実行を表す)に基づいています。
* また、ワークフロー内で実行するタスクは各実行ステップ内で定め、ステップは直列に実行されます。

**そして、昨年のアップデートでStep Functionsから実行できるAWSの各サービスが増加し、ほぼ大体のAWSのサービスと連携することができるようになっています。**

https://dev.classmethod.jp/articles/aws-step-functions-200-aws-sdk-integration/

* **欠点としてStep Functionsには、CodePipelineのような予め用意された手動認証の機能が備わっていない為、もし手動認証のステップが欲しい場合は、少し工夫をしなければいけません。**

## 全体的なパイプラインの一例

* ここからはCodePipelineとStep Functionsを継続的デプロイという観点で比較するために、ある例を基にパイプラインを検討し、評価していきましょう。

### プロジェクト例

#### インフラ構成

* サービスのデプロイ先がAWSで、インフラの構成をIaCで作り、使用ツールとしては[Terraform](https://github.com/hashicorp/terraform-provider-aws)を使用したとしましょう。
* プロジェクトとしてはGitHubリポジトリにてインフラのソースコードを管理しています。
* インフラの設定内容をデプロイしたい場合は、CodeBuildを使用しterraform applyコマンドで実施するように作られています。
=> これが先ほどのCodeBuildの使用方法でこんな使い方もできるよという一例になります。
* なお、チームとしてはGitHubからのPushをトリガーにデプロイさせたくなく、デプロイしたい場合は、他の資源とまとめて手動でCodeBuildを実行するような運用になっています。
* なお、継続的インテグレーション(CI)についてはGitHubへのコード変更をトリガーにterraform planを実施し、Plan結果をS3のバケットに配置することでソースコードの変更内容を確認することができるようになっています。

##### terraformって？

* Terraformって何？と思われる人もいると思うので、簡単に説明します。
* Terraformとは、インフラを安全かつ効率的に管理する為のツール
* terraformでは、terraformのsyntaxで記述されたファイルを利用しawsなどに資源を作成しにいきます。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/509b64e8-5d37-70c3-0ff4-f02a67deb244.png)

(左側の図がtfファイルのサンプルになります。)

* tfファイルが作成できたら、terraformのcliを利用し、図だとaws環境に対して資源作成を行なっています。
* terraformのcliを利用するとterraform planというコマンドで現在のインフラの状況と、変更した差分を確認することができます。
* また、terraform applyコマンドで現在のインフラの構成に変更した内容を更新するといったことも可能です。

#### フロントエンドアプリ/バックエンドアプリ

##### フロントエンドアプリ

* フロントエンドのアプリは、モダンなライブラリ/フレームワークで作成されていることにしましょう。
* ホスティング先のAWSのサービスはAmplifyが利用されています。
* プロジェクトでは、CIとしてGitHubへのソースコード変更をトリガーにCodeBuildを実行します。CodeBuildでは、自動化されたE2Eのテスト、UTなどが走り全て問題なければzip化されたアーティファクトがs3のバケットへ格納されます。
* チームでは、CodeBuildを使用しCIで作成されたアーティファクトをAmplifyにホスティングするようになっています。
* なおこちらのCodeBuildはインフラと同様にGitHubへのソース変更をトリガーとはせずに他の資源と同時にデプロイできるよう手動実行でデプロイするような運用がされています。

##### バックエンドアプリ

* バックエンドのアプリとして、2つのサービスをチームは持っています。また実行環境をコンテナ化しています。
* コンテナ化したアプリを実行するためのAWSのサービスとして、ECS + Fargateが利用されています。
* プロジェクトでは、CIとしてGitHubへのソースコード変更をトリガーにCodeBuildを実行します。CodeBuildでは、自動化されたE2Eのテスト、UTなどが走り全て問題なければ、コンテナのイメージが作成され、作成されたイメージはECRへpushするような動作となっています。
* チームでは、CodeBuildを使用しCIで作成されたアーティファクト(ECRに保持しているコンテナイメージ)をECS Fargateにデプロイするようになっています。
* また、このときCodeDepolyを使用しBlue/Greenデプロイとなるよう設定されています。
* こちらのCodeBuildも他と同様にGitHubへのソース変更をトリガーとはせずに他の資源と同時にデプロイできるよう手動実行でデプロイするような運用がされています。

### パイプライン化する上での条件

* インフラ、フロントエンド、バックエンドアプリをデプロイする前に手動で承認をする必要があります。
* また、デプロイする順番は、インフラ → フロントエンド → バックエンドの順です。
* チームでは、バックエンドの資源を2つ抱えており、リリースする際には毎回それぞれのリソースをデプロイするためのCodeBuildを手動実行していました。
* フロントエンドのアーティファクト、バックエンドのコンテナイメージのversion管理としては、GitHubのコミットハッシュを使用しています。

全体的な流れを下記のようにしたいと考えています。

![スクリーンショット 2022-06-29 3.00.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/de9277ba-c0e4-4354-3054-dc573ebbb3d0.png)


## CodePipelineではなくStep Functionsを選ぶ理由

* 上記の例あげたリソースを継続的デプロイするためにパイプラインを作成していきましょう。
* まずはCodePipelineからです。

### CodePipeline

* 下記のパイプラインが上記の仕様をもとに作成したものになります。
* これで一応要件を満たすことはできたのですが、チームとして開発方針、もう少しいうとブランチ戦略を見直さなければならないくらいのことが発生してしまいます。
* それは、このパイプラインの起動方法に直結するのですが、CodePipelineを作成した際に初めのSourceステージは削除することができません。
* そのため、CodePipelineをトリガーするソースリポジトリのブランチを指定しないといけないということです。
* 要件では、デプロイするためのブランチを作っておらずコミットハッシュを手動で入力してデプロイしたい資源を自由に選択できるようになっていました。
* なので、気持ち悪いですがCodePipelineをトリガーするブランチを例えばmainなどを指定し、GitHubからのソース変更ではトリガーできないよう設定しないといけません。(なお、CodePipelineはコミットハッシュを指定し手動実行することはできます。)
* さらに、起動直後の資源は選択したコミットハッシュから該当資源を発見できますが、それ以降の資源は同じコミットハッシュなわけ無いので、コミットハッシュを用いた資源の選択ができなくなってしまいます。
* 実はここがCodePipelineの一番使いづらいところです。
* 決められたテンプレートに従う形でパイプラインの構成を作らないといけません。

![スクリーンショット 2022-06-29 3.19.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/3beeb1d1-0b9d-b45d-d422-73a733d2a4fb.png)

![スクリーンショット 2022-06-29 3.20.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/e86fc3c0-62df-9f6b-5ce0-dccb0f31ea44.png)

![スクリーンショット 2022-06-29 3.20.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/072d824a-6930-82ae-af43-8b1eb26d3c65.png)

### Step Functions

* Step Functionsで作成したパイプラインは下記のようになりました。

![スクリーンショット 2022-06-29 4.06.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/65d0d05b-ffd9-1aae-ab14-59b315975a4f.png)

* もう少し各サービスをどうに組んだかを見ていきましょう。
* Step Functionsの説明での述べたようにWorkflow Studioを使用するとGUI上でワークフローが可視化されます。

![スクリーンショット 2022-06-29 4.06.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/0fdf3e1b-7813-ae5c-172f-d1e9c7195581.png)

* まずStep Functionsを使用することで用件に沿ったパイプラインを作成することができました。
* CodePipelineで問題になっていたパイプラインのキックについても無理にソースリポジトリを選択することなく、手動でかつそれぞれの資源ごとにデプロイしたいversionをコミットハッシュで入れることで実行することができます。

**この時のコツなのですが、初めに入力したコミットハッシュを後続のステップに引き継がせるには、それぞれのステップごとの出力にInputで受けたものをそのまま出力に流す形にする必要があります。**

![スクリーンショット 2022-06-29 4.05.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/563635/99d8f834-f60f-dde0-cbb1-ee25ed9505a8.png)


* 次にStep Functionsでは予め用意された操作をするアクションを選択することができます。
* この例だと、入力値のチェックや、バックエンドリソースをまとめ並列実行するなど様々なものがあります。
* 最後に重要なのが、手動承認です。先にも述べたようにStep Functionsでは、手動承認というアクションが予め用意されていません。
* なので手動承認をしない限りワークフローが先に進まないようにするにはコツが入ります。
* 一例として、Lambdaを用いたパターンで今回は実装してみました。
* というのは、承認コードをLambdaを利用しSlackにwebhook URL経由で送信します。
* その後、届いた承認コードを使用しStep Functionsの後続の処理が実行されるようCLIを叩くというものです。
* 承認コードというのはこちらをご参照ください。

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/connect-to-resource.html

下記が承認コードを飛ばすLmabdaの例になります。
```py
# coding: utf-8
import json
import urllib.request
import urllib.parse
import os

def lambda_handler(event, context):

    slack_webhook_url = "SlackのWebHook URL"
    slack_channel_name = "承認コードを送信したいSlackのチャンネル名"

    task_token = urllib.parse.quote(event['TaskToken'])

    payload = {
        "text"          : event['Message'],
        "channel"		: slack_channel_name,
        "username"      : "承認コード",
        "attachments"   : [
            {
                "fields": [
                    {
                        "title" : "承認コード",
                        "value" : task_token,
                        "short": True
                    },
                ]
            }
        ]
    }

    payload_text = "payload=" + json.dumps(payload)
    request = urllib.request.Request(
        slack_webhook_url,
        data=payload_text.encode("utf-8"),
        method="POST"
    )

    # Webhook送信
    with urllib.request.urlopen(request) as response:
        response_body = response.read().decode("utf-8")

    return response_body
```

```sh
aws stepfunctions send-task-success --task-token "承認コード" --task-output {}
```

## まとめ

* いかがだったでしょうか？
* 作成したいパイプラインによってはStep FunctionsではなくCodePipelineの方が適しているケースもあると思います。
* チームの思想やデプロイのやりやすさ、また何に重きを置いたパイプラインなのかを見定め、幸せな継続的デプロイになれば幸いです。

# 参考資料

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

https://docs.aws.amazon.com/ja_jp/codepipeline/latest/userguide/welcome.html

https://aws.amazon.com/jp/step-functions/?step-functions.sort-by=item.additionalFields.postDateTime&step-functions.sort-order=desc

https://dev.classmethod.jp/articles/aws-step-functions-200-aws-sdk-integration/