ECS On Fargateを利用する際に検討すべき非機能要件

# 背景
少し前にAWS Elastic BeanstalkからECS On Fargate（以下、Fargate）への移行を行いました。この移行に際し、注意すべき非機能要件がいくつかありましたので、それについて記載します。

# ロギング

ECSのコンテナは必要に応じて起動・削除されるため、コンテナ内にログを蓄積しておくと削除されてしまいます。そのため、ログドライバーを利用して外部にログを転送する必要があります。

ECSのロギングの設定には2つのパターンがあります。

CloudWatchLogsにログを送信するawslogsログドライバーを利用するパターン
CloudWatchLogsにログを送信する形式です。
送信先はCloudWatchLogsしか選択できないため、コスト面が気になる場合やCloudWatchLogs以外のサービスにログを流したい場合などは後続のawsfirelensログドライバーを選択した方が良いでしょう。
こちらのメリットとしては、ログドライバーを設定するだけで済むので設定が簡単です。
自分が担当しているサービスはCloudWatchLogsにサービスのログを集約する運用だったため、こちらのロギングを選択しました。

```typescript:cdk
    taskDefinition.addContainer("NginxContainer", {
      image: ecs.ContainerImage.fromDockerImageAsset(nginxImageAsset),
      //ログドライバー設定
      logging: ecs.LogDrivers.awsLogs({
        logGroup,
        streamPrefix: `nginx`,
      }),
      portMappings: [
        {
          containerPort: 80,
          hostPort: 80,
        },
      ],
      readonlyRootFilesystem: true,
    });
```


1. CloudWatchLogs以外にログを送信したい場合のawsfirelensログドライバーを利用するパターン
FireLensを使い、指定したサービスにログを送信します。自分が担当したサービスではこちらの設定を行いませんでしたが、検討した際に下記ページに詳しく記載があったため紹介させていただきます。

https://zenn.dev/thefirstpenguin/articles/3eb5c15fd3aba3

# RootファイルのReadOnly設定

コンテナ内への攻撃者の侵入を許してしまった場合、コンテナ内のルートファイルへの書き込みが可能な状態では、被害が拡大する恐れがあります。そのため、アプリケーション上の制約がなければ可能な限りRootファイルのReadOnly設定を有効にしておくことをお勧めします。

```typescript:cdk
    taskDefinition.addContainer("NginxContainer", {
      image: ecs.ContainerImage.fromDockerImageAsset(nginxImageAsset),
      logging: ecs.LogDrivers.awsLogs({
        logGroup,
        streamPrefix: `nginx`,
      }),
      portMappings: [
        {
          containerPort: 80,
          hostPort: 80,
        },
      ],
      //RootファイルのReadOnly設定
      readonlyRootFilesystem: true,
    });
```



# スケーリング設定

Fargateのスケーリング設定はサービス構成の中で設定します。
AutoScalingを設定し、maxおよびmin capacityを設定することで、タスクの必要数を増減させることができます。
可用性を向上させるためにも、こちらの設定は入れておく必要があります。

```typescript:cdk
    // Auto Scaling設定
    const scaling = service.autoScaleTaskCount({
      minCapacity,
      maxCapacity,
    });
    scaling.scaleOnRequestCount("RequestScaling", {
      requestsPerTarget: 100, 
      targetGroup,
    });
```


# ECRのイメージスキャン
ECSを利用する際にはECRにイメージをプッシュするのが一般的ですが、ECRにはプッシュされたイメージをスキャンして、コンテナイメージ内のソフトウェアの脆弱性を特定してくれる機能があります。スキャンにはベーシックスキャンと拡張スキャンの2種類があります。

- ベーシックスキャン
ベーシックスキャンでは、プッシュ時にスキャンするようにリポジトリを設定します。
手動スキャンを実行するとAmazon ECRによってスキャン結果のリストが提供されます。また、スキャンは24時間に1回スキャンできます。
<br>

- 拡張スキャン
ECRはAmazon Inspectorと統合され、リポジトリの自動継続的なスキャンを提供します。コンテナイメージは、オペレーティングシステムとプログラミング言語パッケージの両方の脆弱性についてスキャンされます。新しい脆弱性が発生すると、スキャン結果が更新され、Amazon InspectorはEventBridgeにイベントを発行して通知します。
<br>

下記はベーシックスキャンの設定をcdkで行った例です。

```typescript:cdk
    const serviceRepository = new ecr.Repository(this, "ServiceRepository", {
      repositoryName: `service-repo`,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      emptyOnDelete: true,
      imageScanOnPush: true,
    });
```

### ECRのイメージスキャンの注意点

一般的なCI/CDでは、イメージプッシュ前にスキャンを実施し、デプロイを許可しないようにしていることが多いです。

![ecr_scan.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268460/7423d7d7-267b-1e6f-a14d-54648df2e608.png)


ECRのイメージスキャンの場合、スキャン実行は非同期で行われるため、イメージビルド時に脆弱性があってもイメージプッシュを止められません。そのため、下記のような運用や対策が必要になります。

- ECRのスキャン結果を通知する
開発環境などで本番環境デプロイ前に脆弱性の検知を拾える仕組みを作ります。弊社ではECRのスキャン結果をSlackに通知することで、異常検知された場合、本番環境へのデプロイを停止する運用をしています。

