WebLog

terraformでプライベートサブネットにECSを作る

2023/08/05 19:40

はじめに

ECS をプライベートサブネットに置いて、LB をパブリックサブネットに置くような構成はよくあると思います。 その際、ECS はプライベートサブネットにいるので、そのままだと ECR へアクセスしたり、CW Logs にログを吐いたりできません。 そのときの対処法として、以下の 2 つのパターンがあります:

  1. パブリックサブネットに NAT Gateway を置き、NAT Gateway 経由でインターネットに接続し、AWS リソースにアクセスする
  2. VPC エンドポイント(VPCE)経由で VPC 外の AWS リソースにアクセスする

1 の場合のデメリットとして、シンプルに NAT Gateway が高いということがあります。 VPCE の場合はもう少し安めに済むのに加え、通信が AWS 上から出ないため、セキュリティ的にも優れてると言えます。

やり方料金セキュリティ設定の難易度
NAT Gateway を置く高い(月 10000 以上? × AZ 個数)インターネット経由で接続するため、VPCE よりは劣る簡単(public subnet に Nat Gateway を置き、private subnet のルートテーブルを変えるだけ)
VPCE を置くNAT Gateway よりは高くない(GW 型:無料、Interface 型:月 1500 円~)AWS 環境内で接続が完結するサービスごとに VPCE を立てる必要があり、ちょっと難しい?

詳しいコストについては以下の記事が参考になります:

AWSネットワーク料金についてまとめてみた(2020年1月) - 本日も乙
AWSネットワーク料金についてまとめてみた(2020年1月) - 本日も乙

AWSネットワーク料金についてまとめてみた(2020年1月) - 本日も乙

先日、このようなツイートをしたところ想像以上の反響がありました。せいぜい10いいねつけばいいかなって思っていたのですが、1,000いいね以上もついてかなり吃驚しています。 AWSのネットワーク・データ転送料金がひと目で分かる図を作った。Direct ConnectとGlobal Acceleratorは描ききれんかった・・・ pic.twitter.com/97RM8fxgbe— shu1 (@ohsawa0515) 2020年1月30日twitter.com ありがたいことにフィードバックをいただきましたし、この図だけでは説明できないことも多くあったので、図の修整と補足についてブログ記事にし…

やりたいこと

今回やりたいことは以下の通りです。

  • ECS をプライベートサブネットに立てる
  • ECS から ECR, CW Logs, S3, SSM などへの通信を VPCE 経由で行う
  • ECS Exec で稼働中のコンテナに接続する

構成図

以下のような構成をterraformで作ります。

構成図

作っていく

vpce について

ECS を private subnet に立てるためには最低限 3 つの vpce を立てる必要があり、オプションでさらに vpce が必要になります。

vpc エンドポイント種類いつ使うか必須か
com.amazonaws.(region).ecr.apiInterface 型Amazon ECR API への呼び出しに使用される。DescribeImages や CreateRepository などの API アクションは、このエンドポイントを通じて実行される。必須
com.amazonaws.(region).ecr.dkrInterface 型Docker Registry API に使用される。push や pull などの Docker クライアントコマンドでは、このエンドポイントが使用され、イメージマニフェストを取得したりする。必須
com.amazonaws.(region).s3Gateway 型ECR からイメージを pull するときイメージマニフェストを取得したあと、実際には S3 からイメージレイヤーを落としてくるため、その際にこのエンドポイントが使用される。必須
com.amazonaws.(region).logsInterface 型CloudWatch Logs にログを吐きたい場合必須でない
com.amazonaws.(region).secretsmanagerInterface 型Secrets Manager が使いたい場合必須でない
com.amazonaws.(region).ssmInterface 型SSM の parameter store が使いたい場合必須でない
com.amazonaws.(region).ssmmessagesInterface 型ECS Exec したい場合必須でない

詳しくは以下の記事が参考になります

Amazon ECR インターフェイス VPC エンドポイント (AWS PrivateLink) - Amazon ECRAmazon ECR インターフェイス VPC エンドポイント (AWS PrivateLink) - Amazon ECR
Amazon ECR インターフェイス VPC エンドポイント (AWS PrivateLink) - Amazon ECRAmazon ECR インターフェイス VPC エンドポイント (AWS PrivateLink) - Amazon ECR

Amazon ECR インターフェイス VPC エンドポイント (AWS PrivateLink) - Amazon ECRAmazon ECR インターフェイス VPC エンドポイント (AWS PrivateLink) - Amazon ECR

VPC エンドポイントを使用すると、インターネット、NAT デバイス、VPN 接続、または AWS Direct Connect 経由のアクセスを必要とせずに、VPC と Amazon ECR とをプライベートに接続できます。

コードの全体像

