AWS Protonのサンプルプロジェクトを試す


AWS Protonがパブリックプレビューで公開されているので、
サンプルプロジェクトを実施して具体的にどんなことが出来るのかを確かめてみました。
この記事ではサンプルプロジェクトで何ができたのか、またサンプルプロジェクトで実際に何が起きたのかを紹介したいと思います。

# AWS Proton とは

- 公式より

```
コンテナおよびサーバーレスアプリケーションのための初の完全マネージド型アプリケーションデプロイサービスです。
Proton を使用すれば、プラットフォームエンジニアリングチームは、インフラストラクチャのプロビジョニング、
コードデプロイ、モニタリング、更新の際に必要になるさまざまなツールをすべて接続し、協調させることできます。
数百、場合によっては数千のマイクロサービスを、絶えず変化するインフラストラクチャリソースと
継続的インテグレーション/継続的デリバリー (CI/CD) 構成で維持することは、
どんなに有能なプラットフォームチームにとってもほぼ不可能なタスクです。
AWS Proton を利用することで、この煩雑なタスクを管理して一貫した標準を適用するために必要となるツールをプラットフォームチームが
手にすることとなり、デベロッパーはコンテナとサーバーレステクノロジーを利用してコードを簡単にデプロイできるようになるため、前述の問題を解決できます。
```
    
- 私が触った感覚ではざっくりですが
    - AWS上でのサーバレス環境とそのCICD環境をセットで管理できるサービス
    - システムのリソースおよびCICD環境のリソース構築のためのテンプレートを作成し、バージョン管理できる
        - 開発者はテンプレートからアプリケーションをデプロイ、運用できる
        - アプリの更新、削除などもProtonのコマンドで実行可能
    - テンプレートは現状はCloudFormationテンプレートを利用したものになります。またデプロイもCodePipelineを利用しています
        - ロードマップによると今後terraformや他のサービスも利用できるようになるようです


## 環境テンプレートとサービステンプレート

Protonでは２種類のテンプレートを作成、管理することができます。

- 環境テンプレート
    - 主にアプリケーションのインフラ周りのリソースを定義するテンプレートになります
    - 例）VPC,DB,SG,Role,ECSClusterなど
- サービステンプレート
    - アプリケーション本体のリソースを定義するテンプレート
        - 例）APIGateway,Lambda、ECSService、ALBなど
    - CICDパイプライン用のリソースもこちらで定義します
        - 例）CodePipeline,CodeBuild,CodeDeployなど

ただし、テンプレートの記述自体は自由なので、環境テンプレート、サービステンプレートにどのリソースを定義するかは開発者で判断する必要があります。

# チュートリアル実施

公式で用意されているサンプルテンプレートを試してみました。サンプルはAPI Gateway+LambdaでCRUD処理をするAPIをデプロイするサンプルとなっています。

サンプルのソースやサンプルを利用したチュートリアル手順はgithubで公開されていますので、ぜひ試してみてください。

- https://github.com/aws-samples/aws-proton-sample-templates/tree/main/lambda-crud-svc


## 1 事前準備

#### 1.1 サンプルリポジトリのクローン

まずはワークスペース上にサンプルテンプレート用のリポジトリをクローンしてください。
その後、カレントディレクトリを「{ワークスペース}/aws-proton-sample-templates/lambda-crud-svc」まで移動してください。
基本的には今後の操作はそのディレクトリ上で実行していきます。

```bash

git clone https://github.com/aws-samples/aws-proton-sample-templates.git
cd ./aws-proton-sample-templates/lambda-crud-svc/
```

#### 1.2 アカウントIDを環境変数に設定

チュートリアルのCLIコマンドでAWSアカウントIDが必要になるため予め環境変数に登録しておきます。
この手順は自分でコマンド内の`${account_id}`をAWSアカウントIDに置き換えるのであれば実施不要です。

```bash
account_id=`aws sts get-caller-identity|jq -r ".Account"`
```

#### 1.3 CLIでProtonのコマンドを実行できるようにする

Protonはまだプレビュー版なので、aws cliでコマンドを実行できるように設定が必要になります。

```bash
aws s3 cp s3://aws-proton-preview-public-files/model/proton-2020-07-20.normal.json .
aws s3 cp s3://aws-proton-preview-public-files/model/waiters2.json .
aws configure add-model --service-model file://proton-2020-07-20.normal.json --service-name proton-preview
mv waiters2.json ~/.aws/models/proton-preview/2020-07-20/waiters-2.json
rm proton-2020-07-20.normal.json
```
#### 1.4 Protonのテンプレート置き場用のS3バケット作成

