ecspressoの導入に係る考慮事項

## ecspressoの概要
ecspresso はECSのデプロイツールです。json あるいは yaml ファイル形式でECSタスク定義やサービス定義を記述して専用コマンドで柔軟にデプロイを行うことが可能です。
主な導入メリットとしては以下があげられます。
- IaC化することによる管理性能の向上
- ecspresso 専用コマンドにより、依存関係の検証からロールバックなどECSのデプロイを安全かつ柔軟に行うことが可能
- インフラリソースとアプリリソースのリリースサイクル及び担当主体は通常異なるが、ECSの管理を ecspresso に切り分けることで意図しないデプロイといったトラブルを防止することに繋がる

## ecspressoの構成案
近年のモダンなシステムであれば複数のサービスから構成されていると思いますが、ecspresso でこうした複数のECSプロパティを管理する場合、可能な限り重複やべた書きを避けたクリーンなコード構成にしておくことが望ましいです。

### jsonテンプレートの使用
クリーンなコードを書くにあたり、ecspresso のデフォルトフォーマットであるjsonで設定ファイルを記述しようとすると管理コストが高くなりすぎることからjsonテンプレートである jsonnet を採用することをお勧めします。
jsonnet を使用するメリットとしては以下が挙げられます。
- 変数を利用することが出来る
- FOR文やIF文といった基礎的な文法を使用することが出来る
- ファイルをインポートすることで外部ファイルのプロパティを参照することが出来る
- コメントアウト機能を使用できる

### SSMパラメータストアからの値取得
また、ecspresso ではSSMパラメータストアとSecrets Managerからプロパティを取得する機能があります。これにより環境差分を吸収したり、ALBやSGといったecspresso のスコープ外資源を参照する際のべた書き防止することが可能です。
一点注意点としてこうした外部データの取得はjsonnet のレンダリング後に行われます。つまり外部から取得したプロパティを加工してECSの設定に組み込むことは出来ません。これで困るのが例えばタスク数（desiredCount）といったプロパティは数値型で定義されるのですが、SSMパラメータストアには文字列型しか保持することが出来ないため適用出来ません。ecspresso の設定ファイルを環境分用意しておき渡すかecspresso コマンド実行時に渡すことのできるパラメータで表現することになります。

### ecspresso コマンド実行時の変数入力
ecspresso ではコマンド実行時に以下のようにオプションを使用することでecspresso 設定ファイルに変数を入力することが出来ます。これにより対象のアカウントや環境を指定するなどより再利用性の高い運用が可能になります。
```
ecspresso <command> --ext-str <VALNERABLE>="<VALUE>"
```

### コード構成例
前述までの内容を踏まえてコードの構成例を提示します。

```
.
├── config.jsonnet
└── service.jsonnet
└── task.jsonnet
└── templates
|           ├── applicationA(service)
|           |        ├── ecs-task-def.jsonnet
|           |        └── ecs-service-def.jsonnet
|           └── applicationB(service)
|           |        ├── ecs-task-def.jsonnet
|           |        └── ecs-service-def.jsonnet
|           └── applicationC(batch)
|                    └── ecs-service-def.jsonnet
└── side-car-container
            ├── firelens.jsonnet
            └── xray.jsonnet
```

#### config.jsonnet
ecspressoコマンド実行時にecspresso内での環境変数値を外部から入力されたパラメータ、及び共通化されるパラメータを定義する。以降の各種ファイルからconfig.jsonnetファイルをインポートすることでconfig.jsonnetファイルで定義したパラメータを再利用する。
```
local env = std.extVar('ENVIRONMENT');
local target_account_id = std.extVar('AWS_ACCOUNT_ID');
local region = std.extVar('REGION');
local application = std.extVar('APPLICATION');
local image = std.extVar('IMAGE');
local cluster = std.join('-', ['psmxc', env, region, 'ecs', 'cluster']);
local service = std.join('-', ['psmxc', env, region, 'ecs', 'service', application]);

{
    env: env,
    account_id: target_account_id,
    region: region,
    cluster: cluster,
    service: service,
    application: application,
    image: image,
    // Return SSM Parameter Store path
    generateSSMPath(ssmPath):: '{{ ssm `'/' + application + '/' + ssmPath + '` }}',
    // Return SecretsManager Secret path
    generateSecretPath(secretPath, secretKey):: '{{ secretsmanager_arn `'/secrets/' + secretPath + '` }}:' + secretKey + '::',
}
```

#### service.jsonnet

ECSサービスを実装する際にどのサービスを対象とするか等を定義する。デプロイ対象がECSサービスである場合にはecspressoコマンド実行時にconfigオプションでservice.jsonnetを指定することで当該ファイルが最初に呼び出される。
```
local config = import 'config.jsonnet';
local service_definition_path = 'templates/' + config.application + '/ecs-service-def.jsonnet';
local task_definition_path = 'templates/' + config.application + '/ecs-task-def.jsonnet';

{
  region: config.region,
  cluster: config.cluster,
  service: config.service,
  service_definition: service_definition_path,
  task_definition: task_definition_path,
  timeout: '10m0s',
}
```

#### task.jsonnet

