iT邦幫忙

2023 iThome 鐵人賽

DAY 23
0
DevOps

AWS ECS + Gitlab + Laravel + Terraform 從入門到摔坑系列 第 23

Day 23 把 EC2 instance 移到 private subnet

  • 分享至 

  • xImage
  •  

VPC 的 subnet 簡單分成兩種:

  • public subnet:有直接的 route 可以通到 internet gateway 的 subnet

  • private subnet:沒有直接 route 可以到達 internet gateway

到目前為止我們都只有 public subnet,無論是跑 container 的 EC2 instance 還是 Gitlab Runner,都是在 public subnet 上、有 public IP address、可以直接 access internet。一開始把 EC2 instance 放在 public subnet 可以讓網路環境比較單純、讓我們比較容易找問題,不會 container 開不起來要 debug 的時候光是連進 EC2 instance 就被網路搞得頭昏眼花。現在 ECS service 可以正常運作了,我們要把 container instance 們移到 private subnet,讓 EC2 instance 們更安全。(本日程式碼

建立 private subnet

用 terraform 建立一個新的 subnet,開在 AZ ap-northeast-1a

resource "aws_subnet" "private_1a" {
  assign_ipv6_address_on_creation                = false
  availability_zone                              = "${var.region}a"
  cidr_block                                     = "172.16.100.0/24"
  customer_owned_ipv4_pool                       = null
  enable_dns64                                   = false
  enable_resource_name_dns_a_record_on_launch    = false
  enable_resource_name_dns_aaaa_record_on_launch = false
  ipv6_cidr_block                                = null
  ipv6_native                                    = false
  map_public_ip_on_launch                        = false
  outpost_arn                                    = null
  private_dns_hostname_type_on_launch            = "ip-name"
  tags = {
    Name = "my-app-private-1a"
  }
  tags_all = {
    Name = "my-app-private-1a"
  }
  vpc_id = aws_vpc.vpc.id
}

cidr_block 不能跟現有 subnet 重疊。map_public_ip_on_launch 要是 false ,都在 private subnet 裡了怎麼會有 public IP 呢~XD

subnet 如果沒有 associate 到 route table 會使用預設的 route table,default route table 預設只有 vpc 網段的 route 也就是只在 vpc 網段內會通。雖然不明確把 subnet associate 到預設 route table 也剛好符合 private subnet 的需要,但我們還是明確的把 subnet associate 到預設的 route table:

resource "aws_route_table_association" "private_1a" {
  gateway_id     = null
  route_table_id = aws_default_route_table.main.id
  subnet_id      = aws_subnet.private_1a.id
}

注意 default route table 不能加上通往 internet gateway 的 route,不然新開的 subnet 就又變成 public subnet 囉~

在 private subnet 啟動 EC2 instance

原本我們 auto scaling group 會把 EC2 instance 啟動在 public subnet,現在要修改 aws_autoscaling_group.asgvpc_zone_identifier ,讓 EC2 instance 改開到 private subnet:

vpc_zone_identifier       = [aws_subnet.private_1a.id]

terraform apply 後把 auto scaling group 的 desired count 設為 1、啟動一台 EC2 instance,看 EC2 instance 會啟動在哪:

https://ithelp.ithome.com.tw/upload/images/20231003/201606710AaKMRDlLw.png

耶!EC2 instance 開在 private subnet 了!也沒有 public IP!

確認 ECS service 的運作

確認 ECS service 的運作前,先到 ECS cluster 的 infrastructure 檢查有沒有 container instance。噢喔~空空如也:

https://ithelp.ithome.com.tw/upload/images/20231003/20160671YiA0tlCtTk.png

記得前面我們是讓 EC2 instance 有 public IP 了才能 register 到 ECS cluster 嗎?

在 public subnet 的 EC2 instance 要使用 AWS service 時,會經過 public network——EC2 instance 用 AWS service 的 domain name 會得到 public IP,network traffic 就會到 internet 再到 AWS 的 service(有種多去外面繞一下的感覺)。

private subnet 的 resource 如 EC2 instance 要使用 AWS service 一樣拿到 public IP 的時候,因為 private subnet 跟 internet 沒有接通,EC2 instance 也就無法 access 到 AWS 的 service。在 EC2 instance 要跟 ECS cluster 註冊的過程中,當然需要 access ECS 相關的服務,原本在 public subnet 可以運作,搬到 private subnet 來無法運作,自然 ECS cluster 就沒有 container instance 了~

那要怎麼辦呢?其中一個方法是使用 VPC endpoint。

VPC Endpoint

VPC Endpoint 可以沒有 internet 連接的 VPC 中的 resource 存取 AWS service。VPC Endpoint 分成兩種:Interface Endpoint 跟 Gateway Endpoint。大部分的 AWS service 都支援 Interface Endpoint,而 Gateway Endpoint 目前只有 S3 跟 DynamoDB 有支援。

interface endpoint 就是個 ENI(elastic network interface),ENI 可以想成 VPC 內的虛擬網卡。用 interface endpoint 就是在 VPC 裡開一個 ENI 連到某個 AWS service,VPC 內 resource 就能透過這個 ENI 使用 AWS service。

實際操作前的提醒:interface endpoint 是要收費的,費用在這裡。gateway endpoint 則不收費。我們總共會建立 6 個 interface endpoint 以及 1 個 gateway endpoint。

先建立 EC2 instance 跟 vpn endpoint 會用到的 security group,允許所有在 vpc 內的 traffic 通過:

resource "aws_security_group" "allow_all_from_internal" {
  name                   = "allow-all-from-internal"
  name_prefix            = null
  description = "allow all traffic from vpc internal"
  vpc_id                 = aws_vpc.vpc.id
  egress = [
    {
      cidr_blocks      = ["0.0.0.0/0"]
      description      = ""
      from_port        = 0
      ipv6_cidr_blocks = []
      prefix_list_ids  = []
      protocol         = "-1"
      security_groups  = []
      self             = false
      to_port          = 0
    }
  ]
  ingress = [
    {
      cidr_blocks      = [aws_vpc.vpc.cidr_block]
      description      = "allow all traffic"
      protocol         = "-1"
      from_port        = 0
      to_port          = 0
      ipv6_cidr_blocks = []
      prefix_list_ids  = []
      security_groups  = []
      self             = false
    }
  ]
  revoke_rules_on_delete = null
  tags                   = {}
  tags_all               = {}
}

在 launch template 加上 security group(記得也要改 auto scaling group 使用的 launch template 版本):

vpc_security_group_ids               = [aws_security_group.allow_all_from_internal.id]

在有 container instance 開啟中的狀況下,ECS 相關的 vpc endpoint 建立有順序性,既然我們還是在實驗,先把 container instance 們關閉,建立完 vpc endpoint 再啟動,這樣比較簡單。

所有的 vpc endpoint resource 如下,總共有三個 ECS、兩個 ECR、一個 Cloudwatch 的 interface endpoint,以及一個 S3 的 gateway endpoint。

resource "aws_vpc_endpoint" "ecs_agent" {
  vpc_id            = aws_vpc.vpc.id
  service_name      = "com.amazonaws.${var.region}.ecs-agent"
  vpc_endpoint_type = "Interface"

  security_group_ids = [
    aws_security_group.allow_all_from_internal.id
  ]

  subnet_ids = [aws_subnet.private_1a.id]

  private_dns_enabled = true
}

resource "aws_vpc_endpoint" "ecs_telemetry" {
  vpc_id            = aws_vpc.vpc.id
  service_name      = "com.amazonaws.${var.region}.ecs-telemetry"
  vpc_endpoint_type = "Interface"

  security_group_ids = [
    aws_security_group.allow_all_from_internal.id
  ]

  subnet_ids = [aws_subnet.private_1a.id]

  private_dns_enabled = true
}

resource "aws_vpc_endpoint" "ecs" {
  vpc_id            = aws_vpc.vpc.id
  service_name      = "com.amazonaws.${var.region}.ecs"
  vpc_endpoint_type = "Interface"

  security_group_ids = [
    aws_security_group.allow_all_from_internal.id
  ]

  subnet_ids = [aws_subnet.private_1a.id]

  private_dns_enabled = true
}

resource "aws_vpc_endpoint" "ecr_api" {
  vpc_id            = aws_vpc.vpc.id
  service_name      = "com.amazonaws.${var.region}.ecr.api"
  vpc_endpoint_type = "Interface"

  security_group_ids = [
    aws_security_group.allow_all_from_internal.id
  ]

  subnet_ids = [aws_subnet.private_1a.id]

  private_dns_enabled = true
}

resource "aws_vpc_endpoint" "ecr_dkr" {
  vpc_id            = aws_vpc.vpc.id
  service_name      = "com.amazonaws.${var.region}.ecr.dkr"
  vpc_endpoint_type = "Interface"

  security_group_ids = [
    aws_security_group.allow_all_from_internal.id
  ]

  subnet_ids = [aws_subnet.private_1a.id]

  private_dns_enabled = true
}

resource "aws_vpc_endpoint" "cw_logs" {
  vpc_id            = aws_vpc.vpc.id
  service_name      = "com.amazonaws.${var.region}.logs"
  vpc_endpoint_type = "Interface"

  security_group_ids = [
    aws_security_group.allow_all_from_internal.id
  ]

  subnet_ids = [aws_subnet.private_1a.id]

  private_dns_enabled = true
}

resource "aws_vpc_endpoint" "s3" {
  vpc_id            = aws_vpc.vpc.id
  service_name      = "com.amazonaws.${var.region}.s3"
  vpc_endpoint_type = "Gateway"
  route_table_ids = [aws_default_route_table.main.id]
}

interface endpoint 針對每個想 access AWS service 的 AZ,要指定一個 subnet,AWS 就會在這些 subnet 內建立 ENI,這個 ENI 的 IP address 會在它所在 subnet 的 IP address 範圍內。像是想在 AZ ap-northeast-1a 內使用 AWS service,就要指定一個 ap-northeast-1a 的 subnet 給 interface endpoint,這邊我們都用 private subnet。

terraform apply 就遇到 error:

https://ithelp.ithome.com.tw/upload/images/20231003/20160671trhN9WNJV6.png

aws_vpc.vpcenable_dns_hostnames 改成 true 後 apply 成功。vpc endpoint 要等它的 status 變成 available 才能使用,大概幾分鐘會好。

gateway endpoint 要設定 route table,要選擇 private subnet 們 associate 的 route table。AWS 會在選擇的 route table 上增加一個通往 vpc endpoint 的 route:

https://ithelp.ithome.com.tw/upload/images/20231003/20160671kpM2TDlgBW.png

這個長得有點奇怪的 destination 是 AWS 管理的 prefix list,可以簡單理解成要通往 s3 的 traffic 的 destination 就是它。target 是我們建立的 s3 gateway endpoint。這個 route 的意思是要通往 s3 的 traffic 都要往 s3 gateway endpoint 送,也就是說 associate 到這個 route table 的 subnet 要通往 s3 的 traffic 會走 gateway endpoint。

vpc endpoint 都 available 後,我們再透過 auto scaling group 開台 EC2 instance 起來看看是否會正常 register 進 ECS cluster:

https://ithelp.ithome.com.tw/upload/images/20231003/20160671R5lLcv0k9M.png

歐耶~接下來要把 Laravel 的 database 連線資訊改一下,host 改成 db server 的 private IP,因為現在它所在的 EC2 instance 已經無法主動 access internet 了,繼續用 database 的 public IP 是會連不到的。改好後再測試一次從 Gitlab deploy ECS service,看有沒有成功,deploy 成功後一樣連 ALB 的 DNS name 檢查是否正常。理論上應該要一切正常才是~

架構圖

下面是我們現在的架構圖。在 AZ 1a 多了 private subnet,container instance 改成在 private subnet 啟動,最後建立 vpc endpoint 們連向需要的 AWS service 們。

https://ithelp.ithome.com.tw/upload/images/20231003/20160671v0zqyOZ4Br.png

用 terraform 移除部份 resource

如果你跟筆者一樣,不想在沒使用時間被收 vpc endpoint 的 $$,可以用 terraform destory 配合 -target 把它們 destroy 掉:

$ terraform destroy -target=aws_vpc_endpoint.cw_logs -target=aws_vpc_endpoint.ecr_api -target=aws_vpc_endpoint.ecr_dkr -target=aws_vpc_endpoint.ecs -target=aws_vpc_endpoint.ecs_agent -target=aws_vpc_endpoint.ecs_telemetry

-target 可以讓 terraform 只 apply 或 destroy 某些 resource~下次用的時候記得先 terraform apply 把 VPC endpoint 都建回來之後再開啟 ECS instance。

Reference


上一篇
Day 22 ECS logging
下一篇
Day 24 連進 private subnet 的 EC2 instance & container
系列文
AWS ECS + Gitlab + Laravel + Terraform 從入門到摔坑30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言