Fargateから固定IPを使って外部サーバーへ接続する方法を調べてみた

2021.09.22

はじめに

データアナリティクス事業本部の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_definitionsportMappingsは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を使う方法もあります。