この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
はじめに
データアナリティクス事業本部のkobayashiです。
AWS Fargateを使ってサービスを開発している際に実行しているタスクで外部のサーバーへ接続する必要がありました。ただ外部のサーバーの制限が「固定IPからのみの接続を許可」というものでしたのでFargateのAmazon ECSタスクを実行する際に固定IPアドレスを使う方法を調べたところAWS ナレッジセンターにそのものの記事がありましたので記事を参考にしてTerraformで記述してみました。
NAT ゲートウェイ経由での接続
「Fargate の Amazon ECS タスクに静的 IP アドレスまたは Elastic IP アドレスを使用する 」によると外向きのトラフィックで固定IPを使う場合はNAT ゲートウェイを作成して、NAT ゲートウェイ経由で外部サーバーへ接続します。これによりNAT ゲートウェイのElastic IP アドレスで外部サーバーへ接続することができ、要件を満たすことができます。 構成図を簡単に描くと以下のようになります。
Terafformで記述してみる
上記の構成をTerraformで記述してみます。 Terraformのファイル構成としてはリソース別に以下の様に分けました。
- vpc.tf
- ecs.tf
構築する環境はサブネットCIDRを172.20.0.0/16
で、そのうちPublicサブネットを172.20.1.0/24
、Privateサブネットを172.20.3.0/24
としています。そのPublicサブネットにNAT ゲートウェイを作成します。またPrivateサブネットにFargateタスクを配置するようにしています。
ただし、ECRからイメージを取得をNAT ゲートウェイ経由で行ってしまうとNAT ゲートウェイのデータ転送料金が大きくかかってしまうためVPC エンドポイント経由でECRからイメージを取得するようにしています。この部分に関しては弊社ブログに記事がありますのでそちらをご確認ください。
- vpc.tf
変数部分は適宜環境に合わせて読み替えてください
resource "aws_vpc" "main-vpc" {
cidr_block = "172.20.0.0/16"
enable_dns_hostnames = true
tags = {
Name = "${var.aws_project_name}-vpc"
}
}
# Subnet
resource "aws_subnet" "public_1a" {
cidr_block = "172.20.1.0/24"
availability_zone = "${var.aws_region}a"
vpc_id = aws_vpc.main-vpc.id
tags = {
Name = "${var.aws_project_name}-public_1a"
}
}
resource "aws_subnet" "private_1a" {
cidr_block = "172.20.3.0/24"
availability_zone = "${var.aws_region}a"
vpc_id = aws_vpc.main-vpc.id
tags = {
Name = "${var.aws_project_name}-private_1a"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main-igw" {
vpc_id = aws_vpc.main-vpc.id
tags = {
Name = "${var.aws_project_name}-igw"
}
}
# Route Table (Public)
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main-vpc.id
tags = {
Name = "${var.aws_project_name}-public"
}
}
# Route
resource "aws_route" "public" {
destination_cidr_block = "0.0.0.0/0"
route_table_id = aws_route_table.public.id
gateway_id = aws_internet_gateway.main-igw.id
}
# Association (Public)
resource "aws_route_table_association" "public_1a" {
subnet_id = aws_subnet.public_1a.id
route_table_id = aws_route_table.public.id
}
# Route Table (Private)
resource "aws_route_table" "private_1a" {
vpc_id = aws_vpc.main-vpc.id
tags = {
Name = "${var.aws_project_name}-private-1a"
}
}
# Association (Private)
resource "aws_route_table_association" "private_1a" {
subnet_id = aws_subnet.private_1a.id
route_table_id = aws_route_table.private_1a.id
}
# Elasti IP
resource "aws_eip" "nat_1a" {
vpc = true
tags = {
Name = "${var.aws_project_name}-natgw-eip-1a"
}
}
# NAT Gateway
resource "aws_nat_gateway" "nat_1a" {
subnet_id = aws_subnet.public_1a.id
allocation_id = aws_eip.nat_1a.id
tags = {
Name = "${var.aws_project_name}-natgw-1a"
}
}
# VPC Endpoint
resource "aws_vpc_endpoint" "s3" {
service_name = "com.amazonaws.${var.aws_region}.s3"
vpc_endpoint_type = "Gateway"
vpc_id = aws_vpc.main-vpc.id
route_table_ids = [
aws_route_table.private_1a.id,
]
tags = {
"Name" = "${var.aws_project_name}-s3"
}
}
resource "aws_vpc_endpoint" "ecr_api" {
service_name = "com.amazonaws.${var.aws_region}.ecr.api"
vpc_endpoint_type = "Interface"
vpc_id = aws_vpc.main-vpc.id
subnet_ids = [
aws_subnet.private_1a.id,
]
security_group_ids = [
aws_security_group.https.id,
]
private_dns_enabled = true
tags = {
"Name" = "${var.aws_project_name}-ecr-api"
}
}
resource "aws_vpc_endpoint" "ecr_dkr" {
service_name = "com.amazonaws.${var.aws_region}.ecr.dkr"
vpc_endpoint_type = "Interface"
vpc_id = aws_vpc.main-vpc.id
subnet_ids = [
aws_subnet.private_1a.id,
]
security_group_ids = [
aws_security_group.https.id,
]
private_dns_enabled = true
tags = {
"Name" = "${var.aws_project_name}-ecr-dkr"
}
}
resource "aws_vpc_endpoint" "logs" {
service_name = "com.amazonaws.${var.aws_region}.logs"
vpc_endpoint_type = "Interface"
vpc_id = aws_vpc.main-vpc.id
subnet_ids = [
aws_subnet.private_1a.id,
]
security_group_ids = [
aws_security_group.https.id,
]
private_dns_enabled = true
tags = {
"Name" = "${var.aws_project_name}-logs"
}
}
resource "aws_vpc_endpoint" "ssm" {
service_name = "com.amazonaws.${var.aws_region}.ssm"
vpc_endpoint_type = "Interface"
vpc_id = aws_vpc.main-vpc.id
subnet_ids = [
aws_subnet.private_1a.id,
]
security_group_ids = [
aws_security_group.https.id,
]
private_dns_enabled = true
tags = {
"Name" = "${var.aws_project_name}-ssm"
}
}
# SecurityGroup
resource "aws_security_group" "https" {
name = "https"
description = "https"
vpc_id = aws_vpc.main-vpc.id
egress {
cidr_blocks = [aws_vpc.main-vpc.cidr_block]
from_port = 443
protocol = "tcp"
to_port = 443
}
ingress {
cidr_blocks = [aws_vpc.main-vpc.cidr_block]
from_port = 443
protocol = "tcp"
to_port = 443
}
tags = {
Name = "https"
}
}
- ecs.tf
変数部分は適宜環境に合わせて読み替えてください。また 実行しているタスクはAPIサーバーをイメージしているのでcontainer_definitions
のportMappings
は80番ポートをマッピングしてあります。こちらも適宜読み替えてください。
# CloudWatch Logs
resource "aws_cloudwatch_log_group" "cluster_log_group" {
name = "/${var.aws_project_name}/ecs"
retention_in_days = 90
}
# Task Definition
resource "aws_ecs_task_definition" "main" {
family = var.aws_project_name
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
execution_role_arn = aws_iam_role.ecs_task_execution_role.arn
task_role_arn = aws_iam_role.ecs_task_execution_role.arn
cpu = "256"
memory = "512"
# Container Definitions
container_definitions = <<EOL
[
{
"name": "${var.aws_project_name}",
"image": "${var.aws_ecr_repo}",
"portMappings": [
{
"hostPort": 80,
"containerPort": 80
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-region": "${var.aws_region}",
"awslogs-group": "/${var.aws_project_name}/ecs",
"awslogs-stream-prefix": "api-server"
}
}
}
]
EOL
}
# task_execution role
data "aws_iam_policy_document" "assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ecs-tasks.amazonaws.com"]
}
}
}
resource "aws_iam_role" "ecs_task_execution_role" {
name = "${var.aws_project_name}-ecsTaskExecutionRole"
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
resource "aws_iam_role_policy_attachment" "amazon_ecs_task_execution_role_policy" {
role = aws_iam_role.ecs_task_execution_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# ECS Cluster
resource "aws_ecs_cluster" "main" {
name = var.aws_project_name
}
# ECS Service
resource "aws_ecs_service" "main" {
name = var.aws_project_name
cluster = aws_ecs_cluster.main.name
launch_type = "FARGATE"
desired_count = "1"
task_definition = aws_ecs_task_definition.main.arn
network_configuration {
subnets = [
aws_subnet.private_1a.id,
]
security_groups = [aws_security_group.ecs.id]
}
}
# SecurityGroup
resource "aws_security_group" "ecs" {
name = "${var.aws_project_name}-ecs"
description = "${var.aws_project_name} ecs"
vpc_id = aws_vpc.main-vpc.id
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.aws_project_name}-ecs"
}
}
まとめ
Fargateから外部サーバーへの接続で固定IPを使用できるようにNAT ゲートウェイを経由するようにしてみました。その際にすべてのトラフィックをNAT ゲートウェイにするとAWSリソースへの接続もNAT ゲートウェイになってしまいNATNAT ゲートウェイの料金が大きく増えてしまうのでAWSリソースへの接続はVPCエンドポイント経由になるようにしました。 なお今回はFargateを使う前提でしたのでNAT ゲートウェイを使いましたが、Fargate使わずにElasticIPを割り当てたEC2タイプのECSを使う方法もあります。