Protonの環境、サービステンプレートはS3に保存されるため、テンプレート保存用のバケットを作成します。

```bash
aws s3api create-bucket \
  --region us-west-2 \
  --bucket "proton-cli-templates-${account_id}" \
  --create-bucket-configuration LocationConstraint=us-west-2
```
#### 1.5 IAMロール作成

Protonがサービス実行のために必要なIAMロールを作成します。

```bash
aws iam create-role \
  --role-name ProtonServiceRole \
  --assume-role-policy-document file://./policies/proton-service-assume-policy.json
aws iam attach-role-policy \
  --role-name ProtonServiceRole \
  --policy-arn arn:aws:iam::aws:policy/AdministratorAccess
aws proton-preview update-account-roles \
  --region us-west-2 \
  --account-role-details "pipelineServiceRoleArn=arn:aws:iam::${account_id}:role/ProtonServiceRole"
```

#### 1.6 アプリのソースコード用リポジトリの設定

サンプルではGithubのリポジトリからソースコードをpullしてデプロイする構成になっているので、
ソースコードをGithub上のリポジトリに置き、さらにAWSからそのリポジトリを参照できるようにするための接続設定が必要になります。

1. 以下のリポジトリを自分のGithubアカウントにフォークする
    - https://github.com/aws-samples/aws-proton-sample-lambda-crud-service
2. CodeStarで自分のGithubアカウントの接続設定を作成する
    - https://us-west-2.console.aws.amazon.com/codesuite/settings/connections?region=us-west-2
    - 作成した接続設定のARNは後で利用するのでメモしておく



## 2 環境テンプレート作成

まずは環境テンプレートの作成から行っていきます。

#### 2.1 テンプレート作成

まずはテンプレートを作成します。

```bash
aws proton-preview create-environment-template \
  --region us-west-2 \
  --template-name "crud-api" \
  --display-name "CRUD Environment" \
  --description "Environment with DDB Table"
```

コンソール上では環境テンプレート「crud-api」が作成されていることが確認できました。
ただし、この時点ではまだ何もない状態となっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/4e6d5c2f-b67f-132e-4cde-dccc2ec059c3.png)

#### 2.2 メジャーバージョン作成

メジャーバージョンを新規登録します。

```
aws proton-preview create-environment-template-major-version \
  --region us-west-2 \
  --template-name "crud-api" \
  --description "Version 1"
```

コンソール上では新たにバージョン「１」が登録されたことが確認できます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/f1ae7801-b343-057d-e393-090f9742c2a1.png)

#### 2.3 マイナーバージョン作成

さらにマイナーバージョンの登録を行います。
環境テンプレート群を圧縮してS3にアップロードし、マイナーバージョン登録時のコマンドで
そのアップロードされた環境テンプレートのS3パスを指定しています。

```bash
tar -zcvf env-template.tar.gz environment/

aws s3 cp env-template.tar.gz s3://proton-cli-templates-${account_id}/env-template.tar.gz --region us-west-2

rm env-template.tar.gz

aws proton-preview create-environment-template-minor-version \
  --region us-west-2 \
  --template-name "crud-api" \
  --description "Version 1" \
  --major-version-id "1" \
  --source-s3-bucket proton-cli-templates-${account_id} \
  --source-s3-key env-template.tar.gz
```
コンソールでもマイナーバージョン「1.0」が登録されていますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/5fb5c8b2-e9d6-4d2d-717b-274a6acdc8f2.png)

#### 2.4 登録したバージョンの公開

コンソールの忠告にもあるようにまだバージョンの状態が「Draft」なので、この状態ではこのテンプレートはまだ利用できない状態になっています。
利用できるようにするためにステータスを「公開」状態に更新しましょう。

```bash
aws proton-preview update-environment-template-minor-version \
  --region us-west-2 \
  --template-name "crud-api" \
  --major-version-id "1" \
  --minor-version-id "0" \
  --status "PUBLISHED"
```

ステータスが「Published」に更新されました。
ちなみに一度公開状態にしたバージョンはDraft状態には戻せないようです。
やり直す場合は対象のバージョンを削除して作り直す、または新しいバージョンを発行することになるようです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/812bfa27-1921-9744-9780-965d9857778a.png)