```typescript:cdk
interface StackProps extends cdk.StackProps {
  readonly accountName: string;
  readonly infoEmail: string;
}
export class InfoStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: StackProps) {
    super(scope, id, props);
    const { accountName, infoEmail } = props;
    const topic = new Topic(this, "Topic", {
      displayName: `${accountName}-info`,
      topicName: `${accountName}-info-topic`,
    });
    topic.addToResourcePolicy(
      new PolicyStatement({
        actions: ["sns:Publish"],
        effect: Effect.ALLOW,
        principals: [new AnyPrincipal()],
        resources: ["*"],
      })
    );
    topic.addSubscription(new EmailSubscription(infoEmail));
    new StringParameter(this, "ParameterInfoTopicArn", {
      parameterName: `/account/info-topic-arn`,
      stringValue: topic.topicArn,
    });
    // ECR Image Scan Info
    new Rule(this, "EcrImageScanRule", {
      ruleName: `${accountName}-ecr-image-scan-rule`,
      eventPattern: {
        source: ["aws.ecr"],
        detailType: ["ECR Image Scan"],
        detail: {
          "scan-status": ["COMPLETE"],
        },
      },
      targets: [new SnsTopic(topic)],
    });
  }
}

```

# GuardDuty ECS ランタイムモニタリング

GuardDutyのECSランタイムモニタリングを有効にすることで、現在稼働しているECSクラスターの脅威検知をしてくれます。検知できる内容としては下記があります。

- 不審なアクティビティのプロセスIDの把握
- 脆弱性があるコンテナイメージの特定

https://docs.aws.amazon.com/ja_jp/guardduty/latest/ug/how-runtime-monitoring-works-ecs-fargate.html

ECSランタイムモニタリングを有効にすると、ECSのタスクにaws-guardduty-agentというコンテナが起動されるようになります。

![aws-guardduty-agent.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268460/0991d41b-5a45-8c63-f192-e810292cff66.png)


また、GuardDutyのランタイムモニタリングからランタイムカバレッジを参照すると、モニタリングしているクラスターリストが確認できます

![GuardDutyRuntimeMonitoring.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/268460/4ff9c7c3-f276-09e4-fab2-76416c3f7c63.png)


### GuardDuty ECS ランタイムモニタリングの導入時の注意点

ECSランタイムモニタリングを有効にすると、ECSのタスクにaws-guardduty-agentのコンテナが追加されます。そのため、タスクの実行ロールにGuardDutyエージェントのリポジトリに対するイメージのPull権限が必要です。対象のECSサービスのタスクがVPC内のプライベートサブネットにある場合、ssmmessagesのVPCエンドポイントが必要になります。

```typescript:cdk
    const executionRole = new iam.Role(this, "ExecutionRole", {
      assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
      managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonECSTaskExecutionRolePolicy")],
      inlinePolicies: {
        CustomPolicy: new iam.PolicyDocument({
          statements: [
            new iam.PolicyStatement({
              actions: ["ecr:ListImages", "ecr:DescribeImages"],
              resources: [`arn:*:ecr:*:*:repository/aws-guardduty-agent-fargate`],
              effect: iam.Effect.ALLOW,
            }),
          ],
        }),
      },
    });

```

[GuardDuty エージェントのリポジトリ一覧](https://docs.aws.amazon.com/ja_jp/guardduty/latest/ug/ecs-runtime-agent-ecr-image-uri.html)

# ECS Exec

ECS ExecはECSコンテナ内にSystems Manager Session Managerの機能を利用してログインする機能です。アプリケーションやコンテナ内部での問題が発生した場合、調査や復旧の目的で利用します。

ECS Execを利用するには、主に下記の設定が必要です。

1. ECSのタスクロールに権限を追加

    ```json:policy
    {
       "Version": "2012-10-17",
       "Statement": [
           {
           "Effect": "Allow",
           "Action": [
                "ssmmessages:CreateControlChannel",
                "ssmmessages:CreateDataChannel",
                "ssmmessages:OpenControlChannel",
                "ssmmessages:OpenDataChannel"
           ],
          "Resource": "*"
          }
       ]
    }
    ```
    <br>

1. ECS Execの有効化

    下記はcdkでの有効化設定の例です。サービスの設定でECS Execを有効化する必要があります。

    ```typescript:cdk
            const service = new ecs.FargateService(this, "Service", {
              cluster,
              taskDefinition,
              desiredCount: minCapacity,
              securityGroups: [ecsSecurityGroup],
              vpcSubnets: {
                subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
              },
              circuitBreaker: {
                rollback: true,
              },
              enableExecuteCommand: true, // ECS Execの有効化
            });
    ```
    <br>


1. VPC Endpoint
対象のECSサービスのタスクがVPC内のプライベートサブネットにある場合、`ssmmessages`のVPCエンドポイントが必要です。




### 参考

https://zenn.dev/thefirstpenguin/articles/3eb5c15fd3aba3

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/service-auto-scaling.html

https://docs.aws.amazon.com/ja_jp/AmazonECR/latest/userguide/image-scanning.html

https://speakerdeck.com/gotok365/cdk-container-image-scan

https://dev.classmethod.jp/articles/amazon-ecr-notify-slack-using-chatbot/

https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-exec.html

https://blog.serverworks.co.jp/ecs-exec