티스토리 뷰

Terraform, HCL(HashiCorp Configuration Language)을 이용해 AWS 바닥부터 구성해가고 있다.

 

공부를 제대로 시작하려면 HCL의 구성요소, 명령어, 사용법 등등을 알고 넘어가면 좋겠지만

 

내가 공부하는 스타일 상 너무 차근차근하면 시도조차 못해서 일단 시작해봤다.


안전한 production 환경을 만들기 위해서 가장 먼저 구축되어야하는건 VPC이다.

 

VPC에는 내부에 다양한 구성요소를 가지고 있는데 아래에 잘 설명해뒀다.

 

2024.04.16 - [개발/AWS] - 한 눈에 알아보는 AWS VPC(Virtual Private Cloud) 정리

 

내가 구축하려는 환경은 다음과 같다.

 

1. VPC

2. Private/Public Subnet 각각 2개

3. Private/Public Routing Table 

4. Private/Public Route Assosiation(서브넷과 라우팅 테이블 연결) 각각 2개

5. Internet Gateway

6. NAT Gateway

 

공식 홈페이지의 아래의 구조와 매우 유사하게 작업하려고 한다.

https://docs.aws.amazon.com/ko_kr/vpc/latest/userguide/vpc-example-private-subnets-nat.html

 

NAT는 하나만 사용할 예정

 

일단은 눈에 보이는 AWS 리소스를 기준으로 만들어 나갔다.

 

본 포스팅은 VPC를 terraform hcl로 정의하기 위해서 두 가지 방법을 차례대로 진행할 것이다.

 

첫 번째는 각각의 AWS 리소스들을 정의하는 방법이고 두 번째는 Module을 이용한 방법이다.

 

각각의 리소스 정의

1. 사용한 변수들

locals {
  service         = "testbed"
  vpc_name        = "testbed-vpc"
  stage           = "testbed"
  cidr            = "10.0.0.0/16"
  public_subnets  = ["10.0.0.0/22", "10.0.4.0/22"]
  private_subnets = ["10.0.16.0/20", "10.0.32.0/20"]
  azs             = ["ap-northeast-2a", "ap-northeast-2c"]
  cluster_name    = "eks-testbed" # eks까지 진행할 예정
}

각각 리소스 내부에서 지정할 수도 있으나 외부에서 지정해주면 관리하기 쉽다.

 

2. VPC

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/vpc

resource "aws_vpc" "testbed_vpc" {
  cidr_block           = local.cidr

  tags = {
    Name    = local.vpc_name
    Service = local.service
    Stage = local.stage
  }
}

cidr 블록에 대한 설명은 이 전 포스팅에서 간단하게 했고, 이 외에도 다른 옵션들이 있다.

 

대표적으로 DNS, IPv6와 ClassicLink가 있는데 따로 다루진 않겠다. 

(그냥 아무 옵션을 설정안해도 vpc는 사용가능하다)

 

그래도 DNS 설정은 따로 건드리지 않는게 좋다. 해당 VPC에 접근하지 못할 수도 있음

 

3. Subnet

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/subnet

resource "aws_subnet" "private_subnet" {
    count = length(local.private_subnets)
    
    vpc_id = aws_vpc.testbed_vpc.id
    cidr_block = local.private_subnets[count.index]
    
    availability_zone = local.azs[count.index]

    tags = {
      Name    = "${local.vpc_name}-private-${local.azs[count.index]}"
      Service = local.service
      Stage = local.stage
  }
}

resource "aws_subnet" "public_subnet" {
    count = length(local.public_subnets)
    
    vpc_id = aws_vpc.testbed_vpc.id
    cidr_block = local.public_subnets[count.index]

    availability_zone = local.azs[count.index]
    map_public_ip_on_launch = true
    tags = {
      Name    = "${local.vpc_name}-public-${local.azs[count.index]}"
      Service = local.service
      Stage = local.stage
  }

}

 

4. Routing Table

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table

resource "aws_route_table" "private_route_table" {
  vpc_id = aws_vpc.testbed_vpc.id

  route = []

  tags = {
    Name = "${local.vpc_name}-private"
    Service = local.service
    Stage = local.stage
  }
}