## 3 サービステンプレート作成

#### 3.1 テンプレート作成

環境テンプレートと同様にまずはテンプレートを作成します。

```bash
aws proton-preview create-service-template \
  --region us-west-2 \
  --template-name "crud-api-service" \
  --display-name "CRUD API Service" \
  --description "CRUD API Service backed by AWS Lambda"
```
![サービス.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/c44692a7-2c18-875c-634d-53251dd4cfc3.png)

#### 3.2 メジャーバージョン作成

メジャーバージョンを新規登録します。

```
aws proton-preview create-service-template-major-version \
  --region us-west-2 \
  --template-name "crud-api-service" \
  --description "Version 1" \
  --compatible-environment-template-major-version-arns arn:aws:proton:us-west-2:${account_id}:environment-template/crud-api:1
```

コンソール上では新たにバージョン「１」が登録されたことが確認できます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/dbc31ea7-eefc-4d92-40ef-e3f4b9894d71.png)


#### 3.3 マイナーバージョン作成

さらにマイナーバージョンの登録を行います。

```bash
tar -zcvf svc-template.tar.gz service/

aws s3 cp svc-template.tar.gz s3://proton-cli-templates-${account_id}/svc-template.tar.gz --region us-west-2

rm svc-template.tar.gz

aws proton-preview create-service-template-minor-version \
  --region us-west-2 \
  --template-name "crud-api-service" \
  --description "Version 1" \
  --major-version-id "1" \
  --source-s3-bucket proton-cli-templates-${account_id} \
  --source-s3-key svc-template.tar.gz
```

コンソールでもマイナーバージョン「1.0」が登録されていますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/f9c99b55-c1e7-bb68-c557-b3a08b95a42e.png)

#### 2.4 登録したバージョンの公開

コンソールの忠告にもあるようにまだバージョンの状態が「Draft」なので、この状態ではこのテンプレートはまだ利用できない状態になっています。
利用できるようにするためにステータスを「公開」状態に更新しましょう。

```bash
aws proton-preview update-service-template-minor-version \
  --region us-west-2 \
  --template-name "crud-api-service" \
  --major-version-id "1" \
  --minor-version-id "0" \
  --status "PUBLISHED"
```

ステータスが「Published」に更新されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/57d57b1c-c447-95f5-6cf4-86ecfe46db29.png)


## 4 環境デプロイ

まずは環境側のリソースをデプロイします。

```bash
aws proton-preview create-environment \
  --region us-west-2 \
  --environment-name "crud-api-beta" \
  --environment-template-arn arn:aws:proton:us-west-2:${account_id}:environment-template/crud-api \
  --template-major-version-id 1 \
  --proton-service-role-arn arn:aws:iam::${account_id}:role/ProtonServiceRole \
  --spec file://specs/env-spec.yaml
```

ここで指定している`specs/env-spec.yaml`は環境テンプレートに対する入力パラメータが定義されているyamlになります。

このサンプルの環境テンプレートで作成されるテンプレートは以下になります。

- DynamoDBテーブル

コンソールで確認するとProtonコンソールでは環境が作成されていることが確認できます

![環境作成.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/0bcb0f64-5861-120f-d529-f3632f6cb620.png)

CloudFormationコンソールでは環境リソース用のスタックが作成されています。
CloudFormationでDynamoDBテーブルが作成されたことが確認できますね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/bf012cc6-5623-8e2b-ef65-71e84e27ea44.png)



## 5 サービスデプロイ


#### デプロイコマンド実行

次にサービスのデプロイを行います。
デプロイコマンド時にアプリのソースコードがあるリポジトリおよびリポジトリ接続のための接続情報を入力する必要があります。

ここでも環境デプロイ時と同様に入力パラメータは`file://specs/svc-spec.yaml`で定義されています。
サービスがどの環境リソースを利用するかは入力パラメータで指定されています。

```bash
aws proton-preview create-service \
  --region us-west-2 \
  --service-name "tasks-front-end" \
  --repository-connection-arn arn:aws:codestar-connections:us-west-2:${account_id}:connection/<your-codestar-connection-id> \
  --repository-id "<your-source-repo-account>/<your-repository-name>" \
  --branch "main" \
  --template-major-version-id 1 \
  --service-template-arn arn:aws:proton:us-west-2:${account_id}:service-template/crud-api-service \
  --spec file://specs/svc-spec.yaml
```

