ECS ServiceConnectで外部からアクセスできない内部用APIをAWS CDKで作る
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
を実行できるよう設定をしています。
内部用サービスとタスクの設定例
こちらも必要な設定のみ抜粋しています。
{
// 省略
"serviceConnectConfiguration": {
"enabled": true,
"namespace": "ServiceConnectNS", // クラスタ時に指定した名前空間名
"services": [
{
"clientAliases": [
{
"port": 80 // アプリケーションのポート番号
}
],
"portName": "internal-nginx" // これがホスト名になる
}
]
},
"enableExecuteCommand": true
}
{
// 省略
"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
を有効にするのを忘れないでください)
{
"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