network/main.tf
1locals {
2 az = ["(az1)", "(az2)"]
3}
4
5resource "aws_vpc" "main" {
6 cidr_block = "10.0.0.0/16"
7 enable_dns_hostnames = true # パブリックIPアドレスを持つインスタンスにDNSホスト名を付与する
8 enable_dns_support = true # VPC内のインスタンスにDNSサーバーを使用する
9
10 tags = {
11 Name = "main"
12 }
13}
14
15resource "aws_internet_gateway" "main" {
16 vpc_id = aws_vpc.main.id
17
18 tags = {
19 Name = "main"
20 }
21}
22
23resource "aws_subnet" "publics" {
24 count = 2
25 vpc_id = aws_vpc.main.id
26 cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index)
27 availability_zone = local.az[count.index]
28
29 tags = {
30 Name = "public-${local.az[count.index]}"
31 }
32}
33
34resource "aws_subnet" "privates" {
35 count = 2
36 vpc_id = aws_vpc.main.id
37 cidr_block = cidrsubnet(aws_vpc.main.cidr_block, 8, count.index + 2)
38 availability_zone = local.az[count.index]
39
40 tags = {
41 Name = "private-${local.az[count.index]}"
42 }
43}
44
45resource "aws_route_table" "public" {
46 vpc_id = aws_vpc.main.id
47
48 route {
49 cidr_block = "0.0.0.0/0"
50 gateway_id = aws_internet_gateway.main.id
51 }
52}
53
54resource "aws_route_table_association" "public" {
55 count = 2
56 subnet_id = aws_subnet.publics[count.index].id
57 route_table_id = aws_route_table.public.id
58}
59
60resource "aws_route_table" "private" {
61 vpc_id = aws_vpc.main.id
62}
63
64# s3へのvpceはGateway型なので、route tableに追加する必要がある
65resource "aws_vpc_endpoint" "s3" {
66 vpc_id = aws_vpc.main.id
67 service_name = "com.amazonaws.(region).s3"
68 route_table_ids = [aws_route_table.private.id]
69
70 tags = {
71 Name = "s3_vpce"
72 }
73}
74
75resource "aws_route_table_association" "private" {
76 count = 2
77 subnet_id = aws_subnet.privates[count.index].id
78 route_table_id = aws_route_table.private.id
79}
80
81resource "aws_security_group" "vpce" {
82 name = "vpce"
83 description = "vpce"
84 vpc_id = aws_vpc.main.id
85
86 # HTTPSのみ許可すればいい
87 ingress {
88 from_port = 443
89 to_port = 443
90 protocol = "tcp"
91 cidr_blocks = [aws_vpc.main.cidr_block] # vpc内からのみアクセス可能
92 }
93
94 egress {
95 from_port = 0
96 to_port = 0
97 protocol = "-1"
98 cidr_blocks = ["0.0.0.0/0"]
99 }
100}
101
102resource "aws_vpc_endpoint" "ecr_dkr" {
103 vpc_id = aws_vpc.main.id
104 service_name = "com.amazonaws.(region).ecr.dkr"
105 vpc_endpoint_type = "Interface"
106 private_dns_enabled = true
107 subnet_ids = aws_subnet.privates[*].id
108 security_group_ids = [aws_security_group.vpce.id]
109
110 tags = {
111 Name = "ecr_dkr_vpce"
112 }
113}
114
115resource "aws_vpc_endpoint" "ecr_api" {
116 vpc_id = aws_vpc.main.id
117 service_name = "com.amazonaws.(region).ecr.api"
118 vpc_endpoint_type = "Interface"
119 private_dns_enabled = true
120 subnet_ids = aws_subnet.privates[*].id
121 security_group_ids = [aws_security_group.vpce.id]
122
123 tags = {
124 Name = "ecr_api_vpce"
125 }
126}
127
128resource "aws_vpc_endpoint" "cloudwatch_logs" {
129 vpc_id = aws_vpc.main.id
130 service_name = "com.amazonaws.(region).logs"
131 vpc_endpoint_type = "Interface"
132 private_dns_enabled = true
133 subnet_ids = aws_subnet.privates[*].id
134 security_group_ids = [aws_security_group.vpce.id]
135
136 tags = {
137 Name = "cloudwatch_logs_vpce"
138 }
139}
140
141resource "aws_vpc_endpoint" "ssmmessages" {
142 vpc_id = aws_vpc.main.id
143 service_name = "com.amazonaws.(region).ssmmessages"
144 vpc_endpoint_type = "Interface"
145 private_dns_enabled = true
146 subnet_ids = aws_subnet.privates[*].id
147 security_group_ids = [aws_security_group.vpce.id]
148
149 tags = {
150 Name = "ssmmessages_vpce"
151 }
152}
153
154resource "aws_vpc_endpoint" "ssm" {
155 vpc_id = aws_vpc.main.id
156 service_name = "com.amazonaws.(region).ssm"
157 vpc_endpoint_type = "Interface"
158 private_dns_enabled = true
159 subnet_ids = aws_subnet.privates[*].id
160 security_group_ids = [aws_security_group.vpce.id]
161
162 tags = {
163 Name = "ssm_vpce"
164 }
165}
166

ALB