入力パラメータは以下の形式で定義されています。
すこしややこしい&私もまだ理解しきれていないですが、入力パラメータ`instances`は配列のため、name以下のパラメータを複数定義できます。
配列が複数定義されている場合、その要素の数だけサービス用のリソースがデプロイされるような設計になっています。

```svc-spec.yaml
proton: ServiceSpec

# cicd用リソースで利用する入力パラメータ
pipeline:
  unit_test_command: make test
  packaging_commands: make publish

# サービスリソースで利用する入力パラメータ
instances:
  - name: "front-end"
    # どの環境を利用するか
    environment: "crud-api-beta"
    spec:
      resource_name: task
      resource_handler: src/api
      lambda_runtime: ruby2.7

```

#### デプロイコマンド実行直後

デプロイコマンド実行直後ではまずアプリケーションリソース用のテンプレート(`service/infrastructure/cloudformation.yaml`)のデプロイが実施されます。

アプリケーションリソース用のスタックが作成されていますね。
![デプロイすぐのサービススタック.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/ce8b87f2-2f60-9017-8487-730dc13d2dd7.png)

#### CICD用パイプラインリソースのデプロイ

アプリケーションリソース用のテンプレートデプロイ完了後、CICDパイプライン用のテンプレート（`service/pipeline/cloudformation.yaml`）がデプロイされます。
CodePipelineやCodBuildが作成されていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/cb59ef92-6dc5-df45-cd81-41fbb7562850.png)

#### CodePipeline実行

CICDパイプライン用のテンプレートのデプロイ完了後、作成されたCodePipelineが実行し、
CodeBuild上でアプリケーションのデプロイ処理が実行されます。

![pipeline.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/47b520ab-eefd-d4e6-e445-ce98fa2c91ab.png)

パイプライン実行後、アプリケーションリソース用のスタックを再度確認すると新たにリソースが作成されていることが確認できます。

![pipeline実行後のサービススタック.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/203410/6c14769e-2530-40e9-d0fe-a1ee0a7c93db.png)


サンプルのチュートリアル実施はこれで以上となります。
テンプレートが用意されていれば、
コマンド一つでCICD環境の構築からアプリケーションデプロイまで実施できますね。

# サービスデプロイ時の実施フロー

実際に触ってみてわかったのですが、サービステンプレートからサービスをデプロイする際、アプリケーションリソース用のスタックがデプロイコマンド実行時とPipeline実行後でスタックに含まれるリソースが異なっていました。
これ、サンプルソースを確認すると一つのテンプレートを使いまわしていて、入力パラメータによって、デプロイするリソースを切り替えていることがわかりました。ソースを見るだけでは少し分かりづらいので、実際にどのようなフローでリソースが作成されていったのか整理しました。

