【アップデート】ECS on Fargateでサイドカーからプロセス/システムコール監視が簡単にできるようになりました!

ECS on FargateでPIDの名前空間をタスク内のコンテナ間で共有できるようになったので動かしてみました。
2023.08.21

はじめに

CX事業本部の佐藤智樹です。最近のアップデートでECS on FargateでPIDの名前空間をタスク内のコンテナ間で共有できるようになりました!以下の記事にアップデート後の出来ることや嬉しい点が書かれています。

上記記事で紹介されているLinuxカーネルパラメータの設定に関しては、以下の記事でも解説されております。本記事はpidModeの方にフォーカスしてご紹介します。

このアップデートの何が嬉しいのか

これまでは本機能がなかったのでサイドカー側から本体のコンテナのプロセスやシステムコールの取得が難しくかったです。代表的な商用製品だとサイドカーコンテナ上で証跡取得用のプロセスを動かし、サイドカーのボリュームを本体側でマウントして動かす必要がありました。なので、以前までは商用製品を利用しないと、本体側のコンテナをReadOnly状態で運用したとしても、不審なプロセスやシステムコール、想定外の外部ネットワークへのアクセスが発生しているかなどの証跡管理、不審な動作時の検知などを行うことがかなり難しかったです。商用製品を購入できない場合、ECS on EC2を選択するしかないパターンもあったかと思います。

今回のアップデートにより、サイドカー側からプロセスやシステムコールを簡単に確認できるようになりました。これにより、証跡の取得は比較的簡単にできます。今後この機能を使って多くの便利な商用製品が出てくることも期待できます。また注意点を最後まで読むと分かるのですが、ディレクトリも共有されるので、書き込み可能領域に不審なファイルが増えていないかの監視もできます。

やってみた

ここからは実際にPID共有がどのように表示されるのか、サイドカーからのプロセス停止などを試してみます。検証用の環境はCDKで構築します。

検証環境

項目名 バージョン
AWS CDK 2.91.0
Node.js 18.16.1

検証環境用のコード

検証のため、VPC、ECSタスク実行ロール、ECSタスクロール、ECSクラスター/サービス/タスクを作成します。ECSタスクのコンテナは検証用なのでPublicサブネットに配置します。現状CDKではpidModeを指定できないので、デプロイ後にタスク定義を修正して更新します。

lib/fargate_shared_namespace_test-stack.ts

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as iam from "aws-cdk-lib/aws-iam";

export class FargateSharedNamespaceTestStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const myVpc = new ec2.Vpc(this, `${id}-Vpc`, {

      ipAddresses: ec2.IpAddresses.cidr("10.100.0.0/16"),
      maxAzs: 2,
      natGateways: 1,
      flowLogs: {},
      subnetConfiguration: [
        {
          cidrMask: 24,
          name: "Public",
          subnetType: ec2.SubnetType.PUBLIC,
        }
      ],
    });

    // Roles
    const executionRole = new iam.Role(this, `${id}-EcsTaskExecutionRole`, {
      assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
      managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("service-role/AmazonECSTaskExecutionRolePolicy")],
    });
    const serviceTaskRole = new iam.Role(this, `${id}-EcsServiceTaskRole`, {
      assumedBy: new iam.ServicePrincipal("ecs-tasks.amazonaws.com"),
      managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMFullAccess")],
    });

    // ECS Task
    const serviceTaskDefinition = new ecs.FargateTaskDefinition(this, `${id}-ServiceTaskDefinition`, {
      cpu: 512,
      memoryLimitMiB: 1024,
      executionRole: executionRole,
      taskRole: serviceTaskRole,
    });

    serviceTaskDefinition.addContainer(`${id}-nginxContainer`, {
      containerName: "main",
      image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
    });
    serviceTaskDefinition.addContainer(`${id}-sleeperContainer`, {
      containerName: "sleeper",
      image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"),
      command: ["sleep", "infinity"],
    });

    const cluster = new ecs.Cluster(this, `${id}-Cluster`, {
      vpc: myVpc,
    });

    const fargateService = new ecs.FargateService(this, `${id}-FargateService`, {
      cluster,
      vpcSubnets: myVpc.selectSubnets({ subnetGroupName: "Public" }),
      taskDefinition: serviceTaskDefinition,
      desiredCount: 1,
      maxHealthyPercent: 200,
      minHealthyPercent: 50,
      enableExecuteCommand: true,
      assignPublicIp: true,
    });
  }
}

上記デプロイ完了後、一度SSMでサイドカーコンテナにログインし、PIDが共有されていないことを確認します。

% aws ecs execute-command \
    --cluster "clusterName" \
    --task xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
    --container sleeper \
    --interactive \
    --command "/bin/bash"