ECSタスクを実装する際にどのタスクを対象とするか等を定義する。デプロイ対象がECSサービスではなくECSタスク定義登録のみである場合にはecspressoコマンド実行時にconfigオプションでservice.jsonnetを指定することで当該ファイルが最初に呼び出される。
※主にバッチで呼び出されるECSタスクを登録する際に使用する。
```
local config = import 'config.jsonnet';
local task_definition_path = 'templates/' + config.application + '/ecs-task-def.jsonnet';

{
  region: config.region,
  cluster: config.cluster,
  task_definition: task_definition_path,
  timeout: '10m0s',
}
```
#### ecs-service-def.jsonnet
ECSサービスを定義する。config.jsonnetファイルをインポートした上で、環境差分のあるパラメータについてはconfig.jsonnetファイルからSSMパラメータストアのパス情報を取得する。
```
local config = import '../../config.jsonnet';

{
  "deploymentConfiguration": {
    "deploymentCircuitBreaker": {
      "enable": true,
      "rollback": true,
    },
    "maximumPercent": 200,
    "minimumHealthyPercent": 100
  },
  "deploymentController": {
    "type": "ECS"
  },
  "desiredCount": 1,
  "enableECSManagedTags": false,
  "enableExecuteCommand": false,
  "healthCheckGracePeriodSeconds": 0,
  "launchType": "FARGATE",
  "loadBalancers": [
    {
      "containerName": "Application",
      "containerPort": 8080,
      "targetGroupArn": config.generateSSMPath("targetGroupArn"),
    },
  ],
  "networkConfiguration": {
    "awsvpcConfiguration": {
      "assignPublicIp": "DISABLED",
      "securityGroups": [config.generateSSMPath("securityGroup")],
      "subnets": [
        config.generateSSMPathList("subnets", "0"),
        config.generateSSMPathList("subnets", "1"),
        config.generateSSMPathList("subnets", "2"),
      ],
    }
  },
  "pendingCount": 0,
  "platformFamily": "Linux",
  "platformVersion": "LATEST",
  "propagateTags": "NONE",
  "runningCount": 0,
  "schedulingStrategy": "REPLICA",
  "tags": [
    {
      "key": "Env",
      "value": config.env
    },
    {
      "key": "Name",
      "value": config.service
    }
  ]
}
```
#### ecs-task-def.jsonnet

ECSタスクを定義する。config.jsonnetファイルをインポートした上で、環境差分のあるパラメータについてはconfig.jsonnetファイルからSSMパラメータストアのパス情報を取得する。またECSタスクの環境変数に機密情報を渡す場合はSecrets Managerからパラメータを取得する。

サイドカーコンテナの定義ファイルをインポートしてコンテナ定義に追加する。
```
local config = import '../../config.jsonnet';
local xray = import '../../side-car-container/xray.jsonnet';
local firelens = import '../../side-car-container/firelens.jsonnet';
local appLogGroupName = config.generateSSMPath(config.application);
{
  "containerDefinitions": [
    {
      "cpu": 0,
      "dockerLabels": {},
      "environment": [
        {
            "name": "AWS_REGION",
            "value": config.region
        },
      ],
      "secrets": [
        {
            "name": "PASSWORD",
            "valueFrom": config.generateSecretPath('secret/path', 'password')
        },
      ],
      "essential": true,
      "image": config.image,
      "logConfiguration": {
         "logDriver": "awsfirelens",
       },
      "name": "Application",
      "portMappings": [
        { 
            containerPort: 8080,
            protocol: "tcp"
        },
      ],
    },
    xray
    sideCar.enableFirelens(appLogGroupName)
  ],
  "cpu": "1024",
  "executionRoleArn": config.generateSSMPath("executionRoleArn"),
  "family": config.application,
  "ipcMode": "",
  "memory": "2048",
  "networkMode": "awsvpc",
  "pidMode": "",
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "taskRoleArn": config.generateSSMPath("taskRoleArn")
}
```
#### side-car-container

運用機能の実装に使用するサイドカーコンテナの定義ファイルを配置する。ここでは例としてfirelensのサイドカーコンテナの設定を記載する。

```
local config = import '../config.jsonnet';

{
    enableFirelens(appLogGroupName):: {
        "cpu": 0,
        "dockerLabels": {},
        "environment": [
        {
            "name": "AWS_REGION",
            "value": config.region
        },
        {
            "name": "aws_fluent_bit_init_s3_1",
            "value": "arn:aws:s3:::" + config.prefix + "-s3-firelens-config/fluentbit-config/extra.conf"
        },
        {
            "name": "aws_fluent_bit_init_file_1",
            "value": "/fluent-bit/parsers/parsers.conf"
        },
        {
            "name": "LOG_GROUP_NAME",
            "value": appLogGroupName
        },
        {
            "name": "AWS_ACCOUNT_ID",
            "value": config.account_id
        },
        ],
        "essential": true,
        "firelensConfiguration": {
        "options": {},
        "type": "fluentbit"
        },
        "image": config.account_id + ".dkr.ecr." + config.region + ".amazonaws.com/ecr-public/aws-observability/aws-for-fluent-bit:init-latest",
        "name": "Firelens",
        "user": "0"
    }
}
```
### AWS CDKとの併用
最後にAWSの公式IaCツールであるCDKとの併用を考えてみます。
ecspresso はECSに管理スコープを絞っていることもあり、CDKと併用する場合はECSクラスターまではCDKで実装してECSサービスより上のレイヤーのリソースは ecspresso で実装することになります。ALBといったCDKで実装されるリソースはCDK側でSSMパラメータストアへの出力を追加実装しておき、SSMパラメータストアを介して ecspresso 側で参照する方法が基本になりそうです。
CDKの方がIaCツールとしての表現力は高いため、敢えて ecspresso と併用するケースはあまり無いかもしれませんが以下のような理由があれば検討してみても良いかもしれません。

- ECSリソース管理を他リソースとツールレベルで分離することで、リリースサイクルおよび担当主体の違いに起因するコミュニケーション上のトラブルを回避する（CDKでもECSリソースを実装するAppもしくはStackを分離することでデプロイを分離することは可能）
- ecspressoコマンドでデプロイを管理する

### 参考資料
https://zenn.dev/fujiwara/books/ecspresso-handbook-v2

https://github.com/kayac/ecspresso