resource "aws_route_table" "public_route_table" {
  vpc_id = aws_vpc.testbed_vpc.id
  
  route = [
    {
      cidr_block                 = "0.0.0.0/0"
      gateway_id                 = "igw-08948eb0c998e01df"
      carrier_gateway_id         = null
      core_network_arn           = null
      destination_prefix_list_id = null
      egress_only_gateway_id     = null
      ipv6_cidr_block            = null
      local_gateway_id           = null
      nat_gateway_id             = null
      network_interface_id       = null
      transit_gateway_id         = null
      vpc_endpoint_id            = null
      vpc_peering_connection_id  = null
    }
  ]

  tags = {
    Name = "${local.vpc_name}-public"
    Service = local.service
    Stage = local.stage
  }
}

igw가 있는 public routing table에는 igw에 대한 설정도 필요하다.

 

cidr_block이 0.0.0.0/0 이라는 것은 인터넷의 모든 IP 주소를 의미한다. VPC 내에서 생성된 인스턴스가 인터넷에 연결되어야 할 경우, 0.0.0.0/0 경로를 통해 인터넷 게이트웨이(IGW)로 트래픽을 전송한다.

 

5. NAT Gateway

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/nat_gateway

resource "aws_eip" "nat_gateway" {
  domain   = "vpc"
  tags = { 
    Name = "${local.vpc_name}-natgw" 
    Service = local.service
    Stage = local.stage}
}

## 프라이빗 서브넷에서 인터넷 접속시 사용할 NAT 게이트웨이
resource "aws_nat_gateway" "this" {
  allocation_id = aws_eip.nat_gateway.id
  subnet_id     = aws_subnet.public_subnet[0].id # NAT 게이트웨이 자체는 퍼플릭 서브넷에 위치해야 합니다
  tags          = { Name = "${local.vpc_name}-natgw" }
}

 

NAT 게이트웨이는 고정 IP를 필요로 해서, Elasitc IP를 지정해준다.

그런데 NAT는 열어두기만해도 시간당 $0.1씩 지출되기 때문에, 만들어만 보고 바로 삭제했다.

 

6. Route table association

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/route_table_association

resource "aws_route_table_association" "public" {
  count = length(local.public_subnets)

  subnet_id      = aws_subnet.public_subnet[count.index].id
  route_table_id = aws_route_table.public_route_table.id
}

resource "aws_route_table_association" "private" {
  count = length(local.private_subnets)

  subnet_id      = aws_subnet.private_subnet[count.index].id
  route_table_id = aws_route_table.private_route_table.id
}

이전에도 설명했듯이 나는 이미 구축되어있던 VPC에서 AWS Resource를 import하는 방식으로 진행했었다.

 

그래서 사실 association까지는 필요없었다.

 

여기까지가 각각의 리소스를 정의해서 사용하는 방법이다.

 

Module을 사용하여 정의

그런데, 여기서만 봐도 AWS 리소스가 6개고, 세부적으로 정의되는 요소들도 있다보니 소스코드가 너무 길어지고 관리하기 불편하다.

 

그래서 Terraform에서는 세부 요소들을 자동 정의해주는 module이라는 기능을 제공한다.

https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest

 

locals는 그대로 사용했다.

module "vpc" {
  source = "terraform-aws-modules/vpc/aws"

  name = local.vpc_name
  cidr = local.cidr

  azs             = local.azs
  public_subnets  = local.public_subnets
  private_subnets = local.private_subnets

#   enable_nat_gateway     = true
#   one_nat_gateway_per_az = true
  single_nat_gateway   = true
  enable_dns_support   = true
  enable_dns_hostnames = false

  create_database_subnet_group = false

  igw_tags = {
    Name = "testbed-intertnet"
  }

  private_subnet_tags = {
    "kubernetes.io/role/internal-elb" = 1
    "karpenter.sh/discovery" : "ps-eks-${terraform.workspace}"
  }

  public_subnet_tags = {
    "kubernetes.io/role/elb" = 1
  }

  tags = "test"
}

 

module로 코드를 수정하고, terraform plan을 사용해보면 terraform init을 다시하라는 알림이 나온다.

 

terraform init을 해주면 .terraform 파일에 module/vpc가 생성된다.

 

 

기존 데이터와 충돌해 Conlict가 발생할 경우

다만, 여기서 문제가 생길 수 있다.

 

module을 정의하기 전에 AWS 리소스들을 각각 정의해둔 이력이 있다면 terraform init을 했을 때 conflict가 발생한다.

 

왜 conflict이 발생하냐면, module이 각각의 리소스의 이름(AWS에 지정하는 이름이 아니고, 테라폼에서 리소스를 할당받기 위해 지정하는 이름)을 고정적으로 가지고 있기 때문이다.

