Amazon ECS上でPrivateLinkとTerraformを使用してプライベートコンテナにアクセスする
多くの企業がコンテナオーケストレーションサービス(ECSやEKSなど)を使用して、マイクロサービス環境をホストしています。これらのマイクロサービスは、他の顧客がアクセスする必要があるAPIを提供することがあります。
顧客もAWSを使用している場合、すべての通信をAWS内でプライベートに保つのが最善の解決策です。_顧客_のVPC内のサービスが、APIを提供する_プロバイダー_のVPCにホストされているコンテナと通信できるべきです。このデモでは、APIホストにECSを使用します。
私はAWSアカウントを1つ使用しますが、VPCは2つ使用します。1つはAPIプロバイダー用、もう1つは顧客(=API利用者)用です。実際にはそれぞれが別々のアカウントを持っていますが、このセットアップでは大きな違いはありません(後述)。もう1つ重要な点は、基本的なものにするために、APIをHTTP
経由で提供するということです。HTTPS
の使用が推奨されます。
APIプロバイダーは、Fargate(サーバレス)を使用してECS内でコンテナをホストし、その前にプライベートなアプリケーションロードバランサー(ALB)を使用しています。顧客のVPCとAPIプロバイダーのVPCの間のプライベート接続を可能にするために、AWS PrivateLinkをセットアップする必要があります。AWS PrivateLinkは、VPC間またはAWSサービス間のプライベート接続を提供することができます。PrivateLinkはアプリケーションロードバランサーと直接接続することはできません。ネットワークロードバランサー(NLB)が必要です。これにより、アーキテクチャはこのようになります:
このデモで使用するサンプルAPIは、タスクAPIで、すべてのタスクとid
に基づいた特定のタスクを印刷できます。
$ docker run -d -p 8080:8080 lvthillo/python-flask-api
$ curl localhost/api/tasks
[{"id": 1, "name": "task1", "description": "This is task 1"}, {"id": 2, "name": "task2", "description": "This is task 2"}, {"id": 3, "name": "task3", "description": "This is task 3"}]
$ curl localhost/api/task/2
{"id": 2, "name": "task2", "description": "This is task 2"}
インフラストラクチャをセットアップするために、私はTerraformを使用しています。コードは私のGitHubで利用可能です。2つのVPCを作成する必要があります。私はVPCを作成する基本的なモジュールを作成しました。これには2つのAZにわたる4つのサブネットが含まれており、NATゲートウェイとインターネットゲートウェイをデプロイし、ルートテーブルも適切に設定されています。
module "network" {
source = "./modules/network"
vpc_name = "vpc-1"
vpc_cidr = var.vpc
subnet_1a_public_cidr = var.pub_sub_1a
subnet_1b_public_cidr = var.pub_sub_1b
subnet_1a_private_cidr = var.priv_sub_1a
subnet_1b_private_cidr = var.priv_sub_1b
}
次に、APIプロバイダーのVPC内にECSクラスター、サービス、タスク定義をデプロイする必要があります。その前にALBがデプロイされます。
ecs
モジュールは2つのサンプルAPIコンテナをデプロイします。
module "ecs" {
source = "./modules/ecs"
ecs_subnets = module.network.private_subnets
ecs_container_name = "demo"
ecs_port = var.ecs_port # 我々は networkMode awsvpc を使用しているので、ホストとコンテナのポートは一致するべきです
ecs_task_def_name = "demo-task"
ecs_docker_image = "lvthillo/python-flask-api"
vpc_id = module.network.vpc_id
alb_sg = module.alb.alb_sg
alb_target_group_arn = module.alb.alb_tg_arn
}
module "alb" {
source = "./modules/alb"
alb_subnets = module.network.private_subnets
vpc_id = module.network.vpc_id
ecs_sg = module.ecs.ecs_sg
alb_port = var.alb_port
ecs_port = var.ecs_port
default_vpc_sg = module.network.default_vpc_sg
vpc_cidr = var.vpc # AWS PrivateLink経由でロードバランサーリスナーポートへのクライアントトラフィックを許可する
}
ECSサービスのセキュリティグループは、ALBからの接続のみを許可します。
resource "aws_security_group_rule" "ingress" {
type = "ingress"
description = "Allow ALB to ECS"
from_port = var.ecs_port
to_port = var.ecs_port
protocol = "tcp"
security_group_id = aws_security_group.ecs_sg.id
source_security_group_id = var.alb_sg
}
NLBにはセキュリティグループがないため、ALBのセキュリティグループで参照することはできません。ALBアクセスを制限する別の方法を見つける必要があります。
alb
モジュールは、ALBセキュリティグループのソースとしてAPIプロバイダーのVPCサブネットを使用します。これは、APIプロバイダーのVPC内のすべてがALBと通信できることを意味します。
本番環境では、NLBを個別のNLBサブネットにデプロイし、それらのサブネットCIDR範囲に制限することが有効です。
それでは、プライベートALBの前にこのプライベートNLBをデプロイしましょう。
module "nlb" {
source = "./modules/nlb"
nlb_subnets = module.network.private_subnets
vpc_id = module.network.vpc_id
alb_arn = module.alb.alb_arn
nlb_port = var.nlb_port
alb_listener_port = module.alb.alb_listener_port
}
ここで少し新しいAWSの魔法を使います。2021年9月以降、ALBをNLBのターゲットに登録することが可能になりました。
過去にはこれが不可能でした。私は、ALBのIPを監視し、必要に応じてNLBターゲットグループを更新するLambdaをデプロイしたTerraformモジュールを作成しました。もはやそれが必要なくなったことをとても嬉しく思います!
今では、NLBターゲットグループのtarget_type
としてalb
を定義することができます。
resource "aws_lb_target_group" "nlb_tg" {
name = var.nlb_tg_name
port = var.alb_listener_port
protocol = "TCP"
target_type = "alb"
vpc_id = var.vpc_id
}
次に、AWS PrivateLinkで動作するサービス(エンドポイントサービスとして参照)をデプロイすることができます。本番環境では、allowed_principals
を追加して、顧客のアカウントIDをホワイトリスト登録する必要があります。
resource "aws_vpc_endpoint_service" "privatelink" {
acceptance_required = false
network_load_balancer_arns = [module.nlb.nlb_arn]
#checkov:skip=CKV_AWS_123:For this demo I don't need to configure the VPC Endpoint Service for Manual Acceptance
}
APIプロバイダーの観点からすべての準備が整いました。VPC、ECSインフラ、ALB、NLB、およびPrivateLinkをデプロイしました。
顧客のVPCは同じnetwork
モジュールを使用してデプロイできます。顧客はPrivateLinkに接続するためのVPCエンドポイントが必要です。
VPCエンドポイントFQDNは、AWS PrivateLinkエンドポイントFQDNを指すようになります。
これにより、DNSエンドポイントがアクセスできるVPCエンドポイントサービスへの弾性ネットワークインタフェースが作成されます。
resource "aws_vpc_endpoint" "vpce" {
vpc_id = module.network_added.vpc_id
service_name = aws_vpc_endpoint_service.privatelink.service_name
vpc_endpoint_type = "Interface"
security_group_ids = [aws_security_group.vpce_sg.id]
subnet_ids = module.network_added.public_subnets
}
最後に、顧客VPC(公開サブネット)にEC2をデプロイして、統合をテストします。これには、このTerraformモジュールを使用しています。
variables.tf
のkey
変数を、お持ちのAWS SSHキーの名前に更新してください。
module "ec2_instance_added" {
source = "terraform-aws-modules/ec2-instance/aws"
version = "~> 3.0"
name = "test-instance-vpc-2"
associate_public_ip_address = true
ami = "ami-05cd35b907b4ffe77" # eu-west-1 specific
instance_type = "t2.micro"
key_name = var.key
vpc_security_group_ids = [aws_security_group.ssh_sg_added.id]
subnet_id = element(module.network_added.public_subnets, 0)
}
セットアップをテストするためにTerraformスタックをデプロイしてください。
$ git clone https://github.com/lvthillo/aws-ecs-privatelink.git
$ cd aws-ecs-privatelink
$ terraform init
$ terraform apply
Terraformのアウトプットをチェックし、顧客のVPCにデプロイされたEC2インスタンスにSSHで接続します。次に、VPCエンドポイントでcurl
を試してみてください。
$ ssh -i your-key.pem ec2-user@54.247.23.167
$ curl http://vpce-07715651ecce291f6-x6r0avfj.vpce-svc-0beea6830e4c68cd2.eu-west-1.vpce.amazonaws.com/api/tasks
[{"id": 1, "name": "task1", "description": "This is task 1"}, {"id": 2, "name": "task2", "description": "This is task 2"}, {"id": 3, "name": "task3", "description": "This is task 3"}]
$ curl http://vpce-07715651ecce291f6-x6r0avfj.vpce-svc-0beea6830e4c68cd2.eu-west-1.vpce.amazonaws.com/api/task/2
{"id": 2, "name": "task2", "description": "This is task 2"}
それで終わりです!顧客はAWS内にとどまりながら、PrivateLinkを使用してECSでホストされているAPIにアクセスすることができました。
いくつか重要な注意点がありますが、それについてはこの投稿の下部に記述されています。デモスタックをterraform destroy
を使って削除するのを忘れないでください!
お楽しみいただけましたか!
こちらの記事はdev.toの良い記事を日本人向けに翻訳しています。
https://dev.to/aws-builders/access-private-containers-on-amazon-ecs-using-privatelink-and-terraform-2odf