bash-4.2% yum -y install procps
bash-4.2% ps -aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0   4299   749 ?        Ss   07:01   0:00 sleep infinity
root         6  0.0  0.1 1322999 14999 ?       Ssl  07:01   0:00 /managed-agents/execute-command/amazon-ssm-agent
root        21  0.0  0.3 1408999 24999 ?       Sl   07:01   0:00 /managed-agents/execute-command/ssm-agent-worker
root        30  0.1  0.2 1326999 21999 ?       Sl   08:33   0:00 /managed-agents/execute-command/ssm-session-worker ecs-execute-command-xxxxxxxxxxxxxxxxx
root        39  0.0  0.0  13999  3999 pts/0    Ss   08:33   0:00 /bin/sh
root        57  0.0  0.0  53999  3999 pts/0    R+   08:36   0:00 ps -aux

まだ設定変更していないので、メインのコンテナのプロセスは見えません。

次に、今回の機能を有効化します。タスク定義に"pidMode":"task"を足してタスク定義の新しいリビジョンを作成します。

{
    "family": "familyDummyName",
    "containerDefinitions": [
        {
            "name": "main",
            "image": "amazon/amazon-ecs-sample",
            ~(省略)~
        },
        {
            "name": "sleeper",
            "image": "amazon/amazon-ecs-sample",
            ~(省略)~
        }
    ],
    "taskRoleArn": "arn:aws:iam::123456789012:role/FargateSharedNamespaceTes-FargateSharedNamespaceTe-XXXXXXXXX",
    "executionRoleArn": "arn:aws:iam::123456789012:role/FargateSharedNamespaceTes-FargateSharedNamespaceTe-XXXXXX",
    "networkMode": "awsvpc",
    "requiresCompatibilities": [
        "FARGATE"
    ],
    "cpu": "512",
    "memory": "1024",
    "pidMode": "task"
}

上記のタスク定義でデプロイするようサービスを更新します。5分程度するとタスクが更新されます。

新しいコンテナにログインしてみます。

% aws ecs execute-command \
    --cluster "clusterName" \
    --task xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
    --container sleeper \
    --interactive \
    --command "/bin/bash"

bash-4.2% yum -y install procps
bash-4.2% ps -auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root        25  0.1  0.1 1246999 14999 ?       Ssl  08:57   0:00 /managed-agents/execute-command/amazon-ssm-agent
root        92  0.0  0.3 1332999 24999 ?       Sl   08:57   0:00  \_ /managed-agents/execute-command/ssm-agent-worker
root        19  0.1  0.1 1321999 14999 ?       Ssl  08:57   0:00 /managed-agents/execute-command/amazon-ssm-agent
root        93  0.0  0.3 1408999 26999 ?       Sl   08:57   0:00  \_ /managed-agents/execute-command/ssm-agent-worker
root       109  0.1  0.2 1326999 21999 ?       Sl   08:58   0:00      \_ /managed-agents/execute-command/ssm-session-worker ecs-execute-command-xxxxxxxxxxxxxxxx
root       119  0.0  0.0  13999  3999 pts/0    Ss   08:58   0:00          \_ /bin/bash
root       137  0.0  0.0  53999  3999 pts/0    R+   09:00   0:00              \_ ps -auxf
root        13  0.1  0.2 334999 19999 ?        Ss   08:57   0:00 /usr/sbin/apache2 -D FOREGROUND
apache      41  0.0  0.1 615999  9199 ?        Sl   08:57   0:00  \_ /usr/sbin/apache2 -D FOREGROUND
apache      46  0.0  0.1 385999  9199 ?        Sl   08:57   0:00  \_ /usr/sbin/apache2 -D FOREGROUND
apache      64  0.0  0.1 385999  9199 ?        Sl   08:57   0:00  \_ /usr/sbin/apache2 -D FOREGROUND
apache      67  0.0  0.1 385999  9199 ?        Sl   08:57   0:00  \_ /usr/sbin/apache2 -D FOREGROUND
apache      73  0.0  0.1 385999  9199 ?        Sl   08:57   0:00  \_ /usr/sbin/apache2 -D FOREGROUND
root         7  0.1  0.0   4999   799 ?        Ss   08:57   0:00 sleep infinity
root         1  0.1  0.0    991     6 ?        Ss   08:57   0:00 /pause

これでmainコンテナ側のプロセスも見えるようになりました!注意にある通り、PID 1はpauseというプロセスになるので、タスクの終了時は注意が必要になるかもしれないです。いくつか検証してみます。

サイドカーからmain側のコンテナのファイルを確認

後述しますが注意事項まで読むと、共有しているコンテナのファイルへのアクセスが可能と書いてあります。なので、別コンテナのファイルを覗いてみます。今回は、まずは先にmainコンテナ側でダミーファイルを作成します。その後サイドカーからmainコンテナのファイルを見るためPIDの13を覗いてみます。

# mainコンテナ側
% aws ecs execute-command \
    --cluster "clusterName" \
    --task xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx \
    --container main \
    --interactive \
    --command "/bin/bash"
bash-4.2% touch /tmp/hoge.txt

次にサイドカーからファイルを確認してみます。

# サイドカーコンテナ側
bash-4.2% ls -l /proc/13/root/tmp
total 0
-rw-r--r-- 1 root root 0 Aug 20 09:26 hoge.txt

