Terraform

Terraform은 IAC 도구 중 하나로, 클라우드 환경을 GUI가 아닌 Local에서 command 단위로 프로비저닝 할 수 있다는 장점이 있다. 

 

이번 4-2 캡스톤 프로젝트에서 AWS 환경을 구성하기 위해 사용하였다.

 

IAM User, Access key 생성

 

 

Local에서 aws 환경에 접근하고 리소스들을 생성할 권한을 얻을 수 있도록 미리 IAM User를 생성하고 Local 개발환경(여기서는 Vs code)에 미리 연결 시켜줄 필요가 있다.

 

aws sts get-caller-identity

 

AWS CLI를 구성하고 IAM User를 연결한 뒤 위 명령어를 실행하면 현재 연결되어있는 IAM User를 확인할 수 있다 위와 같은 결과가 나올 경우 정상적으로 연결이 된 것으로 Terraform을 사용할 준비가 된 것이다. 

 

리소스 모듈화

 

전체 폴더 구조이다. 가능한 모든 리소스들을 모듈화해서 사용하도록 노력하였다. 

유지보수를 편하게 하기 위함이다. 

 

Bastion

Bastion은 주로 AWS EC2 서버를 의미하며 관리용 서버로 사용된다. 우리 프로젝트에선 서비스들을 도커 컨테이너로 올릴 때 사용할 메인 서버로 사용한다.

 

보안 그룹 설정

EC2 내부에 접근할 서비스들의 Port들을 미리 설정해줄 수 있다. SSH, TCP, 여러 서비스 Port들을 설정한다.

 

resource "aws_security_group" "bastion_sg" {
  vpc_id = var.vpc_id

  # allow all outbound
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }
  # allow ssh
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # TCP port 80 for HTTP
  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # TCP port 443 for HTTPS
  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  # allow test nginx
  ingress {
    from_port   = 8000
    to_port     = 8000
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "test nginx"
  }

  # fastapi 8080
  ingress {
    from_port   = 8080
    to_port     = 8080
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "fast-api"
  }

  # react 3000
  ingress {
    from_port   = 3000
    to_port     = 3000
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
    description = "react"
  }

  tags = {
    Name = "${var.common_info.env}-${var.common_info.service_name}-bastion-sg"
  }
}

 

SSH Key 생성

ec2 서버 접근의 보안을 위해서 ssh key 또한 생성해준다. 

 

# Create RSA key of size 4096 bits
resource "tls_private_key" "bastion_key" {
  algorithm = "RSA"
  rsa_bits  = 4096
}

# # Create local file
# resource "local_file" "bastion_key" {
#   content  = tls_private_key.bastion_key.private_key_pem
#   filename = "./bastion_key.pem"
# }

# Create AWS key pair
resource "aws_key_pair" "bastion_key" {
  key_name   = "bastion_key"
  public_key = tls_private_key.bastion_key.public_key_openssh
}

 

 

bastion 생성

 

resource "aws_instance" "bastion" {
  ami           = "ami-0ee82191e264e07cc"  # Amazon Linux 2 AMI (region-specific) seoul
  instance_type = "t2.micro"
  subnet_id     = var.subnets_public_ids[0]
  key_name      = "bastion_key"  # SSH key

  vpc_security_group_ids = [aws_security_group.bastion_sg.id]

  iam_instance_profile = aws_iam_instance_profile.ec2_instance_profile.name

  user_data = <<-EOF
  #!/bin/bash
  sudo yum update -y
  sudo yum install -y docker
  sudo service docker start
  sudo usermod -a -G docker ec2-user

  sudo timedatectl set-timezone Asia/Seoul

  mkdir -p /home/ec2-user/.docker/cli-plugins
  curl -SL https://github.com/docker/compose/releases/download/v2.29.6/docker-compose-linux-x86_64 -o /home/ec2-user/.docker/cli-plugins/docker-compose
  chmod +x /home/ec2-user/.docker/cli-plugins/docker-compose

  sudo dd if=/dev/zero of=/swapfile bs=128M count=16
  sudo chmod 600 /swapfile
  sudo mkswap /swapfile
  sudo swapon /swapfile
  echo '/swapfile swap swap defaults 0 0' | sudo tee -a /etc/fstab
  EOF

  tags = {
    Name = "${var.common_info.env}-${var.common_info.service_name}-bastion-server"
  }
}

 

시기에 따라 AMI id는 바뀔 수 있기에 AWS GUI에서 실제로 확인을 하고 하드코딩해야 정상적으로 실행된다. 

 

AWS 프리티어 계정을 사용하기 위해서 t2.micro 인스턴스를 사용하였고, user_data를 미리 설정하여 도커를 설치하고, 시간을 동기화하고, OOM 문제를 예방하기 위해 스왑메모리를 사용한다. 

 

 

VPC

우리 프로젝트에선 한 개의 가용영역에 하나의 Public subenet만을 이용할 것이기에 Nat Gateway는 따로 생성하지 않았다.

 

DB를 사용하였더라면 Private Subnet과 함께 생성했겠지만 S3 Bucket만을 사용할 예정이다. 

 

Internet gateway

resource "aws_internet_gateway" "internet_gateway" {
 vpc_id = aws_vpc.vpc.id

 tags = {
    "Name" = "${var.common_info.service_name}-igw"
  }
}

 

 

Subnet

 

resource "aws_subnet" "subnets_public" {
  for_each = var.vpc_info.cidr_blocks_public

  vpc_id     = aws_vpc.vpc.id
  cidr_block = each.value.cidr_block
  availability_zone = each.value.availability_zone
  map_public_ip_on_launch = true
  tags = merge(
    {
      Name = "${var.common_info.env}-${each.value.subnet_name}"
    }
  )
}

