AWS CDK の Amazon ECS Service Extensions で Fargate のタスクに好みのサイドカーを再利用性が高い記述で付与する

[AWS Containers Advent Calendar 2021](https://qiita.com/advent-calendar/2021/aws-containers)　一日目のエントリーです！

AWS Cloud Development Kit　(CDK) は、AWS のインフラを TypeScript や Java などのプログラミング言語で記述できる IaC ツールです。

そして、 CDK では、 Amazon Elastic Container Service (Amazon ECS) のクラスターやタスク、サービスを簡単に定義できるようになっています。例えば、 `ApplicalLoadBalancedFargateService`　のようなクラスを使うだけで、 ApplicationLoadBalancer がフロントに立つ Fargate のサービスを作成できます。

一方で、「CloudWatchAgent を追加したい」「XXX なエージェントを追加したい」「XXX 用の権限を追加したい」となると、 例えば `ApplicalLoadBalancedFargateService` のクラス構造を掘り返し、必要なプロパティを持ってきてそこにいろいろ手を加えるなど、一手間必要になり、クラスの再利用なども容易でない、という課題があります。

そこで、  [ECS Service Extensions](https://www.npmjs.com/package/@aws-cdk-containers/ecs-service-extensions) の出番です。
この CDK のモジュールでは、基本的な ECS のサービスを作成し、そこに様々な拡張を付け足していく、というアスペクト指向的なアプローチでアプリケーションを定義することができるようになっていて、より、再利用性が高い記述が可能です。

例えば、 CloudWatch Agent を付けた ECS のサービスを定義する場合、以下のようになります。

```typescript
import * as cdk from "@aws-cdk/core";
import * as ec2 from "@aws-cdk/aws-ec2";
import * as ecs from "@aws-cdk/aws-ecs";
import * as ecsServiceExtensions from "@aws-cdk-containers/ecs-service-extensions";
import * as ecr from "@aws-cdk/aws-ecr";

export class ServiceStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 既存のクラスターのインポート
    const vpc = ec2.Vpc.fromLookup(this, "ClusterVpc", { vpcId: "vpc-xxxxxxxxxxx" });
    const cluster = ecs.Cluster.fromClusterAttributes(this, "Cluster", {
      vpc, clusterName: "sample-ecs-cluster", securityGroups: []
    });

    // サービス作成先のクラスター環境定義
    const environment = ecsServiceExtensions.Environment.fromEnvironmentAttributes(this, "Environment", {
      capacityType:  ecsServiceExtensions.EnvironmentCapacityType.FARGATE,
      cluster,
    });

    // 基本の Fargate サービスを構築
    const repository = ecr.Repository.fromRepositoryName(this, 'ApplicationRepository', "literalice/sample");
    const image = ecs.ContainerImage.fromEcrRepository(repository, "latest");

    const description = new ecsServiceExtensions.ServiceDescription();
    description.add(new ecsServiceExtensions.Container({
        image,
        cpu: 512,
        memoryMiB: 1024, 
        trafficPort: 8080,
    }));

    // Load Balancer を立てる
    description.add(new ecsServiceExtensions.HttpLoadBalancerExtension());

    // CloudWatch Agent を付ける
    description.add(new ecsServiceExtensions.CloudwatchAgentExtension());

    new ecsServiceExtensions.Service(this, "Service", {
      environment,
      serviceDescription: description,
    });
  }
}
```

上記では、Extension として組み込みの `HttpLoadBalancerExtension` と `CloudwatchAgentExtension` を利用しており、これにより、 ロードバランサーを利用し、 CloudWatch Agent をアタッチした Fargate Service になっているわけですね。このあたりの詳細については以下の記事に詳しいので、ぜひご参照ください。

https://aws.amazon.com/jp/blogs/containers/general-availability-amazon-ecs-service-extensions-for-aws-cdk/

さて、この Extension ですが、自作が容易にできます。例えば、共通ライブラリとして Extension を作っておけば、いろいろなサービスで使い回すことが可能です。
`ApplicalLoadBalancedFargateService` のような高レベルのクラスを様々なパラメーターでインスタンス化させる、のような手法もあるかと思いますが、より整理された方法で再利用性を高めることができますね。

というわけで、この記事では、Extension を自作してみようと思います。上の例では組み込みの `CloudwatchAgentExtension` を利用していますが、これを改良したものを作ってみます。具体的には、

* CloudWatch Agent のコンテナイメージを [ECR Public](https://gallery.ecr.aws/cloudwatch-agent/cloudwatch-agent) から持ってくる
* CloudWatch Agent が出力するメトリクスの名前空間をアプリ独自のものにできるようにする
    * デフォルトでは、 `CWAgent` に全て出力されますので、これを変更したい、ということですね

やることとしては、組み込みの `CloudwatchAgentExtension` をコピーしてきて、上記の修正を入れるだけです！

組み込みの `CloudwatchAgentExtension` のコードは以下にあります。

https://github.com/aws/aws-cdk/blob/v1.134.0/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/cloudwatch-agent.ts

これを、以下のように修正してみました。

```typescript
import * as ecs from "@aws-cdk/aws-ecs";
import * as ecr from "@aws-cdk/aws-ecr";
import * as iam from "@aws-cdk/aws-iam";
import { Service } from "@aws-cdk-containers/ecs-service-extensions";
import { ServiceExtension } from "@aws-cdk-containers/ecs-service-extensions";

import { Construct } from "@aws-cdk/core";

const CLOUDWATCH_AGENT_IMAGE = "public.ecr.aws/cloudwatch-agent/cloudwatch-agent:latest";

/**
 * This extension adds a CloudWatch agent to the task definition and
 * configures the task to be able to publish metrics to CloudWatch.
 */
export class CloudwatchAgentExtension extends ServiceExtension {
  private CW_CONFIG_CONTENT: {};

  constructor(metricsNamespace: string) {
    super("cloudwatchAgent");

    // 環境変数でコンテナに渡される、 CloudWatch Agent の設定
    this.CW_CONFIG_CONTENT = {
      logs: {
        metrics_collected: {
          emf: {},
        },
      },
      metrics: {
        namespace: metricsNamespace,
        metrics_collected: {
          statsd: {},
        },
      },
    };
  }

  public prehook(service: Service, scope: Construct) {
    this.parentService = service;
    this.scope = scope;
  }

  public useTaskDefinition(taskDefinition: ecs.TaskDefinition) {
    // Add the CloudWatch Agent to this task
    this.container = taskDefinition.addContainer("cloudwatch-agent", {
      image: ecs.ContainerImage.fromRegistry(CLOUDWATCH_AGENT_IMAGE),
      environment: {
        CW_CONFIG_CONTENT: JSON.stringify(this.CW_CONFIG_CONTENT),
      },
      logging: new ecs.AwsLogDriver({ streamPrefix: "cloudwatch-agent" }),
      user: '0:1338', // Ensure that CloudWatch agent outbound traffic doesn't go through proxy
      memoryReservationMiB: 50,
    });

    // ECR Public 用のトークンを取得する
    if (taskDefinition.executionRole) {
      ecr.PublicGalleryAuthorizationToken.grantRead(taskDefinition.executionRole);
    }

    // Add permissions that allow the cloudwatch agent to publish metrics
    new iam.Policy(this.scope, `${this.parentService.id}-publish-metrics`, {
      roles: [taskDefinition.taskRole],
      statements: [
        new iam.PolicyStatement({
          resources: ["*"],
          actions: ["cloudwatch:PutMetricData"],
        }),
      ],
    });
  }

  // ...
  }
}
```

これを、以下のように利用するだけで、 好みの設定で CloudWatch Agent を Fargate タスクに組み込むことができます。

```typescript
// ...
import { CloudwatchAgentExtension } from "./cloudwatch-agent-extension";

// ...

description.add(new CloudwatchAgentExtension("sample-app-metrics"));

// ...
```

この記事では、ECS Service Extensions を使って、再利用性の高い記述で ECS サービスを構築する方法を CloudWatch Agent のサイドカーを組み込むコードを例に紹介しました！

ぜひ、皆様のサービスでもご活用いただければと思います。