기존의 AWS의 리소스를 정의할 때 이름을 지정해줬기 때문에 이미 리소스가 할당된 이름이 존재해 재할당하지 못해 에러가 발생한다. (resource "aws_vpc" "testbed_vpc" 에서 testbed_vpc가 이름이다.)

 

영리하게 이름을 맞춰놨으면 상관없지만, 나와 같은 방식으로 진행했다면 리소스 이름을 재할당해야한다.

 

여기서 terraform state mv명령어를 사용하면 된다.

$ terraform state mv aws_vpc.testbed_vpc module.vpc.aws_vpc.this[0]
$ terraform state mv aws_internet_gateway.igw module.vpc.aws_internet_gateway.this[0]
$ terraform state mv aws_subnet.public_subnet[0] module.vpc.aws_subnet.public[0]
$ terraform state mv aws_subnet.public_subnet[1] module.vpc.aws_subnet.public[1]
$ terraform state mv aws_subnet.private_subnet[0] module.vpc.aws_subnet.private[0]
$ terraform state mv aws_subnet.private_subnet[1] module.vpc.aws_subnet.private[1]
$ terraform state mv aws_route_table.public_route_table module.vpc.aws_route_table.public[0]
$ terraform state mv aws_route_table_association.public[0] module.vpc.aws_route_table_association.public[0]
$ terraform state mv aws_route_table_association.public[1] module.vpc.aws_route_table_association.public[1]
$ terraform state mv aws_route_table.private_route_table module.vpc.aws_route_table.private[0]
$ terraform state mv aws_route_table_association.private[0] module.vpc.aws_route_table_association.private[0]
$ terraform state mv aws_route_table_association.private[1] module.vpc.aws_route_table_association.private[1]
$ terraform plan
$ terraform apply

 

그런데, mv를 하다가 잘못 설정할 경우도 있기 때문에 잘 할당 되었나 확인 필요하다.

이럴 때는 terraform state show 리소스_이름 명령어를 사용하고, 잘못 설정되어 있다면 삭제해주고 다시 impoort 합니다.

$ terraform state show module.vpc.aws_route.public_internet_gateway[0]
# module.vpc.aws_route.public_internet_gateway[0]:
resource "aws_route" "public_internet_gateway" {
    destination_cidr_block = "0.0.0.0/0"
    gateway_id             = "igw-08948eb0c998e01df"
    id                     = "r-rtb-0fe9198f51e81cff81080289494"
    origin                 = "CreateRoute"
    route_table_id         = "rtb-0fe9198f51e81cff8"
    state                  = "active"
}

# 아래는 삭제 후 다시 import 하는 예시 코드입니다.
$ terraform state rm module.vpc.aws_route.public_internet_gateway[0]
$ terraform import module.vpc.aws_internet_gateway[0] "igw_id"

 

이런 식으로 리소스들을 재배치 시켜주고 apply 해본 후 terraform state list로 리소스들이 잘 있나 확인해본다.

$ terraform state list
module.vpc.aws_default_network_acl.this[0]
module.vpc.aws_default_route_table.default[0]    
module.vpc.aws_default_security_group.this[0]    
module.vpc.aws_internet_gateway.this[0]
module.vpc.aws_route.public_internet_gateway[0]  
module.vpc.aws_route_table.private[0]
module.vpc.aws_route_table.public[0]
module.vpc.aws_route_table_association.private[0]
module.vpc.aws_route_table_association.private[1]
module.vpc.aws_route_table_association.public[0] 
module.vpc.aws_route_table_association.public[1] 
module.vpc.aws_subnet.private[0]
module.vpc.aws_subnet.private[1]
module.vpc.aws_subnet.public[0]
module.vpc.aws_subnet.public[1]
module.vpc.aws_vpc.this[0]

마지막으로 AWS 콘솔에 들어가서 데이터가 중복생성된게 없나 확인이 필요하니 잘 체크하자.

(추가 과금이 될 수도 있음!)

 

마치며

나는 미리 AWS 리소스들을 각각 작성한 후 모듈로 리팩토링하는 방식으로 진행했었다.

 

그리고 네이밍이 잘못되있는걸 파악 못하고 apply해버려 중복 리소스가 마구마구 생겨서 고생을 좀 했다.

 

이 포스팅을 보고 따라오시는 분들은 그런 실수를 안했으면 좋겠다.

 

그리고 VPC 모듈 외에도 VPC에서 다룰 수 있는 리소스들이 많아서 2편에서 다루겠다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
글 보관함