# resource "aws_subnet" "subnets_private" {
#   for_each = var.vpc_info.cidr_blocks_private

#   vpc_id     = aws_vpc.vpc.id
#   cidr_block = each.value.cidr_block
#   availability_zone = each.value.availability_zone

#   tags = merge(
#     {
#       Name = "${var.common_info.env}-${each.value.subnet_name}"
#     }
#   )
# }

#resource "aws_subnet" "subnets_private_db" {
#  for_each = var.vpc_info.cidr_blocks_private_db

#  vpc_id     = aws_vpc.vpc.id
#  cidr_block = each.value.cidr_block
#  availability_zone = each.value.availability_zone

#  tags = merge(
#    {
#      Name = "${var.common_info.env}-${each.value.subnet_name}"
#    }
#  )
#}

# resource "aws_subnet" "subnets_private_db" {
#   vpc_id     = aws_vpc.vpc.id
#   cidr_block = var.vpc_info.cidr_blocks_private_db["private_db_a"].cidr_block
#   availability_zone = var.vpc_info.cidr_blocks_private_db["private_db_a"].availability_zone

#   tags = merge(
#     {
#       Name = "${var.common_info.env}-${var.vpc_info.cidr_blocks_private_db["private_db_a"].subnet_name}"
#     }
#   )
# }

 

 

Routing table

 

resource "aws_route_table" "route_table_public" {
 vpc_id = aws_vpc.vpc.id

 tags = merge(
   {
     Name    =  "${var.common_info.env}-${var.common_info.service_name}-public"
   }
 )
}

# resource "aws_route_table" "route_table_private" {
#  vpc_id = aws_vpc.vpc.id

#  for_each = var.vpc_info.cidr_blocks_private

#  tags = merge(
#    {
#      Name    = "${var.common_info.env}-${each.value.subnet_name}"
#    },
#    var.common_tags
#  )
# }

# resource "aws_route_table" "route_table_private_db" {
#  vpc_id = aws_vpc.vpc.id

#  tags = merge(
#    {
#      Name    = "${var.common_info.env}-${var.common_info.service_name}-private-db"
#    },
#    var.common_tags
#  )
# }

resource "aws_route" "routes_public" {
 route_table_id         = aws_route_table.route_table_public.id
 destination_cidr_block = "0.0.0.0/0"
 gateway_id             = aws_internet_gateway.internet_gateway.id
}

# resource "aws_route" "routes_private" {
#  count = length(var.vpc_info.cidr_blocks_private)

#  route_table_id = aws_route_table.route_table_private[keys(var.vpc_info.cidr_blocks_private)[count.index]].id
#  destination_cidr_block = "0.0.0.0/0"
#  nat_gateway_id = aws_nat_gateway.nat_gateway.id
# }


# resource "aws_route" "routes_private" {
#   for_each               = var.vpc_info.cidr_blocks_private

#   route_table_id         = aws_route_table.route_table_private[each.key].id
#   destination_cidr_block = "0.0.0.0/0"
  
#   nat_gateway_id         = lookup(var.vpc_info.private_to_public_map, each.key, null) != null ? aws_nat_gateway.nat_gateway[lookup(var.vpc_info.private_to_public_map, each.key)].id : null
# }

resource "aws_route_table_association" "route_table_association_public" {
 for_each = var.vpc_info.cidr_blocks_public
 
 subnet_id = aws_subnet.subnets_public[each.key].id
 route_table_id = aws_route_table.route_table_public.id
}

# resource "aws_route_table_association" "route_table_association_private" {
#  for_each = var.vpc_info.cidr_blocks_private
 
#  subnet_id = aws_subnet.subnets_private[each.key].id
#  route_table_id = aws_route_table.route_table_private[each.key].id
# }

#resource "aws_route_table_association" "route_table_association_private_db" {
# for_each = var.vpc_info.cidr_blocks_private_db

# subnet_id      = aws_subnet.subnets_private_db[each.key].id
# route_table_id = aws_route_table.route_table_private_db.id
#}

# resource "aws_route_table_association" "route_table_association_private_db" {
#  subnet_id      = aws_subnet.subnets_private_db.id
#  route_table_id = aws_route_table.route_table_private_db.id
# }

 

 

Vpc

 

resource "aws_vpc" "vpc" {
  cidr_block = var.vpc_info.cidr_block_vpc
  enable_dns_support = true
  enable_dns_hostnames = true

  tags = {
    "Name" = "${var.common_info.env}-${var.vpc_info.vpc_name}"
  }
}

 

Main.tf

모듈들을 main.tf에서 최종적으로 생성한다. Terraform cli는 이 Main.tf가 있는 폴더나 디렉토리에서 실행되어야 한다.

 

module "vpc" {
  source = "../../modules/vpc"

  common_info = local.common_info
  common_tags = local.common_tags
  vpc_info = local.vpc_info
}

module "bastion" {
  source = "../../modules/bastion"

  common_info = local.common_info
  common_tags = local.common_tags
  vpc_info = local.vpc_info
  vpc_id = module.vpc.vpc_id

  subnets_public_ids  = module.vpc.subnets_public_ids
}

 

 

Terraform apply 및 결과 확인

 

 

 

정상적으로 생성이 된 것을 확인 가능하다.

 

Terraform destroy

 

 

삭제된 것 확인 가능하다.

 

Bastion ssh 접근 확인

Mobaxterm을 사용해서 확인하였다.

 

 

정상적으로 접근이 되는 것을 확인 가능하다.