alb/main.tf
1resource "aws_lb" "main" {
2 name = "main"
3 internal = false
4 subnets = var.public_subnet_ids
5 security_groups = [aws_security_group.alb.id]
6 load_balancer_type = "application"
7
8 tags = {
9 Name = "main"
10 }
11}
12
13resource "aws_security_group" "alb" {
14 name = "alb"
15 description = "alb"
16 vpc_id = var.vpc_id
17
18 ingress {
19 from_port = 80
20 to_port = 80
21 protocol = "tcp"
22 cidr_blocks = ["0.0.0.0/0"]
23 }
24
25 egress {
26 from_port = 0
27 to_port = 0
28 protocol = "-1"
29 cidr_blocks = ["0.0.0.0/0"]
30 }
31}
32
33resource "aws_lb_listener" "main" {
34 load_balancer_arn = aws_lb.main.arn
35 port = 80
36 protocol = "HTTP"
37
38 default_action {
39 type = "forward"
40 target_group_arn = aws_lb_target_group.main.arn
41 }
42}
43
44resource "aws_lb_target_group" "main" {
45 name = "main"
46 port = 80
47 protocol = "HTTP"
48 vpc_id = var.vpc_id
49 target_type = "ip"
50
51 health_check {
52 path = "/"
53 }
54}
55

ECS

ecs/main.tf
1resource "aws_iam_role" "nginx_task_role" {
2 name = "nginx_ecs_task_role"
3 assume_role_policy = jsonencode({
4 Version = "2012-10-17"
5 Statement = [
6 {
7 Action = "sts:AssumeRole"
8 Effect = "Allow"
9 Principal = {
10 Service = "ecs-tasks.amazonaws.com"
11 }
12 }
13 ]
14 })
15
16 # ECS TaskがSSMを使えるようにすることでECS Execできるようにする
17 inline_policy {
18 name = "ecs_exec"
19 policy = jsonencode({
20 Version = "2012-10-17"
21 Statement = [
22 {
23 Action = [
24 "ssmmessages:CreateControlChannel",
25 "ssmmessages:CreateDataChannel",
26 "ssmmessages:OpenControlChannel",
27 "ssmmessages:OpenDataChannel"
28 ]
29 Effect = "Allow"
30 Resource = "*"
31 }
32 ]
33 })
34 }
35}
36
37# ECSコンテナエージェントが使うIAMロール
38# ECSコンテナエージェントの役割は、ECRなどからのイメージのpullや、CloudWatch Logsへのログの書き込みなど
39resource "aws_iam_role" "nginx_execution_role" {
40 name = "nginx_ecs_execution_role"
41 assume_role_policy = jsonencode({
42 Version = "2012-10-17"
43 Statement = [
44 {
45 Action = "sts:AssumeRole"
46 Effect = "Allow"
47 Principal = {
48 Service = "ecs-tasks.amazonaws.com"
49 }
50 }
51 ]
52 })
53
54 managed_policy_arns = [
55 "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
56 ]
57}
58
59
60
61resource "aws_ecs_cluster" "main" {
62 name = "main"
63}
64
65resource "aws_ecs_task_definition" "nginx" {
66 family = "main"
67 network_mode = "awsvpc"
68 requires_compatibilities = ["FARGATE"]
69 cpu = "256"
70 memory = "512"
71 task_role_arn = aws_iam_role.nginx_task_role.arn
72 execution_role_arn = aws_iam_role.nginx_execution_role.arn
73
74 container_definitions = jsonencode(
75 [
76 {
77 "name" : "nginx",
78 "image" : "(ecrのuri)",
79 "essentials" : true,
80 "logConfiguration" : {
81 "logDriver" : "awslogs",
82 "options" : {
83 "awslogs-group" : aws_cloudwatch_log_group.ecs.name,
84 "awslogs-region" : "(region)",
85 "awslogs-stream-prefix" : "ecs"
86 }
87 },
88 "portMappings" : [
89 {
90 "containerPort" : 80,
91 "hostPort" : 80,
92 "protocol" : "tcp"
93 }
94 ],
95 }
96 ]
97 )
98}
99
100resource "aws_security_group" "ecs" {
101 name = "ecs"
102 description = "ecs"
103 vpc_id = var.vpc_id
104
105 ingress {
106 from_port = 80
107 to_port = 80
108 protocol = "tcp"
109 security_groups = [var.aws_lb_security_group_id]
110 }
111
112 egress {
113 from_port = 0
114 to_port = 0
115 protocol = "-1"
116 cidr_blocks = ["0.0.0.0/0"]
117 }
118}
119
120resource "aws_ecs_service" "main" {
121 name = "main"
122 cluster = aws_ecs_cluster.main.id
123 task_definition = aws_ecs_task_definition.nginx.arn
124 desired_count = 1
125 launch_type = "FARGATE"
126 enable_execute_command = true # ECS Execを有効化
127
128 deployment_controller {
129 type = "CODE_DEPLOY"
130 }
131
132 network_configuration {
133 assign_public_ip = false
134 subnets = var.private_subnet_ids
135 security_groups = [aws_security_group.ecs.id]
136 }
137
138 load_balancer {
139 target_group_arn = var.aws_lb_target_group_arn
140 container_name = "nginx"
141 container_port = 80
142 }
143
144}
145
146resource "aws_cloudwatch_log_group" "ecs" {
147 name = "/ecs/main"
148 retention_in_days = 7
149}
150

最新の投稿