🎛️

ECS ServiceConnectで外部からアクセスできない内部用APIをAWS CDKで作る

2024/07/02に公開

ECSでアプリケーションを構築しているとき、システム内部から呼び出すAPI(RESTやGraphQLなど)を作ることがあります。


WebアプリからWebアプリに対してHTTPリクエストをしている図

このAPIは当然外部のインターネットからアクセスされる必要はありません。


外部のユーザが直接内部用のAPIにHTTPリクエストをできてしまっている図

そこで今回はECS Service Connectという機能を使って、外部からはアクセスできない&ECSサービスからしかアクセスができないように制御をするやり方を紹介します。アクセス制限する方法はいくつかありますが、その中でもECS Service Connectは比較的新しい機能であり設定も簡単なのでオススメです。

また、この記事ではAWS CDKで構築、サービスとタスクの定義&デプロイはecspressoを使用していますが、当然、手動構築でもTerraformでも同様にできます。

ECS Service Connectとは

ECS Service Connectは簡単に言うと、ECSサービス間を安全かつスケーラブルに通信できる機能です。雑に別の言い方をすると、特定のサービスに対して http://sample.local のような名前を付けてくれます。


Service Connectで実現する簡易構成図

どうやって名前解決をしているか

ECS Service Connectの名前解決は非常にシンプルで、各タスクの /etc/hosts に接続先のホストとIPアドレスが自動で追記されるだけです。

# cat /etc/hosts
127.255.0.1 internal-nginx.serviceconnectns
2600:f0f0:0:0:0:0:0:1 internal-nginx.serviceconnectns

しかしこれを理解していないとService Connectを設定したのに接続ができない…という状況にハマる可能性があります。

というのも、 /etc/hosts に書き込まれるタイミングはタスクを起動したときです。つまりタスク起動時に、Service Connectの設定が何もされていなければ書き込まれません。当然ですね。ただ、初期構築時にタスクのデプロイ順序を間違えると初回デプロイ後に接続ができないケースがあります。

基本は「内部用タスク」からデプロイし、その後「外部用タスク」をデプロイします。もし設定はあっているはずなのに接続ができない…というときには再デプロイをしてみることをオススメします。

ECS ServiceConnectを構築する

それでは実際にECS ServiceConnectを構築します。

  • ECSクラスタとセキュリティグループはAWS CDKで構築
  • ECSサービスとタスクはecspressoで構築

AWS CDKでECSクラスタを作る

すべてのコードを掲載すると非常に長くなるので、VPCとECSクラスタ、セキュリティグループだけ抜粋しています。すべてのソースコードはGithubに公開しているので必要であればそちらを参照してください。

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

    // VPC
    const vpc = new Vpc(this, "Vpc", { maxAzs: 2 });
    const subnetIdList = vpc.privateSubnets.map((obj) => obj.subnetId);

    // 外部用:Clientコンテナ用のセキュリティグループ
    const clientContainerSg = new ec2.SecurityGroup(this, "ClientContainerSg", {
      vpc,
    });

    // 内部用:Internalコンテナ用のセキュリティグループ
    const internalContainerSg = new ec2.SecurityGroup(
      this,
      "InternalContainerSg",
      { vpc },
    );
    // Clientコンテナからの接続は許可
    clientContainerSg.connections.allowTo(
      internalContainerSg,
      ec2.Port.tcp(80),
    );

    // ECSクラスタ
    new Cluster(this, "EcsCluster", {
      vpc,
      clusterName: "ServiceConnect",
      // Service Connectを使うためには名前空間の設定が必要
      defaultCloudMapNamespace: {
        name: "ServiceConnectNS",
      },
    });
  }
}

ecspressoでサービスとタスクをデプロイ

今回の例では外部用途内部用の2つのサービスをデプロイします。中身は単なるnginxです。Service Connectで外部用タスクから内部用タスクにHTTPリクエストが出来ることを確認するために execute-command を実行できるよう設定をしています。

内部用サービスとタスクの設定例

こちらも必要な設定のみ抜粋しています。

internal-ecs-service-def.json
{
  // 省略
  "serviceConnectConfiguration": {
    "enabled": true,
    "namespace": "ServiceConnectNS", // クラスタ時に指定した名前空間名
    "services": [
      {
        "clientAliases": [
          {
            "port": 80 // アプリケーションのポート番号
          }
        ],
        "portName": "internal-nginx" // これがホスト名になる
      }
    ]
  },
  "enableExecuteCommand": true
}
internal-ecs-task-def.json
{
  // 省略
  "containerDefinitions": [
    {
      "name": "internal-nginx",
      "image": "public.ecr.aws/nginx/nginx:latest",
      "portMappings": [
        {
          "name": "internal-nginx",
          "containerPort": 80,
          "hostPort": 80,
          "protocol": "tcp",
          "appProtocol": "http" // HTTPで通信したい場合はこの指定が必要
        }
      ]
    }
  ]
}

外部用サービスの設定例

接続するだけの外部用ではサービス設定だけService Connectを有効にする定義が必要です。タスクは通常のタスク定義で問題ありません。(動作確認用に enableExecuteCommand を有効にするのを忘れないでください)

client-ecs-service-def.json
{
  "serviceConnectConfiguration": {
    "enabled": true,
    "namespace": "ServiceConnectNS"
  }
}

以上の構成をすると、内部用タスクのネットワーク設定に以下のような情報が出てきます。

画像に記載されている「Service Connect サービス」というところに http://internal-nginx.serviceconnectns:80 というURLが表示されます。このURLで、外部用サービスから内部用サービスにリクエストをすることができます。 http://ポート名.名前空間名:ポート番号 という構成ですね。

動作確認をする

外部用サービスのnginxタスクに入ってみます。

aws ecs execute-command \
  --region ap-northeast-1 \
  --cluster ServiceConnect \
  --task arn:aws:ecs:ap-northeast-1:xxx:task/ServiceConnect/xxx \
  --container client-nginx \
  --interactive \
  --command "/bin/sh"

タスクに入ったら http://internal-nginx.serviceconnectns:80 に対してリクエストを実行します。

curl http://internal-nginx.serviceconnectns:80

これでリクエストが成功すれば設定は完璧です。もしうまくリクエストができない場合は /etc/hosts に記載があるかの確認や、プロトコルやポート番号の設定があっているかどうかを確認してみてください。

すべてのソースコード

AWS CDKとecspressoのすべてのソースコードは以下のGitHubリポジトリにあります。

ムーザルちゃんねる

Discussion