上記より、名前空間の共有だけでなくファイルの閲覧権限なども共有されることがわかりました。ここはLinux上の権限に注意が必要そうです。

サイドカーからプロセスの停止

今度はサイドカーからプロセスの停止を試してみます。まずはmain側でsleepコマンドを動かして、その後にサイドカー側から該当のプロセスを停止してみます。

# mainコンテナ側
bash-4.2% sleep 100000
# サイドカーコンテナ側
bash-4.2% ps -auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root        25  0.1  0.1 1246999 14999 ?       Ssl  08:57   0:00 /managed-agents/execute-command/amazon-ssm-agent
root        92  0.0  0.3 1332999 24999 ?       Sl   08:57   0:00  \_ /managed-agents/execute-command/ssm-agent-worker
root       163  0.0  0.2 1326999 20999 ?       Sl   09:10   0:01      \_ /managed-agents/execute-command/ssm-session-worker ecs-execute-command-yyyyyyyyyyyyyyyyy
root       173  0.0  0.0  13999  3199 pts/0    Ss   09:10   0:00          \_ /bin/bash
root       245  0.0  0.0   6999   749 pts/0    S+   09:39   0:00              \_ sleep 100000

bash-4.2% kill -9 245
bash-4.2% ps -auxf
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root        25  0.1  0.1 1246999 14999 ?       Ssl  08:57   0:00 /managed-agents/execute-command/amazon-ssm-agent
root        92  0.0  0.3 1332999 24999 ?       Sl   08:57   0:00  \_ /managed-agents/execute-command/ssm-agent-worker
root       163  0.0  0.2 1326999 20999 ?       Sl   09:10   0:01      \_ /managed-agents/execute-command/ssm-session-worker ecs-execute-command-yyyyyyyyyyyyyyyyy
root       173  0.0  0.0  13999  3199 pts/0    Ss   09:10   0:00          \_ /bin/bash

プロセスの停止が可能なことが確認できました。今度は、公式記事にあるようにstraceを使いながら同じ操作を行ってみます。

# mainコンテナ側
bash-4.2% sleep 100000
# サイドカーコンテナ側
bash-4.2% yum install strace -y
bash-4.2% cd /tmp
bash-4.2% ps -auxf | grep "sleep 100000"
root       252  0.0  0.0   6164   776 pts/0    S+   09:43   0:00              \_ sleep 100000
root       264  0.0  0.0  10920   904 pts/0    S+   09:45   0:00              \_ grep sleep 100000
bash-4.2% strace -p 252 -o straceoutput.txt &
bash-4.2% kill -9 252
bash-4.2% cat straceoutput.txt 
restart_syscall(<... resuming interrupted read ...>) = ?
+++ killed by SIGKILL +++
[1]+  Done                    strace -p 252 -o straceoutput.txt

最後にmainコンテナのプロセスをトレースしながらサイドカーからkillしてみます。ここでkillしたタイミングでタスクごと落ちてタスク再起動となりました。

# サイドカーコンテナ側
bash-4.2% ps -auxf | grep "0:00 /usr/sbin/apache2 -D FOREGROUND"
root       276  0.0  0.0  10920   904 pts/0    S+   09:51   0:00              \_ grep 0:00 /usr/sbin/apache2 -D FOREGROUND
root        13  0.0  0.2 334999 19999 ?        Ss   08:57   0:00 /usr/sbin/apache2 -D FOREGROUND

bash-4.2% strace -p 13 -o main-straceoutput.txt &
bash-4.2% kill -9 13

注意点

元の記事にも記載はありますが、この機能を有効化する場合いくつか注意事項があります。例を省略した意訳を記載します。

  • サイドカーコンテナ内のプロセスから、アプリコンテナ内のプロセスを監視、停止、または再起動できる
  • サイドカーコンテナ内のプロセスは、アプリコンテナのファイルシステムを表示できます。ファイルシステムの保護は、Unix ファイルのアクセス許可を通じて行われます
  • ECSタスクでプロセスを共有する場合、新しいpauseプロセスはタスク全体の PID 1 として実行されます。
  • アプリコンテナ内で実行されているプロセスの完全なトレーサビリティを提供するために、ECSタスクにSYS_PTRACE Linux capabillityの追加が必要な場合もあります。

特にPIDの共有というタイトルだけだとファイルの共有までされるというイメージがなかったので、サイドカーからアプリのコンテナに見られたくないものはLinuxの権限に注意する必要があります。

所感

今回のPID共有およびファイル共有によって、サイドカーのコンテナから証跡を取得できる範囲はかなり増えたと感じます。ただサイドカーの権限がかなり強くなるので注意は必要です。自作で証跡の収集などは可能なようですが、速度の問題が起きないかやサイドカーが信頼できるコンテナであるかがより重要になりそうです。そうなると個人で証跡管理を実装するというよりは、商用製品がより作られやすくなることや既存の製品がより便利になることを期待したいと思えました。