ECS Fargate上で動くコンテナに対し、任意のコマンドを実行する方法の検討,考察,採択

この記事は[うるるAdvent Calendar 2022](https://qiita.com/advent-calendar/2022/uluru)の15日目の記事です。

# はじめに
タイトルの通りですが、ECS Fargate上で動くコンテナアプリケーションに対し、任意のコマンドを実行したいという要件がありました。
上記を実現する方法はいくつかあり、またそれぞれにメリット,デメリットがあったので、どの方法を採択したか、検討の流れ含め記載していこうと思います。

### この記事で触れること
- Fargate上で動くコンテナに対し、任意のコマンドを発行するいくつかの方法の概要紹介
- それぞれの方法について、メリット,デメリット,要件と照らし合わせた評価・考察・採択/不採択

### この記事で触れないこと
- 各方法の設定, 実行方法詳細（詳細まで記載すると長くなってしまうので、ポイントだけ記載させて頂きます)

# 前段
「ECS Fargate上で動くコンテナアプリケーションに対し、外部から任意のコマンドを実行したい」という要件ですが、より詳細には以下です。

- Fargateをデータプレーンとし、タスクが1台常時稼働している
- ↑のコンテナ内でcrondプロセスが常駐しており、複数ある定時処理(Batch処理)が実行されている
- 一方で、**登録された時間を待たずに任意のタイミングで、Batch処理を手動実行できるようにしたい**