1. デプロイコマンド実行
2. アプリケーション用テンプレート(`service/infrastructure/cloudformation.yaml)からスタック作成
    - この時点に作成されるリソース
        - CrudHttpApi(AWS::Serverless::HttpApi) - CRUD処理用API Gateway
3. CICDパイプライン用のテンプレート（`service/pipeline/cloudformation.yaml`）からスタック作成
    - 作成されるリソース（IAMロール、ポリシーについては割愛しています）
        - FunctionBucket(AWS::S3::Bucket) - Lambdaリソースアップロード用バケット
        - BuildProject(AWS::CodeBuild::Project) - Buildステージ用CodeBuildプロジェクト
        - PipelineArtifactsBucketEncryptionKey(AWS::KMS::Key) - アーティファクトアップロード時の暗号化キー
        - PipelineArtifactsBucketEncryptionKeyAlias(AWS::KMS::Alias) - PipelineArtifactsBucketEncryptionKeyのエイリアス
        - PipelineArtifactsBucket(AWS::S3::Bucket) - アーティファクト用S3バケット
        - Pipeline(AWS::CodePipeline::Pipeline) - CICD用CodePipeline
            - 入力パラメータの`service_instances`の要素数分デプロイ用のActionが追加されます
        - 以下のリソースは入力パラメータの`service_instances`の要素数分作成されます
            - Deploy{{loop.index}}Project(AWS::CodeBuild::Project) - Deployステージ用CodeBuildプロジェクト
4. CodePipeline実行
5. CodePipelineのBuildステージでCodeBuildが実行
    - CodeBuild内でProtonコマンド用の入力パラメータファイル（svc-spec.yamlのような）を作成
    - 具体的にはsvc-spec.yamlに`instances[*].spec.code_uri`パラメータが追加された状態のファイルになります。
6. CodePipelineのDeployステージでCodeBuild実行
    - CodeBuild上でprotonの`update-service-instance`コマンド実行
        - `aws proton-preview --endpoint-url https://proton.$AWS_DEFAULT_REGION.amazonaws.com --region $AWS_DEFAULT_REGION update-service-instance --version-update-type UPDATE_SPEC --service-instance-name $service_instance_name --service-name $service_name --spec file://rendered_service.yaml`
7. `update-service-instance`で再度アプリケーション用テンプレート(`service/infrastructure/cloudformation.yaml)のスタック更新
    - このとき入力パラメータに`instances[*].spec.code_uri`が含まれているため、追加で以下のリソースが作成される
        - ListFunction(AWS::Serverless::Function) - データ一覧取得リクエスト用Lambda
        - CreateFunction(AWS::Serverless::Function) - データ作成リクエスト用Lambda
        - GetFunction(AWS::Serverless::Function) - データ取得リクエスト用Lambda
        - DeleteFunction(AWS::Serverless::Function) - データ削除リクエスト用Lambda
        - UpdateFunction(AWS::Serverless::Function) - データ更新リクエスト用Lambda

このように同じテンプレートファイルを利用しているのにも関わらず、入力パラメータなどによって動的にCfnテンプレートを作成し、実行しています。これはサンプルソースを見ればわかりますが、テンプレートファイルにテンプレートエンジン「jinja」を利用しているためです。
jinjaを利用して入力パラメータの参照はif文などでCfnテンプレートを動的に作成できるようになっています。

以下のコードのように入力パラメータで条件分岐していたため、実行タイミングによって作成されるリソースが切り替わっていました。

```service/infrastructure/cloudformation.yaml
Resources:
  CrudHttpApi:
    Type: AWS::Serverless::HttpApi
    Properties:
      FailOnWarnings: false
{% if service_instance.code_uri|length %} # <-入力パラメータ「code_uri」の有無で条件分岐
  ListFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: "{{service_instance.code_uri}}"
      Handler: "{{service_instance.resource_handler}}.list_{{service_instance.resource_name}}"
      Runtime: "{{service_instance.lambda_runtime}}"
      Environment:
        Variables:
          TABLE_NAME: "{{environment.TableName}}"
      Policies:
        - DynamoDBCrudPolicy:
            TableName: "{{environment.TableName}}"
...

```


# まとめ

チュートリアル実施しただけになりますが、触った感想としては、

- アプリケーションリソースとCICDリソースをまとめて管理できるのは便利だと思いました
    - プロトンでバージョンも管理できるのもいいですね
- 環境テンプレートとサービステンプレートそれぞれ用意できる
    - 一つの環境に複数のサービステンプレートのデプロイ等もできるので色々応用できそうです
    - ただしどちらのテンプレートにも任意のリソースを定義できてしまうので、どのリソースを環境、サービスどちらテンプレートに置くかは開発者で検討する必要があります
    - 個人的には環境テンプレートにはDB等のデータストアや基本的に変更しないリソースを定義、サービステンプレートには全削除しても問題ないリソース群を定義するのが一つの切り分け方になるのかなと感じました
- まだプレビュー版なので仕方がない部分もあるのですが、現状気になる点としては
    - テンプレートの記述がProtonの仕様に制限される
        - service_instancesの取り扱いの都合上必ず、`{% if service_instance.code_uri|length %}`などの記載が必要になっている状態です
    - また当然といえば当然ですが学習コストが高いですね。CloudFormation、CodePipeline、CodeBuildに加えてProtonの仕様も理解する必要があります
    - 現時点ではアプリケーションのソースコードリポジトリがGithubかBitbucketのみ対応
        - CodeCommit対応してほしいですね。（ロードマップにはCodeCommit対応が載っています）
    - 既存のシステムからProtonに移行するのは大変そう
 

プレビュー版なのでまだまだ機能は限定的ですが、ロードマップでCloudFormation以外のテンプレートなどにも対応していくようなので、より汎用的になっていくようです。GAされるが楽しみですね。