![fargate1 (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/0b6c5ca6-34f9-cb67-a5c4-6975fe5fd4c1.jpeg)

また、要件を満たした上で、なるべく最適な実現方法を探っていきたいものです。
そこで今回は、以下3つの観点からもそれぞれ方法を評価し、採択/不採択の判断をしました。

- **運用面**（操作のしやすさ, 保守コスト等）
- **セキュリティ面**
- **コスト面**

# 方法1: ECS Exec を用いてコンテナに接続する
Fargate上で動くコンテナに対するコマンド実行方法を考えた時、最初に思いついたのがこちらの方法です。
[AWS公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/ecs-exec.html)でも紹介されており、結構一般的な方法かなと思います。
![fargate1 (2).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/47f0521d-1969-941e-da54-f903f75a34a7.jpeg)


## 実現方法
[AWS公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/userguide/ecs-exec.html)で記載されている流れで実施ができるかと思います。

- 実行対象のECSタスクロールに、SSMアクセス関連のポリシーをアタッチする
- 実行対象タスクが属するECSサービスについて、`--enable-execute-command`フラグを有効し、サービスを更新する

ということは忘れがちなので、注意が必要です。
↓実行対象のECSタスクロールにアタッチが必要なポリシー

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

## コマンド実行方法
手元のPCから、`aws ecs execute-command`コマンドを実行することでコンテナに接続 & コマンド実行可能です。
```shell
$ aws ecs execute-command \
    --cluster <target-cluster-id> \
    --task <target-task-id> \
    --container <target-container-name> \
    --interactive \
    --command "実行したいコマンド"
```

## 所感
### メリット
- 設定, 接続方法が簡易
- 追加費用なしで利用可能

### デメリット
- AWS CLIを用いて接続するので、手元のPCにIAMユーザアクセスキーなど機密情報を置く必要がある。

### 評価・考察

|観点  |評価  |理由 |
|---|---|---|
|要件(任意のタイミングで、任意のコマンドを実行可能)を満たしているか  |○  |`aws ecs execute-command`コマンド実行により可能   |
|運用面  |○  |設定, 実行方法ともに簡易  |
|セキュリティ面  |△  |AWS CLIを用いる必要があり、手元にIAMユーザ機密情報を置かないといけない  |
|コスト面  |○  |[追加費用なし](https://aws.amazon.com/jp/about-aws/whats-new/2021/03/amazon-ecs-now-allows-you-to-run-commands-in-a-container-running-on-amazon-ec2-or-aws-fargate/)で利用可能  |

運用面, コスト面では優れていて、気軽に使える方法かと思います。
一方、実行するためには手元にIAMユーザ機密情報を置く必要があるため、セキュリティ面では若干不安がありました。

今回の要件では特に、開発者全員が実行できるようにしたかったため、機密情報を各開発者ローカルPCに配置することによるセキュリティリスクは大きいと考え、不採択としました。

# 方法2: SessionManager経由でコンテナに接続する
Systems Managerの機能である SessionManager を使って、Fargate上で動くコンテナに接続することができます。
SessionManager経由でインスタンスに接続する方法は、通常EC2などで使われます。
一方、コンテナ自体をマネージドインスタンスに登録することで、コンテナ自体をサーバーと見立て、接続が可能となります。
![fargate1 (3).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/908fec8a-5aa0-ce8f-3b26-4e61aa080454.jpeg)

## 実現方法
1.Systems Manager > フリートマネージャー > 設定 > インスタンス枠 より、インスタンス枠をスタンダードからアドバンスドに変更します。（扱い的には、オンプレミスインスタンスにアクセスする形になるため）
![スクリーンショット 2022-12-13 11.27.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/c5069aff-3b5d-8a45-a157-666059ac73b3.png)

2.接続開始時のユーザは`ssm-user`となります。利用コンテナに存在しない場合は、接続前に作成が必要です。
以下は、Alpine Linux系のイメージを使っている場合の作成例。

```shell
apk add sudo
adduser -D ssm-user
echo "ssm-user ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/ssm-agent-users
```

3.対象コンテナをマネージドインスタンスに登録します。コンテナ起動時に以下処理を実行することで登録可能です。（起動シェルなどに記述する）

```shell
# ハイブリッドアクティベーションの作成
ACTIVATE_ROLE_NAME="AmazonSSMManagedInstanceCoreポリシーを保有するロール"
SSM_ACTIVATE_INFO=`aws ssm create-activation \
    --iam-role $ACTIVATE_ROLE_NAME \
    --registration-limit 1 \
    --region ap-northeast-1 \
    --default-instance-name target-container-name-$(date "+%Y%m%d-%H%M%S")`
SSM_ACTIVATE_CODE=`echo $SSM_ACTIVATE_INFO | jq -r '.ActivationCode'`
SSM_ACTIVATE_ID=`echo $SSM_ACTIVATE_INFO | jq -r '.ActivationId'`

# SSMエージェントのシンボリックリンクを作成する
# Fargateプラットフォームバージョン1.4.0以降では、既にSSMエージェントがインストールされているため、新規導入は不要
ln -s /managed-agents/execute-command/amazon-ssm-agent /usr/bin/amazon-ssm-agent
ln -s /managed-agents/execute-command/ssm-agent-worker /usr/bin/ssm-agent-worker
ln -s /managed-agents/execute-command/ssm-session-worker /usr/bin/ssm-session-worker

# コンテナ自身をマネージドインスタンスへ登録
amazon-ssm-agent -register -code $SSM_ACTIVATE_CODE -id $SSM_ACTIVATE_ID -region "ap-northeast-1"
rm -rf /var/lib/amazon/ssm/ipc

# SSMエージェントの起動
# より厳密にデーモン化させたい場合は Supervisor などで管理する
amazon-ssm-agent &
```

↓アクティベーション
![スクリーンショット 2022-12-13 12.24.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/4dc2d51a-884c-c4c5-d9b2-e97308f4907c.png)
![スクリーンショット 2022-12-13 12.24.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/888ef5d9-38f5-849e-2fb4-9e6d71a174a2.png)

マネージドインスタンスに登録されます（`mi`という接頭辞がつきます）
![スクリーンショット 2022-12-13 12.27.31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/1fb77bf7-dbed-f9ee-ad58-7233d67c478a.png)

## コマンド実行方法
AWS Systems Manager > Session Manager > セッションの開始 　にて、対象コンテナをターゲットインスタンスで選択することで、コンテナに接続 & コマンド実行が可能です。
![スクリーンショット 2022-12-13 12.30.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/1c7a9dce-3aae-bef0-112c-ff47b7d9ad01.png)

もしくは、AWS Systems Manager > Run Command > AWS-RunShellScript を利用することで、コンテナ内には入らず、コマンドだけ実行することもできます。
（ここでも、対象コンテナをターゲットインスタンスで選択 & 実行します）

## 所感
### メリット
- AWSマネージドコンソール上で操作が完結するので、手元のPCにIAMユーザ機密情報を置かなくて良い
- 「いつ, 誰が操作したか」がCloudTrailに記録されるので、操作の透明性が担保できる

### デメリット
- 設定方法がやや煩雑（特に、実行コンテナイメージに対して色々記述を追加する必要がある）

### 評価・考察

|観点  |評価  |理由 |
|---|---|---|
|要件(任意のタイミングで、任意のコマンドを実行可能)を満たしているか  |○  |SessionManager or RunCommand経由でコマンド発行可能   |
|運用面  |△  |接続方法はシンプルだが、設定方法がやや煩雑で、保守コストがかかる懸念がある  |
|セキュリティ面  |○  |AWSマネージドコンソール上で操作が完結するので、手元のPCにIAMユーザ機密情報を置かなくて良い。また「いつ」「誰が」操作したのかが、CloudTrailに記録されるため、操作透明性も優れている。  |
|コスト面  |△  |[アドバンストインスタンス層の料金がかかる](https://aws.amazon.com/jp/systems-manager/pricing/)(アドバンスドオンプレミスインスタンスごとに時間あたり`0.00695 USD`） |

「AWSマネージドコンソール上で操作が完結する」「操作者の記録が残る」という点において、セキュリティ面は優秀だと言えます。
一方で、設定方法がやや煩雑（稼働中コンテナに影響を及ぼす設定調整を色々している）ということがあり、保守コストがかかる懸念がありました。
またコストについても軽微ですが、ECS Execと比較すると一定かかってくるということもあり、△としています。

総合的に見て、致命的なリスクはないため悪くない方法だと思いましたが、運用面(保守コスト)の懸念がありましたので、不採択としました。

# 方法3: Step Functionを用いて一時的にタスクを起動 & コマンド実行する
Step Function経由で一時タスクを起動 & 任意のコマンドを引き渡すことで、実行コンテナと同一環境にてコマンドを実行する方法です。
![fargate1 (4).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/676dac50-92ca-f141-47ec-424a336a4fa6.jpeg)

## 実現方法
タスクを起動するためのステートマシンを作成します。
ステートメントは以下のように定義します。

```json
{
  "Comment": "Execute Command By Running ECS Task",
  "StartAt": "Execute Command",
  "TimeoutSeconds": <タイムアウト時間>,
  "States": {
    "Execute Command": {
      "Type": "Task",
      "Resource": "arn:aws:states:::ecs:runTask.sync",
      "Parameters": {
        "Cluster": <実行対象ECSクラスター>,
        "TaskDefinition": <実行対象ECSタスク定義>,
        "LaunchType": "FARGATE",
        "PlatformVersion": "LATEST",
        "NetworkConfiguration": {
            "AwsvpcConfiguration": {
                "AssignPublicIp": "<ENABLED / DISABLED>",
                "SecurityGroups": ["<セキュリティグループID>"],
                "Subnets": ["<サブネットID>"]
            }
        },
        "Overrides": {
          "ContainerOverrides": [
            {
              "Name": "container-name",
              "Command.$": "$.commands",
              "Environment": [
                {
                  "Name": "上書きしたい環境変数名",
                  "Value": "値"
                }
              ]
            }
          ]
        }
      },
      "End": true
    }
  }
}
```

ポイントは `Overrides.ContainerOverrides.Command.$`です。上のように記述することにより、開始時に実行コマンドを渡すことができ、任意のコマンドを実行できるようになります。
その他、`Environment`の記述で、起動タスクの環境変数を上書きすることもできます。

ステートマシンに割り当てるポリシーなどは、以下が参考になるかと思います。
https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/ecs-iam.html

## コマンド実行方法
Step Functions > ステートマシン > ↑で作成したステートマシン > 実行の開始 をクリック。
その後、実行したいコマンドを入力の上「実行の開始」で、ECSタスクが起動し、その中でコマンドが実行されます。
※ コマンド実行完了後は、起動したECSタスクは自動で停止(終了)します。その間、常駐して動いているECSタスクが停止する などはありません。
![スクリーンショット 2022-12-14 3.03.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/351743/2e3e77c2-dcac-e61a-2132-05a79eff5905.png)

## 所感
### メリット
- AWSマネージドコンソール上で操作が完結するので、手元のPCにIAMユーザ機密情報を置かなくて良い
- 「いつ, 誰が操作したか」がCloudTrailに記録されるので、操作の透明性が担保できる
- 設定方法, 実行方法ともに比較的簡易。

### デメリット
- 稼働中の(常駐している)コンテナに対してのコマンド発行はできない。そのため、コンテナ内部構成, 状態に依存するようなコマンドは実行することができない。

### 評価・考察

|観点  |評価  |理由 |
|---|---|---|
|要件(任意のタイミングで、任意のコマンドを実行可能)を満たしているか  |○  |StepFunctions経由でECSタスク起動 & コマンド発行可能  |
|運用面  |○  |設定方法, 実行方法ともに比較的簡易  |
|セキュリティ面  |○  |AWSマネージドコンソール上で操作が完結するので、手元のPCにIAMユーザ機密情報を置かなくて良い。また「いつ」「誰が」操作したのかが、CloudTrailに記録されるため、操作透明性も優れている。  |
|コスト面  |△  |[4000/月の状態遷移は無料利用可。以降は0.025USD/1000回の状態遷移](https://aws.amazon.com/jp/step-functions/pricing/) + ECSタスク稼働時間 |

要件, 運用面, セキュリティ面は懸念事項がありませんでした。
コストについては、コマンド実行機会は月に限られた回数が想定されていますので、StepFunctionsについては無料利用枠に収まる範囲で利用可能。ECSタスクについては起動時間分の料金がかかるので △ としていますが、実際には問題にならない料金だと評価しました。

こちらの方法のデメリットとしては上にも記載の通り、稼働中のコンテナに対してのコマンド発行はできない = コンテナ内部構成, 状態に依存するようなコマンドは実行不可 というところです。今回のケースだと、そのようなコマンドを任意で叩きたい用途はなかったので、こちらについても問題がないとしています。

従って、方法3については実装,運用に際し懸念などありませんでしたので、こちらの方法を採択しました。

# まとめ
それぞれの方法の観点を改めてまとめると

|  |要件を満たしているか  |運用面 |セキュリティ面 |コスト面　|メリット |デメリット |
|---|---|---|---|---|---|---|
|方法1  |○  |○  |△ |○ |コストをかけず手軽に実施可能 |IAM認証情報を手元に置く必要あり |
|方法2  |○  |△  |○ |△ |セキュアに実施可能 |設定方法が複雑,運用コストあり |
|方法3  |○  |○ |○ |△ |セキュア & 手軽に実施可能 |稼働中のコンテナに対してコマンド発行はできない |

- 方法1: IAM認証情報をローカルPCに配置することを許容できれば、最も簡単に実施できる方法だと思います。
- 方法2: マネージドコンソール内で操作が完結(実施のためのIAMアクセスキー等発行する必要なし) & 稼働中のコンテナに対しコマンド発行ができます。運用,費用面でコストがかかるものの、稼働中のコンテナに対し安全な方法でコマンド発行したい場合に有効な方法です。
- 方法3: 稼働中コンテナに対してコマンド発行する必要がなければ、安全かつ比較的簡易な方法でコマンド発行できる方法です。

今回の要件では、稼働中コンテナに対してコマンド発行する必要がなく、また安全な方法でコマンド発行したかったので、総合的に見て方法3を採択しました。
いずれにしても、要件を細分化した上でメリット,デメリットを明らかにし、要件と照らし合わせていくと、最適な方法が見つかるかと思います。
（※ちなみに、「コマンド実行」ではなく「コンテナ接続」が要件の場合は、方法,採択も変わってくるかと思います）

明日は[@d510](https://qiita.com/d510)さんの記事です！お楽しみに！